From 8976984d5874b51424464fba04c5c8db906d93b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 16 Feb 2026 14:00:04 +0100 Subject: [PATCH 1/8] Re-add functionality to sync GitHub apps --- src/sync/github/api/mod.rs | 14 ++ src/sync/github/api/read.rs | 72 ++++++++- src/sync/github/api/tokens.rs | 4 + src/sync/github/api/write.rs | 53 +++++++ src/sync/github/mod.rs | 219 +++++++++++++++++++++++++++- src/sync/github/tests/mod.rs | 33 +++++ src/sync/github/tests/test_utils.rs | 13 ++ 7 files changed, 404 insertions(+), 4 deletions(-) diff --git a/src/sync/github/api/mod.rs b/src/sync/github/api/mod.rs index e29ec10b9..4ba40c479 100644 --- a/src/sync/github/api/mod.rs +++ b/src/sync/github/api/mod.rs @@ -360,9 +360,23 @@ impl fmt::Display for RepoPermission { } } +#[derive(serde::Deserialize, Debug)] +pub(crate) struct OrgAppInstallation { + #[serde(rename = "id")] + pub(crate) installation_id: u64, + pub(crate) app_id: u64, +} + +#[derive(serde::Deserialize, Debug)] +pub(crate) struct RepoAppInstallation { + pub(crate) name: String, +} + #[derive(serde::Deserialize, Debug, Clone)] pub(crate) struct Repo { pub(crate) node_id: String, + #[serde(rename = "id")] + pub(crate) repo_id: u64, pub(crate) name: String, #[serde(alias = "owner", deserialize_with = "repo_owner")] pub(crate) org: String, diff --git a/src/sync/github/api/read.rs b/src/sync/github/api/read.rs index e87bf153c..25ed05846 100644 --- a/src/sync/github/api/read.rs +++ b/src/sync/github/api/read.rs @@ -1,9 +1,9 @@ use crate::sync::github::api; use crate::sync::github::api::{BranchPolicy, Ruleset}; use crate::sync::github::api::{ - BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, Repo, RepoTeam, - RepoUser, RestPaginatedError, Team, TeamMember, TeamRole, team_node_id, url::GitHubUrl, - user_node_id, + BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, OrgAppInstallation, + Repo, RepoAppInstallation, RepoTeam, RepoUser, RestPaginatedError, Team, TeamMember, TeamRole, + team_node_id, url::GitHubUrl, user_node_id, }; use crate::sync::utils::ResponseExt; use anyhow::Context as _; @@ -25,6 +25,16 @@ pub(crate) trait GithubRead { /// Get the members of an org async fn org_members(&self, org: &str) -> anyhow::Result>; + /// Get the app installations of an org + async fn org_app_installations(&self, org: &str) -> anyhow::Result>; + + /// Get the repositories enabled for an app installation. + async fn app_installation_repos( + &self, + installation_id: u64, + org: &str, + ) -> anyhow::Result>; + /// Get all teams associated with a org /// /// Returns a list of tuples of team name and slug @@ -183,6 +193,59 @@ impl GithubRead for GitHubApiRead { Ok(members) } + async fn org_app_installations(&self, org: &str) -> anyhow::Result> { + #[derive(serde::Deserialize, Debug)] + struct InstallationPage { + installations: Vec, + } + + let mut installations = Vec::new(); + self.client + .rest_paginated( + &Method::GET, + &GitHubUrl::orgs(org, "installations")?, + |response: InstallationPage| { + installations.extend(response.installations); + Ok(()) + }, + ) + .await?; + Ok(installations) + } + + async fn app_installation_repos( + &self, + installation_id: u64, + org: &str, + ) -> anyhow::Result> { + #[derive(serde::Deserialize, Debug)] + struct InstallationPage { + repositories: Vec, + } + + let mut installations = Vec::new(); + let url = if self.client.github_tokens.is_pat() { + // we are using a PAT + format!("user/installations/{installation_id}/repositories") + } else { + // we are using a GitHub App + "installation/repositories".to_string() + }; + + self.client + .rest_paginated( + &Method::GET, + &GitHubUrl::new(&url, org), + |response: InstallationPage| { + installations.extend(response.repositories); + Ok(()) + }, + ) + .await + .with_context(|| format!("failed to send rest paginated request to {url}"))?; + Ok(installations) + } + async fn org_teams(&self, org: &str) -> anyhow::Result> { let mut teams = Vec::new(); @@ -330,6 +393,7 @@ impl GithubRead for GitHubApiRead { query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { id + databaseId autoMergeAllowed description homepageUrl @@ -350,6 +414,7 @@ impl GithubRead for GitHubApiRead { // Equivalent of `node_id` of the Rest API id: String, // Equivalent of `id` of the Rest API + database_id: u64, auto_merge_allowed: Option, description: Option, homepage_url: Option, @@ -371,6 +436,7 @@ impl GithubRead for GitHubApiRead { .with_context(|| format!("failed to retrieve repo `{org}/{repo}`"))?; let repo = result.and_then(|r| r.repository).map(|repo_response| Repo { + repo_id: repo_response.database_id, node_id: repo_response.id, name: repo.to_string(), description: repo_response.description.unwrap_or_default(), diff --git a/src/sync/github/api/tokens.rs b/src/sync/github/api/tokens.rs index fb3c5060a..6f268cace 100644 --- a/src/sync/github/api/tokens.rs +++ b/src/sync/github/api/tokens.rs @@ -46,6 +46,10 @@ impl GitHubTokens { GitHubTokens::Pat(pat) => Ok(pat), } } + + pub fn is_pat(&self) -> bool { + matches!(self, GitHubTokens::Pat(_)) + } } fn org_name_from_env_var(env_var: &str) -> Option { diff --git a/src/sync/github/api/write.rs b/src/sync/github/api/write.rs index 9a96b8b5b..7917955aa 100644 --- a/src/sync/github/api/write.rs +++ b/src/sync/github/api/write.rs @@ -245,6 +245,7 @@ impl GitHubWrite { if self.dry_run { Ok(Repo { node_id: String::from("ID"), + repo_id: 0, name: name.to_string(), org: org.to_string(), description: settings.description.clone(), @@ -291,6 +292,58 @@ impl GitHubWrite { Ok(()) } + pub(crate) async fn add_repo_to_app_installation( + &self, + installation_id: u64, + repository_id: u64, + org: &str, + ) -> anyhow::Result<()> { + debug!("Adding repository {repository_id} to installation {installation_id}"); + if !self.dry_run { + self.client + .req( + Method::PUT, + &GitHubUrl::new( + &format!( + "user/installations/{installation_id}/repositories/{repository_id}" + ), + org, + ), + )? + .send() + .await? + .custom_error_for_status() + .await?; + } + Ok(()) + } + + pub(crate) async fn remove_repo_from_app_installation( + &self, + installation_id: u64, + repository_id: u64, + org: &str, + ) -> anyhow::Result<()> { + debug!("Removing repository {repository_id} from installation {installation_id}"); + if !self.dry_run { + self.client + .req( + Method::DELETE, + &GitHubUrl::new( + &format!( + "user/installations/{installation_id}/repositories/{repository_id}" + ), + org, + ), + )? + .send() + .await? + .custom_error_for_status() + .await?; + } + Ok(()) + } + /// Update a team's permissions to a repo pub(crate) async fn update_team_repo_permissions( &self, diff --git a/src/sync/github/mod.rs b/src/sync/github/mod.rs index 2f3db0d87..c2ab15eb8 100644 --- a/src/sync/github/mod.rs +++ b/src/sync/github/mod.rs @@ -13,7 +13,7 @@ use futures_util::StreamExt; use log::debug; use rust_team_data::v1::{Bot, BranchProtectionMode, MergeBot, ProtectionTarget}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::fmt::{Display, Write}; +use std::fmt::{Display, Formatter, Write}; static DEFAULT_DESCRIPTION: &str = "Managed by the rust-lang/team repository."; static DEFAULT_PRIVACY: TeamPrivacy = TeamPrivacy::Closed; @@ -38,6 +38,48 @@ pub(crate) async fn create_diff( } type OrgName = String; +type RepoName = String; + +#[derive(Copy, Clone, Debug, PartialEq)] +enum GithubApp { + RenovateBot, + /// New Rust implementation of Bors + Bors, +} + +impl GithubApp { + /// You can find the GitHub app ID e.g. through `gh api apps/` or through the + /// app settings page (if we own the app). + fn from_id(app_id: u64) -> Option { + match app_id { + 2740 => Some(GithubApp::RenovateBot), + 278306 => Some(GithubApp::Bors), + _ => None, + } + } +} + +impl Display for GithubApp { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + GithubApp::RenovateBot => f.write_str("RenovateBot"), + GithubApp::Bors => f.write_str("Bors"), + } + } +} + +#[derive(Clone, Debug)] +struct OrgAppInstallation { + app: GithubApp, + installation_id: u64, + repositories: HashSet, +} + +#[derive(Clone, Debug, PartialEq)] +struct AppInstallation { + app: GithubApp, + installation_id: u64, +} struct SyncGitHub { github: Box, @@ -47,6 +89,7 @@ struct SyncGitHub { usernames_cache: HashMap, org_owners: HashMap>, org_members: HashMap>, + org_apps: HashMap>, } impl SyncGitHub { @@ -78,10 +121,30 @@ impl SyncGitHub { let mut org_owners = HashMap::new(); let mut org_members = HashMap::new(); + let mut org_apps = HashMap::new(); for org in &orgs { org_owners.insert((*org).to_string(), github.org_owners(org).await?); org_members.insert((*org).to_string(), github.org_members(org).await?); + + let mut installations: Vec = vec![]; + for installation in github.org_app_installations(org).await? { + if let Some(app) = GithubApp::from_id(installation.app_id) { + let mut repositories = HashSet::new(); + for repo_installation in github + .app_installation_repos(installation.installation_id, org) + .await? + { + repositories.insert(repo_installation.name); + } + installations.push(OrgAppInstallation { + app, + installation_id: installation.installation_id, + repositories, + }); + } + } + org_apps.insert(org.to_string(), installations); } Ok(SyncGitHub { @@ -92,6 +155,7 @@ impl SyncGitHub { usernames_cache, org_owners, org_members, + org_apps, }) } @@ -431,6 +495,7 @@ impl SyncGitHub { .iter() .map(|(name, env)| (name.clone(), env.clone())) .collect(), + app_installations: self.diff_app_installations(expected_repo, &[])?, })); } }; @@ -469,15 +534,41 @@ impl SyncGitHub { auto_merge_enabled: expected_repo.auto_merge_enabled, }; + let existing_installations = self + .org_apps + .get(&expected_repo.org) + .map(|installations| { + installations + .iter() + .filter_map(|installation| { + // Only load installations from apps that we know about, to avoid removing + // unknown installations. + if installation.repositories.contains(&actual_repo.name) { + Some(AppInstallation { + app: installation.app, + installation_id: installation.installation_id, + }) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default(); + let app_installation_diffs = + self.diff_app_installations(expected_repo, &existing_installations)?; + Ok(RepoDiff::Update(UpdateRepoDiff { org: expected_repo.org.clone(), name: actual_repo.name, repo_node_id: actual_repo.node_id, + repo_id: actual_repo.repo_id, settings_diff: (old_settings, new_settings), permission_diffs, branch_protection_diffs, ruleset_diffs, environment_diffs, + app_installation_diffs, })) } @@ -772,6 +863,64 @@ impl SyncGitHub { Ok(ruleset_diffs) } + fn diff_app_installations( + &self, + expected_repo: &rust_team_data::v1::Repo, + existing_installations: &[AppInstallation], + ) -> anyhow::Result> { + let mut diff = vec![]; + let mut found_apps = Vec::new(); + + // Find apps that should be enabled on the repository + for app in expected_repo.bots.iter().filter_map(|bot| match bot { + Bot::Renovate => Some(GithubApp::RenovateBot), + Bot::Bors => Some(GithubApp::Bors), + Bot::Highfive + | Bot::Rfcbot + | Bot::RustTimer + | Bot::Rustbot + | Bot::Craterbot + | Bot::Glacierbot + | Bot::LogAnalyzer + | Bot::HerokuDeployAccess => None, + }) { + // Find installation ID of this app on GitHub + let gh_installation = self + .org_apps + .get(&expected_repo.org) + .and_then(|installations| { + installations + .iter() + .find(|installation| installation.app == app) + .map(|i| i.installation_id) + }); + let Some(gh_installation) = gh_installation else { + log::warn!( + "Application {app} should be enabled for repository {}/{}, but it is not installed on GitHub", + expected_repo.org, + expected_repo.name + ); + continue; + }; + let installation = AppInstallation { + app, + installation_id: gh_installation, + }; + found_apps.push(installation.clone()); + + if !existing_installations.contains(&installation) { + diff.push(AppInstallationDiff::Add(installation)); + } + } + for existing in existing_installations { + if !found_apps.contains(existing) { + diff.push(AppInstallationDiff::Remove(existing.clone())); + } + } + + Ok(diff) + } + fn expected_role(&self, org: &str, user: u64) -> TeamRole { if let Some(true) = self .org_owners @@ -1269,6 +1418,7 @@ struct CreateRepoDiff { branch_protections: Vec<(String, api::BranchProtection)>, rulesets: Vec, environments: Vec<(String, rust_team_data::v1::Environment)>, + app_installations: Vec, } impl CreateRepoDiff { @@ -1306,6 +1456,10 @@ impl CreateRepoDiff { .await?; } + for installation in &self.app_installations { + installation.apply(sync, repo.repo_id, &self.org).await?; + } + Ok(()) } } @@ -1320,6 +1474,7 @@ impl std::fmt::Display for CreateRepoDiff { branch_protections, rulesets, environments, + app_installations, } = self; let RepoSettings { @@ -1368,6 +1523,12 @@ impl std::fmt::Display for CreateRepoDiff { } } } + + writeln!(f, " App Installations:")?; + for diff in app_installations { + write!(f, "{diff}")?; + } + Ok(()) } } @@ -1377,12 +1538,14 @@ struct UpdateRepoDiff { org: String, name: String, repo_node_id: String, + repo_id: u64, // old, new settings_diff: (RepoSettings, RepoSettings), permission_diffs: Vec, branch_protection_diffs: Vec, ruleset_diffs: Vec, environment_diffs: Vec, + app_installation_diffs: Vec, } #[derive(Debug)] @@ -1410,11 +1573,13 @@ impl UpdateRepoDiff { org: _, name: _, repo_node_id: _, + repo_id: _, settings_diff, permission_diffs, branch_protection_diffs, ruleset_diffs, environment_diffs, + app_installation_diffs, } = self; settings_diff.0 == settings_diff.1 @@ -1422,6 +1587,7 @@ impl UpdateRepoDiff { && branch_protection_diffs.is_empty() && ruleset_diffs.is_empty() && environment_diffs.is_empty() + && app_installation_diffs.is_empty() } fn can_be_modified(&self) -> bool { @@ -1490,6 +1656,12 @@ impl UpdateRepoDiff { .await?; } + for app_installation in &self.app_installation_diffs { + app_installation + .apply(sync, self.repo_id, &self.org) + .await?; + } + Ok(()) } } @@ -1504,11 +1676,13 @@ impl std::fmt::Display for UpdateRepoDiff { org, name, repo_node_id: _, + repo_id: _, settings_diff, permission_diffs, branch_protection_diffs, ruleset_diffs, environment_diffs, + app_installation_diffs, } = self; writeln!(f, "📝 Editing repo '{org}/{name}':")?; @@ -1614,6 +1788,14 @@ impl std::fmt::Display for UpdateRepoDiff { } } + if !app_installation_diffs.is_empty() { + writeln!(f, " App installation changes:")?; + + for diff in app_installation_diffs { + write!(f, "{diff}")?; + } + } + Ok(()) } } @@ -2488,3 +2670,38 @@ impl std::fmt::Display for DeleteTeamDiff { Ok(()) } } + +#[derive(Debug)] +enum AppInstallationDiff { + Add(AppInstallation), + Remove(AppInstallation), +} + +impl AppInstallationDiff { + async fn apply(&self, sync: &GitHubWrite, repo_id: u64, org: &str) -> anyhow::Result<()> { + match self { + AppInstallationDiff::Add(app) => { + sync.add_repo_to_app_installation(app.installation_id, repo_id, org) + .await?; + } + AppInstallationDiff::Remove(app) => { + sync.remove_repo_from_app_installation(app.installation_id, repo_id, org) + .await?; + } + } + Ok(()) + } +} + +impl std::fmt::Display for AppInstallationDiff { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppInstallationDiff::Add(app) => { + writeln!(f, " Install app {}", app.app) + } + AppInstallationDiff::Remove(app) => { + writeln!(f, " Remove app {}", app.app) + } + } + } +} diff --git a/src/sync/github/tests/mod.rs b/src/sync/github/tests/mod.rs index 9a22a42ca..e3309db92 100644 --- a/src/sync/github/tests/mod.rs +++ b/src/sync/github/tests/mod.rs @@ -216,6 +216,7 @@ async fn repo_change_description() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "foo", @@ -234,6 +235,7 @@ async fn repo_change_description() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -255,6 +257,7 @@ async fn repo_change_homepage() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -277,6 +280,7 @@ async fn repo_change_homepage() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -347,6 +351,7 @@ async fn repo_create() { ], rulesets: [], environments: [], + app_installations: [], }, ), ] @@ -375,6 +380,7 @@ async fn repo_add_member() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -402,6 +408,7 @@ async fn repo_add_member() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -429,6 +436,7 @@ async fn repo_change_member_permissions() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -457,6 +465,7 @@ async fn repo_change_member_permissions() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -479,6 +488,7 @@ async fn repo_remove_member() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -506,6 +516,7 @@ async fn repo_remove_member() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -530,6 +541,7 @@ async fn repo_add_team() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -557,6 +569,7 @@ async fn repo_add_team() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -579,6 +592,7 @@ async fn repo_change_team_permissions() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -607,6 +621,7 @@ async fn repo_change_team_permissions() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -629,6 +644,7 @@ async fn repo_remove_team() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -656,6 +672,7 @@ async fn repo_remove_team() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -678,6 +695,7 @@ async fn repo_archive_repo() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -696,6 +714,7 @@ async fn repo_archive_repo() { branch_protection_diffs: [], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -721,6 +740,7 @@ async fn repo_add_branch_protection() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -773,6 +793,7 @@ async fn repo_add_branch_protection() { ], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -817,6 +838,7 @@ async fn repo_update_branch_protection() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -867,6 +889,7 @@ async fn repo_update_branch_protection() { ], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -896,6 +919,7 @@ async fn repo_remove_branch_protection() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -921,6 +945,7 @@ async fn repo_remove_branch_protection() { ], ruleset_diffs: [], environment_diffs: [], + app_installation_diffs: [], }, ), ] @@ -992,6 +1017,7 @@ async fn repo_environment_create() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -1025,6 +1051,7 @@ async fn repo_environment_create() { }, ), ], + app_installation_diffs: [], }, ), ] @@ -1051,6 +1078,7 @@ async fn repo_environment_delete() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -1076,6 +1104,7 @@ async fn repo_environment_delete() { "staging", ), ], + app_installation_diffs: [], }, ), ] @@ -1117,6 +1146,7 @@ async fn repo_environment_update() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -1146,6 +1176,7 @@ async fn repo_environment_update() { "staging", ), ], + app_installation_diffs: [], }, ), ] @@ -1177,6 +1208,7 @@ async fn repo_environment_update_branches() { org: "rust-lang", name: "repo1", repo_node_id: "0", + repo_id: 0, settings_diff: ( RepoSettings { description: "", @@ -1212,6 +1244,7 @@ async fn repo_environment_update_branches() { new_tags: [], }, ], + app_installation_diffs: [], }, ), ] diff --git a/src/sync/github/tests/test_utils.rs b/src/sync/github/tests/test_utils.rs index f894e8b24..92a7072b0 100644 --- a/src/sync/github/tests/test_utils.rs +++ b/src/sync/github/tests/test_utils.rs @@ -138,6 +138,7 @@ impl DataModel { repo.name.clone(), Repo { node_id: org.repos.len().to_string(), + repo_id: org.repos.len() as u64, name: repo.name.clone(), org: repo.org.clone(), description: repo.description.clone(), @@ -604,6 +605,18 @@ impl GithubRead for GithubMock { Ok(self.get_org(org).members.iter().cloned().collect()) } + async fn org_app_installations(&self, _org: &str) -> anyhow::Result> { + Ok(vec![]) + } + + async fn app_installation_repos( + &self, + _installation_id: u64, + _org: &str, + ) -> anyhow::Result> { + Ok(vec![]) + } + async fn org_teams(&self, org: &str) -> anyhow::Result> { Ok(self .get_org(org) From 985d75fe7c5d75bb31be49aa03b94bb3d3af0de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 16 Feb 2026 14:01:35 +0100 Subject: [PATCH 2/8] Add renovatebot to crates-io-auth-action --- repos/rust-lang/crates-io-auth-action.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/rust-lang/crates-io-auth-action.toml b/repos/rust-lang/crates-io-auth-action.toml index 786cedd27..274a84f99 100644 --- a/repos/rust-lang/crates-io-auth-action.toml +++ b/repos/rust-lang/crates-io-auth-action.toml @@ -1,7 +1,7 @@ org = "rust-lang" name = "crates-io-auth-action" description = "Get a crates.io temporary access token" -bots = [] +bots = ["renovate"] [access.teams] crates-io-infra-admins = "write" From 6d73a5b9a5ea9ac56400356ed6f2e310cf2559ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 16 Feb 2026 17:27:59 +0100 Subject: [PATCH 3/8] Do not access installations of the GitHub App used for auth --- src/sync/github/api/read.rs | 8 +------- src/sync/github/api/tokens.rs | 4 ---- src/sync/github/mod.rs | 1 - 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/sync/github/api/read.rs b/src/sync/github/api/read.rs index 25ed05846..d4ff883c7 100644 --- a/src/sync/github/api/read.rs +++ b/src/sync/github/api/read.rs @@ -224,14 +224,8 @@ impl GithubRead for GitHubApiRead { } let mut installations = Vec::new(); - let url = if self.client.github_tokens.is_pat() { - // we are using a PAT - format!("user/installations/{installation_id}/repositories") - } else { - // we are using a GitHub App - "installation/repositories".to_string() - }; + let url = format!("user/installations/{installation_id}/repositories"); self.client .rest_paginated( &Method::GET, diff --git a/src/sync/github/api/tokens.rs b/src/sync/github/api/tokens.rs index 6f268cace..fb3c5060a 100644 --- a/src/sync/github/api/tokens.rs +++ b/src/sync/github/api/tokens.rs @@ -46,10 +46,6 @@ impl GitHubTokens { GitHubTokens::Pat(pat) => Ok(pat), } } - - pub fn is_pat(&self) -> bool { - matches!(self, GitHubTokens::Pat(_)) - } } fn org_name_from_env_var(env_var: &str) -> Option { diff --git a/src/sync/github/mod.rs b/src/sync/github/mod.rs index c2ab15eb8..db278185f 100644 --- a/src/sync/github/mod.rs +++ b/src/sync/github/mod.rs @@ -43,7 +43,6 @@ type RepoName = String; #[derive(Copy, Clone, Debug, PartialEq)] enum GithubApp { RenovateBot, - /// New Rust implementation of Bors Bors, } From 037f0e8c9482d730ffe7001078f6310048fcb59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Wed, 1 Apr 2026 14:42:27 +0200 Subject: [PATCH 4/8] Load enterprise app parameters from the environment --- Cargo.lock | 453 +++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/sync/github/api/mod.rs | 6 +- src/sync/github/api/tokens.rs | 113 ++++++++- src/sync/mod.rs | 2 +- 5 files changed, 565 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 521487f22..250256f87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -86,6 +95,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -126,6 +144,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -133,6 +157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -212,6 +237,39 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "cc" version = "1.2.58" @@ -266,6 +324,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -457,6 +529,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -544,6 +625,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -650,6 +737,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -657,6 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -665,6 +768,23 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + [[package]] name = "futures-macro" version = "0.3.32" @@ -694,9 +814,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -880,7 +1004,7 @@ dependencies = [ "log", "mime", "percent-encoding 1.0.1", - "time", + "time 0.1.45", "unicase", ] @@ -893,13 +1017,28 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -923,6 +1062,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1173,6 +1336,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "aws-lc-rs", + "base64 0.22.1", + "getrandom 0.2.17", + "js-sys", + "pem", + "serde", + "serde_json", + "signature", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.2.2" @@ -1238,6 +1418,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "octocrab" +version = "0.49.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f6687a23731011d0117f9f4c3cdabaa7b5e42ca671f42b5cc0657c492540e3" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.1", + "bytes 1.11.1", + "cargo_metadata", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "getrandom 0.2.17", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding 2.3.2", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "url", + "web-time", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1272,6 +1527,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -1284,6 +1549,26 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1316,6 +1601,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1539,7 +1830,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1552,6 +1843,7 @@ dependencies = [ "async-trait", "atty", "base64 0.22.1", + "chrono", "clap", "derive_builder", "dialoguer", @@ -1563,7 +1855,9 @@ dependencies = [ "hyper-old-types", "indexmap", "insta", + "jsonwebtoken", "log", + "octocrab", "regex", "reqwest", "rust_team_data", @@ -1626,6 +1920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1691,7 +1986,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1767,6 +2062,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -1823,6 +2122,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.0" @@ -1904,12 +2214,33 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time 0.3.47", +] + [[package]] name = "slab" version = "0.4.12" @@ -1922,6 +2253,27 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.6.3" @@ -2045,6 +2397,37 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2169,8 +2552,10 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2189,6 +2574,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2274,6 +2660,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -2290,6 +2682,7 @@ dependencies = [ "idna", "percent-encoding 2.3.2", "serde", + "serde_derive", ] [[package]] @@ -2465,6 +2858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] @@ -2508,12 +2902,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 786a06f5b..15c232e76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,16 @@ license.workspace = true anyhow = { version = "1", features = ["backtrace"] } async-trait = "0.1" base64 = "0.22" +chrono = "0.4" clap = { version = "4.5", features = ["derive"] } dialoguer = "0.12.0" env_logger = { version = "0.11.0", default-features = false } futures-util = "0.3" hyper-old-types = "0.11" indexmap.workspace = true +jsonwebtoken = "10" log = "0.4" +octocrab = { version = "0.49", default-features = false, features = ["jwt-aws-lc-rs", "default-client", "rustls", "rustls-aws-lc-rs", "follow-redirect", "timeout", "retry"] } regex = "1.5.5" reqwest = { version = "0.13.2", features = ["json", "rustls", "charset", "http2", "form", "query"], default-features = false } rust_team_data = { path = "rust_team_data", features = ["email-encryption"] } diff --git a/src/sync/github/api/mod.rs b/src/sync/github/api/mod.rs index 4ba40c479..315c0d67f 100644 --- a/src/sync/github/api/mod.rs +++ b/src/sync/github/api/mod.rs @@ -46,7 +46,7 @@ pub(crate) struct HttpClient { } impl HttpClient { - pub(crate) fn new() -> anyhow::Result { + pub(crate) async fn new() -> anyhow::Result { let mut builder = reqwest::ClientBuilder::default(); let mut map = HeaderMap::default(); @@ -58,7 +58,7 @@ impl HttpClient { Ok(Self { client: builder.build()?, - github_tokens: GitHubTokens::from_env()?, + github_tokens: GitHubTokens::from_env().await?, }) } @@ -67,7 +67,7 @@ impl HttpClient { } fn auth_header(&self, org: &str) -> anyhow::Result { - let token = self.github_tokens.get_token(org)?; + let token = self.github_tokens.get_token_for_org(org)?; let mut auth = HeaderValue::from_str(&format!("token {}", token.expose_secret()))?; auth.set_sensitive(true); Ok(auth) diff --git a/src/sync/github/api/tokens.rs b/src/sync/github/api/tokens.rs index fb3c5060a..e3173633e 100644 --- a/src/sync/github/api/tokens.rs +++ b/src/sync/github/api/tokens.rs @@ -1,12 +1,44 @@ use std::collections::HashMap; use anyhow::Context as _; +use chrono::Duration; +use octocrab::OctocrabBuilder; +use octocrab::models::{AppId, InstallationId}; use secrecy::SecretString; +/// Enterprise GitHub App used for certain operations that cannot be performed with an organization +/// GitHub app. +#[derive(Clone)] +pub struct EnterpriseClientCtx { + /// Token for the enterprise installation of an enterprise GH app. + /// + /// Used to: + /// - Find out in which repositories is an app installation installed in. + /// + /// The token has to be available for the whole duration of the process. + enterprise_token: SecretString, + /// Maps an organization to a pre-configured organization installation token of an enterprise GH + /// app. + /// + /// Used to: + /// - Find which apps are installed in a given organization. + /// + /// The token has to be available for the whole duration of the process. + org_tokens: HashMap, +} + #[derive(Clone)] pub enum GitHubTokens { /// One token per organization (used with GitHub App). - Orgs(HashMap), + /// Optionally can also include a GitHub enterprise app token, which is used to synchronize + /// GitHub apps themselves. + App { + /// Maps an organization to a pre-configured token. + /// The token has to be available for the whole duration of the process. + org_tokens: HashMap, + /// Context for using enterprise GitHub App. + enterprise_client_ctx: EnterpriseClientCtx, + }, /// One token for all API calls (used with Personal Access Token). Pat(SecretString), } @@ -16,7 +48,7 @@ impl GitHubTokens { /// /// Parses environment variables in the format GITHUB_TOKEN_{ORG_NAME} /// to retrieve GitHub tokens. - pub fn from_env() -> anyhow::Result { + pub async fn from_env() -> anyhow::Result { let mut tokens = HashMap::new(); for (key, value) in std::env::vars() { @@ -30,15 +62,82 @@ impl GitHubTokens { .context("failed to get any GitHub token environment variable")?; Ok(GitHubTokens::Pat(SecretString::from(pat_token))) } else { - Ok(GitHubTokens::Orgs(tokens)) + // We are using GitHub App authentication. + + // For the organization-level tokens, we use separate GitHub apps per organization. + // Those apps are preauthorized (from CI), and we directly load their tokens from + // environment variables. + + // Then we also need to load an enterprise GitHub App that is used to manage GitHub App + // installations. Since the CI action that we use does not support enterprise apps yet, + // we instead load the app id and the secret key through environment variables and + // generate the necessary tokens ourselves. + // Ideally, we would at least get the enterprise app installation id from GitHub, but + // for some reason their endpoint for that does not work at the moment. + // So we also have to pass the installation ID manually. + + let enterprise_gh_app_id = get_var("ENTERPRISE_APP_ID")? + .parse::() + .map(AppId) + .context("Enterprise app ID is not a number")?; + let enterprise_gh_app_installation_id = get_var("ENTERPRISE_APP_INSTALLATION_ID")? + .parse::() + .map(InstallationId) + .context("Enterprise app installation ID is not a number")?; + let enterprise_gh_app_secret_key = get_var("ENTERPRISE_APP_SECRET_KEY")?; + + let secret_key = + jsonwebtoken::EncodingKey::from_rsa_pem(enterprise_gh_app_secret_key.as_bytes()) + .context("Cannot load enterprise app secret key")?; + + // Client for the enterprise app + let enterprise_app_client = OctocrabBuilder::new() + .app(enterprise_gh_app_id, secret_key) + .build()?; + // Client for the enterprise app's installation in the enterprise... sigh. + let enterprise_installation_client = + enterprise_app_client.installation(enterprise_gh_app_installation_id)?; + // Token for finding which repositories are GH apps installed in + // Create a 1 hour buffer for the token + let enterprise_token = enterprise_installation_client + .installation_token_with_buffer(chrono::Duration::hours(1)) + .await?; + + let mut enterprise_org_tokens = HashMap::new(); + for org in tokens.keys() { + // Get the corresponding organization installation of the enterprise app + let org_installation = enterprise_app_client + .apps() + .get_org_installation(org) + .await + .with_context(|| { + anyhow::anyhow!( + "Cannot get organization installation for `{org}` of the enterprise app" + ) + })?; + let org_client = enterprise_app_client.installation(org_installation.id)?; + // Generate an installation token for the given org + let org_token = org_client + .installation_token_with_buffer(Duration::hours(1)) + .await?; + enterprise_org_tokens.insert(org.clone(), org_token); + } + + Ok(GitHubTokens::App { + org_tokens: tokens, + enterprise_client_ctx: EnterpriseClientCtx { + enterprise_token, + org_tokens: enterprise_org_tokens, + }, + }) } } /// Get a token for a GitHub organization. /// Return an error if not present. - pub fn get_token(&self, org: &str) -> anyhow::Result<&SecretString> { + pub fn get_token_for_org(&self, org: &str) -> anyhow::Result<&SecretString> { match self { - GitHubTokens::Orgs(orgs) => orgs.get(org).with_context(|| { + GitHubTokens::App { org_tokens, .. } => org_tokens.get(org).with_context(|| { format!( "failed to get the GitHub token environment variable for organization {org}" ) @@ -58,3 +157,7 @@ fn org_name_from_env_var(env_var: &str) -> Option { org.to_lowercase().replace('_', "-") }) } + +fn get_var(name: &str) -> anyhow::Result { + std::env::var(name).with_context(|| anyhow::anyhow!("Environment variable `{name}` not found.")) +} diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 4911dabee..524d6bc95 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -37,7 +37,7 @@ pub async fn run_sync_team( info!("synchronizing {service}"); match service.as_str() { "github" => { - let client = HttpClient::new()?; + let client = HttpClient::new().await?; let gh_read = Box::new(GitHubApiRead::from_client(client.clone())?); let teams = team_api.get_teams().await?; let repos = team_api.get_repos().await?; From e6728a10627bb1b6454c81d03f819e788a0a6c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Wed, 1 Apr 2026 15:02:03 +0200 Subject: [PATCH 5/8] Use the right token for a given token type --- src/sync/github/api/mod.rs | 8 +++++--- src/sync/github/api/read.rs | 33 +++++++++++++++++---------------- src/sync/github/api/tokens.rs | 32 ++++++++++++++++++++++++++------ src/sync/github/api/url.rs | 23 +++++++++++++++++++++++ src/sync/github/mod.rs | 4 ++-- 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/sync/github/api/mod.rs b/src/sync/github/api/mod.rs index 315c0d67f..79ea8b3cc 100644 --- a/src/sync/github/api/mod.rs +++ b/src/sync/github/api/mod.rs @@ -66,8 +66,10 @@ impl HttpClient { matches!(self.github_tokens, GitHubTokens::Pat(_)) } - fn auth_header(&self, org: &str) -> anyhow::Result { - let token = self.github_tokens.get_token_for_org(org)?; + fn auth_header(&self, url: &GitHubUrl) -> anyhow::Result { + let token = self + .github_tokens + .get_token_for_org(url.org(), url.token_type())?; let mut auth = HeaderValue::from_str(&format!("token {}", token.expose_secret()))?; auth.set_sensitive(true); Ok(auth) @@ -75,7 +77,7 @@ impl HttpClient { fn req(&self, method: Method, url: &GitHubUrl) -> anyhow::Result { trace!("http request: {} {}", method, url.url()); - let token = self.auth_header(url.org())?; + let token = self.auth_header(url)?; let client = self .client .request(method, url.url()) diff --git a/src/sync/github/api/read.rs b/src/sync/github/api/read.rs index d4ff883c7..e79b60bf2 100644 --- a/src/sync/github/api/read.rs +++ b/src/sync/github/api/read.rs @@ -1,4 +1,5 @@ use crate::sync::github::api; +use crate::sync::github::api::url::TokenType; use crate::sync::github::api::{BranchPolicy, Ruleset}; use crate::sync::github::api::{ BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, OrgAppInstallation, @@ -200,15 +201,16 @@ impl GithubRead for GitHubApiRead { } let mut installations = Vec::new(); + + // For this endpoint, we have to use an org-specific app installation of + // an enterprise GH app. + let url = GitHubUrl::orgs(org, "installations")? + .with_token_type(TokenType::EnterpriseOrganization); self.client - .rest_paginated( - &Method::GET, - &GitHubUrl::orgs(org, "installations")?, - |response: InstallationPage| { - installations.extend(response.installations); - Ok(()) - }, - ) + .rest_paginated(&Method::GET, &url, |response: InstallationPage| { + installations.extend(response.installations); + Ok(()) + }) .await?; Ok(installations) } @@ -226,15 +228,14 @@ impl GithubRead for GitHubApiRead { let mut installations = Vec::new(); let url = format!("user/installations/{installation_id}/repositories"); + + // For this endpoint, we have to use the enterprise installation of an enterprise GH app + let gh_url = GitHubUrl::new(&url, org).with_token_type(TokenType::Enterprise); self.client - .rest_paginated( - &Method::GET, - &GitHubUrl::new(&url, org), - |response: InstallationPage| { - installations.extend(response.repositories); - Ok(()) - }, - ) + .rest_paginated(&Method::GET, &gh_url, |response: InstallationPage| { + installations.extend(response.repositories); + Ok(()) + }) .await .with_context(|| format!("failed to send rest paginated request to {url}"))?; Ok(installations) diff --git a/src/sync/github/api/tokens.rs b/src/sync/github/api/tokens.rs index e3173633e..8cc8e8b78 100644 --- a/src/sync/github/api/tokens.rs +++ b/src/sync/github/api/tokens.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use crate::sync::github::api::url::TokenType; use anyhow::Context as _; use chrono::Duration; use octocrab::OctocrabBuilder; @@ -135,13 +136,32 @@ impl GitHubTokens { /// Get a token for a GitHub organization. /// Return an error if not present. - pub fn get_token_for_org(&self, org: &str) -> anyhow::Result<&SecretString> { + pub fn get_token_for_org( + &self, + org: &str, + token_type: &TokenType, + ) -> anyhow::Result<&SecretString> { match self { - GitHubTokens::App { org_tokens, .. } => org_tokens.get(org).with_context(|| { - format!( - "failed to get the GitHub token environment variable for organization {org}" - ) - }), + GitHubTokens::App { + org_tokens, + enterprise_client_ctx, + } => match token_type { + TokenType::Organization => org_tokens.get(org).with_context(|| { + format!( + "failed to get the GitHub token environment variable for organization {org}" + ) + }), + TokenType::EnterpriseOrganization => { + enterprise_client_ctx.org_tokens.get(org).with_context(|| { + format!( + "failed to get the GitHub token environment variable for organization `{org}` for the enterprise GH app" + ) + }) + } + TokenType::Enterprise => { + Ok(&enterprise_client_ctx.enterprise_token) + } + }, GitHubTokens::Pat(pat) => Ok(pat), } } diff --git a/src/sync/github/api/url.rs b/src/sync/github/api/url.rs index 3365c9c3d..68b5df613 100644 --- a/src/sync/github/api/url.rs +++ b/src/sync/github/api/url.rs @@ -1,3 +1,15 @@ +/// Type of a token used for an API request. +/// If PAT is used, then token type does not matter. +#[derive(Clone)] +pub enum TokenType { + /// Use token for an organization installation of an organization GitHub App. + Organization, + /// Use token for an organization installation of an enterprise GitHub App. + EnterpriseOrganization, + /// Use token for an enterprise installation of an enterprise GitHub App. + Enterprise, +} + /// A URL to a GitHub API endpoint. /// When using a GitHub App instead of a PAT, the token depends on the organization. /// So storing the token together with the URL is convenient. @@ -5,6 +17,7 @@ pub struct GitHubUrl { url: String, org: String, + token_type: TokenType, } impl GitHubUrl { @@ -18,9 +31,15 @@ impl GitHubUrl { Self { url, org: org.to_string(), + token_type: TokenType::Organization, } } + pub fn with_token_type(mut self, token_type: TokenType) -> Self { + self.token_type = token_type; + self + } + pub fn repos(org: &str, repo: &str, remaining_endpoint: &str) -> anyhow::Result { let remaining_endpoint = if remaining_endpoint.is_empty() { "".to_string() @@ -45,6 +64,10 @@ impl GitHubUrl { pub fn org(&self) -> &str { &self.org } + + pub fn token_type(&self) -> &TokenType { + &self.token_type + } } fn validate_remaining_endpoint(endpoint: &str) -> anyhow::Result<()> { diff --git a/src/sync/github/mod.rs b/src/sync/github/mod.rs index db278185f..e94eed097 100644 --- a/src/sync/github/mod.rs +++ b/src/sync/github/mod.rs @@ -128,6 +128,8 @@ impl SyncGitHub { let mut installations: Vec = vec![]; for installation in github.org_app_installations(org).await? { + // Only load installations from apps that we know about, to avoid removing + // installations of unknown apps. if let Some(app) = GithubApp::from_id(installation.app_id) { let mut repositories = HashSet::new(); for repo_installation in github @@ -540,8 +542,6 @@ impl SyncGitHub { installations .iter() .filter_map(|installation| { - // Only load installations from apps that we know about, to avoid removing - // unknown installations. if installation.repositories.contains(&actual_repo.name) { Some(AppInstallation { app: installation.app, From e409f3183eab04d5fea5803b969c3935ab1f49c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Fri, 3 Apr 2026 09:20:20 +0200 Subject: [PATCH 6/8] Use the right endpoint for fetching repositories where an app is installed with an app token --- src/sync/github/api/read.rs | 53 ++++++++++++++++++++++------- src/sync/github/api/tokens.rs | 45 +++++++++++++++++------- src/sync/github/tests/test_utils.rs | 4 +-- 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/sync/github/api/read.rs b/src/sync/github/api/read.rs index e79b60bf2..ea0deb5a1 100644 --- a/src/sync/github/api/read.rs +++ b/src/sync/github/api/read.rs @@ -202,8 +202,10 @@ impl GithubRead for GitHubApiRead { let mut installations = Vec::new(); - // For this endpoint, we have to use an org-specific app installation of - // an enterprise GH app. + // Without a PAT, we could use an enterprise endpoint instead, which would require an + // organization installation token. However, that endpoint does not return the app ID, just + // its slug. And getting the ID from the slug via the /apps/ endpoint does not work + // for private apps. let url = GitHubUrl::orgs(org, "installations")? .with_token_type(TokenType::EnterpriseOrganization); self.client @@ -227,17 +229,44 @@ impl GithubRead for GitHubApiRead { let mut installations = Vec::new(); - let url = format!("user/installations/{installation_id}/repositories"); + // For a PAT, we have to use the /user/installations//repositories + // endpoint. + // For GH apps we have to use an enterprise endpoint instead. + if self.client.uses_pat() { + let url = format!("user/installations/{installation_id}/repositories"); + let url = GitHubUrl::new(&url, org); + self.client + .rest_paginated(&Method::GET, &url, |response: InstallationPage| { + installations.extend(response.repositories); + Ok(()) + }) + .await + .with_context(|| { + format!("failed to send rest paginated request to {}", url.url()) + })?; + } else { + let url = GitHubUrl::new( + &format!("enterprises/{}/apps/organizations/{org}/installations/{installation_id}/repositories", + self.client.github_tokens.get_enterprise_name()? + ), + org, + ) + .with_token_type(TokenType::Enterprise); + self.client + .rest_paginated( + &Method::GET, + &url, + |repositories: Vec| { + installations.extend(repositories); + Ok(()) + }, + ) + .await + .with_context(|| { + format!("failed to send rest paginated request to {}", url.url()) + })?; + }; - // For this endpoint, we have to use the enterprise installation of an enterprise GH app - let gh_url = GitHubUrl::new(&url, org).with_token_type(TokenType::Enterprise); - self.client - .rest_paginated(&Method::GET, &gh_url, |response: InstallationPage| { - installations.extend(response.repositories); - Ok(()) - }) - .await - .with_context(|| format!("failed to send rest paginated request to {url}"))?; Ok(installations) } diff --git a/src/sync/github/api/tokens.rs b/src/sync/github/api/tokens.rs index 8cc8e8b78..1ed36b61c 100644 --- a/src/sync/github/api/tokens.rs +++ b/src/sync/github/api/tokens.rs @@ -10,35 +10,39 @@ use secrecy::SecretString; /// Enterprise GitHub App used for certain operations that cannot be performed with an organization /// GitHub app. #[derive(Clone)] -pub struct EnterpriseClientCtx { +pub struct EnterpriseAppCtx { /// Token for the enterprise installation of an enterprise GH app. /// /// Used to: - /// - Find out in which repositories is an app installation installed in. + /// - Find out in which repositories is an app installed in. /// /// The token has to be available for the whole duration of the process. enterprise_token: SecretString, /// Maps an organization to a pre-configured organization installation token of an enterprise GH - /// app. + /// app. We need this token, because the enterprise token does not have permissions for fetching + /// everything we need about apps installed in an organization (sigh). /// /// Used to: /// - Find which apps are installed in a given organization. /// /// The token has to be available for the whole duration of the process. org_tokens: HashMap, + /// Name of the enterprise. + enterprise_name: String, } #[derive(Clone)] pub enum GitHubTokens { - /// One token per organization (used with GitHub App). - /// Optionally can also include a GitHub enterprise app token, which is used to synchronize - /// GitHub apps themselves. + /// Authentication using a set of GitHub apps. + /// + /// Stores one token per organization for most API operations. + /// For operations involving other GitHub apps, also stores GitHub enterprise app token(s). App { /// Maps an organization to a pre-configured token. /// The token has to be available for the whole duration of the process. org_tokens: HashMap, /// Context for using enterprise GitHub App. - enterprise_client_ctx: EnterpriseClientCtx, + enterprise_client_ctx: EnterpriseAppCtx, }, /// One token for all API calls (used with Personal Access Token). Pat(SecretString), @@ -69,14 +73,14 @@ impl GitHubTokens { // Those apps are preauthorized (from CI), and we directly load their tokens from // environment variables. - // Then we also need to load an enterprise GitHub App that is used to manage GitHub App + // Then we also need to load an enterprise GitHub App used to manage GitHub App // installations. Since the CI action that we use does not support enterprise apps yet, // we instead load the app id and the secret key through environment variables and // generate the necessary tokens ourselves. // Ideally, we would at least get the enterprise app installation id from GitHub, but // for some reason their endpoint for that does not work at the moment. // So we also have to pass the installation ID manually. - + let enterprise_name = get_var("ENTERPRISE_NAME")?; let enterprise_gh_app_id = get_var("ENTERPRISE_APP_ID")? .parse::() .map(AppId) @@ -95,13 +99,14 @@ impl GitHubTokens { let enterprise_app_client = OctocrabBuilder::new() .app(enterprise_gh_app_id, secret_key) .build()?; - // Client for the enterprise app's installation in the enterprise... sigh. + // Client for the enterprise app's installation in the enterprise... sigh let enterprise_installation_client = enterprise_app_client.installation(enterprise_gh_app_installation_id)?; + // Token for finding which repositories are GH apps installed in // Create a 1 hour buffer for the token let enterprise_token = enterprise_installation_client - .installation_token_with_buffer(chrono::Duration::hours(1)) + .installation_token_with_buffer(Duration::hours(1)) .await?; let mut enterprise_org_tokens = HashMap::new(); @@ -117,7 +122,7 @@ impl GitHubTokens { ) })?; let org_client = enterprise_app_client.installation(org_installation.id)?; - // Generate an installation token for the given org + // Generate an enterprise app installation token for the given org let org_token = org_client .installation_token_with_buffer(Duration::hours(1)) .await?; @@ -126,9 +131,10 @@ impl GitHubTokens { Ok(GitHubTokens::App { org_tokens: tokens, - enterprise_client_ctx: EnterpriseClientCtx { + enterprise_client_ctx: EnterpriseAppCtx { enterprise_token, org_tokens: enterprise_org_tokens, + enterprise_name, }, }) } @@ -165,6 +171,19 @@ impl GitHubTokens { GitHubTokens::Pat(pat) => Ok(pat), } } + + /// Return the name of the enterprise, if present. + pub fn get_enterprise_name(&self) -> anyhow::Result { + match self { + GitHubTokens::App { + enterprise_client_ctx, + .. + } => Ok(enterprise_client_ctx.enterprise_name.clone()), + GitHubTokens::Pat(_) => Err(anyhow::anyhow!( + "No enterprise is configured when using a PAT" + )), + } + } } fn org_name_from_env_var(env_var: &str) -> Option { diff --git a/src/sync/github/tests/test_utils.rs b/src/sync/github/tests/test_utils.rs index 92a7072b0..a87e30f59 100644 --- a/src/sync/github/tests/test_utils.rs +++ b/src/sync/github/tests/test_utils.rs @@ -12,8 +12,8 @@ use rust_team_data::v1::{ use crate::schema; use crate::sync::Config; use crate::sync::github::api::{ - BranchPolicy, BranchProtection, GithubRead, Repo, RepoTeam, RepoUser, Ruleset, Team, - TeamMember, TeamPrivacy, TeamRole, + BranchPolicy, BranchProtection, GithubRead, OrgAppInstallation, Repo, RepoAppInstallation, + RepoTeam, RepoUser, Ruleset, Team, TeamMember, TeamPrivacy, TeamRole, }; use crate::sync::github::{ OrgMembershipDiff, RepoDiff, SyncGitHub, TeamDiff, api, construct_branch_protection, From f743c5edfa3bb67ebf2766e96456017d818d9317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Fri, 3 Apr 2026 12:27:52 +0200 Subject: [PATCH 7/8] Setup GitHub Actions environment variables --- .github/workflows/dry-run.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dry-run.yml b/.github/workflows/dry-run.yml index e7f8fcb7a..562907711 100644 --- a/.github/workflows/dry-run.yml +++ b/.github/workflows/dry-run.yml @@ -72,6 +72,11 @@ jobs: # need to print a diff. CRATES_IO_TOKEN: "" CRATES_IO_USERNAME: "rust-lang-owner" + ENTERPRISE_NAME: "rust-lang" + # sync-team-app-read enterprise app + ENTERPRISE_APP_ID: 2885328 + ENTERPRISE_APP_INSTALLATION_ID: 110701681 + ENTERPRISE_APP_SECRET_KEY: ${{ secrets.ENTERPRISE_APP_SECRET_KEY }} # This applies pipefail, so that the tee pipeline below fails when sync-team fails. shell: bash run: | From efbd0b064c18a241446643073e49f4a5b9e86757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Fri, 3 Apr 2026 12:28:45 +0200 Subject: [PATCH 8/8] Return string reference from `get_enterprise_name` --- src/sync/github/api/tokens.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync/github/api/tokens.rs b/src/sync/github/api/tokens.rs index 1ed36b61c..2aed19ecd 100644 --- a/src/sync/github/api/tokens.rs +++ b/src/sync/github/api/tokens.rs @@ -173,12 +173,12 @@ impl GitHubTokens { } /// Return the name of the enterprise, if present. - pub fn get_enterprise_name(&self) -> anyhow::Result { + pub fn get_enterprise_name(&self) -> anyhow::Result<&str> { match self { GitHubTokens::App { enterprise_client_ctx, .. - } => Ok(enterprise_client_ctx.enterprise_name.clone()), + } => Ok(enterprise_client_ctx.enterprise_name.as_str()), GitHubTokens::Pat(_) => Err(anyhow::anyhow!( "No enterprise is configured when using a PAT" )),