Deploy #28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Deploy: preview → smoke → promote to production | |
| # | |
| # Triggers on successful CI workflow completion (push to main only). | |
| # Deploys a preview first, runs smoke tests, then promotes to production. | |
| # If preview smoke fails, production is untouched — no rollback needed. | |
| # | |
| # Required secrets: | |
| # VERCEL_TOKEN — from https://vercel.com/account/tokens | |
| # VERCEL_ORG_ID — from .vercel/project.json → orgId | |
| # VERCEL_PROJECT_ID — from .vercel/project.json → projectId | |
| # VERCEL_AUTOMATION_BYPASS_SECRET — from Vercel project → Settings → Deployment Protection → Protection Bypass for Automation | |
| name: Deploy | |
| on: | |
| workflow_run: | |
| workflows: ["CI"] | |
| branches: [main] | |
| types: [completed] | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| permissions: | |
| contents: read | |
| issues: write | |
| environment: | |
| name: production | |
| url: https://paulprae.com | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_sha }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build | |
| run: npm run build | |
| - name: Install Vercel CLI | |
| run: npm i -g vercel@latest | |
| - name: Deploy preview | |
| id: preview | |
| run: | | |
| # Deploy to Vercel (builds remotely — no --prebuilt needed) | |
| # --scope required: VERCEL_ORG_ID env var is not picked up by all CLI subcommands | |
| OUTPUT=$(vercel deploy --yes --scope="${VERCEL_ORG_ID}" --token="${VERCEL_TOKEN}" 2>&1) | |
| echo "$OUTPUT" | |
| DEPLOY_URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.vercel\.app' | head -1) | |
| if [ -z "$DEPLOY_URL" ]; then | |
| echo "::error::Failed to extract deployment URL from Vercel output" | |
| exit 1 | |
| fi | |
| echo "url=$DEPLOY_URL" >> "$GITHUB_OUTPUT" | |
| echo "Preview deployed: $DEPLOY_URL" | |
| env: | |
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| - name: Smoke test preview | |
| run: npm run smoke | |
| env: | |
| SMOKE_TEST_URL: ${{ steps.preview.outputs.url }} | |
| SMOKE_WAIT_SECONDS: "10" | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| - name: Promote to production | |
| run: vercel promote "${{ steps.preview.outputs.url }}" --yes --scope="${VERCEL_ORG_ID}" --token="${VERCEL_TOKEN}" | |
| env: | |
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| - name: Smoke test production | |
| run: npm run smoke | |
| env: | |
| SMOKE_TEST_URL: https://paulprae.com | |
| # 30s wait: CDN needs time to invalidate after promote. 15s caused | |
| # false-alarm failures (issue #24) where content was correct but cache stale. | |
| SMOKE_WAIT_SECONDS: "30" | |
| - name: Create issue on failure | |
| if: failure() | |
| run: | | |
| gh issue create \ | |
| --title "Deploy failed: $(date +%Y-%m-%d)" \ | |
| --body "$(cat <<EOF | |
| Deployment workflow failed. | |
| **Commit:** ${{ github.event.workflow_run.head_sha }} | |
| **Preview URL:** ${{ steps.preview.outputs.url || 'N/A' }} | |
| **Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| Check the workflow logs for details. | |
| EOF | |
| )" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |