Skip to content

release

release #100

Workflow file for this run

name: release
# Tag-driven publishing for @leadbay/mcp (npm).
#
# Tag scheme:
# - mcp-v<ver> → publishes @leadbay/mcp
# - v<ver> → legacy alias for mcp-v<ver>
#
# One-time setup (done in repo settings):
# - NPM_TOKEN → npm automation token, scope-owner on @leadbay
#
# The live HTTP endpoint (https://mcp.leadbay.app/mcp) is deployed separately by
# build-image.yml → Azure Container Registry → Argo CD in leadbay/infra; this
# workflow only publishes to npm + the MCP Registry and uploads the .dxt/.mcpb.
#
# No auto-version-bump, no auto-changelog. Bump `packages/mcp/package.json`
# in a normal PR. Tagging is automatic: `auto-tag.yml` watches main, sees the
# bumped version, and pushes the corresponding tag, which triggers this workflow.
# Manual `git tag` is only needed for emergency re-publish of a version already on main.
on:
push:
tags:
- "mcp-v*.*.*"
- "v*.*.*"
workflow_dispatch:
inputs:
package:
description: "Which package to publish (mcp)"
required: true
default: "mcp"
type: choice
options: [mcp]
dry_run:
description: "If true, npm publish uses --dry-run"
required: false
default: "false"
jobs:
preflight-npm:
name: Preflight — verify npm auth + @leadbay scope
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
always-auth: true
- name: whoami + scope probe
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
echo "Logged in as: $(npm whoami --registry=https://registry.npmjs.org)"
# Scope-empty on first publish is OK; fall through. Fatal errors (401/403)
# come from npm whoami above.
npm access list packages @leadbay 2>&1 || echo "scope empty or no packages yet — first publish will create"
publish-mcp:
name: Publish @leadbay/mcp to npm
needs: preflight-npm
if: |
startsWith(github.ref, 'refs/tags/mcp-v') ||
startsWith(github.ref, 'refs/tags/v') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.package == 'mcp')
runs-on: ubuntu-latest
permissions:
contents: write # gh release create/upload for the .dxt bundle
id-token: write # npm provenance
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
# Node 24 ships with npm 11.5+ which speaks Trusted Publishers
# OIDC natively. Node 22's bundled npm 10.x cannot do the OIDC
# handshake, and in-place self-upgrade (`npm i -g npm@latest`)
# consistently breaks with MODULE_NOT_FOUND on the runner image.
node-version: 24
registry-url: "https://registry.npmjs.org"
# NO `always-auth: true` — that writes a .npmrc `_authToken=${NODE_AUTH_TOKEN}`
# line which preempts the Trusted Publishers OIDC handshake.
- name: Install
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm -r build
- name: Test
run: pnpm -r test
- name: Verify tag matches packages/mcp/package.json version
# Runs whenever the workflow is keyed off a tag ref — both real
# tag-push events and auto-tag's workflow_dispatch on a tag ref.
if: startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
REF="${GITHUB_REF#refs/tags/}"
# Strip either "mcp-v" or plain "v" prefix.
TAG="${REF#mcp-v}"
TAG="${TAG#v}"
PKG_VERSION=$(node -p "require('./packages/mcp/package.json').version")
if [ "$TAG" != "$PKG_VERSION" ]; then
echo "Tag $REF → version $TAG does not match packages/mcp/package.json $PKG_VERSION"
exit 1
fi
echo "Aligned: $REF = $PKG_VERSION"
- name: Publish
# Uses npm Trusted Publishers (OIDC). Requirements (all must hold):
# 1. `id-token: write` permission (granted at job level above)
# 2. Trusted Publisher binding configured on npmjs.com for
# @leadbay/mcp pointing at leadbay/mcp + release.yml
# 3. npm CLI ≥ 11.5.1 (we `npm install -g npm@latest` below;
# the runner's bundled npm 10.x doesn't speak OIDC)
# 4. NO NODE_AUTH_TOKEN env var — not even empty string. Per
# the npm migration guide, an empty value is treated as
# "use this token", which preempts OIDC. We deliberately
# omit the env block entirely.
# 5. NO `_authToken=` line in any .npmrc (we don't pass
# `always-auth: true` to setup-node above, so no auth line
# is written into $RUNNER_TEMP/.npmrc).
working-directory: packages/mcp
run: |
set -euo pipefail
# Node 24's bundled npm is ≥11.5 — sufficient for Trusted
# Publishers OIDC. No self-upgrade dance required.
echo "npm $(npm --version) at $(which npm)"
VERSION=$(node -p "require('./package.json').version")
# Idempotent: if @leadbay/mcp@$VERSION is already on npm, skip.
if npm view "@leadbay/mcp@$VERSION" version --silent 2>/dev/null; then
echo "@leadbay/mcp@$VERSION already on npm — skipping"
exit 0
fi
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
npm publish --access public --provenance --dry-run
else
npm publish --access public --provenance
fi
- name: Build .dxt bundle
# Same gate as the tag-verify step: runs on any tag ref, including
# auto-tag's workflow_dispatch. Skipped when running off main.
if: startsWith(github.ref, 'refs/tags/') && github.event.inputs.dry_run != 'true'
run: pnpm --filter @leadbay/dxt build
- name: Upload .dxt and .mcpb to GitHub Release
if: startsWith(github.ref, 'refs/tags/') && github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
# gh release upload needs a release to exist. Create it if the tag
# wasn't already promoted to a release (auto-tag.yml only pushes tags).
if ! gh release view "$TAG" >/dev/null 2>&1; then
# Extract the changelog section for this version (text between
# the matching "## X.Y.Z" header and the next "## " header).
VERSION="${TAG#mcp-v}"
NOTES=$(awk "/^## ${VERSION}/{found=1; next} found && /^## /{exit} found{print}" packages/mcp/CHANGELOG.md | sed '/^[[:space:]]*$/{ N; /^\n$/d }' | head -60)
if [ -z "$NOTES" ]; then
NOTES="See [CHANGELOG.md](https://github.com/leadbay/mcp/blob/main/packages/mcp/CHANGELOG.md) for details."
fi
gh release create "$TAG" \
--title "$TAG" \
--notes "$NOTES"
fi
gh release upload "$TAG" packages/dxt/dist/*.dxt packages/dxt/dist/*.mcpb --clobber
echo "Uploaded .dxt and .mcpb to release $TAG"
# Also publish fixed-name copies so docs can hardcode a stable URL.
# The newest release is GitHub's "Latest", so
# …/releases/latest/download/leadbay-latest.dxt
# always resolves to the current version without per-release edits.
DXT=$(ls packages/dxt/dist/*.dxt | grep -v 'leadbay-latest' | head -1)
MCPB=$(ls packages/dxt/dist/*.mcpb | grep -v 'leadbay-latest' | head -1)
cp "$DXT" packages/dxt/dist/leadbay-latest.dxt
cp "$MCPB" packages/dxt/dist/leadbay-latest.mcpb
gh release upload "$TAG" \
packages/dxt/dist/leadbay-latest.dxt \
packages/dxt/dist/leadbay-latest.mcpb --clobber
echo "Uploaded fixed-name leadbay-latest.{dxt,mcpb} to release $TAG"
publish-mcp-registry:
name: Publish to MCP Registry
needs: publish-mcp
if: |
(startsWith(github.ref, 'refs/tags/mcp-v') || startsWith(github.ref, 'refs/tags/v')) &&
github.event.inputs.dry_run != 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Install mcp-publisher
run: |
set -euo pipefail
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
./mcp-publisher --version
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Verify server.json version matches package.json
run: |
set -euo pipefail
PKG=$(node -p "require('./packages/mcp/package.json').version")
SRV=$(node -p "require('./packages/mcp/server.json').version")
NAME=$(node -p "require('./packages/mcp/server.json').name")
MCP_NAME=$(node -p "require('./packages/mcp/package.json').mcpName")
if [ "$PKG" != "$SRV" ]; then
echo "Drift: package.json=$PKG server.json=$SRV"
exit 1
fi
if [ "$NAME" != "$MCP_NAME" ]; then
echo "Drift: server.json name=$NAME package.json mcpName=$MCP_NAME"
exit 1
fi
echo "Aligned: $PKG / $NAME"
- name: Validate server.json
run: ./mcp-publisher validate packages/mcp/server.json
- name: Publish
run: ./mcp-publisher publish packages/mcp/server.json