hack/promote-rewrite-tags.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/sh
# Rewrite the rc version substring to the stable version in every vendored
# first-party image reference. Promotion does not rebuild: the digests stay
# exactly as the rc built them, and only the cosmetic tag string moves from
# "1.6.0-rc.4" to "1.6.0" so the shipped tree reads as the release it is.
#
# Usage: hack/promote-rewrite-tags.sh <rc-version> <stable-version> [root]
# <rc-version> e.g. 1.6.0-rc.4 (no leading v — the substring as it
# appears inside an image tag)
# <stable-version> e.g. 1.6.0
# [root] tree to rewrite; defaults to "packages"
#
# This lived inline in .github/workflows/promote-rc.yaml until it shipped a
# release whose tags were half-rewritten. Its glob covered the depth-2
# values.yaml plus packages/apps/kubernetes/images/*.tag alone, on the premise
# that the kubernetes app was the only one whose .tag files carry the cozystack
# version — nine other .tag files and one stamped template do. Because the step
# was workflow-inline there was nothing to unit test, so the miss was only
# observable by cutting a release. It is a script so that
# hack/promote-rewrite-tags_test.bats can round-trip it against the real tree.
#
# grep's -r/-I/--exclude/--exclude-dir are non-POSIX, but both GNU and BSD grep
# implement them, so this runs unchanged on the CI runners, in the build image,
# and on a macOS developer box. The edit is written through a temp file rather
# than `sed -i`, whose argument handling differs between the two seds.
set -eu
RC_VERSION="${1:?usage: promote-rewrite-tags.sh <rc-version> <stable-version> [root]}"
STABLE_VERSION="${2:?usage: promote-rewrite-tags.sh <rc-version> <stable-version> [root]}"
ROOT="${3:-packages}"
# Validate both versions before touching a file. RC_VERSION becomes a sed
# pattern and STABLE_VERSION its replacement, so an unvalidated argument is
# both a correctness and an injection concern: a stray "/" or "&" in the
# replacement would corrupt every file this walks.
echo "$RC_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' \
|| { echo "rc-version '$RC_VERSION' must match X.Y.Z-rc.N" >&2; exit 1; }
echo "$STABLE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \
|| { echo "stable-version '$STABLE_VERSION' must match X.Y.Z" >&2; exit 1; }
[ -d "$ROOT" ] || { echo "root '$ROOT' is not a directory" >&2; exit 1; }
# shellcheck source=hack/lib/image-refs.sh
. "$(dirname "$0")/lib/image-refs.sh"
# The dots are the only regex metacharacter a validated version can contain;
# escaping them keeps sed matching the literal string rather than any char.
RC_ESC=$(printf '%s' "$RC_VERSION" | sed -e 's/\./\\./g')
# Materialize the file list rather than piping into the loop: a `while` on the
# right of a pipe runs in a subshell, so it cannot carry a count out and cannot
# abort the script from inside. Both matter here — the count is the operator's
# only signal that the rewrite did anything, and an unreadable file must stop
# the promotion rather than be skipped.
files_list=$(mktemp)
image_ref_files "$ROOT" > "$files_list"
# Failures are recorded and acted on after the loop rather than exiting inside
# it: the loop reads from $files_list, so removing that file in the same
# construct is both a shellcheck SC2094 hit and needless subtlety (the open
# descriptor would survive the unlink, but nobody should have to know that).
rewritten=0
failed_file=""
failed_why=""
while IFS= read -r f; do
# An unreadable file is NOT "a file with no match". Treating the two alike is
# how a ref goes unrewritten while the run still reports success — the same
# silent-skip shape as the enumeration bug this script exists to fix. Fail.
if [ ! -r "$f" ]; then
failed_file="$f"
failed_why="is not readable; refusing to promote a tree that cannot be fully scanned"
break
fi
if grep -q "$RC_ESC" "$f"; then
# Written through a temp file rather than `sed -i`: the in-place flag takes
# the substitution as its backup suffix on BSD and edits nothing, so the
# rewrite would silently no-op on a macOS `make unit-tests`. cat back over the
# original to keep its inode and permissions. `cat` truncates before it
# writes, so an interrupt can leave a half-written file where `mv` would not;
# that is the lesser evil against `mv` stamping mktemp's 0600 onto a file in
# the release tree.
tmp_f=$(mktemp)
sed "s/${RC_ESC}/${STABLE_VERSION}/g" "$f" > "$tmp_f"
cat "$tmp_f" > "$f"
rm -f "$tmp_f"
echo " ~ $f"
rewritten=$((rewritten + 1))
else
# grep exits 1 for "no match" and >1 for a real error; only the former is
# an ordinary outcome.
grep_rc=$?
if [ "$grep_rc" -gt 1 ]; then
failed_file="$f"
failed_why="could not be scanned (grep exit ${grep_rc})"
break
fi
fi
done < "$files_list"
rm -f "$files_list"
if [ -n "$failed_file" ]; then
echo "::error::${failed_file} ${failed_why}" >&2
exit 1
fi
# Postcondition: scan WIDER than the rewrite. The rewrite is deliberately
# narrow — a blind walk of every file under $ROOT would rewrite vendored chart
# defaults that `make update` regenerates — but a narrow rewrite is exactly
# what shipped a half-promoted release, so the check that follows it must not
# share its blind spot. Anything still carrying the rc string here is a ref in
# a storage location the enumeration does not know about: fail the promotion
# loudly rather than publish a release whose images claim to be a release
# candidate.
#
# It must, however, only flag strings that could actually BE an image ref.
# "X.Y.Z-rc.N" is an ordinary version string that other things legitimately
# contain, and a false positive here fails a workflow that runs once per
# release, on release day. Three live examples, each of which aborted the
# promotion before this was shape-filtered:
# - packages/apps/kubernetes/images/kubevirt-csi-driver/go.sum pins
# github.com/golang/protobuf v1.4.0-rc.2 — and v1.4.0-rc.2 is a cozystack
# rc that was actually cut, so this was not hypothetical
# - packages/system/metallb/tests/metallb_test.yaml has a comment naming
# v1.5.0-rc.2 as an example of what release-prep stamps
# - the console's pnpm-lock.yaml carries rolldown@1.0.0-rc.15 and friends
#
# So match the version only where it appears in ref position: after an
# image/repository/tag key (with no `#` in between, which excludes prose in a
# trailing comment), or immediately before an @sha256 digest. The second branch
# is what catches a bare .tag file, whose content has no YAML key at all. The
# first is required for the split shape where the version and the digest live
# under separate keys — packages/system/cilium/values.yaml has `tag: v1.5.0`
# and `digest:` on the next line — so narrowing this to "${RC}@sha256:" alone
# would silently stop covering it.
#
# charts/ is excluded because a vendored upstream chart may legitimately pin an
# unrelated component whose own version happens to contain the substring, and
# the build never stamps into one. Markdown is excluded because documentation
# legitimately names release candidates — an upgrade note or a README example
# citing the rc being promoted is correct prose, not an unrewritten ref.
leftovers=$(grep -rIlE --exclude-dir=charts --exclude='*.md' \
-- "(image|repository|tag)[\"']?:[^#]*${RC_ESC}|${RC_ESC}@sha256:" "$ROOT" 2>/dev/null || true)
if [ -n "$leftovers" ]; then
echo "::error::the following files still carry the rc version '${RC_VERSION}' in image-reference position after the rewrite:" >&2
printf '%s\n' "$leftovers" >&2
echo "Most likely each is a first-party image ref in a storage location hack/lib/image-refs.sh does not enumerate;" >&2
echo "if so, declare it in IMAGE_REF_EXTRA_FILES there — see docs/agents/image-refs.md." >&2
echo "If instead the match is an unrelated version string that merely looks like a ref, narrow the pattern above" >&2
echo "or add an --exclude for that file; do NOT declare a non-image file as a ref source." >&2
exit 1
fi
echo "Rewrote ${RC_VERSION} -> ${STABLE_VERSION} in ${rewritten} file(s) under ${ROOT}; no rc references remain in image-reference position."