-
Notifications
You must be signed in to change notification settings - Fork 27
Release Process
Status: Active. All packages ship at the same version to public npm (npmjs.com), under the
@astryxdesign/*scope. Publishing lives in the dedicated Release workflow (release.yml) and uses npm trusted publishing (OIDC) — there is noNPM_TOKENanywhere. Stable (latest) publishing is a manual on-demand step: merging the version-bump PR does not publish — you then dispatchrelease.ymlto publish the bumped versions. Only canary publishes automatically (on every push tomain). Current published baseline:@astryxdesign/core@0.1.1.
Astryx releases all packages at the same version number for clear compatibility. The release process:
-
Accumulate changes — PRs land on
mainwith changesets - Identify codemods — Breaking changes get AST-based codemods
-
Version bump —
pnpm version-packagesapplies changesets, create a PR - Merge, then publish — merge the version-bump PR (this bumps versions; it does not publish), then dispatch the Release workflow to publish the bumped versions to public npm (tokenless OIDC)
- Post-release — Tag, create the GitHub Release, update agent docs, internal sync, announce
All non-private packages are published together at the same version (a Changesets fixed group). All 12 publishable packages share the @astryxdesign/* scope and are published to public npm:
| Package | Contents |
|---|---|
@astryxdesign/core |
Components, hooks, utilities, tokens, CSS |
@astryxdesign/cli |
CLI commands: init, swizzle, upgrade, agent-docs, component, docs, template |
@astryxdesign/build |
Shared build tooling (tsup/StyleX config) |
@astryxdesign/theme-default |
Default theme (Heroicons) |
@astryxdesign/theme-neutral |
Neutral theme (Lucide icons) |
@astryxdesign/theme-butter · theme-chocolate · theme-daily · theme-gothic · theme-matcha · theme-stone · theme-y2k
|
Additional published themes |
@astryxdesign/lab, @astryxdesign/vega, @astryxdesign/theme-brutalist, and the apps/internal packages are private and never published. @astryxdesign/storybook and @astryxdesign/sandbox are in the changesets ignore list. (Source of truth: the fixed array in .changeset/config.json.)
Rule: All packages bump to the same version. This is enforced by the fixed group — a single changeset co-bumps every publishable package, so if you're on @astryxdesign/core@0.0.15, you use @astryxdesign/theme-default@0.0.15.
Every PR that changes published package behavior should include a changeset.
pnpm changeset:newThis Astryx wrapper (around the Changesets CLI):
- Auto-detects which publishable packages your working tree touched and pre-selects them — no hand-enumerating the frontmatter.
- Prompts for a category (
breaking,component,feat,fix,perf,docs,chore) — this drives changelog grouping, not the semver bump. - Captures the contributor(s) — defaults to your
gh/git identity, so credit is recorded at authoring time. -
Forces a
patchbump while we're pre-1.0 (see the rule below).
The changeset file is committed with the PR. The Astryx body convention is a [category] headline followed by a @handle line:
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch
---
[component] Added CommandPalette component and `xds upgrade` CLI command (#2717)
@yourhandleYou can pass everything as flags for non-interactive use:
pnpm changeset:new --category fix --summary "…" --pr 2717 --contributor yourhandleThe bare pnpm changeset CLI still works, but then you must follow the body convention by hand. CI (pnpm check:changesets, part of check:repo) rejects any changeset that is missing a category or contributor, declares a minor/major bump while pre-1.0, or names a private/ignored package.
⚠️ Pre-1.0 rule: Always usepatchfor changesets while we're on 0.0.x. The Changesets CLI treatsminoras a semver-minor bump, which jumps 0.0.x → 0.1.0. Signal a breaking change with the[breaking]category, not aminor/majorbump.pnpm changeset:newenforcespatchautomatically;pnpm check:changesetsis the CI backstop. (When we hit 1.0, the patch-only gate lifts automatically — it keys off whether publishable packages are still0.x.)
| Change type | Category | Bump (pre-1.0) | Changeset needed? |
|---|---|---|---|
| New component | component |
patch | Yes |
| New feature/prop | feat |
patch | Yes |
| Bug fix | fix |
patch | Yes |
| Prop rename | breaking |
patch | Yes — also needs a codemod |
| Component removal | breaking |
patch | Yes — also needs a codemod |
| Perf improvement | perf |
patch | Yes |
| Docs only | — | — | No |
| Tests only | — | — | No |
| Internal tooling (storybook, vibe-tests) | — | — | No (these are in ignore list) |
- The first body line is
[category] one-line user-facing summary (#PR). - The second line is the contributor handle(s):
@yourhandle(space-separated for multiple). - If there's a breaking change, use the
[breaking]category and mention the codemod:
---
'@astryxdesign/core': patch
---
[breaking] Renamed `items` prop to `options` on Selector for clarity (#2717)
@yourhandle
**Codemod:** `npx xds upgrade --codemod rename-selector-items-to-options`Before releasing, audit all changesets for breaking API changes that need codemods.
| Change | Codemod needed? | Example |
|---|---|---|
| Prop renamed | ✅ Yes |
items → options on Selector |
| Prop removed | ✅ Yes | Remove deprecated prop, add TODO comment |
| Component renamed | ✅ Yes |
HStack → Stack direction="horizontal"
|
| Component removed | ✅ Yes | Replace with new component + direction/variant prop |
| Callback signature changed | ✅ Yes |
onHide: () => void → onOpenChange: (isOpen: boolean) => void
|
| Two props merged into one | ✅ Yes |
onShow/onHide → onOpenChange
|
| New required prop added | If there's a sensible default, no codemod needed | |
| New optional prop | ❌ No | Additive, non-breaking |
| New component | ❌ No | Additive |
| Bug fix | ❌ No | Unless it changes expected behavior |
| Import path changed | ✅ Yes |
@astryxdesign/core/Layout → @astryxdesign/core/Stack
|
git log v0.0.15..HEAD --oneline -- packages/core/src/ | grep -iE 'rename|remove|refactor|unify|deprecat|breaking'Or check the accumulated changesets:
ls .changeset/*.mdCodemods live in packages/cli/src/codemods/transforms/v{VERSION}/. Each transform is a jscodeshift module. See existing transforms for patterns (prop rename, component rename, import rewrite).
- Create the transform file in
transforms/v{VERSION}/ - Create a test file in
transforms/v{VERSION}/__tests__/ - Add it to
transforms/v{VERSION}/index.mjsmanifest - If it's a new version directory, add it to
codemods/registry.mjs
pnpm test --run packages/cli/src/codemods/Once all PRs are merged and codemods are ready:
pnpm version-packagesThis runs changeset version (bumps versions, generates CHANGELOGs, deletes the consumed changesets) and then scripts/format-changelogs.mjs to format the output.
All publishable packages are a Changesets fixed group, so a single changeset co-bumps all of them to the same version automatically — no need to manually bump packages to match. Only genuinely-affected packages receive a changelog entry; the rest get a clean version-only bump.
pnpm version-packages already runs scripts/format-changelogs.mjs, which rewrites each just-bumped package CHANGELOG into the doc-site format:
| Element | Heading level | Example |
|---|---|---|
| Version |
# 0.0.16 (h1) |
# 0.0.16 |
| Section |
#### Breaking Changes (h4) |
#### Fixes |
| Divider |
--- between versions |
--- |
Category sections render in canonical order (Breaking Changes → New Components → New Features → Fixes → Performance → Documentation → Other Changes). The formatter is idempotent and has a --check mode (node scripts/format-changelogs.mjs --check) for CI drift detection. You normally don't touch CHANGELOGs by hand — just review the generated output.
This aligns with the doc site's Markdown rendering which uses headingLevelStart={1}, giving version numbers prominent h1 sizing.
The #### Contributors section is generated from the @handle lines captured in each changeset at authoring time — so it credits the real humans, not the release bot. No git log / gh pr list reconstruction needed.
If a contributor is missing (e.g. an old-format changeset slipped through), add their
@handleto the relevant changeset body and re-runpnpm version-packages, or edit the generated#### Contributorslist directly.
git checkout -b chore/version-packages-vX.X.X
git add .
git commit -m "chore: version packages for vX.X.X"
git push -u origin chore/version-packages-vX.X.X
gh pr create --title "chore: version packages for vX.X.X" --body "Version bump for release X.X.X"Merge the PR. Merging bumps the versions on main but does not publish — continue to Step 4 to dispatch the Release workflow.
All npm publishing lives in the dedicated Release workflow (release.yml) — not deploy.yml (which is website-only: it builds Storybook + Sandbox to GitHub Pages and publishes nothing to npm). release.yml has two jobs, selected by trigger:
| Job | Trigger | dist-tag | Notes |
|---|---|---|---|
publish (stable) |
workflow_dispatch only |
latest |
Manual, on-demand. Version-gated + idempotent. Supports a dry-run input. |
canary |
push to main (automatic) |
canary |
Publishes 0.x.y-canary.<short-sha> on every push. |
Merging the version-bump PR does NOT publish the stable release. It only lands the bumped package.json versions and CHANGELOGs on main (and triggers a canary). To ship the stable latest release you must manually dispatch the Release workflow against main:
# Optional dry-run first: build + resolve what would publish, publish nothing
gh workflow run release.yml --ref main -f dry-run=true
# Real publish
gh workflow run release.yml --ref main
# Watch it
gh run list --workflow=release.yml -L 3(You can also dispatch it from the GitHub UI: Actions → Release → Run workflow → main.)
What the stable publish job does:
- Checks out the dispatched ref and builds all packages (
pnpm build) - For each
@astryxdesign/*package, publishes the currentpackage.jsonversion viapnpm publish ... --tag latest --provenance --access public --no-git-checks -
Version-gated + idempotent: any version already on the registry is skipped, so re-running (or running
dry-runfirst) is safe
No manual npm publish needed. No npm auth tokens — anywhere. Publishing uses npm trusted publishing (OIDC):
- There is no
NPM_TOKENsecret in CI or on anyone's machine. Thepublish(andcanary) jobs are grantedid-token: write; pnpm exchanges a short-lived GitHub OIDC token for a registry credential at publish time. - npm trusts exactly one GitHub Actions workflow per package. Because npm matches the OIDC token's
workflow_ref(the calling workflow) and allows only one trusted publisher per package, both stable and canary publishing live in the single filerelease.yml, and the npm trust config must point atrelease.yml. (Re-point withnode scripts/npm/setup-trusted-publishing.mjs --setup-trust --replace --workflow release.yml.) - Every published package carries provenance (
--provenance) — a cryptographic attestation of where and how it was built, linking the npm tarball back to the exact GitHub commit + workflow run.facebook/astryxis public, so provenance works for both stable and canary.
Background: PR #3037 migrated publishing to public npm; PR #3043 replaced the legacy token with OIDC trusted publishing; publishing was later split out of
deploy.ymlinto the dedicatedrelease.ymlso a single npm trusted-publisher config covers both stable and canary, and so thepages-deployconcurrency group can't starve npm publishing.
Trusted publishing is currently configured per-package — there is no org-wide trusted publishing yet. npm also can't register trust on a name that doesn't exist on the registry (unlike PyPI, there is no "pending publisher"). So whenever a new @astryxdesign/* package is added to the publishable set, an npm org owner (e.g. @cixzhang) must bootstrap it before CI can publish it:
npm i -g npm@latest
npm login --registry https://registry.npmjs.org # must be an @astryxdesign org owner
pnpm run setup-trusted-publishing # audit: shows which packages need bootstrap/trust
pnpm run setup-trusted-publishing --bootstrap --setup-trust --workflow release.ymlWhat this does:
-
Bootstrap — publishes a deprecated placeholder
0.0.0-bootstrap.0stub (under thebootstrapdist-tag, neverlatest) to claim the package name on npm. This is why a brand-new package first appears on npm at version0.0.0-bootstrap.0. -
Setup-trust — runs
npm trust github <pkg> --file release.yml --repo facebook/astryxto registerrelease.ymlas the trusted OIDC publisher for that package.
After the first real OIDC publish, the bootstrap stub is superseded by the real version (it lingers only under the deprecated bootstrap dist-tag). Skip this step and the publish of the new package will fail — npm rejects the OIDC publish for a name it has no trust config for. This is a required manual step in the "add a package" path until org-wide trusted publishing exists.
The
pnpm run setup-trusted-publishingscript is a maintainer-only, run-locally tool with an interactive npm session — it is decoupled from CI and never publishes real releases. It supports--dry-runand an audit-only default (no flags). Requires npm ≥ 11.10 fornpm trust. (Note: the script's built-in--workflowdefault may still readdeploy.ymlin older checkouts — always pass--workflow release.ymlto match where publishing now lives.)
-
Tag the release in git:
git tag vX.X.X git push --tags
-
Create the GitHub Release on that tag, with release notes. The Release workflow publishes to npm but does not open a GitHub Release — this is a manual step. The release notes are compiled from the per-package
CHANGELOG.mdentries for the version just shipped, grouped by package (Breaking Changes → New Features → Fixes → Documentation → Other), plus the contributor list and a compare link.# Pull the version's section out of each package CHANGELOG into one file : > /tmp/notes.md for pkg in core cli build lab; do f="packages/$pkg/CHANGELOG.md" section=$(awk '/^# X\.X\.X$/{flag=1;next} /^# [0-9]/{if(flag)exit} flag' "$f") [ -n "$(echo "$section" | tr -d '[:space:]')" ] || continue echo "## @astryxdesign/$pkg" >> /tmp/notes.md echo "$section" >> /tmp/notes.md done # …then edit /tmp/notes.md: dedupe the per-package Contributors lists into one, # and append a compare link. echo "**Full Changelog**: https://github.com/facebook/astryx/compare/vPREV...vX.X.X" >> /tmp/notes.md gh release create vX.X.X --title vX.X.X --notes-file /tmp/notes.md --verify-tag
Public-repo hygiene: release notes are a public surface. Scrub any internal references (task/diff numbers, internal infra, unixnames) — public GitHub
@handlesfor contributor credit are fine. Publish as a normal (non-draft, non-prerelease) release. -
Update agent docs in consumer projects:
npx xds agent-docs
-
Notify consumers — post in the Astryx chat spaces with a summary of changes and the upgrade command.
After the stable release is published to public npm, internal consumers need to be updated. This involves updating shared libraries in the internal monorepo and submitting a diff.
Once the internal sync lands:
- When an AI agent runs
xds component <Name>, the CLI readsLATEST_VERSIONvia thexds.versionFileconfig - If the installed version is older, the CLI prints an upgrade nudge
- The user decides when to upgrade — the agent won't upgrade automatically
Post a release announcement to the Astryx Open Source Workplace group and GChat space (spaces/AAQAmz6AQ8Y).
Write every line item from the builder's perspective — how they experience the change in their product or dev workflow.
Rules:
- Lead with the outcome — what's better for the builder now?
- One sentence max per highlight
- Skip internal-only changes
- Breaking changes: reassure, don't alarm — mention the codemod handles it
- Group related fixes
The canary job in release.yml publishes a canary version on every push to main (automatically — this is the one publish path that needs no manual dispatch). The @canary dist-tag always points at the latest main commit. facebook/astryx is public, so canary publishes carry --provenance.
<base-version>-canary.<short-sha>
e.g. 0.0.15-canary.fd7c751
npm install @astryxdesign/core@canarySafety guarantee: npm install @astryxdesign/core (no tag) always gets the stable @latest release. Canary versions live on a separate dist-tag.
| Scenario | Use canary? |
|---|---|
| Test codemods on internal apps before a stable release | ✅ Yes |
| Validate a breaking change on a feature branch | ✅ Yes |
| Quick fix for a codemod bug post-release | ✅ Yes |
| Routine release with no breaking changes | ❌ No — just publish stable |
Both stable and canary publishes attach provenance — a signed, cryptographic attestation that links each npm tarball to the exact GitHub commit and workflow run that built it. Provenance requires a public source repo; facebook/astryx is public, so every publish from release.yml carries provenance automatically with no further action. The stable publish job publishes per package with --provenance; a publish that can't attach provenance fails the job rather than silently shipping an unattested package.
- All breaking changes have codemods with tests
- CHANGELOGs follow the standard format
- All packages at the same version number
- Any new package has been bootstrapped + trust-configured by an npm org owner (
pnpm run setup-trusted-publishing ... --workflow release.yml) - Previous release is tagged
- Version bump PR created and reviewed
- Dispatch the Release workflow to publish stable:
gh workflow run release.yml --ref main(optionally-f dry-run=truefirst) - Verify:
npm view @astryxdesign/core dist-tags - Tag:
git tag vX.X.X && git push --tags - GitHub Release created on the tag with compiled release notes (
gh release create vX.X.X --notes-file …) - Internal sync diff submitted
- Release announcement posted
main. Always checkout the exact tag and build from that commit. Because publishing is tokenless OIDC trusted publishing (no local npm token), the supported path is to dispatch release.yml against that ref rather than publishing by hand:
# Dispatch the Release workflow for the exact tag/ref you want republished.
# The publish job no-ops any version already live, and rebuilds from that commit.
gh workflow run release.yml --ref vX.X.X
gh run list --workflow=release.yml -L 3# Add a changeset to your PR (auto-detects packages, prompts category + contributor)
pnpm changeset:new
# Check pending changesets
ls .changeset/*.md
# Validate changesets (category, contributor, patch-only) — also runs in CI
pnpm check:changesets
# Version bump (changeset version + CHANGELOG formatting)
pnpm version-packages
# Create the version-bump PR, merge it (bumps versions on main — does NOT publish)
# Publish the stable release: manually dispatch the Release workflow against main
gh workflow run release.yml --ref main -f dry-run=true # optional: preview what would publish
gh workflow run release.yml --ref main # real publish (latest dist-tag, OIDC, no token)
gh run list --workflow=release.yml -L 3 # watch
# (canary publishes automatically on every push to main — no dispatch needed)
# After publish: tag + create the GitHub Release with compiled notes (manual — neither workflow does)
git tag vX.X.X && git push --tags
gh release create vX.X.X --title vX.X.X --notes-file /tmp/notes.md --verify-tag
# Adding a NEW package: an npm org owner bootstraps + registers trust (one-time, local)
pnpm run setup-trusted-publishing # audit
pnpm run setup-trusted-publishing --bootstrap --setup-trust --workflow release.yml # claim name + register release.yml as trusted publisher
# Consumers upgrade
npx xds upgrade --apply- Distribution — Packages, versioning, source and dist bundles
- API Conventions — Naming and prop conventions that inform when codemods are needed