Skip to content

Commit 56ae6b9

Browse files
BYKGPT-6 Sol (OpenAI)
andauthored
Restore guarded MCP production deployment
Restore the canary-to-production workflow for tested main revisions. Record the prior Worker version and recover only while this run still owns production. Co-Authored-By: GPT-6 Sol (OpenAI) <agent@openai.com>
1 parent 003d66c commit 56ae6b9

9 files changed

Lines changed: 539 additions & 99 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,41 @@ on:
1111
types:
1212
- completed
1313
branches: [main]
14-
workflow_dispatch:
14+
15+
concurrency:
16+
group: mcp-production-deploy
17+
cancel-in-progress: false
1518

1619
jobs:
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

@@ -40,7 +60,7 @@ jobs:
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 }}
@@ -49,7 +69,7 @@ jobs:
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

‎docs/operations/github-actions.md‎

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22

33
CI/CD workflows for the Sentry MCP project.
44

5-
**Toolkit import landing:** The `Deploy to Cloudflare` job is disabled while
6-
the CLI and docs import lands. A passing `Test` run on `main` cannot change the
7-
production Worker. Restore deployments only through a separately reviewed
8-
workflow change. `pnpm build` builds the MCP workspace; `pnpm build:cli` builds
9-
the imported CLI and docs when `SENTRY_CLIENT_ID` is available.
5+
`pnpm build` builds the MCP workspace; `pnpm build:cli` builds the imported CLI
6+
and docs when `SENTRY_CLIENT_ID` is available.
107

118
## Workflows
129

@@ -22,8 +19,12 @@ Package-specific exceptions live in `package.json#sentryCi`; the standalone
2219
smoke-test suite remains in its own workflow.
2320

2421
### deploy.yml
25-
Currently disabled. Its old canary, production, and rollback steps must not
26-
run until a separately reviewed workflow replaces them.
22+
Runs after a successful `Test` push run on `main`. Checks out the tested commit,
23+
requires that it is still the tip of `main`, then deploys and tests canary.
24+
Records the active production version before changing traffic, deploys the
25+
tested commit, and verifies the run-owned candidate before production smoke
26+
tests. If those fail, restores the captured version only while this run's
27+
candidate remains active. External changes stop recovery.
2728

2829
### eval.yml
2930
Runs evaluation tests against the MCP server.
@@ -48,10 +49,13 @@ label creation.
4849

4950
## Required Secrets
5051

51-
Repository secrets (no environment needed):
52+
The `production` environment is restricted to `main` and holds:
5253

5354
- **`CLOUDFLARE_API_TOKEN`** - Cloudflare API token with Workers deployment permissions
54-
- **`CLOUDFLARE_ACCOUNT_ID`** - Your Cloudflare account ID
55+
56+
Other configuration:
57+
58+
- **`CLOUDFLARE_ACCOUNT_ID`** - ID of the account owning the Workers
5559
- **`SENTRY_AUTH_TOKEN`** - For Sentry release tracking
5660
- **`SENTRY_CLIENT_SECRET`** - Sentry OAuth client secret
5761
- **`COOKIE_SECRET`** - Session cookie encryption secret
@@ -75,13 +79,15 @@ Canary and production use separate resources for complete isolation:
7579

7680
### Deployment Flow
7781

78-
No production deployment runs while the import lands. The disabled workflow's
79-
old rollback step must not be used to recover production.
82+
The workflow never deploys an untested revision or a stale `main` commit.
83+
Failure to identify the prior active version, verify deployment ownership, or
84+
confirm the restored version fails the job rather than guessing a recovery.
8085

8186
## Manual Deployment
8287

83-
The deployment job is also disabled for manual workflow dispatch. Do not use
84-
the old rollback path to deploy or recover the production Worker.
88+
Manual production dispatch is unavailable. Use a reviewed change and its
89+
passing `Test` run to deploy. Never run bare `wrangler rollback` against
90+
production; that command chooses from mutable history.
8591

8692
## Troubleshooting
8793

‎docs/releases/cloudflare.md‎

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -136,32 +136,32 @@ pnpm dev
136136

137137
### Production Deployment
138138

139-
#### Automated via GitHub Actions (Recommended)
140-
141-
Production deployments happen automatically when changes are pushed to the main branch:
142-
143-
1. Push to main or merge a PR
144-
2. GitHub Actions runs tests
145-
3. If tests pass, deploys to Cloudflare
146-
147-
Required secrets in GitHub repository settings:
148-
- `CLOUDFLARE_API_TOKEN` - API token with Workers deployment permissions
149-
- `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID
139+
#### Automated via GitHub Actions
140+
141+
Production deployments run only from the trusted `Deploy to Cloudflare` workflow
142+
after the `Test` workflow succeeds for the current `main` commit:
143+
144+
1. Merge a PR into `main`; GitHub Actions tests that exact commit.
145+
2. The workflow builds and deploys `sentry-mcp-canary`, then runs canary smoke tests.
146+
3. After canary succeeds, it records the currently active production version
147+
and deploys the tested commit to `sentry-mcp`.
148+
4. It verifies that this run owns the new deployment and runs production smoke
149+
tests. On failure it restores the captured previous version **only if**
150+
production still serves this run's exact candidate. External changes or
151+
ambiguous traffic allocation stop recovery rather than overwrite them.
152+
153+
The `production` GitHub environment allows only `main`. Store
154+
`CLOUDFLARE_API_TOKEN` there with Workers deployment permissions. Configure
155+
`CLOUDFLARE_ACCOUNT_ID` for the account that owns both Workers. Keep credentials
156+
out of command arguments and logs. After a verified deployment, remove any
157+
repository-level copy of `CLOUDFLARE_API_TOKEN`.
150158

151159
See `github-actions.md` for detailed setup instructions.
152160

153-
#### Manual Deployment
154-
155-
```bash
156-
# Build client assets
157-
pnpm build
158-
159-
# Deploy to Cloudflare
160-
pnpm deploy
161-
162-
# Or deploy specific environment
163-
pnpm deploy --env production
164-
```
161+
Production traffic changes must use the protected workflow. Do not use bare
162+
`wrangler rollback`: it selects from mutable deployment history and can undo
163+
someone else's deployment. If recovery declines because production changed,
164+
inspect the active version and use a new reviewed workflow run to fix forward.
165165

166166
#### Version Uploads (Gradual Rollouts)
167167

‎docs/testing/remote.md‎

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,11 @@ Server runs at: `http://localhost:5173`
104104
- Serves the web UI at root
105105
- MCP endpoint at `/mcp`
106106

107-
### Option 2: Deploy to Cloudflare
107+
### Option 2: Test the Cloudflare Worker
108108

109-
**Deploy to your Cloudflare account:**
110-
```bash
111-
cd packages/mcp-cloudflare
112-
pnpm deploy
113-
```
114-
115-
**Deploy to production (requires permissions):**
116-
```bash
117-
# Automated via GitHub Actions on push to main
118-
# Manual deployment:
119-
pnpm deploy --env production
120-
```
109+
The protected GitHub workflow deploys the canary first and then production
110+
after successful tests on `main`. Use the canary URL to check hosted behavior
111+
before production traffic changes.
121112

122113
## Testing with the CLI Client
123114

@@ -662,14 +653,9 @@ pnpm inspector
662653

663654
### Production Deploy
664655

665-
```bash
666-
# Via GitHub Actions (automatic)
667-
git push origin main
668-
669-
# Manual (if needed)
670-
cd packages/mcp-cloudflare
671-
pnpm deploy --env production
672-
```
656+
Merge a reviewed pull request into `main`. The protected workflow requires
657+
passing tests for that exact revision and a successful canary smoke test before
658+
it changes production traffic.
673659

674660
### After Deploy
675661

@@ -773,9 +759,9 @@ pnpm -w run cli --mcp-host=https://staging.mcp.sentry.dev "who am I?"
773759
### Testing Self-Hosted
774760

775761
```bash
776-
# Deploy to self-hosted Cloudflare account
762+
# Deploy only to your own Worker using your own Wrangler configuration
777763
cd packages/mcp-cloudflare
778-
pnpm deploy
764+
pnpm exec wrangler deploy --config your-worker.jsonc
779765

780766
# Test with self-hosted URL
781767
pnpm -w run cli --mcp-host=https://your-worker.workers.dev "who am I?"

‎package.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@
2323
},
2424
"scripts": {
2525
"docs:check": "node scripts/check-doc-links.mjs",
26-
"test:ci-projects": "node --test scripts/ci-projects.test.mjs",
26+
"test:ci-projects": "node --test scripts/ci-projects.test.mjs scripts/cloudflare-deployment.test.mjs scripts/deploy-workflow.test.mjs",
2727
"dev": "pnpm --filter '@sentry/mcp-cloudflare...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --parallel --filter @sentry/mcp-cloudflare --filter @sentry/mcp-core --filter @sentry/mcp-server-mocks --if-present run dev",
2828
"dev:stdio": "pnpm --filter '@sentry/mcp-server...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --parallel --filter @sentry/mcp-server --filter @sentry/mcp-core --filter @sentry/mcp-server-mocks --if-present run dev",
2929
"build": "dotenv -e .env -e .env.local -- pnpm -r --filter '!sentry' --filter '!sentry-cli-docs' --if-present run build",
3030
"build:cli": "dotenv -e .env -e .env.local -- pnpm --filter sentry run build && pnpm --filter sentry-cli-docs run build",
3131
"check:generated": "pnpm --filter @sentry/mcp-core generate-definitions && git diff --exit-code -- packages/mcp-core/src/toolDefinitions.json packages/mcp-core/src/skillDefinitions.json plugins/sentry-mcp/agents/sentry-mcp.md plugins/sentry-mcp-experimental/agents/sentry-mcp.md",
32-
"deploy": "pnpm --filter '@sentry/mcp-cloudflare...' --if-present run build && pnpm --filter @sentry/mcp-cloudflare run deploy",
3332
"deploy:docs": "pnpm --filter sentry run generate:schema && pnpm --filter sentry run generate:docs && pnpm --filter sentry-cli-docs run build && pnpm --filter sentry-cli-docs run deploy",
3433
"eval": "pnpm --filter '@sentry/mcp-server-evals...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --filter @sentry/mcp-server-evals run eval",
3534
"eval:ci": "pnpm --filter '@sentry/mcp-server-evals...' --if-present run build && CI=true dotenv -e .env -e .env.local -- pnpm --stream -r run eval:ci",

‎packages/mcp-cloudflare/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"scripts": {
2222
"build": "tsc -b && vite build",
2323
"dev": "vite",
24-
"deploy": "pnpm exec wrangler deploy",
2524
"cf:versions:upload": "npx wrangler versions upload",
2625
"preview": "vite preview",
2726
"cf-typegen": "wrangler types",

0 commit comments

Comments
 (0)