hack/promote-retag.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/sh
# Promote a release-candidate to stable by RETAGGING its already-built images —
# no rebuild. Every cluster artifact cozystack ships (component images, the
# cozystack-packages OCI artifact) is content-addressed; this script reads the
# digests the rc baked into the package values.yaml files and copies each, by
# digest, to the stable tag. Because the copy source is the immutable digest,
# the stable image is bit-for-bit the rc image that passed e2e — promotion
# cannot diverge from what was tested.
#
# Usage: hack/promote-retag.sh <stable-version> [--dry-run]
# <stable-version> e.g. v1.4.0 (the tag the rc is promoted to)
# --dry-run print the skopeo copies without executing them
#
# Environment:
# MOVE_LATEST=1 also (re)point :latest at each promoted image. OFF by
# default: :latest must move only when the version being
# promoted is the newest published stable, otherwise a patch
# on an older line (e.g. v1.4.6 while 1.5.x is current) would
# drag :latest backwards. The caller (finalize) computes this
# with the same max-semver test it uses for the release's
# make_latest flag and sets MOVE_LATEST accordingly.
#
# Reads image refs from the CURRENT tree (see hack/lib/image-refs.sh for which
# files that covers), which is expected to be the promoted stable
# digest-vendored tree (the release-X.Y.Z branch, whose digests are the rc's —
# only the cosmetic tag string differs).
# Requires: yq (mikefarah), skopeo, sha256sum, and a registry login already done.
#
# The repo/tag split (ref_repo below) strips the :tag from the last path
# component only, so registry hosts that carry a :port are preserved.
set -eu
STABLE="${1:?usage: promote-retag.sh <stable-version> [--dry-run]}"
DRY_RUN=0
[ "${2:-}" = "--dry-run" ] && DRY_RUN=1
# :latest is repointed only when the caller asserts this is the newest stable.
MOVE_LATEST="${MOVE_LATEST:-0}"
# Only cozystack-owned images (those the build pushed to $REGISTRY) are
# retagged. Everything else vendored by digest — third-party images and bare
# upstream tags — lives in registries this job cannot push to. Override
# REGISTRY to match a fork's build registry; the default mirrors
# hack/common-envs.mk.
REGISTRY="${REGISTRY:-ghcr.io/cozystack/cozystack}"
command -v yq >/dev/null || { echo "yq (mikefarah) is required" >&2; exit 1; }
# The queries below use mikefarah syntax; reject python-yq and other variants
# (mirrors the build-deps check in the Makefile).
yq --version 2>&1 | grep -q mikefarah || { echo "yq (mikefarah) is required" >&2; exit 1; }
# skopeo is only needed to actually copy; a --dry-run just prints the plan.
[ "$DRY_RUN" -eq 1 ] || command -v skopeo >/dev/null || { echo "skopeo is required" >&2; exit 1; }
[ "$DRY_RUN" -eq 1 ] || command -v sha256sum >/dev/null || { echo "sha256sum is required" >&2; exit 1; }
# Ref collection (which files are scanned, and the YAML shapes within them) is
# shared with hack/nightly-mirror.sh and hack/promote-rewrite-tags.sh — see
# hack/lib/image-refs.sh. It was duplicated between the first two for as long
# as both existed, and they drifted: this script and the mirror scanned only
# the depth-2 values.yaml, so every ref stored in an images/*.tag file or
# stamped into a template was silently skipped. That is the same failure mode
# the shape-3 rule was added for — the promotion reports success while never
# creating those images' :<version> tags. Because the retag happens within one
# registry the digests still resolve and digest-pinned installs are unaffected,
# but the release does not carry the tags it claims to. Keep the enumeration in
# one place so a fix reaches every consumer at once.
# shellcheck source=hack/lib/image-refs.sh
. "$(dirname "$0")/lib/image-refs.sh"
collect_refs() { collect_image_refs packages; }
# Split a "<repo>[:<tag>]@sha256:<digest>" ref into repo and digest.
# ref_repo strips the :tag from the LAST path component only, so a registry
# host that carries a :port (e.g. localhost:5000/cozystack/operator, with no
# tag) keeps its port instead of being truncated to the host.
ref_repo() {
r="${1%@*}" # strip @digest
img="${r##*/}" # last path component (may carry :tag)
if [ "$r" = "$img" ]; then
printf '%s' "${img%:*}"
else
printf '%s/%s' "${r%/*}" "${img%:*}"
fi
}
ref_digest() { printf '%s' "${1##*@}"; } # sha256:...
# manifest_digest <ref> — the digest of <ref>'s raw manifest, or empty output if
# the tag is PROVEN not to exist. Anything else is indeterminate and returns
# non-zero, which under `set -e` aborts the promotion before it writes.
#
# The three states matter because the caller turns "empty" into "not published,
# safe to copy over". Inferring that from a failure — or from a successful
# response with no bytes — is how a rate-limit or a proxy hiccup gets read as
# permission to overwrite released bytes. Absence has to be proven by the
# registry saying so.
manifest_digest() {
_ref="$1"
_manifest="$(mktemp)"
_mderr="$(mktemp)"
# Hash the raw bytes so this works for images and OCI artifacts alike. Files
# preserve inspect's status without relying on non-POSIX pipefail.
if skopeo inspect --raw "docker://$_ref" >"$_manifest" 2>"$_mderr"; then
if [ -s "$_manifest" ]; then
printf 'sha256:%s' "$(sha256sum "$_manifest" | cut -d' ' -f1)"
rm -f "$_manifest" "$_mderr"
return 0
fi
# Exit 0 with no bytes — a registry answering 200 with an empty body. Not a
# digest (hashing nothing yields sha256:e3b0c442…, which looks real and
# belongs to no manifest) and not proof of absence either, so refuse to
# decide rather than let the caller copy over whatever is really there.
echo "::error::skopeo inspect of ${_ref} succeeded but returned no manifest bytes; refusing to decide whether that tag exists" >&2
rm -f "$_manifest" "$_mderr"
return 1
fi
# Non-zero. Only a registry that says the manifest is unknown proves absence;
# 429/5xx/auth/network failures all also exit non-zero with no bytes, and
# reading those as "not published" is exactly the fail-open this guards.
if grep -qiE 'manifest unknown|manifest not known|manifest for [^ ]* not found|not found|404' "$_mderr"; then
rm -f "$_manifest" "$_mderr"
return 0
fi
echo "::error::skopeo inspect of ${_ref} failed and did not report the manifest as unknown, so it cannot be treated as unpublished: $(tr '\n' ' ' <"$_mderr")" >&2
rm -f "$_manifest" "$_mderr"
return 1
}
copy() {
_src="$1"; _dst="$2"
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY-RUN skopeo copy --multi-arch all docker://$_src docker://$_dst"
return 0
fi
# --multi-arch all copies the whole manifest list (every platform); it is
# mutually exclusive with the deprecated --all alias, do not combine them.
skopeo copy --multi-arch all "docker://$_src" "docker://$_dst"
}
# Normalize every collected ref to the canonical "<repo>@<digest>" form (drops
# the cosmetic :tag from shape 1) so the dedup is on the real identity — the
# same image vendored in two shapes is retagged once, not twice.
refs=""
for raw in $(collect_refs); do
[ -n "$raw" ] || continue
_repo="$(ref_repo "$raw")"
# Retag only cozystack-owned images: those the build pushes to $REGISTRY, so
# a skopeo copy can succeed. Everything else is vendored by digest from a
# registry this job cannot push to, and a copy there would fail and (under
# set -e) abort the whole promotion. What this drops today:
# - third-party hosts: docker.io/clastix/kubectl,
# ghcr.io/lexfrei/{kuberture,ouroboros} (deliberately not mirrored under
# ghcr.io/cozystack — see those packages' values.yaml)
# - ghcr.io/cozystack/ingress-nginx-with-protobuf-exporter/*, which is a
# cozystack-org repo but sits outside $REGISTRY's path
# - non-ref scalars, e.g. a "--migrate-image=..." arg string
# Note kilo, kube-ovn and keycloak-operator are NOT in that list: all three
# are built and pushed to $REGISTRY, and are selected by shapes 3 and 4. An
# earlier version of this comment named them as unpushed third parties, which
# is what let their missing release tags go unnoticed.
case "$_repo" in
"${REGISTRY}/"*) ;;
*) continue ;;
esac
refs="${refs}${_repo}@$(ref_digest "$raw")
"
done
refs="$(printf '%s' "$refs" | sort -u)"
[ -n "$refs" ] || { echo "No cozystack-owned digest-pinned image refs found under ${REGISTRY}/ — is this the rc's baked tree?" >&2; exit 1; }
echo "$refs" | while IFS= read -r ref; do
[ -n "$ref" ] || continue
repo="${ref%@*}"
digest="${ref##*@}"
echo "▸ ${repo} ${digest}"
# The stable tag is write-once at the image level: resolve the destination's
# raw manifest digest before copying. No-op if it already points at this rc
# digest (idempotent re-run), fail if it points elsewhere (a partial run or
# manual push already put different bytes there) rather than mutate released
# bytes. :latest is intentionally mutable and (re)pointed below only when
# MOVE_LATEST=1.
if [ "$DRY_RUN" -eq 0 ]; then
cur="$(manifest_digest "${repo}:${STABLE}")"
if [ -n "$cur" ] && [ "$cur" != "$digest" ]; then
echo "::error::${repo}:${STABLE} already exists at '${cur}'; refusing to move it to '${digest}' (stable image tags are write-once)" >&2
exit 1
fi
if [ "$cur" = "$digest" ]; then
echo " = ${repo}:${STABLE} already at ${digest}; skipping stable copy"
else
copy "$ref" "${repo}:${STABLE}"
fi
else
copy "$ref" "${repo}:${STABLE}"
fi
[ "$MOVE_LATEST" = "1" ] && copy "$ref" "${repo}:latest"
# Verify the stable tag now resolves to the exact rc digest (skip in dry-run).
if [ "$DRY_RUN" -eq 0 ]; then
got="$(manifest_digest "${repo}:${STABLE}")"
if [ "$got" != "$digest" ]; then
echo "::error::${repo}:${STABLE} resolved to '${got}', expected '${digest}'" >&2
exit 1
fi
fi
done
if [ "$MOVE_LATEST" = "1" ]; then
echo "Retagged image refs to ${STABLE} (+latest)."
else
echo "Retagged image refs to ${STABLE} (:latest left unmoved; MOVE_LATEST=0)."
fi