Skip to content

ci: pin the spec-fetch flags, and the content pin's human boundary - #58

Merged
brunovenceslau merged 1 commit into
mainfrom
ci/pin-fetch-flags
Aug 11, 2026
Merged

ci: pin the spec-fetch flags, and the content pin's human boundary#58
brunovenceslau merged 1 commit into
mainfrom
ci/pin-fetch-flags

Conversation

@brunovenceslau

Copy link
Copy Markdown
Owner

The flags on the two curls that download the vendored spec were reasoned
about at length and enforced by nothing. A future edit could restore -L or
drop --remove-on-error and every gate would stay green; the comments
explaining them are documentation, not a control. So is the decision that
update-spec must never compute contract.SpecSHA256 itself — a recipe that
regenerated the digest would satisfy the content pin on every re-vendor and
assert nothing at all. Both are now checks.

scripts/fetch-flags.sh asserts that the Makefile's update-spec recipe and
drift.yaml's fetch step each contain exactly one spec-fetch curl, that it
still passes --proto '=https', --tlsv1.2, -fsS, --max-time and
--remove-on-error, and that it passes neither -L nor --location.

Getting at "the fetch" is the whole difficulty, and the first draft got it
wrong in three ways that all had the same root: it matched a joined line of
text rather than a command. Review found working bypasses for each, and
they are now fixtures. Hoisting the URL into a Make variable moved it off
the recipe line, so the gate anchored on a commented-out canonical fetch
left for reference and blessed a live curl -L -k. Demoting the flags to a
trailing comment satisfied every substring check. And a compliant probe
fetch sharing one logical line with an unsafe one vouched for it, because
grep -c counted lines. The extractor now drops comments before joining,
joins continuations, and splits on ; and && so each command stands alone.

The same narrowing removed two false positives, also fixtures now: the
whole update-spec recipe is one continued line, so a cp -L three commands
later read as the curl passing -L, and a --max-time anywhere in the recipe
satisfied the check for the curl.

The digest check reads only the recipe's own command lines. A sed range
ending at the next line starting with a letter swept in the following
comment block — so a comment DOCUMENTING this very rule ("never regenerate
SpecSHA256 with sha256sum") turned local-ci red and accused the maintainer
of what it says not to do.

awk rather than sed for both: joining and splitting want a newline in the
output, and \n in a sed replacement is a GNU extension BSD sed rejects.
That trap already cost this stack one commit; CI is Linux-only and would
not have caught it either time.

The gate deliberately does not police every curl in the repo.
go-latest-check and check-version.sh both pass -L legitimately — they parse
a string into a variable rather than vendoring bytes to disk, so a redirect
costs them nothing. A repo-wide ban would either break them or teach the
next maintainer that this gate is noise to route around, and a gate people
route around is worse than no gate. A compliant pair with a legitimate -L
beside it is a passing fixture.

It also fails if it can no longer FIND the fetch it guards, in either
direction: zero matches or two. A grep gate's characteristic failure is
passing on everything, and "the file was restructured and my pattern
stopped matching" is how that happens quietly.

fetch-flags-selftest drives 27 hand-written fixtures, no network. Fixtures
are hand-written rather than derived from the real files, because the
gate's whole job is to notice those files changing. An earlier review
injected 11 independent mutations into the gate — an emptied flag loop,
inverted case arms, fail=1 removed from bad(), the -L regex neutered, the
final exit turned into 0 — and all 11 went red.

Both scripts are shellcheck-clean. Wired into local-ci and ci.yaml beside
the other selftests. Tooling only: no Go file, no module byte, no public
surface, and no CHANGELOG line — CONTRIBUTING scopes that to user-visible
changes.

The flags on the two curls that download the vendored spec were reasoned
about at length and enforced by nothing. A future edit could restore -L or
drop --remove-on-error and every gate would stay green; the comments
explaining them are documentation, not a control. So is the decision that
update-spec must never compute contract.SpecSHA256 itself — a recipe that
regenerated the digest would satisfy the content pin on every re-vendor and
assert nothing at all. Both are now checks.

scripts/fetch-flags.sh asserts that the Makefile's update-spec recipe and
drift.yaml's fetch step each contain exactly one spec-fetch curl, that it
still passes --proto '=https', --tlsv1.2, -fsS, --max-time and
--remove-on-error, and that it passes neither -L nor --location.

Getting at "the fetch" is the whole difficulty, and the first draft got it
wrong in three ways that all had the same root: it matched a joined line of
text rather than a command. Review found working bypasses for each, and
they are now fixtures. Hoisting the URL into a Make variable moved it off
the recipe line, so the gate anchored on a commented-out canonical fetch
left for reference and blessed a live `curl -L -k`. Demoting the flags to a
trailing comment satisfied every substring check. And a compliant probe
fetch sharing one logical line with an unsafe one vouched for it, because
grep -c counted lines. The extractor now drops comments before joining,
joins continuations, and splits on ; and && so each command stands alone.

The same narrowing removed two false positives, also fixtures now: the
whole update-spec recipe is one continued line, so a `cp -L` three commands
later read as the curl passing -L, and a --max-time anywhere in the recipe
satisfied the check for the curl.

The digest check reads only the recipe's own command lines. A sed range
ending at the next line starting with a letter swept in the following
comment block — so a comment DOCUMENTING this very rule ("never regenerate
SpecSHA256 with sha256sum") turned local-ci red and accused the maintainer
of what it says not to do.

awk rather than sed for both: joining and splitting want a newline in the
output, and \n in a sed replacement is a GNU extension BSD sed rejects.
That trap already cost this stack one commit; CI is Linux-only and would
not have caught it either time.

The gate deliberately does not police every curl in the repo.
go-latest-check and check-version.sh both pass -L legitimately — they parse
a string into a variable rather than vendoring bytes to disk, so a redirect
costs them nothing. A repo-wide ban would either break them or teach the
next maintainer that this gate is noise to route around, and a gate people
route around is worse than no gate. A compliant pair with a legitimate -L
beside it is a passing fixture.

It also fails if it can no longer FIND the fetch it guards, in either
direction: zero matches or two. A grep gate's characteristic failure is
passing on everything, and "the file was restructured and my pattern
stopped matching" is how that happens quietly.

fetch-flags-selftest drives 27 hand-written fixtures, no network. Fixtures
are hand-written rather than derived from the real files, because the
gate's whole job is to notice those files changing. An earlier review
injected 11 independent mutations into the gate — an emptied flag loop,
inverted case arms, fail=1 removed from bad(), the -L regex neutered, the
final exit turned into 0 — and all 11 went red.

Both scripts are shellcheck-clean. Wired into local-ci and ci.yaml beside
the other selftests. Tooling only: no Go file, no module byte, no public
surface, and no CHANGELOG line — CONTRIBUTING scopes that to user-visible
changes.
@brunovenceslau
brunovenceslau merged commit 67d0141 into main Aug 11, 2026
4 of 5 checks passed
@brunovenceslau
brunovenceslau deleted the ci/pin-fetch-flags branch August 11, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant