release #100
Workflow file for this run
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: 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 | |