config PRs: an operator's Forgejo merge deploys the merged rev
A config PR merged in the Forgejo UI changed nothing on the hive: the hive's webhook ignores `closed`, its poll then cancels the dashboard card, and `applied/main` stays where it was. swarm-controller reads `merged`/`merge_commit_sha` off the `pull_request` delivery it already receives for `agent-configs`, finds the hive placing the agent by scanning every hive's wanted state (the scan `declarations_elsewhere` already ran, factored out), and queues a `TriggerDeploy` carrying the rev. Zero or several claimants deploy nothing and log the claimants. `DeployRequest` gains `rev: Option<String>` with `serde(default)`, so rev-less payloads from either side keep decoding. hive-c0re, given a rev for an agent it runs: a no-op when `applied/main` already is the rev (a dashboard merge deploys its own PR); otherwise it fetches the forge `main` with the core token, requires the rev to descend from `applied/main` (the ancestry gate, factored out of `run_deploy_merge_verify`), fast-forwards by CAS and queues the usual relocking rebuild. No eval-verify on this path, per mara (#4850 c90075). A refusal is commented on the PR that merged the rev, found by commit. swarm-controller's forge-objects pass converges every config repo's `main` rule to merge whitelist `operators` + `core` and approval whitelist `operators`. The hive's boot PATCH stops forcing `enable_approvals_whitelist` off, so the two do not fight. Refs #4850
This commit is contained in:
parent
9224c0bd15
commit
f1c695c212
11 changed files with 732 additions and 76 deletions
|
|
@ -12,10 +12,10 @@ mod repos;
|
|||
mod users;
|
||||
|
||||
pub use pr_merge::{
|
||||
ForgeMergeError, config_repo, fetch_pr_head_into_applied, merge_config_pr_ff, post_pr_comment,
|
||||
pr_head_sha, pr_is_open,
|
||||
ForgeMergeError, config_repo, fetch_pr_head_into_applied, merge_config_pr_ff,
|
||||
merged_pr_for_commit, post_pr_comment, pr_head_sha, pr_is_open,
|
||||
};
|
||||
pub use reconcile::{reconcile_config_apply, reconcile_config_status};
|
||||
pub use reconcile::{fetch_forge_main, reconcile_config_apply, reconcile_config_status};
|
||||
pub use repos::{
|
||||
clone_config_into_proposed, ensure_config_repo, ensure_meta_remote, ensure_repo,
|
||||
fast_forward_applied_main, fetch_config_main_into_applied, meta_read_access, push_config,
|
||||
|
|
|
|||
|
|
@ -218,6 +218,33 @@ pub async fn merge_config_pr_ff(repo: &str, pr: u64, sha: &str) -> Result<(), Fo
|
|||
}
|
||||
}
|
||||
|
||||
/// The number of the merged PR that put commit `sha` on `repo`'s base branch.
|
||||
///
|
||||
/// # Errors
|
||||
/// `Other` on absent core token, malformed repo, no such PR, or
|
||||
/// transport/API failure.
|
||||
pub async fn merged_pr_for_commit(repo: &str, sha: &str) -> Result<u64, ForgeMergeError> {
|
||||
let token = core_token()
|
||||
.ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?;
|
||||
let (owner, name) = repo.split_once('/').ok_or_else(|| {
|
||||
ForgeMergeError::Other(anyhow::anyhow!("forge repo `{repo}` is not owner/name"))
|
||||
})?;
|
||||
let client = api(&token).map_err(ForgeMergeError::Other)?;
|
||||
let pull = client
|
||||
.repo_get_commit_pull_request(owner, name, sha)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ForgeMergeError::Other(anyhow::Error::from(e).context("GET pull request of commit"))
|
||||
})?;
|
||||
pull.number
|
||||
.and_then(|n| u64::try_from(n).ok())
|
||||
.ok_or_else(|| {
|
||||
ForgeMergeError::Other(anyhow::anyhow!(
|
||||
"pull request of {sha} in {repo} carries no number"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Post a comment to PR (= issue) `pr` on `repo` as the core forge user.
|
||||
/// PRs are issues in Forgejo, so the PR number is the issue index. Used to
|
||||
/// surface a failed config-approval deploy's build log back onto the PR so
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ const FORGE_MAIN_REF: &str = "refs/hyperhive/forge-config-main";
|
|||
|
||||
/// Fetch `agent-configs/<agent>` `main` into the applied repo's scratch
|
||||
/// ref (read-only, no working-tree change) and return the applied dir.
|
||||
async fn fetch_forge_main(agent: &str) -> Result<PathBuf> {
|
||||
/// Also how a swarm deploy of a merged commit gets that commit's objects.
|
||||
pub async fn fetch_forge_main(agent: &str) -> Result<PathBuf> {
|
||||
if !is_present().await {
|
||||
anyhow::bail!("forge is not running");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -667,14 +667,14 @@ fn main_branch_protection_option() -> CreateBranchProtectionOption {
|
|||
/// node (`actions::run_deploy_apply`), which fast-forward-*merges* the reviewed
|
||||
/// head through the forge merge API (`Do=fast-forward-only`,
|
||||
/// `head_commit_id` pinned to the reviewed sha).
|
||||
/// - **merge is whitelisted to `core`** — only hive-c0re can merge a config PR;
|
||||
/// the agent can push feature branches + open PRs but can't land them.
|
||||
/// - **the operator's dashboard approval is the gate** — approval happens on
|
||||
/// the `MergeConfigPr` card and hive-c0re only merges an approved PR. There's
|
||||
/// deliberately no Forgejo `required_approvals` review requirement: the flow
|
||||
/// never does an in-forge review, so requiring one would only dead-block the
|
||||
/// `core` merge. The dashboard approval + the `core`-only merge whitelist are
|
||||
/// the real gate.
|
||||
/// - **merge is whitelisted to `core`** — the agent can push feature branches +
|
||||
/// open PRs but can't land them. swarm-controller adds the `operators` team
|
||||
/// to the whitelist, so an operator can also merge in the forge UI.
|
||||
/// - **the operator's dashboard approval is the gate for `core`** — approval
|
||||
/// happens on the `MergeConfigPr` card and hive-c0re only merges an approved
|
||||
/// PR. There's deliberately no Forgejo `required_approvals` review
|
||||
/// requirement: that flow never does an in-forge review, so requiring one
|
||||
/// would only dead-block the `core` merge.
|
||||
/// - **fast-forward-only** — `main` only ever advances by fast-forward; a raced
|
||||
/// non-ff `main` is refused by the merge API rather than force-moved.
|
||||
///
|
||||
|
|
@ -736,6 +736,11 @@ async fn apply_config_repo_branch_protection(repo: &str, token: &str) -> Result<
|
|||
/// left `None`, so a repo carrying an older push-based or approval-gated rule is
|
||||
/// actually *converged* rather than merely re-asserted — a PATCH leaves unset
|
||||
/// fields untouched. Every field unrelated to this policy stays `None`.
|
||||
///
|
||||
/// The approval and merge whitelists' **teams** stay `None` too, and so does
|
||||
/// `enable_approvals_whitelist`: swarm-controller puts the `operators` team on
|
||||
/// both, so operators can merge config PRs in the forge UI, and this PATCH
|
||||
/// runs on every boot.
|
||||
fn config_repo_protection_edit() -> EditBranchProtectionOption {
|
||||
EditBranchProtectionOption {
|
||||
apply_to_admins: None,
|
||||
|
|
@ -745,7 +750,7 @@ fn config_repo_protection_edit() -> EditBranchProtectionOption {
|
|||
block_on_outdated_branch: None,
|
||||
block_on_rejected_reviews: None,
|
||||
dismiss_stale_approvals: None,
|
||||
enable_approvals_whitelist: Some(false),
|
||||
enable_approvals_whitelist: None,
|
||||
enable_merge_whitelist: Some(true),
|
||||
enable_push: Some(false),
|
||||
enable_push_whitelist: Some(false),
|
||||
|
|
|
|||
Loading…
Reference in a new issue