mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 06:55:40 +00:00
ci(nix): a Go bump's Nix check is green when the build passes, and main heals its own vendorHash (TASK-2954) (#1292)
`vendorHash` pins the Go module set by content hash. Dependabot updates go.mod and go.sum and has no idea `nix/package.nix` exists, so every Go-dependency bump PR failed `Nix build & check` on a fixed-output hash mismatch — structurally, and forever. Both open Go bumps (#1274, #1275) are red that way today; the npm-side bumps (#1276, #1277) are green, which is the control that isolates the cause. A permanently-red check is not a check: a bump that genuinely breaks the build looks identical, at a glance, to one that only moved the hash. Two halves, and they are deliberately in different places. THE CHECK IS MADE HONEST WITHOUT A TOKEN. Every Nix run recomputes the hash in its working tree before the build steps judge it, so green means the build passed with that ref's actual module set. This runs for every author, not just Dependabot: a human's own go.mod change moves the hash the same way, and a check that is honest for one author only is the shape this removes. MAIN HEALS ITSELF ONE COMMIT AFTER A MERGE. The corrected file cannot be pushed from a Dependabot PR run: such a run gets a read-only GITHUB_TOKEN — it runs as if from a fork — and the `permissions` key does NOT lift that. Only a repository-wide setting does, and that setting would hand fork PRs write tokens on a public repo, which is the surface CONVE-2438 exists to keep closed. The merge, however, is authored by a human, so the `push: main` run that follows is ordinary. A separate job with the only `contents: write` in the file commits the recomputed value there, gated on the JOB (a step-level `if` is not a boundary — the job would still hold the token and checkout persists it), on the build having passed, and on this push having touched go.mod or go.sum. The loop guard is the commit's own contents: the bot commit touches nix/package.nix and nothing else, so the run it triggers finds go.mod and go.sum unchanged and stops at the first gate. Not a heuristic about who pushed — the fix cannot invalidate the hash it just wrote. Three ways that gate could have lost a heal, all closed. A cancelled main run's heal is never retried — the next push's run recomputes correctly but its gate sees only its OWN commits — so `cancel-in-progress` is now `pull_request`-only; and that alone is not enough, because GitHub holds only ONE pending run per concurrency group and a third push evicts the queued second, which looks exactly like a run that found nothing to do — so push runs get a per-commit group that nothing can evict. The gate also compares `before..after` on a full clone rather than `HEAD~1..HEAD` on a two-commit one, because a direct push of several commits can carry the go.sum change anywhere in the range. A concurrent merge makes the push a non-fast-forward: the job goes red rather than overwriting, and that merge's own run heals. WHAT THE PARSER REFUSES is the whole correctness argument. This build has many fixed-output derivations: every npm tarball `importNpmLock` fetches is one, and a mismatch in any of them prints the same block with a `got:` line. Taking "the got: hash" writes a tarball's hash into `vendorHash` and looks like it worked — which is what package.nix's old comment told a human to do by eye. So each line is stripped of its runner timestamp and indentation SEPARATELY and matched whole, and a header counts only if it says `error:`, names a single store path segment that STARTS with a 32-character store hash then `-pad-` and ends `-go-modules.drv':` with nothing after it — identity, not resemblance, since `-pad-` anywhere in the name also matches `…-other-pad-tool-…-go-modules.drv`; the hash is then taken only from a canonical-length `got:` on the line IMMEDIATELY after a canonical-length `specified:`. Anything else exits 1 having written nothing, and the build stays red. 40 assertions in nix/bump-vendor-hash_test.sh, wired into the CI Go job and `make test-nix-hash` — in ci.yml rather than nix.yml because otherwise nothing on an ordinary PR would run it, and a break would surface on the next bump. 14 mutants, all killed — but five of them survived the first suite that claimed to cover them, each because the case written for the rule was ALSO refused for a second reason and so discriminated nothing about it: short hashes on both lines never exercised either length rule on its own, and a nested path that also had a malformed store hash never exercised the single-segment rule. A sixth, dropping the canonical length from the `got:` condition, survived because the extractor re-stated the rule; the fix was to state it once. Portability is checked, not assumed: `awk` is gawk here and mawk on the runners, so the suite re-runs itself under mawk, gawk and busybox and is green only if all agree. The parser also had a real defect — it worked only on timestamped CI logs, not the local log package.nix tells a human to produce — found by asserting an exit code rather than file contents, because for a no-op input "did not write" and "could not parse" leave identical files. Claude-Session: https://claude.ai/code/session_01Xk9M5UVPdc84xL5E1mZkm8
This commit is contained in:
@@ -62,6 +62,16 @@ jobs:
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
# The Nix vendorHash bump parser (TASK-2954). Its own gate lives HERE, in
|
||||
# the Go job, and not in nix.yml: over there the script only does anything
|
||||
# when a build has ALREADY failed on a hash mismatch, so an ordinary PR
|
||||
# would exercise none of its logic and a change that broke it would land
|
||||
# green and surface on the next dependency bump. The load-bearing case in
|
||||
# the suite is negative — an npm fetchurl FOD mismatch must NOT be written
|
||||
# into vendorHash.
|
||||
- name: Test the Nix vendorHash bump parser
|
||||
run: nix/bump-vendor-hash_test.sh
|
||||
|
||||
- name: Run golangci-lint
|
||||
# only-new-issues: false means CI fails on ANY linter finding,
|
||||
# not just findings on PR-changed lines. The IDEA-732 cleanup
|
||||
|
||||
+171
-2
@@ -7,8 +7,23 @@ on:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: nix-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# PR runs share a group per ref and supersede each other — a newer push to the
|
||||
# same PR makes the older run's answer worthless.
|
||||
#
|
||||
# PUSH runs get a group of their OWN, one per commit, and this is deliberate
|
||||
# (TASK-2954). A run on `main` may owe main a vendorHash heal, and losing it
|
||||
# loses the fix SILENTLY: the next push's run recomputes correctly, but its
|
||||
# go.mod/go.sum gate sees only its own commits, so nothing ever retries the
|
||||
# skipped heal. `cancel-in-progress: false` is not enough to prevent that —
|
||||
# GitHub holds only ONE pending run per group, so a third push evicts the
|
||||
# queued second, and the eviction looks exactly like a run that decided there
|
||||
# was nothing to do. A per-commit group cannot be evicted by anything.
|
||||
#
|
||||
# What that trades for: two main runs can reach the push step at once, and the
|
||||
# loser gets a non-fast-forward rejection and goes red. That is the right
|
||||
# direction — a red job is read, and the next merge's run heals anyway.
|
||||
group: nix-${{ github.event_name == 'push' && github.sha || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -21,11 +36,62 @@ jobs:
|
||||
nix:
|
||||
name: Nix build & check
|
||||
runs-on: ubuntu-latest
|
||||
# NO write permission here, deliberately — see `push-vendor-hash` below.
|
||||
# This job builds; it never pushes.
|
||||
outputs:
|
||||
vendor_hash_bumped: ${{ steps.vendorhash.outputs.bumped }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
|
||||
|
||||
# THE RED-BASELINE FIX (TASK-2954). `vendorHash` pins the Go module set by
|
||||
# content hash; Dependabot updates go.mod/go.sum and has no idea this file
|
||||
# exists, so EVERY Go bump PR failed the steps below on a hash mismatch,
|
||||
# structurally and forever. A check that is always red is not a check —
|
||||
# a bump that genuinely breaks the build looked identical, at a glance, to
|
||||
# one that only moved the hash.
|
||||
#
|
||||
# This recomputes the hash in the WORKING TREE, so the steps below judge
|
||||
# the build with this ref's actual module set. Nothing is pushed from here
|
||||
# and no token is used: a Dependabot PR run gets a read-only GITHUB_TOKEN
|
||||
# (it runs as if from a fork), and the `permissions` key does NOT lift that
|
||||
# — only a repository-wide setting does, which would hand fork PRs write
|
||||
# tokens too. The push therefore happens on `push: main` instead, in the
|
||||
# job below, where the run is not Dependabot's.
|
||||
#
|
||||
# It runs on every run, not just Dependabot's: a human's own go.mod change
|
||||
# moves the hash the same way, and a check that is honest only for one
|
||||
# author is the shape this item exists to remove.
|
||||
#
|
||||
# It runs BEFORE `nix flake check`, which builds the package too and would
|
||||
# hit the same mismatch first. It is deliberately not a gate: if the build
|
||||
# fails for any reason other than a go-modules hash mismatch, the script
|
||||
# exits non-zero, this step still succeeds, and the real steps below report
|
||||
# the real failure. The gate stays exactly where it was.
|
||||
- name: Recompute vendorHash before judging the build
|
||||
id: vendorhash
|
||||
run: |
|
||||
set -o pipefail
|
||||
if nix build .#default --print-build-logs 2>&1 | tee /tmp/nix-build.log; then
|
||||
echo "Build is green; vendorHash is current."
|
||||
exit 0
|
||||
fi
|
||||
if ! new_hash="$(nix/bump-vendor-hash.sh /tmp/nix-build.log)"; then
|
||||
echo "Not a go-modules hash mismatch — leaving it to the build steps below."
|
||||
exit 0
|
||||
fi
|
||||
echo "vendorHash -> $new_hash"
|
||||
echo "bumped=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Carry the recomputed package.nix to the push job
|
||||
if: steps.vendorhash.outputs.bumped == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: recomputed-package-nix
|
||||
path: nix/package.nix
|
||||
retention-days: 1
|
||||
|
||||
- name: nix flake check
|
||||
run: nix flake check --print-build-logs
|
||||
|
||||
@@ -62,3 +128,106 @@ jobs:
|
||||
run: |
|
||||
GOTOOLCHAIN=auto go install golang.org/x/vuln/cmd/govulncheck@v1.2.0
|
||||
GOVULNCHECK="$(go env GOPATH)/bin/govulncheck" nix/vulnscan.sh result/bin/pad
|
||||
|
||||
# THE ONLY JOB WITH A WRITE TOKEN, and it exists as a separate job precisely so
|
||||
# that the token is never minted in a context an untrusted change could reach.
|
||||
#
|
||||
# An `if:` on a STEP is not a security boundary — the job would still hold a
|
||||
# write-capable GITHUB_TOKEN, and `actions/checkout` persists it in the
|
||||
# workspace for every later command, so any change to this file inside a PR
|
||||
# could use it. An `if:` on a JOB is different in kind: the job does not start,
|
||||
# and no write token is issued for that run at all.
|
||||
#
|
||||
# WHY ON `push: main` AND NOT ON THE PR (ruled on TASK-2954's trail). A
|
||||
# Dependabot PR run gets a read-only token whatever `permissions` says. The
|
||||
# merge, though, is authored by a human, so the `push` run that follows is an
|
||||
# ordinary run with an ordinary token. `main` self-heals one commit after the
|
||||
# merge — usually one commit after it; see the non-fast-forward note below for
|
||||
# the case where it takes the next merge instead — and the PR's own check was
|
||||
# already honest because the job above recomputes in the working tree. The two rejected alternatives were a PAT in
|
||||
# the Dependabot secret store (a standing credential for a once-a-week fix) and
|
||||
# enabling write tokens for pull-request workflows (which would give them to
|
||||
# FORK PRs on a public repo — the surface CONVE-2438 exists to keep closed).
|
||||
#
|
||||
# THE LOOP GUARD IS THE COMMIT'S OWN CONTENTS. This job pushes a commit that
|
||||
# touches `nix/package.nix` and nothing else. That push triggers this workflow
|
||||
# again, the first gate below asks whether go.mod or go.sum moved across the
|
||||
# push's range, and for the bot's own commit the answer is no — so the second
|
||||
# run stops there. The gate is not a heuristic about who pushed; it is the fact
|
||||
# that the fix cannot invalidate the hash it just wrote.
|
||||
#
|
||||
# If another merge lands while this job runs, the push is REJECTED as a
|
||||
# non-fast-forward and this job goes red rather than overwriting anything. The
|
||||
# heal is not lost: that merge's own run recomputes and heals. The window is
|
||||
# the length of a nix build, and the failure mode is loud rather than silent —
|
||||
# which is the same trade the per-commit concurrency group above makes.
|
||||
#
|
||||
# `needs: nix` without `if: always()` is the second half: this runs only when
|
||||
# the build job SUCCEEDED, so a module set whose corrected tree still fails
|
||||
# `nix flake check`, the smoke test or the vulnerability scan is never
|
||||
# committed to main.
|
||||
push-vendor-hash:
|
||||
name: Heal main's vendorHash
|
||||
needs: nix
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
needs.nix.outputs.vendor_hash_bumped == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Full history: the gate below asks what THIS PUSH changed, which is
|
||||
# `before..after`, not `HEAD~1..HEAD`. A direct push of several commits
|
||||
# can carry the go.sum change anywhere in the range, and a two-commit
|
||||
# clone would see only the last one and skip a heal that was owed.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Did this push touch the Go module set?
|
||||
id: gate
|
||||
env:
|
||||
BEFORE: ${{ github.event.before }}
|
||||
AFTER: ${{ github.sha }}
|
||||
run: |
|
||||
# A branch created by this push, or a `before` the clone cannot resolve
|
||||
# (a force push whose old tip is gone), leaves the range unanswerable.
|
||||
# Fall through to the hash comparison rather than skipping: the second
|
||||
# gate is the one that decides, and this one only avoids pointless work.
|
||||
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ] \
|
||||
|| ! git rev-parse --verify -q "$BEFORE^{commit}" >/dev/null; then
|
||||
echo "Push range unresolvable; deferring to the hash comparison."
|
||||
echo "touched=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BEFORE" "$AFTER" | command grep -qE '^go\.(mod|sum)$'; then
|
||||
echo "touched=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "go.mod/go.sum unchanged across $BEFORE..$AFTER — nothing here could have moved the hash."
|
||||
echo "touched=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
if: steps.gate.outputs.touched == 'true'
|
||||
with:
|
||||
name: recomputed-package-nix
|
||||
path: nix/
|
||||
|
||||
- name: Commit and push
|
||||
if: steps.gate.outputs.touched == 'true'
|
||||
run: |
|
||||
# The second gate. The artifact only exists because the build job found
|
||||
# a mismatch, but comparing here keeps the job honest about the tree it
|
||||
# is actually looking at rather than about what another job reported.
|
||||
if git diff --quiet -- nix/package.nix; then
|
||||
echo "package.nix already matches the recomputed hash; nothing to push."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git add nix/package.nix
|
||||
git commit -m "chore(nix): heal vendorHash for the module set in ${SHA} (TASK-2954)"
|
||||
git push
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: build test test-pg test-pg-down test-pg-project dev clean web dev-web serve restart lint install check vuln web-check web-test web-audit
|
||||
.PHONY: build test test-nix-hash test-pg test-pg-down test-pg-project dev clean web dev-web serve restart lint install check vuln web-check web-test web-audit
|
||||
|
||||
BINARY=pad
|
||||
BUILD_DIR=./cmd/pad
|
||||
@@ -55,6 +55,12 @@ install: build
|
||||
test:
|
||||
go test -timeout=45m ./... -v
|
||||
|
||||
# The vendorHash bump parser (TASK-2954). Pure bash, no toolchain, ~1s — its own
|
||||
# target rather than part of `test`, which means `go test`. CI runs this same
|
||||
# script in the Go job so a change to the parser is gated rather than trusted.
|
||||
test-nix-hash:
|
||||
@nix/bump-vendor-hash_test.sh
|
||||
|
||||
# Run tests against PostgreSQL (starts a container automatically).
|
||||
#
|
||||
# THE HOST PORT IS EPHEMERAL (TASK-2708). It was hardcoded to 5445, which let
|
||||
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Rewrite `vendorHash` in nix/package.nix from a failed `nix build` log.
|
||||
#
|
||||
# WHY THIS EXISTS (TASK-2954). `vendorHash` pins the Go module set by content
|
||||
# hash, so any go.mod/go.sum change invalidates it. Dependabot updates go.mod
|
||||
# and go.sum and has no idea this file exists, so every Go-dependency bump PR
|
||||
# failed `Nix build & check` on a hash mismatch — structurally, forever. A
|
||||
# permanently-red check is not a check: a bump that genuinely breaks the build
|
||||
# looked identical, at a glance, to one that only moved the hash.
|
||||
#
|
||||
# WHAT IT IS SCOPED TO, AND WHY THAT IS THE WHOLE CORRECTNESS ARGUMENT. `nix
|
||||
# build` prints "hash mismatch in fixed-output derivation" for ANY FOD, and this
|
||||
# build has many: the Go module set is one, and every per-package `fetchurl`
|
||||
# that `importNpmLock` generates from web/package-lock.json is another. Taking
|
||||
# "the `got:` hash" from such a log would happily write an npm tarball's hash
|
||||
# into `vendorHash` — a green-looking rewrite that pins the wrong thing. So the
|
||||
# match is anchored to the derivation whose name ends in `-go-modules.drv`, and
|
||||
# a log with no such block is NOT an error this script can fix: it exits 1
|
||||
# having changed nothing, and the caller leaves the build red.
|
||||
#
|
||||
# Usage: bump-vendor-hash.sh <build-log> [package.nix]
|
||||
# Exit: 0 = rewritten (new hash on stdout)
|
||||
# 1 = no go-modules hash mismatch in the log; nothing written
|
||||
# 2 = usage / the file does not look like what we expect
|
||||
set -euo pipefail
|
||||
|
||||
log="${1:-}"
|
||||
pkg="${2:-nix/package.nix}"
|
||||
|
||||
if [[ -z "$log" || ! -r "$log" ]]; then
|
||||
echo "usage: $0 <build-log> [package.nix]" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -w "$pkg" ]]; then
|
||||
echo "$0: $pkg is not writable" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Find the `got:` line belonging to the go-modules mismatch block.
|
||||
#
|
||||
# The block nix prints is:
|
||||
#
|
||||
# error: hash mismatch in fixed-output derivation '/nix/store/…-pad-…-go-modules.drv':
|
||||
# specified: sha256-…
|
||||
# got: sha256-…
|
||||
#
|
||||
# and in a CI log every line carries a runner timestamp in front of it. The
|
||||
# timestamp is stripped SEPARATELY from the indentation, because a log captured
|
||||
# locally (`nix build 2>&1 | tee`) has the indentation and no timestamp — the
|
||||
# first version folded the two and worked only on CI logs, which the suite's
|
||||
# exit-code assertion is what caught. (The
|
||||
# timestamp pattern avoids interval quantifiers like `{4}`, which older `mawk` —
|
||||
# the default `awk` on the ubuntu runners — does not accept; the suite re-runs
|
||||
# itself under every awk on the box for exactly this reason). So each
|
||||
# line is stripped of a leading ISO timestamp and then matched WHOLE — anchored
|
||||
# at both ends — rather than searched for a substring. Substring matching is what
|
||||
# lets ordinary build output impersonate a nix diagnostic: a `warning:` carrying
|
||||
# the same phrase, a path that merely CONTAINS `-go-modules.drv`, or a log line
|
||||
# that happens to say `got:` next to a hash would each be enough.
|
||||
#
|
||||
# Four things the header must satisfy, all of them narrowing:
|
||||
# - `error:`, not `warning:` or any other severity;
|
||||
# - `/nix/store/` followed by a single path SEGMENT — no further `/`, which is
|
||||
# what makes it a store path rather than any path ending in the right
|
||||
# characters;
|
||||
# - that segment ends `-go-modules.drv':` with nothing at all after it;
|
||||
# - the segment begins with a 32-character store hash and then `-pad-`, which
|
||||
# is derivation IDENTITY rather than a resemblance: `-pad-` matched anywhere
|
||||
# in the name would also accept `…-other-pad-tool-…-go-modules.drv`, whose
|
||||
# hash is not ours. If this project's pname ever changes, this stops matching
|
||||
# and the script exits 1 — red, and a human reads the log. That is the
|
||||
# intended direction to fail in.
|
||||
#
|
||||
# The hashes are matched at their CANONICAL LENGTH (43 base64 characters plus
|
||||
# the `=`), not as `sha256-<anything>`. A truncated or hand-typed value in a log
|
||||
# is then not something this can write into the build.
|
||||
#
|
||||
# The `got:` line is accepted only as the line IMMEDIATELY AFTER a `specified:`
|
||||
# line inside an armed block (`ready == NR - 1`), because after the timestamp
|
||||
# strip a `got: sha256-…` line from any other producer is byte-indistinguishable
|
||||
# from nix's. Sequence alone was not enough — a valid header, a valid
|
||||
# `specified:`, and then somebody else's `got:` twenty lines later would have
|
||||
# been accepted. Adjacency is the binding: nix prints the two together.
|
||||
#
|
||||
# Arming disarms on the next `error:` line, so a mismatch whose `got:` never
|
||||
# arrives (interleaved output) yields nothing rather than the next FOD's hash.
|
||||
new_hash="$(awk '
|
||||
{
|
||||
body = $0
|
||||
sub(/^[0-9][0-9-]*T[0-9][0-9:.]*Z/, "", body)
|
||||
sub(/^[ \t]+/, "", body)
|
||||
sub(/[\r]+$/, "", body)
|
||||
}
|
||||
body ~ /^error: hash mismatch in fixed-output derivation \x27\/nix\/store\/[0-9a-z]{32}-pad-[^\x27\/]*-go-modules\.drv\x27:$/ {
|
||||
armed = 1
|
||||
ready = 0
|
||||
next
|
||||
}
|
||||
armed && body ~ /^specified:[ \t]+sha256-[A-Za-z0-9+\/]{43}=$/ {
|
||||
ready = NR
|
||||
next
|
||||
}
|
||||
armed && ready == NR - 1 && body ~ /^got:[ \t]+sha256-[A-Za-z0-9+\/]{43}=$/ {
|
||||
# The line is already matched WHOLE above, so the hash is what is left
|
||||
# after the label. Extracting with a second pattern would put the
|
||||
# canonical-length rule in two places, and a mutation run showed what
|
||||
# that costs: dropping it from the condition alone changed nothing,
|
||||
# because the extractor still enforced it. One rule, one place.
|
||||
hash = body
|
||||
sub(/^got:[ \t]+/, "", hash)
|
||||
print hash
|
||||
exit
|
||||
}
|
||||
body ~ /^error:/ { armed = 0 }
|
||||
' "$log")"
|
||||
|
||||
if [[ -z "$new_hash" ]]; then
|
||||
echo "$0: no go-modules hash mismatch in $log — leaving $pkg alone" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -qE '^[[:space:]]*vendorHash[[:space:]]*=[[:space:]]*"sha256-[A-Za-z0-9+/]+=*";[[:space:]]*$' "$pkg"; then
|
||||
echo "$0: no vendorHash line to rewrite in $pkg" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# The replacement is anchored on the whole line, and the hash is injected via an
|
||||
# awk variable rather than interpolated into a sed script: a base64 hash contains
|
||||
# `/` and `+`, which are a sed delimiter and a regex metacharacter respectively.
|
||||
tmp="$(mktemp)"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
awk -v h="$new_hash" '
|
||||
/^[[:space:]]*vendorHash[[:space:]]*=[[:space:]]*"sha256-/ {
|
||||
match($0, /^[[:space:]]*/)
|
||||
printf "%svendorHash = \"%s\";\n", substr($0, 1, RLENGTH), h
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$pkg" > "$tmp"
|
||||
cat "$tmp" > "$pkg"
|
||||
|
||||
echo "$new_hash"
|
||||
Executable
+367
@@ -0,0 +1,367 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Tests for bump-vendor-hash.sh (TASK-2954).
|
||||
#
|
||||
# The load-bearing case is NEGATIVE: this build's npm dependencies are
|
||||
# per-package `fetchurl` derivations, so a log can carry a "hash mismatch in
|
||||
# fixed-output derivation" block that has nothing to do with `vendorHash`.
|
||||
# A parser that takes "the got: hash" would write an npm tarball's hash into
|
||||
# vendorHash and the rewrite would look like it worked. Cases 3, 4 and 5 are
|
||||
# there to fail if the anchoring on `-go-modules.drv` is ever loosened.
|
||||
#
|
||||
# The two positive logs are the REAL text from the failed runs on PR #1274 and
|
||||
# #1275, timestamps included, because the timestamps are what break a
|
||||
# fixed-offset parser.
|
||||
set -uo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
script="$here/bump-vendor-hash.sh"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
ORIG='sha256-8L7gH7Yy5+Fig3wK2SPLYSJjcY9nF/jumQ7PATJ3RIE='
|
||||
|
||||
# A package.nix stub carrying the real vendorHash line at its real indentation.
|
||||
make_pkg() {
|
||||
cat > "$1" <<EOF
|
||||
{
|
||||
pname = "pad";
|
||||
|
||||
# Update alongside go.sum.
|
||||
vendorHash = "$ORIG";
|
||||
|
||||
subPackages = [ "cmd/pad" ];
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
check() {
|
||||
local name="$1" want="$2" got="$3"
|
||||
if [[ "$want" == "$got" ]]; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
echo "FAIL: $name"
|
||||
echo " want: $want"
|
||||
echo " got: $got"
|
||||
fi
|
||||
}
|
||||
|
||||
hash_in() { grep -oE 'sha256-[A-Za-z0-9+/]+=*' "$1" | head -1; }
|
||||
|
||||
# ---------------------------------------------------------------- case 1
|
||||
# The real log from PR #1274 (go-minor-and-patch, 4 updates).
|
||||
log="$work/1274.log"
|
||||
cat > "$log" <<'EOF'
|
||||
2026-09-07T13:11:02.5087484Z error: hash mismatch in fixed-output derivation '/nix/store/ghbb18gfh6pl8vqf1fzqrfcsjk4ikh8z-pad-0.15.0-go-modules.drv':
|
||||
2026-09-07T13:11:02.5093617Z specified: sha256-8L7gH7Yy5+Fig3wK2SPLYSJjcY9nF/jumQ7PATJ3RIE=
|
||||
2026-09-07T13:11:02.5094211Z got: sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=
|
||||
2026-09-07T13:11:03.5316739Z ##[error]To correct the hash mismatch for pad-0.15.0-go-modules, use "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk="
|
||||
EOF
|
||||
pkg="$work/p1.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"; rc=$?
|
||||
check "1274: exit 0" "0" "$rc"
|
||||
check "1274: prints the new hash" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$out"
|
||||
check "1274: rewrites the file" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$(hash_in "$pkg")"
|
||||
check "1274: keeps the indentation" ' vendorHash = "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=";' "$(grep vendorHash "$pkg")"
|
||||
|
||||
# ---------------------------------------------------------------- case 2
|
||||
# The real log from PR #1275 (mcp-go 0.58.0 -> 1.0.0).
|
||||
log="$work/1275.log"
|
||||
cat > "$log" <<'EOF'
|
||||
2026-09-07T13:12:44.1000000Z error: hash mismatch in fixed-output derivation '/nix/store/zzzz1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
2026-09-07T13:12:44.1000001Z specified: sha256-8L7gH7Yy5+Fig3wK2SPLYSJjcY9nF/jumQ7PATJ3RIE=
|
||||
2026-09-07T13:12:44.1000002Z got: sha256-6xXfuNrBAJZ3KPXPFlwt3i8cRs9w+wpAWtEnbTj0Ebc=
|
||||
EOF
|
||||
pkg="$work/p2.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"
|
||||
check "1275: rewrites to that bump's hash" "sha256-6xXfuNrBAJZ3KPXPFlwt3i8cRs9w+wpAWtEnbTj0Ebc=" "$out"
|
||||
|
||||
# ---------------------------------------------------------------- case 3
|
||||
# THE ONE THAT MATTERS. An npm fetchurl FOD mismatch and NOTHING else. A parser
|
||||
# that greps for `got:` rewrites vendorHash to a tarball hash here.
|
||||
log="$work/npm.log"
|
||||
cat > "$log" <<'EOF'
|
||||
2026-09-07T13:20:00.0000000Z error: hash mismatch in fixed-output derivation '/nix/store/aaaa-vitest-4.1.11.tgz.drv':
|
||||
2026-09-07T13:20:00.0000001Z specified: sha512-AAAA=
|
||||
2026-09-07T13:20:00.0000002Z got: sha256-NPMnpmNPMnpmNPMnpmNPMnpmNPMnpmNPMnpmNPMnpmA=
|
||||
EOF
|
||||
pkg="$work/p3.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"; rc=$?
|
||||
check "npm-only: exits 1" "1" "$rc"
|
||||
check "npm-only: prints nothing on stdout" "" "$out"
|
||||
check "npm-only: leaves vendorHash alone" "$ORIG" "$(hash_in "$pkg")"
|
||||
|
||||
# ---------------------------------------------------------------- case 4
|
||||
# npm mismatch FIRST, go-modules second: the go-modules hash must win.
|
||||
log="$work/both.log"
|
||||
cat "$work/npm.log" "$work/1274.log" > "$log"
|
||||
pkg="$work/p4.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"
|
||||
check "npm-then-go: takes the go-modules hash" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$out"
|
||||
|
||||
# ---------------------------------------------------------------- case 5
|
||||
# go-modules mismatch FIRST, an unrelated FOD after it: the later block must not
|
||||
# be attributed to the armed one.
|
||||
log="$work/both2.log"
|
||||
cat "$work/1274.log" "$work/npm.log" > "$log"
|
||||
pkg="$work/p5.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"
|
||||
check "go-then-npm: still the go-modules hash" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$out"
|
||||
|
||||
# ---------------------------------------------------------------- case 6
|
||||
# A build that failed for a REAL reason. Nothing to rewrite, and the caller
|
||||
# must be able to tell, because this is the case where red is correct.
|
||||
log="$work/real.log"
|
||||
cat > "$log" <<'EOF'
|
||||
2026-09-07T13:30:00.0000000Z internal/store/items.go:412:2: undefined: ErrNope
|
||||
2026-09-07T13:30:00.0000001Z error: builder for '/nix/store/bbbb-pad-0.15.0.drv' failed with exit code 1
|
||||
EOF
|
||||
pkg="$work/p6.nix"; make_pkg "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1; rc=$?
|
||||
check "real failure: exits 1" "1" "$rc"
|
||||
check "real failure: leaves vendorHash alone" "$ORIG" "$(hash_in "$pkg")"
|
||||
|
||||
# ---------------------------------------------------------------- case 7
|
||||
# Idempotence: a log whose got: equals what is already pinned rewrites to the
|
||||
# same bytes, so a re-run of the fixing workflow pushes nothing.
|
||||
log="$work/same.log"
|
||||
cat > "$log" <<EOF
|
||||
error: hash mismatch in fixed-output derivation '/nix/store/cccc1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: $ORIG
|
||||
EOF
|
||||
pkg="$work/p7.nix"; make_pkg "$pkg"
|
||||
before="$(cat "$pkg")"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1
|
||||
check "idempotent: file is byte-identical" "$before" "$(cat "$pkg")"
|
||||
|
||||
# ---------------------------------------------------------------- case 8
|
||||
# A package.nix with no vendorHash line is a usage error (2), not a silent pass.
|
||||
log="$work/1274.log"
|
||||
pkg="$work/p8.nix"; echo '{ pname = "pad"; }' > "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1; rc=$?
|
||||
check "no vendorHash line: exits 2" "2" "$rc"
|
||||
|
||||
# ---------------------------------------------------------------- case 9
|
||||
# An unrelated `error:` line lands BETWEEN the go-modules header and its `got:`
|
||||
# — CI interleaves output, so this is a real log shape. The armed state must be
|
||||
# dropped rather than carried across it: carrying it would attribute the NEXT
|
||||
# FOD's `got:` to vendorHash, and a wrong hash written confidently is worse than
|
||||
# a build left red. Red is the correct outcome here.
|
||||
log="$work/interleaved.log"
|
||||
cat > "$log" <<'EOF'
|
||||
2026-09-07T13:40:00.0000000Z error: hash mismatch in fixed-output derivation '/nix/store/dddd1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
2026-09-07T13:40:00.0000001Z specified: sha256-8L7gH7Yy5+Fig3wK2SPLYSJjcY9nF/jumQ7PATJ3RIE=
|
||||
2026-09-07T13:40:00.0000002Z error: unrelated interleaved failure from another build
|
||||
2026-09-07T13:40:00.0000003Z got: sha256-NPMnpmNPMnpmNPMnpmNPMnpmNPMnpmNPMnpmNPMnpmA=
|
||||
EOF
|
||||
pkg="$work/p9.nix"; make_pkg "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1; rc=$?
|
||||
check "interleaved error: exits 1 rather than guessing" "1" "$rc"
|
||||
check "interleaved error: leaves vendorHash alone" "$ORIG" "$(hash_in "$pkg")"
|
||||
|
||||
# --------------------------------------------------------------- case 10
|
||||
# Two go-modules mismatch blocks in one log. FIRST wins, and exactly one hash is
|
||||
# printed — the caller reads stdout as a single value, so a second line would be
|
||||
# a silent corruption of whatever consumes it.
|
||||
log="$work/twice.log"
|
||||
cat "$work/1274.log" "$work/1275.log" > "$log"
|
||||
pkg="$work/p10.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"
|
||||
check "two blocks: first wins" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$out"
|
||||
check "two blocks: exactly one line on stdout" "1" "$(printf '%s\n' "$out" | wc -l | tr -d ' ')"
|
||||
|
||||
# --------------------------------------------------------------- case 11
|
||||
# IMPERSONATION SET. Each of these carries the shape of a nix diagnostic without
|
||||
# being one, and each must leave vendorHash alone. They exist because the first
|
||||
# draft of this parser matched substrings, and every line below defeats that
|
||||
# draft while looking, to a reader, like the real thing.
|
||||
impersonation_case() {
|
||||
local name="$1" text="$2"
|
||||
local l="$work/imp.log" k="$work/imp.nix"
|
||||
printf '%s\n' "$text" > "$l"
|
||||
make_pkg "$k"
|
||||
"$script" "$l" "$k" >/dev/null 2>&1
|
||||
check "impersonation ($name): vendorHash untouched" "$ORIG" "$(hash_in "$k")"
|
||||
}
|
||||
impersonation_case "warning, not error" \
|
||||
"warning: hash mismatch in fixed-output derivation '/nix/store/eeee1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "not a store path" \
|
||||
"error: hash mismatch in fixed-output derivation '/tmp/fake-pad-0.15.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "a name that merely CONTAINS -pad-" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/oooo1qv8w0k3n7d2m5x9c4b6f8h0j2l4-other-pad-tool-1.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "another package's go-modules FOD" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/ffff1qv8w0k3n7d2m5x9c4b6f8h0j2l4-othertool-1.2.3-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "trailing text after the drv path" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/gggg1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv': while evaluating something
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "a bare got: line with no header at all" \
|
||||
"some build tool says: got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
|
||||
# --------------------------------------------------------------- case 12
|
||||
# A stray `got:` inside the armed block, BEFORE the real specified/got pair.
|
||||
# After the timestamp strip such a line is byte-identical to nix's, so the only
|
||||
# thing that separates them is the sequence.
|
||||
log="$work/strayget.log"
|
||||
cat > "$log" <<EOF
|
||||
2026-09-07T14:00:00.0000000Z error: hash mismatch in fixed-output derivation '/nix/store/hhhh1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
2026-09-07T14:00:00.0000001Z got: sha256-STRAYstrayHASHstrayHASHstrayHASHstrayHASHstrayA=
|
||||
2026-09-07T14:00:00.0000002Z specified: $ORIG
|
||||
2026-09-07T14:00:00.0000003Z got: sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=
|
||||
EOF
|
||||
pkg="$work/p12.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"
|
||||
check "stray got: before the pair is ignored" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$out"
|
||||
|
||||
# --------------------------------------------------------------- case 13
|
||||
# Formatting variants of the vendorHash line. The rewrite must still land — an
|
||||
# `= ` with extra spaces is a nix-fmt away and would otherwise silently return
|
||||
# the whole class of PR to permanently red.
|
||||
log="$work/1274.log"
|
||||
for variant in 'vendorHash = "PLACEHOLDER";' ' vendorHash = "PLACEHOLDER";' 'vendorHash="PLACEHOLDER";'; do
|
||||
pkg="$work/pv.nix"
|
||||
printf '{\n%s\n}\n' "${variant/PLACEHOLDER/$ORIG}" > "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1
|
||||
check "spacing variant [$variant]" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$(hash_in "$pkg")"
|
||||
done
|
||||
|
||||
# --------------------------------------------------------------- case 14
|
||||
# Exit codes on the positive paths, and on the idempotent one. Asserting only
|
||||
# the file contents lets a script that fails AFTER writing pass as success.
|
||||
log="$work/1274.log"
|
||||
pkg="$work/p14.nix"; make_pkg "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1
|
||||
check "positive path: exit 0" "0" "$?"
|
||||
log="$work/same.log"
|
||||
pkg="$work/p14b.nix"; make_pkg "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1
|
||||
check "idempotent path: exit 0" "0" "$?"
|
||||
|
||||
# --------------------------------------------------------------- case 15
|
||||
# A log captured LOCALLY, with nix's real indentation and no runner timestamps.
|
||||
# This is the shape a human gets from `nix build 2>&1 | tee`, which is what
|
||||
# nix/package.nix's comment tells them to run. The first version of the parser
|
||||
# folded the timestamp and the indentation into one strip and silently handled
|
||||
# only the CI shape; nothing caught it until a case asserted the EXIT CODE
|
||||
# rather than just that the file was unchanged — for a no-op input, "did not
|
||||
# write" and "could not parse" leave identical files.
|
||||
log="$work/local.log"
|
||||
cat > "$log" <<EOF
|
||||
error: hash mismatch in fixed-output derivation '/nix/store/iiii1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=
|
||||
EOF
|
||||
pkg="$work/p15.nix"; make_pkg "$pkg"
|
||||
out="$("$script" "$log" "$pkg" 2>/dev/null)"; rc=$?
|
||||
check "local log (no timestamps): exit 0" "0" "$rc"
|
||||
check "local log (no timestamps): rewrites" "sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk=" "$out"
|
||||
|
||||
# --------------------------------------------------------------- case 16
|
||||
# Round-2 impersonations. Each one satisfied an EARLIER version of this parser.
|
||||
impersonation_case "nested path under /nix/store" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/fake/not-a-store-pad-x-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "got: adjacent to nothing — three lines after specified:" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/jjjj1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
building '/nix/store/kkkk-something-else.drv'
|
||||
copying path '/nix/store/llll-another' from 'https://cache.nixos.org'
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
impersonation_case "non-canonical (short) hash on both lines" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/mmmm1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
specified: sha256-A=
|
||||
got: sha256-B="
|
||||
# Canonical `specified:`, short `got:`. Without this the length check on the got
|
||||
# line is never exercised on its own — the specified line's check refuses the
|
||||
# case above before the got line is reached, and a mutant that drops only the
|
||||
# got-side length survives. Found by mutating, not by reading.
|
||||
impersonation_case "non-canonical hash on the got: line only" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/nnnn1qv8w0k3n7d2m5x9c4b6f8h0j2l4-pad-0.15.0-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-B="
|
||||
|
||||
# --------------------------------------------------------------- case 17
|
||||
# ROUND-3 MUTATION HOLES. Each of these was added because a faithful mutant
|
||||
# SURVIVED the suite: the case that should have caught it was being refused for
|
||||
# a second reason, so it discriminated nothing about the rule it was written for.
|
||||
H32='aaaa1qv8w0k3n7d2m5x9c4b6f8h0j2l4'
|
||||
|
||||
# Ours, but not the module set: another FOD of the same package. Only the
|
||||
# `-go-modules` part of the anchor separates it.
|
||||
impersonation_case "our package, a different FOD" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/$H32-pad-0.15.0-source.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
|
||||
# A store path whose ONLY defect is a slash — everything else, including the
|
||||
# 32-character store hash, is well formed. Without the single-segment rule this
|
||||
# is accepted.
|
||||
impersonation_case "a slash inside the derivation name" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/$H32-pad-0.15.0/x-go-modules.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA="
|
||||
|
||||
# Short `specified:`, CANONICAL `got:`. The mirror of case 16's last entry: with
|
||||
# both short, the got-side rule refuses it first and the specified-side rule is
|
||||
# never exercised.
|
||||
impersonation_case "non-canonical hash on the specified: line only" \
|
||||
"error: hash mismatch in fixed-output derivation '/nix/store/$H32-pad-0.15.0-go-modules.drv':
|
||||
specified: sha256-A=
|
||||
got: sha256-2hHhUx/J9CgDGU4fE5EE2Y/h6nRTA5vZ7GzuQ2oajkk="
|
||||
|
||||
# Our header, then a DIFFERENT FOD's complete mismatch block. The adjacency rule
|
||||
# alone cannot refuse this — the npm block's specified/got ARE adjacent — so
|
||||
# what refuses it is dropping the armed state at the second `error:`.
|
||||
log="$work/twoblocks.log"
|
||||
cat > "$log" <<EOF
|
||||
error: hash mismatch in fixed-output derivation '/nix/store/$H32-pad-0.15.0-go-modules.drv':
|
||||
error: hash mismatch in fixed-output derivation '/nix/store/${H32}bb-vitest-4.1.11.tgz.drv':
|
||||
specified: $ORIG
|
||||
got: sha256-IMPimpIMPimpIMPimpIMPimpIMPimpIMPimpIMPimpA=
|
||||
EOF
|
||||
pkg="$work/p17.nix"; make_pkg "$pkg"
|
||||
"$script" "$log" "$pkg" >/dev/null 2>&1
|
||||
check "a second error: block does not inherit our arming" "$ORIG" "$(hash_in "$pkg")"
|
||||
|
||||
echo "bump-vendor-hash: $pass passed, $fail failed${BVH_AWK:+ (awk: $BVH_AWK)}"
|
||||
[[ "$fail" -eq 0 ]] || exit 1
|
||||
|
||||
# PORTABILITY LEG. The script is awk, and `awk` is a different program depending
|
||||
# on where it runs: gawk on this workstation, mawk on the ubuntu-24.04 runners
|
||||
# where the Dependabot path actually executes. A regex feature one accepts and
|
||||
# the other rejects would pass every case above and still fail the only run that
|
||||
# matters. So the suite re-runs itself once per awk implementation present,
|
||||
# through a PATH shim, and is green only if all of them are.
|
||||
if [[ -z "${BVH_SUBRUN:-}" ]]; then
|
||||
rc=0
|
||||
for impl in mawk gawk busybox-awk; do
|
||||
case "$impl" in
|
||||
busybox-awk) command -v busybox >/dev/null || continue; target="$(command -v busybox)" ;;
|
||||
*) command -v "$impl" >/dev/null || continue; target="$(command -v "$impl")" ;;
|
||||
esac
|
||||
shim="$(mktemp -d)"
|
||||
if [[ "$impl" == busybox-awk ]]; then
|
||||
printf '#!/bin/sh\nexec %s awk "$@"\n' "$target" > "$shim/awk"
|
||||
chmod +x "$shim/awk"
|
||||
else
|
||||
ln -s "$target" "$shim/awk"
|
||||
fi
|
||||
BVH_SUBRUN=1 BVH_AWK="$impl" PATH="$shim:$PATH" "$0" || rc=1
|
||||
rm -rf "$shim"
|
||||
done
|
||||
exit "$rc"
|
||||
fi
|
||||
+16
-2
@@ -59,8 +59,22 @@ buildGoModule {
|
||||
|
||||
go = go_1_26;
|
||||
|
||||
# Update alongside go.sum. Regenerate via:
|
||||
# nix build .#default 2>&1 | grep -A2 'got:'
|
||||
# Update alongside go.sum — though CI no longer depends on you remembering
|
||||
# (TASK-2954). Every Nix run IN CI recomputes this in its working tree before
|
||||
# judging the build — a local `nix build` does not; it just fails the way it
|
||||
# always did, and the line below tells you how to fix it — so a PR's check is
|
||||
# green exactly when the build passes
|
||||
# with that PR's module set; and the `push: main` run commits the corrected
|
||||
# value back, so main heals one commit after a merge. It exists because a hash
|
||||
# Dependabot could not update made every Go bump PR permanently red, and a
|
||||
# check that is always red is not a check.
|
||||
#
|
||||
# Updating it by hand still works and is still the faster loop locally:
|
||||
# nix build .#default 2>&1 | tee /tmp/b.log; nix/bump-vendor-hash.sh /tmp/b.log
|
||||
# Use that script rather than reading the hash out of the log by eye: this
|
||||
# build has other fixed-output derivations (every npm tarball importNpmLock
|
||||
# fetches is one), so `grep got:` can hand you a hash that belongs to
|
||||
# something else entirely. The script anchors on the go-modules derivation.
|
||||
vendorHash = "sha256-8L7gH7Yy5+Fig3wK2SPLYSJjcY9nF/jumQ7PATJ3RIE=";
|
||||
|
||||
subPackages = [ "cmd/pad" ];
|
||||
|
||||
Reference in New Issue
Block a user