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:
parent
a5ea015bc6
commit
efbfec6d01
39 changed files with 286 additions and 3199 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue