mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-28 15:47:33 +09:00
feat(release): add nightly OCIR->GHCR image mirror script
hack/nightly-mirror.sh copies the latest main build's cozystack-owned component images from the CI build registry (OCIR) to the public release registry (GHCR) by digest — no rebuild — then rewrites the baked package tree's image hosts so the GHCR install closure is self-contained. It reuses promote-retag.sh's three-shape ref enumeration, filters to cozystack-owned refs, and deliberately excludes the cozystack-packages artifact (rebuilt downstream from the rewritten tree). Dry-run bats test under the cozytest harness guards the ref selection, the cozystack-packages exclusion, third-party filtering, and the host rewrite plan. shellcheck clean. Assisted-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
1 parent
a49ee8c42e
commit
4777273dbd
2 files changed
+229
No files matched your search
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/bin/sh
|
||||
# Mirror the latest main-branch build's cozystack-owned component images from the
|
||||
# CI build registry (OCIR) to the public release registry (GHCR) BY DIGEST — no
|
||||
# rebuild — and rewrite the baked package tree so the GHCR install closure is
|
||||
# self-contained on GHCR.
|
||||
#
|
||||
# A nightly is a *renamed copy* of what main already built: every image cozystack
|
||||
# ships is content-addressed, so copying each `<repo>@<digest>` from the source
|
||||
# registry to the destination registry preserves the digest bit-for-bit. The
|
||||
# nightly that users install from GHCR is the exact image set built and cached on
|
||||
# the last push to main — nothing is rebuilt here.
|
||||
#
|
||||
# Usage: hack/nightly-mirror.sh <version> <baked-tree-dir> [--dry-run]
|
||||
# <version> dest tag, e.g. 0.0.0-nightly.20260626 (a floating tag is
|
||||
# also moved — see FLOATING)
|
||||
# <baked-tree-dir> the extracted cozystack-packages OCI artifact: a directory
|
||||
# whose layout is <dir>/<group>/<pkg>/values.yaml (apps/,
|
||||
# core/, system/, extra/). This is the digest-vendored tree
|
||||
# main's build baked but never committed.
|
||||
# --dry-run print the skopeo copies and the host rewrite without doing
|
||||
# them
|
||||
#
|
||||
# On success the baked tree's values.yaml files have every SRC_REGISTRY image
|
||||
# host rewritten to DST_REGISTRY (digests untouched). The caller (nightly.yaml)
|
||||
# then re-pushes that rewritten tree as the GHCR cozystack-packages artifact and
|
||||
# repackages the cozy-installer chart against it — this script deliberately does
|
||||
# NOT touch the cozystack-packages artifact itself (it is rebuilt downstream
|
||||
# from the rewritten content, so a copy here would just be overwritten).
|
||||
#
|
||||
# Requires: yq (mikefarah), skopeo, and a login to both registries already done.
|
||||
set -eu
|
||||
|
||||
VERSION="${1:?usage: nightly-mirror.sh <version> <baked-tree-dir> [--dry-run]}"
|
||||
TREE="${2:?usage: nightly-mirror.sh <version> <baked-tree-dir> [--dry-run]}"
|
||||
DRY_RUN=0
|
||||
[ "${3:-}" = "--dry-run" ] && DRY_RUN=1
|
||||
|
||||
# Source = the per-CI build registry main pushes to; dest = the public release
|
||||
# registry nightlies are served from. Defaults mirror hack/common-envs.mk
|
||||
# (REGISTRY) and the CI workflows; override either for a fork.
|
||||
SRC_REGISTRY="${SRC_REGISTRY:-iad.ocir.io/idyksih5sir9/cozystack}"
|
||||
DST_REGISTRY="${DST_REGISTRY:-ghcr.io/cozystack/cozystack}"
|
||||
# Floating tag moved to this nightly in addition to the pinned <version>.
|
||||
FLOATING="${FLOATING:-nightly}"
|
||||
|
||||
[ -d "$TREE" ] || { echo "baked-tree-dir '$TREE' is not a directory" >&2; exit 1; }
|
||||
command -v yq >/dev/null || { echo "yq (mikefarah) is required" >&2; exit 1; }
|
||||
yq --version 2>&1 | grep -q mikefarah || { echo "yq (mikefarah) is required" >&2; exit 1; }
|
||||
[ "$DRY_RUN" -eq 1 ] || command -v skopeo >/dev/null || { echo "skopeo is required" >&2; exit 1; }
|
||||
|
||||
# Collect "repo@sha256:..." refs from every package values.yaml, across the three
|
||||
# shapes the build writes (identical to hack/promote-retag.sh):
|
||||
# 1. single string <repo>:<tag>@sha256:<digest>
|
||||
# 2. split map {repository, tag, digest}
|
||||
# 3. OCI artifact {platformSourceUrl: oci://<repo>, platformSourceRef: digest=...}
|
||||
collect_refs() {
|
||||
for f in "$TREE"/*/*/values.yaml; do
|
||||
[ -f "$f" ] || continue
|
||||
yq -r '.. | select(tag == "!!str") | select(test("@sha256:[0-9a-f]{64}"))' "$f" 2>/dev/null || true
|
||||
yq -r '.. | select(tag == "!!map") | select(has("repository") and has("digest")) | .repository + "@" + .digest' "$f" 2>/dev/null || true
|
||||
yq -r '.. | select(tag == "!!map") | select(has("platformSourceUrl") and has("platformSourceRef")) | (.platformSourceUrl | sub("^oci://"; "")) + "@" + (.platformSourceRef | sub("^digest="; ""))' "$f" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# Split a "<repo>[:<tag>]@sha256:<digest>" ref into repo and digest, stripping
|
||||
# the :tag from the LAST path component only so a host :port is preserved.
|
||||
ref_repo() {
|
||||
r="${1%@*}"
|
||||
img="${r##*/}"
|
||||
if [ "$r" = "$img" ]; then
|
||||
printf '%s' "${img%:*}"
|
||||
else
|
||||
printf '%s/%s' "${r%/*}" "${img%:*}"
|
||||
fi
|
||||
}
|
||||
ref_digest() { printf '%s' "${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
|
||||
skopeo copy --multi-arch all "docker://$_src" "docker://$_dst"
|
||||
}
|
||||
|
||||
# Normalize to canonical "<repo>@<digest>", keep only SRC_REGISTRY-owned refs
|
||||
# (third-party images live in registries this job cannot push to), and drop the
|
||||
# cozystack-packages artifact — the caller rebuilds it from the rewritten tree,
|
||||
# so copying it here would only be overwritten downstream. Dedup on real identity.
|
||||
refs=""
|
||||
for raw in $(collect_refs); do
|
||||
[ -n "$raw" ] || continue
|
||||
_repo="$(ref_repo "$raw")"
|
||||
case "$_repo" in
|
||||
"${SRC_REGISTRY}/cozystack-packages") continue ;;
|
||||
"${SRC_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 ${SRC_REGISTRY}/ in '${TREE}' — is this the baked main tree?" >&2; exit 1; }
|
||||
|
||||
echo "$refs" | while IFS= read -r ref; do
|
||||
[ -n "$ref" ] || continue
|
||||
src_repo="${ref%@*}"
|
||||
digest="${ref##*@}"
|
||||
# Swap the source host prefix for the dest host; the path tail is identical.
|
||||
dst_repo="${DST_REGISTRY}/${src_repo#"${SRC_REGISTRY}/"}"
|
||||
echo "▸ ${src_repo} -> ${dst_repo} ${digest}"
|
||||
copy "${src_repo}@${digest}" "${dst_repo}:${VERSION}"
|
||||
copy "${src_repo}@${digest}" "${dst_repo}:${FLOATING}"
|
||||
# Verify the dest pinned tag resolves to the exact source digest.
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
got="$(skopeo inspect --format '{{.Digest}}' "docker://${dst_repo}:${VERSION}" 2>/dev/null || echo '')"
|
||||
if [ "$got" != "$digest" ]; then
|
||||
echo "::error::${dst_repo}:${VERSION} resolved to '${got}', expected '${digest}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Rewrite the image host SRC_REGISTRY -> DST_REGISTRY across the whole baked tree,
|
||||
# digests untouched. Only cozystack-owned refs carry the SRC_REGISTRY prefix, so a
|
||||
# literal substring replace cannot touch third-party image hosts. Escape the dots
|
||||
# so sed matches the literal host, not "any character". This also rewrites the
|
||||
# platformSourceUrl (oci://SRC/cozystack-packages -> oci://DST/cozystack-packages);
|
||||
# its digest (platformSourceRef) is reset by the caller after the GHCR re-push.
|
||||
SRC_ESC=$(printf '%s' "$SRC_REGISTRY" | sed -e 's/[].[^$*/\\]/\\&/g')
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "DRY-RUN sed -i 's|${SRC_REGISTRY}/|${DST_REGISTRY}/|g' over ${TREE}/*/*/values.yaml"
|
||||
else
|
||||
find "$TREE" -name values.yaml -print0 \
|
||||
| xargs -0 sed -i "s|${SRC_ESC}/|${DST_REGISTRY}/|g"
|
||||
fi
|
||||
|
||||
echo "Mirrored cozystack-owned images to ${DST_REGISTRY} (:${VERSION} +:${FLOATING}) and rewrote tree hosts."
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bats
|
||||
# Tests for hack/nightly-mirror.sh — the OCIR->GHCR nightly image-mirror selector.
|
||||
#
|
||||
# Guards the ref selection and host rewrite: only cozystack-owned component
|
||||
# images are copied (third-party images and bare upstream tags are skipped, as
|
||||
# they live in registries this job cannot push to), the cozystack-packages
|
||||
# artifact is deliberately excluded (it is rebuilt downstream from the rewritten
|
||||
# tree), and every copy targets the destination registry with the source digest
|
||||
# preserved.
|
||||
#
|
||||
# Harness note: the CI path is hack/cozytest.sh, NOT real bats — see the same
|
||||
# note in hack/promote-retag_test.bats. No `run`, `$status`, `$output`, `skip`,
|
||||
# or setup()/teardown(); each @test is a shell function under `set -eu -x`, so a
|
||||
# non-zero exit aborts the test (that is the exit-0 assertion). A test that
|
||||
# expects a non-zero exit must capture it with `|| rc=$?`. mikefarah yq is
|
||||
# assumed present (provided by the test toolchain).
|
||||
#
|
||||
# Run with: hack/cozytest.sh hack/nightly-mirror_test.bats
|
||||
|
||||
# Build a synthetic baked tree exercising all three image-ref shapes plus the
|
||||
# refs that MUST be filtered (third-party host, cozystack-packages artifact).
|
||||
_make_tree() {
|
||||
D="$(printf 'a%.0s' $(seq 1 64))" # 64-hex fake digest body
|
||||
t="$1"
|
||||
mkdir -p "$t/system/foo" "$t/system/bar" "$t/system/third" "$t/core/installer"
|
||||
# shape 1: single string, cozystack-owned -> copied
|
||||
printf 'image: iad.ocir.io/idyksih5sir9/cozystack/foo:main@sha256:%s\n' "$D" > "$t/system/foo/values.yaml"
|
||||
# shape 2: split map, cozystack-owned -> copied
|
||||
{
|
||||
echo 'image:'
|
||||
echo ' repository: iad.ocir.io/idyksih5sir9/cozystack/bar'
|
||||
echo ' tag: main'
|
||||
printf ' digest: sha256:%s\n' "$D"
|
||||
} > "$t/system/bar/values.yaml"
|
||||
# third-party single string -> skipped
|
||||
printf 'image: docker.io/clastix/kubectl:1.0@sha256:%s\n' "$D" > "$t/system/third/values.yaml"
|
||||
# shape 3: operator (cozystack-owned -> copied) + platformSource (cozystack-packages -> skipped)
|
||||
{
|
||||
echo 'cozystackOperator:'
|
||||
printf ' image: iad.ocir.io/idyksih5sir9/cozystack/cozystack-operator:main@sha256:%s\n' "$D"
|
||||
echo ' platformSourceUrl: oci://iad.ocir.io/idyksih5sir9/cozystack/cozystack-packages'
|
||||
printf ' platformSourceRef: "digest=sha256:%s"\n' "$D"
|
||||
} > "$t/core/installer/values.yaml"
|
||||
}
|
||||
|
||||
@test "dry-run mirrors only cozystack-owned component images to the dest registry" {
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
_make_tree "$tmp/tree"
|
||||
|
||||
rc=0
|
||||
hack/nightly-mirror.sh 0.0.0-nightly.test "$tmp/tree" --dry-run \
|
||||
>"$tmp/out" 2>"$tmp/err" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "nightly-mirror.sh exited $rc; yq: $(yq --version 2>&1)" >&2
|
||||
echo "--- stderr ---" >&2; cat "$tmp/err" >&2
|
||||
echo "--- stdout ---" >&2; cat "$tmp/out" >&2
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
# The three cozystack-owned component images are each copied to GHCR by digest.
|
||||
grep -q 'docker://iad.ocir.io/idyksih5sir9/cozystack/foo@sha256:.* docker://ghcr.io/cozystack/cozystack/foo:0.0.0-nightly.test' "$tmp/out"
|
||||
grep -q 'docker://ghcr.io/cozystack/cozystack/bar:0.0.0-nightly.test' "$tmp/out"
|
||||
grep -q 'docker://ghcr.io/cozystack/cozystack/cozystack-operator:0.0.0-nightly.test' "$tmp/out"
|
||||
# A floating tag is moved alongside the pinned version.
|
||||
grep -q 'docker://ghcr.io/cozystack/cozystack/foo:nightly' "$tmp/out"
|
||||
|
||||
# Third-party images never appear in the copy plan.
|
||||
! grep -q 'docker.io/clastix' "$tmp/out"
|
||||
# The cozystack-packages artifact is excluded — it is rebuilt downstream.
|
||||
! grep -qE 'skopeo copy.*cozystack-packages' "$tmp/out"
|
||||
|
||||
# The host rewrite is planned source->dest.
|
||||
grep -q "s|iad.ocir.io/idyksih5sir9/cozystack/|ghcr.io/cozystack/cozystack/|g" "$tmp/out"
|
||||
}
|
||||
|
||||
@test "empty selection (wrong source registry) exits non-zero with a diagnostic" {
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
_make_tree "$tmp/tree"
|
||||
|
||||
# No images live under example.com/nope, so nothing is selected and the script
|
||||
# exits non-zero rather than silently mirroring an empty set.
|
||||
rc=0
|
||||
SRC_REGISTRY="example.com/nope" hack/nightly-mirror.sh 0.0.0-nightly.test "$tmp/tree" --dry-run \
|
||||
>"$tmp/out" 2>"$tmp/err" || rc=$?
|
||||
|
||||
[ "$rc" -ne 0 ]
|
||||
grep -q 'No cozystack-owned digest-pinned image refs found' "$tmp/err"
|
||||
}
|
||||
Reference in new issue
Block a user