clusev/.github/workflows/promote-public.yml

82 lines
3.7 KiB
YAML

name: Promote to public (manual fallback)
# Primary promotion is the gated `promote` job in ci-staging.yml (auto on a green stable tag). This
# manual dispatch re-runs the same procedure for a specific tag (e.g. after a promote.sh fix). It
# mirrors the auto job: resolve the exact image digests, build+guard the public code tree, publish
# the images, then push atomically. promote.sh refuses if the tag is already public — yank first.
on:
workflow_dispatch:
inputs:
tag:
description: 'Stable tag to (re-)publish (e.g. v0.11.0)'
required: true
permissions:
contents: read
jobs:
promote:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
packages: write
# The tag reaches every run-step via this env var (never inline `${{ inputs.tag }}` in a shell —
# that would be a command-injection sink for a hand-typed input).
env:
TAG: ${{ inputs.tag }}
steps:
- name: Enforce a clean stable SemVer tag
run: |
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|| { echo "::error::not a stable vX.Y.Z tag: $TAG"; exit 1; }
- uses: actions/checkout@v5
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
path: src
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Resolve private image digests
id: img
run: |
set -euo pipefail
appd=$(docker buildx imagetools inspect "ghcr.io/${{ vars.GHCR_OWNER }}/clusev:$TAG" --format '{{.Manifest.Digest}}')
termd=$(docker buildx imagetools inspect "ghcr.io/${{ vars.GHCR_OWNER }}/clusev-terminal:$TAG" --format '{{.Manifest.Digest}}')
{ echo "appd=$appd"; echo "termd=$termd"; } >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v5
with:
repository: ${{ vars.PUBLIC_REPO_SLUG }}
token: ${{ secrets.PUBLIC_REPO_TOKEN }}
fetch-depth: 0
path: pub
- name: Build the public tree (guarded, no publish)
env:
CLUSEV_PROMOTE_FORBIDDEN: ${{ secrets.PROMOTE_FORBIDDEN }}
APP_DIGEST: ghcr.io/${{ vars.PUBLIC_GHCR_OWNER }}/clusev@${{ steps.img.outputs.appd }}
TERM_DIGEST: ghcr.io/${{ vars.PUBLIC_GHCR_OWNER }}/clusev-terminal@${{ steps.img.outputs.termd }}
run: |
set -euo pipefail
git config --global user.name 'clusev-release'
git config --global user.email 'release@clusev'
chmod +x src/scripts/promote.sh
src/scripts/promote.sh "$PWD/src" "$TAG" "$PWD/pub" "Release $TAG"
printf '{ "version": "%s", "app": "%s", "terminal": "%s" }\n' "$TAG" "$APP_DIGEST" "$TERM_DIGEST" > pub/release-images.lock
git -C pub add release-images.lock
git -C pub commit -q --amend --no-edit
git -C pub tag -f "$TAG"
# Publish AFTER guards, BEFORE the push (publish-then-advertise): a release ref always has an
# image. A push failure leaves only unreferenced idempotent image tags; re-run to recover.
- name: Publish images to public GHCR
env:
APP_SRC: ghcr.io/${{ vars.GHCR_OWNER }}/clusev@${{ steps.img.outputs.appd }}
TERM_SRC: ghcr.io/${{ vars.GHCR_OWNER }}/clusev-terminal@${{ steps.img.outputs.termd }}
run: |
set -euo pipefail
docker buildx imagetools create -t "ghcr.io/${{ vars.PUBLIC_GHCR_OWNER }}/clusev:$TAG" "$APP_SRC"
docker buildx imagetools create -t "ghcr.io/${{ vars.PUBLIC_GHCR_OWNER }}/clusev-terminal:$TAG" "$TERM_SRC"
- name: Push code + tag to public (atomic)
run: git -C pub push --atomic origin HEAD:main "refs/tags/$TAG"