Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 79 additions & 37 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,41 @@ on:
types:
- completed
branches: [main]
workflow_dispatch:

concurrency:
group: mcp-production-deploy
cancel-in-progress: false

jobs:
deploy:
name: Deploy to Cloudflare
runs-on: ubuntu-latest
# Keep the production Worker unchanged while the CLI/docs import lands.
# Restore deployments only through a separately reviewed workflow change.
if: ${{ false }}
environment: production
if: >-
${{ github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_repository.id == github.event.repository.id &&
github.event.workflow_run.head_branch == 'main' }}

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

- name: Require tested revision on main
env:
GH_TOKEN: ${{ github.token }}
EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
current_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
if [[ "$current_sha" != "$EXPECTED_SHA" ]]; then
echo 'The tested revision is no longer at main; refusing deployment.' >&2
exit 1
fi

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"

Expand All @@ -40,7 +60,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"

- uses: actions/cache@v4
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
Expand All @@ -49,7 +69,7 @@ jobs:
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile

# === BUILD AND DEPLOY CANARY WORKER ===
- name: Build
Expand All @@ -61,13 +81,11 @@ jobs:

- name: Deploy to Canary Worker
id: deploy_canary
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: packages/mcp-cloudflare
command: deploy --config wrangler.canary.jsonc
packageManager: pnpm
working-directory: packages/mcp-cloudflare
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm exec wrangler deploy --config wrangler.canary.jsonc

- name: Wait for Canary to Propagate
if: success()
Expand Down Expand Up @@ -95,27 +113,55 @@ jobs:
fail_on_failure: false

# === DEPLOY PRODUCTION WORKER (only if canary tests pass) ===
- name: Require tested revision on main before production
if: steps.canary_smoke_tests.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
current_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
if [[ "$current_sha" != "$EXPECTED_SHA" ]]; then
echo 'Main advanced during canary testing; refusing production deployment.' >&2
exit 1
fi

- name: Capture active production version
if: steps.canary_smoke_tests.outcome == 'success'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: node scripts/cloudflare-deployment.mjs capture

- name: Deploy to Production Worker
id: deploy_production
if: steps.canary_smoke_tests.outcome == 'success'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: packages/mcp-cloudflare
command: deploy
packageManager: pnpm
working-directory: packages/mcp-cloudflare
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: pnpm exec wrangler deploy --message "toolkit-mcp:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT:$TESTED_SHA"

- name: Wait for Production to Propagate
- name: Verify production deployment ownership
id: verify_production
if: steps.deploy_production.outcome == 'success'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: node scripts/cloudflare-deployment.mjs verify

- name: Wait for Production to Propagate
if: steps.verify_production.outcome == 'success'
run: |
echo "Waiting 30 seconds for production deployment to propagate..."
sleep 30

# === SMOKE TEST PRODUCTION ===
- name: Run Smoke Tests on Production
id: production_smoke_tests
if: steps.deploy_production.outcome == 'success'
if: steps.verify_production.outcome == 'success'
env:
PREVIEW_URL: https://mcp.sentry.dev
run: |
Expand All @@ -131,20 +177,16 @@ jobs:
check_name: "Production Smoke Test Results"
fail_on_failure: false

# === ROLLBACK IF PRODUCTION SMOKE TESTS FAIL ===
- name: Rollback Production on Smoke Test Failure
if: steps.production_smoke_tests.outcome == 'failure'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: packages/mcp-cloudflare
command: rollback
packageManager: pnpm
continue-on-error: true
- name: Recover captured previous version after smoke failure
if: failure() && steps.production_smoke_tests.outcome == 'failure' && steps.verify_production.outcome == 'success'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: node scripts/cloudflare-deployment.mjs recover

- name: Fail Job if Production Smoke Tests Failed
if: steps.production_smoke_tests.outcome == 'failure'
if: failure() && steps.production_smoke_tests.outcome == 'failure'
run: |
echo "Production smoke tests failed - job failed after rollback"
echo 'Production smoke tests failed. Inspect the deployment before changing traffic.' >&2
exit 1
32 changes: 19 additions & 13 deletions docs/operations/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

CI/CD workflows for the Sentry MCP project.

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

## Workflows

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

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

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

## Required Secrets

Repository secrets (no environment needed):
The `production` environment is restricted to `main` and holds:

- **`CLOUDFLARE_API_TOKEN`** - Cloudflare API token with Workers deployment permissions
- **`CLOUDFLARE_ACCOUNT_ID`** - Your Cloudflare account ID

Other configuration:

- **`CLOUDFLARE_ACCOUNT_ID`** - ID of the account owning the Workers
- **`SENTRY_AUTH_TOKEN`** - For Sentry release tracking
- **`SENTRY_CLIENT_SECRET`** - Sentry OAuth client secret
- **`COOKIE_SECRET`** - Session cookie encryption secret
Expand All @@ -75,13 +79,15 @@ Canary and production use separate resources for complete isolation:

### Deployment Flow

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

## Manual Deployment

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

## Troubleshooting

Expand Down
46 changes: 23 additions & 23 deletions docs/releases/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,32 +136,32 @@ pnpm dev

### Production Deployment

#### Automated via GitHub Actions (Recommended)

Production deployments happen automatically when changes are pushed to the main branch:

1. Push to main or merge a PR
2. GitHub Actions runs tests
3. If tests pass, deploys to Cloudflare

Required secrets in GitHub repository settings:
- `CLOUDFLARE_API_TOKEN` - API token with Workers deployment permissions
- `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID
#### Automated via GitHub Actions

Production deployments run only from the trusted `Deploy to Cloudflare` workflow
after the `Test` workflow succeeds for the current `main` commit:

1. Merge a PR into `main`; GitHub Actions tests that exact commit.
2. The workflow builds and deploys `sentry-mcp-canary`, then runs canary smoke tests.
3. After canary succeeds, it records the currently active production version
and deploys the tested commit to `sentry-mcp`.
4. It verifies that this run owns the new deployment and runs production smoke
tests. On failure it restores the captured previous version **only if**
production still serves this run's exact candidate. External changes or
ambiguous traffic allocation stop recovery rather than overwrite them.

The `production` GitHub environment allows only `main`. Store
`CLOUDFLARE_API_TOKEN` there with Workers deployment permissions. Configure
`CLOUDFLARE_ACCOUNT_ID` for the account that owns both Workers. Keep credentials
out of command arguments and logs. After a verified deployment, remove any
repository-level copy of `CLOUDFLARE_API_TOKEN`.

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

#### Manual Deployment

```bash
# Build client assets
pnpm build

# Deploy to Cloudflare
pnpm deploy

# Or deploy specific environment
pnpm deploy --env production
```
Production traffic changes must use the protected workflow. Do not use bare
`wrangler rollback`: it selects from mutable deployment history and can undo
someone else's deployment. If recovery declines because production changed,
inspect the active version and use a new reviewed workflow run to fix forward.

#### Version Uploads (Gradual Rollouts)

Expand Down
32 changes: 9 additions & 23 deletions docs/testing/remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,11 @@ Server runs at: `http://localhost:5173`
- Serves the web UI at root
- MCP endpoint at `/mcp`

### Option 2: Deploy to Cloudflare
### Option 2: Test the Cloudflare Worker

**Deploy to your Cloudflare account:**
```bash
cd packages/mcp-cloudflare
pnpm deploy
```

**Deploy to production (requires permissions):**
```bash
# Automated via GitHub Actions on push to main
# Manual deployment:
pnpm deploy --env production
```
The protected GitHub workflow deploys the canary first and then production
after successful tests on `main`. Use the canary URL to check hosted behavior
before production traffic changes.

## Testing with the CLI Client

Expand Down Expand Up @@ -662,14 +653,9 @@ pnpm inspector

### Production Deploy

```bash
# Via GitHub Actions (automatic)
git push origin main

# Manual (if needed)
cd packages/mcp-cloudflare
pnpm deploy --env production
```
Merge a reviewed pull request into `main`. The protected workflow requires
passing tests for that exact revision and a successful canary smoke test before
it changes production traffic.

### After Deploy

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

```bash
# Deploy to self-hosted Cloudflare account
# Deploy only to your own Worker using your own Wrangler configuration
cd packages/mcp-cloudflare
pnpm deploy
pnpm exec wrangler deploy --config your-worker.jsonc

# Test with self-hosted URL
pnpm -w run cli --mcp-host=https://your-worker.workers.dev "who am I?"
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@
},
"scripts": {
"docs:check": "node scripts/check-doc-links.mjs",
"test:ci-projects": "node --test scripts/ci-projects.test.mjs",
"test:ci-projects": "node --test scripts/ci-projects.test.mjs scripts/cloudflare-deployment.test.mjs scripts/deploy-workflow.test.mjs",
"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",
"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",
"build": "dotenv -e .env -e .env.local -- pnpm -r --filter '!sentry' --filter '!sentry-cli-docs' --if-present run build",
"build:cli": "dotenv -e .env -e .env.local -- pnpm --filter sentry run build && pnpm --filter sentry-cli-docs run build",
"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",
"deploy": "pnpm --filter '@sentry/mcp-cloudflare...' --if-present run build && pnpm --filter @sentry/mcp-cloudflare run deploy",
"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",
"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",
"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",
Expand Down
1 change: 0 additions & 1 deletion packages/mcp-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"scripts": {
"build": "tsc -b && vite build",
"dev": "vite",
"deploy": "pnpm exec wrangler deploy",
"cf:versions:upload": "npx wrangler versions upload",
"preview": "vite preview",
"cf-typegen": "wrangler types",
Expand Down
Loading
Loading