-
Notifications
You must be signed in to change notification settings - Fork 28
Release Process
Status: Active. All packages ship at the same version to public npm (npmjs.com), under the
@astryxdesign/*scope. CI handles publishing automatically — merge a version bump tomainand packages are published. Publishing uses npm trusted publishing (OIDC) — there is noNPM_TOKENanywhere. Current published baseline:@astryxdesign/core@0.0.15.
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 — CI builds and publishes to public npm automatically (tokenless OIDC)
- Post-release — Tag, 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. CI takes it from here.
When the version bump PR merges to main, the Deploy workflow (deploy.yml) runs automatically:
- Tests pass
- All packages are built (
pnpm build) - The
publishjob publishes any@astryxdesign/*package whose version is not yet on npm, viapnpm publish ... --provenance --access public --no-git-checks - Already-published versions are skipped (npm 409 Conflict → no-op), so re-running
mainis 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 —
deploy.ymlinfacebook/astryx— and nothing else. The trust is matched against the OIDC token'sworkflow_ref(the calling workflow), so the file that must be trusted is the one that runs the publish (deploy.yml), not any reusable workflow. - 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.
Background: PR #3037 migrated publishing to public npm; PR #3043 replaced the legacy token with OIDC trusted publishing. The repo doc
docs/release.mdis the deeper reference for the OIDC mechanics, pnpm/npm version floors, and the 2FA flow during setup.
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-trustWhat 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 deploy.yml --repo facebook/astryxto registerdeploy.ymlas the trusted OIDC publisher for that package.
After the first real OIDC publish from CI, the bootstrap stub is superseded by the real version (it lingers only under the deprecated bootstrap dist-tag). Skip this step and CI's 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.
-
Tag the release in git:
git tag vX.X.X git push --tags
-
Create the GitHub Release on that tag, with release notes. CI 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 CI publishes 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
⚠️ Canaries are not publishing right now (repo is still private). Thecanaryjob publishes with--provenance, and npm provenance requires a public source repo — it fails with HTTP 422 whilefacebook/astryxis private. Canary publishes will start working automatically once the repo goes public. The mechanics below describe the intended behavior post-go-live.
The canary job in deploy.yml publishes a canary version on every push to main. The @canary dist-tag always points at the latest main commit. No manual steps needed.
<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 |
Astryx is configured to publish with provenance — a signed, cryptographic attestation that links each npm tarball to the exact GitHub commit and workflow run that built it. Because provenance does not work on private repos, the full provenance story is gated on the repo going public:
- While
facebook/astryxis private, provenance fails (HTTP 422). This is why canaries don't publish yet (see above). -
PR #3050 (
[DO NOT LAND]until go-live) switches the stablepublishjob to a per-package loop that enforces provenance on thelatesttag — a publish that can't attach provenance fails the job rather than silently shipping an unattested package. It lands once the repo is public.
Once the repo is public, both stable and canary publishes attach provenance automatically with no further action.
- 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) - Previous release is tagged
- Version bump PR created and reviewed
- 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, build from that commit, and re-run the deploy. Because publishing is tokenless OIDC trusted publishing (no local npm token), the supported path is to re-trigger deploy.yml for that commit rather than publishing by hand:
git checkout vX.X.X
pnpm install && pnpm build
# Publishing happens in CI via deploy.yml (OIDC trusted publishing) — there is no
# local npm token. Re-run the Deploy workflow for this ref (workflow_dispatch) so the
# tokenless publish/provenance path runs; the publish job no-ops any already-live version.
git checkout main# 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 version bump PR, merge → CI publishes automatically (OIDC trusted publishing, no token)
# After publish: tag + create the GitHub Release with compiled notes (manual — CI does not)
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 # claim name + register deploy.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