Skip to content

Release Process

cixzhang edited this page Jun 27, 2026 · 4 revisions

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 no NPM_TOKEN anywhere. Stable (latest) publishing is a manual on-demand step: merging the version-bump PR does not publish — you then dispatch release.yml to publish the bumped versions. Only canary publishes automatically (on every push to main). Current published baseline: @astryxdesign/core@0.1.1.

Overview

Astryx releases all packages at the same version number for clear compatibility. The release process:

  1. Accumulate changes — PRs land on main with changesets
  2. Identify codemods — Breaking changes get AST-based codemods
  3. Version bumppnpm version-packages applies changesets, create a PR
  4. 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)
  5. Post-release — Tag, create the GitHub Release, update agent docs, internal sync, announce

Packages

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.


Step 1: Changesets

Every PR that changes published package behavior should include a changeset.

Creating a changeset

pnpm changeset:new

This Astryx wrapper (around the Changesets CLI):

  1. Auto-detects which publishable packages your working tree touched and pre-selects them — no hand-enumerating the frontmatter.
  2. Prompts for a category (breaking, component, feat, fix, perf, docs, chore) — this drives changelog grouping, not the semver bump.
  3. Captures the contributor(s) — defaults to your gh/git identity, so credit is recorded at authoring time.
  4. Forces a patch bump 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)
@yourhandle

You can pass everything as flags for non-interactive use:

pnpm changeset:new --category fix --summary "" --pr 2717 --contributor yourhandle

The 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 use patch for changesets while we're on 0.0.x. The Changesets CLI treats minor as a semver-minor bump, which jumps 0.0.x → 0.1.0. Signal a breaking change with the [breaking] category, not a minor/major bump. pnpm changeset:new enforces patch automatically; pnpm check:changesets is the CI backstop. (When we hit 1.0, the patch-only gate lifts automatically — it keys off whether publishable packages are still 0.x.)

When to create a changeset

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)

Changeset conventions

  • 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`

Step 2: Identify Codemods

Before releasing, audit all changesets for breaking API changes that need codemods.

What needs a codemod

Change Codemod needed? Example
Prop renamed ✅ Yes itemsoptions on Selector
Prop removed ✅ Yes Remove deprecated prop, add TODO comment
Component renamed ✅ Yes HStackStack direction="horizontal"
Component removed ✅ Yes Replace with new component + direction/variant prop
Callback signature changed ✅ Yes onHide: () => voidonOpenChange: (isOpen: boolean) => void
Two props merged into one ✅ Yes onShow/onHideonOpenChange
New required prop added ⚠️ Maybe 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

How to audit

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/*.md

Writing a codemod

Codemods 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).

Registering a codemod

  1. Create the transform file in transforms/v{VERSION}/
  2. Create a test file in transforms/v{VERSION}/__tests__/
  3. Add it to transforms/v{VERSION}/index.mjs manifest
  4. If it's a new version directory, add it to codemods/registry.mjs

Testing codemods

pnpm test --run packages/cli/src/codemods/

Step 3: Version Bump

Once all PRs are merged and codemods are ready:

pnpm version-packages

This 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.

CHANGELOG formatting (automatic)

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.

Contributors (automatic)

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 @handle to the relevant changeset body and re-run pnpm version-packages, or edit the generated #### Contributors list directly.

Create a version bump PR

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.


Step 4: Publish (manual dispatch of 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:

  1. Checks out the dispatched ref and builds all packages (pnpm build)
  2. For each @astryxdesign/* package, publishes the current package.json version via pnpm publish ... --tag latest --provenance --access public --no-git-checks
  3. Version-gated + idempotent: any version already on the registry is skipped, so re-running (or running dry-run first) is safe

No manual npm publish needed. No npm auth tokens — anywhere. Publishing uses npm trusted publishing (OIDC):

  • There is no NPM_TOKEN secret in CI or on anyone's machine. The publish (and canary) jobs are granted id-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 file release.yml, and the npm trust config must point at release.yml. (Re-point with node 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/astryx is 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.yml into the dedicated release.yml so a single npm trusted-publisher config covers both stable and canary, and so the pages-deploy concurrency group can't starve npm publishing.

Adding a NEW package (one-time bootstrap by an npm org owner)

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.yml

What this does:

  1. Bootstrap — publishes a deprecated placeholder 0.0.0-bootstrap.0 stub (under the bootstrap dist-tag, never latest) to claim the package name on npm. This is why a brand-new package first appears on npm at version 0.0.0-bootstrap.0.
  2. Setup-trust — runs npm trust github <pkg> --file release.yml --repo facebook/astryx to register release.yml as 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-publishing script 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-run and an audit-only default (no flags). Requires npm ≥ 11.10 for npm trust. (Note: the script's built-in --workflow default may still read deploy.yml in older checkouts — always pass --workflow release.yml to match where publishing now lives.)


Step 5: Post-Release

  1. Tag the release in git:

    git tag vX.X.X
    git push --tags
  2. 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.md entries 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 @handles for contributor credit are fine. Publish as a normal (non-draft, non-prerelease) release.

  3. Update agent docs in consumer projects:

    npx xds agent-docs
  4. Notify consumers — post in the Astryx chat spaces with a summary of changes and the upgrade command.


Step 6: Internal Sync

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.

How this enables the update loop

Once the internal sync lands:

  1. When an AI agent runs xds component <Name>, the CLI reads LATEST_VERSION via the xds.versionFile config
  2. If the installed version is older, the CLI prints an upgrade nudge
  3. The user decides when to upgrade — the agent won't upgrade automatically

Step 7: Release Announcement

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:

  1. Lead with the outcome — what's better for the builder now?
  2. One sentence max per highlight
  3. Skip internal-only changes
  4. Breaking changes: reassure, don't alarm — mention the codemod handles it
  5. Group related fixes

Canary Builds

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.

Version format

<base-version>-canary.<short-sha>
e.g. 0.0.15-canary.fd7c751

How consumers install a canary

npm install @astryxdesign/core@canary

Safety guarantee: npm install @astryxdesign/core (no tag) always gets the stable @latest release. Canary versions live on a separate dist-tag.

When to use canaries

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

Provenance

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.


Pre-Release Checklist

  • 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

After merge — publish + post-release

  • Dispatch the Release workflow to publish stable: gh workflow run release.yml --ref main (optionally -f dry-run=true first)
  • 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

Republishing an Older Version

⚠️ Never publish old version numbers from current 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

Quick Reference

# 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

Related

  • Distribution — Packages, versioning, source and dist bundles
  • API Conventions — Naming and prop conventions that inform when codemods are needed

Clone this wiki locally