From be690acea913d0394840cac5b5e60e38fa41d172 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 3 Nov 2026 07:25:29 +0000 Subject: [PATCH] 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> --- bin/gstack-evidence | 6 ++++++ changelog.d/gsta258-l6-guard-carry.md | 4 ++-- docs/pregate.md | 4 +++- test/ship-ci-carry.test.ts | 20 ++++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/bin/gstack-evidence b/bin/gstack-evidence index d682c7342..48ba452d7 100755 --- a/bin/gstack-evidence +++ b/bin/gstack-evidence @@ -95,6 +95,7 @@ import { isUnavailable, loadPolicyFromBase, resolveBaseBranch } from "../lib/shi import { versionAtRef } from "../lib/restamp"; import { bashCommandInvocation, runScriptSync, wtreeFingerprint } from "../lib/script-invocation"; import { findCarry, laneMapping, mappingCoverage, perFileCommand } from "../lib/ci-carry"; +import { runChanged } from "../lib/run-evidence"; import { isConfigUnavailable, loadConfigFromBase } from "../lib/pregate/config"; import { buildRunCheck } from "../lib/pregate/run-check"; @@ -710,6 +711,11 @@ async function cmdRecordCi(argv: string[]): Promise { let rows: EvidenceRecord[] = []; try { rows = readJsonl(ledgerPath().file); } catch { rows = []; } here = { label: hereLabel, files: v.remainder, tests_ran: rows.findLast((r) => r.label === hereLabel && r.source === undefined)?.tests_ran ?? "unknown" }; + if (typeof here.tests_ran !== "number" || here.tests_ran <= 0) return none("skipped_zero_run", `the per_file run of ${v.remainder.length} skipped file(s) ran ${here.tests_ran} tests here (label=${hereLabel}); a lane passes on its runner count, never exit 0 alone`); + for (const e of v.runs) { + const changed = runChanged(check.deps, e.run_id, e.run_attempt); + if (changed) return none("run_changed", changed); + } } let paths: ReturnType; try { diff --git a/changelog.d/gsta258-l6-guard-carry.md b/changelog.d/gsta258-l6-guard-carry.md index 4aefc75df..452c19683 100644 --- a/changelog.d/gsta258-l6-guard-carry.md +++ b/changelog.d/gsta258-l6-guard-carry.md @@ -9,7 +9,7 @@ - An unknown `.gstack/pregate.json` key under a `min_gstack_version` above the running gstack now says `needs gstack >= ; upgrade` (`PREGATE_GSTACK_TOO_OLD`). It still refuses. - docs/pregate.md#prove-a-mapping documents the proof route, the version floors, and that `tree.json` writers using Bun's JUnit must take the root `testsuite` count. -- A test file whose every test skipped on the CI runner no longer counts as carried (GST-1190, harness-evals: 75 tests). `tree.json` now lists such files in `skipped_files`, written by gstack's own Free Tests from the per-file JUnit counts; the pregate tests stage runs them here. A /ship lane or guard whose inventory names one carries the covered files and runs only the skipped ones here, through the `per_file` runner its `ship_lanes_from` entry declares (for gstack, `bun test --timeout=30000 {files}`, so gstack runs just its 6 runner-skipped files). The CI row is written only after that run passes (`skipped_failed` otherwise), and an entry without `per_file` refuses as `inventory_skipped`, naming the files. A partly skipped file still counts. An older `tree.json` without the list is read as before. +- A test file whose every test skipped on the CI runner no longer counts as carried (GST-1190, harness-evals: 75 tests). `tree.json` now lists such files in `skipped_files`, written by gstack's own Free Tests from the per-file JUnit counts; the pregate tests stage runs them here. A /ship lane or guard whose inventory names one carries the covered files and runs only the skipped ones here, through the `per_file` runner its `ship_lanes_from` entry declares (for gstack, `bun test --timeout=30000 {files}`, so gstack runs just its 6 runner-skipped files). The CI row is written only after that run passes with a test count above zero and the carried runs still stand (`skipped_failed`, `skipped_zero_run` or `run_changed` otherwise), and an entry without `per_file` refuses as `inventory_skipped`, naming the files. A partly skipped file still counts. An older `tree.json` without the list is read as before. - A run profile can declare `skippable: { jobs, gate, scripts }` for jobs a path filter skips (GST-1190, truther #1169). A skipped job counts only in a `pull_request` run whose gate job succeeded, with the workflow, the declared `scripts` (every file the gate reads), the paths the gate job names (also under its `working-directory`) and `package.json` when it runs a package script matching `origin/` byte for byte; the lane then prints `not owed: CI skipped at this tree (gate job , workflow unchanged)`. If the branch changed any of them, it controlled the skip, so the lane runs here. - `gstack-ci-carry find --policy-from head --simulate` is the same dry run for one lane command, every line starting `SIMULATION:`. @@ -21,4 +21,4 @@ - `test/ci-tree-reuse.test.ts` and `test/ship-ci-carry.test.ts` (GST-1190 skipped files): the writer lists only fully skipped files and leaves the list out without complete counts; a fully skipped inventory file stops the lane carry, an empty list or older `tree.json` carries, and the tests stage runs only the skipped selected file. All three fail without the change. - `test/ship-ci-carry.test.ts` (path-skipped jobs and `find --simulate`): a skip under an unchanged, green gate is not owed; a changed workflow, a changed gate script or a failed gate job runs the lane here; `find --policy-from head --simulate` prints only SIMULATION lines and writes nothing. All five fail without the change. - `test/ship-ci-carry.test.ts` (review fixes): a gate script behind `${{ github.workspace }}`, under `working-directory` or behind a package script is compared; the gate is the job, not a same-named step; only a `pull_request` run's skip counts; a skippable profile must declare `scripts`; `passed` never counts a CI row; a path-skipped guard says not owed; `--simulate` runs a branch inventory only with `--allow-repo-commands`. Eight fail without the fixes; a newer-failure veto test covers existing behavior. -- `test/ship-ci-carry.test.ts` (per-file lane carry): the covered file carries and only the skipped file runs through `per_file`, a failing remainder records no CI row and runs the lane whole, and `per_file` needs `{files}` and an inventory entry. All three fail without the change. +- `test/ship-ci-carry.test.ts` (per-file lane carry): the covered file carries and only the skipped file runs through `per_file`, a failing or zero-test remainder, or a run that moved to a new attempt meanwhile, records no CI row and runs the lane whole, and `per_file` needs `{files}` and an inventory entry. All five fail without the change. diff --git a/docs/pregate.md b/docs/pregate.md index 4c81c318e..c7f10a703 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -761,7 +761,9 @@ once: such as `"per_file": "bun test --timeout=30000 {files}"`, the same form as the tests stage's `runner`). That run is recorded under `