diff --git a/CLAUDE.md b/CLAUDE.md index 8ba010733..e47e4762d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,12 +118,14 @@ semantic-release. Two repo-specific rules: reject an unscoped revert in that form — keep reverts conventional and scoped by hand, and know that a scoped revert releases nothing: ship a rollback as `fix()` (see VERSIONING.md for why). -- **Packages release independently, keyed off the scope**: `feat(bulma-ui)` bumps only - `@allxsmith/bestax-bulma`; `fix(create-bestax)` bumps only `create-bestax`. The - `releaseRules` in each package's `release.config.js` are the source of truth. +- **Packages release independently, keyed off the scope**: `fix(create-bestax)` bumps only + `create-bestax`. The exception is bestax-mcp, which also cuts a patch with every + `@allxsmith/bestax-bulma` release because its index documents the library (VERSIONING.md, + "bestax-mcp releases with bestax-bulma"). The `releaseRules` in each package's + `release.config.js` are the source of truth. ``` -feat(bulma-ui): add Collapse component → minor release of bulma-ui only +feat(bulma-ui): add Collapse component → minor release of bulma-ui, patch of bestax-mcp fix(create-bestax): handle missing TTY → patch release of create-bestax only docs: fix typo in contributing guide → no release; scope optional ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8deddabba..9d817d4cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -380,7 +380,7 @@ The short version for contributors: We use [Semantic Release](https://semantic-release.gitbook.io/) to automate publishing of every package to npm: `bulma-ui` as [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma), plus [`create-bestax`](https://www.npmjs.com/package/create-bestax), [`bestax-migrate`](https://www.npmjs.com/package/bestax-migrate), [`bestax-mcp`](https://www.npmjs.com/package/bestax-mcp) and `eslint-plugin` as [`@allxsmith/eslint-plugin-bestax`](https://www.npmjs.com/package/@allxsmith/eslint-plugin-bestax). - Use [Conventional Commits](https://www.conventionalcommits.org/) to trigger releases — see [Commit Message Guidelines](#commit-message-guidelines). -- **Packages version and release independently, keyed off the commit scope** — `feat(bulma-ui)` releases only bestax-bulma. See [`VERSIONING.md`](./VERSIONING.md). +- **Packages version and release independently, keyed off the commit scope**: `fix(create-bestax)` releases only create-bestax, and the one exception is the bestax-mcp patch that follows every bestax-bulma release. See [`VERSIONING.md`](./VERSIONING.md). - Only the `main` branch is published. ### npm authentication (OIDC trusted publishing) diff --git a/VERSIONING.md b/VERSIONING.md index d217f8077..bea75f9b5 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -2,7 +2,8 @@ `@allxsmith/bestax-bulma`, `create-bestax`, `bestax-migrate`, `bestax-mcp`, and `@allxsmith/eslint-plugin-bestax` are versioned -and released **independently**. Each package releases only when a commit is scoped to it — the +and released **independently**. Each package releases when a commit is scoped to it, except that +bestax-mcp also releases with bestax-bulma ([below](#bestax-mcp-releases-with-bestax-bulma)). The version numbers are unrelated (e.g. bestax-bulma 5.x alongside create-bestax 3.x). The source of truth is the `releaseRules` in each package's semantic-release config: @@ -14,21 +15,21 @@ The source of truth is the `releaseRules` in each package's semantic-release con ## Release Rules -A commit releases **only** the package its scope names. Representative examples — the same -`feat`/`fix`/`perf`/`refactor`/`style` and `BREAKING CHANGE:` rules apply to every package -through its own scope: - -| Commit | bestax-bulma | create-bestax | bestax-migrate | bestax-mcp | eslint-plugin | -| ----------------------------------------------------------------- | ------------ | ------------- | -------------- | ---------- | ------------- | -| `feat(bulma-ui): …` | minor | — | — | — | — | -| `fix(bulma-ui): …` | patch | — | — | — | — | -| `perf/refactor/style(bulma-ui): …` | patch | — | — | — | — | -| `feat(create-bestax): …` | — | minor | — | — | — | -| `fix(bestax-migrate): …` | — | — | patch | — | — | -| `feat(bestax-mcp): …` | — | — | — | minor | — | -| `feat(eslint-plugin): …` | — | — | — | — | minor | -| `feat(bulma-ui): …` + `BREAKING CHANGE:` footer | major | — | — | — | — | -| `docs: …`, `chore: …`, `ci: …`, `test: …`, `build: …` (any scope) | — | — | — | — | — | +A commit releases **only** the package its scope names, apart from the bestax-mcp patch that +follows every bestax-bulma release ([below](#bestax-mcp-releases-with-bestax-bulma)). Representative +examples follow, and the same `feat`/`fix`/`perf`/`refactor`/`style` and `BREAKING CHANGE:` rules +apply to every package through its own scope: + +| Commit | bestax-bulma | create-bestax | bestax-migrate | bestax-mcp | eslint-plugin | +| ----------------------------------------------- | ------------ | ------------- | -------------- | ---------- | ------------- | +| `feat(bulma-ui): …` | minor | — | — | patch | — | +| `fix(bulma-ui): …` | patch | — | — | patch | — | +| `perf/refactor/style(bulma-ui): …` | patch | — | — | patch | — | +| `feat(create-bestax): …` | — | minor | — | — | — | +| `fix(bestax-migrate): …` | — | — | patch | — | — | +| `feat(bestax-mcp): …` | — | — | — | minor | — | +| `feat(eslint-plugin): …` | — | — | — | — | minor | +| `feat(bulma-ui): …` + `BREAKING CHANGE:` footer | major | — | — | patch | — | Notes: @@ -53,6 +54,15 @@ Notes: keep reverts in conventional, scoped form, and don't expect them to publish on their own. - A commit scoped to `docs` never releases any package. +### bestax-mcp releases with bestax-bulma + +bestax-mcp ships an index of the library, and a new index only reaches npm when bestax-mcp +releases. So every commit that releases bestax-bulma releases bestax-mcp too, in the same run, and +the published index keeps up with the library (#932). That release is a patch whatever +bestax-bulma's own bump, a major included. The rules, the reasons for them and what they do to +bestax-mcp's changelog are in [`bestax-mcp/release.config.js`](./bestax-mcp/release.config.js), +and `scripts/release-rules.test.mjs` holds them to bulma-ui's. + ## Tags & Changelogs Each package tags and logs its own releases: @@ -83,7 +93,8 @@ On merge to `main`, CI (`.github/workflows/ci.yml`) runs semantic-release in eac - Note the ordering, because it decides what a failed publish costs: semantic-release runs **every** `prepare` step — including the release commit and tag — before **any** `publish` step. A publish that fails leaves the commit and tag behind, and that version is spent. -3. A push may release any subset of the packages — they never bump each other. +3. A push may release any subset of the packages. None bumps another, apart from bestax-mcp + following bestax-bulma. For `bestax-mcp`, the GitHub release then triggers `.github/workflows/mcp-registry.yml`, which lists that version in the official MCP Registry. The listing is metadata pointing at the npm @@ -138,7 +149,7 @@ are in [`CONTRIBUTING.md`](./CONTRIBUTING.md#codemod-registry-bestax-migrate). ```bash git commit -m "feat(bulma-ui): add new Modal variant" -# → bestax-bulma minor bump; create-bestax untouched +# → bestax-bulma minor bump, bestax-mcp patch; create-bestax untouched git commit -m "fix(create-bestax): correct template scaffolding issue" # → create-bestax patch bump; bestax-bulma untouched @@ -147,7 +158,7 @@ git commit -m "docs: update README" # → no release git commit -m "feat(bulma-ui): rename Theme props" -m "BREAKING CHANGE: bulmaVars renamed to vars" -# → bestax-bulma major bump +# → bestax-bulma major bump, bestax-mcp patch ``` ## History diff --git a/bestax-mcp/CLAUDE.md b/bestax-mcp/CLAUDE.md index 9810832ae..f2eb6848a 100644 --- a/bestax-mcp/CLAUDE.md +++ b/bestax-mcp/CLAUDE.md @@ -109,8 +109,9 @@ test that stubs it proves nothing about what ships. ## Releases -Independent semantic-release keyed off the `bestax-mcp` commit scope -(`release.config.js`, tag `bestax-mcp@x.y.z`). It publishes with +Released by semantic-release, keyed off the `bestax-mcp` commit scope (`release.config.js`, tag +`bestax-mcp@x.y.z`), and patched with every bulma-ui release so the index ships with the +library (VERSIONING.md, "bestax-mcp releases with bestax-bulma"). It publishes with `pnpm publish`, like every package here (#532) — the command, its flags, and the ways that publish fails quietly are documented in `VERSIONING.md` and `scripts/lib/pnpm-publish.mjs`. diff --git a/bestax-mcp/release.config.js b/bestax-mcp/release.config.js index 8ca9dbe16..d227466dd 100644 --- a/bestax-mcp/release.config.js +++ b/bestax-mcp/release.config.js @@ -8,21 +8,27 @@ export default { '@semantic-release/commit-analyzer', { preset: 'angular', - // commit-analyzer evaluates EVERY matching rule and a matching - // `release: false` always wins (its priority index is -1), so rules - // are not first-match-wins. Values are micromatch globs, hence the - // negated glob below; a `release: false` rule must never be able to - // match a commit that should release. + // commit-analyzer checks every rule against each commit, in this + // order, so rules are not first-match-wins. A matching + // `release: false` replaces whatever matched above it, a release type + // matching after a `false` replaces the `false`, the higher of two + // release types stays, and `major` ends the walk. So the place of a + // rule in this list can decide a commit, which the bulma-ui group at + // the end depends on. Values are micromatch globs, hence the negated + // glob below. releaseRules: [ - // Any commit scoped to something other than bestax-mcp never - // releases this package. Without this, commits matching no rule - // fall back to the angular defaults and would bump this package - // too. (Does not match unscoped commits — a scope on releasing - // types is enforced by commitlint instead.) - { scope: '!(bestax-mcp)', release: false }, + // Any commit scoped to something other than bestax-mcp or bulma-ui + // never releases this package. Without this, commits matching no + // rule fall back to the angular defaults and would bump this + // package too. bulma-ui is left out because its commits can release + // this package (the group at the end), so this `false` never meets + // them and its place in the list decides nothing. (Does not match + // unscoped commits: a scope on releasing types is enforced by + // commitlint instead.) + { scope: '!(bestax-mcp|bulma-ui)', release: false }, // bestax-mcp releases. Breaking changes need a "BREAKING CHANGE:" - // footer — the angular preset does not parse "feat(x)!:". + // footer, since the angular preset does not parse "feat(x)!:". { breaking: true, scope: 'bestax-mcp', release: 'major' }, { type: 'feat', scope: 'bestax-mcp', release: 'minor' }, { type: 'fix', scope: 'bestax-mcp', release: 'patch' }, @@ -30,15 +36,49 @@ export default { { type: 'refactor', scope: 'bestax-mcp', release: 'patch' }, { type: 'style', scope: 'bestax-mcp', release: 'patch' }, - // Non-releasing types, any scope (safe: never release-triggering) + // Types that release nothing, in any scope, unless a + // "BREAKING CHANGE:" footer brings in a breaking rule: the + // bestax-mcp `major` above ends the walk before these, and the + // bulma-ui rule below comes after them. { type: 'docs', release: false }, { type: 'test', release: false }, { type: 'chore', release: false }, { type: 'ci', release: false }, { type: 'build', release: false }, + + // Every commit that releases bulma-ui releases this package too, + // as a patch (#932). data/ indexes the library, so it ships when + // the library does: the release job regenerates it after bulma-ui's + // release and before this one, and this one packs it. These are + // the commits bulma-ui/release.config.js releases on, and + // scripts/release-rules.test.mjs holds the two lists together. + // + // Patch whatever bulma-ui's own bump, a major included. The + // server's tools and prompts stay as they were and only what they + // answer from moves, and a major here would leave every + // `bestax-mcp@1` install and listing behind. + // + // Last on purpose, for the breaking rule. bulma-ui releases a major + // for a breaking commit of any type, `build` and `chore` included, + // because its `major` ends the walk before the type's + // `release: false`. A patch ends nothing, so above the + // non-releasing types the breaking rule would lose to that `false`, + // and a library major would ship no index. + { type: 'feat', scope: 'bulma-ui', release: 'patch' }, + { type: 'fix', scope: 'bulma-ui', release: 'patch' }, + { type: 'perf', scope: 'bulma-ui', release: 'patch' }, + { type: 'refactor', scope: 'bulma-ui', release: 'patch' }, + { type: 'style', scope: 'bulma-ui', release: 'patch' }, + { breaking: true, scope: 'bulma-ui', release: 'patch' }, ], }, ], + // The notes list the feat, fix and perf commits since the last + // bestax-mcp tag whatever their scope, so a release the rules above take + // from bulma-ui lists bulma-ui's commits. Left that way: they are what + // the new index documents. A breaking one lands under BREAKING CHANGES + // in a patch, and that heading then describes the library, not this + // server. '@semantic-release/release-notes-generator', [ '@semantic-release/changelog', diff --git a/bestax-migrate/release.config.js b/bestax-migrate/release.config.js index d3f2f9567..e4f766b93 100644 --- a/bestax-migrate/release.config.js +++ b/bestax-migrate/release.config.js @@ -8,11 +8,9 @@ export default { '@semantic-release/commit-analyzer', { preset: 'angular', - // commit-analyzer evaluates EVERY matching rule and a matching - // `release: false` always wins (its priority index is -1), so rules - // are not first-match-wins. Values are micromatch globs, hence the - // negated glob below; a `release: false` rule must never be able to - // match a commit that should release. + // commit-analyzer evaluates EVERY matching rule, so rules are not + // first-match-wins. Values are micromatch globs, hence the negated + // glob below. releaseRules: [ // Any commit scoped to something other than bestax-migrate never // releases this package. Without this, commits matching no rule diff --git a/bulma-ui/CLAUDE.md b/bulma-ui/CLAUDE.md index 02161b257..931b1c5ba 100644 --- a/bulma-ui/CLAUDE.md +++ b/bulma-ui/CLAUDE.md @@ -153,7 +153,9 @@ These keep it working, and each of them failed once: ## Releases Independent semantic-release keyed off the `bulma-ui` commit scope -(`release.config.js`, tag `@allxsmith/bestax-bulma@x.y.z`). It publishes with +(`release.config.js`, tag `@allxsmith/bestax-bulma@x.y.z`). Each release also patches +bestax-mcp, so its index ships with the library (VERSIONING.md, "bestax-mcp releases with +bestax-bulma"). It publishes with `pnpm publish`, like every package here (#532) — the command, its flags, and the ways that publish fails quietly are documented in `VERSIONING.md` and `scripts/lib/pnpm-publish.mjs`. diff --git a/bulma-ui/release.config.js b/bulma-ui/release.config.js index 4fbe9babf..020ff6834 100644 --- a/bulma-ui/release.config.js +++ b/bulma-ui/release.config.js @@ -8,11 +8,9 @@ export default { '@semantic-release/commit-analyzer', { preset: 'angular', - // commit-analyzer evaluates EVERY matching rule and a matching - // `release: false` always wins (its priority index is -1), so rules - // are not first-match-wins. Values are micromatch globs, hence the - // negated glob below; a `release: false` rule must never be able to - // match a commit that should release. + // commit-analyzer evaluates EVERY matching rule, so rules are not + // first-match-wins. Values are micromatch globs, hence the negated + // glob below. releaseRules: [ // Any commit scoped to something other than bulma-ui never // releases this package. (Does not match unscoped commits — a diff --git a/create-bestax/release.config.js b/create-bestax/release.config.js index 2d076146b..92a4ca8eb 100644 --- a/create-bestax/release.config.js +++ b/create-bestax/release.config.js @@ -8,11 +8,9 @@ export default { '@semantic-release/commit-analyzer', { preset: 'angular', - // commit-analyzer evaluates EVERY matching rule and a matching - // `release: false` always wins (its priority index is -1), so rules - // are not first-match-wins. Values are micromatch globs, hence the - // negated glob below; a `release: false` rule must never be able to - // match a commit that should release. + // commit-analyzer evaluates EVERY matching rule, so rules are not + // first-match-wins. Values are micromatch globs, hence the negated + // glob below. releaseRules: [ // Any commit scoped to something other than create-bestax never // releases this package. Without this, commits matching no rule diff --git a/docs/docs/guides/getting-started/contributing.md b/docs/docs/guides/getting-started/contributing.md index 12ac1fe15..3bf0cc6ab 100644 --- a/docs/docs/guides/getting-started/contributing.md +++ b/docs/docs/guides/getting-started/contributing.md @@ -164,14 +164,14 @@ and `scripts/require-pnpm-publish.mjs`. 2. Add/update tests (bulma-ui holds 99% coverage; every other jest package 95%, 78% branches) and Storybook stories for UI changes. 3. Run `pnpm all`, then open a PR targeting `main`. -Commits follow [Conventional Commits](https://www.conventionalcommits.org/) — the type and scope -drive [semantic-release](https://semantic-release.gitbook.io/). Releasing types (`feat`, `fix`, -`perf`, `refactor`, `style`, `revert`) must carry one of the scopes in `RELEASE_SCOPES` -(`bulma-ui`, `docs`, `create-bestax`, `bestax-migrate`, `bestax-mcp`, `eslint-plugin`); `docs`, `chore`, `ci`, -`build`, and `test` don't publish — note `docs` is both a valid scope and a non-releasing -type, so `docs(bulma-ui):` and `docs:` alike publish nothing. A scoped `revert(scope):` -commit releases **nothing** — the only revert rule is commit-analyzer's default, and its -`revertPattern` matches a `Revert "…"` or bare `revert: …` header (never the parenthesized +Commits follow [Conventional Commits](https://www.conventionalcommits.org/), and the type and scope +drive [semantic-release](https://semantic-release.gitbook.io/). Every bestax-bulma release also +patches bestax-mcp, so its index ships with the library +([`VERSIONING.md`](https://github.com/allxsmith/bestax/blob/main/VERSIONING.md) has the rules). +Releasing types (`feat`, `fix`, `perf`, `refactor`, `style`, `revert`) must carry one of the +scopes in `RELEASE_SCOPES` (`bulma-ui`, `docs`, `create-bestax`, `bestax-migrate`, `bestax-mcp`, +`eslint-plugin`). A scoped `revert(scope):` commit releases **nothing** — the only revert rule +is commit-analyzer's default, and its `revertPattern` matches a `Revert "…"` or bare `revert: …` header (never the parenthesized `revert(scope):` form) followed by a `This reverts commit ` body; ship a rollback with a follow-up `fix(scope):` commit. An unscoped `revert: …` is stopped by commitlint's scope rule, but git's own `Revert "…"` form is the real hazard: commitlint's default ignore rules let it diff --git a/eslint-plugin/release.config.js b/eslint-plugin/release.config.js index 4864a52bb..7f31b74d2 100644 --- a/eslint-plugin/release.config.js +++ b/eslint-plugin/release.config.js @@ -8,11 +8,9 @@ export default { '@semantic-release/commit-analyzer', { preset: 'angular', - // commit-analyzer evaluates EVERY matching rule and a matching - // `release: false` always wins (its priority index is -1), so rules - // are not first-match-wins. Values are micromatch globs, hence the - // negated glob below; a `release: false` rule must never be able to - // match a commit that should release. + // commit-analyzer evaluates EVERY matching rule, so rules are not + // first-match-wins. Values are micromatch globs, hence the negated + // glob below. releaseRules: [ // Any commit scoped to something other than eslint-plugin never // releases this package. Without this, commits matching no rule diff --git a/scripts/release-rules.test.mjs b/scripts/release-rules.test.mjs new file mode 100644 index 000000000..66ec5c59d --- /dev/null +++ b/scripts/release-rules.test.mjs @@ -0,0 +1,148 @@ +/** + * Holds bestax-mcp's release rules to bulma-ui's (#932). + * + * bestax-mcp ships an index of the library, which the release job regenerates + * after bulma-ui's release and before bestax-mcp's. That only reaches npm when + * bestax-mcp releases too, so its config releases a patch on every commit that + * releases bulma-ui. Nothing else notices if that stops: both packages keep + * releasing on their own commits while the published index falls behind the + * library again, which is what #932 found. + * + * So these run commits through commit-analyzer itself rather than reading the + * rules. What a rule list does depends on how the analyzer walks it, and the + * bestax-mcp config leans on that walk: its bulma-ui breaking rule sits last + * for a reason a reading would not check. The analyzer is the copy + * semantic-release loads, resolved through semantic-release the way its plugin + * loader resolves it, so an upgrade that changes the walk shows up here. + */ +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +import conventional from '@commitlint/config-conventional'; + +import { pluginOptions } from './lib/release-config.mjs'; + +const ANALYZER = '@semantic-release/commit-analyzer'; +const REPO = fileURLToPath(new URL('..', import.meta.url)); + +const semanticRelease = createRequire(import.meta.url).resolve( + 'semantic-release' +); +const { analyzeCommits } = await import( + pathToFileURL(createRequire(semanticRelease).resolve(ANALYZER)).href +); + +const LIBRARY = await pluginOptions('bulma-ui', ANALYZER); +const SERVER = await pluginOptions('bestax-mcp', ANALYZER); + +const BREAKING = '\n\nBREAKING CHANGE: a prop was renamed'; + +/** The release type a package's rules give these commits, or null. */ +async function release(options, ...messages) { + return analyzeCommits(options, { + cwd: REPO, + logger: { log() {} }, + commits: messages.map((message, i) => ({ message, hash: `c${i}` })), + }); +} + +/** + * Every type a commit here can carry, which commitlint holds to its + * conventional list, plus any type either rule list names, so a rule for a + * type commitlint does not know is still exercised. + */ +const TYPES = [ + ...new Set([ + ...conventional.rules['type-enum'][2], + ...[LIBRARY, SERVER].flatMap(o => o.releaseRules.map(r => r.type)), + ]), +].filter(Boolean); + +test('bestax-mcp patches on exactly the bulma-ui commits that release bulma-ui', async () => { + const seen = new Set(); + for (const type of TYPES) { + for (const footer of ['', BREAKING]) { + const message = `${type}(bulma-ui): change the library${footer}`; + const library = await release(LIBRARY, message); + seen.add(library); + assert.equal( + await release(SERVER, message), + library ? 'patch' : null, + `${JSON.stringify(message)} gives bulma-ui ${library}, so bestax-mcp ` + + `should ${library ? 'patch' : 'not release'}. The bulma-ui group in ` + + 'bestax-mcp/release.config.js says how its rules meet these.' + ); + } + } + // Not vacuous: the loop met every kind of bulma-ui release. + for (const kind of ['major', 'minor', 'patch', null]) { + assert.ok(seen.has(kind), `no bulma-ui commit gave ${kind}`); + } +}); + +test('a bulma-ui commit never lifts bestax-mcp past patch or lowers its own bump', async () => { + assert.equal( + await release( + SERVER, + `feat(bulma-ui): rename a prop${BREAKING}`, + 'fix(bestax-mcp): answer a lookup' + ), + 'patch' + ); + assert.equal( + await release( + SERVER, + `feat(bulma-ui): rename a prop${BREAKING}`, + 'feat(bestax-mcp): add a tool' + ), + 'minor' + ); + assert.equal( + await release( + SERVER, + 'feat(bulma-ui): add a helper', + `feat(bestax-mcp): drop a tool${BREAKING}` + ), + 'major' + ); +}); + +test("bestax-mcp's own commits keep their own bumps", async () => { + const cases = { + 'feat(bestax-mcp): add a tool': 'minor', + 'fix(bestax-mcp): answer a lookup': 'patch', + 'perf(bestax-mcp): answer faster': 'patch', + 'refactor(bestax-mcp): split a module': 'patch', + 'style(bestax-mcp): format': 'patch', + [`feat(bestax-mcp): drop a tool${BREAKING}`]: 'major', + 'docs(bestax-mcp): explain a tool': null, + 'test(bestax-mcp): cover a tool': null, + }; + for (const [message, expected] of Object.entries(cases)) { + assert.equal(await release(SERVER, message), expected, message); + } +}); + +test("other scopes and the release job's own commits release no bestax-mcp", async () => { + for (const message of [ + 'fix(create-bestax): handle a missing TTY', + `feat(create-bestax): change a template${BREAKING}`, + 'feat(bestax-migrate): convert a class', + 'feat(eslint-plugin): add a rule', + 'feat(docs): add a page', + 'fix(docs): word a page', + 'docs: tidy the readme', + 'chore: regenerate', + 'chore(release): 5.27.9 [skip ci]', + 'chore(bestax-mcp): restamp index after release [skip ci]', + ]) { + assert.equal(await release(SERVER, message), null, message); + } +}); + +test('the link runs one way: a bestax-mcp commit releases no bulma-ui', async () => { + assert.equal(await release(LIBRARY, 'feat(bestax-mcp): add a tool'), null); + assert.equal(await release(LIBRARY, 'feat(bulma-ui): add a helper'), 'minor'); +});