1111 types :
1212 - completed
1313 branches : [main]
14- workflow_dispatch :
14+
15+ concurrency :
16+ group : mcp-production-deploy
17+ cancel-in-progress : false
1518
1619jobs :
1720 deploy :
1821 name : Deploy to Cloudflare
1922 runs-on : ubuntu-latest
20- # Keep the production Worker unchanged while the CLI/docs import lands.
21- # Restore deployments only through a separately reviewed workflow change.
22- if : ${{ false }}
23+ environment : production
24+ if : >-
25+ ${{ github.event.workflow_run.conclusion == 'success' &&
26+ github.event.workflow_run.event == 'push' &&
27+ github.event.workflow_run.head_repository.id == github.event.repository.id &&
28+ github.event.workflow_run.head_branch == 'main' }}
2329
2430 steps :
25- - uses : actions/checkout@v4
31+ - uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
32+ with :
33+ ref : ${{ github.event.workflow_run.head_sha }}
34+ persist-credentials : false
35+
36+ - name : Require tested revision on main
37+ env :
38+ GH_TOKEN : ${{ github.token }}
39+ EXPECTED_SHA : ${{ github.event.workflow_run.head_sha }}
40+ run : |
41+ current_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
42+ if [[ "$current_sha" != "$EXPECTED_SHA" ]]; then
43+ echo 'The tested revision is no longer at main; refusing deployment.' >&2
44+ exit 1
45+ fi
2646
2747 - name : Setup Node.js
28- uses : actions/setup-node@v4
48+ uses : actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
2949 with :
3050 node-version : " 22"
3151
4060 run : |
4161 echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"
4262
43- - uses : actions/cache@v4
63+ - uses : actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
4464 name : Setup pnpm cache
4565 with :
4666 path : ${{ env.STORE_PATH }}
4969 ${{ runner.os }}-pnpm-store-
5070
5171 - name : Install dependencies
52- run : pnpm install
72+ run : pnpm install --frozen-lockfile
5373
5474 # === BUILD AND DEPLOY CANARY WORKER ===
5575 - name : Build
@@ -61,13 +81,11 @@ jobs:
6181
6282 - name : Deploy to Canary Worker
6383 id : deploy_canary
64- uses : cloudflare/wrangler-action@v3
65- with :
66- apiToken : ${{ secrets.CLOUDFLARE_API_TOKEN }}
67- accountId : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
68- workingDirectory : packages/mcp-cloudflare
69- command : deploy --config wrangler.canary.jsonc
70- packageManager : pnpm
84+ working-directory : packages/mcp-cloudflare
85+ env :
86+ CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
87+ CLOUDFLARE_ACCOUNT_ID : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
88+ run : pnpm exec wrangler deploy --config wrangler.canary.jsonc
7189
7290 - name : Wait for Canary to Propagate
7391 if : success()
@@ -95,27 +113,55 @@ jobs:
95113 fail_on_failure : false
96114
97115 # === DEPLOY PRODUCTION WORKER (only if canary tests pass) ===
116+ - name : Require tested revision on main before production
117+ if : steps.canary_smoke_tests.outcome == 'success'
118+ env :
119+ GH_TOKEN : ${{ github.token }}
120+ EXPECTED_SHA : ${{ github.event.workflow_run.head_sha }}
121+ run : |
122+ current_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
123+ if [[ "$current_sha" != "$EXPECTED_SHA" ]]; then
124+ echo 'Main advanced during canary testing; refusing production deployment.' >&2
125+ exit 1
126+ fi
127+
128+ - name : Capture active production version
129+ if : steps.canary_smoke_tests.outcome == 'success'
130+ env :
131+ CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
132+ CLOUDFLARE_ACCOUNT_ID : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
133+ TESTED_SHA : ${{ github.event.workflow_run.head_sha }}
134+ run : node scripts/cloudflare-deployment.mjs capture
135+
98136 - name : Deploy to Production Worker
99137 id : deploy_production
100138 if : steps.canary_smoke_tests.outcome == 'success'
101- uses : cloudflare/wrangler-action@v3
102- with :
103- apiToken : ${{ secrets.CLOUDFLARE_API_TOKEN }}
104- accountId : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
105- workingDirectory : packages/mcp-cloudflare
106- command : deploy
107- packageManager : pnpm
139+ working-directory : packages/mcp-cloudflare
140+ env :
141+ CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
142+ CLOUDFLARE_ACCOUNT_ID : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
143+ TESTED_SHA : ${{ github.event.workflow_run.head_sha }}
144+ run : pnpm exec wrangler deploy --message "toolkit-mcp:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT:$TESTED_SHA"
108145
109- - name : Wait for Production to Propagate
146+ - name : Verify production deployment ownership
147+ id : verify_production
110148 if : steps.deploy_production.outcome == 'success'
149+ env :
150+ CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
151+ CLOUDFLARE_ACCOUNT_ID : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
152+ TESTED_SHA : ${{ github.event.workflow_run.head_sha }}
153+ run : node scripts/cloudflare-deployment.mjs verify
154+
155+ - name : Wait for Production to Propagate
156+ if : steps.verify_production.outcome == 'success'
111157 run : |
112158 echo "Waiting 30 seconds for production deployment to propagate..."
113159 sleep 30
114160
115161 # === SMOKE TEST PRODUCTION ===
116162 - name : Run Smoke Tests on Production
117163 id : production_smoke_tests
118- if : steps.deploy_production .outcome == 'success'
164+ if : steps.verify_production .outcome == 'success'
119165 env :
120166 PREVIEW_URL : https://mcp.sentry.dev
121167 run : |
@@ -131,20 +177,16 @@ jobs:
131177 check_name : " Production Smoke Test Results"
132178 fail_on_failure : false
133179
134- # === ROLLBACK IF PRODUCTION SMOKE TESTS FAIL ===
135- - name : Rollback Production on Smoke Test Failure
136- if : steps.production_smoke_tests.outcome == 'failure'
137- uses : cloudflare/wrangler-action@v3
138- with :
139- apiToken : ${{ secrets.CLOUDFLARE_API_TOKEN }}
140- accountId : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
141- workingDirectory : packages/mcp-cloudflare
142- command : rollback
143- packageManager : pnpm
144- continue-on-error : true
180+ - name : Recover captured previous version after smoke failure
181+ if : failure() && steps.production_smoke_tests.outcome == 'failure' && steps.verify_production.outcome == 'success'
182+ env :
183+ CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
184+ CLOUDFLARE_ACCOUNT_ID : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
185+ TESTED_SHA : ${{ github.event.workflow_run.head_sha }}
186+ run : node scripts/cloudflare-deployment.mjs recover
145187
146188 - name : Fail Job if Production Smoke Tests Failed
147- if : steps.production_smoke_tests.outcome == 'failure'
189+ if : failure() && steps.production_smoke_tests.outcome == 'failure'
148190 run : |
149- echo " Production smoke tests failed - job failed after rollback"
191+ echo ' Production smoke tests failed. Inspect the deployment before changing traffic.' >&2
150192 exit 1
0 commit comments