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
|
|
@ -1203,19 +1203,15 @@ fn cancelled_power_op_runs_no_compensating_node() {
|
|||
fn cancelled_dag_still_runs_its_approval_tail() {
|
||||
let q = JobQueue::new(1);
|
||||
insert(&q, |builder| {
|
||||
templates::approval_deploy(builder, "agent-a", 7);
|
||||
templates::meta_update(builder, Vec::new(), Some(7));
|
||||
});
|
||||
// Found by kind, not returned: `approval_deploy` deliberately names
|
||||
// nothing, because nothing polls it — the approval row is how an operator
|
||||
// follows a deploy, so the template is fire-and-forget in production and a
|
||||
// test must not make it return an id it would otherwise have no use for.
|
||||
let window = node_of(&q, "deploy_window").get();
|
||||
assert!(q.cancel(window), "fully-queued dag cancels");
|
||||
// Found by kind, not returned: `meta_update` names nothing, because the
|
||||
// approval row is how an operator follows it.
|
||||
let lock = node_of(&q, "meta_lock").get();
|
||||
assert!(q.cancel(lock), "fully-queued dag cancels");
|
||||
// The `Cancelled` tail is the only node whose edge accepts a dropped
|
||||
// dependency, so it is the only one `cancel` spares — and *which* tail
|
||||
// survives is the whole assertion: the template emits one per outcome and
|
||||
// the spared one names how the approval row is about to be resolved.
|
||||
// Nothing computes it, so reading the survivor is reading the answer.
|
||||
// survives names how the approval row is about to be resolved.
|
||||
let spared = pending_payloads(&q);
|
||||
assert!(
|
||||
matches!(
|
||||
|
|
@ -1227,171 +1223,9 @@ fn cancelled_dag_still_runs_its_approval_tail() {
|
|||
),
|
||||
"only the cancelled-outcome tail is spared, got {spared:?}"
|
||||
);
|
||||
// ⚠️ `Cancelled`, and it reads terminal **while the spared tail is still
|
||||
// pending** — the one place this differs from the container era, where the
|
||||
// cancel landed on a node *above* the window and the window rolled up
|
||||
// `Finishing`. Here the operator cancels the window itself, so its own
|
||||
// state is `Cancelled` however its subtree is doing. Deliberately asserted
|
||||
// rather than routed around: the group's card goes terminal while a
|
||||
// bookkeeping node runs on. That is acceptable for the tail this test
|
||||
// protects (it resolves the approval row and nothing waits on it), and it
|
||||
// would not be for work an operator expects to still be watching.
|
||||
assert_eq!(state_of(&q, window), State::Cancelled);
|
||||
// An unrelated group landing in the same graph doesn't disturb this one's
|
||||
// state — a root rolls up its own subtree, not the graph.
|
||||
let _other = insert_named(&q, |builder| rebuild(builder, "agent-b"));
|
||||
assert_eq!(state_of(&q, window), State::Cancelled);
|
||||
assert_eq!(state_of(&q, lock), State::Cancelled);
|
||||
}
|
||||
|
||||
// ---- approval deploy subtree ----
|
||||
|
||||
/// The config-PR deploy is a subtree, not one opaque node. The
|
||||
/// resource-holding root completes immediately (its `Finishing` state is the
|
||||
/// parent gate that releases the children), then the phases run strictly in
|
||||
/// order — and the `AfterAny` tail still runs when the irreversible half fails,
|
||||
/// because it's the node that compensates for it.
|
||||
#[test]
|
||||
fn deploy_dag_runs_phases_in_order_and_tails_a_failed_apply() {
|
||||
let q = JobQueue::new(1);
|
||||
insert(&q, |builder| {
|
||||
templates::approval_deploy(builder, "agent-a", 7);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
declared_shape(&q),
|
||||
vec![
|
||||
// The window is the group root and holds the meta window for the
|
||||
// whole subtree; the three phases are its sub-nodes.
|
||||
row("deploy_window", None, &[]),
|
||||
row("merge_verify", Some("deploy_window"), &[]),
|
||||
// Apply only on a clean verify — a failed verify cancel-cascades
|
||||
// it, which is what leaves the forge and the applied repo untouched.
|
||||
row(
|
||||
"deploy_apply",
|
||||
Some("deploy_window"),
|
||||
&[("merge_verify", "done")]
|
||||
),
|
||||
// The compensation tail accepts every terminal outcome of apply,
|
||||
// *including `skipped`* — which is the state apply lands in when
|
||||
// verify failed and it never ran. That one edge is the entire
|
||||
// "still tails a failed apply / a failed verify" behaviour, and it
|
||||
// is why two separate DAG-driving tests collapsed into this table.
|
||||
row(
|
||||
"deploy_tail",
|
||||
Some("deploy_window"),
|
||||
&[("deploy_apply", "done|failed|skipped")]
|
||||
),
|
||||
// One approval tail per outcome, gated on the window's roll-up.
|
||||
row("resolve_approval", None, &[("deploy_window", "done")]),
|
||||
row("resolve_approval", None, &[("deploy_window", "failed")]),
|
||||
row("resolve_approval", None, &[("deploy_window", "cancelled")]),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// The deploy's happy path: `DeployApply` does not build. It grows the ordinary
|
||||
/// rebuild chain into the live DAG under itself, and `FinalizeDeploy` — gated on
|
||||
/// that graft finishing — plants the deploy tag last.
|
||||
///
|
||||
/// The queue is built with **one** build slot on purpose. `DeployWindow` already
|
||||
/// holds that slot (and the meta window) for the whole subtree, so the grafted
|
||||
/// `Prebuild` can only ever claim by *re-entering* its ancestor's hold. If the
|
||||
/// graft were rooted anywhere outside `DeployWindow`'s subtree it would block on
|
||||
/// a resource its own DAG owns and deadlock — this test is what pins that down.
|
||||
#[test]
|
||||
fn deploy_apply_grows_rebuild_subgraph_and_finalizes_after_it() {
|
||||
// What `DeployApply` grows is `deploy_rebuild_nodes`' output, and that is a
|
||||
// pure declaration — so it is declared here directly rather than by running
|
||||
// a deploy far enough to graft it. **Reproducing the runtime path is not
|
||||
// needed to test what the runtime path declares.**
|
||||
//
|
||||
// The grafting mechanism itself is hive_jobq's and tested there: the work
|
||||
// lands under the emitter *before* it settles, and the emitter parks in
|
||||
// `Finishing` so a downstream `AfterAny` gate stays shut while the new
|
||||
// children run (`a_completing_node_grows_the_work_it_declared`,
|
||||
// `parent_parks_in_finishing_until_children_roll_up`).
|
||||
let q = JobQueue::new(1);
|
||||
insert(&q, |builder| {
|
||||
templates::deploy_rebuild_nodes(builder, "agent-a", 11);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
declared_shape(&q),
|
||||
vec![
|
||||
row("meta_sync", None, &[]),
|
||||
row("agent_window", None, &[("meta_sync", "done")]),
|
||||
row("prebuild", Some("agent_window"), &[]),
|
||||
row(
|
||||
"stop_for_update",
|
||||
Some("agent_window"),
|
||||
&[("prebuild", "done")]
|
||||
),
|
||||
row("swap", Some("stop_for_update"), &[]),
|
||||
row(
|
||||
"rebuild_bookkeeping",
|
||||
Some("stop_for_update"),
|
||||
&[("swap", "done")]
|
||||
),
|
||||
row(
|
||||
"reconcile",
|
||||
None,
|
||||
&[("agent_window", "done|failed|skipped")]
|
||||
),
|
||||
// The deploy tag is planted only after the rebuild came up clean:
|
||||
// `AfterOk` on **both** roots, so either one failing skips it. That
|
||||
// pair of edges is the whole "skips finalize on a failed graft"
|
||||
// behaviour — no run needed to see it.
|
||||
row(
|
||||
"finalize_deploy",
|
||||
None,
|
||||
&[("agent_window", "done"), ("reconcile", "done")]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// `deploy_dag_skips_finalize_but_still_tails_a_failed_graft` lived here.
|
||||
//
|
||||
// A failure *inside* the grafted rebuild is the failure mode subgraph growth
|
||||
// introduces: the deploy is already merged and the container half-swapped, so
|
||||
// `FinalizeDeploy` must be cancel-cascaded (no `deployed/<id>` tag planted)
|
||||
// while the tail still runs to compensate, and `Reconcile` is deliberately
|
||||
// still reached — it boots the container back up.
|
||||
//
|
||||
// Every declared half of that is asserted by
|
||||
// `deploy_apply_grows_rebuild_subgraph_and_finalizes_after_it`, which reads
|
||||
// `deploy_rebuild_nodes`' shape directly:
|
||||
//
|
||||
// - "a failed swap still reaches Reconcile" is the `reconcile` row's
|
||||
// `AfterAny` edge on `prebuild` (`done|failed|skipped`);
|
||||
// - "finalize is cancel-cascaded" is `finalize_deploy`'s `AfterOk` pair —
|
||||
// either root failing skips it;
|
||||
// - "the tail still runs" is `deploy_tail`'s own `done|failed|skipped` edge
|
||||
// on apply, asserted in the `approval_deploy` table above.
|
||||
//
|
||||
// The runtime halves are hive_jobq's: cascade on failure, roll-up, and
|
||||
// `first_error` digging past a group root that rolled up `Failed` while
|
||||
// carrying no error of its own (`first_error_skips_a_rolled_up_failure_
|
||||
// carrying_no_error`). That last one is why the DAG reports "profile swap
|
||||
// failed" rather than nothing — the mechanism, not this shape.
|
||||
|
||||
// `deploy_dag_skips_apply_but_still_runs_tail_when_verify_fails` lived here.
|
||||
//
|
||||
// A pre-merge rejection (drift gate, eval failure) cancel-cascades the
|
||||
// irreversible half via its `AfterOk` edge, while the tail is still reached —
|
||||
// it owns the forge mirror, not just compensation. That test and
|
||||
// `deploy_dag_runs_phases_in_order_and_tails_a_failed_apply` differed only in
|
||||
// *where* they injected the failure, and each drove the whole DAG to watch the
|
||||
// tail run anyway.
|
||||
//
|
||||
// Both outcomes follow from one declared edge, which the surviving test now
|
||||
// asserts directly: the tail accepts `done|failed|skipped` on apply, and
|
||||
// `skipped` is exactly the state apply lands in when verify failed and it never
|
||||
// ran. The runtime halves are hive_jobq's and tested there —
|
||||
// `failed_after_ok_dep_cancels_dependents_but_after_any_still_runs` and
|
||||
// `failed_child_rolls_parent_up_to_failed` (an Ok tail cannot launder a failed
|
||||
// deploy into a success).
|
||||
|
||||
// ---- history ----
|
||||
//
|
||||
// The node → build-log link is no longer queue state: the log row carries
|
||||
|
|
|
|||
Loading…
Reference in a new issue