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 — BUNDLEDRULESETVERSION 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
SCANNERTRACEEXCLUDES 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.