diff --git a/changelog.d/gsta258-l6-guard-carry.md b/changelog.d/gsta258-l6-guard-carry.md index 452c19683..f74a3be14 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 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 test file whose every test skipped on the CI runner no longer counts as carried (GST-1190, harness-evals: 75 tests). A `tree.json` that lists such files in `skipped_files` makes the pregate tests stage run 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. 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. gstack itself changes nothing yet: its own writer and a `per_file` entry for its `bun run test` lane switch on together in a follow-up pull request, after the pull requests now queued on older gstack have landed, because an older gstack rejects the new key in main's policy. - 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:`. @@ -18,7 +18,7 @@ - `test/ship-ci-carry.test.ts`: in founderstar's shape (guards only, no lanes), `check-quick` carries from CI, `play-through` reuses a same-tree ledger pass, `assets` runs, and a dirty tree carries nothing. It fails without the guard carry. - `test/ship-ci-carry.test.ts`: a lane mapped by `jobs` carries when every named job passed at the same tree with no test files, and refuses for a missing job, a skipped job, a rerun whose newest attempt skipped the job, and another tree's run. Those five fail without the change. - `test/ship-ci-carry.test.ts` (GST-1190): an alias carries and an undeclared env prefix does not; a narrow lane reports its covered file count, not 9677; a jobs-mapped `bunx tsc --noEmit` guard carries with `n/a`; the newer-gstack message; `--simulate` writes and clears nothing and the branch's own map never carries; `carried` drops a row another base mapped. Six of these fail without the change. -- `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` (GST-1190 skipped files): 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. Both 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 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 c7f10a703..721a8a5d6 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -766,9 +766,10 @@ once: `skipped_failed` and a run of zero tests is `skipped_zero_run`; either way the whole lane runs here. An entry with no `per_file` refuses as `inventory_skipped`, naming the files, and the lane runs here whole. A partly - skipped file ran tests and stays covered. gstack's own writer - (`scripts/ci-tree-reuse.ts record`) derives the list from the shards' per-file - JUnit counts and leaves it out when those counts are incomplete. A `tree.json` + skipped file ran tests and stays covered. gstack's own Free Tests writer does + not emit the list yet: it starts in the same follow-up pull request that + declares `per_file` for gstack's `bun run test` lane, so gstack's carry does not + change until both switch on together. A `tree.json` without the key (older writers, other repos) is read as before, with every listed file counted as run; add the list to your writer to close that gap. The remote gate's clearance does not use it: that gate runs on a Linux runner diff --git a/scripts/ci-tree-reuse.ts b/scripts/ci-tree-reuse.ts index a7a3d65f2..9107a0a0a 100755 --- a/scripts/ci-tree-reuse.ts +++ b/scripts/ci-tree-reuse.ts @@ -6,8 +6,7 @@ * The producer. A Free Tests or Windows Free Tests aggregate runs it only after its strict verification * passed, and uploads the file as the `tested-tree` artifact: run_id, run_attempt, event, sha (GITHUB_SHA), * head_sha (the pull request head for pull_request, else GITHUB_SHA), tree (git's tree of GITHUB_SHA), - * workflow, os (the OSes the run's test jobs ran on), files (every test file in the plan), tests and, from complete - * per-file counts, skipped_files (the planned files whose every test skipped on the runner; GST-1190). + * workflow, os (the OSes the run's test jobs ran on), files (every test file in the plan) and tests. * * bun run scripts/ci-tree-reuse.ts find --workflow --profile * The main-push consumer (the `reuse` job). A push to main whose tree a pull request already tested green @@ -73,8 +72,7 @@ export function findReuse(deps: ReuseDeps, o: { repo: string; base: string; push } /** tree.json for the `tested-tree` artifact; throws on any missing or malformed input, so a broken record fails the aggregate instead of uploading. */ -type FileCounts = { tests: number; pass: number; fail: number }; -export function treeRecord(env: NodeJS.ProcessEnv, o: { plan: { shards: Array<{ files: string[] }> }; results: Array<{ outcome?: { summary?: { testsRan?: number }; counts?: { complete?: boolean; files?: Record } } }>; os: string[]; tree: string }) { +export function treeRecord(env: NodeJS.ProcessEnv, o: { plan: { shards: Array<{ files: string[] }> }; results: Array<{ outcome?: { summary?: { testsRan?: number } } }>; os: string[]; tree: string }) { const int = (name: string) => { const v = Number(env[name]); if (!Number.isSafeInteger(v) || v < 1) throw new Error(`${name} is not a positive integer`); return v; }; const sha = env.GITHUB_SHA ?? ''; const event = env.GITHUB_EVENT_NAME ?? ''; @@ -89,11 +87,7 @@ export function treeRecord(env: NodeJS.ProcessEnv, o: { plan: { shards: Array<{ if (!files.length) throw new Error('the plan lists no test files'); const counts = o.results.map(r => r.outcome?.summary?.testsRan); const tests = o.results.length && counts.every(n => Number.isSafeInteger(n) && n! >= 0) ? counts.reduce((a, b) => a! + b!, 0)! : files.length; - // skipped_files (GST-1190): planned files whose every test skipped on this runner, so a carry runs them where they might not - // skip. A partly skipped file ran tests and stays covered. Only from complete per-file counts; otherwise left out (unknown). - const counted = o.results.length > 0 && o.results.every(r => r.outcome?.counts?.complete === true && r.outcome.counts.files); - const skipped_files = counted ? files.filter(f => o.results.some(r => { const c = r.outcome!.counts!.files![f]; return !!c && c.tests > 0 && c.pass === 0 && c.fail === 0; })) : undefined; - return { run_id: int('GITHUB_RUN_ID'), run_attempt: int('GITHUB_RUN_ATTEMPT'), event, sha, head_sha: head, tree: o.tree, workflow, os: o.os, files, tests, ...(skipped_files ? { skipped_files } : {}) }; + return { run_id: int('GITHUB_RUN_ID'), run_attempt: int('GITHUB_RUN_ATTEMPT'), event, sha, head_sha: head, tree: o.tree, workflow, os: o.os, files, tests }; } function flag(args: string[], name: string): string | undefined { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; } @@ -108,7 +102,7 @@ function record(args: string[]): number { const out = need(args, '--out'); fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true }); fs.writeFileSync(out, JSON.stringify(rec, null, 2) + '\n'); - console.log(`TESTED_TREE: run ${rec.run_id}/${rec.run_attempt} ${rec.event} head ${rec.head_sha.slice(0, 12)} tree ${rec.tree.slice(0, 12)} files=${rec.files.length} tests=${rec.tests} skipped_files=${rec.skipped_files?.length ?? 'unknown'} os=${rec.os.join(',')}`); + console.log(`TESTED_TREE: run ${rec.run_id}/${rec.run_attempt} ${rec.event} head ${rec.head_sha.slice(0, 12)} tree ${rec.tree.slice(0, 12)} files=${rec.files.length} tests=${rec.tests} os=${rec.os.join(',')}`); return 0; } diff --git a/ship/SKILL.md b/ship/SKILL.md index 46c051fd3..5b0c6f9ec 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -871,7 +871,7 @@ A new PR or an existing draft needs no question: Step 19 creates or keeps the dr ``` It checks the branch, the offer, the gate above (`--gate-open `; `--gate-open-approved` is B) -`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `` against HEAD: a failed lookup STOPs, a match prints +`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/` against HEAD: a failed lookup STOPs, a match prints `PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin `. **If the push fails, STOP.** No Step 19 or publication claim. Report the error: diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index 06436f8b9..0d2e3ca5b 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -592,7 +592,7 @@ A new PR or an existing draft needs no question: Step 19 creates or keeps the dr ``` It checks the branch, the offer, the gate above (`--gate-open `; `--gate-open-approved` is B) -`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `` against HEAD: a failed lookup STOPs, a match prints +`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/` against HEAD: a failed lookup STOPs, a match prints `PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin `. **If the push fails, STOP.** No Step 19 or publication claim. Report the error: diff --git a/test/ci-tree-reuse.test.ts b/test/ci-tree-reuse.test.ts index 21a447fdd..1fc8cc913 100644 --- a/test/ci-tree-reuse.test.ts +++ b/test/ci-tree-reuse.test.ts @@ -129,19 +129,6 @@ describe('treeRecord: the tested-tree producer', () => { expect(treeRecord(env({ GITHUB_EVENT_NAME: 'pull_request', GITHUB_SHA: MERGE, PR_HEAD_SHA: HEAD }), { plan, results, os: ['Windows'], tree: TREE })).toMatchObject({ event: 'pull_request', sha: MERGE, head_sha: HEAD }); expect(treeRecord(env(), { plan, results: [{ outcome: {} }], os: ['Linux'], tree: TREE }).tests).toBe(3); }); - // Value: protects=a carry knows which files ran nothing on CI (GST-1190, harness-evals: 75 tests skipped on the runner); fails_when=a fully - // skipped file is not listed, a partly skipped one is, or a run without complete counts claims a list - test('skipped_files lists only files whose every test skipped; a partly skipped file stays covered; incomplete counts leave it out', () => { - const n = (tests: number, pass: number, skip: number) => ({ tests, pass, fail: 0, skip, todo: 0, expects: pass }); - const counted: Array<{ outcome: { summary: { testsRan: number }; counts: { complete: boolean; files: Record> } } }> = [ - { outcome: { summary: { testsRan: 7 }, counts: { complete: true, files: { 'test/a.test.ts': n(3, 0, 3), 'test/b.test.ts': n(4, 2, 2) } } } }, - { outcome: { summary: { testsRan: 5 }, counts: { complete: true, files: { 'test/c.test.ts': n(5, 5, 0) } } } }, - ]; - expect(treeRecord(env(), { plan, results: counted, os: ['Linux'], tree: TREE }).skipped_files).toEqual(['test/a.test.ts']); - expect(treeRecord(env(), { plan, results, os: ['Linux'], tree: TREE })).not.toHaveProperty('skipped_files'); - const partial = [counted[0]!, { outcome: { ...counted[1]!.outcome, counts: { complete: false, files: {} } } }]; - expect(treeRecord(env(), { plan, results: partial, os: ['Linux'], tree: TREE })).not.toHaveProperty('skipped_files'); - }); test('missing or malformed inputs throw, so the aggregate fails instead of uploading a record', () => { expect(() => treeRecord(env({ GITHUB_EVENT_NAME: 'pull_request', GITHUB_SHA: MERGE }), { plan, results, os: ['Linux'], tree: TREE })).toThrow(/PR_HEAD_SHA/); expect(() => treeRecord(env({ GITHUB_RUN_ID: '' }), { plan, results, os: ['Linux'], tree: TREE })).toThrow(/GITHUB_RUN_ID/); diff --git a/test/fixtures/golden/agy-ship-SKILL.md b/test/fixtures/golden/agy-ship-SKILL.md index 909818a82..4f7182562 100644 --- a/test/fixtures/golden/agy-ship-SKILL.md +++ b/test/fixtures/golden/agy-ship-SKILL.md @@ -872,7 +872,7 @@ A new PR or an existing draft needs no question: Step 19 creates or keeps the dr ``` It checks the branch, the offer, the gate above (`--gate-open `; `--gate-open-approved` is B) -`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `` against HEAD: a failed lookup STOPs, a match prints +`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/` against HEAD: a failed lookup STOPs, a match prints `PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin `. **If the push fails, STOP.** No Step 19 or publication claim. Report the error: diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index 46c051fd3..5b0c6f9ec 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -871,7 +871,7 @@ A new PR or an existing draft needs no question: Step 19 creates or keeps the dr ``` It checks the branch, the offer, the gate above (`--gate-open `; `--gate-open-approved` is B) -`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `` against HEAD: a failed lookup STOPs, a match prints +`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/` against HEAD: a failed lookup STOPs, a match prints `PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin `. **If the push fails, STOP.** No Step 19 or publication claim. Report the error: diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index 1dfe0e2d0..bf43c6d27 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -878,7 +878,7 @@ $GSTACK_ROOT/bin/gstack-ship-release push --branch ``` It checks the branch, the offer, the gate above (`--gate-open `; `--gate-open-approved` is B) -`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `` against HEAD: a failed lookup STOPs, a match prints +`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/` against HEAD: a failed lookup STOPs, a match prints `PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin `. **If the push fails, STOP.** No Step 19 or publication claim. Report the error: diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 207c665ab..2abf26fff 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -880,7 +880,7 @@ $GSTACK_ROOT/bin/gstack-ship-release push --branch ``` It checks the branch, the offer, the gate above (`--gate-open `; `--gate-open-approved` is B) -`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `` against HEAD: a failed lookup STOPs, a match prints +`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/` against HEAD: a failed lookup STOPs, a match prints `PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin `. **If the push fails, STOP.** No Step 19 or publication claim. Report the error: diff --git a/test/size-ledger/ship/gsta258-l6-guard-carry.json b/test/size-ledger/ship/gsta258-l6-guard-carry.json index 9734176f5..b030d3c24 100644 --- a/test/size-ledger/ship/gsta258-l6-guard-carry.json +++ b/test/size-ledger/ship/gsta258-l6-guard-carry.json @@ -1,4 +1,4 @@ { - "bytes": 1, - "why": "GST-1138: ship names the pre-push guards in Step 17 and the early push, with the ls-remote line shortened to fit" + "bytes": 14, + "why": "GST-1138: ship names the push guards in Step 17 and the early push" }