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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: HTML Title Mismatch Blocks Deployments

The docs worker's HTML title in packages/docs/client/index.html is <title>Sentry MCP Docs</title>. However, the deploy workflow and smoke tests expect <title>Documentation. This mismatch causes docs canary and production smoke tests to fail, blocking deployments and triggering rollbacks.

Additional Locations (2)

Fix in Cursor Fix in Web


# === 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
13 changes: 13 additions & 0 deletions packages/docs/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sentry MCP Docs</title>
</head>
<body class="bg-black text-white">
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

19 changes: 19 additions & 0 deletions packages/docs/client/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./src/App";
import "./src/index.css";

const router = createBrowserRouter([
{
path: "/docs/*",
element: <App />,
},
]);

const root = createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
51 changes: 51 additions & 0 deletions packages/docs/client/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none cursor-pointer",
{
variants: {
variant: {
default: "bg-violet-300 text-black shadow hover:bg-violet-300/90",
outline:
"bg-slate-800/50 border border-slate-600/50 hover:bg-slate-700/50 hover:text-white",
secondary:
"bg-transparent border border-slate-600/60 hover:bg-slate-800/50",
link: "text-violet-300 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
xs: "h-7 gap-1.5 px-2 has-[>svg]:px-1.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
},
active: { true: "text-violet-300 underline" },
},
defaultVariants: { variant: "default", size: "default" },
},
);

export function Button({
className,
variant,
size,
active = false,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
active?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className, active }))}
{...props}
/>
);
}

export { buttonVariants };
26 changes: 26 additions & 0 deletions packages/docs/client/src/components/ui/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SentryIcon } from "./icons/sentry";
import { Button } from "./button";

export function Header() {
return (
<header className="sticky top-0 z-10 backdrop-blur border-b border-slate-800 bg-black/50">
<div className="h-14 mx-auto max-w-[1200px] px-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<SentryIcon className="h-7 w-7 text-violet-400" />
<div className="text-lg font-semibold">Sentry MCP</div>
</div>
<div className="flex items-center gap-2">
<Button asChild variant="secondary">
<a
href="https://github.com/getsentry/sentry-mcp"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
</Button>
</div>
</div>
</header>
);
}
26 changes: 26 additions & 0 deletions packages/docs/client/src/components/ui/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
interface IconProps {
className?: string;
path: string;
viewBox?: string;
title?: string;
}

export function Icon({
className,
path,
viewBox = "0 0 32 32",
title = "Icon",
}: IconProps) {
return (
<svg
className={className}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="icon-title"
>
<title id="icon-title">{title}</title>
<path d={path} fill="currentColor" />
</svg>
);
}
Loading
Loading