6dd0e951e6
* ci: publish accounted-mcp and gnubok-mcp to npm when their version changes accounted-mcp has never been published (npm view is E404) although every "connect Claude" doc says `npx -y accounted-mcp`, and gnubok-mcp is at 1.0.1 on the registry while the repo has carried 1.1.0 since #706. No workflow published to npm; this adds one. .github/workflows/npm-publish.yml runs on a push to main that touches a packages/*/package.json, and on workflow_dispatch (package: all or one, plus a dry_run that packs and validates without touching the registry). One matrix job per package: it fails first with a message naming the NPM_TOKEN secret if it is absent, then compares the package.json version with `npm view <name> versions` (E404 counts as "never published", any other failure is an error), skips when the version is already on the registry, and otherwise runs `npm publish --provenance --access public`. Permissions are contents: read plus id-token: write for the provenance attestation. Actions are pinned to the same SHAs as the sibling workflows. npm rejects a provenance attestation whose package.json repository.url does not match the source repository, and gnubok-mcp still pointed at erp-mafia/gnubok, so both repository fields now name erp-mafia/accounted in npm's canonical form with the monorepo directory. `npm pkg fix` normalised the bin paths, and accounted-mcp's index.mjs gets the executable bit gnubok-mcp's already had. Versions are not bumped. Both READMEs get a Releasing section: bump version, merge to main, the workflow publishes; the NPM_TOKEN repository secret must exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * fix(packages): keep the ./index.mjs bin form the package tests pin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(npm-publish): scope NPM_TOKEN to the publish step and keep the matrix static The token was job-level env, visible to checkout, setup-node and the version gate; it now reaches only npm publish. The matrix no longer interpolates the workflow_dispatch input into an expression: both packages always get a job and a Select step skips the one not requested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
195 lines
8.1 KiB
YAML
195 lines
8.1 KiB
YAML
name: Publish MCP bridges to npm
|
|
|
|
# Publishes the two stdio-to-HTTP MCP bridges, packages/accounted-mcp and
|
|
# packages/gnubok-mcp, to the public npm registry.
|
|
#
|
|
# The trigger is a version bump, not a git tag: a push to main that touches a
|
|
# packages/*/package.json runs one job per package, and each job publishes only
|
|
# if the version in its package.json is not already on the registry. A package
|
|
# whose version did not change is skipped, so a merge that bumps one bridge
|
|
# never republishes the other, and re-running a finished workflow is a no-op.
|
|
#
|
|
# Auth is the repository secret NPM_TOKEN, an npm granular access token with
|
|
# publish rights on both packages. A run without the secret fails at its first
|
|
# step with a message naming it, rather than inside `npm publish` with an opaque
|
|
# ENEEDAUTH. A token rather than OIDC trusted publishing because accounted-mcp
|
|
# has never been published, and npm cannot bind a trusted publisher to a package
|
|
# that does not exist yet.
|
|
#
|
|
# --provenance attaches a Sigstore attestation that ties the tarball to this
|
|
# workflow run and commit; id-token: write exists for that. The registry rejects
|
|
# the attestation unless package.json `repository.url` matches this repository,
|
|
# which is why both package.jsons point at erp-mafia/accounted.
|
|
#
|
|
# workflow_dispatch runs the same job on demand, optionally for one package (the
|
|
# other package's job is skipped by the Select step), and with dry_run to
|
|
# exercise the version gate and `npm publish --dry-run` without touching the
|
|
# registry. Dispatching from a branch is the way to test this file
|
|
# before merging it.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'packages/*/package.json'
|
|
workflow_dispatch:
|
|
inputs:
|
|
package:
|
|
description: Package to publish
|
|
type: choice
|
|
options: [all, accounted-mcp, gnubok-mcp]
|
|
default: all
|
|
dry_run:
|
|
description: Pack and validate only, do not publish
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# A dispatch overlapping a push could race to publish the same version; the
|
|
# loser would only fail with a confusing E403. Queue instead of cancelling.
|
|
concurrency:
|
|
group: npm-publish
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
publish:
|
|
name: Publish ${{ matrix.package }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
# OIDC token for the --provenance attestation.
|
|
id-token: write
|
|
strategy:
|
|
# The packages are independent: a failure in one must not cancel the other.
|
|
fail-fast: false
|
|
matrix:
|
|
# Static on purpose: both packages always get a job. On push the
|
|
# version gate skips the one that did not change; on dispatch the
|
|
# Select step skips the one that was not requested. A matrix built
|
|
# from the dispatch input would put workflow input text into an
|
|
# expression, which is the shape injection scanners flag.
|
|
package: [accounted-mcp, gnubok-mcp]
|
|
env:
|
|
DRY_RUN: ${{ inputs.dry_run == true }}
|
|
PACKAGE_DIR: packages/${{ matrix.package }}
|
|
|
|
steps:
|
|
- name: Select package
|
|
id: select
|
|
env:
|
|
REQUESTED: ${{ github.event_name == 'push' && 'all' || inputs.package }}
|
|
PACKAGE: ${{ matrix.package }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$REQUESTED" = "all" ] || [ "$REQUESTED" = "$PACKAGE" ]; then
|
|
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Skipping $PACKAGE: dispatch requested $REQUESTED."
|
|
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Require the NPM_TOKEN secret
|
|
# Before checkout, so a missing secret is the first line of the log.
|
|
# Only the presence of the secret is checked here; the token itself is
|
|
# exposed to the publish step alone.
|
|
if: steps.select.outputs.run == 'true'
|
|
env:
|
|
NPM_TOKEN_SET: ${{ secrets.NPM_TOKEN != '' }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$DRY_RUN" = "true" ]; then
|
|
echo "Dry run: NPM_TOKEN is not required."
|
|
exit 0
|
|
fi
|
|
if [ "$NPM_TOKEN_SET" != "true" ]; then
|
|
echo "::error::Repository secret NPM_TOKEN is not set. Create an npm granular access token with read and write access to accounted-mcp and gnubok-mcp (see the Releasing section in packages/*/README.md) and add it under Settings > Secrets and variables > Actions as NPM_TOKEN."
|
|
exit 1
|
|
fi
|
|
echo "NPM_TOKEN is set."
|
|
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
if: steps.select.outputs.run == 'true'
|
|
with:
|
|
# Nothing here pushes over git; the only credential this job needs is
|
|
# the npm token, and that never touches the checkout.
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
|
if: steps.select.outputs.run == 'true'
|
|
with:
|
|
node-version: 22
|
|
# Writes an .npmrc that reads the auth token from NODE_AUTH_TOKEN.
|
|
registry-url: https://registry.npmjs.org
|
|
|
|
- name: Compare package.json version with the registry
|
|
id: gate
|
|
if: steps.select.outputs.run == 'true'
|
|
working-directory: ${{ env.PACKAGE_DIR }}
|
|
run: |
|
|
set -euo pipefail
|
|
NAME=$(node -p "require('./package.json').name")
|
|
VERSION=$(node -p "require('./package.json').version")
|
|
|
|
# `npm view` exits 1 with an E404 body when the package has never
|
|
# been published. That is the first-release case and counts as
|
|
# "nothing on the registry". Any other failure (network, registry
|
|
# outage, bad token) is an error: assuming "not published" there
|
|
# would only move the failure into `npm publish`.
|
|
set +e
|
|
VIEW=$(npm view "$NAME" versions --json 2>&1)
|
|
STATUS=$?
|
|
set -e
|
|
if [ "$STATUS" -ne 0 ]; then
|
|
if grep -q 'E404' <<< "$VIEW"; then
|
|
echo "$NAME has never been published (E404): $VERSION would be its first release."
|
|
VIEW='[]'
|
|
else
|
|
echo "::error::npm view $NAME failed (exit $STATUS)."
|
|
echo "$VIEW"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# `npm view <name> versions --json` prints a bare string, not a
|
|
# one-element array, when exactly one version exists.
|
|
ON_REGISTRY=$(VIEW="$VIEW" VERSION="$VERSION" node -e '
|
|
const raw = JSON.parse(process.env.VIEW);
|
|
const list = Array.isArray(raw) ? raw : [raw];
|
|
console.error("Versions on registry: " + (list.length ? list.join(", ") : "(none)"));
|
|
process.stdout.write(list.includes(process.env.VERSION) ? "yes" : "no");
|
|
')
|
|
|
|
if [ "$ON_REGISTRY" = "yes" ]; then
|
|
echo "Skipping: $NAME@$VERSION is already on the registry."
|
|
echo "publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "- \`$NAME@$VERSION\`: already on the registry, skipped" >> "$GITHUB_STEP_SUMMARY"
|
|
else
|
|
echo "Publishing: $NAME@$VERSION is not on the registry."
|
|
echo "publish=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Publish to npm
|
|
if: steps.select.outputs.run == 'true' && steps.gate.outputs.publish == 'true'
|
|
working-directory: ${{ env.PACKAGE_DIR }}
|
|
env:
|
|
# The only step that sees the token.
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
NAME: ${{ steps.gate.outputs.name }}
|
|
VERSION: ${{ steps.gate.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$DRY_RUN" = "true" ]; then
|
|
# --dry-run packs and validates but never contacts the registry, so
|
|
# it also runs without a token.
|
|
npm publish --dry-run --access public
|
|
echo "- \`$NAME@$VERSION\`: dry run, not published" >> "$GITHUB_STEP_SUMMARY"
|
|
exit 0
|
|
fi
|
|
npm publish --provenance --access public
|
|
echo "- \`$NAME@$VERSION\`: published, https://www.npmjs.com/package/$NAME/v/$VERSION" >> "$GITHUB_STEP_SUMMARY"
|