Skip to content

ci(check): restore the base commit's Turbo cache first and report every check job - #279

Merged
ryanleecode merged 14 commits into
mainfrom
ci/wall-clock-u1
Oct 10, 2026
Merged

ryanleecode merged 14 commits into
mainfrom
ci/wall-clock-u1

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Unit U1 of the CI wall-clock plan (docs/plans/2026-10-10-0702-perf-ci-wall-clock-plan.md, ### U1.). Scope granted by unit it_b2aa4120: .github/workflows/*. AGENTS.md still lists .github/workflows/ as Read-only and is left unchanged.

What changes

  • check restores the Turbo cache entry of the commit's base by exact key. The base is the first parent, or the merge-base with main on dispatch. It falls back to the newest prefix match this ref can read only when the base commit has no entry. Before this, one restore step with restore-keys returned the ref's newest entry, which could be stale, ahead of main's exact base entry. That cost 13.7-18.4-minute Gates on PRs whose base entry existed.
  • TURBO_RUN_SUMMARY=true. A verdict step derives a reason code from the step outcomes and from Turbo's run summaries, read by scripts/turbo-summary.ts. The codes are GATE_OK, TURBO_TASK_FAILED, GATE_NON_TURBO_FAILED, CHECK_SETUP_FAILED, CANCELLED and GATE_VERDICT_UNPARSEABLE.
  • The reader exits 2 and names the first missing field when a summary lacks a field the verdict depends on, or when no summary was written. The verdict then reports GATE_VERDICT_UNPARSEABLE with a next action and fails the job. It never reports a silent GATE_OK. scripts/turbo-summary.test.ts runs in "Repo scripts tests" and feeds the reader summaries missing the exit code, the cache status, or the whole summary. If the exit-code guard is removed, the test fails with got {"kind":"read",...}.
  • The cache is saved only by a passing, warm run that missed at least one task and whose own key has no entry yet. A lookup-only restore checks that last condition. A cache: cold dispatch never saves. Before saving, restored entries that this run's task hashes do not reference are pruned, the same way as mutation.yml's build job. The summary line says save: attempted (...) or save: skipped (<why>), never "saved".
  • New cache-sweep job, which runs on main pushes only and is the only job with actions: write. It deletes Turbo entries by id: main keeps its newest 3, every other ref its newest 1, closed PRs 0.
    • A failed delete is a SWEEP_DELETE_FAILED warning plus a count.
    • A failed PR-state lookup keeps that PR's entries, with a SWEEP_PR_STATE_UNKNOWN warning.
    • Only a failed listing fails the job.
  • New composite .github/actions/job-report. It gives every job it is wired into a step summary: outcome, reason code, duration, Turbo cached/total, and the slowest missed tasks. On failure it adds an annotation titled with the reason code, whose message is the next action.
  • workflow_dispatch input cache: cold skips both restores, to measure a cold Gate.
  • The e2e comment claimed a cancelled job skips if: always() steps. Run 38041980554 disproved that, and the comment now says what timeout buys.

Coverage

  • Same pnpm check:ci, same planted two-plan check, same e2e matrix.
  • turbo run … --dry=json gives 112 Gate tasks and 17 gate:dist tasks at both base add6faa73 and head, with 0 differing task hashes.
  • Every warm run executed 112/112 and 17/17, all cached.
  • The type-error plant missed exactly the 3 tasks whose inputs it changed.
  • actionlint (with shellcheck) passes on every workflow change before it is pushed. The remaining SC2086 infos are on lines inherited from main.

Before / after (minutes, first job start to last job end)

Workflow / job Before (last 16 PR runs, p50 / p90) Warm 38041069181 6a91cdf74 Warm 38043221462 541aa86 Cold 38044445700 541aa86 Warm 38044944944 d1c3489
CI wall 14.4 / 19.5 14.0 13.8 22.4 (e2e timeouts, see below) e2e timeouts, see below
check 14.2 / 19.5 4.37 4.02 20.8 (Gate 19.4, nothing restored) 4.00
e2e rest-1 13.6 / 14.6 14.0 13.8 20.0 timeout timeout
e2e lifecycle 12.1 / 12.4 12.5 12.9 20.0 timeout timeout
e2e sabotage 10.9 / 11.4 11.8 9.3 20.0 timeout 20.0 timeout
e2e svelte-composite 9.3 / 9.7 8.5 9.6 20.0 timeout timeout
e2e rest-2 8.5 / 8.8 7.6 8.4 20.0 timeout timeout
Changeset Check 7.7 / 8.5 8.0 6.7 - 7.6
Nix 5.4 / 5.7 5.2 5.5 - 5.4
Commitlint 0.4 / 0.5 0.35 0.45 - 0.5
  • Cold check: 20.8 min, with the Gate at 19.4 min and both restores skipped. Before this change, a cache miss took 17.2-18.4 min of Gate (run 38026672592). U1 removes the stale-restore misses but does not make a true cold run faster; that is U6's lever.
  • Cold-run cache save: the run saved an entry because its head 541aa86 predates the never-save-when-cold fix.

E2E timeouts are repo-wide, not this diff

Since about 10:10Z, every e2e leg times out at exactly 20.0 min in "E2E lane":

  • main push 38044574358 (ee2c4cfb0): all 5 legs;
  • verdict-store 38043976128: all 6 legs;
  • tsgo-checker and tsgo-checker-parity-lane runs in the same window;
  • this branch's cold run.

The same tree 541aa86 passed every leg at 09:59-10:13Z. In the cold run's exported traces (rest-2), e2e.setup.pack finishes in 21 s and boot_sandbox and await_readiness succeed at 24 s. After that, no span closes until the timeout. The guest bake job never completes. This diff touches no e2e step except a comment.

Agent surface

  • GATE_OK, warm, d1c3489, job 114192840110: "Turbo cache restored: turbo-Linux-dist-ee2c4cfb0… (base commit); save: skipped (every task was cached)".
  • GATE_NON_TURBO_FAILED, dprint plant in docs/, run 38041980554: annotation "A Gate script outside Turbo failed: format:check (fix with pnpm format), lint:conventions, or gate:repo. Reproduce with: pnpm check:ci". Turbo was still 112/112 cached.
  • TURBO_TASK_FAILED, type-error plant in packages/ignorers/angular/tests, run 38042401509: annotation "Turbo task failed: @systemfsoftware/stryker-ignorer-angular#test …#typecheck. Reproduce with: pnpm exec turbo run …". 109/112 were cached.
  • Both plants are removed in 541aa8621, whose tree is byte-identical to 6a91cdf74.

Residual risks

  • Job timeout: a check that hits its timeout-minutes: 45 has not been observed. GitHub documents that a cancelled job runs if: always() steps within a 5-minute window, so the verdict and job report should run, but no run proves it for the timeout case. Superseded-push cancellation is proven: in run 38041980554, the legs' always() steps ran after "E2E lane" was cancelled.
  • Sweep untested: cache-sweep runs only on main pushes, so it has not run on this branch. It lists at most 1000 entries.
  • Main entries by recency: the sweep keeps main's newest 3 entries by recency, so a PR whose base is older falls back to the prefix match.

…ry check job

`check` restored `turbo-Linux-dist-<sha>` through prefix restore-keys, so a PR
got the newest entry its ref could read rather than its base commit's. #269
(one changeset file) restored an older main entry and re-ran 53 of 112 tasks;
turbo's own dry-run hashes are identical between that base and head, so the
key was never invalidated, the restored entry was simply another commit's.

- check restores `turbo-<os>-dist-<base>` in its own step (first parent, or
  merge-base with main on dispatch), and falls back to the prefix only on a
  miss; actions/cache searches the current ref before main, so one step with
  restore-keys would still prefer this ref's stale entry.
- Turbo writes run summaries (TURBO_RUN_SUMMARY); the cache is pruned to this
  run's task hashes and saved only when the Gate passed and missed a task.
- cache-sweep (main pushes only, the one job with actions: write) deletes
  turbo entries by cache id: main keeps its newest three, other refs one,
  closed pull requests none.
- .github/actions/job-report writes each job's summary (duration, Turbo
  cached/total, slowest missed tasks, restored key) and, on failure, an
  annotation titled with the reason code: GATE_OK, TURBO_TASK_FAILED,
  GATE_NON_TURBO_FAILED, CHECK_SETUP_FAILED, CANCELLED, SWEEP_OK/FAILED.
- workflow_dispatch input `cache: cold` skips the restore to measure cold.

Adds the CI wall-clock plan (docs/plans) this change is unit U1 of.
…cache

- The save guard compared only the fallback restore's exact hit, which is
  empty whenever the base entry restored, so a same-sha rerun tried to save
  over its own key. It now skips when either restore matched this run's key.
- cache-sweep lists and deletes through `gh cache list` / `gh cache delete
  <id>` (still by id) and checks out only the job-report action.
- job-report names its reason vocabulary itself and reports NO_REASON_CODE
  instead of an empty title when a caller passes none.
cancelled() is valid only in if: conditionals, so the workflow failed to
compile on every push of this branch (actionlint: calling function
"cancelled" is not allowed here).
…rden the sweep

- .github/scripts/turbo-summary.sh reads missed and failed tasks from Turbo's run
  summaries and exits 2 naming the first missing field. The verdict maps that to
  GATE_VERDICT_UNPARSEABLE with a next action and fails the job, instead of reading
  a format change as "nothing missed, nothing failed". turbo-summary.test.sh feeds it
  summaries missing fields and runs in check before the Gate.
- A lookup-only probe of this commit's own entry replaces the key comparisons: a
  re-run never re-tars and re-saves an entry that exists. A cache: cold dispatch
  never saves. The summary says "save: attempted" or "skipped (<why>)".
- cache-sweep: a failed delete is a SWEEP_DELETE_FAILED warning plus a count, and a
  failed PR-state lookup keeps that PR's entries; only a failed listing fails the job.
- The e2e comment claimed a cancelled job skips if: always() steps; a cancelled run
  ran them. It now says what timeout buys.
… with the repo scripts

#275 runs every scripts/*.test.ts in check, with Deno. The summary reader moves there as
scripts/turbo-summary.ts (run with node in the verdict) beside a Deno test that feeds it
summaries missing the exit code, the cache status, or the whole summary. The bash reader
and its own test step are gone.
…eno test

The previous commit removed the bash reader without the workflow and script changes that
replace it.
…h install

@effect/platform-node, platform-node-shared, opentelemetry and vitest 4.0.3
were published at 10:03 UTC with a peer effect@^4.0.3 that is not on the
registry. The bake resolves registry dependencies fresh, so npm resolved
forever and every e2e leg of run 38043976128 timed out in global setup.
Both bake installs now pass --min-release-age=1, and each runs under a
300 s timeout whose expiry names the fixture and step on stderr.
The bake guest is node:24-alpine, whose timeout is busybox: a killed
install exits with its own signal status (143 or 137), never GNU's 124,
so the reason line never printed. The script now decides from elapsed
seconds, and timeout -k 10 kills an npm that ignores TERM.

@ryanleecode ryanleecode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root verified at 1ab873d: all checks green, 0 threads, no suppressions. Over-10m e2e/check legs are inherited (Stream K).

@ryanleecode
ryanleecode merged commit 61d40db into main Oct 10, 2026
14 checks passed
@ryanleecode
ryanleecode deleted the ci/wall-clock-u1 branch October 10, 2026 17:48
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