From a2b3590f2c80ba69392849defc0771235587dc8c Mon Sep 17 00:00:00 2001 From: Court Schuett Date: Fri, 3 Jul 2026 18:30:33 -0500 Subject: [PATCH] ci(ghost): tag-triggered publish with version-shape dist-tags; release 0.1.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt the @learning-with-court/cli release model: publish ghost-blog-mcp from CI on a pushed `ghost-mcp-v*` tag via OIDC, choosing the npm dist-tag by version shape — prerelease (0.1.5-dev.0) -> @dev, clean (0.1.5) -> @latest. Each version is published once to the right tag, so there's no manual `npm dist-tag add` and no OTP; shipping to prod is just tagging a clean version. Kept the workflow filename to preserve the npm trusted-publisher config. Bumps package + plugin + marketplace to 0.1.4 (lockstep) — the code-aware card/table splitting fix, cut as a clean release to @latest. Claude-Session: https://claude.ai/code/session_01BfjjpBQzjignajX8sZXVam --- .claude-plugin/marketplace.json | 2 +- .github/workflows/publish-ghost-mcp-dev.yml | 62 +++++----- ghost/.claude-plugin/plugin.json | 2 +- ghost/docs/releasing.md | 118 +++++++++----------- ghost/mcp-server/package.json | 2 +- 5 files changed, 90 insertions(+), 96 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bfa699f..edb723a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -44,7 +44,7 @@ "name": "ghost", "source": "./ghost", "description": "Write, revise, and push blog posts to a Ghost site from Claude Code — a Ghost Admin API MCP plus a plan→draft→revise→push skill set.", - "version": "0.1.2" + "version": "0.1.4" } ] } diff --git a/.github/workflows/publish-ghost-mcp-dev.yml b/.github/workflows/publish-ghost-mcp-dev.yml index 5e657a2..8848617 100644 --- a/.github/workflows/publish-ghost-mcp-dev.yml +++ b/.github/workflows/publish-ghost-mcp-dev.yml @@ -1,27 +1,29 @@ -# Publish ghost-blog-mcp to the `dev` npm dist-tag on every push to `dev` -# that changes the package (and only when its version is new). +# Publish ghost-blog-mcp to npm via OIDC trusted publishing, on a pushed tag +# matching `ghost-mcp-v*` (e.g. ghost-mcp-v0.1.4). The tag's version must match +# ghost/mcp-server/package.json. # -# Tokenless — uses npm OIDC Trusted Publishing (no NPM_TOKEN secret). This -# requires a ONE-TIME setup on npmjs.com: configure a trusted publisher for the -# `ghost-blog-mcp` package pointing at: +# The dist-tag is chosen by VERSION SHAPE (mirrors @learning-with-court/cli): +# - a prerelease (contains "-", e.g. 0.1.4-dev.0) publishes to `dev` +# - a clean release (e.g. 0.1.4) publishes to `latest` +# Each version is published exactly once, to the right tag — so there is no +# manual `npm dist-tag add` and no OTP. `@latest` == the current clean release +# tested via `@dev`; promoting is just tagging a clean version. +# +# Tokenless — npm OIDC Trusted Publishing (no NPM_TOKEN). This needs a ONE-TIME +# setup on npmjs.com: a trusted publisher for `ghost-blog-mcp` pointing at # repository: schuettc/claude-code-plugins # workflow: .github/workflows/publish-ghost-mcp-dev.yml -# Until that's configured, the publish step will fail with an auth error. -# -# Promotion to prod (@dev -> @latest) is a deliberate MANUAL step — see -# ghost/docs/releasing.md. It is intentionally not automated here. +# (Keeping this filename preserves that trust config.) -name: Publish ghost-blog-mcp (dev) +name: Publish ghost-blog-mcp on: push: - branches: [dev] - paths: - - "ghost/mcp-server/**" - - ".github/workflows/publish-ghost-mcp-dev.yml" + tags: + - "ghost-mcp-v*" concurrency: - group: publish-ghost-mcp-dev + group: publish-ghost-mcp cancel-in-progress: false permissions: @@ -29,7 +31,7 @@ permissions: id-token: write # required for OIDC trusted publishing jobs: - publish-dev: + publish: runs-on: ubuntu-latest defaults: run: @@ -47,6 +49,15 @@ jobs: - name: Use latest npm (OIDC trusted publishing needs npm >= 11.5) run: npm install -g npm@latest + - name: Verify tag matches package.json version + run: | + TAG_VERSION="${GITHUB_REF_NAME#ghost-mcp-v}" + PKG_VERSION=$(node -p "require('./package.json').version") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::tag ($TAG_VERSION) does not match package.json ($PKG_VERSION)" + exit 1 + fi + - name: Install run: npm ci @@ -56,17 +67,16 @@ jobs: npm run typecheck npm test - - name: Skip if this version is already published - id: guard + - name: Publish (dist-tag by version shape) run: | VERSION=$(node -p "require('./package.json').version") + case "$VERSION" in + *-*) NPM_TAG=dev ;; + *) NPM_TAG=latest ;; + esac if npm view "ghost-blog-mcp@$VERSION" version >/dev/null 2>&1; then - echo "ghost-blog-mcp@$VERSION already published — skipping publish." - echo "publish=false" >> "$GITHUB_OUTPUT" - else - echo "publish=true" >> "$GITHUB_OUTPUT" + echo "ghost-blog-mcp@$VERSION already published — skipping." + exit 0 fi - - - name: Publish to @dev - if: steps.guard.outputs.publish == 'true' - run: npm publish --tag dev --provenance + echo "Publishing ghost-blog-mcp@$VERSION to dist-tag '$NPM_TAG'" + npm publish --tag "$NPM_TAG" --provenance diff --git a/ghost/.claude-plugin/plugin.json b/ghost/.claude-plugin/plugin.json index e574455..4fe8b48 100644 --- a/ghost/.claude-plugin/plugin.json +++ b/ghost/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "0.1.3", + "version": "0.1.4", "description": "Write, revise, and push blog posts to a Ghost site from Claude Code — a Ghost Admin API MCP plus a plan→draft→revise→push skill set.", "author": { "name": "Court Schuett" diff --git a/ghost/docs/releasing.md b/ghost/docs/releasing.md index efd5a94..86830d5 100644 --- a/ghost/docs/releasing.md +++ b/ghost/docs/releasing.md @@ -7,99 +7,83 @@ The plugin has two versioned artifacts that move together: - the **plugin** itself (`ghost/.claude-plugin/plugin.json` + the `ghost` entry in `.claude-plugin/marketplace.json`). -Releases follow a **dev → prod** flow using npm **dist-tags**, mirroring the -repo's `feat → dev → main` promotion model: you publish to the `dev` tag, -test against it, then *promote the exact same artifact* to `latest`. +Releases are **CI-published on a pushed tag**, via OIDC trusted publishing — no +`NPM_TOKEN`, no local `npm publish`, no `npm dist-tag`, no OTP. The dist-tag is +chosen by **version shape**, mirroring `@learning-with-court/cli`: + +- a **prerelease** version (contains `-`, e.g. `0.1.5-dev.0`) publishes to + **`@dev`** — the test channel; +- a **clean** version (e.g. `0.1.5`) publishes to **`@latest`** — prod. + +Each version is published exactly once, to the right tag. There is no +"promote": shipping to prod is just tagging a clean version. `@latest` is +whatever clean release is current; `@dev` is the last prerelease. ``` -publish ──► ghost-blog-mcp@dev ──(test in ghost-site)──► promote ──► ghost-blog-mcp@latest +tag ghost-mcp-v0.1.5-dev.0 ─► CI ─► ghost-blog-mcp@dev ──(test in ghost-site) +tag ghost-mcp-v0.1.5 ─► CI ─► ghost-blog-mcp@latest (prod) ``` -- **`@dev`** — what `ghost-site` (and any dev consumer) points at to test a build. -- **`@latest`** — what the shipped plugin's `.mcp.json` points at; what prod gets. - The bundled `ghost/.mcp.json` always references `@latest`. Dev testing opts into -`@dev` via a **project-level `.mcp.json` override** in the consuming repo, so the -shipped file never has to flip between branches. See `enable-in-a-project.md`. +`@dev` (or an exact version) via a **project-level `.mcp.json` override** in the +consuming repo, so the shipped file never has to change. See `enable-in-a-project.md`. --- -## Dev release (publish to `@dev`) +## Dev build (publish to `@dev`) ```bash cd ghost/mcp-server -npm version patch # or minor / major — updates package.json + git tag -npm publish --tag dev # prepublishOnly runs build + typecheck + test first -``` - -This publishes `ghost-blog-mcp@` under the `dev` dist-tag only — -`@latest` is untouched, so nothing prod-facing changes. - -Verify: - -```bash -npm view ghost-blog-mcp dist-tags # dev: +npm version prerelease --preid dev --no-git-tag-version # e.g. 0.1.5-dev.0 +V=$(node -p "require('./package.json').version") +git commit -am "chore(ghost): ghost-blog-mcp $V" +git push +git tag "ghost-mcp-v$V" && git push origin "ghost-mcp-v$V" # triggers CI → @dev ``` -(A brand-new version can take a couple of minutes to be readable; the publish -itself is confirmed by the `+ ghost-blog-mcp@` line and by -`npm access list packages`.) - -Test it: in `ghost-site`, the project `.mcp.json` points at `ghost-blog-mcp@dev` -(see `enable-in-a-project.md`), then run `/ghost:setup-ghost` and exercise the -flow against a real Ghost site. +Test it: in `ghost-site`, point the project `.mcp.json` at `ghost-blog-mcp@$V` +(pinning the exact version beats `@dev`, which npx may cache), restart so the MCP +respawns, and exercise the flow against a real Ghost site. -## Promote to prod (move `@latest`) +## Prod release (publish to `@latest`) -Once the `@dev` build passes testing, promote the **exact same version** — no -rebuild, no republish: +Once the dev build passes, cut a **clean** version and take it through +`feat → dev → main`, then tag it from `main`: ```bash -npm dist-tag add ghost-blog-mcp@ latest +cd ghost/mcp-server +npm version patch --no-git-tag-version # or minor / major — clean version ``` -Now `@latest` and `@dev` point at the same version. Prod consumers (the shipped -plugin's `.mcp.json`) pick it up via `npx -y ghost-blog-mcp@latest`. - -## Bump the plugin to match +Bump the plugin in lockstep in **both**: -Keep the plugin version in lockstep with the npm release, in **both** files: +- `ghost/.claude-plugin/plugin.json` → `"version": "X.Y.Z"` +- `.claude-plugin/marketplace.json` → `"version": "X.Y.Z"` in the `ghost` entry -- `ghost/.claude-plugin/plugin.json` → `"version": "X.X.X"` -- `.claude-plugin/marketplace.json` → `"version": "X.X.X"` in the `ghost` entry - -Commit and push (feat → dev → main per the repo's promotion model): +Commit, promote `dev → main`, then tag the release commit on `main`: ```bash -git add ghost/.claude-plugin/plugin.json .claude-plugin/marketplace.json ghost/mcp-server/package.json -git commit -m "chore(ghost): release vX.X.X" -git push +git commit -am "chore(ghost): release X.Y.Z" +# open PR to dev, merge, promote dev → main +git tag "ghost-mcp-vX.Y.Z" && git push origin "ghost-mcp-vX.Y.Z" # CI → @latest ``` +CI verifies the tag matches `package.json`, builds/typechecks/tests, and +publishes to `@latest`. Prod consumers pick it up via `npx -y ghost-blog-mcp@latest`. + --- ## Automation -**Dev publish is automated** via `.github/workflows/publish-ghost-mcp-dev.yml` -(OIDC Trusted Publishing — no `NPM_TOKEN`). On every push to `dev` that touches -`ghost/mcp-server/**`, it builds/typechecks/tests and, **if the version in -`package.json` is new**, runs `npm publish --tag dev --provenance`. So the dev -release becomes: bump the version on a `feat/*` branch, merge to `dev`, and CI -publishes `@dev`. (The manual `npm publish --tag dev` above is the local -fallback; `--provenance` only works from CI.) - -**One-time setup (npmjs.com):** configure a *trusted publisher* for the -`ghost-blog-mcp` package → repository `schuettc/claude-code-plugins`, workflow -`.github/workflows/publish-ghost-mcp-dev.yml`. Until that's done the publish -step fails with an auth error. (npm CLI ≥ 11.5 is required for tokenless OIDC; -the workflow upgrades npm before publishing.) - -**Prod promotion stays manual — on purpose.** Moving `@latest` is the "ship to -prod" gate and should be a deliberate human action, so it is *not* automated: -run `npm dist-tag add ghost-blog-mcp@ latest` yourself (see "Promote to -prod" above). It also sidesteps a limitation: OIDC trusted publishing authorises -`npm publish`, not `dist-tag` operations, so automating promotion would require a -classic token — which we avoid. - -The repo's `/release` skill is currently scoped to `feature-workflow`; until it's -generalised to take a plugin parameter + npm-publish step, use this document. +Publishing is handled by `.github/workflows/publish-ghost-mcp.yml` +(filename on disk: `publish-ghost-mcp-dev.yml` — kept to preserve the trusted-publisher +config). It triggers on `ghost-mcp-v*` tags, verifies the tag matches +`package.json`, builds/tests, and runs `npm publish --tag ` with the +dist-tag chosen by version shape. Tokenless via OIDC. + +**One-time setup (npmjs.com):** a *trusted publisher* for `ghost-blog-mcp` → +repository `schuettc/claude-code-plugins`, workflow +`.github/workflows/publish-ghost-mcp-dev.yml`. Until that's set the publish step +fails with an auth error. (npm CLI ≥ 11.5 is required for tokenless OIDC; the +workflow upgrades npm first.) + diff --git a/ghost/mcp-server/package.json b/ghost/mcp-server/package.json index dd7cd69..7ed7c0f 100644 --- a/ghost/mcp-server/package.json +++ b/ghost/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "ghost-blog-mcp", - "version": "0.1.3", + "version": "0.1.4", "description": "MCP server for the Ghost Admin API", "type": "module", "repository": {