Watch
0
0
Fork
You've already forked hyperhive
0

config PRs: remove the hive's config-PR webhook, poll and core merge

An operator's merge on the forge deploys a config PR through
swarm-controller's DeployRequest{rev}. The hive-side path that queued a
MergeConfigPr approval and merged the PR as `core` goes:

- the `/webhook/config-pr` receiver, its HMAC secret, the WebhookRegister
  boot node and the org-hook registration; the hive vhost's `/webhook/`
  location
- the 5-minute config-PR poll
- ApprovalKind::MergeConfigPr, its dashboard card, and the deploy DAG it
  drove (DeployWindow, MergeVerify, DeployApply, FinalizeDeploy,
  DeployTail), with verify_commit, the two-phase meta deploy, rollback
  refs, the PR-failure comment and forge/pr_merge.rs
- `fetched_sha`, `sha_short`/`pr_number` on approval events, and
  `sha`/`tag` on HelperEvent::ApprovalResolved: only the merge path set
  them

`config_repo`, `merged_pr_for_commit` and `post_pr_comment` move to
forge/pr_comment.rs for the merged-rev deploy's refusal comment.
Approvals v5 drops stored `merge_config_pr` rows; a test reopens a v4
database holding them.

Closes #4850
This commit is contained in:
atlas 2026-10-02 22:32:17 +02:00
commit efbfec6d01
39 changed files with 286 additions and 3199 deletions

View file

@ -1,103 +0,0 @@
//! Config-approval submit helper `submit_merge_config_pr`.
//!
//! `submit_merge_config_pr` is called from the dashboard webhook handler
//! (`dashboard::webhook`) — agents no longer need an MCP tool for config
//! changes; opening a config PR on `agent-configs/<agent>` is enough to
//! trigger hive-c0re's webhook-driven queue path.
use std::sync::Arc;
use crate::coordinator::Coordinator;
/// Submit-time half of the PR-merge flow: fetch the PR head sha from the
/// forge, queue the approval row, and emit the `approval_added` event so the
/// dashboard shows the pending card immediately.
///
/// The PR head sha is stored as `fetched_sha` on the approval row — the
/// "reviewed sha" the deploy's `MergeVerify` node drift-gates
/// against before doing anything irreversible. This does NOT fetch the commit
/// into the applied repo at submission time (that happens inside the approve
/// handler, step 2, after the drift check). No flake pre-flight either —
/// eval-verify happens at approval time too.
pub(crate) async fn submit_merge_config_pr(
coord: &Arc<Coordinator>,
agent: &str,
pr_number: u64,
description: Option<&str>,
submitter: &str,
) -> anyhow::Result<i64> {
let applied_dir = crate::paths::applied_dir(agent);
if !applied_dir.join(".git").exists() {
anyhow::bail!(
"applied repo missing for agent '{agent}' (expected at {}) — \
merge_config_pr requires the agent to be fully provisioned; \
create it first (`swarmctl agent create`) before opening config PRs",
applied_dir.display()
);
}
let repo = crate::forge::config_repo(agent);
// Verify the PR is still open before queueing an approval that would
// fail at approve time anyway (a closed/merged PR has no live head ref
// for the drift gate to compare against).
if !crate::forge::pr_is_open(&repo, pr_number)
.await
.map_err(|e| anyhow::anyhow!("check PR state for {agent} PR #{pr_number}: {e}"))?
{
anyhow::bail!(
"PR #{pr_number} on {repo} is closed or already merged — \
merge_config_pr requires an open PR"
);
}
// Fetch the current PR head sha — becomes the "reviewed" sha.
// Submitted together with the approval row (atomic single INSERT) so a
// crash mid-submit cannot leave a stranded sha-less row.
let sha = crate::forge::pr_head_sha(&repo, pr_number)
.await
.map_err(|e| anyhow::anyhow!("fetch PR head sha for {agent} PR #{pr_number}: {e}"))?;
// Both the webhook (`synchronize`) and the poll fallback call this on
// every PR update. If an approval for this PR is already pending, reconcile
// it against the live head sha rather than blindly queuing another:
// - same sha → the PR hasn't moved, so this is a duplicate signal — no-op.
// - drifted sha → the reviewed head is stale. Don't mutate the
// pending row in place (that races a concurrent approve); cancel it and
// fall through to queue a FRESH approval pinned to the new head.
if let Some((old_id, old_sha)) = coord.approvals.pending_merge_config_pr(agent, pr_number)? {
if old_sha.as_deref() == Some(sha.as_str()) {
return Ok(old_id);
}
let cancelled = coord
.approvals
.mark_cancelled(old_id, "config PR updated — superseded by a fresh approval")
.map_err(|e| anyhow::anyhow!("cancel superseded merge_config_pr approval: {e:#}"))?;
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
id: old_id,
agent,
approval_kind: "merge_config_pr",
sha_short: old_sha.map(|s| s[..s.len().min(12)].to_owned()),
status: "cancelled",
note: Some("PR head moved; superseded by a fresh approval".to_owned()),
description: cancelled.description,
});
}
let id = coord
.approvals
.submit_kind(
agent,
hive_sh4re::approvals::ApprovalKind::MergeConfigPr,
&pr_number.to_string(),
description,
submitter,
Some(&sha), // atomic: sha inserted with the row, not in a separate UPDATE
)
.map_err(|e| anyhow::anyhow!("queue merge_config_pr approval row: {e:#}"))?;
let sha_short = sha[..sha.len().min(12)].to_owned();
coord.emit_approval_added(crate::coordinator::ApprovalAdded {
id,
agent,
approval_kind: "merge_config_pr",
sha_short: Some(sha_short),
description: description.map(str::to_owned),
pr_number: Some(pr_number),
});
Ok(id)
}

View file

@ -21,14 +21,10 @@ use tokio::task::JoinHandle;
use crate::coordinator::Coordinator;
mod config_approvals;
mod schedules;
pub(crate) use config_approvals::submit_merge_config_pr;
pub(crate) use schedules::filter_ghost_schedule_targets;
pub use schedules::schedule_to_wire_public;
#[cfg(test)]
pub(crate) use schedules::tests::coordinator;
use schedules::{
EditSchedulePatch, handle_cancel_schedule, handle_edit_schedule, handle_fire_schedule_now,
@ -735,15 +731,10 @@ fn handle_cancel_loose_end(
.mark_cancelled(id, canceller)
.map_err(|e| format!("{e:#}"))?;
tracing::info!(%id, %canceller, agent = %approval.agent, "approval cancelled");
let sha_short = approval
.fetched_sha
.as_deref()
.map(|s| s[..s.len().min(12)].to_owned());
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
id: approval.id,
agent: approval.agent.as_str(),
approval_kind: <&str>::from(approval.kind),
sha_short,
status: "cancelled",
note: approval.note,
description: approval.description,

View file

@ -75,7 +75,6 @@ pub(super) fn handle_request_schedule_prompt(
&commit_ref,
payload.description.as_deref(),
requester,
None,
) {
Ok(id) => id,
Err(e) => {
@ -96,9 +95,7 @@ pub(super) fn handle_request_schedule_prompt(
id,
agent: requester,
approval_kind: "schedule_prompt",
sha_short: None,
description: payload.description.clone(),
pr_number: None,
});
Response::Ok
}