The package publishes as @imjohnbo/kit-cli.
The unscoped kit-cli on npm belongs to another author. It has existed since
2015 and sits at version 0.0.4, so that name is not available.
The installed command is still kit, because bin names it separately from the
package.
To move to a different name later, such as @kit/cli once that organization
exists, change name in package.json and nothing else.
src/package-info.js reads it, so the update check and kit upgrade follow. Then
publish the old name one last time with a deprecation pointing at the new one.
A scoped package needs publishConfig.access set to public. It is already set.
A pushed version tag starts the workflow. A manual approval finishes it. Nothing else publishes.
npm version patch # or minor, or major
git push --follow-tags
npm version bumps package.json, makes a commit, and creates the tag. The
--follow-tags push sends the tag, and the tag starts
.github/workflows/release.yml.
The project starts at 0.0.1. It is pre-release, so treat the surface as
unstable and expect breaking changes in minor bumps until 1.0.0.
A prerelease tag such as v0.1.0-rc.1 also triggers the workflow. It publishes
under the next dist-tag rather than latest, so kit upgrade does not move
users onto it.
The workflow has three jobs. The first two run on their own. The third waits for a person.
| Job | Runs | Stops the release when |
|---|---|---|
verify |
Node 20, 22 | A test fails |
verify-package |
Once | The version breaks a semver rule, or the tarball does not match the source |
publish |
After approval | The repacked tarball differs from the verified one |
The publish job targets the npm-publish environment. Add a required reviewer
to that environment. A person then has to approve every release. A pushed tag on
its own cannot ship anything.
- Create a GitHub environment named
npm-publish. Add yourself as a required reviewer. - Set up npm trusted publishing for the
@imjohnbo/kit-clipackage. Point it at this repository and atrelease.yml. The workflow then publishes with the OIDC token and needs no npm token. - If you skip step 2, create an npm automation token instead. Store it in the
npm-publishenvironment as theNPM_TOKENsecret. The workflow reads either one.
package.json sets publishConfig.provenance to true. A local npm publish
therefore fails, because provenance needs a CI identity. This is deliberate. It
stops an unattested build from reaching the registry.
npm run check:semver runs four checks. CI runs the same script on every tagged
release.
- The version is valid semver. A syntax check.
- The tag matches
package.json. Av0.1.0tag cannot ship0.0.9. - The version is newer than what npm serves. This blocks a re-publish and blocks going backwards.
- A breaking change carries a big enough bump. This is the only check that enforces the meaning of a version.
Check 4 needs a machine-readable definition of the public surface. For a library that would be the exported symbols. For a CLI it is the command tree: the commands, their arguments, and their flags. Removing or renaming any of those breaks somebody's script. Adding one does not.
spec/cli-surface.json holds that surface. Regenerate it with:
npm run surface
A test asserts the committed snapshot matches the current tree, so a surface change has to be committed on purpose and shows up in review. On release, the gate reads the snapshot from the previous tag, compares it to the one being released, and classifies the difference.
| Surface change | Smallest allowed bump below 1.0.0 | From 1.0.0 on |
|---|---|---|
| A command or flag was removed or renamed | minor | major |
| A required argument was added | minor | major |
| An argument became required | minor | major |
| A command or flag was added | minor | minor |
| Nothing changed | patch | patch |
While the major version is 0, a breaking change needs a minor bump rather than a major one. Semver leaves 0.x unstable, but npm's caret range treats minor as the breaking axis below 1.0.0, and the ecosystem reads it that way.
Prereleases skip check 4. They exist to ship an unstable surface.
The gate sees the shape of the command tree. It does not see behavior. A flag that keeps its name and changes its meaning, an output format that changes, or an exit code that changes will all pass. Those still need a human to notice and to bump accordingly.
Three independent checks. Each answers a different question.
Did this package come from this repository? npm provenance answers this. It links the tarball to the workflow run and the commit.
npm audit signatures
Is the release asset the same artifact? The GitHub attestation answers this.
gh attestation verify <tarball> --repo imjohnbo/kit-cli
Does the package contain the source, and nothing else? The
verify-package job answers this, and it is the check that matters most.
npm provenance proves where a tarball was built. It does not prove that the tarball matches the source. A workflow step between checkout and publish could change a file and still produce valid provenance.
kit-cli has no build step and no devDependencies. The published tarball is the source. So CI proves it directly: pack the tree, unpack the tarball, and diff the two. The release stops if they differ.
Keep it that way. A build step, a generated file, or a code-generation step would end this guarantee. If you ever need one, publish the generated file to the repository as well, so the diff still holds.
- Merge the work. Make sure
mainis green. - Run
npm testlocally. - Run
npm run surface. Commit the result if it changed. - Run
npm run check:semverto see how the gate reads the change. - Run
npm version patch,npm version minor, ornpm version major. - Run
git push --follow-tags. - Watch the
verifyandverify-packagejobs. - Approve the
npm-publishenvironment. - Check that
npm view <name> versionreports the new version. - Run
npm audit signaturesin a scratch install as a smoke test.
package.json holds the version. src/package-info.js reads it at run time.
src/program.js passes it to commander. The same module reads the package name,
so renaming the package is a single edit.
Do not add a second copy. Two tests guard this:
scripts/upgrade.test.jsasserts that no file insrc/hardcodes the version.scripts/spec-coverage.test.jsasserts thatkit --versionmatchespackage.json.
verify-package repeats the second check in CI, so a mismatched tag can never
publish.
Do not delete a published npm version. Unpublishing breaks anyone who already installed it, and npm blocks republishing the same version number.
Publish a patch release instead. If the bad version must stop reaching new users, deprecate it:
npm deprecate <name>@0.0.2 "Broken release. Use 0.0.3."
Then move the latest tag if it still points at the bad version:
npm dist-tag add <name>@0.0.3 latest
kit upgrade hands the work to the package manager that installed the CLI. It
does not download or unpack a release itself. The manager already verifies
tarball integrity and provenance. A hand-rolled updater would replace that with
unaudited code.
The passive notice reads a cached version number from config. It never blocks a
command and never fails one. A background request refreshes the cache at most
once a day. The notice goes to stderr, so --format json output stays parseable.
Users turn it off in two ways:
kit config set-update-check false
export KIT_NO_UPDATE_CHECK=1
The check also stays off when CI is set, so pipelines make no outbound request.
Every action is pinned to a commit SHA, with the version in a trailing comment:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1A tag is mutable. @v7 is a promise from the action's owner, not a guarantee, so
a compromised or retagged release would flow straight into a job that holds
id-token: write. A SHA cannot move.
Check for updates with gh api repos/actions/checkout/releases/latest, or let
Dependabot raise the pull request. Dependabot reads the version comment and keeps
it in step with the SHA.