diff --git a/scripts/repin-sha.sh b/scripts/repin-sha.sh index 15ea743..6e59910 100644 --- a/scripts/repin-sha.sh +++ b/scripts/repin-sha.sh @@ -1,12 +1,7 @@ #!/bin/sh -# Recompute a Dockerfile sha256 integrity pin after its version pin moved. -# -# Renovate can bump a version literal but cannot compute the sha256 of the -# artifact that version names: no datasource publishes it (github-tags exposes a -# git commit, npm exposes SHA-512, most dist tarballs publish nothing). Every -# such pin therefore used to need a human to run curl | sha256sum and paste the -# result into the PR. This script is that step, run by Renovate itself via -# postUpgradeTasks so the recomputed pin lands in the bump commit. +# Recompute a Dockerfile sha256 integrity pin after Renovate moved its version +# literal. Run by Renovate itself via postUpgradeTasks, because no datasource +# publishes a sha256, so the recomputed pin lands in the bump commit. # # Each pin declares its own source URL in a marker comment on the line directly # above the ARG it protects: @@ -21,11 +16,10 @@ # # Usage: repin-sha.sh [dockerfile ...] # -# Exits 0 and changes nothing when no marker names : the Renovate task -# is wired for a SET of deps and must be a silent no-op for every other one. -# Exits non-zero on a marker it cannot honour (bad shape, unreachable URL, -# unchanged file), because a silent miss reproduces exactly the stale-pin build -# failure this script exists to prevent. +# Exits 0 and changes nothing when no marker names : the Renovate task is +# wired for a SET of deps and must be a silent no-op for every other one. Exits +# non-zero on a marker it cannot honour (bad shape, unreachable URL, unchanged file), +# because a silent miss reproduces the stale-pin build failure this script prevents. set -eu usage() { @@ -39,12 +33,10 @@ version=$2 shift 2 [ -n "$dep" ] && [ -n "$version" ] || usage -# The version is interpolated into the sed EXPRESSION below, and GNU sed's `e` -# flag and `e` command execute the pattern space as a shell command, so a -# version carrying `|` and `;` is arbitrary code execution here. It also reaches -# curl as part of a URL, where `{}`/`[]` trigger curl's own URL globbing. -# The value is not ours: it is whatever version a third-party datasource -# reports, so constrain it to the shape a version has before any use. +# The version is interpolated into the sed EXPRESSION below (GNU sed's `e` flag and +# `e` command execute the pattern space as a shell command) and reaches curl inside +# a URL (where `{}`/`[]` trigger curl's URL globbing), and its value is whatever a +# third-party datasource reports. Constrain it to the shape a version has first. case $version in *[!A-Za-z0-9._+~-]*) printf 'repin: refusing version with unexpected characters: %s\n' "$version" >&2 @@ -59,11 +51,9 @@ if [ $# -eq 0 ]; then fi tmp=$(mktemp -d) -# staged is the in-place rewrite target beside the Dockerfile, tracked here so -# the trap can remove it: it lives OUTSIDE $tmp by necessity (a rename must be -# same-filesystem), so the mktemp -d cleanup cannot reach it, and an interrupt -# between the copy and the rename would otherwise leave it in the working tree -# for Renovate to carry into a branch. +# staged lives OUTSIDE $tmp by necessity (a rename must be same-filesystem), so the +# mktemp -d cleanup cannot reach it; tracked here so the trap removes it when an +# interrupt lands between the copy and the rename. staged= cleanup() { rm -rf "$tmp" @@ -72,20 +62,11 @@ cleanup() { } trap cleanup EXIT INT TERM HUP -# resolve_target prints the real path of $1, following symlinks. -# -# It matters because the rewrite below commits by RENAME, which must land beside -# the actual file, and because a Dockerfile reached through a symlink has to be -# updated at its TARGET rather than replaced by a regular file — silently -# breaking whatever the symlink was arranged for. -# -# Neither realpath nor readlink is in POSIX (realpath arrived only in -# POSIX.1-2024) and this script is `#!/bin/sh` synced across every repo, so both -# are probed rather than assumed. With neither available a SYMLINKED Dockerfile -# fails closed: the rewrite commits by rename, so returning the unresolved path -# would turn a tracked symlink into a regular file, and a warning does not -# preserve the arrangement the symlink exists for. An ordinary file still uses -# the path as given, which needs no resolver. +# resolve_target prints the real path of $1, following symlinks: the rewrite below +# commits by RENAME, so a Dockerfile reached through a symlink must be updated at its +# TARGET rather than replaced by a regular file. Neither realpath nor readlink is in +# POSIX (realpath arrived only in POSIX.1-2024) and this script is `#!/bin/sh`, so both +# are probed and a SYMLINKED Dockerfile with neither available fails closed. resolve_target() { if command -v realpath >/dev/null 2>&1; then realpath "$1" @@ -110,9 +91,9 @@ for dockerfile in "$@"; do exit 1 } - # Emit " " for every marker naming this dep. The - # marker must sit on the line immediately above its ARG so the pairing is - # unambiguous in a file that carries several pins. + # Emit " " for every marker naming this dep. The marker + # must sit on the line immediately above its ARG so the pairing is unambiguous in + # a file that carries several pins. awk -v dep="$dep" ' /^#[[:space:]]*repin:/ { # END reports it: printing here as well would double the message, because @@ -152,10 +133,9 @@ for dockerfile in "$@"; do while read -r name url; do [ -n "$name" ] || continue - # A sed replacement is NOT a literal context ('&' re-inserts the match, - # '\1' a group, '|' closes the command), so this is safe only because the - # version was parsed to [A-Za-z0-9._+~-] at the argument boundary above. - # The URL is then built from the Dockerfile marker plus that parsed value. + # A sed replacement is NOT a literal context ('&' re-inserts the match, '\1' a + # group, '|' closes the command), so this is safe only because the version was + # shape-checked at the argument boundary above. resolved=$(printf '%s\n' "$url" \ | sed -e "s|{version}|$version|g" -e "s|{version_nov}|$version_nov|g") @@ -181,9 +161,9 @@ for dockerfile in "$@"; do ;; esac - # Anchored on the ARG name and the 64-hex value, so nothing else in the - # file can match; the optional trailing group preserves an inline - # Renovate anchor comment (ARG X= # tool v1.2.3). + # Anchored on the ARG name and the 64-hex value, so nothing else in the file can + # match; the optional trailing group preserves an inline Renovate anchor comment + # (ARG X= # tool v1.2.3). sed -E "s|^(ARG ${name}=)[0-9a-f]{64}([[:space:]].*)?\$|\1${sha}\2|" \ "$dockerfile_target" >"$tmp/rewritten" @@ -192,23 +172,16 @@ for dockerfile in "$@"; do exit 1 fi - # Replace atomically. '>' truncates the target before the first byte lands, - # so a killed postUpgradeTask or an ENOSPC leaves a truncated Dockerfile in - # the branch Renovate commits. Stage beside the TARGET (the mktemp -d above - # is a different filesystem, so a rename out of it cannot work) and rename - # over it; copying the original first carries its mode across the replace, - # which `cp -p` does portably where `chmod --reference` is GNU-only. + # Replace atomically. '>' truncates the target before the first byte lands, so a + # killed postUpgradeTask or an ENOSPC leaves a truncated Dockerfile in the branch + # Renovate commits. Stage beside the TARGET ($tmp is a different filesystem, so a + # rename out of it cannot work) and rename over it; `cp -p` carries the original's + # mode across the replace (so mktemp's own 0600 does not leak into the committed + # file) where `chmod --reference` is GNU-only. # # mktemp, not a name built from $$: it creates the file with O_EXCL under an - # unpredictable name, so nothing can be sitting at the path when the copy opens - # it -- a $$-derived name is guessable and `rm -f` then `cp -p` reopens by path, - # which is a window a symlink planted there turns into a write through it. The - # rename still commits, and `cp -p` still carries the original's mode across the - # replace (verified: cp -p sets the source's mode on an existing destination), - # which is why mktemp's own 0600 does not leak into the committed file. A - # leftover matches neither postUpgradeTasks fileFilter (Dockerfile, - # **/Dockerfile), so it can never be committed. shell.md, "Temp files and atomic - # writes". + # unpredictable name, closing the window a symlink planted at a guessable path + # would turn into a write through it. shell.md, "Temp files and atomic writes". staged=$(mktemp "$dockerfile_target.repin.XXXXXX") cp -p "$dockerfile_target" "$staged" cat "$tmp/rewritten" >"$staged"