From 7caca3322e88f280111c495e68b1d917404ffdc5 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 3 Nov 2026 08:39:31 -0800 Subject: [PATCH] v1.94.26.0 gstack-evidence: sum sharded bun lanes; docs: dispatch-carry warning and record-ci recheck order (GSTA-258 L6 follow-up) (#3934) * feat(pregate): a guard that already passed at this tree carries from CI or the ledger (GST-1186) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(ci-carry): jobs-mapped lanes, aliases, narrow-lane counts, --simulate proof, policy-base check on carried (GST-1189, GST-1190) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ci-carry): a file whose every test skipped on CI is not carried; tree.json records skipped_files (GST-1190) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(ci-carry): path-skipped jobs count only under an unchanged gate; find --policy-from head --simulate (GST-1190) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: name the pre-push guards in Step 17 and the early push (moved from #3923 per GSTA-258) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci-carry: per-file lane carry for runner-skipped files; review fixes (gate inputs, passed ignores CI rows, simulate consent, pull_request-only skips, guard not-owed) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: Step 17 names the pre-push guards without a code this branch cannot print Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * pregate: gstack's bun run test lane runs its runner-skipped files with per_file Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci-carry: a per_file remainder must run tests here and the carried runs must still stand (l6c-p2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci-carry: read record-ci from its own last record line; the remainder's count must come from this run's row (l6c-p2 red-team) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ci-tree-reuse): type the skipped_files fixture for the test typecheck ratchet Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Revert "pregate: gstack's bun run test lane runs its runner-skipped files with per_file" This reverts commit fafbae5b9bae1a07065bd0706bc6bf5f41a6ef09. * ci-carry: ship the skipped_files reader inert; gstack's writer and per_file move to the adoption PR; Step 17 keeps its refs/heads marker Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: guard carry, per_file and the too-old message in pregate and troubleshooting (docs audit l6cdocs2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * gstack-evidence record-ci: recheck the working tree after the post-remainder run recheck (#3929 second fix-check) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * gstack-evidence: sum bun --shard processes in a lane's tests_ran; docs: dispatch-carry warning and record-ci recheck order (GBEV-3, GSTA-258 L6 follow-up) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * gstack-evidence: keep shard and count lines that leave the output tail; sum only shards 1..N in order; soften the dispatch-carry mechanism claim (l6f-p1 fixes) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci-carry: a pull_request run whose base moved names the one-command dispatch fix (TRUT-7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: the tree_mismatch refusal names its fix command (l6fdocs3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci-carry: a pull_request run that recorded its merge tree names the base-moved fix (the real tree_mismatch path; #3934 fix-check) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bin/gstack-evidence | 45 +++++++++++++++++++++++++--------- docs/pregate.md | 27 ++++++++++++++++---- lib/ci-carry.ts | 2 +- lib/run-evidence.ts | 16 ++++++++---- package.json | 2 +- test/evidence.test.ts | 18 ++++++++++++++ test/ship-ci-carry.test.ts | 8 ++++++ 10 files changed, 108 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f309606d8..ed15030c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.94.26.0] - 2026-11-03 + +- A lane whose command runs several `bun test --shard=i/N` processes now records the sum of the shards' counts, not the last shard's (GBEV-3, gbrain-evals#317: 3336 recorded against 6,095 run across 2 shards). The sum needs shards 1/N to N/N, in order, at the end of the log; any other set records the last summary as before, so a command that runs a suite twice, or reruns one shard, still reports its last run. The shard lines and count lines that scroll out of the 64 KiB output tail are kept for this, so a long shard run still sums (a real 2-shard, 2000-test bun run recorded 1000 before and 2000 after). +- docs/pregate.md#dispatch-carry warns that a `workflow_dispatch` run reports under the same job names on the head commit, so without strict branch protection a green dispatch run can hide a red merge-commit check. It recommends strict protection or distinct job names for dispatch runs, and notes that gstack's own merge trains test the merged tree. +- When a pull_request run tested a merge whose base moved after the run, `CI_CARRY: none (… tree_mismatch …)` now says so and prints the one-command fix: `gh workflow run --ref , then gstack-ci-carry wait` (TRUT-7). +- docs/pregate.md#skipped-files names the order of record-ci's checks after a skipped-file remainder, including the final `tree_changed` recheck. + +### Tests +- `test/ship-ci-carry.test.ts`: a pull_request run whose merge commit has another tree prints the base-moved reason and `gh workflow run free.yml --ref feat, then gstack-ci-carry wait`. It fails without the change. +- `test/evidence.test.ts`: two `--shard=i/2` processes in one log record 6095, two shards with more than 64 KiB of per-test lines record 4000, and an incomplete, repeated or rerun shard set records the last summary. The sum cases fail without the change (3336 and 2000). + ## [1.94.25.0] - 2026-11-03 - A lane label with a space or a colon, such as `pnpm lint` or `db:test`, no longer goes MISSING right after it carries (TRUT-7, truther#1170). `gstack-evidence` writes a label to the ledger with unsafe characters turned into `_`, but `check`, which `gstack-ship-release finish` and `accept` call, looked it up under the raw label, so /ship stopped with "not FRESH at the pushed head". `run`, `record-ci` and `check` now share one label spelling. diff --git a/VERSION b/VERSION index 6d54e88b9..c101bdb3d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.94.25.0 +1.94.26.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 849c10f13..d55a7ffa6 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.94.25.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.94.26.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-evidence b/bin/gstack-evidence index 39059086c..d87f76e28 100755 --- a/bin/gstack-evidence +++ b/bin/gstack-evidence @@ -37,7 +37,9 @@ * the tail is the stdout tail then the stderr tail, so a line's position says * nothing about when it was printed across streams, and bun's own counts (on * stderr) would otherwise outrank a 10k-test run whichever of the two ran - * first. Any summary when it did not speak follows the last-summary-wins rule; + * first. Any summary when it did not speak follows the last-summary-wins rule, + * except that a complete set of `bun test --shard=i/N` processes ending the log + * is summed (GBEV-3); * anything else is "unknown". * Three states, never merged: * a count, 0 (ZERO-RUN, graded like STALE by `check`) and unknown (non-blocking). @@ -146,18 +148,30 @@ const TAIL_MAX_BYTES = 64 * 1024; * Ring of the last TAIL_MAX_BYTES of each child stream, kept per stream: the * pumps interleave by arrival, so a stderr summary read while stdout still has * pipe-buffered bytes behind it would otherwise be pushed out of a shared ring. + * Lines leaving the ring that bun's shard sum reads (a "--shard=i/N: running" + * line, " N pass", " N fail", "Ran N tests across") are kept in order ahead of + * the tail, so every shard of a long sharded run still counts (GBEV-3). */ class OutputTail { private readonly decoders = { stdout: new TextDecoder("utf-8"), stderr: new TextDecoder("utf-8") }; private readonly text = { stdout: "", stderr: "" }; + private readonly kept = { stdout: [] as string[], stderr: [] as string[] }; + private trim(origin: "stdout" | "stderr", text: string): string { + const cut = text.lastIndexOf("\n", text.length - TAIL_MAX_BYTES); + if (cut < 0) return text.slice(-TAIL_MAX_BYTES); + const lines = text.slice(0, cut).split("\n").filter((l) => /^(?:--shard=\d+\/\d+: running |\s*\d+ (?:pass|fail)\s*$|Ran \d+ tests? across )/.test(strip(l))); + this.kept[origin] = [...this.kept[origin], ...lines].slice(-4096); + return text.slice(cut + 1); + } push(chunk: Uint8Array, origin: "stdout" | "stderr"): void { this.text[origin] += this.decoders[origin].decode(chunk, { stream: true }); - if (this.text[origin].length > TAIL_MAX_BYTES * 2) this.text[origin] = this.text[origin].slice(-TAIL_MAX_BYTES); + if (this.text[origin].length > TAIL_MAX_BYTES * 2) this.text[origin] = this.trim(origin, this.text[origin]); } end(): string { const tails = (["stdout", "stderr"] as const).map((origin) => { const text = this.text[origin] + this.decoders[origin].decode(); - return text.length > TAIL_MAX_BYTES ? text.slice(-TAIL_MAX_BYTES) : text; + const tail = text.length > TAIL_MAX_BYTES ? this.trim(origin, text) : text; + return [...this.kept[origin], tail].join("\n"); }); return tails.join("\n"); } @@ -172,7 +186,10 @@ const count = (text: string, word: string): number => Number(new RegExp(`(\\d+) /** * Bun: the last "Ran N tests across M files" block; executed = its " N pass" + - * " N fail" lines. Bun always prints that counts block (" 0 pass" / " 0 fail" + * " N fail" lines. When that block ends a `bun test --shard=i/N` process (bun's + * "--shard=i/N: running" line since the previous block), the blocks of shards + * 1/N to N/N, in that order, that end the log are summed (a sharded runner prints + * every shard, GBEV-3); any other shard set counts the last block alone. Bun always prints that counts block (" 0 pass" / " 0 fail" * even for an all-skip or empty run), so a terminal line with no " N pass" * above it is a line gstack's quiet shard runner forwarded (it forwards the * terminal line without the " N pass" line), not a bun summary: @@ -189,7 +206,9 @@ async function bunTestsRan(lines: string[]): Promise<{ at: number; ran: number } } catch { warn("shard-engine classifier not importable; using the built-in bun summary parser"); } - for (let i = lines.length - 1; i >= 0; i--) { + const blocks: { at: number; ran: number; shard?: string }[] = []; + let since = 0; + for (let i = 0; i < lines.length; i++) { if (!isTerminal(lines[i])) continue; let pass: number | null = null; let fail = 0; @@ -200,13 +219,17 @@ async function bunTestsRan(lines: string[]): Promise<{ at: number; ran: number } if (p) pass = Number(p[1]); if (f) fail = Number(f[1]); } - if (pass === null) { - if (/^Ran 0 tests? across/.test(lines[i])) return { at: i, ran: 0 }; - continue; - } - return { at: i, ran: pass + fail }; + const shard = lines.slice(since, i).map((l) => /^--shard=(\d+\/\d+): running /.exec(l)?.[1]).findLast(Boolean); + since = i + 1; + if (pass !== null) blocks.push({ at: i, ran: pass + fail, shard }); + else if (/^Ran 0 tests? across/.test(lines[i])) blocks.push({ at: i, ran: 0 }); } - return null; + const last = blocks.at(-1); + if (!last) return null; + const total = Number(last.shard?.split("/")[1] ?? 1); + const set = blocks.slice(-total); + const complete = total > 1 && set.length === total && set.every((b, k) => b.shard === `${k + 1}/${total}`); + return { at: last.at, ran: complete ? set.reduce((n, b) => n + b.ran, 0) : last.ran }; } /** diff --git a/docs/pregate.md b/docs/pregate.md index 673887593..3c30a7639 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -765,10 +765,12 @@ once: such as `"per_file": "bun test --timeout=30000 {files}"`, the same form as the tests stage's `runner`). That run is recorded under `