From 02792e2bc4ea6ef7e9a39f08cc3224781438c8b3 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Tue, 1 Sep 2026 18:20:10 -0700 Subject: [PATCH] fix(ci): derive control base tags from control/Dockerfile scripts/update-control-base-images.sh hardcoded `golang:1.26.6-alpine` both as the candidate to resolve and as a literal regex that control/Dockerfile had to match. Any reviewed tag bump therefore aborted the script with `unexpected Go build base reference`, which failed the "License and prose hygiene" and "Build, boot, persist, and scan candidate" jobs on every Docker dependency PR. #62 (golang 1.26.6-alpine -> 1.27.0-alpine) has been blocked on this since 2026-08-24. The script now reads the pinned tags out of control/Dockerfile and re-resolves their digests. The gate is unchanged in strength: the Go base must still be `golang:-alpine` pinned by a canonical sha256 digest, and the runtime base must still be `alpine:` pinned the same way. Unpinned or off-family references still exit 1. scripts/check-image-drift.sh defaulted to the same literal tag, so after a bump it would have reported permanent phantom drift against a tag the repository no longer builds from. Its defaults now come from control/Dockerfile too; the GHOSTLIGHT_*_BASE_CANDIDATE overrides are unchanged. Verified locally on this branch: - `bash scripts/test-image-safety.sh` passes. - `bash scripts/test-browser-update-workflow.sh` passes. - `bash scripts/check-repo-hygiene.sh` passes. - `shellcheck scripts/update-control-base-images.sh scripts/check-image-drift.sh` is clean. - With #62 merged into this branch, `bash scripts/check-image-safety.sh` and `bash scripts/test-image-safety.sh` both pass and the updater resolves `golang:1.27.0-alpine@sha256:4c9fe60...`. - Negative cases still fail: an unpinned `golang:1.27.0-alpine` and an off-family `golang:1.27.0-bookworm@sha256:...` both exit 1. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01XpuXXVrWCZk3Tq5NRXejNP --- scripts/check-image-drift.sh | 7 +++++-- scripts/update-control-base-images.sh | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/scripts/check-image-drift.sh b/scripts/check-image-drift.sh index 2550ff5..c620b60 100644 --- a/scripts/check-image-drift.sh +++ b/scripts/check-image-drift.sh @@ -6,8 +6,11 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" CONTROL_DIR="$ROOT_DIR/control" neko_candidate="${GHOSTLIGHT_NEKO_CANDIDATE_IMAGE:-ghcr.io/m1k1o/neko/chromium:latest}" -go_candidate="${GHOSTLIGHT_GO_BASE_CANDIDATE:-golang:1.26.6-alpine}" -alpine_candidate="${GHOSTLIGHT_ALPINE_BASE_CANDIDATE:-alpine:3.24}" +# Default the drift candidates to the tags control/Dockerfile already pins, so a +# reviewed tag bump does not turn into permanent phantom drift against a tag the +# repository no longer builds from. +go_candidate="${GHOSTLIGHT_GO_BASE_CANDIDATE:-$(awk '$1 == "FROM" && $2 ~ /^golang:/ { image = $2; sub(/@.*$/, "", image); print image; exit }' "$CONTROL_DIR/Dockerfile")}" +alpine_candidate="${GHOSTLIGHT_ALPINE_BASE_CANDIDATE:-$(awk '$1 == "FROM" && $2 ~ /^alpine:/ { image = $2; sub(/@.*$/, "", image); print image; exit }' "$CONTROL_DIR/Dockerfile")}" # The deployed NEKO_IMAGE pin tracks the hardened ghostlight-viewer rebuild, so # upstream drift is measured against the base the hardened viewer builds from. neko_pinned="$(awk '$1 == "FROM" && $2 ~ /^ghcr\.io\/m1k1o\/neko\/chromium@/ { image = $2; sub(/^[^@]*@/, "", image); print image; exit }' "$ROOT_DIR/viewer/Dockerfile")" diff --git a/scripts/update-control-base-images.sh b/scripts/update-control-base-images.sh index 2951294..85ed8d8 100755 --- a/scripts/update-control-base-images.sh +++ b/scripts/update-control-base-images.sh @@ -62,24 +62,29 @@ resolve_digest() { printf '%s\n' "$digest" } -go_candidate=golang:1.26.6-alpine -alpine_candidate=alpine:3.24 -go_digest=$(resolve_digest "$go_candidate" "${GHOSTLIGHT_GO_BASE_RESOLVED_DIGEST:-}") -alpine_digest=$(resolve_digest "$alpine_candidate" "${GHOSTLIGHT_ALPINE_BASE_RESOLVED_DIGEST:-}") -go_new="$go_candidate@$go_digest" -alpine_new="$alpine_candidate@$alpine_digest" - +# control/Dockerfile is the source of truth for which base tags this repository +# builds from. Deriving the candidates from it lets a reviewed tag bump (for +# example a Dependabot Docker update) flow through without editing this script, +# while the shape checks below still require an immutable digest pin on the +# expected image families. go_current=$(awk '$1 == "FROM" && $2 ~ /^golang:/ { print $2; exit }' "$dockerfile") alpine_current=$(awk '$1 == "FROM" && $2 ~ /^alpine:/ { print $2; exit }' "$dockerfile") -[[ "$go_current" =~ ^golang:1\.26\.6-alpine@sha256:[0-9a-f]{64}$ ]] || { +[[ "$go_current" =~ ^golang:[0-9]+\.[0-9]+(\.[0-9]+)?-alpine@sha256:[0-9a-f]{64}$ ]] || { printf 'unexpected Go build base reference: %s\n' "$go_current" >&2 exit 1 } -[[ "$alpine_current" =~ ^alpine:3\.24@sha256:[0-9a-f]{64}$ ]] || { +[[ "$alpine_current" =~ ^alpine:[0-9]+\.[0-9]+(\.[0-9]+)?@sha256:[0-9a-f]{64}$ ]] || { printf 'unexpected Alpine runtime base reference: %s\n' "$alpine_current" >&2 exit 1 } +go_candidate=${go_current%@*} +alpine_candidate=${alpine_current%@*} +go_digest=$(resolve_digest "$go_candidate" "${GHOSTLIGHT_GO_BASE_RESOLVED_DIGEST:-}") +alpine_digest=$(resolve_digest "$alpine_candidate" "${GHOSTLIGHT_ALPINE_BASE_RESOLVED_DIGEST:-}") +go_new="$go_candidate@$go_digest" +alpine_new="$alpine_candidate@$alpine_digest" + if [[ "$go_current" != "$go_new" || "$alpine_current" != "$alpine_new" ]]; then GO_CURRENT="$go_current" GO_NEW="$go_new" ALPINE_CURRENT="$alpine_current" ALPINE_NEW="$alpine_new" \ perl -0pi -e 's/\Q$ENV{GO_CURRENT}\E/$ENV{GO_NEW}/g; s/\Q$ENV{ALPINE_CURRENT}\E/$ENV{ALPINE_NEW}/g' "$dockerfile"