Skip to content

Releasing

The rule in one line — Versioning: semver per package, one CHANGELOG, monthly minor releases, migration notes on every breaking rename. This file is that line written out, in the order a release actually raises the questions: what a release physically is today; which version a change needs; when one ships; how a component’s status and a package’s version relate; what a rename owes the people who read the old name; and where the record lives. Who may cut one is GOVERNANCE.md §2 — the design lead, and it is their action — and how a change reaches the queue is CONTRIBUTING.md.

Versions follow Semantic Versioning 2.0.0, the changelog follows Keep a Changelog 1.1.0, and a token says it is deprecated the way the DTCG format 2025.10 does.

1. What a release is — a tag, a publish, and four channels

Section titled “1. What a release is — a tag, a publish, and four channels”
  • A release is a git tag on main of the form <package>-v<semver> — tokens-v1.0.0, css-v1.0.0 — which is the form every release tag here takes (scripts/changelog.py, which refuses any other). It is cut on a commit where the package’s package.json states that version, npm test is green and artifacts/changelog.json is current. The tag message names the package and the version, and from this file on carries the headings of §6 for what changed.
  • Who cuts it: The design lead — a release reaches every consumer, so cutting a tag, pushing one or publishing is their action and never the builder’s.
  • What a tag delivers today, measured rather than assumed: the whole repository under the name pepper-ui, its files imported by path — pepper-ui/packages/tokens/dist/tokens.css — because npm installs the manifest at a git URL’s root. Since the packages were published, a release is the same tag and npm publish of the same commit as @pepperui/<package>@<version>: one version in two places, the tag first, never a version on npm that no tag carries.
  • A release is four channels, and one command runs it. The system reaches a consumer through the repository (the tag, and the manifest that states its version), npm (what npm install @pepperui/… gets), the design system in Claude Design (the artifact a canvas copies its tokens and its bundle from) and the documentation site (the version /changelog/ states to a reader with no terminal). Until this file said so, each was updated by a different hand-run act and nothing held them together: a token changed on Monday could be on npm and not in Claude Design, on the site and not on npm, and the only way to find out was to look in four places. python3 scripts/release.py runs the release across all four in the order §3 sets — it does every step that is the builder’s, and stops with the exact command for the two that are the design lead’s. python3 scripts/release.py --check reads the version back out of each channel and fails when two disagree; --plan says what the record measures the next version to be, and why. A version that has reached three channels is not released.
  • Four packages are versioned; the fifth is not. @pepperui/tokens, @pepperui/css, @pepperui/charts and, since its first release on 2026-09-25, @pepperui/illustrations each carry their own semver. The illustrations are a growing bank rather than a fixed set, so that package stays 0.x while its catalogue grows; its public API is dist/catalogue.json — the scene and object names, each with the sha1 of its committed still, and the element’s attributes — so a scene added is a minor, a scene redrawn is a value changed (a minor, under Changed, both stills shown), and a name removed is a major, exactly as the table below reads it. @pepperui/react is 0.0.0 and private: its items reach a project as a shadcn registry — copied-in code updates by shadcn diff, not by npm update — so it has no version to bump, and this file reaches it only through a component’s status (§4).
  • The Figma library is not a package. It is published when a consumer file needs it, not on this rhythm — publishing matters only when another Figma file subscribes to the library — and a library publish carries no version of its own; its publish note names the tokens version its variables match, because the variables are generated from the same source.
  • Consumers pin. The architecture’s bar says consumers pinned to a version: a consumer names the tag it reads — #css-v1.0.0 — or, once published, the exact version, and moves by its own choice, never by latest. A consumer that has not moved is not behind; it is pinned.
  • The documentation site is one of those consumers. docs-site/package.json pins @pepperui/tokens and @pepperui/css to exact published versions and the site installs them from npm into its own node_modules — it is outside the root workspaces, so it can never pick up the working folder under the same version number — the way the corporate site will install the kit. Its pages are still written from the repository at the commit; what styles them is the published kit, so the site is the live proof that what a developer installs works. A release moves that pin as its step 10, after the publish: the pin to the versions just published, a fresh install from npm, the installed files proved byte for byte to be what the tagged commit ships, the workshop and the site rebuilt and gated, and the move committed. python3 scripts/release.py --check reads the pin beside the four channels and fails when it disagrees with npm or the repository; scripts/docs_gate.py fails a built site whose kit is a range, a stale install or a link to packages/.
  • CI on a tag. bitbucket-pipelines.yml runs the build, the workshop and the site on any *-v* tag.

Semantic Versioning’s definition, verbatim: Given a version number MAJOR.MINOR.PATCH, increment the: 1. MAJOR version when you make incompatible API changes 2. MINOR version when you add functionality in a backward compatible manner 3. PATCH version when you make backward compatible bug fixes. A design system’s “API” is its public names — every --pui-* custom property, every .pui-* class and modifier, every chart theme key, every token path in figma.json and pepper_tokens.py — and its values are what those names resolve to. So:

The change Version Why
A public name removed, or renamed without an alias — a custom property, a class or modifier, a theme key, a token path, a whole component MAJOR a consumer’s stylesheet stops resolving; semver’s incompatible API changes
A public name added — a token, a class, a component, a theme value, a new modifier, an illustration scene or object, an attribute of the element MINOR add functionality in a backward compatible manner
A value changed on purpose — a colour, a size, a spacing step, a shadow, a font weight MINOR, listed under Changed with the old value and the new the name still resolves, so the code is compatible; the page looks different, so the person is not — it is never silent, and npm test re-runs the contrast matrix and the focus gate on it
A value corrected to what its source states, a build defect, an output that contradicted its own rule PATCH backward compatible bug fixes — the intended value did not change, the wrong one did
A rename with the old name kept as an alias MINOR, the old name Deprecated (§5) semver’s own rule for deprecation: update your documentation to let users know about the change and issue a new minor release with the deprecation in place
A component’s API — its modifiers, its markup, its properties — changed while the component is Beta MINOR, under Changed the ladder’s promise for Beta (§4): the API may still move
The same while the component is Ready MAJOR, or a deprecation first (§5) the ladder’s promise for Ready: the API changes only with a migration note
A change under packages/react no version — the registry has none a consumer pulls it by shadcn diff; the component’s status (§4) is the promise

Two consequences the table has that the one-line rule did not. A value change is a minor, not a patch: the focus ring’s border/focus #2b25d9 → #6a66e4 is exactly the kind of change a consumer must be able to see coming, and a patch is what a consumer takes without reading. A rename is a major unless the old name survives as an alias — and the alias is the cheaper path in every case this project has met, which is why §5 makes it the default.

  • A minor, monthly, at most one per package. It is cut with the monthly release note, which is its announcement — the note is written from the tags, not the tags from the note. A package with nothing changed since its last tag gets no tag that month: there should be an entry for every single version (Keep a Changelog), and so no version without an entry.
  • A patch, when it is needed, without waiting for the month; it is listed in the next note.
  • A major, announced one release ahead. The deprecation (§5) ships in a minor first, and the major that removes is cut on the monthly rhythm no sooner than the note after — so every consumer has read, in a release note, that a name is going before it goes.
  • The order of a cut, so the record and the tag cannot disagree: npm run build → npm test → python3 scripts/changelog.py → the version in package.json → npm run build (the outputs print the version) → one commit → python3 scripts/changelog.py and npm run docs:gen, so the record sees the new version → a second commit → the tag on that second commit, <package>-v<semver> with its message → push, tag included → python3 scripts/changelog.py again, because the record must now see the tag → commit → the release note. The tagged commit is one every check passes on — the tag pipeline runs them all (§1) — and its manifest states the version; a tag on a commit whose manifest says another version fails the record.
  • What is not on any rhythm: publishing — a decision of the design lead’s, once, after which every tag is also a publish.

4. Status and version are two different things

Section titled “4. Status and version are two different things”

A component’s status — Not started → Draft → Beta → Ready → Deprecated — is a promise about one component, made by the nine-point bar and kept by this file’s rules; a package version is a promise about one package’s names and values. The two are read together, and neither waits for the other:

  • Draft — being designed; do not build on it. It may change or vanish in any release.
  • Beta — usable; the API may still move, in a minor, with a Changed line that says how.
  • Ready — stable; the API changes only in a major, and a breaking change carries a migration note (§5).
  • Deprecated — has a replacement and a date: the major that removes it.

A package’s 1.0.0 does not make its components Ready, and Ready does not wait for a release. The queue’s own line is Nothing here is Ready until it passes the bar below — the bar, not a version — and the architecture’s bar for the word is Figma master + code + doc page (anatomy, props, do/don’t with why, tokens used) + a11y notes + visual test. A component moves to Ready when an approver holds it against the bar and says so on its queue row — a decision, because principle 11 rules out promotion by habit rather than by decision, and Status is enforced by the build, not displayed as decoration. What a version adds to the ladder is the size of the promise: Beta’s API moves in a minor, Ready’s only in a major.

5. Deprecation and migration — what a rename owes

Section titled “5. Deprecation and migration — what a rename owes”

The rule is migration notes on every breaking rename; this is what a note is, and the order it happens in. A rename is never a swap.

  1. The old name stays, as an alias, for at least one monthly minor. In the token source the alias is a token whose value references the new one, marked deprecated the way the format the source already uses says to — "$deprecated": true, or a string that says what to use instead (DTCG format 2025.10 §5.2.4) — so the build can list it and, later, refuse it. In @pepperui/css the old class is kept as a second selector on the same rule. artifacts/migration-map.csv is its shape — one row per old name, the new name beside it.
  2. The release ships as a minor with the rename under Deprecated (§6): old name → new name, and the version that will remove the old one.
  3. The release note says it — a manager reads this name is going, this is the new one, this is when before it goes.
  4. The removal is a major, listed under Removed, no sooner than the note after the deprecation (§3).
  5. A component is deprecated on the board with its replacement and the removing major’s month, and its page says so; a value that changes is not a deprecation — it is a Changed line (§2) with both values.

6. The record — the changelog and the note

Section titled “6. The record — the changelog and the note”
  • The CHANGELOG is the site’s /changelog/ page, read from the tags and the history rather than kept beside them: every tag against its manifest, each package’s surface at its release, and every commit since with what it added, removed and changed. It already keeps Keep a Changelog’s principles — for humans, not machines; an entry for every version; the latest version comes first; the release date shown; semver stated — and its Unreleased is that format’s word.
  • The types of change are Keep a Changelog’s six, used as the headings of a tag message and of a release note: Added — for new features · Changed — for changes in existing functionality · Deprecated — for soon-to-be removed features · Removed — for now removed features · Fixed — for any bug fixes · Security — in case of vulnerabilities. A migration note is a Deprecated line with its old → new pair and a Removed line in the major that follows.
  • The monthly note to managers is written from the tags: what shipped, what changed visibly (the Changed values, with both values), what is deprecated and when it goes, the decisions of the month, the requests answered and the time they took, and anything awaiting a second signature.

7. The first releases this file asked for — cut 2026-09-18

Section titled “7. The first releases this file asked for — cut 2026-09-18”

By §2’s own rules, applied to what the changelog record measured at the cut: @pepperui/tokens has 78 custom properties added and 9 values changed since tokens-v1.0.0, nothing removed — a minor, tokens-v1.1.0; @pepperui/css has 9 classes added and 43 stylesheets changed, nothing removed — a minor, css-v1.1.0; @pepperui/charts had never been tagged and says 1.0.0 — its first tag, charts-v1.0.0. The nine changed token values, border/focus among them, are under Changed with both values. All three were cut by the design lead on 2026-09-18, on one commit — the one where the changelog record already sees the new versions (§3). The message of tokens-v1.1.0 counts 140: every definition, a name set in each accent block counted once per block; the 78 here are names.

8. What this file decides, and what it waits on

Section titled “8. What this file decides, and what it waits on”
  • The four channels, the one command and the level a release takes from the record are recorded as D147, vetoable. What is not vetoable there is whose hands each step is: that is D75 and D49, and the command stops rather than taking one of them.
  • The first tags (§7) and publishing are the design lead’s.
  • The first release note.
  • The distance between majors. One monthly note between a deprecation and its removal — because the system has three consumers and one maintainer, and a year of aliases is a cost with no reader. If a fourth consumer arrives, this is the line to revisit.

Pepper UI v1.5.0

The Mobupps design system. 386 tokens, 88 components, 65 pages in the workshop — every page of this site generated from the repository, so nothing here is typed twice.