Watch
0
0
Fork
You've already forked hyperhive
0

config PRs: document the operator merge, deploy only merges into main

docs: the config-repo `main` merge gate (merge = `core` + team
`operators`, approvals = `operators`), the operator merge in the forge
UI and what it deploys, the hand-added `operators` membership, and that
with no eval-verify a failed rebuild leaves `applied/main` at the merged
commit. The swarm README lists the converged gate and the merge deploy.

`merged()` also requires `pull_request.base.ref == "main"`: the hive
deploys its config repo's `main`, so a merge into another branch would
only cost a forge fetch and a refusal comment (argus, #4894).

Refs #4850
This commit is contained in:
atlas 2026-10-02 19:27:16 +02:00
commit a5ea015bc6
4 changed files with 90 additions and 16 deletions

View file

@ -62,6 +62,16 @@ struct WebhookPullRequest {
/// The commit the merge left on the base branch.
#[serde(default)]
merge_commit_sha: Option<String>,
/// The branch the PR targets. Only a merge into `main` is deployed: the
/// hive builds its config repo's `main`.
#[serde(default)]
base: Option<WebhookBranch>,
}
#[derive(Deserialize)]
struct WebhookBranch {
#[serde(rename = "ref")]
name: String,
}
/// A config PR that was just merged: whose config, and the commit to deploy.
@ -75,11 +85,17 @@ pub struct MergedConfigPr {
///
/// Only the `closed` action counts: Forgejo also sends `merged: true` on
/// later events about an already-merged PR (a label or an edit), and those
/// must not deploy it again. A body that does not parse is `None`;
/// [`ConfigPrCache::apply_webhook_delivery`] already logs it.
/// must not deploy it again. So does only a merge into `main`. A body that
/// does not parse is `None`; [`ConfigPrCache::apply_webhook_delivery`]
/// already logs it.
pub fn merged(body: &[u8]) -> Option<MergedConfigPr> {
let payload: ConfigPrWebhookPayload = serde_json::from_slice(body).ok()?;
if payload.action != "closed" || !payload.pull_request.merged {
let into_main = payload
.pull_request
.base
.as_ref()
.is_some_and(|base| base.name == "main");
if payload.action != "closed" || !payload.pull_request.merged || !into_main {
return None;
}
let Some(rev) = payload.pull_request.merge_commit_sha else {
@ -301,6 +317,7 @@ mod tests {
"html_url": "https://forge.example/pr",
"merged": merged,
"merge_commit_sha": merged.then_some("abc123"),
"base": { "ref": "main" },
},
"repository": { "name": "damocles" },
})
@ -332,6 +349,14 @@ mod tests {
assert_eq!(super::merged(body.to_string().as_bytes()), None);
}
#[test]
fn a_merge_into_another_branch_is_not_deployed() {
let mut body: serde_json::Value =
serde_json::from_slice(&closed(true)).expect("fixture is json");
body["pull_request"]["base"]["ref"] = "staging".into();
assert_eq!(super::merged(body.to_string().as_bytes()), None);
}
#[test]
fn an_open_delivery_is_not_a_merge() {
assert_eq!(super::merged(&payload("damocles", 5, "open")), None);