name: Promote Stable Release to Docker Latest # Manual workflow to promote a stable vX.Y.Z release to Docker mutable tags # (:latest, :MAJOR, :MINOR) after the rc.1 soak window. # # Flow: # 1. Validate that the input tag exists as a stable (non-prerelease) GitHub release. # 2. Dispatch docker-release.yml with promote_to_latest=true for the given tag. # This triggers the promote-mutable-tags job (gated on workflow_dispatch # with promote_to_latest=true) which adds :latest/:MAJOR/:MINOR Docker aliases. # # Pre-conditions: # - The stable tag must already exist as a GitHub release (created automatically # by semantic-release when the PR merges to main). # - The immutable : Docker image must already be published and smoke-tested # (done automatically by docker-release.yml on the release: published event). # - Operator has verified the immutable image is stable (24h soak recommended). # # See docs/release-process.md for the full soak + promote procedure. on: workflow_dispatch: inputs: tag: description: > Stable tag to promote, e.g. v7.80.0. Must already exist as a GitHub stable release with the immutable Docker : image already published and smoke-tested. required: true type: string jobs: promote: name: Promote ${{ inputs.tag }} Docker mutable tags runs-on: [self-hosted, linux, x64, cliproxy] permissions: contents: read # to verify the release actions: write # to dispatch docker-release.yml steps: - name: Validate stable semver tag format env: TAG: ${{ inputs.tag }} run: | if [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "[OK] Tag format valid: ${TAG}" else echo "[X] Expected vX.Y.Z format, got: ${TAG}" echo " For rc tags use docker-release.yml directly with promote_to_latest=true." exit 1 fi - name: Verify release exists and is stable (not a prerelease) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ inputs.tag }} run: | IS_PRERELEASE=$(gh release view "${TAG}" \ --repo "${{ github.repository }}" \ --json isPrerelease --jq '.isPrerelease') if [[ "${IS_PRERELEASE}" == "true" ]]; then echo "[X] Release ${TAG} is still a prerelease (isPrerelease=true)" echo " Semantic-release publishes stable releases to main automatically." echo " Check that the tag was created from a main merge, not a dev prerelease." exit 1 fi if [[ "${IS_PRERELEASE}" != "false" ]]; then echo "[X] Could not read release state for ${TAG} (got: ${IS_PRERELEASE})" echo " Verify the tag exists: gh release view ${TAG} --repo ${{ github.repository }}" exit 1 fi echo "[OK] Release ${TAG} is stable — proceeding with Docker mutable tag promotion" - name: Verify immutable Docker image exists env: TAG: ${{ inputs.tag }} run: | TAG_SANS_V="${TAG}" TAG_SANS_V="${TAG_SANS_V#v}" OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]') IMAGE_REF="ghcr.io/${OWNER_LOWER}/ccs:${TAG_SANS_V}" echo "[i] Verifying immutable image: ${IMAGE_REF}" if ! docker manifest inspect "${IMAGE_REF}" > /dev/null 2>&1; then echo "[X] Immutable image ${IMAGE_REF} not found in ghcr.io" echo " Run docker-release.yml for this tag first, or wait for smoke tests to complete." exit 1 fi echo "[OK] Immutable image ${IMAGE_REF} confirmed in registry" - name: Dispatch Docker mutable tag promotion env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ inputs.tag }} run: | RUN_URL=$(gh workflow run "Publish Docker Image" \ --repo "${{ github.repository }}" \ --field "tag=${TAG}" \ --field "promote_to_latest=true" \ 2>&1) echo "[OK] Dispatched docker-release.yml with tag=${TAG} promote_to_latest=true" echo "[i] Monitor the run at: https://github.com/${{ github.repository }}/actions/workflows/docker-release.yml" echo "[i] After the run completes, verify with:" echo " docker buildx imagetools inspect ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/ccs:latest"