ci: manual production promotion workflow with e2e-gate verification
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
name: Promote to Production (manual)
|
||||
|
||||
# Second approval gate of the deploy model: run this workflow from the
|
||||
# Actions UI after testing the feature on staging. It refuses commits
|
||||
# whose staging E2E gate is not green. See docs/DEPLOY.md.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sha:
|
||||
description: >-
|
||||
Commit SHA on main to promote (full or >=7 chars).
|
||||
Leave empty to promote the latest main commit.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
env:
|
||||
REGISTRY: privaterepo.sitaru.org
|
||||
BACKEND_IMAGE_NAME: ${{ gitea.repository }}-backend
|
||||
FRONTEND_IMAGE_NAME: ${{ gitea.repository }}-frontend
|
||||
PIPELINE_IMAGE_NAME: ${{ gitea.repository }}-pipeline
|
||||
|
||||
jobs:
|
||||
promote-prod:
|
||||
name: Promote approved commit to Production
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Resolve target SHA
|
||||
id: resolve
|
||||
run: |
|
||||
SHA_INPUT="${{ gitea.event.inputs.sha }}"
|
||||
if [ -z "$SHA_INPUT" ]; then
|
||||
SHA_INPUT="${{ gitea.sha }}"
|
||||
fi
|
||||
# Normalise to the full sha via the API so short inputs work
|
||||
FULL_SHA=$(curl -fsS \
|
||||
-H "Authorization: token ${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"https://${REGISTRY}/api/v1/repos/${{ gitea.repository }}/git/commits/${SHA_INPUT}" \
|
||||
| python3 -c "import json,sys; print(json.load(sys.stdin)['sha'])")
|
||||
SHORT_SHA="sha-$(echo "$FULL_SHA" | cut -c1-7)"
|
||||
echo "full=$FULL_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "short=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Promoting $FULL_SHA (images tagged $SHORT_SHA)"
|
||||
|
||||
- name: Verify the staging E2E gate passed for this commit
|
||||
run: |
|
||||
STATUS_JSON=$(curl -fsS \
|
||||
-H "Authorization: token ${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"https://${REGISTRY}/api/v1/repos/${{ gitea.repository }}/commits/${{ steps.resolve.outputs.full }}/status")
|
||||
echo "$STATUS_JSON" | python3 -c "
|
||||
import json, sys
|
||||
d = json.load(sys.stdin)
|
||||
ok = [s for s in d.get('statuses', [])
|
||||
if 'E2E Journeys against Staging' in s.get('context', '')
|
||||
and s.get('status') == 'success']
|
||||
if not ok:
|
||||
print('REFUSED: no successful \"E2E Journeys against Staging\" status on this commit.')
|
||||
print('Contexts found:', [s.get('context') for s in d.get('statuses', [])])
|
||||
sys.exit(1)
|
||||
print('E2E gate verified green for this commit.')
|
||||
"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Retag approved images as prod (keeping rollback pointer)
|
||||
run: |
|
||||
SHORT_SHA="${{ steps.resolve.outputs.short }}"
|
||||
for IMAGE in \
|
||||
"${REGISTRY}/${BACKEND_IMAGE_NAME}" \
|
||||
"${REGISTRY}/${FRONTEND_IMAGE_NAME}" \
|
||||
"${REGISTRY}/${PIPELINE_IMAGE_NAME}"; do
|
||||
# Keep a rollback pointer before moving :prod
|
||||
docker buildx imagetools create -t "${IMAGE}:prod-previous" "${IMAGE}:prod" || true
|
||||
docker buildx imagetools create -t "${IMAGE}:prod" "${IMAGE}:${SHORT_SHA}"
|
||||
echo "Promoted ${IMAGE}:${SHORT_SHA} -> :prod"
|
||||
done
|
||||
|
||||
- name: Trigger production stack update
|
||||
run: curl -fsSk -X POST "${{ secrets.PORTAINER_PROD_WEBHOOK }}"
|
||||
|
||||
- name: Wait for production to become healthy
|
||||
run: |
|
||||
echo "Polling ${PROD_BASE_URL} for up to 5 minutes..."
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS -o /dev/null --max-time 10 "${PROD_BASE_URL}/"; then
|
||||
echo "Production is up (attempt $i)"
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
echo "Production did not become healthy in time" >&2
|
||||
exit 1
|
||||
env:
|
||||
PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }}
|
||||
Reference in New Issue
Block a user