This document owns living release policy and the operator runbook. Dated tag, package, qualification, and dist-tag observations belong in release history and must be rechecked before an operation.
Publishing is a separate, explicit action after a release-preparation pull request has merged. npm registry bytes and package versions cannot be replaced, so do not create or push a release tag as a dry run.
Only a version that npm reports as published or a protected release tag that exists is immutable. Before either exists, an unpublished candidate may be revised without changing its proposed version; its exact source SHA and reviewed digests identify it. Reconcile the registry and remote tags before deciding that a version was consumed.
Before 1.0.0, First Draft CLI uses ordinary 0.MINOR.PATCH versions. Increase MINOR for a breaking
compatibility-line change. Increase PATCH for a change that is otherwise backward-compatible within the current
minor line. Never reuse a published version to preserve compatibility; publish the next version required by this
policy. Do not add aliases or shims solely to make a breaking compatibility line look patch-compatible. The policy
applies to ordinary versions; historical prereleases do not establish an ordinary compatibility line.
Version semantics and npm distribution channels are independent. An approved candidate is published first under
the approval-gated next tag even when it has an ordinary version such as 0.1.0. The release workflow does not
move latest. Moving latest requires a later, separate approval after the exact next candidate has completed
its explicitly named release-specific qualification. Candidate publication is not stable release completion. A
stable CLI release is complete only when that separately approved candidate is selected by npm's latest dist-tag.
Release-specific qualification means the exact gate named for that candidate; it does not imply unrelated or full
service qualification. Until promotion, latest remains the supported stable release; a distinct next candidate
is supported only for its named qualification. When both tags identify one version, that version fills both roles.
release/compatibility.json declares this package's SemVer version, the First Draft API-contract range it accepts,
and the exact Foundation Plan formats it accepts. It is source-only release metadata and is intentionally absent from
the npm tarball. The normal test suite validates the manifest's shape, keeps its version equal to package.json, and
binds its Foundation Plan format to the implemented CLI constant.
The script/release_compatibility_check evaluator in firstdraft/firstdraft reads this declaration with the matching
declarations from exact, clean checkouts of firstdraft/firstdraft and firstdraft/skills. It implements SemVer 2.0
precedence. CLI 0.2.x requires the service's 0.3.x API contract because Analysis now returns the complete reviewed
GapSet and digest. The released CLI 0.1.0 accepts only API 0.2.x, uses a generic 2 MiB response bound for Analysis,
and cannot safely consume every schema-valid API 0.3.x result. CLI 0.2.x retains that generic bound but gives
Analysis and Compilation artifacts dedicated 128 MiB bounds. Comparator arrays form one conjunction, while
foundation_plan_formats lists alternatives. A prerelease satisfies a comparator set only when a comparator
explicitly names a prerelease with the same major, minor, and patch numbers. Skills names the candidate CLI version
explicitly, so a stale comparator makes the three-repository candidate ineligible.
firstdraft.release-compatibility/1 is intentionally closed. The evaluator in firstdraft/firstdraft rejects an
unrecognized format and unknown keys, so adding a key requires a coordinated compatibility-format bump rather than
silently changing version 1.
A compatible result establishes candidate eligibility, not authorization or runtime proof. Exact Git SHAs identify
the three-repository candidate. A merge to main is integration only: report the merged SHA and ask the user whether
to coordinate the three repositories and promote that candidate. If promotion is declined, record the SHA as
unpromoted.
Promotion is manual and approval-gated. One operator serializes mutations: qualify the exact candidate on staging, obtain human approval, and only then promote the approved service revision to production or authorize the corresponding npm and plugin releases. Do not publish npm, deploy either environment, or release the plugin merely because the compatibility check passes.
Before a release, a repository administrator must confirm:
-
Confirm
firstdraft/cliis public. The release workflow deliberately removes checkout credentials and re-fetches the public release refs anonymously. -
Confirm
mainhas pull-request and CI requirements and av*tag ruleset restricts tag creation, update, and deletion. -
Confirm the GitHub environment named
npmis restricted to release tags, requires an explicit reviewer, disables administrator bypass, and definesNPM_RELEASE_ENABLED=true. The workflow fails before publishing when this variable is absent. -
Confirm that the
firstdraft.comnpm organization and@firstdraft.com/clipackage still identify the intended publisher and repository. The publisher account must have write-protecting 2FA enabled. Verify authenticated identity, organization membership, package identity, and current tags:npm whoami npm org ls firstdraft.com --json npm view '@firstdraft.com/cli' name repository.url versions dist-tags --json -
Before creating a
v<package-version>tag, verify npm trusted publishing for the exact package, repository, workflow, protected environment, and allowed publish operation. No persistent npm credential is permitted for a release workflow.
Use the repository-pinned Node.js 24.18.0 toolchain with npm 11.16.0 to verify the organization's durable read/write access:
npm --version
npm access list packages firstdraft.com:developers '@firstdraft.com/cli' --jsonRequire the package to report read-write access for the developers team. If access is missing or changed, stop:
restoring it is a separate administrator and security mutation, not a routine release step.
For every release, inspect the existing trusted-publisher relationship read-only:
npm trust list '@firstdraft.com/cli'Confirm npm trust list reports type github, repository firstdraft/cli, file publish.yml, environment npm,
and permission createPackage, which is npm's trust-list vocabulary for the allowed publish operation. npm does not
validate the saved relationship by attempting an exchange, so each case-sensitive value must be inspected. If the
relationship is missing or changed, stop: restoring it is a separate administrator and security mutation, not a
routine release step. The publish job must remain on a GitHub-hosted runner with id-token: write and must not read
NODE_AUTH_TOKEN, an npm token, or any GitHub Actions secret. Confirm the repository and npm environment secret
lists contain no npm automation secret. Trusted publishing's short-lived OIDC exchange is the sole workflow
publication credential; an authentication failure stops the release and must never fall back to a persistent token.
As optional defense-in-depth after trusted publication is operationally proven, an npm administrator may complete the separate security-key ceremony and set package Publishing access to Require two-factor authentication and disallow tokens. Do not report that setting as enabled until it is directly observed.
-
Update
package.json,package-lock.json, andrelease/compatibility.jsonto the exact release version. -
When that version changes, coordinate the matching explicit CLI comparator in
firstdraft/skillsbefore qualification; a stale comparator intentionally makes the three-repository candidate ineligible. -
Apply the pre-1.0 policy: use a minor increment for a breaking compatibility line and a patch increment for a change that is otherwise backward-compatible. Keep the initial distribution under
nextindependently of that version choice. Do not movelatestduring release publication. -
Confirm neither the exact package version nor its
v<package-version>tag already exists. If both remain absent, the unpublished candidate may retain its proposed version while its exact SHA and digests are revised. -
Re-run
npm trust list '@firstdraft.com/cli', verify the exactgithub/repository/file/environment/createPackagerelationship described above, and confirm the workflow contains no persistent npm credential or GitHub Actions secret. -
Update user-facing documentation and release notes for behavior changes.
-
Run:
npm ci --ignore-scripts npm audit npm run check
-
Merge the reviewed pull request only after local and hosted checks pass.
The manual boundary is creation of the version tag. From an up-to-date, clean main, verify the intended commit and
confirm that both the intended package version and v<package-version> tag are absent. If either identity is already
consumed, prepare the next version required by the pre-1.0 policy rather than moving or reusing it. Otherwise create
and push the tag. Push one release tag at a time; the workflow serializes publication, but GitHub retains at most one
pending run in a concurrency group.
The workflow rejects accidental or stale inputs unless they use a protected v* tag in firstdraft/cli, the tag
equals v plus the version in package.json, the remote tag still identifies the triggering commit, and that commit
appears in the first-parent history of origin/main. First-parent membership allows an older reviewed main state
after another change lands while rejecting intermediate commits from a merged side branch. The workflow reruns the
complete check, waits for approval in the npm environment, reverifies the remote refs, and publishes to the public
registry with provenance under next. It authenticates only through the exact npm trusted-publisher relationship and
the job's short-lived GitHub OIDC token; it reads no persistent npm credential or GitHub Actions secret.
The tag ruleset and npm environment approval are the external trust boundary because a tag-push run loads its
workflow from the tagged commit. Before approving the npm deployment, the reviewer must confirm:
- The tag, package version, and commit SHA are the intended release.
- The commit is a known reviewed state in protected
mainhistory and its required checks passed. .github/workflows/publish.ymlat that commit is the reviewed workflow, still selects thenpmenvironment, and publishes the public@firstdraft.com/clipackage only undernextwith provenance.- npm lists the exact
github/firstdraft/cli/publish.yml/npm/createPackagetrusted-publisher relationship, and the GitHub repository andnpmenvironment contain no npm automation secret. - The unprivileged verification job passed for that exact commit.
Do not move or reuse a release tag. If the tagged commit is not a first-parent state of main, merge the intended
change and prepare a new version rather than moving an already shared tag.
After publication, inspect the registry before retrying any reported failure; the package may already exist. From
the tagged checkout, verify the exact version, next dist-tag, unchanged latest dist-tag, integrity metadata, and
provenance metadata:
FD_CLI_RELEASE_VERSION="$(node -p "require('./package.json').version")"
npm view "@firstdraft.com/cli@$FD_CLI_RELEASE_VERSION" \
version dist.integrity dist.shasum repository.url engines bin --json
npm dist-tag ls '@firstdraft.com/cli'Install that exact version into a fresh temporary prefix, confirm firstdraft --version, compare the
packed file list with the release workflow, and run npm audit signatures after an exact installation.
If OIDC authentication fails, reconcile both the registry version and protected remote tag first. If only npm's listed relationship is wrong, correct it on npm and rerun the existing workflow's failed jobs without changing the tagged source. If the workflow filename, environment, permissions, or other identity at the tagged commit is wrong, the protected tag is immutable: prepare the next version rather than moving the tag. Never add a token fallback.
A published version cannot be overwritten or reused. For a bad release, move next only to a known-good compatible
version if one exists; otherwise deprecate the bad version and publish a corrected higher version. Treat
unpublishing as an exceptional incident response, not a routine rollback.
Publishing under next is not promotion to the default install channel. After the exact next version completes
its named release-specific qualification and a human separately approves promotion, one operator may move latest
to that exact version from the tagged checkout:
FD_CLI_RELEASE_VERSION="$(node -p "require('./package.json').version")"
npm dist-tag add "@firstdraft.com/cli@$FD_CLI_RELEASE_VERSION" latest
npm dist-tag ls '@firstdraft.com/cli'Verify both tags after the mutation, then append the exact dated observation and qualification boundary to
release history. The stable CLI release is complete only after next and latest both
name the exact release-specific qualified version. Do not call a candidate fully promoted before that equality is
observed. Do not move latest merely because a release merged, published successfully, or passed candidate
compatibility checks, and do not use a dist-tag change to repair or disguise a bad immutable version.