Shadscan release
Ship one new @shadscan/cli version to npm with its changelog entries and
verification. Prereleases (0.1.0-rc.N) publish under the npm next tag;
stable releases publish under latest through the staged GitHub workflow.
Before acting, read docs/releasing.md in full — it is the source of truth
for owner prerequisites, gates, trusted publishing, and recovery. Keep this
skill as the executable coordinator; never weaken or skip the runbook's gates.
Completion contract
- One release = one strictly higher, previously unpublished version. Confirm
absence with
npm view @shadscan/cli versions --json before starting.
Published name/version pairs are immutable — corrections always fix forward
with a new version, never a republish.
- A release is complete only when: the version is live on npm under the
intended dist-tag, the exact published version runs
--version and a real
audit from a clean directory, the dist-tag map is inspected and correct, and
both changelog surfaces (root CHANGELOG.md and changelog/<version>.md)
are committed and pushed.
- Publish only from
packages/cli. The repository root is a private Next.js
app and must never be published.
- npm 2FA belongs to the owner. Prepare and run the publish command for them,
but never attempt to bypass, script, or wait out the 2FA prompt; hand the
terminal to the owner at that step.
- If any gate fails, stop and report the exact failing gate. Never publish a
version whose gates did not all pass on the exact commit being published.
Prerequisites
- Clean working tree on up-to-date
main (no uncommitted or unrelated WIP —
the gates verify the tree as a whole).
- Node.js version from
.node-version and pnpm from the root packageManager
field.
- Owner is present for
npm login / npm whoami and 2FA at publish time.
- The target version does not exist on npm yet.
1. Prepare the version and changelogs
- Bump
packages/cli/package.json to the next unused version.
- Update root
CHANGELOG.md: move the ## Unreleased content into a new
## <version> - <YYYY-MM-DD> section with ### Added / Changed / Fixed / Security groupings, and leave an empty ## Unreleased behind.
- Write the narrative site entry at
changelog/<version>.md:
- Frontmatter:
version, date, channel (next for prereleases,
latest for stable), title, summary, highlights (3–5 bullets).
- Body: a few
## sections telling the release as a story for users, not
a commit list. Write it from the new CHANGELOG.md section.
- The loader (
lib/changelog.ts) validates the frontmatter at build time —
pnpm ci:build-site failing on /changelog means the entry is malformed.
- Verify the three versions agree:
packages/cli/package.json,
the new CHANGELOG.md heading, and the changelog/<version>.md
frontmatter.
- Sweep the product surfaces that no generator owns.
pnpm docs:check
catches the advertised version pins and rule counts, but run it now
rather than discovering it at gate time:
- Advertised rule count — both
README.md and
packages/cli/README.md state the rule count in prose ("N rules",
"contains N deterministic checks"). A release that adds or removes a
rule must update all four. Never touch the counts in CHANGELOG.md
or changelog/*.md: those are historical records of what shipped
then, and rewriting them is falsifying the record.
- Ruleset version —
BUNDLED_RULESET_VERSION in
packages/cli/src/scan.ts must already be bumped by whatever added
or changed a rule, and docs/rules.md regenerated with
pnpm docs:rules.
- Hardcoded rule counts in tests —
packages/cli/test/public-api.test.ts
and packages/cli/scripts/smoke-package.mjs both assert
RULE_CATALOG.length. The smoke one fails late, after a full build
and npm pack, with a message that says nothing about rule counts.
- Commit the preparation and push.
2. Run every release gate
From the repository root, per docs/releasing.md:
pnpm install --frozen-lockfile
pnpm check
pnpm docs:check
pnpm --filter ./packages/cli typecheck
pnpm cli:test
pnpm test:api
pnpm test:web
pnpm exec playwright install chromium
pnpm test:e2e
pnpm typecheck
pnpm audit:dependencies
pnpm audit:self
pnpm build
pnpm cli:smoke
CLI_VERSION=$(node -p "require('./packages/cli/package.json').version")
pnpm cli:release:check -- --tag next --git-tag "v${CLI_VERSION}"
For a stable release, use --tag latest and the matching stable Git tag.
pnpm build also proves the new /changelog entry renders and the
production trace verifier passes.
3. Publish
Prerelease (next)
- Owner authenticates:
npm login, confirm with npm whoami.
- Run
(cd packages/cli && npm publish --tag next --access public) and let
the owner complete 2FA.
Stable (latest)
Follow the staged flow in docs/releasing.md: signed v<version> Git tag,
GitHub release, the publish workflow stages the artifact, owner inspects the
staged tarball and approves on npm with 2FA.
4. Verify the published artifact
- Inspect the tag map:
npm view @shadscan/cli dist-tags --json. Confirm
the new version is under the intended tag and latest did not move
unintentionally during a prerelease.
- Run the exact published version from a clean directory:
npx --yes @shadscan/cli@<version> --version.
- Run representative audits of the published version in clean temporary
Next.js, Vite React, and generic React fixture projects. Run one fixture
twice and confirm the two reports are identical — determinism is the
product promise.
- Verify the deployed site:
/changelog shows the new entry.
/stats reflects the release. Nothing on this page is edited by
hand — every tile (Latest, Versions, Downloads, Stars) is
pulled live from the npm registry and the GitHub API, and the page
revalidates hourly (export const revalidate = 3600). So this is a
confirmation step, not an update step: after publishing, the Latest
tile should show the new version within the hour, and a new bar
should appear in the per-version chart. If it still shows the
previous version after the revalidate window, the publish did not
reach the registry — investigate rather than editing the page.
/rules lists the new rule count; it reads
lib/generated/rule-catalog.json, so a wrong number there means the
catalog was not regenerated.
- The audit badge/scoring flows still work (site examples stay pinned to
@next or an exact version during the RC window).
5. Close out
- Announce from
changelog/<version>.md content only after verification
passes (the summary paragraph is written to be quotable).
- For a stable release only: replace
@next-pinned examples in public docs
and UI with unqualified @shadscan/cli commands, per the runbook.
- If anything shipped broken: move the dist-tag back to the last good
version,
npm deprecate the bad version naming its replacement, and fix
forward. Unpublish only for security or accidental disclosure, per policy.
Hard-won rules
Tag the release before publishing the next one. Three releases
published before their Git tag existed (0.8.0, 0.9.0, 0.10.0), leaving no
commit to diff a shipped version against. check-release.mjs now fails
when the previous release in CHANGELOG.md has no matching tag and names
the command to fix it, but push the tag as part of the release rather than
waiting for the next one to complain.
Prose counts go stale silently. The rule count in both READMEs is not
owned by any generator. pnpm docs:rules --check now compares them against
RULE_CATALOG.length, which is why that gate exists — do not "fix" a
failure by editing the generated files.
Any new top-level file or directory in this repository must be added to
SCANNER_TRACE_EXCLUDES in next.config.ts, or pnpm build fails in the
postbuild trace verifier ("unrelated source"). This has broken the build
before (CONTRIBUTING.md).
npm requires every package to keep a latest tag: the first publish got
latest even with --tag next. During the RC window, never advertise the
unqualified package name; keep every public command pinned to @next or an
exact version.
Verify with npx --yes @shadscan/cli@<exact-version>, not a dist-tag —
runner caches can serve a stale tag resolution minutes after publish.
pnpm 11.15+ ships a default minimumReleaseAge supply-chain gate: tag
specs like @next silently resolve to the newest version OLD ENOUGH to
pass the gate, while exact pins bypass it. A pnpm user reporting an
ancient version days after a publish is probably hitting this gate, not a
cache — check their pnpm major first. Locally it can be disabled with
minimumReleaseAge: 0 in ~/Library/Preferences/pnpm/config.yaml (the
YAML global config, not the legacy rc file).
The local shadscan audit binary (node_modules/.bin/shadscan) breaks when
packages/cli/dist is mid-rebuild; use the published one-shot
(pnpm dlx @shadscan/cli@next --json) for pre-commit audits during release
work.
Yaml frontmatter parses unquoted dates as Date objects; the changelog
loader normalizes this, but keep date: in YYYY-MM-DD form anyway.
The ci:* script variants skip the CLI rebuild; local release work must use
the ordinary commands so lifecycle hooks rebuild the CLI and stale output
cannot be verified or published.
1---2name: shadscan-release3description: Cut and publish a new @shadscan/cli version to npm, including version bump, CHANGELOG.md and site changelog entries, the full release gate suite, npm publish under the next or latest dist-tag, published-artifact verification in clean fixture projects, and post-publish checks. Use when the user asks to release, publish, ship, or cut a new shadscan or @shadscan/cli version, or to prepare a release candidate.4---56# Shadscan release78Ship one new `@shadscan/cli` version to npm with its changelog entries and9verification. Prereleases (`0.1.0-rc.N`) publish under the npm `next` tag;10stable releases publish under `latest` through the staged GitHub workflow.1112Before acting, read `docs/releasing.md` in full — it is the source of truth13for owner prerequisites, gates, trusted publishing, and recovery. Keep this14skill as the executable coordinator; never weaken or skip the runbook's gates.1516## Completion contract1718- One release = one strictly higher, previously unpublished version. Confirm19 absence with `npm view @shadscan/cli versions --json` before starting.20 Published name/version pairs are immutable — corrections always fix forward21 with a new version, never a republish.22- A release is complete only when: the version is live on npm under the23 intended dist-tag, the exact published version runs `--version` and a real24 audit from a clean directory, the dist-tag map is inspected and correct, and25 both changelog surfaces (root `CHANGELOG.md` and `changelog/<version>.md`)26 are committed and pushed.27- Publish only from `packages/cli`. The repository root is a private Next.js28 app and must never be published.29- npm 2FA belongs to the owner. Prepare and run the publish command for them,30 but never attempt to bypass, script, or wait out the 2FA prompt; hand the31 terminal to the owner at that step.32- If any gate fails, stop and report the exact failing gate. Never publish a33 version whose gates did not all pass on the exact commit being published.3435## Prerequisites3637- Clean working tree on up-to-date `main` (no uncommitted or unrelated WIP —38 the gates verify the tree as a whole).39- Node.js version from `.node-version` and pnpm from the root `packageManager`40 field.41- Owner is present for `npm login` / `npm whoami` and 2FA at publish time.42- The target version does not exist on npm yet.4344## 1. Prepare the version and changelogs45461. Bump `packages/cli/package.json` to the next unused version.472. Update root `CHANGELOG.md`: move the `## Unreleased` content into a new48 `## <version> - <YYYY-MM-DD>` section with `### Added / Changed / Fixed /49 Security` groupings, and leave an empty `## Unreleased` behind.503. Write the narrative site entry at `changelog/<version>.md`:51 - Frontmatter: `version`, `date`, `channel` (`next` for prereleases,52 `latest` for stable), `title`, `summary`, `highlights` (3–5 bullets).53 - Body: a few `##` sections telling the release as a story for users, not54 a commit list. Write it from the new CHANGELOG.md section.55 - The loader (`lib/changelog.ts`) validates the frontmatter at build time —56 `pnpm ci:build-site` failing on `/changelog` means the entry is malformed.574. Verify the three versions agree: `packages/cli/package.json`,58 the new `CHANGELOG.md` heading, and the `changelog/<version>.md`59 frontmatter.605. Sweep the product surfaces that no generator owns. `pnpm docs:check`61 catches the advertised version pins and rule counts, but run it now62 rather than discovering it at gate time:63 - **Advertised rule count** — both `README.md` and64 `packages/cli/README.md` state the rule count in prose ("N rules",65 "contains N deterministic checks"). A release that adds or removes a66 rule must update all four. Never touch the counts in `CHANGELOG.md`67 or `changelog/*.md`: those are historical records of what shipped68 then, and rewriting them is falsifying the record.69 - **Ruleset version** — `BUNDLED_RULESET_VERSION` in70 `packages/cli/src/scan.ts` must already be bumped by whatever added71 or changed a rule, and `docs/rules.md` regenerated with72 `pnpm docs:rules`.73 - **Hardcoded rule counts in tests** — `packages/cli/test/public-api.test.ts`74 and `packages/cli/scripts/smoke-package.mjs` both assert75 `RULE_CATALOG.length`. The smoke one fails late, after a full build76 and npm pack, with a message that says nothing about rule counts.776. Commit the preparation and push.7879## 2. Run every release gate8081From the repository root, per `docs/releasing.md`:8283```bash84pnpm install --frozen-lockfile85pnpm check86pnpm docs:check87pnpm --filter ./packages/cli typecheck88pnpm cli:test89pnpm test:api90pnpm test:web91pnpm exec playwright install chromium92pnpm test:e2e93pnpm typecheck94pnpm audit:dependencies95pnpm audit:self96pnpm build97pnpm cli:smoke98CLI_VERSION=$(node -p "require('./packages/cli/package.json').version")99pnpm cli:release:check -- --tag next --git-tag "v${CLI_VERSION}"100```101102For a stable release, use `--tag latest` and the matching stable Git tag.103`pnpm build` also proves the new `/changelog` entry renders and the104production trace verifier passes.105106## 3. Publish107108### Prerelease (`next`)1091101. Owner authenticates: `npm login`, confirm with `npm whoami`.1112. Run `(cd packages/cli && npm publish --tag next --access public)` and let112 the owner complete 2FA.113114### Stable (`latest`)115116Follow the staged flow in `docs/releasing.md`: signed `v<version>` Git tag,117GitHub release, the publish workflow stages the artifact, owner inspects the118staged tarball and approves on npm with 2FA.119120## 4. Verify the published artifact1211221. Inspect the tag map: `npm view @shadscan/cli dist-tags --json`. Confirm123 the new version is under the intended tag and `latest` did not move124 unintentionally during a prerelease.1252. Run the exact published version from a clean directory:126 `npx --yes @shadscan/cli@<version> --version`.1273. Run representative audits of the published version in clean temporary128 Next.js, Vite React, and generic React fixture projects. Run one fixture129 twice and confirm the two reports are identical — determinism is the130 product promise.1314. Verify the deployed site:132 - `/changelog` shows the new entry.133 - `/stats` reflects the release. **Nothing on this page is edited by134 hand** — every tile (`Latest`, `Versions`, `Downloads`, `Stars`) is135 pulled live from the npm registry and the GitHub API, and the page136 revalidates hourly (`export const revalidate = 3600`). So this is a137 confirmation step, not an update step: after publishing, the `Latest`138 tile should show the new version within the hour, and a new bar139 should appear in the per-version chart. If it still shows the140 previous version after the revalidate window, the publish did not141 reach the registry — investigate rather than editing the page.142 - `/rules` lists the new rule count; it reads143 `lib/generated/rule-catalog.json`, so a wrong number there means the144 catalog was not regenerated.145 - The audit badge/scoring flows still work (site examples stay pinned to146 `@next` or an exact version during the RC window).147148## 5. Close out149150- Announce from `changelog/<version>.md` content only after verification151 passes (the summary paragraph is written to be quotable).152- For a stable release only: replace `@next`-pinned examples in public docs153 and UI with unqualified `@shadscan/cli` commands, per the runbook.154- If anything shipped broken: move the dist-tag back to the last good155 version, `npm deprecate` the bad version naming its replacement, and fix156 forward. Unpublish only for security or accidental disclosure, per policy.157158## Hard-won rules159160- **Tag the release before publishing the next one.** Three releases161 published before their Git tag existed (0.8.0, 0.9.0, 0.10.0), leaving no162 commit to diff a shipped version against. `check-release.mjs` now fails163 when the previous release in `CHANGELOG.md` has no matching tag and names164 the command to fix it, but push the tag as part of the release rather than165 waiting for the next one to complain.166- **Prose counts go stale silently.** The rule count in both READMEs is not167 owned by any generator. `pnpm docs:rules --check` now compares them against168 `RULE_CATALOG.length`, which is why that gate exists — do not "fix" a169 failure by editing the generated files.170171- Any new top-level file or directory in this repository must be added to172 `SCANNER_TRACE_EXCLUDES` in `next.config.ts`, or `pnpm build` fails in the173 postbuild trace verifier ("unrelated source"). This has broken the build174 before (CONTRIBUTING.md).175- npm requires every package to keep a `latest` tag: the first publish got176 `latest` even with `--tag next`. During the RC window, never advertise the177 unqualified package name; keep every public command pinned to `@next` or an178 exact version.179- Verify with `npx --yes @shadscan/cli@<exact-version>`, not a dist-tag —180 runner caches can serve a stale tag resolution minutes after publish.181- pnpm 11.15+ ships a default `minimumReleaseAge` supply-chain gate: tag182 specs like `@next` silently resolve to the newest version OLD ENOUGH to183 pass the gate, while exact pins bypass it. A pnpm user reporting an184 ancient version days after a publish is probably hitting this gate, not a185 cache — check their pnpm major first. Locally it can be disabled with186 `minimumReleaseAge: 0` in `~/Library/Preferences/pnpm/config.yaml` (the187 YAML global config, not the legacy `rc` file).188- The local shadscan audit binary (`node_modules/.bin/shadscan`) breaks when189 `packages/cli/dist` is mid-rebuild; use the published one-shot190 (`pnpm dlx @shadscan/cli@next --json`) for pre-commit audits during release191 work.192- Yaml frontmatter parses unquoted dates as Date objects; the changelog193 loader normalizes this, but keep `date:` in `YYYY-MM-DD` form anyway.194- The `ci:*` script variants skip the CLI rebuild; local release work must use195 the ordinary commands so lifecycle hooks rebuild the CLI and stale output196 cannot be verified or published.