From 48a9d92a2f5c94fce1be2a31e2cc4d4f3a93d25d Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 31 Oct 2026 18:40:33 +0000 Subject: [PATCH] ci: keep the route and backstop policy reads inline (git and jq only); docs follow Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .github/workflows/evals-main-backstop.yml | 13 +++++++++++-- .github/workflows/evals.yml | 13 +++++++++++-- changelog.d/capy-blobless-clones-on-the.md | 2 +- docs/troubleshooting.md | 9 +++++---- test/ci-history-read.test.ts | 1 + test/evals-train-workflow.test.ts | 2 -- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/workflows/evals-main-backstop.yml b/.github/workflows/evals-main-backstop.yml index 9a37c2bfb..2c3f30303 100644 --- a/.github/workflows/evals-main-backstop.yml +++ b/.github/workflows/evals-main-backstop.yml @@ -45,11 +45,20 @@ jobs: with: fetch-depth: 0 - # Read with git and jq only (history-read.sh is git alone): without a train block nothing below runs. + # Read with git and jq only: without a train block nothing below runs. - name: Read the train policy (main) id: policy run: | - policy=$(bash .github/scripts/history-read.sh "$GITHUB_SHA" .gstack/ship-policy.json --absent null) + ref="$GITHUB_SHA" file=.gstack/ship-policy.json policy=null + read_failed() { echo "::error::cannot read ${file} at ${ref}. $1 ($(git --version)). Fix: see docs/troubleshooting.md#history-read-failed (HISTORY_READ_FAILED)" >&2; exit 1; } + listed=$(git ls-tree --full-tree "$ref" -- "$file") || read_failed "git ls-tree failed" + if [ -n "$listed" ]; then + for try in 1 2 3; do + policy=$(git show "${ref}:${file}") && break + if [ "$try" = 3 ]; then read_failed "git show failed 3 times"; fi + sleep $((try * 2)) + done + fi train=$(jq -c 'if (.train | type) == "object" then .train else null end' <<<"$policy") || { echo "::error::${GITHUB_SHA}:.gstack/ship-policy.json is not valid JSON; fix the policy file" >&2; exit 1; } if [ -z "$train" ] || [ "$train" = null ]; then echo "train=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml index de2b56c46..fd5ed557c 100644 --- a/.github/workflows/evals.yml +++ b/.github/workflows/evals.yml @@ -186,7 +186,7 @@ jobs: with: bun-version: 1.4.2 - # Read with git and jq only (history-read.sh is git alone): no repository code decides whether PR pushes start paid work. + # Read with git and jq only: no repository code decides whether PR pushes start paid work. - name: Read the train policy (base revision) id: policy env: @@ -194,7 +194,16 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | base="${BASE_SHA:-origin/${DEFAULT_BRANCH}}" - policy=$(bash .github/scripts/history-read.sh "$base" .gstack/ship-policy.json --absent null) + ref="$base" file=.gstack/ship-policy.json policy=null + read_failed() { echo "::error::cannot read ${file} at ${ref}. $1 ($(git --version)). Fix: see docs/troubleshooting.md#history-read-failed (HISTORY_READ_FAILED)" >&2; exit 1; } + listed=$(git ls-tree --full-tree "$ref" -- "$file") || read_failed "git ls-tree failed" + if [ -n "$listed" ]; then + for try in 1 2 3; do + policy=$(git show "${ref}:${file}") && break + if [ "$try" = 3 ]; then read_failed "git show failed 3 times"; fi + sleep $((try * 2)) + done + fi train=$(jq -c 'if (.train | type) == "object" then .train else null end' <<<"$policy") || { echo "::error::${base}:.gstack/ship-policy.json is not valid JSON; fix the policy file" >&2; exit 1; } artifact=$(jq -r '.record_artifact // ""' <<<"${train:-null}") check=$(jq -r '.full_gate_check // ""' <<<"${train:-null}") diff --git a/changelog.d/capy-blobless-clones-on-the.md b/changelog.d/capy-blobless-clones-on-the.md index d9812a172..30605a3cf 100644 --- a/changelog.d/capy-blobless-clones-on-the.md +++ b/changelog.d/capy-blobless-clones-on-the.md @@ -2,5 +2,5 @@ ### Changed - **Blobless checkouts** (`filter: blob:none`) in `free-tests.yml` (free-plan), `version-gate.yml` and `evals.yml` (route, screen-summary, plan-slices, eval-slices, eval-rerun, train-bisect, slices-report). The release, CSO, periodic, marathon, sweep and recorder workflows and the main backstop keep full clones. `test/ci-history-read.test.ts` names each with its reason and fails on any other full-history checkout without the filter, printing the file and line to fix. -- **History reads fail loudly.** The train-policy reads in the route and backstop jobs and the Version Gate's base `VERSION` read go through `.github/scripts/history-read.sh`. The Version Gate's train classification goes through its TypeScript twin, `lib/history-read.ts`. Each one checks existence with `git ls-tree`, reads with `git show` (3 tries in a partial clone) and fails with `HISTORY_READ_FAILED` naming the ref, the path and the git version (docs/troubleshooting.md#history-read-failed). A file that is really absent keeps its old meaning. A ship policy that is not valid JSON now fails the route and backstop jobs instead of reading as "no train". The Version Gate goes red when `git fetch origin ` fails 3 times instead of carrying on. +- **History reads fail loudly.** The train-policy reads in the route and backstop jobs stay inline (git and jq only, so no repository code decides whether a PR push starts paid work). The Version Gate's base `VERSION` read goes through `.github/scripts/history-read.sh` from the gate's merge-commit checkout, and its train classification through the TypeScript twin, `lib/history-read.ts`, which also lists the merge base's changed files without rename detection so that diff needs no file contents. Each one checks existence with `git ls-tree`, reads with `git show` (3 tries in a partial clone) and fails with `HISTORY_READ_FAILED` naming the ref, the path and the git version (docs/troubleshooting.md#history-read-failed). A file that is really absent keeps its old meaning. A ship policy that is not valid JSON now fails the route and backstop jobs instead of reading as "no train". The Version Gate goes red when `git fetch origin ` fails 3 times instead of carrying on. - **The eval image asserts partial clone support.** `.github/docker/Dockerfile.ci` makes a blobless clone and fetches a missing blob at build time, so an image whose git cannot do that fails to build. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 59bb4e2a9..5c7d35e74 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2829,10 +2829,11 @@ tree is there, but an older file's contents are fetched from GitHub the first time something reads them. When that fetch fails three times (a GitHub outage, a network drop), or the ref does not resolve at all, the read fails here instead of counting as "the file is absent". A file the commit really does not -have still reads as absent: no ship policy at the base means no train, no -VERSION means `0.0.0.0`, as before. `.github/scripts/history-read.sh` (the -workflow steps) and `lib/history-read.ts` (the Version Gate's train -classification) print this line. The Version Gate prints the same code when +have still reads as absent, as before. No ship policy at the base means no +train, and no VERSION means `0.0.0.0`. The route and backstop policy reads (inline +in their workflows), `.github/scripts/history-read.sh` (the Version Gate's base +`VERSION`) and `lib/history-read.ts` (the Version Gate's train classification) +print this line. The Version Gate prints the same code when `git fetch origin ` fails three times. **Fix.** Rerun the failed job once GitHub is reachable. If it fails the same diff --git a/test/ci-history-read.test.ts b/test/ci-history-read.test.ts index 79061d8cc..8c4d064a7 100644 --- a/test/ci-history-read.test.ts +++ b/test/ci-history-read.test.ts @@ -186,6 +186,7 @@ describe.skipIf(!Bun.which('jq') || !Bun.which('bash'))('the workflow steps that for (const { file, job, name, env } of policySteps) { test(`${file} ${job}: an unfetchable policy fails, a malformed one fails, an absent one routes as before`, () => { const script = stepOf(file, job, name).run!; + expect(script, 'the policy read runs no repository code: git and jq only').not.toMatch(/\.github\/scripts|\bbun\b/); expect(script).not.toContain('|| true'); expect(script).not.toContain('2>/dev/null'); diff --git a/test/evals-train-workflow.test.ts b/test/evals-train-workflow.test.ts index 8d7cd4afc..4e098f78e 100644 --- a/test/evals-train-workflow.test.ts +++ b/test/evals-train-workflow.test.ts @@ -379,8 +379,6 @@ describe.skipIf(!Bun.which('jq') || !Bun.which('bash'))('the route job reads the fs.writeFileSync(path.join(dir, 'README.md'), 'x\n'); git('add', '-A'); git('commit', '-q', '-m', 'base'); - fs.mkdirSync(path.join(dir, '.github/scripts'), { recursive: true }); - fs.copyFileSync(path.join(ROOT, '.github/scripts/history-read.sh'), path.join(dir, '.github/scripts/history-read.sh')); return { dir, base: git('rev-parse', 'HEAD').stdout.trim() }; } function run(script: string, dir: string, env: Record): Record {