diff --git a/.github/workflows/pr-label.yml b/.github/workflows/pr-label.yml index 047fa9b..7138850 100644 --- a/.github/workflows/pr-label.yml +++ b/.github/workflows/pr-label.yml @@ -20,8 +20,7 @@ jobs: // Dependabot and friends label their own PRs (dependencies, // javascript, github_actions) and cannot be asked to pick a // changelog label, so the gate does not apply to them. Passing - // rather than skipping the job keeps the check reportable if it - // ever becomes a required status check. + // rather than skipping the job keeps the required check reportable. if (pullRequest.user.type === "Bot") { core.info( `Skipping the changelog label check for bot author ${pullRequest.user.login}.`, @@ -41,4 +40,15 @@ jobs: core.setFailed( `This PR needs at least one repository label. Available labels: ${repositoryLabels.sort().join(", ")}.`, ); + return; + } + // Keep #33's repository-derived acceptance rule. These are + // lerna-changelog's default rendered categories, not an allowlist. + const changelogLabels = [ + "breaking", "enhancement", "bug", "documentation", "internal", + ]; + if (!matched.some((label) => changelogLabels.includes(label.toLowerCase()))) { + core.warning( + `This PR will not appear in the changelog with its current labels. Add a rendered label if it needs release notes: ${changelogLabels.join(", ")}.`, + ); } diff --git a/README.md b/README.md index 626eded..34a61b6 100644 --- a/README.md +++ b/README.md @@ -869,6 +869,6 @@ Examples: Exit codes: - 0 when docs are current or updated -- 1 when --check finds stale docs +- 1 when --check finds stale docs or CLI reference metadata is missing diff --git a/RELEASE.md b/RELEASE.md index f452149..40f0915 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,92 +1,138 @@ -# Release Process +# Release process -Releases are mostly automated using +A maintainer runs releases with [release-it](https://github.com/release-it/release-it/) and [lerna-changelog](https://github.com/lerna/lerna-changelog/). ## Preparation -Since the majority of the actual release process is automated, the primary -remaining task prior to releasing is confirming that all pull requests that -have been merged since the last release have been labeled with the appropriate -`lerna-changelog` labels and the titles have been updated to ensure they -represent something that would make sense to our users. Some great information -on why this is important can be found at -[keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall -guiding principle here is that changelogs are for humans, not machines. +Review every PR merged since the last release. Give each PR that needs release +notes a rendered `lerna-changelog` label and a title that describes the change +for users. Changelogs are for people; see +[Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -When reviewing merged PR's the labels to be used are: +The rendered labels are: -- breaking - Used when the PR is considered a breaking change. -- enhancement - Used when the PR adds a new feature or enhancement. -- bug - Used when the PR fixes a bug included in a previous release. -- documentation - Used when the PR adds or updates documentation. -- internal - Used for internal changes that still require a mention in the - changelog/release notes. +- `breaking`: an incompatible change to the public contract described below. +- `enhancement`: a new feature or enhancement. +- `bug`: a fix for a bug in a previous release. +- `documentation`: a documentation change. +- `internal`: an internal change that needs release notes. -## Release - -Once the prep work is completed, the actual release is straight forward: +Check the generated changelog for omitted PRs, especially bot PRs and PRs with +only labels outside these five categories. The label check does not guarantee +that a change appears in the changelog or has the right category. -- First, ensure that you have installed your projects dependencies: +## Release -```sh -npm install -``` +1. Check out `main` and pull the reviewed commits. release-it requires `main`. +2. Install the locked dependencies with `npm ci`. +3. Run `npm run check` and `npm run audit`. +4. Set `GITHUB_AUTH` to a [GitHub personal access token][generate-token] with + the `repo` scope. Both lerna-changelog and release-it use this variable. +5. Choose the version increment explicitly. For the 0.12.0 release from 0.11.1: -- Second, ensure that you have obtained a - [GitHub personal access token][generate-token] with the `repo` scope (no - other permissions are needed). Make sure the token is available as the - `GITHUB_AUTH` environment variable. + ```sh + npm run release -- minor + ``` - For instance: +6. Review the proposed version and generated changelog in the prompts. Stop if + either is wrong. release-it then creates the release commit and tag, pushes + them, publishes to npm, and creates the GitHub Release. - ```bash - export GITHUB_AUTH=abc123def456 - ``` +Use `npm run release -- minor` for breaking 0.x releases, not an inferred bump +from a PR label. A `breaking` label controls the changelog category; it does +not choose the release increment. Use `npm run release -- patch` for a patch, +or `npm run release -- major` for a major version. [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable -- And last (but not least 😁) do your release. - -```sh -npx release-it -``` - -[release-it](https://github.com/release-it/release-it/) manages the actual -release process. It will prompt you to to choose the version number after which -you will have the chance to hand tweak the changelog to be used (for the -`CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, -pushing the tag and commits, etc. - ## What counts as a breaking change -pluginpack's public contract isn't just its TypeScript API (`src/index.ts`) -and CLI flags — it's also the _shape of the files it writes into a consumer's -repo_ (managed-file layout, manifest field names, default directory -conventions). A change that alters generated output for an existing config -with no code-level signature change (e.g. a different default `outDir` -convention, a renamed manifest field, a different `.pluginpack/.json` -shape) is breaking for the same reason a database migration is breaking: it -can affect a consumer's already-generated, already-committed repo on their -next build, even though nothing in `import { ... } from "@gleanwork/pluginpack"` -changed. Label PRs with `breaking` under that broader definition, not just for -API/CLI signature changes. - -## PR label enforcement - -CI (`.github/workflows/ci.yml`, `labels` job) requires every PR to carry one -of the five labels above before merge — this catches an unlabeled PR, but it -cannot judge whether the label chosen is the _correct_ one. Whether a given -diff is actually breaking under the definition above is still a human call at -review time. +The public contract includes the TypeScript API (`src/index.ts`), CLI flags, +config, and the files written into a consumer's repository. An incompatible +change includes a new default directory, a renamed manifest field, a change to +JSON field order, or a different `.pluginpack/.json` shape. Output can +break a consumer's committed repository even when no API signature changes. +Label these PRs `breaking` and provide migration notes. During 0.x, breaking +changes require a minor release; starting with 1.0, they require a major release. + +### Correctness fixes + +Correctness fixes can ship in a patch release even when output changes, but only +when the old behavior violated the documented contract for the selected target +and format. Examples include preserving source bytes instead of corrupting them, +preventing data loss, and fixing output that the documented host format rejects. +Include a regression test that proves the violation. Label the PR `bug` and +explain the old and corrected behavior in release notes. Call out any action a +consumer must take to rebuild or recover files. + +This exception does not cover a new upstream format, a new default, or removing +previously valid config. Those changes follow the compatibility policy below +and need migration notes. A correctness fix is not a reason to silently replace +one supported host format with another. + +## Output compatibility + +For a fixed target and `format`, 1.x preserves the documented output contract +for existing valid config, including paths, manifest fields and field order. +New optional config may add output when a user opts in. The correctness-fix +exception above still applies. + +An upstream host format change ships as a new opt-in `format` value in a minor release. +The default changes only in a major release. Keep the existing format available +through the current major series. A host dropping support for that format does +not permit pluginpack to change it silently; document the affected host versions +and the opt-in migration. Starting with 1.0, removing a supported format also +requires a major release. The pre-1.0 `codex.format: "legacy"` migration follows +the breaking-minor rule: deprecated in 0.12 and removed in 0.13. + +### Verified against host versions + +These are the verification limits recorded in each target's citations in +`src/targets/.ts`, not a promise of compatibility with every past or +future client release. Where a client version was not recorded, the dated docs +snapshot is the available evidence. It is not a tested version range. Agent +Plugins 1.0.0 is a format version, not a Codex or Copilot client version. + +| Target | Verified version or evidence | Source | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `claude` | Claude Code client version was not recorded; manifest validation checked 2026-07-26, install commands 2026-07-25. | [Claude plugin reference](https://code.claude.com/docs/en/plugins-reference) | +| `cursor` | Cursor client version was not recorded; plugin and marketplace docs checked 2026-07-26. Vendored Glean schemas are pinned at `424c3485`, rechecked against `6f3ad6e6` on 2026-07-28. | [Cursor plugin reference](https://cursor.com/docs/reference/plugins.md), [schema provenance](tests/fixtures/cursor/SOURCE.md) | +| `copilot` | Copilot CLI client version was not recorded; native plugin docs checked 2026-07-25 and published manifest placement 2026-07-26. | [Copilot CLI plugin reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference), [published plugin](https://github.com/github/copilot-advanced-security-plugin/blob/main/.github/plugin/plugin.json) | +| `antigravity` | Antigravity client version was not recorded; CLI layout and IDE hooks docs checked 2026-07-25. | [CLI plugins](https://antigravity.google/docs/cli/plugins), [IDE plugins](https://antigravity.google/docs/ide/plugins) | +| `codex` | Agent Plugins 1.0.0 package format; Codex client version was not recorded. Packaging and marketplace docs checked 2026-10-02; legacy manifest checked 2026-07-26, install commands 2026-07-25. | [Codex packaging](https://developers.openai.com/codex/plugins/build), [install commands](https://learn.chatgpt.com/codex/developer-commands), [schema provenance](tests/fixtures/agent-plugins/SOURCE.md) | +| `agent-plugins` | Agent Plugins 1.0.0, checked 2026-10-02. Client profiles do not imply a tested client version range. | [Agent Plugins specification](https://agent-plugins.org/specification), [schema provenance](tests/fixtures/agent-plugins/SOURCE.md) | + +Recheck the relevant citations and conformance tests when changing a target's +output. Record the exact client version when running a host CLI, or the spec +version and docs verification date when no versioned host oracle exists. +Update this list in the same PR. See [CONFORMANCE.md](CONFORMANCE.md) for the +strength and limits of each target's checks. Package-client citations also live +in `src/package-clients.ts`. + +## PR label check and branch protection + +`main` requires the checks `test (22.12.0)`, `test (24)`, and `labels`. +Admins can bypass these requirements. No approving review is required. +The two test checks run `.github/workflows/ci.yml`; the `labels` check runs +`.github/workflows/pr-label.yml`. Maintainers remain responsible for reviewing +changes and the changelog, including any use of the admin bypass. + +The label check accepts any label in the repository's current, paginated label +catalog, as decided in [PR #33](https://github.com/gleanwork/pluginpack/pull/33). +It does not require one of lerna-changelog's five rendered labels. For example, +a PR labeled only `question` passes if that label exists in the repository, +but it will not appear in the changelog. The check warns about this case; the +maintainer must add a rendered label if the change needs release notes. +Bot authors are exempt: their `labels` job reports success without checking +labels. A passing label check cannot judge whether a diff is breaking. ## Accepted risk: dev-only audit findings -`npm audit` (unscoped) reports vulnerabilities in `@release-it-plugins/lerna-changelog`'s -dependency chain (`lerna-changelog` → `make-fetch-happen` → `cacache`/`tar`), -with no upstream fix available. `npm run audit` (what CI runs) is scoped to -`--omit=dev` and reports clean, per `CLAUDE.md`'s documented rationale: -devDependencies never ship in the published package. This is a tracked, -accepted gap, not an oversight — revisit if `lerna-changelog` is ever run -against untrusted input, or replace it if a maintained alternative appears. +`npm audit` without `--omit=dev` reports vulnerabilities in development tools, +including lerna-changelog's dependency chain. `npm run audit` (what CI runs) +is scoped to production dependencies and reports clean. Development +dependencies do not ship in the published package. Do not add overrides for +dev-only findings; revisit this risk if release tooling processes untrusted +input, or replace it when a maintained alternative is available. diff --git a/package-lock.json b/package-lock.json index 2936e9e..a3aefcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,6 +142,24 @@ "node": "12.* || 14.* || >= 16" } }, + "node_modules/@embroider/shared-internals/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@embroider/shared-internals/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@embroider/shared-internals/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -218,6 +236,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -235,6 +254,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -252,6 +272,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -269,6 +290,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -286,6 +308,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -303,6 +326,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -320,6 +344,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -337,6 +362,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -354,6 +380,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -371,6 +398,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -388,6 +416,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -405,6 +434,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -422,6 +452,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -439,6 +470,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -456,6 +488,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -473,6 +506,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -490,6 +524,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -507,6 +542,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -524,6 +560,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -541,6 +578,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -558,6 +596,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -575,6 +614,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -592,6 +632,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -609,6 +650,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -626,6 +668,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -643,6 +686,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -4813,6 +4857,24 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/cacache/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5431,6 +5493,13 @@ "node": ">=22.12.0" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", @@ -5825,6 +5894,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6427,6 +6497,19 @@ "node": ">= 14.*" } }, + "node_modules/fixturify-project/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/fixturify-project/node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -6677,6 +6760,23 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/glob/node_modules/minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", @@ -6845,6 +6945,23 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ignore-walk/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/ignore-walk/node_modules/minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", @@ -8244,6 +8361,24 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/matcher-collection/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/matcher-collection/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/matcher-collection/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -9476,6 +9611,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -10417,6 +10562,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -11237,6 +11400,490 @@ } } }, + "node_modules/tsup/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11752,6 +12399,24 @@ "node": "10.* || >= 12.*" } }, + "node_modules/walk-sync/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/walk-sync/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/walk-sync/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", diff --git a/package.json b/package.json index 683e624..f003fac 100644 --- a/package.json +++ b/package.json @@ -103,18 +103,14 @@ "access": "public", "registry": "https://registry.npmjs.org" }, - "overrides": { - "esbuild": "^0.28.1", - "tmp": "^0.2.7", - "brace-expansion": "^5.0.8" - }, "release-it": { "git": { - "tagName": "v${version}" + "tagName": "v${version}", + "requireBranch": "main" }, "github": { "release": true, - "tokenRef": "GITHUB_TOKEN" + "tokenRef": "GITHUB_AUTH" }, "npm": { "publish": true diff --git a/src/cli.ts b/src/cli.ts index b18c24d..6f8981e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -430,7 +430,7 @@ function commandUsage(command: Command): string { /** * Example invocations shown in the generated CLI reference, keyed by * command name. Hand-maintained, not derived from the command definitions — - * a new command needs a case added here or its docs section omits examples. + * a new command needs a case added here or docs generation fails. */ function commandExamples(commandName: string): string[] { switch (commandName) { @@ -459,7 +459,9 @@ function commandExamples(commandName: string): string[] { case "docs": return ["pluginpack docs", "pluginpack docs --check"]; default: - return []; + throw new Error( + `src/cli.ts: commandExamples is missing examples for command "${commandName}". Add a commandExamples case.`, + ); } } @@ -501,10 +503,12 @@ function commandExitCodes(commandName: string): string[] { case "docs": return [ "0 when docs are current or updated", - "1 when --check finds stale docs", + "1 when --check finds stale docs or CLI reference metadata is missing", ]; default: - return []; + throw new Error( + `src/cli.ts: commandExitCodes is missing exit codes for command "${commandName}". Add a commandExitCodes case.`, + ); } } diff --git a/tests/cli-docs.test.ts b/tests/cli-docs.test.ts new file mode 100644 index 0000000..a29d98b --- /dev/null +++ b/tests/cli-docs.test.ts @@ -0,0 +1,81 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { createContext, runInContext } from "node:vm"; +import { Command } from "commander"; +import ts from "typescript"; +import { describe, expect, it } from "vitest"; + +function docsRenderer() { + // Load the private rendering functions without running the CLI entry point. + // This keeps CLI-only helpers out of the package's public exports. + const file = path.resolve(import.meta.dirname, "../src/cli.ts"); + const source = ts.createSourceFile( + file, + readFileSync(file, "utf8"), + ts.ScriptTarget.Latest, + true, + ); + const names = new Set([ + "renderCliReference", + "commandUsage", + "commandExamples", + "commandExitCodes", + ]); + const functions = source.statements + .filter( + (node) => + ts.isFunctionDeclaration(node) && + node.name !== undefined && + names.has(node.name.text), + ) + .map((node) => node.getText(source)) + .join("\n"); + const context = createContext({}); + runInContext(ts.transpileModule(functions, {}).outputText, context); + const render = runInContext("renderCliReference", context) as ( + program: Command, + ) => string; + return { context, render }; +} + +describe("CLI reference completeness", () => { + it("fails generation for a new command with no examples", () => { + const { render } = docsRenderer(); + const program = new Command(); + program.command("preview").description("Preview a package."); + expect(() => render(program)).toThrow( + 'src/cli.ts: commandExamples is missing examples for command "preview". Add a commandExamples case.', + ); + }); + + it("fails generation when examples exist but exit codes are missing", () => { + const { context, render } = docsRenderer(); + // Model a new command whose author added examples but forgot exit codes. + context.commandExamples = () => ["pluginpack preview"]; + const program = new Command(); + program.command("preview").description("Preview a package."); + expect(() => render(program)).toThrow( + 'src/cli.ts: commandExitCodes is missing exit codes for command "preview". Add a commandExitCodes case.', + ); + }); + + it("documents incomplete metadata as a docs command failure", () => { + const { render } = docsRenderer(); + const program = new Command(); + program.command("docs").description("Generate the CLI reference."); + expect(render(program)).toContain( + "1 when --check finds stale docs or CLI reference metadata is missing", + ); + }); + + it("still renders examples and exit codes for a documented command", () => { + const { render } = docsRenderer(); + const program = new Command(); + program.command("build").description("Build a package."); + const reference = render(program); + expect(reference).toContain("Examples:"); + expect(reference).toContain("pluginpack build --target cursor"); + expect(reference).toContain("Exit codes:"); + expect(reference).toContain("0 when all selected targets build"); + }); +}); diff --git a/tests/release-config.test.ts b/tests/release-config.test.ts new file mode 100644 index 0000000..2833ff3 --- /dev/null +++ b/tests/release-config.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const root = path.resolve(import.meta.dirname, ".."); +const pkg = JSON.parse( + readFileSync(path.join(root, "package.json"), "utf8"), +) as { + overrides?: Record; + "release-it": { + git: { requireBranch?: string }; + github: { tokenRef: string }; + }; +}; +const release = readFileSync(path.join(root, "RELEASE.md"), "utf8"); + +describe("release configuration", () => { + it("uses the documented token variable for the GitHub release", () => { + expect(pkg["release-it"].github.tokenRef).toBe("GITHUB_AUTH"); + expect(release).toContain("GITHUB_AUTH"); + expect(release).not.toContain("GITHUB_TOKEN"); + }); + + it("requires releases from main", () => { + expect(pkg["release-it"].git.requireBranch).toBe("main"); + }); + + it("does not override dev-only dependency versions", () => { + expect(pkg.overrides).toBeUndefined(); + }); +}); diff --git a/tests/release-tooling.test.ts b/tests/release-tooling.test.ts new file mode 100644 index 0000000..6583a5d --- /dev/null +++ b/tests/release-tooling.test.ts @@ -0,0 +1,151 @@ +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { load } from "js-yaml"; +import { describe, expect, it, vi } from "vitest"; + +const root = path.resolve(import.meta.dirname, ".."); +const release = readFileSync(path.join(root, "RELEASE.md"), "utf8"); + +// Exercise the actual github-script body with API responses, not a second +// implementation of the workflow. No network or GitHub credentials are needed. +const workflow = load( + readFileSync(path.join(root, ".github/workflows/pr-label.yml"), "utf8"), +) as { jobs: { labels: { steps: { with: { script: string } }[] } } }; +const script = workflow.jobs.labels.steps[0].with.script; +const runLabelCheck = new Function( + "context", + "github", + "core", + `return (async () => { ${script} })();`, +) as (context: object, github: object, core: object) => Promise; + +async function checkLabels(labels: string[], authorType = "User") { + const core = { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn() }; + const listLabelsForRepo = vi.fn(); + const paginate = vi.fn(async (_method, _params, map) => [ + ...map({ data: [{ name: "question" }, { name: "bug" }] }), + ...map({ data: [{ name: "enhancement" }] }), + ]); + await runLabelCheck( + { + repo: { owner: "example", repo: "plugins" }, + payload: { + pull_request: { + user: { type: authorType, login: "author" }, + labels: labels.map((name) => ({ name })), + }, + }, + }, + { paginate, rest: { issues: { listLabelsForRepo } } }, + core, + ); + return { core, paginate, listLabelsForRepo }; +} + +describe("PR label check", () => { + it("still derives accepted labels from the paginated repository catalog", async () => { + const { core, paginate, listLabelsForRepo } = await checkLabels([ + "enhancement", + ]); + expect(paginate).toHaveBeenCalledWith( + listLabelsForRepo, + { owner: "example", repo: "plugins", per_page: 100 }, + expect.any(Function), + ); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.warning).not.toHaveBeenCalled(); + }); + + it("does not silently pass a label lerna-changelog does not render", async () => { + const { core } = await checkLabels(["question"]); + // #33 deliberately accepts every repository label, not just changelog + // categories. Warn without changing which labels the gate accepts. + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.warning).toHaveBeenCalledWith( + expect.stringContaining("will not appear in the changelog"), + ); + }); + + it("keeps the warning's rendered-label list in sync with lerna-changelog", async () => { + const require = createRequire(import.meta.url); + const { fromPath } = require("lerna-changelog/lib/configuration.js") as { + fromPath: (root: string) => { labels: Record }; + }; + const { core } = await checkLabels(["question"]); + expect(core.warning).toHaveBeenCalledWith( + expect.stringContaining(Object.keys(fromPath(root).labels).join(", ")), + ); + }); + + it("does not warn if a rendered changelog label is also present", async () => { + const { core } = await checkLabels(["question", "bug"]); + expect(core.warning).not.toHaveBeenCalled(); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it("fails for no label or a label absent from the repository", async () => { + for (const labels of [[], ["not-in-the-catalog"]]) { + const { core } = await checkLabels(labels); + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("at least one repository label"), + ); + } + }); + + it("exempts bot authors without fetching the label catalog", async () => { + const { core, paginate } = await checkLabels([], "Bot"); + expect(paginate).not.toHaveBeenCalled(); + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Skipping the changelog label check"), + ); + expect(core.setFailed).not.toHaveBeenCalled(); + }); +}); + +describe("release policy", () => { + it("records the actual branch protection and label workflow limits", () => { + for (const text of [ + "test (22.12.0)", + "test (24)", + "labels", + "Admins can bypass", + "No approving review is required", + ".github/workflows/pr-label.yml", + "Bot authors are exempt", + "question", + "will not appear in the changelog", + ]) { + expect(release).toContain(text); + } + expect(release).not.toContain("before merge"); + }); + + it("uses explicit increments for breaking 0.x releases", () => { + expect(release).toContain("npm run release -- minor"); + expect(release).toContain("0.12.0"); + }); + + it("distinguishes correctness fixes from host-format migrations", () => { + expect(release).toContain("Correctness fixes can ship in a patch release"); + expect(release).toContain("new opt-in `format` value in a minor release"); + expect(release).toContain("default changes only in a major release"); + expect(release).toContain("field order"); + }); + + it("lists evidence for every target without inventing client versions", () => { + expect(release).toContain("Verified against host versions"); + expect(release).toContain("client version was not recorded"); + expect(release).toContain("Agent Plugins 1.0.0"); + for (const target of [ + "claude", + "cursor", + "copilot", + "antigravity", + "codex", + "agent-plugins", + ]) { + expect(release).toMatch(new RegExp(`\\| \\x60${target}\\x60\\s+\\|`)); + } + }); +});