Skip to content

Publish MCP Package #13

Publish MCP Package

Publish MCP Package #13

Workflow file for this run

name: Publish MCP Package
# workflow_dispatch-only (no push:tags: trigger): a GITHUB_TOKEN-created tag (e.g. from the release
# automation) does not fire push-triggered workflows, so the release automation must explicitly
# dispatch this workflow after it tags a release. A bare manual dispatch (released_by_release_please
# left false) is the human override path and self-tags HEAD from packages/loopover-mcp/package.json's
# version, matching the old "push a tag to publish" flow just invoked differently.
on:
workflow_dispatch:
inputs:
released_by_release_please:
description: "Internal: set by the release automation's dispatch so this run skips re-creating the GitHub release it already made."
type: boolean
default: false
permissions:
contents: read
concurrency:
group: npm-publish-${{ github.ref_name }}
cancel-in-progress: false
jobs:
# Unprivileged: resolves the version, runs the MCP package's own build/test/pack, all with
# contents: read only. npm ci here executes dependency lifecycle scripts; keeping that in a job
# with no write/id-token permission means a compromised build dependency has nothing to abuse
# (Superagent P2 / mirrors metagraphed's publish-client.yml "validate" job, codex #251).
validate:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false
- name: Verify release commit is on main
env:
RELEASE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$RELEASE_SHA" refs/remotes/origin/main; then
echo "::error::MCP package releases must be cut from a commit reachable from main."
exit 1
fi
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
# workflow_dispatch has no tag ref to derive a version from (unlike the old push:tags: trigger),
# so the dispatched commit's package.json is now the single source of truth for VERSION/TAG. Pure
# read here -- if the tag already exists (release automation tagged it before dispatching this
# workflow) it's verified against HEAD; if not, the privileged publish job below creates it, since
# this job has no contents: write.
- name: Resolve release version
id: version
run: |
set -euo pipefail
VERSION="$(node -p "require('./packages/loopover-mcp/package.json').version")"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid package version: $VERSION"
exit 1
fi
TAG="mcp-v${VERSION}"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
HEAD_SHA="$(git rev-parse HEAD)"
TAG_SHA="$(git rev-list -n 1 "$TAG")"
if [ "$TAG_SHA" != "$HEAD_SHA" ]; then
echo "::error::Tag $TAG already exists but points at $TAG_SHA, not the dispatched commit $HEAD_SHA"
exit 1
fi
echo "Tag $TAG already exists and matches HEAD."
else
echo "Tag $TAG does not exist yet; the publish job will create it."
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Install dependencies
run: npm ci
# packages/loopover-mcp's CLI test suite (test/unit/mcp-cli-*.test.ts) spawns the real binary,
# which imports @loopover/engine. That package's dist/ is gitignored (see ci.yml's own
# build --workspace @loopover/engine step for the same reason) -- without building it first,
# the CLI test suite below fails with "Failed to resolve entry for package @loopover/engine".
- name: Build loopover-engine
run: npm run build --workspace @loopover/engine
# engine/miner/ui-kit's own publish workflows only validate their OWN package (a workspace
# test run + pack/smoke-test) -- mcp previously ran the full npm run test:ci (every workspace,
# every drift-check, ui:lint/build, migrations, etc.), coupling an MCP release to the health of
# the entire monorepo. Confirmed harmful live: an unrelated packages/loopover-miner env-reference
# doc going stale blocked MCP's publish outright, even though MCP's own package code was
# completely unaffected -- and main's required CI already re-verifies whole-repo health on
# every PR, including release-please's own, so re-running all of it here is redundant with what
# already gated the merge. Scoped down to match its siblings instead: MCP's own syntax check
# (node --check, same as build:mcp) plus its own CLI test suite -- test/unit/mcp-cli-*.test.ts
# specifically (imports packages/loopover-mcp and spawns the built binary), NOT the sibling
# test/unit/mcp-*.test.ts files that exercise the Worker's separate remote MCP server
# (src/mcp/server.ts, a different deployable entirely).
- name: MCP package syntax validation
run: npm run build:mcp
- name: MCP CLI test suite
run: npx vitest run mcp-cli-
# Build + pack happen in THIS unprivileged job (no id-token). The privileged publish job below
# never runs npm install/build, so a compromised build dependency can't reach the OIDC token.
- name: Pack and smoke-test the tarball
run: |
set -euo pipefail
PACK_JSON="$(npm pack --workspace @loopover/mcp --pack-destination "$RUNNER_TEMP" --json)"
TARBALL="$(node -e 'const fs=require("fs"); const input=fs.readFileSync(0,"utf8"); process.stdout.write(JSON.parse(input)[0].filename)' <<< "$PACK_JSON")"
TARBALL_PATH="$RUNNER_TEMP/$TARBALL"
UNEXPECTED_FILES="$(tar -tzf "$TARBALL_PATH" | grep -Ev '^(package/(bin|lib|scripts)/.+|package/(package.json|README.md|CHANGELOG.md|LICENSE))$' || true)"
if [ -n "$UNEXPECTED_FILES" ]; then
printf '%s\n' "$UNEXPECTED_FILES"
echo "Unexpected file in package tarball"
exit 1
fi
if tar -xOf "$TARBALL_PATH" | grep -qE '(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|github_pat_|gh[pousr]_|gts_[0-9a-f]{64}|[A-Z0-9_]*(TOKEN|SECRET|PRIVATE_KEY)=)'; then
echo "Secret-like content found in package tarball"
exit 1
fi
TMP="$(mktemp -d)"
npm --prefix "$TMP" init -y >/dev/null
npm --prefix "$TMP" install "$TARBALL_PATH" >/dev/null
"$TMP/node_modules/.bin/loopover-mcp" --help >/dev/null
- name: Upload package tarball
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: loopover-mcp-tarball
path: ${{ runner.temp }}/*.tgz
if-no-files-found: error
retention-days: 7
# Privileged: tags + publishes the EXACT tarball the unprivileged job already tested. No npm
# install/build runs here, so nothing with dependency-lifecycle-script access ever sees contents:
# write or the OIDC token (Superagent P2). environment: release requires reviewer approval per repo
# Settings > Environments, same gate release-selfhost.yml already uses (Superagent P1).
publish:
runs-on: ubuntu-latest
needs: validate
environment: release
timeout-minutes: 15
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false
- name: Verify release commit is on main
env:
RELEASE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$RELEASE_SHA" refs/remotes/origin/main; then
echo "::error::MCP package releases must be cut from a commit reachable from main."
exit 1
fi
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
registry-url: https://registry.npmjs.org
- name: Create or verify release tag
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.validate.outputs.tag }}
VERSION: ${{ needs.validate.outputs.version }}
run: |
set -euo pipefail
HEAD_SHA="$(git rev-parse HEAD)"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Tag $TAG already exists (verified against HEAD by the validate job)."
else
echo "Creating tag $TAG at HEAD ($HEAD_SHA)."
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "@loopover/mcp v${VERSION}"
git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git"
gh auth setup-git
git push origin "$TAG"
fi
- name: Download package tarball
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: loopover-mcp-tarball
path: ${{ runner.temp }}/loopover-mcp-package
- name: Publish to npm (OIDC trusted publishing)
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
set -euo pipefail
count=$(find "$RUNNER_TEMP/loopover-mcp-package" -maxdepth 1 -type f -name "*.tgz" | wc -l | tr -d ' ')
if [ "$count" != "1" ]; then
echo "Expected exactly one tarball, found $count" >&2
find "$RUNNER_TEMP/loopover-mcp-package" -maxdepth 1 -type f -name "*.tgz" -print >&2
exit 1
fi
tarball=$(find "$RUNNER_TEMP/loopover-mcp-package" -maxdepth 1 -type f -name "*.tgz" -print -quit)
npx -y npm@11.15.0 publish "$tarball" --access public --provenance
# apps/loopover-ui/src/lib/mcp-package.ts's MCP_PACKAGE_KNOWN_LATEST_VERSION (ui:version-audit,
# already CI-enforced) tracks the latest PUBLISHED release -- its own header comment explains why
# it can't be pre-synced in the Release PR itself: it must never claim a version ahead of npm, so
# the earliest correct moment to update it is right here, after publish actually succeeds. Left as
# a manual "did anyone notice CI red and run the sync command" step before, this blocked completely
# unrelated PRs' CI with zero connection to what they actually changed (confirmed live: an
# auto-generated release PR for a DIFFERENT package went red on this exact gap, twice in one
# session). Opens a small PR instead of pushing straight to main: this repo's branch protection
# requires PRs (required_pull_request_reviews), so a direct push would just be rejected.
sync-ui-version-copy:
runs-on: ubuntu-latest
needs: [validate, publish]
if: ${{ !cancelled() && needs.publish.result == 'success' }}
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
- name: Sync and open a PR if the known-latest copy is now stale
env:
# A real collaborator token, not github.token -- mirrors mcp-release-please.yml's own
# rationale: PRs/commits pushed as github-actions[bot] repeatedly get re-flagged as needing
# manual workflow-run approval, which a real account sidesteps.
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
run: |
set -euo pipefail
node scripts/check-ui-mcp-version-copy.mjs --write
if git diff --quiet apps/loopover-ui/src/lib/mcp-package.ts; then
echo "Already in sync, nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
branch="chore/sync-mcp-version-copy-${{ needs.validate.outputs.version }}"
git checkout -B "$branch"
git add apps/loopover-ui/src/lib/mcp-package.ts
git commit -m "chore(ui): sync MCP_PACKAGE_KNOWN_LATEST_VERSION with npm dist-tags.latest"
git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git"
gh auth setup-git
git push origin "HEAD:$branch"
gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$branch" \
--title "chore(ui): sync MCP_PACKAGE_KNOWN_LATEST_VERSION with npm dist-tags.latest" \
--body "Automated: @loopover/mcp v${{ needs.validate.outputs.version }} just published to npm; keeps apps/loopover-ui/src/lib/mcp-package.ts's known-latest-version copy in sync (ui:version-audit)."
github-release:
runs-on: ubuntu-latest
needs: [validate, publish]
# Skip when the release automation dispatched this run: it already created the GitHub release
# with its own generated changelog notes before dispatching, so running this unconditionally
# would overwrite those richer notes with the generic blurb below.
if: ${{ inputs.released_by_release_please != true }}
timeout-minutes: 5
permissions:
contents: write
steps:
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
set -euo pipefail
NOTES_FILE="$(mktemp)"
cat > "$NOTES_FILE" <<EOF
Published [@loopover/mcp v${RELEASE_VERSION}](https://www.npmjs.com/package/@loopover/mcp/v/${RELEASE_VERSION}) to npm with provenance.
Install:
\`\`\`sh
npm install -g @loopover/mcp@${RELEASE_VERSION}
\`\`\`
EOF
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --title "@loopover/mcp v${RELEASE_VERSION}" --notes-file "$NOTES_FILE"
else
gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --title "@loopover/mcp v${RELEASE_VERSION}" --notes-file "$NOTES_FILE" --verify-tag
fi