Skip to content
Open
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
78 changes: 69 additions & 9 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,38 @@ jobs:
- name: Install dependencies
run: pnpm install

# === BUILD AND DEPLOY CANARY WORKER ===
- name: Build
# === DEPLOY DOCS (CANARY) FIRST ===
- name: Deploy Docs Canary Worker
id: deploy_docs_canary
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: packages/docs
command: deploy --config wrangler.canary.jsonc
packageManager: pnpm

- name: Smoke Test Docs Canary
id: smoke_docs_canary
if: success()
run: |
set -euo pipefail
URL="https://sentry-mcp-docs-canary.getsentry.workers.dev/docs"
echo "Testing $URL"
STATUS=$(curl -s -o /tmp/docs_canary.html -w "%{http_code}" "$URL")
if [ "$STATUS" -ne 200 ]; then
echo "Unexpected status: $STATUS"; cat /tmp/docs_canary.html; exit 1; fi
if ! grep -q "<title>Documentation" /tmp/docs_canary.html; then
echo "Title check failed"; cat /tmp/docs_canary.html; exit 1; fi

# === BUILD AND DEPLOY APP (CANARY) ===
- name: Build App (Canary)
working-directory: packages/mcp-cloudflare
run: pnpm build
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

- name: Deploy to Canary Worker
- name: Deploy App Canary Worker
id: deploy_canary
uses: cloudflare/wrangler-action@v3
with:
Expand Down Expand Up @@ -91,11 +115,23 @@ jobs:
check_name: "Canary Smoke Test Results"
fail_on_failure: false

# === DEPLOY PRODUCTION WORKER (only if canary tests pass) ===
- name: Deploy to Production Worker
id: deploy_production
# === DEPLOY DOCS PRODUCTION (only if canary tests pass) ===
- name: Deploy Docs Production Worker
id: deploy_docs_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/docs
command: deploy
packageManager: pnpm

# === DEPLOY APP PRODUCTION (only if canary tests pass) ===
- name: Deploy App Production Worker
id: deploy_production
if: steps.canary_smoke_tests.outcome == 'success' && steps.smoke_docs_canary.outcome == 'success'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Expand Down Expand Up @@ -128,9 +164,22 @@ jobs:
check_name: "Production Smoke Test Results"
fail_on_failure: false

- name: Smoke Test Docs Production
id: smoke_docs_production
if: steps.deploy_docs_production.outcome == 'success'
run: |
set -euo pipefail
URL="https://sentry-mcp-docs.getsentry.workers.dev/docs"
echo "Testing $URL"
STATUS=$(curl -s -o /tmp/docs_prod.html -w "%{http_code}" "$URL")
if [ "$STATUS" -ne 200 ]; then
echo "Unexpected status: $STATUS"; cat /tmp/docs_prod.html; exit 1; fi
if ! grep -q "<title>Documentation" /tmp/docs_prod.html; then
echo "Title check failed"; cat /tmp/docs_prod.html; exit 1; fi

# === ROLLBACK IF PRODUCTION SMOKE TESTS FAIL ===
- name: Rollback Production on Smoke Test Failure
if: steps.production_smoke_tests.outcome == 'failure'
- name: Rollback App Production on Smoke Test Failure
if: steps.production_smoke_tests.outcome == 'failure' || steps.smoke_docs_production.outcome == 'failure'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand All @@ -140,8 +189,19 @@ jobs:
packageManager: pnpm
continue-on-error: true

- name: Rollback Docs Production on Smoke Test Failure
if: steps.production_smoke_tests.outcome == 'failure' || steps.smoke_docs_production.outcome == 'failure'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: packages/docs
command: rollback
packageManager: pnpm
continue-on-error: true

- name: Fail Job if Production Smoke Tests Failed
if: steps.production_smoke_tests.outcome == 'failure'
if: steps.production_smoke_tests.outcome == 'failure' || steps.smoke_docs_production.outcome == 'failure'
run: |
echo "Production smoke tests failed - job failed after rollback"
exit 1
35 changes: 29 additions & 6 deletions .github/workflows/smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,34 @@ jobs:
- name: Build
run: pnpm build

- name: Start local dev server
working-directory: packages/mcp-cloudflare
- name: Start local dev servers (app + docs)
working-directory: .
run: |
# Start wrangler in background and capture output
# Start docs worker first (port 8790)
cd packages/docs
pnpm exec wrangler dev --port 8790 --local > wrangler-docs.log 2>&1 &
DOCS_PID=$!
echo "DOCS_PID=$DOCS_PID" >> $GITHUB_ENV
echo "Waiting for docs server to start (PID: $DOCS_PID)..."

# Wait for docs to be ready (up to 2 minutes)
MAX_ATTEMPTS=24
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
if ! kill -0 $DOCS_PID 2>/dev/null; then
echo "❌ Docs wrangler died unexpectedly!"; tail -50 wrangler-docs.log; exit 1; fi
if curl -s -f -o /dev/null http://localhost:8790/docs; then
echo "✅ Docs server is ready!"; cat wrangler-docs.log; break; fi
ATTEMPT=$((ATTEMPT+1)); sleep 5
done
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then echo "❌ Docs failed to start"; cat wrangler-docs.log; exit 1; fi

# Start app worker (port 8788) in its package
cd ../mcp-cloudflare
pnpm exec wrangler dev --port 8788 --local > wrangler.log 2>&1 &
WRANGLER_PID=$!
echo "WRANGLER_PID=$WRANGLER_PID" >> $GITHUB_ENV
echo "Waiting for server to start (PID: $WRANGLER_PID)..."
echo "Waiting for app server to start (PID: $WRANGLER_PID)..."

# Wait for server to be ready (up to 2 minutes)
MAX_ATTEMPTS=24
Expand Down Expand Up @@ -131,9 +151,12 @@ jobs:
check_name: "Local Smoke Test Results"
fail_on_failure: true

- name: Stop local server
- name: Stop local servers
if: always()
run: |
if [ ! -z "$WRANGLER_PID" ]; then
kill $WRANGLER_PID || true
fi
fi
if [ ! -z "$DOCS_PID" ]; then
kill $DOCS_PID || true
fi
30 changes: 29 additions & 1 deletion docs/cloudflare/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,32 @@ Monitor via Cloudflare dashboard:
- Worker code: `packages/mcp-cloudflare/src/server/`
- Client UI: `packages/mcp-cloudflare/src/client/`
- Wrangler config: `packages/mcp-cloudflare/wrangler.jsonc`
- Cloudflare docs: https://developers.cloudflare.com/workers/
- Cloudflare docs: https://developers.cloudflare.com/workers/

---

## Multi-Worker Deployment (App + Docs)

We deploy two Workers: the App (mcp-cloudflare) and Docs (docs). The App proxies `/docs` to the Docs service binding.

Key configs
- App (wrangler.jsonc):
- `services: [{ binding: "DOCS", service: "sentry-mcp-docs" }]`
- App Canary (wrangler.canary.jsonc):
- `services: [{ binding: "DOCS", service: "sentry-mcp-docs-canary" }]`
- Docs: `packages/docs/wrangler.jsonc` and `wrangler.canary.jsonc` (names: `sentry-mcp-docs`, `sentry-mcp-docs-canary`).

Deploy order (GitHub Actions)
- Deploy Docs Canary → curl `https://sentry-mcp-docs-canary.getsentry.workers.dev/docs` and assert `<title>Documentation`.
- Build + Deploy App Canary (bound to docs-canary) → smoke tests via app.
- If canary smoke passes: deploy Docs Production, then App Production.
- Smoke-test app (existing suite) and docs directly at `https://sentry-mcp-docs.getsentry.workers.dev/docs`.
- On failure: rollback both app and docs.

Local CI smoke (smoke-tests.yml)
- Starts Docs worker (8790) first, then App (8788), so `/docs` binding is live.

Notes
- Keep two Vite servers in dev (5173 app, 5174 docs) to mirror prod.
- Root `pnpm run dev` uses Turbo to start app dev and co-run server/docs dev.
- Build uses Turbo for ordering/caching; unchanged packages aren’t rebuilt.
42 changes: 42 additions & 0 deletions docs/cloudflare/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,45 @@ Think of it as:
- Live deployment: https://mcp.sentry.dev
- Package location: `packages/mcp-cloudflare`
- **For MCP Server docs**: See "Architecture" in @docs/architecture.mdc

## Local Dev Topology

Two Vite servers run in development to mirror production:

- mcp-cloudflare (primary)
- Worker: http://localhost:8788
- Vite HMR: http://localhost:5173
- docs worker (service: `sentry-mcp-docs`)
- Worker: http://localhost:8790
- Vite HMR: http://localhost:5174

Service bindings are defined in `packages/mcp-cloudflare/wrangler.jsonc`:

- `services: [{ binding: "DOCS", service: "sentry-mcp-docs" }]`
- `dev.services: [{ binding: "DOCS", local_port: 8790 }]`

The default export in `packages/mcp-cloudflare/src/server/index.ts` forwards `/docs` to the `DOCS` binding before falling through to the app/OAuth handler.

## Dev Commands (Root)

- `pnpm run dev`
- Uses Turbo to start `@sentry/mcp-cloudflare#dev` and co-run `@sentry/mcp-server#dev` and `@sentry/mcp-docs#dev` via a package-level Turbo config.
- Do not start auxiliary workers manually; the two Vite servers are intentional.

## Build Commands (Root)

- `pnpm run build`
- Turbo builds upstream packages first (`^build`), then `mcp-cloudflare`. Unchanged packages are skipped via caching.

## Troubleshooting

- If Vite briefly fails to resolve `@sentry/mcp-server/*` during dev, it’s usually because the server package is mid-build. Restart dev after the initial build completes.
- Ensure `@sentry/mcp-server` is a runtime dependency of `@sentry/mcp-cloudflare` (declared under `dependencies`).
- For per-package workflows, prefer root `dev`/`build`; Turbo handles ordering and caching.

## Smoke Test

Run smoke tests against a local or preview URL to verify `/docs` routing:

- `PREVIEW_URL=http://localhost:8788 pnpm --filter @sentry/mcp-smoke-tests test`
- Expects status `200` and `<title>Documentation` in the `/docs` response.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"scripts": {
"docs:check": "node scripts/check-doc-links.mjs",
"dev": "dotenv -e .env -e .env.local -- turbo dev",
"dev": "dotenv -e .env -e .env.local -- turbo run dev --filter @sentry/mcp-cloudflare",
"build": "turbo build after-build",
"deploy": "turbo deploy",
"eval": "dotenv -e .env -e .env.local -- turbo eval",
Expand Down
28 changes: 28 additions & 0 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@sentry/mcp-docs",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "FSL-1.1-ALv2",
"files": ["./dist/*"],
"exports": {
".": {
"types": "./dist/index.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"dev": "vite",
"deploy": "wrangler deploy"
},
"devDependencies": {
"@cloudflare/vite-plugin": "catalog:",
"@cloudflare/workers-types": "catalog:",
"@sentry/mcp-server-tsconfig": "workspace:*",
"vite": "catalog:",
"wrangler": "catalog:"
},
"dependencies": {
"hono": "catalog:"
}
}
49 changes: 49 additions & 0 deletions packages/docs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Hono } from "hono";

// Define Cloudflare bindings if/when needed
export type Env = Record<string, never>;

const app = new Hono<{ Bindings: Env }>();

app.get("/docs", (c) => {
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Documentation</title>
<style>
:root { color-scheme: light dark; }
body { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; }
.wrap { display: grid; min-height: 100svh; place-items: center; padding: 2rem; }
.card { width: min(720px, 100%); border: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius: 12px; padding: 24px; }
.skeleton { display: grid; gap: 12px; }
.sk { height: 12px; border-radius: 8px; background: color-mix(in srgb, currentColor 12%, transparent); }
.sk.lg { height: 18px; }
.sk.w25 { width: 25%; }
.sk.w40 { width: 40%; }
.sk.w60 { width: 60%; }
.sk.w80 { width: 80%; }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1 style="margin:0 0 16px 0; font-size: 1.25rem;">Documentation (Placeholder)</h1>
<div class="skeleton" role="status" aria-label="Loading documentation skeleton">
<div class="sk lg w40"></div>
<div class="sk w80"></div>
<div class="sk w60"></div>
<div class="sk w80"></div>
<div class="sk w25"></div>
</div>
</div>
</div>
</body>
</html>`;

return c.html(html);
});

// Default export compatible with Cloudflare Modules
export default { fetch: app.fetch };
12 changes: 12 additions & 0 deletions packages/docs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../mcp-server-tsconfig/tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.docs.tsbuildinfo",
"outDir": "dist",
"rootDir": "src",
"types": [
"@cloudflare/workers-types"
]
},
"include": ["src"]
}
10 changes: 10 additions & 0 deletions packages/docs/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../mcp-server-tsconfig/tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"outDir": "dist",
"types": ["node"]
},
"include": ["vite.config.ts"]
}

9 changes: 9 additions & 0 deletions packages/docs/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [cloudflare({ configPath: "./wrangler.jsonc" })],
server: {
port: 5174,
},
});
19 changes: 19 additions & 0 deletions packages/docs/wrangler.canary.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Canary configuration for sentry-mcp-docs worker
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "sentry-mcp-docs-canary",
"main": "./src/index.ts",
"compatibility_date": "2025-03-21",
"compatibility_flags": [
"nodejs_compat",
"nodejs_compat_populate_process_env",
"global_fetch_strictly_public"
],
"keep_vars": true,
"vars": {},
"dev": {
"port": 8791
}
}
Loading