ci: SHA-pin GitHub Actions + pin the Vercel CLI (#223) #158
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
| name: Deploy Production | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: Dry run - validate without deploying | |
| required: false | |
| default: false | |
| type: boolean | |
| rollback: | |
| description: Rollback to previous deployment | |
| required: false | |
| default: false | |
| type: boolean | |
| rollback_deployment_url: | |
| description: Specific deployment URL to promote (optional) | |
| required: false | |
| type: string | |
| concurrency: | |
| group: deploy-prod | |
| cancel-in-progress: false | |
| env: | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | |
| PRODUCTION_URL: https://docs.sharpapi.io | |
| jobs: | |
| validate: | |
| name: Validate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: inputs.rollback != true | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - name: Typecheck | |
| # `next build` performs its own tsc-equivalent pass, but only | |
| # for files reachable from routes. This runs tsc against the | |
| # whole repo so MDX page scripts, shared utils, and unused | |
| # helpers don't drift types. | |
| run: pnpm typecheck | |
| - name: Build | |
| run: pnpm build | |
| - name: Check links | |
| # Spins up a local static server on out/ and crawls every | |
| # internal link with linkinator. Catches dead anchors and | |
| # rotten internal paths before they ship to docs.sharpapi.io. | |
| # External URLs are skipped — their drift is a third-party | |
| # concern and would fail CI on transient upstream outages. | |
| run: pnpm check-links | |
| backup-deployment: | |
| name: Backup Current Deployment | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| previous_deployment: ${{ steps.backup.outputs.deployment_url }} | |
| steps: | |
| - run: npm install --global vercel@53 | |
| - name: Get current production deployment | |
| id: backup | |
| run: | | |
| CURRENT=$(vercel ls --prod --token=$VERCEL_TOKEN 2>/dev/null | grep -E "https://" | head -1 | awk '{print $2}' || echo "") | |
| echo "deployment_url=$CURRENT" >> $GITHUB_OUTPUT | |
| echo "Previous deployment: $CURRENT" | |
| echo "## Backup" >> $GITHUB_STEP_SUMMARY | |
| echo "**Previous deployment**: $CURRENT" >> $GITHUB_STEP_SUMMARY | |
| deploy-production: | |
| name: Deploy Production | |
| needs: [validate, backup-deployment] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| if: inputs.dry_run != true && inputs.rollback != true | |
| outputs: | |
| deployment_url: ${{ steps.deploy.outputs.deployment_url }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - run: npm install --global vercel@53 | |
| - run: vercel pull --yes --environment=production --token=$VERCEL_TOKEN | |
| - run: vercel build --prod --token=$VERCEL_TOKEN | |
| - name: Deploy to Vercel Production | |
| id: deploy | |
| run: | | |
| DEPLOYMENT_URL=$(vercel deploy --prebuilt --prod --token=$VERCEL_TOKEN) | |
| echo "deployment_url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT | |
| echo "Deployed to production: $DEPLOYMENT_URL" | |
| echo "## Production Deployment" >> $GITHUB_STEP_SUMMARY | |
| echo "**URL**: ${{ env.PRODUCTION_URL }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Deployment**: $DEPLOYMENT_URL" >> $GITHUB_STEP_SUMMARY | |
| rollback: | |
| name: Rollback | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: inputs.rollback == true | |
| steps: | |
| - run: npm install --global vercel@53 | |
| - name: Determine rollback target | |
| id: target | |
| run: | | |
| if [ -n "${{ inputs.rollback_deployment_url }}" ]; then | |
| echo "target_url=${{ inputs.rollback_deployment_url }}" >> $GITHUB_OUTPUT | |
| echo "Rolling back to specified deployment: ${{ inputs.rollback_deployment_url }}" | |
| else | |
| PREVIOUS=$(vercel ls --prod --token=$VERCEL_TOKEN 2>/dev/null | grep -E "https://" | sed -n '2p' | awk '{print $2}') | |
| echo "target_url=$PREVIOUS" >> $GITHUB_OUTPUT | |
| echo "Rolling back to previous deployment: $PREVIOUS" | |
| fi | |
| - name: Promote deployment | |
| run: | | |
| vercel promote "${{ steps.target.outputs.target_url }}" --token=$VERCEL_TOKEN --yes | |
| echo "## Rollback" >> $GITHUB_STEP_SUMMARY | |
| echo "**Promoted**: ${{ steps.target.outputs.target_url }}" >> $GITHUB_STEP_SUMMARY | |
| changelog: | |
| name: Changelog & Release | |
| needs: deploy-production | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: inputs.dry_run != true && inputs.rollback != true | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine version tag | |
| id: version | |
| run: | | |
| TAG="v$(date -u +%Y.%m.%d)" | |
| # Append run number suffix if tag already exists | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| TAG="${TAG}.${{ github.run_number }}" | |
| fi | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| - name: Generate changelog | |
| uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4 | |
| id: cliff | |
| with: | |
| config: .github/cliff.toml | |
| args: --latest --strip header | |
| env: | |
| OUTPUT: CHANGES.md | |
| GITHUB_REPO: ${{ github.repository }} | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: ${{ steps.version.outputs.tag }} | |
| body: ${{ steps.cliff.outputs.content }} | |
| generate_release_notes: false | |
| health-check: | |
| name: Health Check | |
| needs: deploy-production | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: inputs.dry_run != true && inputs.rollback != true | |
| steps: | |
| - name: Wait and check health | |
| run: | | |
| sleep 45 | |
| URL="${{ env.PRODUCTION_URL }}" | |
| for i in 1 2 3 4 5; do | |
| HTTP_STATUS=$(curl -sL -o /dev/null -w "%{http_code}" "$URL" --max-time 30) | |
| if [ "$HTTP_STATUS" = "200" ]; then | |
| echo "$URL - OK (HTTP $HTTP_STATUS)" | |
| echo "## Health Check" >> $GITHUB_STEP_SUMMARY | |
| echo "**Status**: Passed" >> $GITHUB_STEP_SUMMARY | |
| exit 0 | |
| fi | |
| echo "Attempt $i: $URL returned HTTP $HTTP_STATUS, retrying..." | |
| sleep 15 | |
| done | |
| echo "::error::Health check failed! Run: gh workflow run deploy-prod.yml -f rollback=true" | |
| exit 1 | |
| health-check-rollback: | |
| name: Health Check (Rollback) | |
| needs: rollback | |
| runs-on: ubuntu-latest | |
| if: inputs.rollback == true | |
| steps: | |
| - run: | | |
| sleep 30 | |
| HTTP_STATUS=$(curl -sL -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}" --max-time 30) | |
| if [ "$HTTP_STATUS" = "200" ]; then | |
| echo "Rollback successful" | |
| echo "## Rollback Health Check" >> $GITHUB_STEP_SUMMARY | |
| echo "**Status**: Passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "Rollback health check failed with HTTP $HTTP_STATUS" | |
| exit 1 | |
| fi | |
| dry-run-summary: | |
| name: Dry Run Summary | |
| needs: backup-deployment | |
| runs-on: ubuntu-latest | |
| if: inputs.dry_run == true && inputs.rollback != true | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 10 | |
| - run: | | |
| echo "=== DRY RUN MODE ===" | |
| echo "Would deploy main branch to Vercel Production" | |
| echo "Previous deployment: ${{ needs.backup-deployment.outputs.previous_deployment }}" | |
| echo "## Dry Run Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "Validation passed. Would deploy to Vercel Production." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Previous deployment**: ${{ needs.backup-deployment.outputs.previous_deployment }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Recent commits:" >> $GITHUB_STEP_SUMMARY | |
| git log --oneline -10 >> $GITHUB_STEP_SUMMARY |