| name | shadscan-release |
| description | 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. |
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.
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.