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>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-03 07:42:25 +00:00
1 parent 58d6635d3e
commit 4a9d85e150
11 files changed
+18 -36

No files matched your search

+2 -2
View File
@@ -9,7 +9,7 @@
- An unknown `.gstack/pregate.json` key under a `min_gstack_version` above the running gstack now says `needs gstack >= <version>; 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/<base>` byte for byte; the lane then prints `not owed: CI skipped at this tree (gate job <name>, 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.
+4 -3
View File
@@ -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
+4 -10
View File
@@ -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 <file.yml> --profile <run_profiles name>
* 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<string, FileCounts> } } }>; 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;
}
+1 -1
View File
@@ -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 <n>`; `--gate-open-approved` is B)
`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -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 <n>`; `--gate-open-approved` is B)
`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
-13
View File
@@ -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<string, ReturnType<typeof n>> } } }> = [
{ 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/);
+1 -1
View File
@@ -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 <n>`; `--gate-open-approved` is B)
`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -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 <n>`; `--gate-open-approved` is B)
`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -878,7 +878,7 @@ $GSTACK_ROOT/bin/gstack-ship-release push --branch <branch-name>
```
It checks the branch, the offer, the gate above (`--gate-open <n>`; `--gate-open-approved` is B)
`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
+1 -1
View File
@@ -880,7 +880,7 @@ $GSTACK_ROOT/bin/gstack-ship-release push --branch <branch-name>
```
It checks the branch, the offer, the gate above (`--gate-open <n>`; `--gate-open-approved` is B)
`gstack-pregate sealed` and the pre-push guards, then `git ls-remote` of `<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`gstack-pregate sealed` and push guards, then `git ls-remote --heads origin refs/heads/<branch-name>` against HEAD: a failed lookup STOPs, a match prints
`PUSH: ALREADY_PUSHED` (go to Step 18), else it runs `git push -u origin <branch-name>`.
**If the push fails, STOP.** No Step 19 or publication claim. Report the error:
@@ -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"
}