Files
pi-devbox/scripts/check-dockerfile-directives.sh
T
Joakim Persson 56e3742a2d
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Failing after 13s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 33s
Publish Docker Image / lint-gate (push) Successful in 32s
Publish Docker Image / resolve-versions (push) Successful in 17s
Publish Docker Image / base-decide (push) Successful in 13s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-studio (push) Successful in 5m38s
Publish Docker Image / smoke (push) Successful in 8m20s
Publish Docker Image / build-variant-studio (push) Successful in 20m34s
Publish Docker Image / build-variant (push) Successful in 21m5s
Publish Docker Image / update-description (push) Successful in 8s
Publish Docker Image / promote-base-latest (push) Successful in 9s
fix(ci): skip the BuildKit check whose warning embedded this Dockerfile in CI output
v1.10.1 (run 707) failed with ZERO failing steps: every step Success, `smoke`
printing "101 passed, 0 failed", `smoke-studio` "104 passed, 0 failed", both
jobs red. The clipboard fix from f3b3748 worked exactly as predicted; this is a
second, independent defect that run 704 had masked.

CHAIN, measured end to end rather than reasoned:

`ARG BASE_IMAGE` has no default on purpose (the two-phase build always supplies
it), which trips BuildKit's InvalidDefaultArgInFrom check. buildx attaches that
warning's source context to the build metadata as
buildx.build.warnings[].sourceInfo.data — the ENTIRE Dockerfile, base64, on ONE
line. docker/build-push-action writes that metadata to $GITHUB_OUTPUT as a
`name<<ghadelimiter_<uuid>` heredoc. Gitea's act_runner truncates any single
line at exactly 65536 chars, so the closing delimiter was cut off:

  invalid format delimiter 'ghadelimiter_...' not found before end of file

and the runner failed the job while attributing it to no step at all.

  v1.9.4  run 695 SUCCESS: longest metadata line 56355, 0 delimiter errors
  v1.10.0 run 704 failed : longest metadata line 65536, 1 delimiter error
  v1.10.1 run 707 failed : longest metadata line 65536, 1 delimiter error

65536 = 2^16: cut AT the cap, not merely long. Independent route via file size
rather than log parsing: 42251 B at v1.9.4 (base64 56335, matching the log) vs
50034 B now (base64 66712, truncated). The cap corresponds to a 49152 B
Dockerfile, so this cycle's comment growth crossed it by 882 B.

Located by asking which top-level metadata key CONTAINS the base64, instead of
assuming: it is buildx.build.warnings -> sourceInfo -> data in BOTH runs.

THIS IS THE SECOND FIX FOR THIS BUG. The first, `provenance: false` on the two
smoke build steps, was committed as 784fad7 and is REVERTED here: a real buildx
on another host showed provenance metadata present in both modes with a longest
string of 71 chars, i.e. the base64 was never in provenance. Shipping it would
have left the release broken a third time while looking like a fix.

Also rejected, each measured: floating action tags moving (act action-bundle
hashes byte-identical between runs 695 and 707, all four) and the build-check
annotation text changing (byte-identical).

FIX: `# check=skip=InvalidDefaultArgInFrom` as the FIRST line of
Dockerfile.variant — BuildKit parses `# check=` only before any other line, so
placement is load-bearing. No honest default exists for BASE_IMAGE: `scratch`
would satisfy the linter while being a lie, and would convert today's instant
"invalid reference format" into a failure deep in the build. Verified against
the actual edited file with `docker buildx build --check`: "Check complete, no
warnings found." Zero warnings means no sourceInfo, so the longest metadata line
drops 65536 -> ~1936 and the file's SIZE stops gating CI.

GUARD: scripts/check-dockerfile-directives.sh, wired into BOTH lint.yml and
docker-publish.yml's lint-gate, because a check that gates only `push` lets a
tag regress — the adoption slip that let doc-drift land 27 h after the v1.9.3
tag. It also fails if ARG BASE_IMAGE gains a default, so the directive cannot
rot into guarding a check that can no longer fire. Truth table, exit codes:
present 0, removed 1, demoted to line 2 → 1, ARG defaulted 1, file missing 2
(a gate that cannot run must not pass).

Release renamed v1.10.1 -> v1.10.2 with both failed tags left standing as
tombstones. Docs swept again (README "since" marker, Dockerfile decision
comments) because CI reads them from the TAG.

Gates: doc-drift 23 OK / 0 DRIFT; base-hash, workflow-shell, skill-floor,
lint-shell (17 files now), dockerfile-directives all rc=0.
2026-10-02 10:58:03 +02:00

70 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Guard: Dockerfile.variant must OPEN with its BuildKit `# check=` directive.
#
# Why this is a gate and not a comment. BuildKit parses `# check=` ONLY before
# any other line in the file, so moving it below the title comment — or dropping
# it during an unrelated header edit — silently re-enables the
# InvalidDefaultArgInFrom warning on `ARG BASE_IMAGE` / `FROM ${BASE_IMAGE}`.
#
# A re-enabled warning is not cosmetic. buildx attaches the warning's source
# context to the build metadata as buildx.build.warnings[].sourceInfo.data: the
# entire Dockerfile, base64, on ONE line. docker/build-push-action writes that
# metadata to $GITHUB_OUTPUT as a `name<<ghadelimiter_<uuid>` heredoc, and
# Gitea's act_runner truncates any single line at exactly 65536 chars. Since
# base64(Dockerfile.variant) passed 64 KiB (50034 B source -> 66712 chars, cut
# to 65536), the closing delimiter is lost and act_runner fails the job with
# invalid format delimiter 'ghadelimiter_...' not found before end of file
# while attributing it to NO step: every step reports Success, the smoke suite
# prints "0 failed", and the job is red regardless. That cost two tags —
# v1.10.0 (run 704, where a stale clipboard assertion masked it) and v1.10.1
# (run 707) — and most of a session to localise.
#
# So: one deterministic grep, run both on push AND in the publish workflow's
# lint-gate. A check that gates only `push` lets a tag regress — the same
# adoption slip that let doc-drift land 27 h after the v1.9.3 tag.
#
# Exit codes: 0 OK, 1 violation, 2 cannot-run (missing file) — matching
# lint-shell.sh / check-skill-floor.sh / check-doc-drift.sh, because a gate that
# cannot run must not pass.
set -euo pipefail
DF="${1:-Dockerfile.variant}"
EXPECTED='# check=skip=InvalidDefaultArgInFrom'
if [ ! -f "$DF" ]; then
echo "::error::check-dockerfile-directives: '$DF' not found (cannot-run)" >&2
exit 2
fi
first="$(head -n 1 "$DF")"
if [ "$first" != "$EXPECTED" ]; then
{
echo "::error::$DF line 1 must be exactly: $EXPECTED"
echo "::error::found instead: ${first:-<empty>}"
echo "::error::"
echo "::error::BuildKit only honours '# check=' before any other line. Without it the"
echo "::error::InvalidDefaultArgInFrom warning returns, buildx embeds this whole file as"
echo "::error::base64 in buildx.build.warnings[].sourceInfo.data, that single line exceeds"
echo "::error::act_runner's 65536-char cap, and the smoke jobs fail with"
echo "::error:: invalid format delimiter 'ghadelimiter_...' not found before end of file"
echo "::error::and NO failing step. See the header of $DF for the full measurement."
} >&2
exit 1
fi
# Second, independent assertion: the condition the directive exists FOR. If a
# later edit gives ARG BASE_IMAGE a default, the directive becomes dead weight
# and should be removed deliberately rather than left to rot — and if the ARG is
# renamed, this guard would otherwise keep passing while guarding nothing.
if ! grep -qE '^ARG BASE_IMAGE$' "$DF"; then
{
echo "::error::$DF no longer contains a bare 'ARG BASE_IMAGE' (no default)."
echo "::error::That is the only thing '$EXPECTED' suppresses. Either restore the bare ARG"
echo "::error::or drop the directive and this guard together — do not leave a skip"
echo "::error::directive pointing at a check that can no longer fire."
} >&2
exit 1
fi
echo "Dockerfile directive guard OK — $DF opens with the check-skip directive and still declares a bare ARG BASE_IMAGE."