diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b310ebf..fd0ed87 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Parallel Codex workers inside Claude Code.", - "version": "0.5.1" + "version": "0.5.2" }, "plugins": [ { "name": "magic-codex", "source": "./plugin", "description": "Parallel Codex workers inside Claude Code — multi-agent orchestration with git worktree isolation, resumable sessions, and dual-model PR review.", - "version": "0.5.1", + "version": "0.5.2", "author": { "name": "Wenqing Yu" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index d206dc4..167c733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes documented here. Format follows [Keep a Changelog](https://keepachangelog.com/). +## [0.5.2] — 2026-04-29 + +Tooling-only release. + +### Added +- **`npm run release` script** (`scripts/release.mjs`) automates the full release procedure: validates clean `main`, refuses to clobber existing tags, verifies the CHANGELOG has an entry for the version in `package.json`, runs build (with the version-drift guard) + tests, creates the annotated tag, pushes it, and creates the GitHub Release with the CHANGELOG section as the body and `--latest` flag. Closes the gap from 0.5.0/0.5.1 where tags got pushed but GitHub Releases didn't, leaving the project's release page stuck at 0.3.9. +- **`npm run release:dry-run`** prints what would happen without mutating anything. +- **CONTRIBUTING.md** documents the maintainer release procedure end to end, calling out the five duplicated version literals (and that `plugin/.claude-plugin/plugin.json` is the one Claude Code's plugin loader actually reads). + +### Internal +- Backfilled GitHub Releases for v0.4.0 / v0.4.1 / v0.4.2 / v0.5.0 / v0.5.1 with their CHANGELOG bodies. + ## [0.5.1] — 2026-04-29 Docs-only release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5281ebc..2f2a679 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,37 @@ Open a GitHub issue with: Maintainers look for: clear test coverage, strict types, docs updated if behavior visible to users, no secret-bearing files committed. +## Cutting a release (maintainers) + +We use one tag per release, an annotated GitHub Release for each, and the version literal duplicated in 5 files (the build's drift guard catches drift). A small script handles the tag + push + GitHub Release flow so you don't forget any step. + +**Procedure:** + +1. **On a feature branch:** bump the version in `package.json` and let the build's drift guard tell you the other 4 files to bump in lockstep: + - `.claude-plugin/marketplace.json` (two version fields — `metadata.version` and `plugins[0].version`) + - `plugin/.claude-plugin/plugin.json` ← **easy to forget; this is the file Claude Code's loader actually reads** + - `src/index.ts` MCP banner + - `src/mcp/codex-client.ts` MCP banner +2. **Add a CHANGELOG entry** with `## [X.Y.Z] — YYYY-MM-DD` heading and the relevant `### Added / ### Fixed / ### Changed / ### Internal` subsections. +3. **PR + merge to `main`** (the release script refuses to run from a feature branch). +4. **From a clean `main` checked out at the merge commit:** + + ```bash + npm run release:dry-run # show what would happen + npm run release # tag, push, create GitHub Release, mark latest + ``` + +The script: +- Verifies you're on `main` and synced with origin +- Verifies the tag doesn't already exist on origin (refuses to clobber) +- Verifies the CHANGELOG has a section for the version in `package.json` +- Runs `npm run build` (drift guard validates all 5 version literals match) +- Runs `npm test` +- Creates an annotated tag, pushes it +- Creates the GitHub Release with the CHANGELOG section as the body, marks it `latest` + +If `gh release create` fails because the release already exists for a tag, delete it first (`gh release delete vX.Y.Z`) — the script intentionally won't overwrite. + ## Licensing of your contributions By submitting a pull request you agree that your contribution will be distributed under the project's current license ([PolyForm Noncommercial 1.0.0](./LICENSE)). You retain copyright to your contribution; you grant the project maintainer a license broad enough to redistribute the combined work under the project license and any future dual / commercial license we may offer. diff --git a/package.json b/package.json index e958380..c630782 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "magic-cc-codex-worker", - "version": "0.5.1", + "version": "0.5.2", "description": "Parallel Codex workers inside Claude Code — multi-agent orchestration.", "private": true, "license": "SEE LICENSE IN LICENSE", @@ -15,7 +15,9 @@ "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", - "clean": "rm -rf plugin/dist" + "clean": "rm -rf plugin/dist", + "release": "node scripts/release.mjs", + "release:dry-run": "node scripts/release.mjs --dry-run" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 828ad5d..5df3ada 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "magic-codex", - "version": "0.5.1", + "version": "0.5.2", "description": "Parallel Codex workers inside Claude Code — multi-agent orchestration with git worktree isolation, resumable sessions, and dual-model PR review.", "author": { "name": "Wenqing Yu", diff --git a/plugin/dist/index.js b/plugin/dist/index.js index 71e4823..9e06a49 100755 --- a/plugin/dist/index.js +++ b/plugin/dist/index.js @@ -27716,7 +27716,7 @@ var CodexChild = class { stderr: "pipe" }); this.client = new Client( - { name: "magic-codex", version: "0.5.1" }, + { name: "magic-codex", version: "0.5.2" }, { capabilities: {} } ); await this.client.connect(this.transport); @@ -28278,7 +28278,7 @@ async function main() { mfConventions }); const server = new Server( - { name: "magic-codex", version: "0.5.1" }, + { name: "magic-codex", version: "0.5.2" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ diff --git a/scripts/release.mjs b/scripts/release.mjs new file mode 100644 index 0000000..c388499 --- /dev/null +++ b/scripts/release.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +// Cut a release: validate, tag, push, and create the GitHub Release with +// notes extracted from CHANGELOG.md. Idempotent — re-running on a tag +// that already exists locally or on origin will skip the create step +// for that side and proceed; re-running after the GitHub Release also +// exists is a hard error (refuses to clobber). +// +// Usage: +// node scripts/release.mjs # cut release for whatever package.json says +// node scripts/release.mjs --dry-run # show what would happen, change nothing +// +// Preconditions (script enforces): +// 1. Working tree clean on `main`, up to date with origin. +// 2. package.json version not already tagged on origin. +// 3. CHANGELOG.md has a `## []` section (notes body). +// 4. `npm run build` passes — drift guard validates all 5 version +// literals match package.json. +// 5. `gh auth status` is authenticated. +// +// What it does: +// 1. Builds + tests (sanity check; same as CI would do). +// 2. Annotated tag `vX.Y.Z` with the CHANGELOG section's first prose +// line as the message (becomes the GitHub Release title via +// gh release create --verify-tag). +// 3. Pushes the tag to origin. +// 4. Creates the GitHub Release with --latest, body = CHANGELOG +// section. +// +// To make a future release: bump version literals (build will fail if +// any is missed), add a CHANGELOG entry, commit/PR/merge to main, then +// run `npm run release`. That's the whole procedure. + +import { readFile } from "node:fs/promises"; +import { execSync } from "node:child_process"; +import { writeFileSync, unlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dryRun = process.argv.includes("--dry-run"); + +function sh(cmd, opts = {}) { + if (dryRun && opts.mutating) { + console.log(`[dry-run] would run: ${cmd}`); + return ""; + } + return execSync(cmd, { stdio: opts.silent ? "pipe" : "inherit", encoding: "utf8" }); +} + +function shCapture(cmd) { + return execSync(cmd, { encoding: "utf8" }).trim(); +} + +function fail(msg) { + console.error(`\n✗ ${msg}\n`); + process.exit(1); +} + +// 1. Read package version +const pkg = JSON.parse(await readFile("package.json", "utf8")); +const version = pkg.version; +const tag = `v${version}`; +console.log(`→ release target: ${tag}`); + +// 2. Sanity: clean tree on main, synced +const branch = shCapture("git rev-parse --abbrev-ref HEAD"); +if (branch !== "main") { + fail(`must release from main; currently on ${branch}`); +} +const status = shCapture("git status --porcelain"); +if (status) { + fail(`working tree not clean:\n${status}`); +} +sh("git fetch origin main --quiet", { silent: true }); +const localHead = shCapture("git rev-parse HEAD"); +const remoteHead = shCapture("git rev-parse origin/main"); +if (localHead !== remoteHead) { + fail(`local main (${localHead.slice(0, 8)}) is not in sync with origin/main (${remoteHead.slice(0, 8)})`); +} + +// 3. Verify tag doesn't already exist on origin +const remoteTags = shCapture("git ls-remote --tags origin").split("\n"); +if (remoteTags.some((line) => line.endsWith(`refs/tags/${tag}`))) { + fail(`tag ${tag} already exists on origin — refusing to clobber. If you need to redo, delete it first: git push origin :refs/tags/${tag}`); +} + +// 4. Verify CHANGELOG has an entry for this version +const changelog = await readFile("CHANGELOG.md", "utf8"); +const sectionRe = new RegExp( + `^## \\[${version.replace(/\./g, "\\.")}\\][^\\n]*\\n([\\s\\S]*?)(?=^## \\[|\\Z)`, + "m", +); +const match = sectionRe.exec(changelog); +if (!match) { + fail(`CHANGELOG.md has no entry for [${version}]`); +} +const notesBody = match[1].trim() + "\n"; + +// 5. Derive a release title. Use the first non-blank, non-heading line +// from the CHANGELOG section as the summary. Fall back to "vX.Y.Z". +const summaryLine = notesBody + .split("\n") + .find((line) => line.trim() && !line.startsWith("#") && !line.startsWith("-") && !line.startsWith(">")); +const titleSuffix = summaryLine + ? summaryLine.replace(/^\*\*/, "").replace(/\*\*\.?$/, "").replace(/\.$/, "").slice(0, 80) + : ""; +const title = titleSuffix ? `${tag} — ${titleSuffix}` : tag; +console.log(`→ title: ${title}`); + +// 6. Run build (drift guard validates version literals match) +console.log("\n→ npm run build (drift guard)"); +sh("npm run build"); + +// 7. Run tests (sanity) +console.log("\n→ npm test"); +sh("npm test"); + +// 8. Verify gh is authenticated +try { + sh("gh auth status", { silent: true }); +} catch { + fail("gh CLI not authenticated; run `gh auth login`"); +} + +// 9. Tag locally (annotated, message = title) +console.log(`\n→ git tag -a ${tag}`); +sh(`git tag -a ${tag} -m "${title.replace(/"/g, '\\"')}"`, { mutating: true }); + +// 10. Push tag +console.log(`→ git push origin ${tag}`); +sh(`git push origin ${tag}`, { mutating: true }); + +// 11. Create GitHub Release. --verify-tag makes gh use the existing +// pushed tag; --latest marks this as the latest release on the +// project page. +const notesPath = join(tmpdir(), `release-notes-${tag}.md`); +writeFileSync(notesPath, notesBody, "utf8"); +try { + console.log(`→ gh release create ${tag}`); + if (!dryRun) { + sh( + `gh release create ${tag} --verify-tag --latest --title "${title.replace(/"/g, '\\"')}" --notes-file "${notesPath}"`, + { mutating: true }, + ); + } else { + console.log(`[dry-run] would gh release create ${tag} --notes-file ${notesPath}`); + } +} finally { + if (!dryRun) { + try { + unlinkSync(notesPath); + } catch { + // ignore + } + } +} + +console.log(`\n✓ released ${tag}`); +console.log(` https://github.com/wenqingyu/magic-cc-codex-worker/releases/tag/${tag}`); diff --git a/src/index.ts b/src/index.ts index cdf22f5..dc3ba47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -182,7 +182,7 @@ async function main() { }); const server = new Server( - { name: "magic-codex", version: "0.5.1" }, + { name: "magic-codex", version: "0.5.2" }, { capabilities: { tools: {} } }, ); diff --git a/src/mcp/codex-client.ts b/src/mcp/codex-client.ts index 28f8284..09cfcb6 100644 --- a/src/mcp/codex-client.ts +++ b/src/mcp/codex-client.ts @@ -52,7 +52,7 @@ export class CodexChild { stderr: "pipe", }); this.client = new Client( - { name: "magic-codex", version: "0.5.1" }, + { name: "magic-codex", version: "0.5.2" }, { capabilities: {} }, ); await this.client.connect(this.transport);