Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
62 changes: 36 additions & 26 deletions .github/workflows/publish-ghost-mcp-dev.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
# 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:
contents: read
id-token: write # required for OIDC trusted publishing

jobs:
publish-dev:
publish:
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -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

Expand All @@ -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
2 changes: 1 addition & 1 deletion ghost/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
118 changes: 51 additions & 67 deletions ghost/docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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@<version>` under the `dev` dist-tag only —
`@latest` is untouched, so nothing prod-facing changes.

Verify:

```bash
npm view ghost-blog-mcp dist-tags # dev: <version>
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@<version>` 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@<version> 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@<version> 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 <dev|latest>` 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.)
</content>
2 changes: 1 addition & 1 deletion ghost/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading