The fleet is uniform on jq 1.7 per the job logs; 1.6 stays in the set only because the image tag is unpinned. Real-jq 1.6 coverage is currently absent, and the shims cover only the empty-input -e quirk, not the NUL or parse-error ones. Measurement dated by the job date. Remaining "runner's jq 1.6" comments reworded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: f9ce6bc6-5b36-491a-8d72-89c1f70dd3e1
6.8 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| ci.jq-accepted-version-set | 2026-10-29 — jq 1.6 stays the FLOOR; `script-tests` accepts exactly the SET {1.6, 1.7} until server-management#1080 pins the runner fleet (#935) | active | 2026-10-29 | ci.jq-version-contract@2026-07-26 | none | Every shell gate that shells out to `jq` is authored to the jq 1.6-compatible subset (floor 1.6, unchanged). Both `small` runners map to the floating `runner-images:ubuntu-latest` image with no digest pin, so the fleet jq version is not guaranteed; measured 2026-10-28, both hosts report jq 1.7 and no known runner reports 1.6. `scripts/jq-preflight.sh --expect 1.6,1.7` in the `script-tests` job accepts exactly that comma-separated SET and fails loudly on any other version; 1.6 stays in the set only because the tag is unpinned, and real-jq 1.6 coverage of `scripts/tests/` is currently absent. server-management#1080 (fleet digest pin) is the event that narrows the set to one version. `review-verdict.yml` stays floor-only (no `--expect`): it writes the branch-protection-required `review-verdict/h10` status. | jq version divergence, jq 1.6 vs 1.7, script-tests jq tripwire red, jq-1.7 (parsed 1.7; floor 1.6), --expect set, unpinned runner image, floating runner-images ubuntu-latest, server-management#1080, jq-preflight, merge blocked by combined status · paths: `scripts/jq-preflight.sh`, `scripts/tests/test_jq_preflight.py`, `.gitea/workflows/pr-checks.yml`, `.gitea/workflows/review-verdict.yml`, `docs/ci-cd.md` · issues: #935, #648, #643, #647 | `scripts/jq-preflight.sh` (no args) -> floor+observability in every gate job; `scripts/jq-preflight.sh --expect 1.6,1.7` -> set tripwire, `script-tests` job only (`test_jq_preflight.py` pins the exact value); `docs/ci-cd.md` -> "The jq contract" |
Supersedes ci.jq-version-contract (2026-07-26, archived), whose premise was that the CI runner
ships jq 1.6. That is no longer a fact the repo can state.
Measured 2026-10-28. script-tests' tripwire (--expect 1.6) went red on jazz-small-runner
(PR #933, job 12347) and on bumblebee's small-runner (PR #934, job 12360), each reporting
jq-1.7 (parsed 1.7; floor 1.6). Cause (server-management#1080): both small runners map to the
floating runner-images:ubuntu-latest image with no digest pin or forced pull, so each host keeps
whatever it last pulled and the fleet's jq version is a function of pull history. A digest pin across
the fleet is proposed in server-management#1080 and not yet decided.
The invariant. script-tests accepts exactly the set {1.6, 1.7}:
scripts/jq-preflight.sh --expect 1.6,1.7parses the argument as a comma-separated set of<major>.<minor>members; an empty or free-form member (1.6,,1.6,,1.7,1.x) is a usage error (exit 2), never a member that matches nothing or everything. Membership is exact onmajor.minor, not a prefix or range test (1.10and1.71do not match1.7/1.6).- A version outside the set (
1.8, which every developer Mac ships,2.x) exits 1 with the tripwire message. That red is witnessed byscripts/tests/test_jq_preflight.py(test_a_version_outside_the_accepted_set_is_LOUD), and the exact--expect 1.6,1.7value inpr-checks.ymlis pinned bytest_script_tests_pins_the_jq_version. - The set is exactly {1.6, 1.7}, not "1.6 or newer": widening it to every version would delete the tripwire. Admitting 1.7 is evidence-driven (both measured hosts run it); admitting 1.8 is not, and a 1.8 runner should go red so a person decides.
The floor stays at 1.6. Raising it would assert that no host in the fleet can report 1.6, which
the unpinned image does not guarantee, and which only the #1080 decision can establish. A gate
authored to the 1.6 subset is harmless on 1.7, and the three divergences that motivated the contract
(jq -e over empty input, contains("\u0000"), the parse-error exit code) remain real on whichever
host still runs 1.6.
What coverage is lost. As of 2026-10-28 no known runner provides jq 1.6, so the real-jq 1.6 run
of scripts/tests/ is currently absent; it would happen only if a host held an older image. In-repo
jq shims reproduce ONE of the three 1.6 divergences: jq -e over empty input exiting 0
(test_pr_changed_files.py, test_merge_consent_exemption.py). The contains("\u0000") truncation
and the parse-error exit code (4 vs 5) have no shim, so on a 1.7 run they are covered only by the
constructs having been replaced with version-stable ones, and any NEW 1.6-only divergence is
uncovered.
What narrows the set. server-management#1080. When it pins the fleet to one jq version, change
the --expect value in pr-checks.yml to that single version (the script accepts a one-member set)
and record the pin here by superseding this record. If the pin lands on 1.7, decide then whether the
floor rises, with the fleet evidence in hand.
Unchanged from the superseded record.
- Pin vs floor is asymmetric.
review-verdict.ymlruns the no-args floor-only mode and never--expect: it writesreview-verdict/h10, the branch-protection-required status onmain, so a version assertion there would turn any runner jq change into a repo-wide merge deadlock.test_review_verdict_never_pins_a_jq_versionholds that. - Firing the tripwire blocks merges.
.claude/hooks/pretooluse-merge-consent.shreads the combined commit status and denies on anything notsuccess/skipped(ci.advisory-red-blocks-the-merge-gate, #598), andscript-testsis a Gitea Actions job folded into it. Only themerge_when_checks_succeedpath, which defers to Gitea's required checks, is spared. This is the cost of the tripwire, accepted because the remedy is a one-line edit to--expect, and the 2026-10-28 firing is that cost being paid. - Baking jq into
docker/ci/Dockerfilecannot reach the gate.review-verdict.ymlisruns-on: smallwith no toolchain-image pin andci.small-lane-git-onlykeeps that lane git-only, so it gets the host's jq whatever the toolchain image holds. The fleet pin in server-management#1080 is the only lever that reaches it. - The parse is strictly fail-closed.
jq-preflight.shaccepts only a first line of the formjq-<X>.<Y>orjq version <X>.<Y>; anything else exits 1. If a wrapper banner ever breaks that in the requiredreview-verdict.ymljob, widen the accepted forms, never relax fail-closed: an unparsed version must not satisfy the floor. - The three constructs to avoid: never infer "empty input" from a jq exit status (check the string
in shell first); never use
contains("\u0000")(useexplode | index(0)); never readjq's exit code as a specific parse-error signal on ambiguous input (validate the response shape).
Full rationale for how the three divergences were found is in
docs/decisions/archive/ci/jq-version-contract.md and docs/decisions/records/ci/script-tests-job.md.