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
|
|
@ -18,6 +18,10 @@
|
|||
//! The low-latency path: a PR opening or closing shows up immediately
|
||||
//! instead of waiting up to `POLL_INTERVAL`.
|
||||
//!
|
||||
//! [`merged`] reads the same delivery for a merge, which the webhook handler
|
||||
//! turns into a deploy. The poll has no counterpart: it lists open PRs only,
|
||||
//! so a merge whose delivery is lost deploys nothing.
|
||||
//!
|
||||
//! Per mara's review call: ship both from the start rather than the poll
|
||||
//! alone — the eventual swarm-level replacement for `hive-c0re`'s own
|
||||
//! poll+webhook pair needs both anyway, so building only half here would be
|
||||
|
|
@ -36,6 +40,10 @@ use crate::forge::{Client, ConfigPrStatus};
|
|||
/// whether it's still open.
|
||||
#[derive(Deserialize)]
|
||||
pub struct ConfigPrWebhookPayload {
|
||||
/// `"closed"` on both a merge and a close without merging; `merged`
|
||||
/// tells them apart.
|
||||
#[serde(default)]
|
||||
action: String,
|
||||
pull_request: WebhookPullRequest,
|
||||
repository: WebhookRepository,
|
||||
}
|
||||
|
|
@ -49,6 +57,43 @@ struct WebhookPullRequest {
|
|||
/// doesn't matter to it.
|
||||
state: String,
|
||||
html_url: Option<String>,
|
||||
#[serde(default)]
|
||||
merged: bool,
|
||||
/// The commit the merge left on the base branch.
|
||||
#[serde(default)]
|
||||
merge_commit_sha: Option<String>,
|
||||
}
|
||||
|
||||
/// A config PR that was just merged: whose config, and the commit to deploy.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct MergedConfigPr {
|
||||
pub agent: String,
|
||||
pub rev: String,
|
||||
}
|
||||
|
||||
/// The merge a verified `ConfigPr` delivery reports, if it reports one.
|
||||
///
|
||||
/// 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.
|
||||
pub fn merged(body: &[u8]) -> Option<MergedConfigPr> {
|
||||
let payload: ConfigPrWebhookPayload = serde_json::from_slice(body).ok()?;
|
||||
if payload.action != "closed" || !payload.pull_request.merged {
|
||||
return None;
|
||||
}
|
||||
let Some(rev) = payload.pull_request.merge_commit_sha else {
|
||||
tracing::warn!(
|
||||
agent = %payload.repository.name,
|
||||
pr = payload.pull_request.number,
|
||||
"config-pr webhook: merged PR carries no merge_commit_sha; nothing deployed"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
Some(MergedConfigPr {
|
||||
agent: payload.repository.name,
|
||||
rev,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -247,6 +292,51 @@ mod tests {
|
|||
assert_eq!(snapshot["iris"].pr_number, 9);
|
||||
}
|
||||
|
||||
fn closed(merged: bool) -> Vec<u8> {
|
||||
serde_json::json!({
|
||||
"action": "closed",
|
||||
"pull_request": {
|
||||
"number": 5,
|
||||
"state": "closed",
|
||||
"html_url": "https://forge.example/pr",
|
||||
"merged": merged,
|
||||
"merge_commit_sha": merged.then_some("abc123"),
|
||||
},
|
||||
"repository": { "name": "damocles" },
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_merged_delivery_names_the_agent_and_the_merge_commit() {
|
||||
assert_eq!(
|
||||
super::merged(&closed(true)),
|
||||
Some(super::MergedConfigPr {
|
||||
agent: "damocles".to_owned(),
|
||||
rev: "abc123".to_owned(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_unmerged_delivery_is_not_a_merge() {
|
||||
assert_eq!(super::merged(&closed(false)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_merged_pr_on_a_non_close_action_is_not_a_merge() {
|
||||
let mut body: serde_json::Value =
|
||||
serde_json::from_slice(&closed(true)).expect("fixture is json");
|
||||
body["action"] = "label_updated".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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_malformed_payload_leaves_the_cache_unchanged() {
|
||||
let cache = ConfigPrCache::new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue