Skip to content

seo: noindex all release pages, retire the release sitemap (#2218) #1342

seo: noindex all release pages, retire the release sitemap (#2218)

seo: noindex all release pages, retire the release sitemap (#2218) #1342

name: Deploy Workers
on:
push:
branches: [main]
workflow_dispatch:
inputs:
worker:
description: Which worker to deploy (manual run)
type: choice
options: [all, api, mcp, discovery, webhooks]
default: all
# Target environment for a manual run. On push:main, the api/mcp/discovery
# jobs fan out to BOTH prod and staging via matrix (webhooks + the MCP
# registry remain prod-only). Constrained to a choice so the value is one
# of production|staging; shell steps reference it through env: to keep
# interpolation inert.
environment:
description: Target environment (manual run only; push deploys both)
type: choice
options: [production, staging]
default: production
concurrency:
group: deploy-workers-${{ github.ref }}
cancel-in-progress: false
env:
BUN_VERSION: "1.3.14"
# wrangler 4.88+ requires Node 22+; portless ≥0.13 requires Node 24+. bunx
# invokes wrangler's bin via the `#!/usr/bin/env node` shebang, so the runner's
# Node version is what counts. Pinned to 24.15.0 (bundles npm 11.12.1).
NODE_VERSION: "24.15.0"
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
api: ${{ steps.filter.outputs.api }}
mcp: ${{ steps.filter.outputs.mcp }}
mcp_registry: ${{ steps.filter.outputs.mcp_registry }}
discovery: ${{ steps.filter.outputs.discovery }}
webhooks: ${{ steps.filter.outputs.webhooks }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
_shared: &shared
- 'packages/**'
- 'package.json'
- 'bun.lock'
- '.github/workflows/deploy-workers.yml'
api:
- *shared
- 'workers/api/**'
# The API worker compiles the persisted-document allowlist into
# the bundle (workers/api/src/graphql/persisted.ts). When the web
# codegen mints a new hash, the API must redeploy or non-admin
# callers hit PERSISTED_QUERY_NOT_IN_LIST.
- 'web/src/lib/graphql/__generated__/persisted-documents.json'
mcp:
- *shared
- 'workers/mcp/**'
mcp_registry:
- 'workers/mcp/server.json'
- '.github/workflows/deploy-workers.yml'
discovery:
- *shared
- 'workers/discovery/**'
- 'src/**'
webhooks:
- *shared
- 'workers/webhooks/**'
deploy-api:
name: Deploy API worker (${{ matrix.environment }})
needs: changes
if: |
(github.event_name == 'push' && needs.changes.outputs.api == 'true') ||
(github.event_name == 'workflow_dispatch' && (inputs.worker == 'all' || inputs.worker == 'api'))
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# On push: fan out to prod + staging. On manual dispatch: just the chosen env.
environment: ${{ github.event_name == 'workflow_dispatch' && fromJSON(format('["{0}"]', inputs.environment)) || fromJSON('["production", "staging"]') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ env.BUN_VERSION }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- run: bun install --frozen-lockfile
# Apply D1 migrations before the worker starts serving the new code so
# new schema lands before anything queries it. Idempotent: wrangler
# skips entries already tracked in d1_migrations.
#
# Prod passes the DB name (`released-db`); staging passes the binding name
# (`DB`) + `--env staging`, so wrangler resolves through the env block.
- name: Apply D1 migrations
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TARGET_ENV: ${{ matrix.environment }}
run: |
if [ "$TARGET_ENV" = "staging" ]; then
bunx wrangler d1 migrations apply DB --env staging --remote --config workers/api/wrangler.jsonc
else
bunx wrangler d1 migrations apply released-db --remote --config workers/api/wrangler.jsonc
fi
- name: Deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
WRANGLER_ENV_FLAG: ${{ matrix.environment == 'staging' && '--env staging' || '' }}
run: bunx wrangler deploy $WRANGLER_ENV_FLAG --config workers/api/wrangler.jsonc
deploy-mcp:
name: Deploy MCP worker (${{ matrix.environment }})
needs: changes
if: |
(github.event_name == 'push' && needs.changes.outputs.mcp == 'true') ||
(github.event_name == 'workflow_dispatch' && (inputs.worker == 'all' || inputs.worker == 'mcp'))
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
environment: ${{ github.event_name == 'workflow_dispatch' && fromJSON(format('["{0}"]', inputs.environment)) || fromJSON('["production", "staging"]') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ env.BUN_VERSION }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- run: bun install --frozen-lockfile
# workers/mcp is intentionally excluded from root workspaces (cloudflare:workers
# imports break Bun's eager workspace resolution). Install its deps locally.
- name: Install MCP worker deps
working-directory: workers/mcp
run: bun install --frozen-lockfile
- name: Deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
WRANGLER_ENV_FLAG: ${{ matrix.environment == 'staging' && '--env staging' || '' }}
run: bunx wrangler deploy $WRANGLER_ENV_FLAG --config workers/mcp/wrangler.jsonc
deploy-discovery:
name: Deploy discovery worker (${{ matrix.environment }})
needs: changes
if: |
(github.event_name == 'push' && needs.changes.outputs.discovery == 'true') ||
(github.event_name == 'workflow_dispatch' && (inputs.worker == 'all' || inputs.worker == 'discovery'))
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
environment: ${{ github.event_name == 'workflow_dispatch' && fromJSON(format('["{0}"]', inputs.environment)) || fromJSON('["production", "staging"]') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ env.BUN_VERSION }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- run: bun install --frozen-lockfile
- name: Install discovery worker deps
working-directory: workers/discovery
run: bun install --frozen-lockfile
- name: Deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
WRANGLER_ENV_FLAG: ${{ matrix.environment == 'staging' && '--env staging' || '' }}
run: bunx wrangler deploy $WRANGLER_ENV_FLAG --config workers/discovery/wrangler.jsonc
deploy-webhooks:
name: Deploy webhooks worker
needs: changes
# Webhooks has no staging env; skip when staging is selected.
if: |
inputs.environment != 'staging' && (
(github.event_name == 'push' && needs.changes.outputs.webhooks == 'true') ||
(github.event_name == 'workflow_dispatch' && (inputs.worker == 'all' || inputs.worker == 'webhooks'))
)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ env.BUN_VERSION }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- run: bun install --frozen-lockfile
# workers/webhooks is intentionally excluded from root workspaces (cloudflare:workers
# imports break Bun's eager workspace resolution). Install its deps locally.
- name: Install webhooks worker deps
working-directory: workers/webhooks
run: bun install --frozen-lockfile
- name: Deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: bunx wrangler deploy --config workers/webhooks/wrangler.jsonc
# Live e2e: enqueue a synthetic event through the echo subscriber and tail
# deliveries. Only runs on push to main when WEBHOOK_E2E_SUBSCRIPTION_ID is set.
# continue-on-error keeps the job green; the `|| echo` lines print a named
# diagnostic before the per-command failure is swallowed.
- name: Live e2e — webhook delivery smoke test
if: github.event_name == 'push'
continue-on-error: true
env:
RELEASES_API_URL: https://api.releases.sh
RELEASES_API_KEY: ${{ secrets.RELEASES_API_KEY || secrets.RELEASED_API_KEY }}
SUBSCRIPTION_ID: ${{ secrets.WEBHOOK_E2E_SUBSCRIPTION_ID }}
run: |
set -euo pipefail
if [ -z "${SUBSCRIPTION_ID:-}" ]; then
echo "[e2e] WEBHOOK_E2E_SUBSCRIPTION_ID not set — skipping"
exit 0
fi
AUTH="Authorization: Bearer $RELEASES_API_KEY"
curl -fsS -X POST -H "$AUTH" "$RELEASES_API_URL/v1/admin/webhooks/$SUBSCRIPTION_ID/test" \
|| echo "[e2e] webhook test call failed (non-fatal)"
# Queue max_batch_timeout is 5s, so a plain sleep races the flush. Poll up to ~14s.
for _ in 1 2 3 4 5 6 7; do
ROWS=$(curl -fsS -H "$AUTH" "$RELEASES_API_URL/v1/admin/webhooks/$SUBSCRIPTION_ID/deliveries?limit=1" 2>/dev/null || true)
if echo "$ROWS" | grep -q '"event_id"'; then break; fi
sleep 2
done
curl -fsS -H "$AUTH" "$RELEASES_API_URL/v1/admin/webhooks/$SUBSCRIPTION_ID/deliveries?limit=5" \
|| echo "[e2e] deliveries call failed (non-fatal)"
publish-mcp-registry:
name: Publish MCP registry metadata
needs: [changes, deploy-mcp]
# Registry metadata is production-only — staging deploys skip it.
if: |
inputs.environment != 'staging' && (
(github.event_name == 'push' && needs.changes.outputs.mcp_registry == 'true') ||
(github.event_name == 'workflow_dispatch' && (inputs.worker == 'all' || inputs.worker == 'mcp'))
)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install mcp-publisher
run: |
set -euo pipefail
curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" -o /tmp/mcp-publisher.tar.gz
tar -xzf /tmp/mcp-publisher.tar.gz -C /tmp mcp-publisher
sudo mv /tmp/mcp-publisher /usr/local/bin/mcp-publisher
- name: Login and publish
working-directory: workers/mcp
env:
PRIVATE_KEY_PEM: ${{ secrets.MCP_REGISTRY_PRIVATE_KEY_PEM }}
run: |
set -euo pipefail
if [ -z "${PRIVATE_KEY_PEM:-}" ]; then
echo "MCP_REGISTRY_PRIVATE_KEY_PEM secret is not set" >&2
exit 1
fi
KEY_FILE="$(mktemp)"
trap 'rm -f "$KEY_FILE"' EXIT
printf '%s\n' "$PRIVATE_KEY_PEM" > "$KEY_FILE"
PRIVATE_KEY="$(openssl pkey -in "$KEY_FILE" -noout -text | grep -A3 'priv:' | tail -n +2 | tr -d ' :\n')"
mcp-publisher login http --domain releases.sh --private-key "$PRIVATE_KEY"
# Registry rejects re-publishing an existing version; treat that as a no-op
# so workflow-dispatch reruns and workflow-only edits don't fail the deploy.
if mcp-publisher publish > /tmp/publish.log 2>&1; then
cat /tmp/publish.log
else
cat /tmp/publish.log
if grep -qE 'already exists|duplicate version' /tmp/publish.log; then
echo "Version already published — skipping."
exit 0
fi
exit 1
fi