Skip to content

ci: gate generated-artifact drift, and stop builds mutating tracked source (2/2) - #472

Open
josecarneiro wants to merge 9 commits into
claude/build-artifacts-outdated-1zq6jqfrom
claude/build-artifacts-outdated-1zq6jq-ci
Open

ci: gate generated-artifact drift, and stop builds mutating tracked source (2/2)#472
josecarneiro wants to merge 9 commits into
claude/build-artifacts-outdated-1zq6jqfrom
claude/build-artifacts-outdated-1zq6jq-ci

Conversation

@josecarneiro

@josecarneiro josecarneiro commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Part 2 of 2 — the actual fix. Stacked on #466, which does the one-off regeneration. Merge #466 first; GitHub will retarget this to main automatically.

Three files, ~40 lines. This is the part worth reviewing.

⚠️ No checks will appear on this PR until #466 merges. The workflow triggers on pull_request: branches: ["main"], which filters by base branch — and this PR's base is #466's branch, not main. CI starts running the moment #466 lands and GitHub retargets this to main. Both gates were verified locally in the meantime; see the test plan.

The problem

pnpm install && pnpm build on a clean main left 68 tracked files dirty. CI was building fine — it just never checked what it regenerated:

- run: pnpm generate-sdk    # regenerate
- run: pnpm build           # compile
- run: pnpm test            # test

CI regenerated the artifacts, then compiled and tested the freshly regenerated versions, then threw them away. That proves "the code compiles and tests pass." It never asks "do the committed files match what the generator just produced?" There was no git diff --exit-code anywhere in .github/workflows/, so drift was invisible by construction.

The changes

Two gates in the test job:

  1. After running both generators, fail if the tree is dirty — a PR that changes a spec without regenerating is rejected, with the command to run in the error message.
  2. Directly after pnpm build, fail if the tree is dirty — a build can never quietly regenerate or re-fetch committed source. Placed before pnpm test so a dirty tree can only be the build's doing, not a test writing a file.

auto-release now stages what it regenerates. It previously staged only packages/cli/package.json. Two things had to be true together, and neither was:

94   Build and test CLI                   (generates index.ts with the OLD version)
100  Bump versions…                       (bumps package.json)
181  pnpm --filter @epilot/cli generate   ← added: regenerate with the NEW version
183  FILES_TO_ADD=… packages/cli/         ← widened from just package.json
188  git add $FILES_TO_ADD
203  git push
205  Rebuild CLI with bumped version      (for the tarball, after the push)

Generation already happened before release — that is why npm was always correct. But for the CLI it ran before the version bump, so src/index.ts baked in the old number, and the rebuild that fixes it runs after the push. Staging alone would have committed the wrong version.

Gate 2 requires two client builds to stop generating. journey-client and configuration-hub-client were the only 2 of 52 clients running codegen from build. journey's npm run openapi fetched the live spec from docs.api.epilot.io and overwrote committed source — so a build needed network access, mutated your working tree, and produced different output depending on what was deployed to prod at that moment. Both now match the other 50: build only compiles, and typegen moves to prepublishOnly so published tarballs still carry freshly generated types. Updating a spec stays the deliberate npm run openapi step from CONTRIBUTING.md.

Test plan

Verified locally on this branch (CI cannot run yet — see the note above):

  • Gate 1 passes — generators produce no diff
  • Gate 2 passespnpm build exits 0 and leaves the tree clean (was 68 dirty files before this stack)
  • pnpm lint — clean (524 files)
  • Workflow YAML parses; step order confirmed as generate → gate 1 → build → gate 2 → test
  • Gate 1 demonstrated catching a real regression: on the earlier combined branch, merging main after the @epilot/cli@0.1.141 release flagged exactly the four artifacts that release had left stale
  • The equivalent combined tree (this change plus chore: bring committed generated artifacts back in sync with their generators (1/2) #466's regeneration) passed full CI green — Test, Lint, CodeQL, Analyze — before the split

What this prevents

While the combined version of this work sat waiting for review, @epilot/cli@0.1.141 released off the back of #467 and re-introduced the identical drift on main within the hour — definitions/entity-mapping.json stale against its spec, src/index.ts back to reporting 0.1.51. The failure mode reproduces on its own roughly every release. These gates are what stop it.

…ource

The repo commits the generated artifacts of @epilot/sdk and @epilot/cli, but
nothing verified the committed copies matched their generators. CI ran
generate-sdk and build on every push, tested the result and discarded it, so
drift accumulated silently.

Two gates in the test job:

- after running both generators, fail if the tree is dirty, so a PR that
  changes a spec without regenerating is rejected with the command to run.
- directly after `pnpm build`, fail if the tree is dirty, so a build can never
  quietly regenerate or re-fetch committed source. Checked before `pnpm test`
  so a dirty tree can only be the build's doing.

The auto-release job now regenerates the CLI after bumping its version and
stages all of packages/cli/. It previously staged only package.json: the
pre-bump "Build and test CLI" step generated src/index.ts with the old version
and "Rebuild CLI with bumped version" runs after the push, so neither produced
output the release commit could carry.

Gate 2 needs two client builds to stop generating. journey-client and
configuration-hub-client were the only 2 of 52 running codegen from `build`;
journey's `npm run openapi` fetched the live spec from docs.api.epilot.io and
overwrote committed source, so a build had a network dependency and its result
varied with whatever was deployed. Both now match the other 50: `build` only
compiles, `typegen` moved to `prepublishOnly` so published tarballs still carry
freshly generated types. Updating a spec stays the deliberate
`npm run openapi` step from CONTRIBUTING.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhYbcfHQfe7bk2R37P75zV
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.

2 participants