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 {