refactor(#2416): remove the non-pr config-change flow (request_apply_commit / applycommit)
This commit is contained in:
parent
fbbd5d921c
commit
c2bd7db998
34 changed files with 293 additions and 1635 deletions
|
|
@ -1,13 +1,8 @@
|
|||
//! Approval endpoints + diff machinery for the dashboard.
|
||||
//! Approval endpoints for the dashboard.
|
||||
//!
|
||||
//! Approve/deny actions, the orphan-approval GC sweep used by the
|
||||
//! `/api/state` builder, and the unified-diff endpoints (on-demand
|
||||
//! `/api/approval-diff/{id}` against a chosen base, plus the `pub(crate)`
|
||||
//! `approval_diff` the manager-socket handler pre-computes at submit time).
|
||||
//! Approve/deny actions plus the orphan-approval GC sweep used by the
|
||||
//! `/api/state` builder.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
extract::{Form, Path as AxumPath, State},
|
||||
http::StatusCode,
|
||||
|
|
@ -16,12 +11,9 @@ use axum::{
|
|||
use hive_sh4re::Approval;
|
||||
use serde::Deserialize;
|
||||
|
||||
use problem_details::ProblemDetails;
|
||||
|
||||
use super::{AppState, error_problem, error_response};
|
||||
use super::{AppState, error_response};
|
||||
use crate::actions;
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle;
|
||||
|
||||
pub(super) async fn post_approve(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -53,7 +45,7 @@ pub(super) async fn post_deny(
|
|||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
match actions::deny(&state.coord, id, note).await {
|
||||
match actions::deny(&state.coord, id, note) {
|
||||
Ok(()) => (StatusCode::OK, "ok").into_response(),
|
||||
Err(e) => error_response(&format!("deny {id} failed: {e:#}")),
|
||||
}
|
||||
|
|
@ -87,7 +79,7 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
|
|||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id: a.id,
|
||||
agent: &a.agent,
|
||||
approval_kind: "apply_commit",
|
||||
approval_kind: a.kind.as_str(),
|
||||
sha_short,
|
||||
status: "failed",
|
||||
note: Some(note.to_owned()),
|
||||
|
|
@ -98,142 +90,3 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Multi-file unified diff between the currently-deployed tree and
|
||||
/// the proposal for this approval. Runs against the applied repo
|
||||
/// since the canonical proposal commit lives there (manager-side
|
||||
/// amendments don't move it). Empty output means proposal == main —
|
||||
/// a no-op approval.
|
||||
///
|
||||
/// `pub(crate)` so the manager-socket handler can pre-compute the
|
||||
/// diff once at submission time and embed it in the `ApprovalAdded`
|
||||
/// dashboard event (instead of forcing the dashboard to wait a
|
||||
/// `/api/state` cycle to see the diff for newly-queued approvals).
|
||||
pub(crate) async fn approval_diff(agent: &str, approval_id: i64) -> String {
|
||||
let applied = crate::paths::applied_dir(agent);
|
||||
if !applied.join(".git").exists() {
|
||||
return format!("(no applied git repo at {})", applied.display());
|
||||
}
|
||||
let proposal_ref = format!("refs/tags/proposal/{approval_id}");
|
||||
match git_diff_refs(&applied, "refs/heads/main", &proposal_ref).await {
|
||||
Ok(s) if s.is_empty() => "(proposal matches currently-deployed tree)".to_owned(),
|
||||
Ok(s) => s,
|
||||
Err(e) => format!("(error: {e:#})"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn git_diff_refs(applied_dir: &Path, base_ref: &str, target_ref: &str) -> Result<String> {
|
||||
let out = lifecycle::git_command()
|
||||
.current_dir(applied_dir)
|
||||
.args(["diff", &format!("{base_ref}..{target_ref}")])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("spawn `git diff` in {}", applied_dir.display()))?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"git diff {base_ref}..{target_ref} failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
/// Numeric ids of `<prefix>/<n>` tags in the applied repo (e.g.
|
||||
/// `proposal/3` → `3`). Unparseable suffixes are skipped. Used to
|
||||
/// resolve the `approved` / `previous` diff bases for an approval.
|
||||
async fn tag_ids(applied_dir: &Path, prefix: &str) -> Vec<i64> {
|
||||
let Ok(out) = lifecycle::git_command()
|
||||
.current_dir(applied_dir)
|
||||
.args(["tag", "-l", &format!("{prefix}/*")])
|
||||
.output()
|
||||
.await
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !out.status.success() {
|
||||
return Vec::new();
|
||||
}
|
||||
let strip = format!("{prefix}/");
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| l.trim().strip_prefix(&strip))
|
||||
.filter_map(|s| s.parse::<i64>().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct DiffBaseQuery {
|
||||
/// `applied` (running tree — default), `approved` (most recent
|
||||
/// earlier approved proposal), or `previous` (the prior queued
|
||||
/// proposal for this agent).
|
||||
base: Option<String>,
|
||||
}
|
||||
|
||||
/// On-demand unified diff for one `ApplyCommit` approval against a
|
||||
/// chosen base. `applied` = `applied/main` (what's running);
|
||||
/// `approved` = the most recent earlier `approved/<n>` tag (the last
|
||||
/// proposal the operator OK'd, even if its build then failed);
|
||||
/// `previous` = the prior queued `proposal/<n>` (the incremental
|
||||
/// delta when the manager chains proposals). Returns the raw diff
|
||||
/// text — the dashboard classifies lines client-side.
|
||||
pub(super) async fn get_approval_diff(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
axum::extract::Query(q): axum::extract::Query<DiffBaseQuery>,
|
||||
) -> Result<Response, ProblemDetails> {
|
||||
let base = q.base.as_deref().unwrap_or("applied");
|
||||
let approval = match state.coord.approvals.get(id) {
|
||||
Ok(Some(a)) => a,
|
||||
Ok(None) => return Err(error_problem(&format!("approval {id} not found"))),
|
||||
Err(e) => return Err(error_problem(&format!("approval {id}: {e:#}"))),
|
||||
};
|
||||
if !matches!(approval.kind, hive_sh4re::ApprovalKind::ApplyCommit) {
|
||||
return Err(error_problem("spawn approvals carry no commit to diff"));
|
||||
}
|
||||
let applied = crate::paths::applied_dir(&approval.agent);
|
||||
if !applied.join(".git").exists() {
|
||||
return Ok(plain_text(format!(
|
||||
"(no applied git repo at {})",
|
||||
applied.display()
|
||||
)));
|
||||
}
|
||||
let target = format!("refs/tags/proposal/{id}");
|
||||
let base_ref = match base {
|
||||
"applied" => Some("refs/heads/main".to_owned()),
|
||||
"approved" => {
|
||||
let ids = tag_ids(&applied, "approved").await;
|
||||
ids.into_iter()
|
||||
.filter(|&n| n != id)
|
||||
.max()
|
||||
.map(|n| format!("refs/tags/approved/{n}"))
|
||||
}
|
||||
"previous" => {
|
||||
let ids = tag_ids(&applied, "proposal").await;
|
||||
ids.into_iter()
|
||||
.filter(|&n| n < id)
|
||||
.max()
|
||||
.map(|n| format!("refs/tags/proposal/{n}"))
|
||||
}
|
||||
other => {
|
||||
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||
.with_detail(format!("unknown diff base {other:?}")));
|
||||
}
|
||||
};
|
||||
let Some(base_ref) = base_ref else {
|
||||
return Ok(plain_text(match base {
|
||||
"approved" => "(no earlier approved proposal to diff against)".to_owned(),
|
||||
_ => "(no previous proposal to diff against)".to_owned(),
|
||||
}));
|
||||
};
|
||||
match git_diff_refs(&applied, &base_ref, &target).await {
|
||||
Ok(s) if s.is_empty() => Ok(plain_text(
|
||||
"(identical — no changes vs this base)".to_owned(),
|
||||
)),
|
||||
Ok(s) => Ok(plain_text(s)),
|
||||
Err(e) => Err(error_problem(&format!("git diff: {e:#}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn plain_text(body: String) -> Response {
|
||||
(StatusCode::OK, body).into_response()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue