Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
103 lines
4.0 KiB
YAML
103 lines
4.0 KiB
YAML
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 }}
|