mcp-release.yml
In .github/workflows/ · At 62577fbCopy the commit ID
Preview
Code · 7 KiBname: "Release: MCP"
# Release-gated corpus updates: the MCP indexes a release tag's tree, so the
# served corpus always matches the published crates — `main` content never
# bleeds into a released version label. Versions are immutable once seeded
# (skip-if-indexed); re-runs and GitHub's occasional duplicate tag events are
# harmless. The escape hatch re-seeds a version FROM ITS OWN TAG (`force`),
# e.g. after an indexer bugfix — same source, better representation.
#
# No `paths:` filter on purpose: paths semantics are unreliable for tag pushes
# (no stable diff base), which is also why this is a separate workflow from
# the PR-validation mcp.yml.
on:
push:
tags:
- 'v*.*.*'
# Manual escape hatch (mirrors release.yml's): re-run for an existing tag,
# or force a re-seed of an already-indexed version after an indexer fix.
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to (re)index (e.g. v0.2.0). Must be pushed to origin.'
required: true
type: string
force:
description: 'Re-seed even if this version is already indexed'
type: boolean
default: false
permissions:
contents: read
# Serialize racing tags / re-seed dispatches against each other.
concurrency:
group: mcp-deploy
cancel-in-progress: false
jobs:
release:
name: Seed corpus from tag
runs-on: ubuntu-latest
environment: mcp
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
steps:
- name: Validate tag input shape (dispatch only)
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::tag '$RELEASE_TAG' is not a vX.Y.Z release tag"
exit 1
fi
# Check out the TAG, not a branch — the indexed tree IS the released
# source by construction.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.tag || github.ref }}
# workflow_dispatch always runs THIS workflow file (from the ref you
# dispatch from, normally main) against the checked-out TAG's tree —
# the two can disagree on layout. The MCP app lived at top-level `mcp/`
# before the apps/ reorg (PR #444/#504, merged 2026-06-23) and at
# `apps/mcp/` after. Detect which one the checked-out tree actually has
# instead of hardcoding a path that only matches post-reorg tags.
- name: Locate MCP app directory in checked-out tree
id: locate-mcp
run: |
if [ -d apps/mcp ]; then
echo "dir=apps/mcp" >> "$GITHUB_OUTPUT"
elif [ -d mcp ]; then
echo "dir=mcp" >> "$GITHUB_OUTPUT"
else
echo "::error::could not locate the MCP app directory (checked apps/mcp and mcp) in the checked-out tree for ${{ inputs.tag || github.ref }}"
exit 1
fi
- name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
- name: Install MCP dependencies
working-directory: ${{ steps.locate-mcp.outputs.dir }}
run: npm ci
- name: Typecheck + test (never seed from a tree whose indexer fails)
working-directory: ${{ steps.locate-mcp.outputs.dir }}
run: npm run ci
- name: Build corpus index from the tag's tree
working-directory: ${{ steps.locate-mcp.outputs.dir }}
run: npm run index
# Guard: the indexer derives the version from rust/Cargo.toml. If the tag
# was pushed before the release-plz version-bump merged, the label would
# lie about the content — fail loudly instead.
- name: Guard — tag matches workspace version
working-directory: ${{ steps.locate-mcp.outputs.dir }}
run: |
INDEXED="$(jq -r .version dist/manifest.json)"
if [[ "$INDEXED" != "$RELEASE_TAG" ]]; then
echo "::error::workspace version '$INDEXED' (rust/Cargo.toml at this tag) != tag '$RELEASE_TAG' — refusing to seed a mislabeled corpus"
exit 1
fi
echo "version check ok: $INDEXED"
- name: Check whether this version is already indexed
id: indexed
working-directory: ${{ steps.locate-mcp.outputs.dir }}
shell: bash # explicit: gets `-o pipefail`, so a wrangler failure fails the step
env:
FORCE: ${{ inputs.force }}
run: |
set -o pipefail
# Run wrangler separately so its exit code and output are visible in
# the log (a command-substitution failure under `set -e` otherwise dies
# before any diagnostic prints). Capture stdout; keep stderr in the log.
set +e
RAW="$(npx --yes wrangler d1 execute mkit-mcp-search --remote --json \
--command "SELECT version FROM versions WHERE version = '$RELEASE_TAG'" 2>wrangler.err)"
rc=$?
set -e
echo "--- wrangler stdout (rc=$rc) ---"
printf '%s\n' "$RAW"
echo "--- wrangler stderr ---"
cat wrangler.err || true
echo "------------------------------"
if [ "$rc" -ne 0 ]; then
echo "::error::wrangler d1 execute failed (rc=$rc) for $RELEASE_TAG"
exit 1
fi
echo "--- wrangler --json stdout ---"
printf '%s\n' "$RAW"
echo "------------------------------"
# wrangler's `d1 execute --json` shape drifts across versions (array-
# wrapped `[{results:[...]}]` vs bare object `{results:[...]}`, and it
# may print a leading banner). Recursively collect any `results` arrays
# and count rows — tolerant of all of these.
COUNT="$(printf '%s' "$RAW" | jq -s '[.. | objects | .results? // empty] | add // [] | length')"
echo "indexed-row count for $RELEASE_TAG: '$COUNT'"
# A non-numeric COUNT means the query/parse failed — fail the run, never
# masquerade as "already indexed".
if [[ ! "$COUNT" =~ ^[0-9]+$ ]]; then
echo "::error::could not determine indexed state for $RELEASE_TAG (got: '$COUNT')"
exit 1
fi
if [[ "$COUNT" != "0" && "$FORCE" != "true" ]]; then
echo "seed=false" >> "$GITHUB_OUTPUT"
echo "$RELEASE_TAG already indexed — corpus untouched (immutable); pass force=true to re-seed from this tag."
else
echo "seed=true" >> "$GITHUB_OUTPUT"
fi
- name: Apply D1 migrations (idempotent)
if: ${{ steps.indexed.outputs.seed == 'true' }}
working-directory: ${{ steps.locate-mcp.outputs.dir }}
run: npm run db:migrate
- name: Seed corpus into remote D1
if: ${{ steps.indexed.outputs.seed == 'true' }}
working-directory: ${{ steps.locate-mcp.outputs.dir }}
run: npm run db:seed