seo: point footer Status link at canonical URL (fix 310 temp redirect… #198
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 | |
| - 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 | |
| - 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 | |
| - name: Notify on failure | |
| if: failure() | |
| env: | |
| PAPERCLIP_API_URL: ${{ secrets.PAPERCLIP_API_URL }} | |
| PAPERCLIP_API_KEY: ${{ secrets.PAPERCLIP_API_KEY }} | |
| run: | | |
| echo "::error::Deployment to prod failed. Check the logs above for details." | |
| echo "To rollback, run this workflow manually with 'rollback' set to true." | |
| # Extract Paperclip issue identifier from branch name or commit message | |
| COMMIT_MSG=$(git log -1 --format=%s ${{ github.sha }}) | |
| ISSUE_ID=$(echo "$COMMIT_MSG" | grep -oP 'SHA-\d+' | head -1) | |
| # If no issue in commit message, check the merge branch name | |
| if [ -z "$ISSUE_ID" ]; then | |
| ISSUE_ID=$(echo "$COMMIT_MSG" | grep -oP 'paperclip/(SHA-\d+)' | head -1 | sed 's|paperclip/||') | |
| fi | |
| if [ -n "$ISSUE_ID" ] && [ -n "$PAPERCLIP_API_URL" ] && [ -n "$PAPERCLIP_API_KEY" ]; then | |
| # Comment on the existing issue | |
| COMMENT="## CI Deploy Failed\n\n**Workflow**: ${{ github.workflow }}\n**Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n**Commit**: \`${{ github.sha }}\`\n\nThe deploy to prod failed. Check the CI logs and fix the issue. Do NOT mark this issue as done until a successful deploy is verified." | |
| curl -sf -X PATCH "$PAPERCLIP_API_URL/issues/$ISSUE_ID" \ | |
| -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -n --arg status "blocked" --arg comment "$COMMENT" '{status: $status, comment: $comment}')" || true | |
| echo "Commented on Paperclip issue $ISSUE_ID" | |
| elif [ -n "$PAPERCLIP_API_URL" ] && [ -n "$PAPERCLIP_API_KEY" ]; then | |
| # No issue found — create one assigned to Coordinator for triage | |
| curl -sf -X POST "$PAPERCLIP_API_URL/companies/953ff6b0-4d1b-4007-9859-8cb0a53629f6/issues" \ | |
| -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -n \ | |
| --arg title "CI Failed: docs.sharpapi.io (${{ github.ref_name }})" \ | |
| --arg desc "GitHub Actions workflow **${{ github.workflow }}** failed.\n\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\nCommit: \`$(git log -1 --format='%h %s' ${{ github.sha }})\`\n\nInvestigate and fix." \ | |
| '{title: $title, description: $desc, status: "todo", priority: "critical", assigneeAgentId: "3a976364-1186-4c30-a136-bba410c2209c"}')" || true | |
| echo "Created Paperclip issue for deploy failure" | |
| fi | |
| 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| 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 |