This repository is a pnpm monorepo containing several public npm packages
(runable, @runablejs/cli, create-runable) plus two private apps
(playground, website) that are never published. Versioning and
publishing are handled entirely by Changesets —
there is no manual npm version / npm publish step to remember.
devis the active development branch. Feature branches are created fromdevand merged back into it through a pull request.mainis the release branch. It only moves forward whendev(or a hotfix) is merged into it, and every push tomainis what triggers the release pipeline described below.
# 1. make your change
pnpm changesetpnpm changeset asks two questions:
-
Which packages did you change? Select every package whose published behavior changed. You don't need to select
playgroundorwebsite— they are private and excluded from versioning entirely. -
What kind of change is it?
- patch — bug fix, internal improvement, docs, anything that doesn't change the public API.
- minor — a new, backwards-compatible feature or API addition.
- major — a breaking change to the public API.
These keep their normal SemVer meaning even while the project is in
alpha/beta— see Prereleases below for how that interacts with the-alpha.N/-beta.Nsuffix.
Then write a short, user-facing description. It is copied straight into the
package's CHANGELOG.md, so write it for someone installing the package,
not for a teammate reading the diff.
This creates a Markdown file under .changeset/. Commit it together with
your code:
git add .
git commit
git pushAdd one whenever a change affects what a consumer of runable,
@runablejs/cli, or create-runable experiences: bug fixes, new options,
behavior changes, dependency bumps that affect the public surface, etc.
Create it as part of the same change — don't defer it to a follow-up commit or PR. If a change needs a changeset, it isn't finished until the changeset exists alongside it (patch/minor/major keep the meaning described above).
- Changes scoped to
playgroundorwebsiteonly. - Repo tooling that doesn't touch a published package (CI config, this file, root-level docs).
- Pure refactors with zero observable behavior change — use judgment; when in doubt, add a patch changeset, it costs nothing.
A PR without a changeset isn't blocked automatically today (no changeset status gate is wired into CI), so this is a matter of discipline rather
than an enforced check.
PR → CI (install, lint/typecheck/test/build) → merge into dev → …
… eventually dev is merged into main → Release PR (Changesets) → merge → publish
- CI (
.github/workflows/ci.yml) runs on every pull request targetingmainordev, and on every push todev. It installs with a frozen lockfile, then runs lint, typecheck, and test for whichever packages define those scripts (lint and test are no-ops today, since no package has alintortestscript yet — they'll start running automatically the day one is added), then builds every publishable package. - Merging into
main(directly, or via adev → mainPR) pushes tomain, which triggers.github/workflows/release.yml. Its first job,select-mode, looks at the.changeset/*.mdfiles currently onmainand decides what happens next:- Changesets are pending → the
versionjob opens/updates a Release PR titledchore(release): version packages. That PR contains the version bumps, the regeneratedCHANGELOG.mdfiles, the internalworkspace:*/workspace:^dependency bumps, and it deletes the changesets it consumed. Review it like any other PR — it's just a diff. - No changesets left (the Release PR was just merged) → the
packjob builds every package (pnpm build) and packs the publishable ones into tarballs, then thepublishjob pushes those tarballs to npm (private packages are never published). The three public packages form a Changesets fixed group, so they share one framework version. Changesets keeps a<name>@<version>tag for each package, while the workflow creates a single GitHub Release taggedv<version>with a changelog section for each package with user-facing changes. Packages bumped only to align the fixed group or update an internal dependency are omitted from the release notes. Contributors credited by those changelog entries are deduplicated and listed at the end of the release.
- Changesets are pending → the
- Publishing uses npm Trusted Publishing (OIDC) — no
NPM_TOKENis stored anywhere. Only thepublishjob gets an OIDC token (id-token: write); it authenticates to npm using its GitHub Actions identity, and provenance is attached automatically. See the engineering notes in the release-system PR for the one-time npm-side configuration this requires.
The project is currently in alpha (Changesets is in pre mode, tag
alpha — see .changeset/pre.json). This doesn't change your day-to-day
use of pnpm changeset: patch/minor/major always keep their normal
SemVer meaning — they determine the target version Changesets is
computing towards. Prerelease mode just appends a -alpha.N (or
-beta.N) suffix on top of that target once changeset version runs:
changeset
│
├── patch
├── minor
└── major
│
▼
target version (SemVer)
│
▼
prerelease mode active?
│
▼
<target version>-alpha.N
So keep choosing patch for a compatible fix, minor for a compatible
feature, and major for a breaking change exactly as you would outside
prerelease mode — Changesets takes care of turning that into the right
-alpha.N/-beta.N counter itself.
Changing the prerelease tag mid-prerelease does not go through pre exit. Per the official Changesets prerelease guide,
the supported way to move between prerelease stages (alpha → beta →
rc, etc.) is to edit the "tag" field in .changeset/pre.json directly:
{
"mode": "pre",
- "tag": "alpha"
+ "tag": "beta"
}Do this in its own dedicated PR (no unrelated changes bundled in), and commit it normally:
git add .changeset/pre.json
git commit -m "chore: move prerelease tag from alpha to beta"
git pushThe cycle looks like this:
alpha
│
▼
edit .changeset/pre.json: "tag": "alpha" → "beta"
│
▼
merge that PR into main
│
▼
next Changesets release cycle (version job, then publish)
│
▼
versions become 1.0.0-beta.N
The exact starting counter (beta.0 or otherwise) depends on Changesets'
internal state at that point — don't assume a specific number in advance.
To leave prerelease mode entirely, run once, in its own PR:
pnpm changeset pre exitThis only records the intent to exit prerelease mode in
.changeset/pre.json — it doesn't version or publish anything by itself.
The next regular release cycle (Release PR, then merge) is what actually
produces the stable version:
alpha
│
▼
beta
│
▼
pnpm changeset pre exit
│
▼
Release PR (next Changesets release cycle)
│
▼
stable release (e.g. 1.0.0)
│
▼
published under the npm "latest" dist-tag
Changesets keeps the npm dist-tag in sync with the Changesets prerelease tag automatically — nothing to configure on the npm side:
| Changesets pre tag | npm dist-tag |
|---|---|
alpha |
alpha |
beta |
beta |
| (not in pre mode) | latest |
In practice:
pnpm add runable@alpha # during the current alpha phase
pnpm add runable@beta # once the tag has moved to beta
pnpm add runable # once stable — resolves to the "latest" tag| Command | What it does |
|---|---|
pnpm changeset |
Interactively record what you changed and how (patch/minor/major). |
pnpm changeset status |
Show which packages currently have pending changesets, without changing anything. |
pnpm version-packages |
Apply pending changesets: bump versions, update changelogs, refresh the lockfile. Only ever run by the version job in release.yml — you don't need this locally. |
pnpm release:local |
Build every publishable package, then changeset publish. A manual/local fallback for an emergency publish (requires npm login first); the automated pipeline publishes through changesets/action/pack + changesets/action/publish (OIDC) instead. |
pnpm ci:local |
Run the same lint/typecheck/test/build sequence as CI, locally. (Named ci:local, not ci — pnpm ci is pnpm's own built-in alias for clean-install and would silently shadow a script named ci.) |
pnpm build |
Build runable and @runablejs/cli (the packages with a build step). |