Skip to content
Open
77 changes: 77 additions & 0 deletions .github/workflows/deploy-static.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,80 @@ jobs:
environment: ${{ needs.detect.outputs.env }}
secrets:
INFRA_WORKFLOW_TOKEN: ${{ secrets.INFRA_WORKFLOW_TOKEN }}

#######
# Manual promotion jobs — appear after deploy on push to master.
# Developers click "Review deployments" in the Actions UI to trigger.
# Requires GitHub environments "promote-preprod" and "promote-prod"
# configured with required reviewers in repo Settings → Environments.
#######
promote-to-preprod:
name: "Promote: master → preprod"
needs: [detect, trigger-deploy]
if: >
always() && !failure() && !cancelled() &&
needs.detect.outputs.env == 'staging'
runs-on: ubuntu-latest
environment: promote-preprod
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog and create PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./build/ci/generate-promotion-pr.sh master preprod

promote-to-prod:
name: "Promote: preprod → prod"
needs: [detect, trigger-deploy]
if: >
always() && !failure() && !cancelled() &&
needs.detect.outputs.env == 'staging'
runs-on: ubuntu-latest
environment: promote-prod
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog and create PR
id: create_pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./build/ci/generate-promotion-pr.sh preprod prod

- name: Post to Slack
if: ${{ steps.create_pr.outputs.pr_url != 'already exists' && steps.create_pr.outputs.pr_url != 'no-changes' }}
uses: slackapi/slack-github-action@v1.27.0
with:
payload: |
{
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "Production Promotion PR Created" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Commits:* ${{ steps.create_pr.outputs.total }}" },
{ "type": "mrkdwn", "text": "*Features:* ${{ steps.create_pr.outputs.feat_count }}" },
{ "type": "mrkdwn", "text": "*Fixes:* ${{ steps.create_pr.outputs.fix_count }}" },
{ "type": "mrkdwn", "text": "*PR:* <${{ steps.create_pr.outputs.pr_url }}|View PR>" }
]
},
{ "type": "divider" },
{
"type": "section",
"text": { "type": "mrkdwn", "text": "_Triggered by ${{ github.actor }}_" }
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK }}
60 changes: 60 additions & 0 deletions .github/workflows/promotion-gate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: "Promotion Gate"

# Runs on PRs targeting preprod or prod.
# Validates the source branch matches the allowed promotion path.
# Add "Promotion Gate" as a required status check in branch protection
# settings for preprod and prod — PRs cannot merge without this passing.
#
# Allowed paths:
# preprod ← master, hotfix/*
# prod ← preprod, hotfix/*
#
# Who can approve merges is controlled by GitHub environment reviewers
# on the "promote-preprod" and "promote-prod" environments, not here.

on:
pull_request:
branches: [preprod, prod]
merge_group:
branches: [preprod, prod]

jobs:
validate-promotion-path:
name: "Promotion Gate"
runs-on: ubuntu-latest
steps:
- name: Check source branch
run: |
TARGET="${{ github.base_ref }}"
SOURCE="${{ github.head_ref }}"

echo "PR: $SOURCE → $TARGET"

# hotfix/* branches are allowed into both preprod and prod
if [[ "$SOURCE" == hotfix/* ]]; then
echo "✅ $TARGET ← $SOURCE: hotfix branch allowed"
exit 0
fi

if [[ "$TARGET" == "preprod" ]]; then
if [[ "$SOURCE" == "master" ]]; then
echo "✅ preprod ← master: allowed"
else
echo "❌ BLOCKED: preprod only accepts PRs from master or hotfix/*"
echo ""
echo " Got: $SOURCE → preprod"
echo " Expected: master → preprod (or hotfix/* → preprod)"
exit 1
fi

elif [[ "$TARGET" == "prod" ]]; then
if [[ "$SOURCE" == "preprod" ]]; then
echo "✅ prod ← preprod: allowed"
else
echo "❌ BLOCKED: prod only accepts PRs from preprod or hotfix/*"
echo ""
echo " Got: $SOURCE → prod"
echo " Expected: preprod → prod (or hotfix/* → prod)"
exit 1
fi
fi
69 changes: 69 additions & 0 deletions build/ci/generate-promotion-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/bash
# generate-promotion-pr.sh — Creates a promotion PR with auto-generated changelog.
# Usage: ./generate-promotion-pr.sh <source-branch> <target-branch>
#
# Requires: GH_TOKEN environment variable for gh CLI authentication.
# Outputs (to GITHUB_OUTPUT if set): pr_url, total, feat_count, fix_count

set -euo pipefail

SOURCE="${1:?Usage: $0 <source-branch> <target-branch>}"
TARGET="${2:?Usage: $0 <source-branch> <target-branch>}"
TIMESTAMP=$(date -u +'%Y-%m-%d %H:%M UTC')

# Generate changelog from commits between branches
COMMITS=$(git log "${TARGET}..${SOURCE}" --pretty=format:"%s (%h) by %an" --no-merges 2>/dev/null || echo "")

if [[ -z "$COMMITS" || "$COMMITS" == "" ]]; then
echo "No new commits between ${SOURCE} and ${TARGET}. Skipping PR creation."
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "pr_url=no-changes" >> "$GITHUB_OUTPUT"
echo "total=0" >> "$GITHUB_OUTPUT"
echo "feat_count=0" >> "$GITHUB_OUTPUT"
echo "fix_count=0" >> "$GITHUB_OUTPUT"
fi
exit 0
fi

FEAT_COUNT=$(echo "$COMMITS" | grep -c "^feat" || true)
FIX_COUNT=$(echo "$COMMITS" | grep -c "^fix" || true)
TOTAL=$(echo "$COMMITS" | grep -c . || true)

# Build PR body
{
echo "## Promote to \`${TARGET}\`"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Source** | \`${SOURCE}\` |"
echo "| **Target** | \`${TARGET}\` |"
echo "| **Date** | ${TIMESTAMP} |"
echo "| **Total Commits** | ${TOTAL} |"
echo "| **Features** | ${FEAT_COUNT} |"
echo "| **Bug Fixes** | ${FIX_COUNT} |"
echo ""
echo "### Changes"
echo ""
echo "$COMMITS" | sed 's/^/- /'
echo ""
echo "---"
echo "_Auto-generated by Deploy Static workflow_"
} > /tmp/pr_body.md

TITLE="Promote to ${TARGET} — ${TIMESTAMP}"

PR_URL=$(gh pr create \
--base "${TARGET}" \
--head "${SOURCE}" \
--title "${TITLE}" \
--body-file /tmp/pr_body.md 2>&1) || PR_URL="already exists"

echo "PR: ${PR_URL}"

# Export outputs for GitHub Actions
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
echo "total=${TOTAL}" >> "$GITHUB_OUTPUT"
echo "feat_count=${FEAT_COUNT}" >> "$GITHUB_OUTPUT"
echo "fix_count=${FIX_COUNT}" >> "$GITHUB_OUTPUT"
fi
Loading