mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 14:21:20 +00:00
506 lines
20 KiB
YAML
506 lines
20 KiB
YAML
name: Publish Docker Image
|
|
|
|
on:
|
|
release:
|
|
types:
|
|
- published
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Stable tag to publish manually, for example v7.55.0
|
|
required: true
|
|
type: string
|
|
promote_to_latest:
|
|
description: >
|
|
Promote mutable :latest tags (and major/minor aliases) after rc soak.
|
|
Skipped by default so the first publish of a tag only pushes the
|
|
immutable version tag. Use only after rc.N soak confirms stability.
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
# Prevent stale parallel runs for the same tag; never cancel in-progress so
|
|
# a publish + sign + promote sequence always completes atomically.
|
|
concurrency:
|
|
group: docker-release-${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag || github.ref || github.run_id }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# ---------------------------------------------------------------------------
|
|
# Job 1: Legacy ccs-dashboard image (2-release sunset window)
|
|
# ---------------------------------------------------------------------------
|
|
publish-dashboard:
|
|
name: Publish legacy ccs-dashboard image
|
|
# Skip on prerelease release events — only stable vX.Y.Z GitHub releases
|
|
# publish the legacy image. workflow_dispatch always passes through.
|
|
if: ${{ github.event_name != 'release' || !github.event.release.prerelease }}
|
|
runs-on: [self-hosted, linux, x64, cliproxy]
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
env:
|
|
DEPRECATION_BASELINE_VERSION: "7.80.0"
|
|
STABLE_RELEASE_WINDOW: "2"
|
|
|
|
steps:
|
|
- name: Resolve target tag
|
|
id: target
|
|
env:
|
|
MANUAL_TAG: ${{ inputs.tag }}
|
|
RELEASE_EVENT_TAG: ${{ github.event.release.tag_name }}
|
|
run: |
|
|
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
|
|
TARGET_TAG="${RELEASE_EVENT_TAG}"
|
|
else
|
|
TARGET_TAG="${MANUAL_TAG}"
|
|
fi
|
|
echo "tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Validate stable semver tag
|
|
id: tag
|
|
run: |
|
|
if [[ "${{ steps.target.outputs.tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "publish=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
echo "publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "Skipping non-stable semver tag ${{ steps.target.outputs.tag }}"
|
|
|
|
- name: Checkout release tag
|
|
if: steps.tag.outputs.publish == 'true'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ format('refs/tags/{0}', steps.target.outputs.tag) }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Check legacy dashboard sunset
|
|
if: steps.tag.outputs.publish == 'true'
|
|
id: sunset
|
|
env:
|
|
TARGET_TAG: ${{ steps.target.outputs.tag }}
|
|
run: node scripts/docker-dashboard-sunset-guard.js
|
|
|
|
- name: Set up QEMU
|
|
if: steps.tag.outputs.publish == 'true' && steps.sunset.outputs.publish == 'true'
|
|
uses: docker/setup-qemu-action@v3
|
|
|
|
- name: Set up Docker Buildx
|
|
if: steps.tag.outputs.publish == 'true' && steps.sunset.outputs.publish == 'true'
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Derive image metadata
|
|
if: steps.tag.outputs.publish == 'true' && steps.sunset.outputs.publish == 'true'
|
|
id: meta
|
|
run: |
|
|
VERSION="${{ steps.target.outputs.tag }}"
|
|
VERSION="${VERSION#v}"
|
|
MINOR="${VERSION%.*}"
|
|
MAJOR="${VERSION%%.*}"
|
|
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
|
IMAGE="ghcr.io/${OWNER_LOWER}/ccs-dashboard"
|
|
REVISION=$(git rev-parse HEAD)
|
|
TAGS="${IMAGE}:${VERSION}"
|
|
|
|
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
|
|
TAGS="${TAGS}
|
|
${IMAGE}:${MINOR}
|
|
${IMAGE}:${MAJOR}
|
|
${IMAGE}:latest"
|
|
fi
|
|
|
|
{
|
|
echo "version=${VERSION}"
|
|
echo "minor=${MINOR}"
|
|
echo "major=${MAJOR}"
|
|
echo "image=${IMAGE}"
|
|
echo "revision=${REVISION}"
|
|
echo "tags<<EOF"
|
|
echo "${TAGS}"
|
|
echo "EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
if: steps.tag.outputs.publish == 'true' && steps.sunset.outputs.publish == 'true'
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push legacy dashboard image
|
|
if: steps.tag.outputs.publish == 'true' && steps.sunset.outputs.publish == 'true'
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: docker/Dockerfile
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
provenance: mode=max
|
|
sbom: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: |
|
|
org.opencontainers.image.title=ccs-dashboard
|
|
org.opencontainers.image.description=CCS Dashboard container image (deprecated - migrate to ghcr.io/kaitranntt/ccs:latest)
|
|
org.opencontainers.image.url=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
|
|
org.opencontainers.image.revision=${{ steps.meta.outputs.revision }}
|
|
annotations: |
|
|
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
index:org.opencontainers.image.description=CCS Dashboard container image (deprecated - migrate to ghcr.io/kaitranntt/ccs:latest)
|
|
cache-from: type=gha,scope=dashboard
|
|
cache-to: type=gha,mode=max,scope=dashboard
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Job 2: Integrated image — CCS + CLIProxy (single build, no :full variant)
|
|
# Publishes ONLY the immutable :<ver> tag here on every release event.
|
|
# Mutable :latest / major / minor aliases are added by promote-mutable-tags
|
|
# ONLY via explicit workflow_dispatch (promote_to_latest=true) after the
|
|
# operator has verified the immutable image is stable. This keeps the rc.1
|
|
# soak window entirely in the Docker promotion path — npm @latest is always
|
|
# published immediately by semantic-release on the stable release.
|
|
# ---------------------------------------------------------------------------
|
|
publish-integrated:
|
|
name: Publish integrated image
|
|
runs-on: [self-hosted, linux, x64, cliproxy]
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
id-token: write # required for keyless cosign signing
|
|
|
|
outputs:
|
|
version: ${{ steps.meta.outputs.version }}
|
|
image_ref: ${{ steps.meta.outputs.image_ref }}
|
|
publish: ${{ steps.tag.outputs.publish }}
|
|
|
|
steps:
|
|
- name: Resolve target tag
|
|
id: target
|
|
env:
|
|
MANUAL_TAG: ${{ inputs.tag }}
|
|
RELEASE_EVENT_TAG: ${{ github.event.release.tag_name }}
|
|
run: |
|
|
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
|
|
TARGET_TAG="${RELEASE_EVENT_TAG}"
|
|
else
|
|
TARGET_TAG="${MANUAL_TAG}"
|
|
fi
|
|
echo "tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Validate semver tag (stable or rc)
|
|
id: tag
|
|
run: |
|
|
TAG="${{ steps.target.outputs.tag }}"
|
|
# Accept stable (vX.Y.Z) and prerelease rc (vX.Y.Z-rc.N)
|
|
if [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
|
|
echo "publish=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
echo "publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "Skipping unrecognised tag format: ${TAG}"
|
|
|
|
- name: Checkout release tag
|
|
if: steps.tag.outputs.publish == 'true'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ format('refs/tags/{0}', steps.target.outputs.tag) }}
|
|
persist-credentials: false
|
|
|
|
- name: Set up QEMU
|
|
if: steps.tag.outputs.publish == 'true'
|
|
uses: docker/setup-qemu-action@v3
|
|
|
|
- name: Set up Docker Buildx
|
|
if: steps.tag.outputs.publish == 'true'
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Derive image metadata
|
|
if: steps.tag.outputs.publish == 'true'
|
|
id: meta
|
|
run: |
|
|
VERSION="${{ steps.target.outputs.tag }}"
|
|
VERSION="${VERSION#v}"
|
|
MINOR="${VERSION%.*}"
|
|
MAJOR="${VERSION%%.*}"
|
|
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
|
IMAGE="ghcr.io/${OWNER_LOWER}/ccs"
|
|
REVISION=$(git rev-parse HEAD)
|
|
|
|
# Only the immutable version-pinned tag is pushed here.
|
|
# Mutable tags (:latest, :MAJOR, :MINOR) are added by promote-mutable-tags.
|
|
TAGS="${IMAGE}:${VERSION}"
|
|
IMAGE_REF="${IMAGE}:${VERSION}"
|
|
|
|
{
|
|
echo "version=${VERSION}"
|
|
echo "minor=${MINOR}"
|
|
echo "major=${MAJOR}"
|
|
echo "image=${IMAGE}"
|
|
echo "revision=${REVISION}"
|
|
echo "image_ref=${IMAGE_REF}"
|
|
echo "tags<<EOF"
|
|
echo "${TAGS}"
|
|
echo "EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
if: steps.tag.outputs.publish == 'true'
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push integrated image
|
|
if: steps.tag.outputs.publish == 'true'
|
|
id: build
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: docker
|
|
file: docker/Dockerfile.integrated
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
provenance: mode=max
|
|
sbom: true
|
|
build-args: |
|
|
CCS_NPM_VERSION=${{ steps.meta.outputs.version }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: |
|
|
org.opencontainers.image.title=ccs
|
|
org.opencontainers.image.description=CCS integrated image — CLIProxy + CCS CLI
|
|
org.opencontainers.image.url=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
|
|
org.opencontainers.image.revision=${{ steps.meta.outputs.revision }}
|
|
annotations: |
|
|
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
index:org.opencontainers.image.description=CCS integrated image — CLIProxy + CCS CLI
|
|
cache-from: type=gha,scope=integrated
|
|
cache-to: type=gha,mode=max,scope=integrated
|
|
|
|
- name: Install cosign
|
|
if: steps.tag.outputs.publish == 'true'
|
|
uses: sigstore/cosign-installer@v3
|
|
|
|
- name: Sign image with cosign (keyless OIDC)
|
|
if: steps.tag.outputs.publish == 'true'
|
|
env:
|
|
COSIGN_EXPERIMENTAL: "1"
|
|
run: |
|
|
DIGEST="${{ steps.build.outputs.digest }}"
|
|
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
|
cosign sign --yes "ghcr.io/${OWNER_LOWER}/ccs@${DIGEST}"
|
|
echo "[OK] Signed ghcr.io/${OWNER_LOWER}/ccs@${DIGEST}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Job 3: Smoke test — pull the immutable :<ver> tag and verify it boots
|
|
# Runs after publish-integrated; mutable tags are only promoted if this passes.
|
|
# ---------------------------------------------------------------------------
|
|
smoke-test:
|
|
name: Smoke test integrated image
|
|
# Depends only on publish-integrated, NOT publish-dashboard.
|
|
# publish-dashboard is skipped on GitHub prerelease events — if it were
|
|
# listed here, GitHub Actions would also skip smoke-test when that condition
|
|
# is not met, preventing the immutable tag from being verified.
|
|
needs: [publish-integrated]
|
|
if: ${{ needs.publish-integrated.outputs.publish == 'true' }}
|
|
runs-on: [self-hosted, linux, x64, cliproxy]
|
|
|
|
steps:
|
|
- name: Checkout release tag (for test scripts)
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.publish-integrated.outputs.version != '' && format('refs/tags/v{0}', needs.publish-integrated.outputs.version) || github.ref }}
|
|
persist-credentials: false
|
|
|
|
- name: Derive image reference
|
|
id: image
|
|
run: |
|
|
echo "ref=${{ needs.publish-integrated.outputs.image_ref }}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Pull image
|
|
run: docker pull "${{ steps.image.outputs.ref }}"
|
|
|
|
- name: Verify anonymous pull access
|
|
run: |
|
|
CLEAN_DOCKER_CONFIG="$(mktemp -d)"
|
|
trap 'rm -rf "${CLEAN_DOCKER_CONFIG}"' EXIT
|
|
if ! DOCKER_CONFIG="${CLEAN_DOCKER_CONFIG}" docker pull "${{ steps.image.outputs.ref }}"; then
|
|
echo "[X] ghcr.io/kaitranntt/ccs is not anonymously pullable." >&2
|
|
echo "[i] Make the GHCR package public, then rerun the Docker publish workflow." >&2
|
|
exit 1
|
|
fi
|
|
echo "[OK] Anonymous pull succeeded for ${{ steps.image.outputs.ref }}"
|
|
|
|
- name: Assert image size budget (amd64)
|
|
run: |
|
|
chmod +x tests/docker/image-size.sh
|
|
tests/docker/image-size.sh "${{ steps.image.outputs.ref }}" "367001600" "--platform" "linux/amd64"
|
|
|
|
- name: Assert image size budget (arm64)
|
|
run: |
|
|
tests/docker/image-size.sh "${{ steps.image.outputs.ref }}" "367001600" "--platform" "linux/arm64"
|
|
|
|
- name: Boot container and wait for healthcheck
|
|
id: boot
|
|
run: |
|
|
CONTAINER_NAME="ccs-smoke-${{ github.run_id }}"
|
|
echo "container=${CONTAINER_NAME}" >> "$GITHUB_OUTPUT"
|
|
|
|
docker run -d \
|
|
--name "${CONTAINER_NAME}" \
|
|
--rm \
|
|
-p 13000:3000 \
|
|
-p 18317:8317 \
|
|
"${{ steps.image.outputs.ref }}"
|
|
|
|
echo "[i] Waiting for container healthcheck (up to 60s)..."
|
|
HEALTHY=0
|
|
for i in $(seq 1 12); do
|
|
STATUS=$(docker inspect "${CONTAINER_NAME}" --format='{{.State.Health.Status}}' 2>/dev/null || echo "missing")
|
|
if [[ "${STATUS}" == "healthy" ]]; then
|
|
echo "[OK] Container is healthy after $((i * 5))s"
|
|
HEALTHY=1
|
|
break
|
|
fi
|
|
if [[ "${STATUS}" == "unhealthy" ]]; then
|
|
echo "[X] Container marked unhealthy"
|
|
docker logs "${CONTAINER_NAME}" --tail 50 >&2
|
|
exit 1
|
|
fi
|
|
echo " [${i}/12] status=${STATUS}, waiting 5s..."
|
|
sleep 5
|
|
done
|
|
if [[ "${HEALTHY}" -ne 1 ]]; then
|
|
echo "[X] Container did not become healthy within 60s (last status: ${STATUS})" >&2
|
|
docker logs "${CONTAINER_NAME}" --tail 100 >&2
|
|
docker inspect "${CONTAINER_NAME}" --format='{{json .State}}' >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
- name: Run network-contract test
|
|
run: |
|
|
# network-contract.sh signature: <compose-file> [image-ref]
|
|
# Pass compose.yaml as $1 and the pinned image ref as $2 so the
|
|
# smoke test exercises the canonical compose file with the exact
|
|
# image that was just published, not whatever tag is in the file.
|
|
bash tests/docker/network-contract.sh docker/compose.yaml "${{ steps.image.outputs.ref }}"
|
|
|
|
- name: Probe dashboard port 3000
|
|
run: |
|
|
# Single-shot probe by design: healthcheck and network-contract already
|
|
# handle readiness waits, and curl --retry would not retry HTTP errors.
|
|
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time 10 http://127.0.0.1:13000/ || echo "000")
|
|
if [[ "${HTTP_CODE}" == "000" ]]; then
|
|
echo "[X] Could not reach dashboard on :3000 (connection failed)"
|
|
exit 1
|
|
fi
|
|
echo "[OK] Dashboard port 3000 responded with HTTP ${HTTP_CODE}"
|
|
|
|
- name: Probe CLIProxy port 8317
|
|
run: |
|
|
# Single-shot probe by design: healthcheck and network-contract already
|
|
# handle readiness waits, and curl --retry would not retry HTTP errors.
|
|
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time 10 http://127.0.0.1:18317/ || echo "000")
|
|
if [[ "${HTTP_CODE}" == "000" ]]; then
|
|
echo "[X] Could not reach CLIProxy on :8317 (connection failed)"
|
|
exit 1
|
|
fi
|
|
echo "[OK] CLIProxy port 8317 responded with HTTP ${HTTP_CODE}"
|
|
|
|
- name: Stop smoke test container
|
|
if: always()
|
|
run: |
|
|
docker stop "ccs-smoke-${{ github.run_id }}" 2>/dev/null || true
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Job 4: Promote mutable tags — runs ONLY after smoke tests pass
|
|
# Adds :latest, :<major>, :<minor> aliases pointing to the immutable digest.
|
|
#
|
|
# INTENTIONAL DESIGN (issue #1251 rc.1 soak — see docs/release-process.md):
|
|
# GitHub release events publish ONLY the immutable :<ver> tag. The mutable
|
|
# :latest / :<major> / :<minor> aliases STAY POINTED AT THE PRIOR STABLE
|
|
# RELEASE until an operator manually promotes after a soak window (~24h).
|
|
# Trade-off: zero-install users (curl ccs.kaitran.ca/docker-compose.yaml &&
|
|
# docker compose up -d) keep receiving last-known-good :latest during the
|
|
# soak; operators wanting the new version pull :<ver> directly.
|
|
#
|
|
# Operator promotion:
|
|
# gh workflow run promote-release.yml -f tag=v<X.Y.Z>
|
|
# (or equivalently: gh workflow run "Publish Docker Image" -f tag=v<X.Y.Z>
|
|
# -f promote_to_latest=true)
|
|
# ---------------------------------------------------------------------------
|
|
promote-mutable-tags:
|
|
name: Promote mutable tags (:latest / major / minor)
|
|
needs: [smoke-test, publish-integrated]
|
|
# Only promote on explicit operator dispatch — never on automatic release events.
|
|
# Release events publish the immutable :<ver> tag only (see publish-integrated).
|
|
if: |
|
|
needs.publish-integrated.outputs.publish == 'true' &&
|
|
github.event_name == 'workflow_dispatch' && inputs.promote_to_latest == true
|
|
runs-on: [self-hosted, linux, x64, cliproxy]
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
id-token: write # for cosign re-sign of mutable aliases
|
|
|
|
steps:
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Promote :latest / major / minor aliases
|
|
env:
|
|
IMAGE_REF: ${{ needs.publish-integrated.outputs.image_ref }}
|
|
VERSION: ${{ needs.publish-integrated.outputs.version }}
|
|
run: |
|
|
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
|
IMAGE="ghcr.io/${OWNER_LOWER}/ccs"
|
|
MINOR="${VERSION%.*}"
|
|
MAJOR="${VERSION%%.*}"
|
|
|
|
echo "[i] Pointing mutable tags to ${IMAGE_REF}"
|
|
docker buildx imagetools create \
|
|
--tag "${IMAGE}:latest" \
|
|
--tag "${IMAGE}:${MINOR}" \
|
|
--tag "${IMAGE}:${MAJOR}" \
|
|
"${IMAGE_REF}"
|
|
|
|
echo "[OK] Promoted: :latest :${MINOR} :${MAJOR} → ${IMAGE_REF}"
|
|
|
|
- name: Verify promoted tags are anonymously pullable
|
|
env:
|
|
VERSION: ${{ needs.publish-integrated.outputs.version }}
|
|
run: |
|
|
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
|
IMAGE="ghcr.io/${OWNER_LOWER}/ccs"
|
|
MINOR="${VERSION%.*}"
|
|
MAJOR="${VERSION%%.*}"
|
|
CLEAN_DOCKER_CONFIG="$(mktemp -d)"
|
|
trap 'rm -rf "${CLEAN_DOCKER_CONFIG}"' EXIT
|
|
|
|
for TAG in latest "${MINOR}" "${MAJOR}"; do
|
|
REF="${IMAGE}:${TAG}"
|
|
if ! DOCKER_CONFIG="${CLEAN_DOCKER_CONFIG}" docker pull "${REF}"; then
|
|
echo "[X] ${REF} is not anonymously pullable." >&2
|
|
echo "[i] Confirm the GHCR package is public, then rerun promotion." >&2
|
|
exit 1
|
|
fi
|
|
echo "[OK] Anonymous pull succeeded for ${REF}"
|
|
done
|