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 `