Skip to content

Commit 0d7a301

Browse files
committed
fix fetching environment branch policies
1 parent 3c88214 commit 0d7a301

7 files changed

Lines changed: 138 additions & 95 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,6 @@ serde_json = "1.0"
6868
serde-untagged = "0.1"
6969
sync-team = { path = "sync-team" }
7070
tempfile = "3.19.1"
71+
thiserror = "2.0.18"
7172
toml = "1.0"
7273
walkdir = "2.3.1"

sync-team/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ hyper-old-types.workspace = true
1414
serde_json.workspace = true
1515
secrecy.workspace = true
1616
indexmap.workspace = true
17+
thiserror.workspace = true
1718

1819
[dev-dependencies]
1920
indexmap.workspace = true

sync-team/src/github/api/mod.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,27 @@ use reqwest::{
1818
use secrecy::ExposeSecret;
1919
use serde::{Deserialize, de::DeserializeOwned};
2020
use std::fmt;
21+
use thiserror::Error;
2122
use tokens::GitHubTokens;
2223
use url::GitHubUrl;
2324

2425
pub(crate) use read::{GitHubApiRead, GithubRead};
2526
pub(crate) use write::GitHubWrite;
2627

28+
#[derive(Debug, Error)]
29+
pub(crate) enum RestPaginatedError {
30+
#[error("{method} request to '{url}' failed with status {status}")]
31+
Http {
32+
method: Method,
33+
url: String,
34+
status: StatusCode,
35+
#[source]
36+
source: anyhow::Error,
37+
},
38+
#[error(transparent)]
39+
Other(#[from] anyhow::Error),
40+
}
41+
2742
#[derive(Clone)]
2843
pub(crate) struct HttpClient {
2944
client: Client,
@@ -157,7 +172,12 @@ impl HttpClient {
157172
})
158173
}
159174

160-
fn rest_paginated<F, T>(&self, method: &Method, url: &GitHubUrl, mut f: F) -> anyhow::Result<()>
175+
fn rest_paginated<F, T>(
176+
&self,
177+
method: &Method,
178+
url: &GitHubUrl,
179+
mut f: F,
180+
) -> Result<(), RestPaginatedError>
161181
where
162182
F: FnMut(T) -> anyhow::Result<()>,
163183
T: DeserializeOwned,
@@ -167,12 +187,25 @@ impl HttpClient {
167187
let resp = self
168188
.req(method.clone(), &next_url)?
169189
.send()
170-
.with_context(|| format!("failed to send request to {}", next_url.url()))?
171-
.custom_error_for_status()?;
190+
.with_context(|| format!("failed to send request to {}", next_url.url()))?;
191+
192+
let status = resp.status();
193+
let resp =
194+
resp.custom_error_for_status()
195+
.map_err(|source| RestPaginatedError::Http {
196+
method: method.clone(),
197+
url: next_url.url().to_string(),
198+
status,
199+
source,
200+
})?;
172201

173202
// Extract the next page
174203
if let Some(links) = resp.headers().get(header::LINK) {
175-
let links: Link = links.to_str()?.parse()?;
204+
let links: Link = links
205+
.to_str()
206+
.context("failed to convert LINK header to string")?
207+
.parse()
208+
.context("failed to parse LINK header")?;
176209
for link in links.values() {
177210
if link
178211
.rel()
@@ -637,3 +670,15 @@ pub(crate) enum RulesetOp {
637670
CreateForRepo,
638671
UpdateRuleset(i64),
639672
}
673+
674+
#[derive(Debug, serde::Deserialize)]
675+
pub(crate) struct BranchPolicy {
676+
pub(crate) id: u64,
677+
pub(crate) name: String,
678+
#[serde(rename = "type", default = "default_branch_policy_type")]
679+
pattern_type: String,
680+
}
681+
682+
fn default_branch_policy_type() -> String {
683+
"branch".to_string()
684+
}

sync-team/src/github/api/read.rs

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
use crate::github::api::Ruleset;
1+
use crate::github::api::{BranchPolicy, Ruleset};
22
use crate::github::api::{
33
BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, Repo, RepoTeam,
4-
RepoUser, Team, TeamMember, TeamRole, team_node_id, url::GitHubUrl, user_node_id,
4+
RepoUser, RestPaginatedError, Team, TeamMember, TeamRole, team_node_id, url::GitHubUrl,
5+
user_node_id,
56
};
67
use anyhow::Context as _;
7-
use reqwest::Method;
8+
use reqwest::{Method, StatusCode};
89
use rust_team_data::v1::Environment;
910
use std::collections::{HashMap, HashSet};
1011

@@ -68,6 +69,13 @@ pub(crate) trait GithubRead {
6869
org: &str,
6970
repo: &str,
7071
) -> anyhow::Result<Vec<crate::github::api::Ruleset>>;
72+
73+
fn environment_branch_policies(
74+
&self,
75+
org: &str,
76+
repo: &str,
77+
environment: &str,
78+
) -> anyhow::Result<Vec<BranchPolicy>>;
7179
}
7280

7381
pub(crate) struct GitHubApiRead {
@@ -479,22 +487,6 @@ impl GithubRead for GitHubApiRead {
479487
environments: Vec<GitHubEnvironment>,
480488
}
481489

482-
#[derive(serde::Deserialize)]
483-
struct BranchPolicy {
484-
name: String,
485-
#[serde(rename = "type", default = "default_branch_type")]
486-
pattern_type: String,
487-
}
488-
489-
fn default_branch_type() -> String {
490-
"branch".to_string()
491-
}
492-
493-
#[derive(serde::Deserialize)]
494-
struct BranchPoliciesResponse {
495-
branch_policies: Vec<BranchPolicy>,
496-
}
497-
498490
let mut env_infos = Vec::new();
499491

500492
// Fetch all environments with their protection_rules metadata
@@ -522,23 +514,21 @@ impl GithubRead for GitHubApiRead {
522514
let (branches, tags) = if has_branch_policies {
523515
let mut branches = Vec::new();
524516
let mut tags = Vec::new();
525-
self.client.rest_paginated(
526-
&Method::GET,
527-
&GitHubUrl::repos(
528-
org,
529-
repo,
530-
&format!("environments/{}/deployment-branch-policies", env_info.name),
531-
)?,
532-
|resp: BranchPoliciesResponse| {
533-
for p in resp.branch_policies {
534-
match p.pattern_type.as_str() {
535-
"tag" => tags.push(p.name),
536-
_ => branches.push(p.name),
537-
}
538-
}
539-
Ok(())
540-
},
541-
)?;
517+
let policies =
518+
self.environment_branch_policies(org, repo, &env_info.name).with_context(
519+
|| {
520+
format!(
521+
"failed to load deployment branch policies for environment '{}' in '{org}/{repo}'",
522+
env_info.name
523+
)
524+
},
525+
)?;
526+
for p in policies {
527+
match p.pattern_type.as_str() {
528+
"tag" => tags.push(p.name),
529+
_ => branches.push(p.name),
530+
}
531+
}
542532
(branches, tags)
543533
} else {
544534
(Vec::new(), Vec::new())
@@ -570,4 +560,47 @@ impl GithubRead for GitHubApiRead {
570560

571561
Ok(rulesets)
572562
}
563+
564+
fn environment_branch_policies(
565+
&self,
566+
org: &str,
567+
repo: &str,
568+
environment: &str,
569+
) -> anyhow::Result<Vec<BranchPolicy>> {
570+
#[derive(serde::Deserialize)]
571+
struct BranchPoliciesResponse {
572+
branch_policies: Vec<BranchPolicy>,
573+
}
574+
575+
let mut policies = Vec::new();
576+
let url = GitHubUrl::repos(
577+
org,
578+
repo,
579+
&format!("environments/{environment}/deployment-branch-policies"),
580+
)?;
581+
582+
if let Err(err) =
583+
self.client
584+
.rest_paginated(&Method::GET, &url, |resp: BranchPoliciesResponse| {
585+
policies.extend(resp.branch_policies);
586+
Ok(())
587+
})
588+
{
589+
match err {
590+
RestPaginatedError::Http {
591+
status: StatusCode::NOT_FOUND,
592+
..
593+
} => return Ok(policies),
594+
other => {
595+
return Err(anyhow::Error::from(other)).with_context(|| {
596+
format!(
597+
"failed to fetch deployment branch policies for environment '{environment}' in '{org}/{repo}'"
598+
)
599+
});
600+
}
601+
}
602+
}
603+
604+
Ok(policies)
605+
}
573606
}

sync-team/src/github/api/write.rs

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,12 @@ use std::collections::HashSet;
44

55
use crate::github::api::url::GitHubUrl;
66
use crate::github::api::{
7-
AppPushAllowanceActor, BranchProtection, BranchProtectionOp, HttpClient, Login,
8-
PushAllowanceActor, Repo, RepoPermission, RepoSettings, Team, TeamPrivacy,
7+
AppPushAllowanceActor, BranchProtection, BranchProtectionOp, GitHubApiRead, GithubRead,
8+
HttpClient, Login, PushAllowanceActor, Repo, RepoPermission, RepoSettings, Team, TeamPrivacy,
99
TeamPushAllowanceActor, TeamRole, UserPushAllowanceActor, allow_not_found,
1010
};
1111
use crate::utils::ResponseExt;
1212

13-
#[derive(Debug)]
14-
struct BranchPolicyInfo {
15-
id: u64,
16-
name: String,
17-
pattern_type: String,
18-
}
19-
2013
pub(crate) struct GitHubWrite {
2114
client: HttpClient,
2215
dry_run: bool,
@@ -570,50 +563,6 @@ impl GitHubWrite {
570563
Ok(())
571564
}
572565

573-
/// Get existing branch policies for an environment
574-
fn get_environment_branch_policies(
575-
&self,
576-
org: &str,
577-
repo: &str,
578-
environment: &str,
579-
) -> anyhow::Result<Vec<BranchPolicyInfo>> {
580-
#[derive(serde::Deserialize)]
581-
struct BranchPolicy {
582-
id: u64,
583-
name: String,
584-
#[serde(rename = "type", default = "default_branch_type")]
585-
pattern_type: String,
586-
}
587-
588-
fn default_branch_type() -> String {
589-
"branch".to_string()
590-
}
591-
592-
#[derive(serde::Deserialize)]
593-
struct BranchPoliciesResponse {
594-
branch_policies: Vec<BranchPolicy>,
595-
}
596-
597-
let url = GitHubUrl::repos(
598-
org,
599-
repo,
600-
&format!("environments/{}/deployment-branch-policies", environment),
601-
)?;
602-
603-
let response: BranchPoliciesResponse =
604-
self.client.req(Method::GET, &url)?.send()?.json()?;
605-
606-
Ok(response
607-
.branch_policies
608-
.into_iter()
609-
.map(|bp| BranchPolicyInfo {
610-
id: bp.id,
611-
name: bp.name,
612-
pattern_type: bp.pattern_type,
613-
})
614-
.collect())
615-
}
616-
617566
/// Delete a specific branch policy by ID
618567
fn delete_environment_branch_policy(
619568
&self,
@@ -649,7 +598,8 @@ impl GitHubWrite {
649598
tags: &[String],
650599
) -> anyhow::Result<()> {
651600
// 1. Fetch existing policies
652-
let existing_policies = self.get_environment_branch_policies(org, repo, environment)?;
601+
let existing_policies = GitHubApiRead::from_client(self.client.clone())?
602+
.environment_branch_policies(org, repo, environment)?;
653603

654604
#[derive(Hash, Eq, PartialEq)]
655605
struct PatternKey {

sync-team/src/github/tests/test_utils.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use rust_team_data::v1::{
1010

1111
use crate::Config;
1212
use crate::github::api::{
13-
BranchProtection, GithubRead, Repo, RepoTeam, RepoUser, Team, TeamMember, TeamPrivacy, TeamRole,
13+
BranchPolicy, BranchProtection, GithubRead, Repo, RepoTeam, RepoUser, Team, TeamMember,
14+
TeamPrivacy, TeamRole,
1415
};
1516
use crate::github::{
1617
OrgMembershipDiff, RepoDiff, SyncGitHub, TeamDiff, api, construct_branch_protection,
@@ -648,6 +649,17 @@ impl GithubRead for GithubMock {
648649
.cloned()
649650
.unwrap_or_default())
650651
}
652+
653+
fn environment_branch_policies(
654+
&self,
655+
_org: &str,
656+
_repo: &str,
657+
_environment: &str,
658+
) -> anyhow::Result<Vec<BranchPolicy>> {
659+
unimplemented!(
660+
"call the function repo_environments instead, and read branch policies from there"
661+
)
662+
}
651663
}
652664

653665
#[derive(Default)]

0 commit comments

Comments
 (0)