seo: noindex all release pages, retire the release sitemap (#2218) #1342
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |