# Shadscan Release

> 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.

- Skill: `theorcdev/shadscan-release` (Agent Skill)
- Install (CLI): `npx skillmds@latest add theorcdev/shadscan-release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/theorcdev/shadscan-release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: theorcdev (https://skillmd.com/u/theorcdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/theorcdev/shadscan-release

---


# 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

1. Bump `packages/cli/package.json` to the next unused version.
2. 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.
3. 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.
4. Verify the three versions agree: `packages/cli/package.json`,
   the new `CHANGELOG.md` heading, and the `changelog/<version>.md`
   frontmatter.
5. 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.
6. Commit the preparation and push.

## 2. Run every release gate

From the repository root, per `docs/releasing.md`:

```bash
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`)

1. Owner authenticates: `npm login`, confirm with `npm whoami`.
2. 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

1. 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.
2. Run the exact published version from a clean directory:
   `npx --yes @shadscan/cli@<version> --version`.
3. 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.
4. 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.

