Files
ersatztv/docs/decisions/records/ci/jq-accepted-version-set.md
T
timothyandClaude Opus 5.5 8acba80a1a fix(935): state the measured fact (both hosts 1.7 on 2026-10-28), not an unmeasured mixed fleet
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
2026-10-29 22:26:09 +01:00

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.7 parses 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 on major.minor, not a prefix or range test (1.10 and 1.71 do not match 1.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 by scripts/tests/test_jq_preflight.py (test_a_version_outside_the_accepted_set_is_LOUD), and the exact --expect 1.6,1.7 value in pr-checks.yml is pinned by test_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.yml runs the no-args floor-only mode and never --expect: it writes review-verdict/h10, the branch-protection-required status on main, so a version assertion there would turn any runner jq change into a repo-wide merge deadlock. test_review_verdict_never_pins_a_jq_version holds that.
  • Firing the tripwire blocks merges. .claude/hooks/pretooluse-merge-consent.sh reads the combined commit status and denies on anything not success/skipped (ci.advisory-red-blocks-the-merge-gate, #598), and script-tests is a Gitea Actions job folded into it. Only the merge_when_checks_succeed path, 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/Dockerfile cannot reach the gate. review-verdict.yml is runs-on: small with no toolchain-image pin and ci.small-lane-git-only keeps 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.sh accepts only a first line of the form jq-<X>.<Y> or jq version <X>.<Y>; anything else exits 1. If a wrapper banner ever breaks that in the required review-verdict.yml job, 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") (use explode | index(0)); never read jq'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.