Skip to content

fix(promotion): commit the files of transitions that applied when a later one fails - #65

Open
scott-lowe-vapi wants to merge 1 commit into
test/promotion-golden-baselinefrom
fix/promotion-commit-applied-transitions
Open

scott-lowe-vapi wants to merge 1 commit into
test/promotion-golden-baselinefrom
fix/promotion-commit-applied-transitions

Conversation

@scott-lowe-vapi

@scott-lowe-vapi scott-lowe-vapi commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Value

V.A.L.U.E. tier: project — PR 9 of 10 for inline simulation PR checks (TEST-141). This is a fix to promotion, independent of the check stack (cut from main), and the promotion gate (PR 10) depends on it.

  • Problem: when a multi-transition promotion fails partway, the "Commit reconciled files and UUID state" step pushes nothing: not the UUID state, and not the files of transitions that already reached the platform.
    • Why: on failure the step stages only .vapi-state.*.json, but the failed transition's promotionPlanApply has already rewritten tracked files in its target org, so git pull --rebase refuses with "You have unstaged changes".
    • Result: git and the platform disagree, and the next promotion plans from stale files.
  • Who it affects: teams using promotion.yml with two or more transitions (dev → staging → prod), and anyone who adds the promotion gate in PR 10, where a failing gate is exactly a mid-run failure.
  • What changes:
    • src/promote-cmd.ts:

      • each --apply run truncates tmp/promotion-applied.txt (gitignored);
      • after each successful apply.ts, it records each path git status --porcelain -z --untracked-files=all -- resources/<target> reports (new, modified, deleted and renamed files, by current path), with its content at that moment written to git's object store (git hash-object -w; - for a deletion). These are read from git, not the plan, because apply's own pull and push can rewrite files the plan didn't name;
      • promotionCommandRun(args, deps = { childRun }) gains a test seam.
    • .github/workflows/promotion.yml: on a non-success outcome, the commit step:

      1. stages the state files plus exactly the recorded blobs (git update-index --cacheinfo; deletions with git rm --cached), and commits. Staging the recorded content, not the working tree, means a later failed transition that rewrote the same file can't replace what was applied;
      2. runs git reset --hard HEAD && git clean -fd -- resources, so the failed transition's rewrites are discarded;
      3. then rebases and pushes as before.

      On success it's unchanged: it commits all of resources.

    • improvements.md: refactor(engine): extract shared slug + folder helpers into slug-utils #35, RESOLVED.

Evidence of value

Red/green in a scratch repo.

  • Setup: a bare origin plus a clone; the pipeline is a → b → c, and c starts with a tracked assistants/intake.yml.
  • The run: promote --all --apply runs this branch's promote-cmd with a fake child runner. Apply into b succeeds (writing state and one extra file, as apply's pull can). Apply into c writes state, then fails.
  • Then: the workflow's real commit step, extracted from each ref's promotion.yml, runs with PROMOTION_OUTCOME=failure.
main (69c7e83) This branch
Commit step error: cannot pull with rebase: You have unstaged changes. exit 128 pushed, exit 0
origin/main unchanged (only base) chore: record promoted Vapi state [skip promotion]
.vapi-state.b.json / .c.json on origin {} / {} (state lost) uuid-b / uuid-c (both recorded)
resources/b/assistants/intake.yml, pulled-by-apply.yml missing committed
resources/c/assistants/intake.yml old content old content (the failed rewrite is discarded)
resources/c/assistants/pulled-by-apply.yml — removed by git clean

tmp/promotion-applied.txt after the run held exactly the two resources/b/... paths.

Same-org case: red before the fix, green after. Two pipelines promote into the same org in one --all run. The first applies resources/c/assistants/intake.yml with a's content, and the second rewrites that file with b's content and then fails. Before the fix, the committed file was b's (the failed rewrite); after it, the commit holds a's, the content that was actually applied.

Tests: on top of #68, npm test goes from 476 to 484 passing.

Testing plan

  • tests/promote-cmd.test.ts (3 tests, a real temp git repo, injected child runner):

    • one transition applies and the next fails: only the applied transition's files are recorded, including a file apply rewrote that the plan didn't name;
    • every --apply run starts a fresh record, and a plan-only run leaves it alone;
    • deleted and renamed files are recorded by their current path.
  • tests/promote-cmd.test.ts also checks that each applied file is recorded with its content at apply time (git cat-file of the recorded blob, after a later rewrite), and a deletion as -.

  • New tests/promotion-workflow-commit.test.ts (4 tests) runs the workflow's real commit step, read from promotion.yml and run with bash -e, against a clone of a bare origin, after a promotion driven through promote-cmd:

    • partial failure: state and the applied transition's files are pushed, and the failed rewrite is not;
    • the same-org case above;
    • success commits every change;
    • no changes pushes nothing.

    A later edit to the YAML that brings either bug back fails here.

  • The existing tests/promotion.test.ts, and test(promotion): pin the exact files promoting an assistant writes to the target #68's golden promotion test, pass unchanged.

  • Not tested:

    • a real GitHub Actions promotion run (the step runs locally under bash, read from the YAML) — manual QA with two non-production orgs before merge.

Stacked on #68.

Refs TEST-141

🤖 Generated with Claude Code

@scott-lowe-vapi
scott-lowe-vapi force-pushed the fix/promotion-commit-applied-transitions branch from 308b8d5 to ef619e1 Compare October 3, 2026 06:18
@scott-lowe-vapi
scott-lowe-vapi marked this pull request as ready for review October 3, 2026 06:19
…ater one fails

When a promotion failed partway, the workflow's commit step staged only
the UUID state, but the failing transition had already rewritten tracked
files in its target org, so `git pull --rebase` refused and nothing was
pushed — not the state, and not the files of transitions that had
already reached the platform.

- promote-cmd truncates tmp/promotion-applied.txt at the start of each
  --apply run and, after each successful apply.ts, records every path git
  reports changed under resources/<target>/ (from git, not the plan:
  apply's own pull and push can rewrite other files) with its content at
  that moment, written to git's object store (`-` for a deletion).
- On a non-success outcome, the commit step stages the state files plus
  exactly those recorded blobs, commits, then resets and cleans
  resources/ so the failed transition's rewrites can't block the rebase.
  Staging the recorded content rather than the working tree means a later
  failed transition that rewrote the same file (two pipelines promoting
  into one org in one --all run) can't replace what was applied.
- promotionCommandRun(args, deps) takes an injectable child runner.
- tests/promotion-workflow-commit.test.ts runs the workflow's real commit
  step (read from promotion.yml, run with bash -e) against a bare origin:
  partial failure, the same-org case, success, and no changes.

Refs TEST-141

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant