meta: stage before lock, single commit per change
git+file://'s dirty-tree fetcher reads tracked + staged content from the index (not the working tree, not untracked files). so staging is enough to make a new flake.nix or flake.lock visible to nix without committing first. sync_agents now stages flake .nix, runs lock, stages the resulting flake.lock, then commits both together in a single 'regenerate meta flake' (or 'seed meta from N agents') commit — no more two-commit churn. prepare_deploy applies the same trick to the two-phase deploy: runs nix flake update, stages flake.lock so nixos-container update sees it, doesn't commit yet. finalize_deploy commits with the deployed/<id> message on build success; abort_deploy git-restores the staged lock back to HEAD on failure. meta history continues to record only successful deploys (and now one commit per success instead of one + amend).
This commit is contained in:
parent
220e9b4af6
commit
63e8a98df2
1 changed files with 49 additions and 78 deletions
|
|
@ -73,110 +73,81 @@ pub async fn sync_agents(
|
||||||
if initial {
|
if initial {
|
||||||
git(&dir, &["init", "--initial-branch=main"]).await?;
|
git(&dir, &["init", "--initial-branch=main"]).await?;
|
||||||
}
|
}
|
||||||
// Commit flake.nix *before* running nix flake lock — when meta is
|
// Stage flake.nix *before* running nix flake lock. When meta is
|
||||||
// a git repo, nix treats it as a `git+file://` self-reference and
|
// a git repo, nix treats it as a `git+file://` self-reference;
|
||||||
// only sees TRACKED files. Locking against an untracked flake.nix
|
// its dirty-tree fetcher includes index entries (tracked +
|
||||||
// surfaces as "source tree does not contain '/flake.nix'".
|
// staged) but skips untracked files, so without the stage step
|
||||||
|
// an untracked flake.nix surfaces as "source tree does not
|
||||||
|
// contain '/flake.nix'". Lock then commit once with both
|
||||||
|
// flake.nix and flake.lock — single commit per change.
|
||||||
git(&dir, &["add", "flake.nix"]).await?;
|
git(&dir, &["add", "flake.nix"]).await?;
|
||||||
|
nix(&dir, &["flake", "lock"]).await?;
|
||||||
|
if std::path::Path::new(&dir).join("flake.lock").exists() {
|
||||||
|
git(&dir, &["add", "flake.lock"]).await?;
|
||||||
|
}
|
||||||
let msg = if initial {
|
let msg = if initial {
|
||||||
format!("seed meta from {} agent(s)", agents.len())
|
format!("seed meta from {} agent(s)", agents.len())
|
||||||
} else {
|
} else {
|
||||||
"regenerate meta flake".to_owned()
|
"regenerate meta flake".to_owned()
|
||||||
};
|
};
|
||||||
git_commit(&dir, &msg).await?;
|
git_commit(&dir, &msg).await?;
|
||||||
nix(&dir, &["flake", "lock"]).await?;
|
|
||||||
if !git_is_clean(&dir).await? {
|
|
||||||
git(&dir, &["add", "flake.lock"]).await?;
|
|
||||||
git_commit(&dir, "lock update").await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 1 of an apply-commit deploy. Updates the locked rev of
|
/// Phase 1 of an apply-commit deploy. Updates the locked rev of
|
||||||
/// `agent-<name>` to whatever `applied/<name>/main` currently points
|
/// `agent-<name>` to whatever `applied/<name>/main` currently points
|
||||||
/// at **and commits** the bump immediately — `git+file://` semantics
|
/// at and **stages** the lock so `nixos-container update --flake
|
||||||
/// mean nixos-container would otherwise build against the previously
|
/// meta#<n>` (which reads via `git+file://`) sees the new rev via
|
||||||
/// tracked lock, ignoring the working-tree change. `finalize_deploy`
|
/// the index. Doesn't commit — `finalize_deploy` commits on build
|
||||||
/// later amends the message with the deployed tag; `abort_deploy`
|
/// success, `abort_deploy` drops the staged change on failure so
|
||||||
/// drops the commit entirely so meta history shows only successful
|
/// meta history only carries successful deploys.
|
||||||
/// deploys.
|
|
||||||
#[allow(dead_code)] // wired up by actions::run_apply_commit in a later commit
|
#[allow(dead_code)] // wired up by actions::run_apply_commit in a later commit
|
||||||
pub async fn prepare_deploy(name: &str) -> Result<()> {
|
pub async fn prepare_deploy(name: &str) -> Result<()> {
|
||||||
let dir = meta_dir();
|
let dir = meta_dir();
|
||||||
let input = format!("agent-{name}");
|
let input = format!("agent-{name}");
|
||||||
nix(&dir, &["flake", "update", &input]).await?;
|
nix(&dir, &["flake", "update", &input]).await?;
|
||||||
if git_is_clean(&dir).await? {
|
// Stage the new lock — git+file://'s dirty-tree fetcher reads
|
||||||
// Lock unchanged (rev already matches). Nothing to commit;
|
// index entries, so the upcoming nixos-container update sees the
|
||||||
// finalize_deploy will be a no-op too.
|
// bumped rev without a commit yet.
|
||||||
return Ok(());
|
git(&dir, &["add", "flake.lock"]).await
|
||||||
}
|
|
||||||
git(&dir, &["add", "flake.lock"]).await?;
|
|
||||||
git_commit(&dir, &format!("deploy {name} (building)")).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 2-success. Amend the prepare-deploy commit's message with
|
/// Phase 2-success. Commit the staged lock with the deployed tag +
|
||||||
/// the canonical deployed tag + sha. No-op when prepare didn't
|
/// sha as the message. No-op when the rev was already at the right
|
||||||
/// commit (input was already at the right rev).
|
/// place (nothing staged → nothing to commit).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn finalize_deploy(name: &str, sha: &str, tag: &str) -> Result<()> {
|
pub async fn finalize_deploy(name: &str, sha: &str, tag: &str) -> Result<()> {
|
||||||
let dir = meta_dir();
|
let dir = meta_dir();
|
||||||
// Detect prepare's commit by its placeholder message; if HEAD is
|
if !has_staged_changes(&dir).await? {
|
||||||
// some other commit (e.g. prepare no-op'd, or a concurrent change
|
return Ok(());
|
||||||
// landed) just add a fresh commit instead of amending.
|
|
||||||
let head_msg = git_head_msg(&dir).await.unwrap_or_default();
|
|
||||||
let short = &sha[..sha.len().min(12)];
|
|
||||||
let new_msg = format!("deploy {name} {tag} {short}");
|
|
||||||
if head_msg.starts_with(&format!("deploy {name} (building)")) {
|
|
||||||
git(
|
|
||||||
&dir,
|
|
||||||
&[
|
|
||||||
"-c",
|
|
||||||
&format!("user.name={GIT_NAME}"),
|
|
||||||
"-c",
|
|
||||||
&format!("user.email={GIT_EMAIL}"),
|
|
||||||
"commit",
|
|
||||||
"--amend",
|
|
||||||
"-m",
|
|
||||||
&new_msg,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
let short = &sha[..sha.len().min(12)];
|
||||||
|
git_commit(&dir, &format!("deploy {name} {tag} {short}")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 2-failure. Drop the prepare-deploy commit so meta history
|
/// Phase 2-failure. Unstage + restore the lock so meta returns to
|
||||||
/// only carries successful deploys. The failed proposal is still
|
/// the previously-committed shas. The failed proposal is still
|
||||||
/// captured in `applied/<n>`'s annotated `failed/<id>` tag.
|
/// captured in `applied/<n>`'s annotated `failed/<id>` tag.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn abort_deploy() -> Result<()> {
|
pub async fn abort_deploy() -> Result<()> {
|
||||||
let dir = meta_dir();
|
let dir = meta_dir();
|
||||||
let head_msg = git_head_msg(&dir).await.unwrap_or_default();
|
git(&dir, &["restore", "--staged", "flake.lock"]).await?;
|
||||||
if head_msg.starts_with("deploy ") && head_msg.contains("(building)") {
|
|
||||||
// hard reset drops the commit + its working-tree changes.
|
|
||||||
git(&dir, &["reset", "--hard", "HEAD~1"]).await
|
|
||||||
} else {
|
|
||||||
// Prepare no-op'd (lock unchanged); also restore any lingering
|
|
||||||
// lock-only changes as a safety belt.
|
|
||||||
git(&dir, &["restore", "flake.lock"]).await
|
git(&dir, &["restore", "flake.lock"]).await
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn git_head_msg(dir: &Path) -> Result<String> {
|
async fn has_staged_changes(dir: &Path) -> Result<bool> {
|
||||||
let out = lifecycle::git_command()
|
let st = lifecycle::git_command()
|
||||||
.current_dir(dir)
|
.current_dir(dir)
|
||||||
.args(["log", "-1", "--format=%s"])
|
.args(["diff", "--cached", "--quiet"])
|
||||||
.output()
|
.status()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("git log in {}", dir.display()))?;
|
.with_context(|| format!("git diff --cached in {}", dir.display()))?;
|
||||||
if !out.status.success() {
|
// exit 1 = differences present, 0 = no diff, other = error
|
||||||
bail!(
|
match st.code() {
|
||||||
"git log -1 failed: {}",
|
Some(0) => Ok(false),
|
||||||
String::from_utf8_lossy(&out.stderr).trim()
|
Some(1) => Ok(true),
|
||||||
);
|
_ => bail!("git diff --cached exited unexpectedly"),
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One-shot used by the manual-rebuild path: relock just one
|
/// One-shot used by the manual-rebuild path: relock just one
|
||||||
|
|
@ -188,11 +159,11 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
|
||||||
let dir = meta_dir();
|
let dir = meta_dir();
|
||||||
let input = format!("agent-{name}");
|
let input = format!("agent-{name}");
|
||||||
nix(&dir, &["flake", "update", &input]).await?;
|
nix(&dir, &["flake", "update", &input]).await?;
|
||||||
if !git_is_clean(&dir).await? {
|
if git_is_clean(&dir).await? {
|
||||||
git(&dir, &["add", "flake.lock"]).await?;
|
return Ok(());
|
||||||
git_commit(&dir, &format!("rebuild {name}: lock update")).await?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
git(&dir, &["add", "flake.lock"]).await?;
|
||||||
|
git_commit(&dir, &format!("rebuild {name}: lock update")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One-shot used by the auto-update path: pin the latest hyperhive
|
/// One-shot used by the auto-update path: pin the latest hyperhive
|
||||||
|
|
@ -202,11 +173,11 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
|
||||||
pub async fn lock_update_hyperhive() -> Result<()> {
|
pub async fn lock_update_hyperhive() -> Result<()> {
|
||||||
let dir = meta_dir();
|
let dir = meta_dir();
|
||||||
nix(&dir, &["flake", "update", "hyperhive"]).await?;
|
nix(&dir, &["flake", "update", "hyperhive"]).await?;
|
||||||
if !git_is_clean(&dir).await? {
|
if git_is_clean(&dir).await? {
|
||||||
git(&dir, &["add", "flake.lock"]).await?;
|
return Ok(());
|
||||||
git_commit(&dir, "bump hyperhive").await?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
git(&dir, &["add", "flake.lock"]).await?;
|
||||||
|
git_commit(&dir, "bump hyperhive").await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_flake(hyperhive_flake: &str, dashboard_port: u16, agents: &[AgentSpec]) -> String {
|
fn render_flake(hyperhive_flake: &str, dashboard_port: u16, agents: &[AgentSpec]) -> String {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue