Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 75 additions & 6 deletions .github/workflows/promote-latest.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
name: Promote latest image tag (release)

# GA promotion, run MANUALLY (workflow_dispatch) once the release has merged to
# master. Build-once model: both release images were already built by
# publish-testing.yml at the testing cut. GA does NOT rebuild anything -- it
# repoints tags and parameters at those existing digests, so the bytes validated
# in testing are byte-identical to the bytes that reach production.
# GA promotion. Fires automatically when a release lands on master (the
# testing -> master merge that code-development's GA opens), and is also
# dispatchable for heals. Build-once model: both release images were already
# built by publish-testing.yml at the testing cut. GA does NOT rebuild anything
# -- it repoints tags and parameters at those existing digests, so the bytes
# validated in testing are byte-identical to the bytes that reach production.
#
# Auto-firing is safe precisely BECAUSE nothing is rebuilt. The 20260820-001
# failure came from rebuilding on a master push and racing the GA bundle upload;
# a retag touches no bundle. The deliberate release gate now lives where the
# release decision is actually made -- the code-development testing -> master
# merge, restricted to release owners -- rather than in a second dispatch that
# nobody is prompted to run.
#
# On a push the run is idempotent: if :latest already resolves to the digest we
# would promote, every mutating step is skipped. A workflow_dispatch always runs
# in full, so a heal can re-mirror or re-write SSM deliberately.
#
# 1. Docker Hub :latest -> the existing <VERSION> RC digest, for both
# simplerisk/simplerisk-minimal (<VERSION>-php85) and simplerisk/simplerisk
Expand All @@ -20,6 +32,15 @@ name: Promote latest image tag (release)
# See design code-development docs/superpowers/specs/2026-07-10-release-image-promotion-design.

on:
# The GA merge. Path-filtered to the minimal Dockerfile so a docs-only or
# workflow-only master push does not touch production; that file carries the
# `ENV version=` this job promotes, so it changes on exactly the pushes that
# matter. The idempotence guard below covers the case where it changes for
# some other reason (a CVE regeneration at an unchanged version).
push:
branches: [master]
paths:
- simplerisk-minimal/Dockerfile
workflow_dispatch:
inputs:
skip_full_image:
Expand Down Expand Up @@ -86,7 +107,52 @@ jobs:
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# Idempotence guard for the automatic path. A master push can touch the
# minimal Dockerfile without being a release (a CVE regeneration at an
# unchanged version), and re-promoting would re-mirror to GHCR, mint fresh
# cosign signatures, and rewrite SSM for no reason. If :latest already
# resolves to the digest we would promote for BOTH images, there is
# nothing to do. A workflow_dispatch always proceeds, so a heal can force
# the work deliberately.
- name: Decide whether anything needs promoting
id: guard
env:
VERSION: ${{ steps.ver.outputs.version }}
EVENT: ${{ github.event_name }}
SKIP_FULL: ${{ inputs.skip_full_image }}
run: |
set -euo pipefail
if [ "$EVENT" = "workflow_dispatch" ]; then
echo "manual dispatch — proceeding regardless of current tag state"
echo "needed=true" >> "$GITHUB_OUTPUT"; exit 0
fi
# `|| true` so a missing tag yields an empty string rather than aborting.
digest() { docker buildx imagetools inspect "$1" --format '{{.Manifest.Digest}}' 2>/dev/null || true; }
MIN_SRC=$(digest "${MINIMAL_IMAGE}:${VERSION}-php85")
MIN_CUR=$(digest "${MINIMAL_IMAGE}:latest")
# An ABSENT RC tag is not "nothing to do" -- it means this release has
# no images to promote, which is the loudest thing this workflow can
# tell you. Fail here rather than skipping into a green no-op run.
if [ -z "$MIN_SRC" ]; then
echo "::error::${MINIMAL_IMAGE}:${VERSION}-php85 not found on Docker Hub — the RC was never published, so there is nothing to promote"; exit 1
fi
NEEDED=false
[ "$MIN_SRC" != "$MIN_CUR" ] && NEEDED=true
if [ "$SKIP_FULL" != "true" ]; then
FULL_SRC=$(digest "${FULL_IMAGE}:${VERSION}-noble")
FULL_CUR=$(digest "${FULL_IMAGE}:latest")
# Missing full-stack RC tag: let the promote step run so it fails with
# its own message, which names the skip_full_image escape hatch.
[ -z "$FULL_SRC" ] || [ "$FULL_SRC" != "$FULL_CUR" ] && NEEDED=true
fi
echo "needed=$NEEDED" >> "$GITHUB_OUTPUT"
if [ "$NEEDED" != "true" ]; then
echo "${VERSION}: :latest already points at the RC digest for every image — nothing to promote." \
>> "$GITHUB_STEP_SUMMARY"
fi

- name: Promote simplerisk-minimal — :latest → <VERSION>-php85
if: steps.guard.outputs.needed == 'true'
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
Expand All @@ -113,7 +179,7 @@ jobs:
echo "retagged ${MINIMAL_IMAGE}:latest -> ${VERSION}-php85 ($SRC_DIGEST)" >> "$GITHUB_STEP_SUMMARY"

- name: Promote simplerisk (full-stack) — :latest → <VERSION>-noble
if: ${{ !inputs.skip_full_image }}
if: steps.guard.outputs.needed == 'true' && !inputs.skip_full_image
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
Expand All @@ -132,6 +198,7 @@ jobs:
echo "retagged ${FULL_IMAGE}:latest -> ${VERSION}-noble ($SRC_DIGEST)" >> "$GITHUB_STEP_SUMMARY"

- name: Mirror the promoted digests to GHCR (cosign-signed)
if: steps.guard.outputs.needed == 'true'
env:
VERSION: ${{ steps.ver.outputs.version }}
SKIP_FULL: ${{ inputs.skip_full_image }}
Expand Down Expand Up @@ -163,12 +230,14 @@ jobs:
fi

- name: Configure AWS credentials (OIDC → customers account)
if: steps.guard.outputs.needed == 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.IMAGE_PROMOTER_LATEST_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Promote — SSM /image-tag/latest = <VERSION>-php85
if: steps.guard.outputs.needed == 'true'
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
Expand Down
Loading