diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index ed7f8d4..16edc3a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cc", - "version": "1.0.2", + "version": "1.0.3", "description": "Claude Code Plugin for Codex. Delegate code reviews, investigations, and tracked tasks to Claude Code from inside Codex.", "author": { "name": "Sendbird, Inc.", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe20508..1933459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: cache: npm - run: npm ci - run: npm run check:version-sync + - run: npm run check:changelog - run: npm run lint - run: npm run typecheck - run: npm run test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d36dad3..3c5d8f1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,7 @@ jobs: registry-url: "https://registry.npmjs.org" - run: npm ci - run: npm run check:version-sync + - run: npm run check:changelog - run: npm run check - run: npm pack --dry-run - run: npm publish diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d23d9..d2f60ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v1.0.3 + +- Refresh the README opening copy and update the bundled visual assets for launch/readme presentation. +- Add a GitHub-friendly social preview asset under `assets/social-preview.{svg,png}`. +- Add a changelog release gate so `check`, `prepack`, CI, publish, and `npm version` all fail when the current package version is missing from `CHANGELOG.md`. + +## v1.0.2 + +- Add fallback `cc-*` skill and prompt wrappers only when Codex's official `plugin/install` path is unavailable. +- Remove stale managed fallback wrappers after official install succeeds again and during uninstall/self-cleanup. +- Clarify that marketplace-style installs which bypass the installer should run `$cc:setup` once to install hooks. +- Stabilize the concurrent polling integration assertion used in release verification. + ## v1.0.1 - Install and uninstall through Codex app-server when available, with safe fallback activation on unsupported builds. diff --git a/README.md b/README.md index ce38e22..d7a53c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# Claude Code plugin for Codex +# cc-plugin-codex -Use Claude Code from inside Codex for reviews, delegated implementation work, and tracked background jobs. +Use Claude Code from inside Codex. + +`cc-plugin-codex` is an open-source Codex plugin for Claude-powered review, rescue, and tracked background workflows. This repository is maintained by Sendbird and follows the overall shape of [openai/codex-plugin-cc](https://github.com/openai/codex-plugin-cc), but in the opposite direction: Codex hosts the plugin and delegates work to Claude Code. @@ -8,10 +10,8 @@ This repository is maintained by Sendbird and follows the overall shape of [open - `$cc:review` for a normal read-only Claude Code review - `$cc:adversarial-review` for a steerable challenge review -- `$cc:rescue`, `$cc:status`, `$cc:result`, and `$cc:cancel` for delegated Claude Code work and tracked jobs -- `$cc:setup` to verify Claude Code readiness, auto-install hooks when missing, and manage the review gate -- Optional stop-time review gating through Codex hooks -- One-shot unread background-result nudges on the next user prompt when a same-session background Claude job finished but has not been viewed yet +- `$cc:rescue`, `$cc:status`, `$cc:result`, and `$cc:cancel` to delegate work and manage tracked jobs +- `$cc:setup` to verify Claude Code readiness, hook installation, rescue-agent wiring, and review-gate state ## How This Differs From Upstream @@ -21,20 +21,19 @@ The goal is to stay close to the upstream OpenAI plugin's UX, but Claude Code an | --- | --- | --- | | Host app | Claude Code hosts the plugin | Codex hosts the plugin | | User command surface | Claude slash commands such as `/codex:review` | Codex skills such as `$cc:review` | -| Install lifecycle | Installed inside Claude's plugin flow | Installed through Codex's personal marketplace plus `plugin/install` / `plugin/uninstall` when available | -| Managed global state | Plugin-local runtime pieces inside Claude | Managed Codex hooks only; no global rescue agent | | Delegated runtime | Codex app-server + broker | Fresh `claude -p` subprocess per invocation | | Review gate subject | Reviews the previous Claude response before Claude stops | Reviews the previous Codex response before Codex stops | -| Rescue path | Plugin-local Codex rescue agent inside Claude | Built-in Codex forwarding subagent plus tracked Claude jobs | +| Rescue agent | Plugin-local Codex rescue agent inside Claude | Global `cc-rescue` agent in `~/.codex/agents` | | Model / effort flags | Codex model names and Codex effort controls | Claude model names and Claude effort values: `low`, `medium`, `high`, `max` | ## Where This Goes Further -- The stop-time review gate computes a turn baseline and skips Claude review entirely when the latest Codex turn made no net edits, which reduces unnecessary token spend. -- Nested helper sessions suppress stop-time review and unread-result prompts, so user-facing hooks stay attached to the top-level Codex thread instead of recursive child runs. +In addition to mirroring the upstream command surface, this repository adds a few implementation-focused optimizations: + +- The stop-time review gate computes a turn baseline and skips Claude review entirely when the most recent Codex turn made no net edits, which helps avoid unnecessary token spend. +- Nested helper sessions suppress stop-time review and unread-result prompts, so the review gate stays attached to the user-facing Codex thread instead of recursive child runs. - Background Claude jobs track unread/viewed state and session ownership, which makes `$cc:status`, `$cc:result`, and follow-up rescue flows safer for concurrent work. -- Because Codex and Claude Code background jobs cannot proactively create a new foreground user turn, the `UserPromptSubmit` hook injects a one-shot nudge on the next prompt when an unread same-session background result is waiting. -- The installer is idempotent and manages the personal marketplace entry, Codex hook installation, and Codex app-server install/uninstall path together. +- The installer is idempotent and manages the personal marketplace entry, hooks, and global `cc-rescue` registration together, so install and reinstall are a single step. ## Requirements @@ -46,20 +45,7 @@ The goal is to stay close to the upstream OpenAI plugin's UX, but Claude Code an ## Install -Choose either install path below. - -Both install flows: - -- stage the plugin under `~/.codex/plugins/cc` -- create or update `~/.agents/plugins/marketplace.json` -- enable `codex_hooks = true` -- install the managed Codex hooks used for review gate, session lifecycle, and unread background-result nudges -- ask Codex app-server to run `plugin/install` when that API is available -- fall back to config-based activation on older or unsupported Codex builds - -When Codex's official `plugin/install` API is unavailable, the installer also writes fallback `cc-*` wrappers into `~/.codex/skills` and `~/.codex/prompts` so `$cc:*` commands remain discoverable. - -Outside the plugin directory, the managed state is the hook entries in `~/.codex/hooks.json`, plus fallback `cc-*` wrappers in `~/.codex/skills` and `~/.codex/prompts` when the installer has to use the older compatibility path. This plugin no longer installs a global rescue agent under `~/.codex/agents`. +Choose either install path below. Both install the plugin into `~/.codex/plugins/cc`, create or update `~/.agents/plugins/marketplace.json`, enable `cc@local-plugins` in `~/.codex/config.toml`, enable `codex_hooks = true`, and install Codex hooks plus the global `cc-rescue` agent. ### npx @@ -75,7 +61,7 @@ curl -fsSL "https://raw.githubusercontent.com/sendbird/cc-plugin-codex/main/scri ### Update -Rerun either install command. The installer refreshes the staged plugin copy in place and keeps the marketplace entry and managed hooks consistent. +Rerun either install command. The installer is safe to run again and will refresh the installed copy in place. ```bash npx cc-plugin-codex update @@ -100,8 +86,6 @@ cd ~/.codex/plugins/cc node scripts/local-plugin-install.mjs install --plugin-root ~/.codex/plugins/cc ``` -## First Run - If Claude Code is not installed yet: ```bash @@ -115,40 +99,31 @@ Then run: $cc:setup ``` -`$cc:setup` is recommended, not required as an unlock step. - -If Claude Code is already installed and authenticated, the other `$cc:*` skills should work immediately after install. `$cc:setup` is useful when you want to: - -- verify Claude Code readiness -- auto-install missing hooks -- diagnose missing auth -- enable or disable the review gate - -If the plugin was installed through another marketplace path or copied into Codex without running this installer, run `$cc:setup` once so it can install the managed hooks. - After install, you should see: - the `$cc:*` skills listed in Codex -- managed hook entries in `~/.codex/hooks.json` - -## Background Results And Nudges +- the global `cc-rescue` agent installed under `~/.codex/agents/cc-rescue.toml` -Background Claude jobs can finish while the foreground Codex thread is idle. Neither Codex background jobs nor Claude Code background work can proactively initiate a new foreground turn on their own. +One simple first run is: -Because of that limitation, this plugin keeps background results in an unread state until the user views them. On the next `UserPromptSubmit` event in the same session, the unread-result hook injects a one-shot nudge if there is a finished unread Claude Code background job waiting. +```text +$cc:review --background +$cc:status +$cc:result +``` -That nudge points the user back to: +## Usage -- `$cc:status` to inspect current and recent jobs -- `$cc:result` to view the stored result and mark it as read +### `$cc:review` -This is why unread background-result handling is implemented as a Codex hook instead of trying to push a foreground message directly from a background worker. +Runs a normal Claude Code review on your current work. -## Usage +Use it when you want: -### `$cc:review` +- a review of your current uncommitted changes +- a review of your branch compared to a base branch like `main` -Runs a standard read-only Claude Code review on the current working tree or a branch diff. +It supports `--base `, `--scope `, `--wait`, `--background`, and `--model `. Examples: @@ -158,21 +133,42 @@ $cc:review --base main $cc:review --background ``` +This command is read-only. When run in the background, use `$cc:status` to check progress and `$cc:cancel` to stop it. + ### `$cc:adversarial-review` -Runs a more skeptical review that challenges design choices, assumptions, and tradeoffs. +Runs a steerable review that questions the chosen implementation and design. + +Use it when you want: + +- a review before shipping that challenges the direction, not just the code details +- review focused on design choices, tradeoffs, hidden assumptions, and alternative approaches +- pressure-testing around specific risk areas like auth, data loss, rollback, race conditions, or reliability + +It uses the same target selection as `$cc:review`, including `--base `, and also accepts extra focus text after the flags. Examples: ```text $cc:adversarial-review -$cc:adversarial-review --base main question the retry and rollback strategy -$cc:adversarial-review --background focus on race conditions +$cc:adversarial-review --base main challenge whether this was the right caching and retry design +$cc:adversarial-review --background look for race conditions and question the chosen approach ``` +This command is read-only. It does not fix code. + ### `$cc:rescue` -Delegates substantial work to Claude Code through the built-in Codex forwarding subagent and tracked-job runtime. +Hands a task to Claude Code through the global `cc-rescue` agent. + +Use it when you want Claude Code to: + +- investigate a bug +- try a fix +- continue a previous Claude task +- take a cheaper or faster pass with a smaller Claude model + +It supports `--background`, `--wait`, `--resume`, `--resume-last`, `--fresh`, `--write`, `--model `, `--effort `, and `--prompt-file `. Examples: @@ -180,6 +176,7 @@ Examples: $cc:rescue investigate why the tests started failing $cc:rescue fix the failing test with the smallest safe patch $cc:rescue --resume apply the top fix from the last run +$cc:rescue --model sonnet --effort medium investigate the flaky integration test $cc:rescue --background investigate the regression ``` @@ -187,6 +184,8 @@ $cc:rescue --background investigate the regression Shows running and recent Claude Code jobs for the current repository. +Examples: + ```text $cc:status $cc:status task-abc123 @@ -194,7 +193,15 @@ $cc:status task-abc123 ### `$cc:result` -Shows the stored final output for a finished Claude Code job. When available, it also includes the Claude session ID so you can reopen that run directly. +Shows the final stored Claude Code output for a finished job. + +When available, it also includes the Claude session ID so you can reopen that run directly with: + +```bash +claude --resume +``` + +Examples: ```text $cc:result @@ -205,6 +212,8 @@ $cc:result task-abc123 Cancels an active background Claude Code job. +Examples: + ```text $cc:cancel $cc:cancel task-abc123 @@ -212,16 +221,14 @@ $cc:cancel task-abc123 ### `$cc:setup` -Recommended readiness check. It does not unlock the plugin. +Checks whether Claude Code is installed and authenticated. -It verifies: +It also verifies: -- Claude Code availability and authentication - hook installation +- global `cc-rescue` registration - current review-gate state for this workspace -If hooks are missing, `$cc:setup` installs them and reruns the final readiness check automatically. - #### Enabling Review Gate ```text diff --git a/assets/cc-plugin-codex-logo.svg b/assets/cc-plugin-codex-logo.svg index 93dbe5a..cad590f 100644 --- a/assets/cc-plugin-codex-logo.svg +++ b/assets/cc-plugin-codex-logo.svg @@ -1,7 +1,62 @@ - - - - Claude Code - Plugin for Codex + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Claude Code + + Claude Code + + Plugin for Codex + + + diff --git a/assets/cc-plugin-codex-screenshot-1.png b/assets/cc-plugin-codex-screenshot-1.png index b8bf8a9..c097341 100644 Binary files a/assets/cc-plugin-codex-screenshot-1.png and b/assets/cc-plugin-codex-screenshot-1.png differ diff --git a/assets/cc-plugin-codex-screenshot-1.svg b/assets/cc-plugin-codex-screenshot-1.svg index 64abfb2..1331db0 100644 --- a/assets/cc-plugin-codex-screenshot-1.svg +++ b/assets/cc-plugin-codex-screenshot-1.svg @@ -1,34 +1,248 @@ - - - - - Claude Code Plugin for Codex - Use Claude Code through the $cc skill surface for reviews, rescue tasks, and tracked job management. - - - - $cc:review - Run a standard Claude Code review on the - current working tree or branch diff. - Args: --wait, --background, --base, --scope, --model - - - - $cc:adversarial-review - Challenge design choices, assumptions, and - tradeoffs before you ship. - Args: --wait, --background, --base, --scope, --model, [focus] - - - - $cc:rescue - Delegate substantial implementation or - debugging work to Claude Code. - Args: --background, --wait, --resume, --write, --model, --effort - - - Tracked jobs and follow-up skills - $cc:status shows live progress, $cc:result returns stored output, and $cc:cancel stops active jobs. - Marketplace-ready metadata includes Sendbird publisher info, legal links, install copy, and visual assets. - Repository: github.com/sendbird/cc-plugin-codex + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Claude Code + Plugin for Codex + + + Delegate code reviews, design challenges, and rescue tasks from Codex to Claude Code. + + + + + Interactive + + + + Write + + + + Apache-2.0 + + + + + + + + + + + + + + + + + + + + + + $cc:review + + Run a standard Claude Code review on + your current working tree or branch diff. + Read-only. Safe to run anytime. + + + + $ + $cc:review --base main + + + --wait --background --base --scope --model + + + + + + + + + + + + + + + $cc:adversarial-review + + Challenge design choices, assumptions, + and tradeoffs before you ship. + Steerable focus. Read-only. + + + $ + $cc:adversarial-review + + --wait --background --base --scope --model [focus] + + + + + + + + + + + + + $cc:rescue + + Delegate substantial implementation or + debugging work to Claude Code. + Writable. Tracked background jobs. + + + $ + $cc:rescue fix the failing test + + --background --wait --resume --write --model --effort + + + + + + + + + + + + + + + + Terminal — Typical Workflow + + + + + $cc:review --background + + + Background job started: + task-a1b2c3 + + + + $cc:status + + + task-a1b2c3 + review + running + 45s + + + + $cc:result + + + Review complete. + 3 findings + across 5 files. + + + + + Job management + + $cc:status + Live progress of running jobs + + $cc:result + Stored output from finished jobs + + $cc:cancel + Stop an active background job + + $cc:setup + Verify install and review-gate state + + + Built by + Sendbird + | + github.com/sendbird/cc-plugin-codex + Apache-2.0 diff --git a/assets/cc-plugin-codex.png b/assets/cc-plugin-codex.png index eb15ad2..b1fb43a 100644 Binary files a/assets/cc-plugin-codex.png and b/assets/cc-plugin-codex.png differ diff --git a/assets/cc-small.svg b/assets/cc-small.svg index 34709c7..92f7662 100644 --- a/assets/cc-small.svg +++ b/assets/cc-small.svg @@ -1,6 +1,39 @@ - - - - - Claude Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/social-preview.png b/assets/social-preview.png new file mode 100644 index 0000000..2d281ae Binary files /dev/null and b/assets/social-preview.png differ diff --git a/assets/social-preview.svg b/assets/social-preview.svg new file mode 100644 index 0000000..fd94b8b --- /dev/null +++ b/assets/social-preview.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Claude Code Plugin for Codex + + Claude Code Plugin for Codex + + + Delegate code reviews, design challenges, and rescue tasks + + + + + $cc:review + + + + $cc:adversarial-review + + + + $cc:rescue + + + + Sendbird + github.com/sendbird/cc-plugin-codex + diff --git a/package-lock.json b/package-lock.json index 21ad426..c1a8600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-plugin-codex", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-plugin-codex", - "version": "1.0.2", + "version": "1.0.3", "license": "Apache-2.0", "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/package.json b/package.json index 677cfb3..6f57d3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cc-plugin-codex", - "version": "1.0.2", + "version": "1.0.3", "description": "Claude Code Plugin for Codex by Sendbird", "type": "module", "author": { @@ -43,18 +43,19 @@ "scripts": { "lint": "eslint .", "typecheck": "tsc -p tsconfig.json", + "check:changelog": "node scripts/check-changelog.mjs", "check:version-sync": "node scripts/check-version-sync.mjs", "sync:plugin-version": "node scripts/sync-plugin-version.mjs", - "check": "npm run check:version-sync && npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e", + "check": "npm run check:version-sync && npm run check:changelog && npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e", "install:codex": "node scripts/installer-cli.mjs install", - "prepack": "npm run check:version-sync", + "prepack": "npm run check:version-sync && npm run check:changelog", "setup:git-hooks": "node scripts/setup-git-hooks.mjs", "test": "node --test tests/*.test.mjs", "test:integration": "node --test tests/integration/*.test.mjs", "test:e2e": "node --test tests/e2e/*.test.mjs", "uninstall:codex": "node scripts/installer-cli.mjs uninstall", "update:codex": "node scripts/installer-cli.mjs update", - "version": "npm run sync:plugin-version && npm run check:version-sync" + "version": "npm run sync:plugin-version && npm run check:version-sync && npm run check:changelog" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/scripts/check-changelog.mjs b/scripts/check-changelog.mjs new file mode 100644 index 0000000..994f5ea --- /dev/null +++ b/scripts/check-changelog.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +import { assertChangelogIncludesVersion } from "./lib/changelog.mjs"; + +try { + const version = assertChangelogIncludesVersion(); + process.stdout.write( + `Changelog OK: CHANGELOG.md contains a non-empty section for v${version}.\n` + ); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; +} diff --git a/scripts/lib/changelog.mjs b/scripts/lib/changelog.mjs new file mode 100644 index 0000000..65e172c --- /dev/null +++ b/scripts/lib/changelog.mjs @@ -0,0 +1,68 @@ +import fs from "node:fs"; +import path from "node:path"; + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +export function readCurrentVersion(repoRoot = process.cwd()) { + const packageJson = readJson(path.join(repoRoot, "package.json")); + if (!packageJson?.version || typeof packageJson.version !== "string") { + throw new Error("package.json is missing a string version field."); + } + return packageJson.version; +} + +export function readChangelog(repoRoot = process.cwd()) { + const changelogPath = path.join(repoRoot, "CHANGELOG.md"); + if (!fs.existsSync(changelogPath)) { + throw new Error("CHANGELOG.md does not exist."); + } + return fs.readFileSync(changelogPath, "utf8"); +} + +export function findVersionSection(version, changelogText) { + const normalized = String(changelogText ?? ""); + const headingPattern = new RegExp(`^##\\s+v?${version.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m"); + const headingMatch = headingPattern.exec(normalized); + if (!headingMatch) { + return null; + } + + const sectionStart = headingMatch.index + headingMatch[0].length; + const remainder = normalized.slice(sectionStart); + const nextHeadingMatch = /^\s*##\s+/m.exec(remainder); + const sectionBody = nextHeadingMatch + ? remainder.slice(0, nextHeadingMatch.index) + : remainder; + + return { + heading: headingMatch[0], + body: sectionBody, + }; +} + +export function assertChangelogIncludesVersion(repoRoot = process.cwd()) { + const version = readCurrentVersion(repoRoot); + const changelog = readChangelog(repoRoot); + const section = findVersionSection(version, changelog); + if (!section) { + throw new Error( + `CHANGELOG.md is missing a section for v${version}. Add a heading like \`## v${version}\` before releasing.` + ); + } + + const meaningfulLines = section.body + .split("\n") + .map((line) => line.trim()) + .filter((line) => line !== ""); + + const hasBullet = meaningfulLines.some((line) => /^[-*]\s+\S+/.test(line)); + if (!hasBullet) { + throw new Error( + `CHANGELOG.md section for v${version} exists but has no bullet items. Add at least one release note bullet before releasing.` + ); + } + + return version; +} diff --git a/tests/changelog.test.mjs b/tests/changelog.test.mjs new file mode 100644 index 0000000..bba76cd --- /dev/null +++ b/tests/changelog.test.mjs @@ -0,0 +1,71 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + assertChangelogIncludesVersion, + findVersionSection, + readCurrentVersion, +} from "../scripts/lib/changelog.mjs"; + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function createTempRepo({ version, changelog }) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-changelog-")); + writeJson(path.join(dir, "package.json"), { + name: "cc-plugin-codex", + version, + }); + fs.writeFileSync(path.join(dir, "CHANGELOG.md"), changelog, "utf8"); + return dir; +} + +describe("changelog gate", () => { + it("asserts the live repo changelog contains the current version", () => { + assert.equal(assertChangelogIncludesVersion(), readCurrentVersion()); + }); + + it("finds a matching version section", () => { + const section = findVersionSection( + "1.2.3", + "# Changelog\n\n## v1.2.3\n\n- hello\n\n## v1.2.2\n\n- older\n" + ); + assert.equal(section?.heading.trim(), "## v1.2.3"); + assert.match(section?.body ?? "", /- hello/); + }); + + it("rejects a missing version section", () => { + const dir = createTempRepo({ + version: "1.2.3", + changelog: "# Changelog\n\n## v1.2.2\n\n- older\n", + }); + try { + assert.throws( + () => assertChangelogIncludesVersion(dir), + /CHANGELOG\.md is missing a section for v1\.2\.3/ + ); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("rejects an empty version section", () => { + const dir = createTempRepo({ + version: "1.2.3", + changelog: "# Changelog\n\n## v1.2.3\n\nNo bullets here\n", + }); + try { + assert.throws( + () => assertChangelogIncludesVersion(dir), + /has no bullet items/ + ); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); +});