Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5420a31fd | ||
|
|
96e41c807d | ||
|
|
ef958983e0 | ||
|
|
0b86295776 | ||
|
|
5f5626456a | ||
|
|
449e3fcb7b |
7 changed files with 410 additions and 84 deletions
|
|
@ -90,7 +90,7 @@ initial configuration before any container is created.
|
||||||
|
|
||||||
### Approval kinds (wire shapes)
|
### Approval kinds (wire shapes)
|
||||||
|
|
||||||
`ApprovalKind` carries five variants; each maps to a different
|
`ApprovalKind` carries six variants; each maps to a different
|
||||||
`commit_ref` encoding because that field is overloaded as the
|
`commit_ref` encoding because that field is overloaded as the
|
||||||
kind-specific payload carrier.
|
kind-specific payload carrier.
|
||||||
|
|
||||||
|
|
@ -99,6 +99,16 @@ kind-specific payload carrier.
|
||||||
proposal fetch lives in `fetched_sha` on the same `Approval`
|
proposal fetch lives in `fetched_sha` on the same `Approval`
|
||||||
row (only `ApplyCommit` populates it). See the End-to-end flow
|
row (only `ApplyCommit` populates it). See the End-to-end flow
|
||||||
above.
|
above.
|
||||||
|
- `MergeConfigPr` — the PR-based config flow's counterpart to
|
||||||
|
`ApplyCommit`. `commit_ref` stores the **PR number** (decimal),
|
||||||
|
and `fetched_sha` is the PR **head sha the operator reviewed**.
|
||||||
|
On approve, `run_merge_config_pr` re-reads the live PR head and
|
||||||
|
aborts if it drifted from `fetched_sha` (re-review), then fetches
|
||||||
|
that head into the applied repo, eval-verifies it, fast-forwards
|
||||||
|
the forge config repo's `main` to it (the merge), marks the PR
|
||||||
|
merged (best-effort — `main` is already there), and runs the same
|
||||||
|
shared deploy tail as `ApplyCommit` (`deploy_applied_target`).
|
||||||
|
Never a first spawn.
|
||||||
- `Spawn` — direct container creation under the default
|
- `Spawn` — direct container creation under the default
|
||||||
`agent.nix` template. `commit_ref` is empty. Submitted via
|
`agent.nix` template. `commit_ref` is empty. Submitted via
|
||||||
`HostRequest::RequestSpawn` (operator-gated, the
|
`HostRequest::RequestSpawn` (operator-gated, the
|
||||||
|
|
@ -323,6 +333,7 @@ the approval handler enqueues a `QueueEntry` into the global
|
||||||
| `ApprovalKind` | `QueueKind` queued | `QueueSource` |
|
| `ApprovalKind` | `QueueKind` queued | `QueueSource` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ApplyCommit` | `Rebuild` | `Approval` |
|
| `ApplyCommit` | `Rebuild` | `Approval` |
|
||||||
|
| `MergeConfigPr` | `Rebuild` | `Approval` |
|
||||||
| `UpdateMetaInputs` | `MetaUpdate` | `Approval` |
|
| `UpdateMetaInputs` | `MetaUpdate` | `Approval` |
|
||||||
| `Spawn` | `Spawn` | `Approval` |
|
| `Spawn` | `Spawn` | `Approval` |
|
||||||
| `InitConfig` | — runs inline (sub-second git seed) | — |
|
| `InitConfig` | — runs inline (sub-second git seed) | — |
|
||||||
|
|
@ -331,7 +342,8 @@ the approval handler enqueues a `QueueEntry` into the global
|
||||||
Each queue entry carries the originating `approval_id` so the
|
Each queue entry carries the originating `approval_id` so the
|
||||||
worker can re-fetch the approval row when it dispatches, run the
|
worker can re-fetch the approval row when it dispatches, run the
|
||||||
kind-specific pipeline (`run_approval_apply_commit` /
|
kind-specific pipeline (`run_approval_apply_commit` /
|
||||||
`run_approval_update_meta_inputs` / `run_approval_spawn`), and
|
`run_approval_merge_config_pr` / `run_approval_update_meta_inputs` /
|
||||||
|
`run_approval_spawn`), and
|
||||||
fire the matching `HelperEvent::*` on completion via
|
fire the matching `HelperEvent::*` on completion via
|
||||||
`finish_approval`.
|
`finish_approval`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,20 +46,12 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await
|
run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await
|
||||||
}
|
}
|
||||||
ApprovalKind::ApplyCommit => {
|
ApprovalKind::ApplyCommit => {
|
||||||
coord
|
enqueue_approval_rebuild(
|
||||||
.rebuild_queue
|
&coord,
|
||||||
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
&approval.agent,
|
||||||
kind: crate::rebuild_queue::QueueKind::Rebuild,
|
id,
|
||||||
agent: approval.agent.clone(),
|
format!("approval #{id} apply commit"),
|
||||||
source: crate::rebuild_queue::QueueSource::Approval,
|
);
|
||||||
reason: format!("approval #{id} apply commit"),
|
|
||||||
parent_id: None,
|
|
||||||
inputs: Vec::new(),
|
|
||||||
approval_id: Some(id),
|
|
||||||
perm_payload: None,
|
|
||||||
depends_on: Vec::new(),
|
|
||||||
});
|
|
||||||
coord.emit_rebuild_queue_snapshot();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
ApprovalKind::UpdateMetaInputs => {
|
ApprovalKind::UpdateMetaInputs => {
|
||||||
|
|
@ -128,9 +120,49 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
ApprovalKind::MergeConfigPr => {
|
||||||
|
// Like ApplyCommit, the work ends in a container rebuild, so
|
||||||
|
// route it through the rebuild queue. The queue worker
|
||||||
|
// dispatches MergeConfigPr approvals to `run_merge_config_pr`
|
||||||
|
// (verify the reviewed PR head, ff the forge config repo's
|
||||||
|
// main to it, mark merged, then the shared deploy tail).
|
||||||
|
enqueue_approval_rebuild(
|
||||||
|
&coord,
|
||||||
|
&approval.agent,
|
||||||
|
id,
|
||||||
|
format!("approval #{id} merge config pr"),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enqueue a `Rebuild` queue entry tied to an approval id. Shared by the
|
||||||
|
/// `ApplyCommit` and `MergeConfigPr` dispatch arms — both end in a container
|
||||||
|
/// rebuild routed through the queue, differing only in the queue `reason`.
|
||||||
|
/// The queue worker branches on the approval's kind to pick the right handler.
|
||||||
|
fn enqueue_approval_rebuild(
|
||||||
|
coord: &Arc<Coordinator>,
|
||||||
|
agent: &str,
|
||||||
|
approval_id: i64,
|
||||||
|
reason: String,
|
||||||
|
) {
|
||||||
|
coord
|
||||||
|
.rebuild_queue
|
||||||
|
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
||||||
|
kind: crate::rebuild_queue::QueueKind::Rebuild,
|
||||||
|
agent: agent.to_owned(),
|
||||||
|
source: crate::rebuild_queue::QueueSource::Approval,
|
||||||
|
reason,
|
||||||
|
parent_id: None,
|
||||||
|
inputs: Vec::new(),
|
||||||
|
approval_id: Some(approval_id),
|
||||||
|
perm_payload: None,
|
||||||
|
depends_on: Vec::new(),
|
||||||
|
});
|
||||||
|
coord.emit_rebuild_queue_snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
/// Worker entry point for `ApprovalKind::ApplyCommit` queue entries.
|
/// Worker entry point for `ApprovalKind::ApplyCommit` queue entries.
|
||||||
/// Re-fetches the approval row, runs the commit pipeline, and fires
|
/// Re-fetches the approval row, runs the commit pipeline, and fires
|
||||||
/// `ApprovalResolved` + the lifecycle event (`Rebuilt` / `Spawned`
|
/// `ApprovalResolved` + the lifecycle event (`Rebuilt` / `Spawned`
|
||||||
|
|
@ -160,6 +192,159 @@ pub async fn run_approval_apply_commit(
|
||||||
finish_approval(coord, &approval, result, terminal_tag, is_first_spawn)
|
finish_approval(coord, &approval, result, terminal_tag, is_first_spawn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Worker entry point for `ApprovalKind::MergeConfigPr` queue entries —
|
||||||
|
/// the PR-based config flow's counterpart to `run_approval_apply_commit`.
|
||||||
|
/// Re-fetches the approval row, runs the merge pipeline, and fires
|
||||||
|
/// `ApprovalResolved` + the `Rebuilt` lifecycle event via `finish_approval`.
|
||||||
|
/// Unlike the apply-commit path it does NOT call `push_config` afterwards:
|
||||||
|
/// `run_merge_config_pr` already fast-forwarded the forge repo's `main` to
|
||||||
|
/// the reviewed head (that IS the merge), so a mirror push would be a no-op.
|
||||||
|
/// A `MergeConfigPr` is never a first spawn (the agent already exists).
|
||||||
|
pub async fn run_approval_merge_config_pr(
|
||||||
|
coord: &Arc<Coordinator>,
|
||||||
|
queue_entry_id: Option<u64>,
|
||||||
|
approval_id: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::MergeConfigPr)?;
|
||||||
|
let agent_dir = coord.ensure_runtime(&approval.agent)?;
|
||||||
|
let applied_dir = Coordinator::agent_applied_dir(&approval.agent);
|
||||||
|
coord.set_queue_step(queue_entry_id, "merge config pr");
|
||||||
|
let (result, terminal_tag) =
|
||||||
|
run_merge_config_pr(coord, &approval, &agent_dir, &applied_dir, queue_entry_id).await;
|
||||||
|
finish_approval(coord, &approval, result, terminal_tag, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PR-merge config pipeline. `approval.commit_ref` is the PR number;
|
||||||
|
/// `approval.fetched_sha` is the PR head sha the operator reviewed. Steps:
|
||||||
|
/// 1. drift gate — re-read the live PR head; if it moved since review, abort
|
||||||
|
/// WITHOUT mutating anything (the operator must re-review the new head);
|
||||||
|
/// 2. fetch the reviewed head into the applied repo so later git ops resolve
|
||||||
|
/// it locally;
|
||||||
|
/// 3. eval-verify the reviewed commit against the meta flake BEFORE the
|
||||||
|
/// irreversible push (same gate `run_apply_commit` uses);
|
||||||
|
/// 4. fast-forward the forge repo's `main` to the reviewed head — THE merge;
|
||||||
|
/// 5. mark the PR merged (best-effort: `main` is already at the head, so a
|
||||||
|
/// failure here is logged, not fatal);
|
||||||
|
/// 6. run the shared deploy tail (`deploy_applied_target`): ff applied/main,
|
||||||
|
/// meta deploy, container rebuild, finalize/rollback.
|
||||||
|
///
|
||||||
|
/// Returns `(build result, terminal tag)` like `deploy_applied_target`. Any
|
||||||
|
/// pre-merge abort returns `Err` with no mutation; the operator re-reviews.
|
||||||
|
async fn run_merge_config_pr(
|
||||||
|
coord: &Arc<Coordinator>,
|
||||||
|
approval: &hive_sh4re::Approval,
|
||||||
|
agent_dir: &std::path::Path,
|
||||||
|
applied_dir: &std::path::Path,
|
||||||
|
queue_entry_id: Option<u64>,
|
||||||
|
) -> (Result<()>, Option<String>) {
|
||||||
|
let id = approval.id;
|
||||||
|
let pr: u64 = match approval.commit_ref.parse() {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"parse PR number from commit_ref {:?}: {e}",
|
||||||
|
approval.commit_ref
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let reviewed = match approval.fetched_sha.as_deref() {
|
||||||
|
Some(s) => s.to_owned(),
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"merge config pr approval {id} has no reviewed head sha"
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let repo = crate::forge::config_repo(&approval.agent);
|
||||||
|
|
||||||
|
// 1. Drift gate: the live PR head must still equal what was reviewed.
|
||||||
|
coord.set_queue_step(queue_entry_id, "verify PR head");
|
||||||
|
let head = match crate::forge::pr_head_sha(&repo, pr).await {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(e) => return (Err(anyhow::anyhow!("read PR #{pr} head: {e}")), None),
|
||||||
|
};
|
||||||
|
if head != reviewed {
|
||||||
|
return (
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"PR #{pr} head drifted since review (reviewed {reviewed}, now {head}); re-review before merging"
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch the reviewed head into applied so ff/verify/deploy resolve it.
|
||||||
|
coord.set_queue_step(queue_entry_id, "fetch PR head");
|
||||||
|
if let Err(e) = crate::forge::fetch_pr_head_into_applied(&repo, pr).await {
|
||||||
|
return (
|
||||||
|
Err(anyhow::anyhow!("fetch PR #{pr} head into applied: {e}")),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Eval-verify BEFORE the irreversible push (bad nix fails fast here).
|
||||||
|
coord.set_queue_step(queue_entry_id, "verify proposal (eval)");
|
||||||
|
if let Err(e) = crate::meta::verify_commit(&approval.agent, applied_dir, &reviewed).await {
|
||||||
|
return (
|
||||||
|
Err(anyhow::anyhow!("verify merge head {reviewed}: {e:#}")),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the currently-deployed sha for the deploy tail's rollback.
|
||||||
|
let prev_main_sha = match lifecycle::git_rev_parse(applied_dir, "refs/heads/main").await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return (Err(anyhow::anyhow!("read applied/main: {e:#}")), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. THE merge: fast-forward the forge repo's main to the reviewed head.
|
||||||
|
coord.set_queue_step(queue_entry_id, "fast-forward forge main");
|
||||||
|
match crate::forge::ff_push_to_main(&repo, &reviewed).await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(crate::forge::ForgeMergeError::NotFastForward { .. }) => {
|
||||||
|
return (
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"PR #{pr}: forge main raced ahead of reviewed {reviewed}; re-review before merging"
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => return (Err(anyhow::anyhow!("ff-push PR #{pr} to main: {e}")), None),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Mark the PR merged. Best-effort: main is already at the reviewed
|
||||||
|
// head, so the deploy is correct regardless — a failure here (incl.
|
||||||
|
// HeadDrift vs the forge PR record) is logged, not fatal.
|
||||||
|
coord.set_queue_step(queue_entry_id, "mark PR merged");
|
||||||
|
if let Err(e) = crate::forge::mark_pr_merged(&repo, pr, &reviewed).await {
|
||||||
|
tracing::warn!(
|
||||||
|
agent = %approval.agent, %id, %pr, error = ?e,
|
||||||
|
"mark PR merged failed; main already at reviewed head, continuing to deploy"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Shared deploy tail. target == finalize == the reviewed head;
|
||||||
|
// never a first spawn (the agent already exists).
|
||||||
|
deploy_applied_target(
|
||||||
|
coord,
|
||||||
|
&approval.agent,
|
||||||
|
agent_dir,
|
||||||
|
applied_dir,
|
||||||
|
&reviewed,
|
||||||
|
&reviewed,
|
||||||
|
id,
|
||||||
|
&prev_main_sha,
|
||||||
|
false,
|
||||||
|
queue_entry_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Inline (non-queued) handler for `ApprovalKind::SchedulePrompt`.
|
/// Inline (non-queued) handler for `ApprovalKind::SchedulePrompt`.
|
||||||
/// On approve, decode the `SchedulePromptPayload` JSON from the
|
/// On approve, decode the `SchedulePromptPayload` JSON from the
|
||||||
/// approval's `commit_ref`, insert a row into `scheduled_prompts`
|
/// approval's `commit_ref`, insert a row into `scheduled_prompts`
|
||||||
|
|
@ -381,6 +566,7 @@ fn finish_approval(
|
||||||
ApprovalKind::InitConfig => "init_config",
|
ApprovalKind::InitConfig => "init_config",
|
||||||
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
||||||
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
||||||
|
ApprovalKind::MergeConfigPr => "merge_config_pr",
|
||||||
};
|
};
|
||||||
let sha_short = approval
|
let sha_short = approval
|
||||||
.fetched_sha
|
.fetched_sha
|
||||||
|
|
@ -423,13 +609,19 @@ fn finish_approval(
|
||||||
sha: approval.fetched_sha.clone(),
|
sha: approval.fetched_sha.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ApprovalKind::ApplyCommit => coord.notify_manager(&HelperEvent::Rebuilt {
|
// MergeConfigPr ends in a container rebuild just like a
|
||||||
agent: approval.agent.clone(),
|
// non-first-spawn ApplyCommit, so both surface the same Rebuilt
|
||||||
ok,
|
// lifecycle event. (MergeConfigPr is never a first spawn — the
|
||||||
note,
|
// agent already exists — so it never hits the Spawned arm above.)
|
||||||
sha: approval.fetched_sha.clone(),
|
ApprovalKind::ApplyCommit | ApprovalKind::MergeConfigPr => {
|
||||||
tag: terminal_tag,
|
coord.notify_manager(&HelperEvent::Rebuilt {
|
||||||
}),
|
agent: approval.agent.clone(),
|
||||||
|
ok,
|
||||||
|
note,
|
||||||
|
sha: approval.fetched_sha.clone(),
|
||||||
|
tag: terminal_tag,
|
||||||
|
});
|
||||||
|
}
|
||||||
// UpdateMetaInputs / SchedulePrompt: ApprovalResolved already
|
// UpdateMetaInputs / SchedulePrompt: ApprovalResolved already
|
||||||
// carries the result. No separate lifecycle event needed.
|
// carries the result. No separate lifecycle event needed.
|
||||||
ApprovalKind::UpdateMetaInputs | ApprovalKind::SchedulePrompt => {}
|
ApprovalKind::UpdateMetaInputs | ApprovalKind::SchedulePrompt => {}
|
||||||
|
|
@ -446,12 +638,8 @@ fn finish_approval(
|
||||||
/// (`deployed/<id>`) or annotate `failed/<id>` with the build error
|
/// (`deployed/<id>`) or annotate `failed/<id>` with the build error
|
||||||
/// and reset the working tree back to the last known-good main. main
|
/// and reset the working tree back to the last known-good main. main
|
||||||
/// never advances on a failed build, so a crash-and-recover doesn't
|
/// never advances on a failed build, so a crash-and-recover doesn't
|
||||||
/// leave the agent pointing at a tree it can't evaluate.
|
/// leave the agent pointing at a tree it can't evaluate. The shared
|
||||||
#[allow(
|
/// ff/deploy/rebuild/finalize tail lives in `deploy_applied_target`.
|
||||||
clippy::too_many_lines,
|
|
||||||
reason = "one sequential build/tag/notify pipeline; splitting the steps \
|
|
||||||
across helpers would obscure the linear flow without shrinking it"
|
|
||||||
)]
|
|
||||||
async fn run_apply_commit(
|
async fn run_apply_commit(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
approval: &hive_sh4re::Approval,
|
approval: &hive_sh4re::Approval,
|
||||||
|
|
@ -542,28 +730,76 @@ async fn run_apply_commit(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast-forward applied/main to the proposal, run the meta deploy +
|
||||||
|
// container rebuild, and finalize/roll-back — the tail shared with the
|
||||||
|
// PR-merge flow. ApplyCommit's target == finalize sha source is
|
||||||
|
// `fetched_sha` (or the proposal ref when unset), matching the prior
|
||||||
|
// inline behavior exactly.
|
||||||
|
let (result, tag) = deploy_applied_target(
|
||||||
|
coord,
|
||||||
|
&approval.agent,
|
||||||
|
agent_dir,
|
||||||
|
applied_dir,
|
||||||
|
&proposal_ref,
|
||||||
|
approval.fetched_sha.as_deref().unwrap_or(&proposal_ref),
|
||||||
|
id,
|
||||||
|
&prev_main_sha,
|
||||||
|
is_first_spawn,
|
||||||
|
queue_entry_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
(result, tag, is_first_spawn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared deploy tail for config-applying approvals (`ApplyCommit` + the
|
||||||
|
/// PR-merge flow). Fast-forwards `applied/main` to `target_ref`, syncs the
|
||||||
|
/// working tree, runs the meta two-phase deploy + container rebuild, and
|
||||||
|
/// plants the `deployed/<tag_base>` / `failed/<tag_base>` bookkeeping tags.
|
||||||
|
/// On build failure it rolls `applied/main` back to `prev_main_sha` and aborts
|
||||||
|
/// the staged meta lock so the agent stays on its last-good tree. Returns the
|
||||||
|
/// build result + the terminal tag name.
|
||||||
|
///
|
||||||
|
/// Caller-specific bits stay OUT of here: the source fetch (proposal tag vs
|
||||||
|
/// forge fetch), the `approved/building` tags, `verify_commit`, and any forge
|
||||||
|
/// ff-push / mark-merged. `is_first_spawn` gates the one-time meta
|
||||||
|
/// `sync_agents` step (only `ApplyCommit`'s first spawn passes `true`;
|
||||||
|
/// the PR-merge flow always passes `false` — the agent already exists).
|
||||||
|
/// `finalize_sha` is the sha recorded by `meta::finalize_deploy`; `target_ref`
|
||||||
|
/// is what `applied/main` fast-forwards to (a proposal ref or a commit sha).
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "one sequential ff/deploy/rebuild/finalize pipeline shared by both \
|
||||||
|
config-apply callers; splitting it would obscure the linear flow"
|
||||||
|
)]
|
||||||
|
async fn deploy_applied_target(
|
||||||
|
coord: &Arc<Coordinator>,
|
||||||
|
agent: &str,
|
||||||
|
agent_dir: &std::path::Path,
|
||||||
|
applied_dir: &std::path::Path,
|
||||||
|
target_ref: &str,
|
||||||
|
finalize_sha: &str,
|
||||||
|
tag_base: i64,
|
||||||
|
prev_main_sha: &str,
|
||||||
|
is_first_spawn: bool,
|
||||||
|
queue_entry_id: Option<u64>,
|
||||||
|
) -> (Result<()>, Option<String>) {
|
||||||
|
let id = tag_base;
|
||||||
|
|
||||||
coord.set_queue_step(queue_entry_id, "fast-forward applied/main");
|
coord.set_queue_step(queue_entry_id, "fast-forward applied/main");
|
||||||
// Fast-forward applied/main to proposal/<id> + sync the working
|
// Fast-forward applied/main to target_ref + sync the working tree.
|
||||||
// tree. Meta input pins `?ref=main`, so this is what makes nix
|
// Meta input pins `?ref=main`, so this is what makes nix re-lock to
|
||||||
// re-lock to the proposal commit on the prepare_deploy step
|
// the target commit on the prepare_deploy step below. On build
|
||||||
// below. On build failure we roll main back to prev_main_sha so
|
// failure we roll main back to prev_main_sha so a crash leaves the
|
||||||
// a crash leaves the agent on its last-good tree.
|
// agent on its last-good tree.
|
||||||
if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await {
|
if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", target_ref).await {
|
||||||
return (
|
return (Err(anyhow::anyhow!("ff main to {target_ref}: {e:#}")), None);
|
||||||
Err(anyhow::anyhow!("ff main to {proposal_ref}: {e:#}")),
|
|
||||||
None,
|
|
||||||
is_first_spawn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if let Err(e) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
if let Err(e) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
||||||
// main is ahead; working tree didn't sync. Roll main back to
|
// main is ahead; working tree didn't sync. Roll main back to
|
||||||
// keep the two consistent before bailing.
|
// keep the two consistent before bailing.
|
||||||
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
return (
|
return (Err(anyhow::anyhow!("read-tree to main: {e:#}")), None);
|
||||||
Err(anyhow::anyhow!("read-tree to main: {e:#}")),
|
|
||||||
None,
|
|
||||||
is_first_spawn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First spawn: sync_agents must add this agent to the meta flake
|
// First spawn: sync_agents must add this agent to the meta flake
|
||||||
|
|
@ -571,40 +807,34 @@ async fn run_apply_commit(
|
||||||
// exist yet if this is the agent's first deploy).
|
// exist yet if this is the agent's first deploy).
|
||||||
if is_first_spawn {
|
if is_first_spawn {
|
||||||
coord.set_queue_step(queue_entry_id, "meta sync_agents (first spawn)");
|
coord.set_queue_step(queue_entry_id, "meta sync_agents (first spawn)");
|
||||||
let agents = match lifecycle::agents_for_meta_listing_with(&approval.agent).await {
|
let agents = match lifecycle::agents_for_meta_listing_with(agent).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ =
|
let _ =
|
||||||
lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
||||||
return (
|
return (
|
||||||
Err(anyhow::anyhow!("agents_for_meta_listing_with: {e:#}")),
|
Err(anyhow::anyhow!("agents_for_meta_listing_with: {e:#}")),
|
||||||
None,
|
None,
|
||||||
is_first_spawn,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = crate::meta::sync_agents(&coord.hive_env(), &agents).await {
|
if let Err(e) = crate::meta::sync_agents(&coord.hive_env(), &agents).await {
|
||||||
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
||||||
return (
|
return (
|
||||||
Err(anyhow::anyhow!("meta sync_agents for first spawn: {e:#}")),
|
Err(anyhow::anyhow!("meta sync_agents for first spawn: {e:#}")),
|
||||||
None,
|
None,
|
||||||
is_first_spawn,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coord.set_queue_step(queue_entry_id, "meta prepare_deploy");
|
coord.set_queue_step(queue_entry_id, "meta prepare_deploy");
|
||||||
// Phase 1 of the meta two-phase deploy: relock without committing.
|
// Phase 1 of the meta two-phase deploy: relock without committing.
|
||||||
if let Err(e) = crate::meta::prepare_deploy(&approval.agent).await {
|
if let Err(e) = crate::meta::prepare_deploy(agent).await {
|
||||||
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
||||||
return (
|
return (Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), None);
|
||||||
Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")),
|
|
||||||
None,
|
|
||||||
is_first_spawn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container-level rebuild (or first-time create) against meta#<name>.
|
// Container-level rebuild (or first-time create) against meta#<name>.
|
||||||
|
|
@ -612,9 +842,9 @@ async fn run_apply_commit(
|
||||||
// the dashboard reflects actual phase progress rather than a static
|
// the dashboard reflects actual phase progress rather than a static
|
||||||
// "nixos-container update" label for the whole multi-minute window.
|
// "nixos-container update" label for the whole multi-minute window.
|
||||||
let hive = coord.hive_env();
|
let hive = coord.hive_env();
|
||||||
let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf());
|
let paths = Coordinator::agent_paths(agent, agent_dir.to_path_buf());
|
||||||
let build_result = lifecycle::rebuild_no_meta(
|
let build_result = lifecycle::rebuild_no_meta(
|
||||||
&approval.agent,
|
agent,
|
||||||
&hive,
|
&hive,
|
||||||
&paths,
|
&paths,
|
||||||
&|step| coord.set_queue_step(queue_entry_id, step),
|
&|step| coord.set_queue_step(queue_entry_id, step),
|
||||||
|
|
@ -632,54 +862,47 @@ async fn run_apply_commit(
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
coord.set_queue_step(queue_entry_id, "finalize deploy");
|
coord.set_queue_step(queue_entry_id, "finalize deploy");
|
||||||
let tag = format!("deployed/{id}");
|
let tag = format!("deployed/{id}");
|
||||||
if let Err(e) = lifecycle::git_tag(applied_dir, &tag, &proposal_ref).await {
|
if let Err(e) = lifecycle::git_tag(applied_dir, &tag, target_ref).await {
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?e, "plant deployed tag failed");
|
tracing::warn!(%agent, %id, error = ?e, "plant deployed tag failed");
|
||||||
}
|
}
|
||||||
if let Err(e) = crate::meta::finalize_deploy(
|
if let Err(e) = crate::meta::finalize_deploy(agent, finalize_sha, &tag).await {
|
||||||
&approval.agent,
|
|
||||||
approval.fetched_sha.as_deref().unwrap_or(&proposal_ref),
|
|
||||||
&tag,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
// The build itself succeeded — meta lock landed but
|
// The build itself succeeded — meta lock landed but
|
||||||
// couldn't be committed. Surface as a soft warn so the
|
// couldn't be committed. Surface as a soft warn so the
|
||||||
// operator can git-commit by hand if they care.
|
// operator can git-commit by hand if they care.
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?e, "meta finalize_deploy failed");
|
tracing::warn!(%agent, %id, error = ?e, "meta finalize_deploy failed");
|
||||||
}
|
}
|
||||||
// Wake the agent on its next turn so claude sees the
|
// Wake the agent on its next turn so claude sees the
|
||||||
// config change took effect. Same hint pattern as
|
// config change took effect. Same hint pattern as
|
||||||
// auto_update::rebuild_agent — manager approved a
|
// auto_update::rebuild_agent — manager approved a
|
||||||
// proposal, agent picks up where it left off with the
|
// proposal, agent picks up where it left off with the
|
||||||
// new env / packages.
|
// new env / packages.
|
||||||
coord.kick_agent(&approval.agent, "config update applied");
|
coord.kick_agent(agent, "config update applied");
|
||||||
(Ok(()), Some(tag), is_first_spawn)
|
(Ok(()), Some(tag))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let tag = format!("failed/{id}");
|
let tag = format!("failed/{id}");
|
||||||
let body = format!("{e:#}");
|
let body = format!("{e:#}");
|
||||||
if let Err(te) =
|
if let Err(te) =
|
||||||
lifecycle::git_tag_annotated(applied_dir, &tag, &proposal_ref, &body).await
|
lifecycle::git_tag_annotated(applied_dir, &tag, target_ref, &body).await
|
||||||
{
|
{
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?te, "annotate failed tag failed");
|
tracing::warn!(%agent, %id, error = ?te, "annotate failed tag failed");
|
||||||
}
|
}
|
||||||
// Roll main back to last known-good so the on-disk state
|
// Roll main back to last known-good so the on-disk state
|
||||||
// matches what nixos-container last successfully built.
|
// matches what nixos-container last successfully built.
|
||||||
if let Err(re) =
|
if let Err(re) =
|
||||||
lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await
|
lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await
|
||||||
{
|
{
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?re, "main rollback failed");
|
tracing::warn!(%agent, %id, error = ?re, "main rollback failed");
|
||||||
}
|
}
|
||||||
if let Err(re) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
if let Err(re) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?re, "rollback read-tree failed");
|
tracing::warn!(%agent, %id, error = ?re, "rollback read-tree failed");
|
||||||
}
|
}
|
||||||
// Drop the staged meta lock change so the deploy log
|
// Drop the staged meta lock change so the deploy log
|
||||||
// only ever shows successes.
|
// only ever shows successes.
|
||||||
if let Err(ae) = crate::meta::abort_deploy().await {
|
if let Err(ae) = crate::meta::abort_deploy().await {
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?ae, "meta abort_deploy failed");
|
tracing::warn!(%agent, %id, error = ?ae, "meta abort_deploy failed");
|
||||||
}
|
}
|
||||||
let _ = coord;
|
(Err(e), Some(tag))
|
||||||
(Err(e), Some(tag), is_first_spawn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -809,6 +1032,7 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()
|
||||||
ApprovalKind::InitConfig => "init_config",
|
ApprovalKind::InitConfig => "init_config",
|
||||||
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
||||||
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
||||||
|
ApprovalKind::MergeConfigPr => "merge_config_pr",
|
||||||
};
|
};
|
||||||
let sha_short = sha.as_deref().map(|s| s[..s.len().min(12)].to_owned());
|
let sha_short = sha.as_deref().map(|s| s[..s.len().min(12)].to_owned());
|
||||||
let description = a.description.clone();
|
let description = a.description.clone();
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,7 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
|
||||||
"init_config" => ApprovalKind::InitConfig,
|
"init_config" => ApprovalKind::InitConfig,
|
||||||
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
||||||
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
||||||
|
"merge_config_pr" => ApprovalKind::MergeConfigPr,
|
||||||
other => {
|
other => {
|
||||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
return Err(rusqlite::Error::FromSqlConversionFailure(
|
||||||
2,
|
2,
|
||||||
|
|
@ -383,6 +384,7 @@ pub(crate) fn kind_to_str(kind: ApprovalKind) -> &'static str {
|
||||||
ApprovalKind::InitConfig => "init_config",
|
ApprovalKind::InitConfig => "init_config",
|
||||||
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
||||||
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
||||||
|
ApprovalKind::MergeConfigPr => "merge_config_pr",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,6 +395,7 @@ fn kind_from_str(s: &str) -> Result<ApprovalKind> {
|
||||||
"init_config" => ApprovalKind::InitConfig,
|
"init_config" => ApprovalKind::InitConfig,
|
||||||
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
||||||
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
||||||
|
"merge_config_pr" => ApprovalKind::MergeConfigPr,
|
||||||
other => bail!("unknown approval kind '{other}'"),
|
other => bail!("unknown approval kind '{other}'"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -870,6 +870,7 @@ fn history_view(a: Approval) -> ApprovalHistoryView {
|
||||||
hive_sh4re::ApprovalKind::InitConfig => "init_config",
|
hive_sh4re::ApprovalKind::InitConfig => "init_config",
|
||||||
hive_sh4re::ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
hive_sh4re::ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
||||||
hive_sh4re::ApprovalKind::SchedulePrompt => "schedule_prompt",
|
hive_sh4re::ApprovalKind::SchedulePrompt => "schedule_prompt",
|
||||||
|
hive_sh4re::ApprovalKind::MergeConfigPr => "merge_config_pr",
|
||||||
};
|
};
|
||||||
ApprovalHistoryView {
|
ApprovalHistoryView {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
|
|
@ -939,6 +940,24 @@ async fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
description: a.description,
|
description: a.description,
|
||||||
requested_at: a.requested_at,
|
requested_at: a.requested_at,
|
||||||
},
|
},
|
||||||
|
hive_sh4re::ApprovalKind::MergeConfigPr => {
|
||||||
|
// commit_ref = PR number; fetched_sha = the reviewed PR
|
||||||
|
// head. Show the head sha; the forge PR diff surface is
|
||||||
|
// a later phase of the PR-based config flow — None for now.
|
||||||
|
let sha = a
|
||||||
|
.fetched_sha
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| s[..s.len().min(12)].to_owned());
|
||||||
|
ApprovalView {
|
||||||
|
id: a.id,
|
||||||
|
agent: a.agent,
|
||||||
|
kind: "merge_config_pr",
|
||||||
|
sha_short: sha,
|
||||||
|
diff: None,
|
||||||
|
description: a.description,
|
||||||
|
requested_at: a.requested_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
|
||||||
|
|
@ -1225,6 +1225,43 @@ pub async fn pr_head_sha(repo: &str, pr: u64) -> Result<String, ForgeMergeError>
|
||||||
Ok(sha.to_string())
|
Ok(sha.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full `owner/name` path of an agent's config repo on the forge — the
|
||||||
|
/// `agent-configs` org mirror that the PR-merge flow reads + fast-forwards.
|
||||||
|
pub fn config_repo(agent: &str) -> String {
|
||||||
|
format!("{CONFIG_ORG}/{agent}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch PR #`pr`'s head into the agent's applied repo via
|
||||||
|
/// `refs/pull/<pr>/head` (which Forgejo always serves — a bare-sha fetch can
|
||||||
|
/// be refused by uploadpack policy). This makes the reviewed head an object
|
||||||
|
/// in the applied repo so the ancestor check in [`ff_push_to_main`], the
|
||||||
|
/// `git_update_ref(main, …)` in the deploy tail, and the eval-verify all
|
||||||
|
/// resolve it locally before the irreversible push.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// `Other` on transport failure or a non-zero git exit.
|
||||||
|
pub async fn fetch_pr_head_into_applied(repo: &str, pr: u64) -> Result<(), ForgeMergeError> {
|
||||||
|
let token = core_token()
|
||||||
|
.ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?;
|
||||||
|
let url = tokenised_repo_url(repo, &token);
|
||||||
|
let applied = Coordinator::agent_applied_dir(repo_agent_name(repo));
|
||||||
|
let refspec = format!("refs/pull/{pr}/head");
|
||||||
|
let out = crate::lifecycle::git_command()
|
||||||
|
.current_dir(&applied)
|
||||||
|
.args(["fetch", "--no-tags", &url, &refspec])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("git fetch PR head into applied")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return Err(ForgeMergeError::Other(anyhow::anyhow!(
|
||||||
|
"git fetch {repo} {refspec} into applied failed ({}): {}",
|
||||||
|
out.status,
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Fast-forward the forge repo's `main` to `sha` — THE merge in the PR flow.
|
/// Fast-forward the forge repo's `main` to `sha` — THE merge in the PR flow.
|
||||||
/// Reads `main`'s current sha (`git ls-remote … refs/heads/main`), verifies it
|
/// Reads `main`'s current sha (`git ls-remote … refs/heads/main`), verifies it
|
||||||
/// is a strict ancestor of `sha` (`git merge-base --is-ancestor`, run in the
|
/// is a strict ancestor of `sha` (`git merge-base --is-ancestor`, run in the
|
||||||
|
|
|
||||||
|
|
@ -852,13 +852,36 @@ pub async fn run_fast_worker(coord: std::sync::Arc<crate::coordinator::Coordinat
|
||||||
/// `actions::run_approval_*` which carry the kind-specific commit
|
/// `actions::run_approval_*` which carry the kind-specific commit
|
||||||
/// pipeline + the `ApprovalResolved` event fan-out. Non-approval
|
/// pipeline + the `ApprovalResolved` event fan-out. Non-approval
|
||||||
/// entries hit the original auto/manual rebuild paths.
|
/// entries hit the original auto/manual rebuild paths.
|
||||||
|
/// Pick the right approval pipeline for a `Rebuild` queue entry. Both
|
||||||
|
/// `ApplyCommit` and `MergeConfigPr` approvals enqueue a `Rebuild` entry
|
||||||
|
/// (both end in a container rebuild); branch on the approval kind. Falls
|
||||||
|
/// back to the apply-commit path if the row can't be read — it re-fetches
|
||||||
|
/// + surfaces a clean error itself.
|
||||||
|
async fn dispatch_rebuild_approval(
|
||||||
|
coord: &std::sync::Arc<crate::coordinator::Coordinator>,
|
||||||
|
entry: &QueueEntry,
|
||||||
|
approval_id: i64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let kind = coord
|
||||||
|
.approvals
|
||||||
|
.get(approval_id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|a| a.kind);
|
||||||
|
if kind == Some(hive_sh4re::ApprovalKind::MergeConfigPr) {
|
||||||
|
crate::actions::run_approval_merge_config_pr(coord, Some(entry.id), approval_id).await
|
||||||
|
} else {
|
||||||
|
crate::actions::run_approval_apply_commit(coord, Some(entry.id), approval_id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn dispatch(
|
async fn dispatch(
|
||||||
coord: &std::sync::Arc<crate::coordinator::Coordinator>,
|
coord: &std::sync::Arc<crate::coordinator::Coordinator>,
|
||||||
entry: &QueueEntry,
|
entry: &QueueEntry,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match (entry.kind, entry.approval_id) {
|
match (entry.kind, entry.approval_id) {
|
||||||
(QueueKind::Rebuild, Some(approval_id)) => {
|
(QueueKind::Rebuild, Some(approval_id)) => {
|
||||||
crate::actions::run_approval_apply_commit(coord, Some(entry.id), approval_id).await
|
dispatch_rebuild_approval(coord, entry, approval_id).await
|
||||||
}
|
}
|
||||||
(QueueKind::Rebuild, None) => {
|
(QueueKind::Rebuild, None) => {
|
||||||
let current_rev =
|
let current_rev =
|
||||||
|
|
|
||||||
|
|
@ -154,10 +154,12 @@ pub struct Approval {
|
||||||
/// Kind-specific payload (git sha / inputs array / schedule
|
/// Kind-specific payload (git sha / inputs array / schedule
|
||||||
/// payload / empty). See the Approval struct doc.
|
/// payload / empty). See the Approval struct doc.
|
||||||
pub commit_ref: String,
|
pub commit_ref: String,
|
||||||
/// `ApplyCommit` only: the canonical hive-c0re-vouched sha after
|
/// The canonical hive-c0re-vouched sha. For `ApplyCommit`: the sha
|
||||||
/// the proposal fetch, tagged `proposal/<id>`. Stable for the
|
/// after the proposal fetch, tagged `proposal/<id>` (stable for the
|
||||||
/// approval's lifetime — manager amends in proposed don't change
|
/// approval's lifetime — manager amends in proposed don't change
|
||||||
/// what gets built.
|
/// what gets built). For `MergeConfigPr`: the reviewed PR head
|
||||||
|
/// pinned at submit; if the PR head drifts off it before merge,
|
||||||
|
/// hive-c0re refreshes this + re-renders the card for re-review.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub fetched_sha: Option<String>,
|
pub fetched_sha: Option<String>,
|
||||||
pub requested_at: i64,
|
pub requested_at: i64,
|
||||||
|
|
@ -192,6 +194,12 @@ pub enum ApprovalKind {
|
||||||
UpdateMetaInputs,
|
UpdateMetaInputs,
|
||||||
/// Add a scheduled prompt to the broker queue.
|
/// Add a scheduled prompt to the broker queue.
|
||||||
SchedulePrompt,
|
SchedulePrompt,
|
||||||
|
/// Merge an operator-reviewed config PR: hive-c0re verifies the
|
||||||
|
/// reviewed PR head, fast-forwards the forge config repo's `main`
|
||||||
|
/// to it, marks the PR merged, then runs the same deploy tail as
|
||||||
|
/// `ApplyCommit`. `commit_ref` = PR number; `fetched_sha` = the
|
||||||
|
/// reviewed PR head pinned at submit. See `docs/approvals.md`.
|
||||||
|
MergeConfigPr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue