Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9667f9c1a | ||
|
|
beaa220dc2 |
1 changed files with 62 additions and 6 deletions
|
|
@ -10,7 +10,8 @@ use anyhow::{Context, Result};
|
||||||
use forgejo_api::structs::{
|
use forgejo_api::structs::{
|
||||||
AddCollaboratorOption, AddCollaboratorOptionPermission, CreateBranchProtectionOption,
|
AddCollaboratorOption, AddCollaboratorOptionPermission, CreateBranchProtectionOption,
|
||||||
CreateOrgOption, CreateRepoOption, CreateTeamOption, CreateTeamOptionPermission,
|
CreateOrgOption, CreateRepoOption, CreateTeamOption, CreateTeamOptionPermission,
|
||||||
EditRepoOption, MigrateRepoOptions, MigrateRepoOptionsService, Repository,
|
EditRepoOption, EditTeamOption, EditTeamOptionPermission, MigrateRepoOptions,
|
||||||
|
MigrateRepoOptionsService, Repository,
|
||||||
};
|
};
|
||||||
use forgejo_api::{ApiErrorKind, ForgejoError};
|
use forgejo_api::{ApiErrorKind, ForgejoError};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
@ -561,7 +562,7 @@ async fn ensure_mirror_repo(
|
||||||
/// merge/approval whitelist; the operator adds herself as a member via the
|
/// merge/approval whitelist; the operator adds herself as a member via the
|
||||||
/// forge UI / hivectl. `includes_all_repositories` so the gate applies to
|
/// forge UI / hivectl. `includes_all_repositories` so the gate applies to
|
||||||
/// every repo in the org; `write` is enough to approve + merge. hive-c0re
|
/// every repo in the org; `write` is enough to approve + merge. hive-c0re
|
||||||
/// never manages membership. Idempotent (422/409 = already exists).
|
/// never manages membership. Idempotent (409 = already exists).
|
||||||
///
|
///
|
||||||
/// Must run for BOTH [`AGENTS_ORG`] and [`CONFIG_ORG`]: Gitea teams are
|
/// Must run for BOTH [`AGENTS_ORG`] and [`CONFIG_ORG`]: Gitea teams are
|
||||||
/// org-scoped, so a config-repo branch-protection rule referencing
|
/// org-scoped, so a config-repo branch-protection rule referencing
|
||||||
|
|
@ -569,25 +570,80 @@ async fn ensure_mirror_repo(
|
||||||
/// there 422'd every `apply_config_repo_branch_protection`, leaving config
|
/// there 422'd every `apply_config_repo_branch_protection`, leaving config
|
||||||
/// repos unprotected — operator-merged config PRs then bypassed the deploy
|
/// repos unprotected — operator-merged config PRs then bypassed the deploy
|
||||||
/// pipeline and silently didn't apply.
|
/// pipeline and silently didn't apply.
|
||||||
|
///
|
||||||
|
/// Uses `is_conflict` (409 only) — NOT `is_already_exists` (which also
|
||||||
|
/// folds 422 into "already exists"). A 422 from `org_create_team` is a
|
||||||
|
/// real validation error (bad request shape, missing units, etc.) that
|
||||||
|
/// must surface so it can be fixed; the previous 422-swallowing hid the
|
||||||
|
/// true cause and left the team silently uncreated every boot.
|
||||||
|
/// Repo-unit access flags for the `operators` team.
|
||||||
|
/// Explicit list so Forgejo doesn't reject a null/absent `units` field;
|
||||||
|
/// a `write`-permission team needs at least `repo.code` + `repo.pulls`
|
||||||
|
/// to review and merge PRs.
|
||||||
|
const OPERATORS_TEAM_UNITS: &[&str] = &[
|
||||||
|
"repo.code",
|
||||||
|
"repo.issues",
|
||||||
|
"repo.pulls",
|
||||||
|
"repo.releases",
|
||||||
|
"repo.wiki",
|
||||||
|
"repo.projects",
|
||||||
|
"repo.packages",
|
||||||
|
];
|
||||||
|
|
||||||
pub(super) async fn ensure_operators_team(org: &str, token: &str) -> Result<()> {
|
pub(super) async fn ensure_operators_team(org: &str, token: &str) -> Result<()> {
|
||||||
|
let units_vec: Vec<String> = OPERATORS_TEAM_UNITS.iter().map(|&s| s.to_owned()).collect();
|
||||||
let team = CreateTeamOption {
|
let team = CreateTeamOption {
|
||||||
can_create_org_repo: Some(false),
|
can_create_org_repo: Some(false),
|
||||||
description: Some("hyperhive operators — merge gate for agent repos".to_owned()),
|
description: Some("hyperhive operators — merge gate for agent repos".to_owned()),
|
||||||
includes_all_repositories: Some(true),
|
includes_all_repositories: Some(true),
|
||||||
name: OPERATORS_TEAM.to_owned(),
|
name: OPERATORS_TEAM.to_owned(),
|
||||||
permission: Some(CreateTeamOptionPermission::Write),
|
permission: Some(CreateTeamOptionPermission::Write),
|
||||||
units: None,
|
units: Some(units_vec.clone()),
|
||||||
units_map: None,
|
units_map: None,
|
||||||
};
|
};
|
||||||
match api(token)?.org_create_team(org, team).await {
|
let client = api(token)?;
|
||||||
|
match client.org_create_team(org, team).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!(%org, "forge: created {OPERATORS_TEAM} team");
|
tracing::info!(%org, "forge: created {OPERATORS_TEAM} team");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) if is_already_exists(&e) => {
|
// 409: team already exists — reconcile settings to desired state so
|
||||||
tracing::debug!(%org, "forge: {OPERATORS_TEAM} team already exists");
|
// a team created with an older/wrong shape self-heals on next boot.
|
||||||
|
// List teams to find the id (required by org_edit_team), then
|
||||||
|
// unconditionally PATCH to the desired settings. Members are a
|
||||||
|
// separate endpoint; this never touches membership.
|
||||||
|
Err(e) if is_conflict(&e) => {
|
||||||
|
let (_headers, teams) = client
|
||||||
|
.org_list_teams(org)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("list teams for {org}"))?;
|
||||||
|
let team_id = teams
|
||||||
|
.into_iter()
|
||||||
|
.find(|t| t.name.as_deref() == Some(OPERATORS_TEAM))
|
||||||
|
.and_then(|t| t.id)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("{OPERATORS_TEAM} team not found in {org} after 409 conflict")
|
||||||
|
})?;
|
||||||
|
let edit = EditTeamOption {
|
||||||
|
can_create_org_repo: Some(false),
|
||||||
|
description: Some("hyperhive operators — merge gate for agent repos".to_owned()),
|
||||||
|
includes_all_repositories: Some(true),
|
||||||
|
name: OPERATORS_TEAM.to_owned(),
|
||||||
|
permission: Some(EditTeamOptionPermission::Write),
|
||||||
|
units: Some(units_vec),
|
||||||
|
units_map: None,
|
||||||
|
};
|
||||||
|
client
|
||||||
|
.org_edit_team(team_id, edit)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("reconcile {org}/{OPERATORS_TEAM} team settings"))?;
|
||||||
|
tracing::debug!(%org, "forge: reconciled {OPERATORS_TEAM} team settings");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
// 422 and any other error: surface it — don't mask a real failure
|
||||||
|
// as "already exists". A 422 here typically means the request body
|
||||||
|
// is invalid (e.g. Forgejo rejected the units list or another
|
||||||
|
// field); it will repeat every boot until fixed.
|
||||||
Err(e) => Err(e).with_context(|| format!("create team {org}/{OPERATORS_TEAM}")),
|
Err(e) => Err(e).with_context(|| format!("create team {org}/{OPERATORS_TEAM}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue