Releasing @workflowbuilder/sdk
August 14, 2026 · View on GitHub
Maintainer-only procedure. Consumer docs live in README.md. High-level overview in monorepo root CLAUDE.md → "Releasing @workflowbuilder/sdk".
The flow is A+ (Changesets + commitlint + release branch + tag-triggered CI publish) — adapted from synergycodes/ng-diagram (defensive pre-publish checks, npm-view idempotency) plus Changesets for automated version/CHANGELOG management.
This repo publishes two packages: @workflowbuilder/sdk and @workflowbuilder/ui. Each has its own scoped release tag and its own workflow:
| Package | Tag format | Workflow |
|---|---|---|
@workflowbuilder/sdk | @workflowbuilder/sdk@X.Y.Z | .github/workflows/release-sdk.yml |
@workflowbuilder/ui | @workflowbuilder/ui@X.Y.Z | .github/workflows/release-ui.yml |
The steps below describe the SDK; the UI release is identical with the UI tag/workflow/package path substituted. (The old single-package v* tag scheme has been retired - scoped tags are required so the two packages don't collide.)
Mental model
main ─────●───●───●───●─────●─── ...
│ │ │ │ │
└ feature PRs land here.
Each consumer-visible SDK change carries a `.changeset/*.md`.
release ───────────────●─────────── ...
│
└ Each commit on `release` = one published version.
Tag `@workflowbuilder/<pkg>@X.Y.Z` lives on that commit.
Tag push → GitHub Action publishes that package to npm.
main is "what we're building"; release is "what's currently on npm". The tag is the single source of truth for "this exact commit became version X.Y.Z".
One-time setup
-
npm organization access. Get added as a maintainer on the
workflowbuildernpm organization (or create it the first time at https://www.npmjs.com/settings/workflowbuilder). The org name has no hyphen, matching the scope@workflowbuilder/sdk. -
Configure the npm Trusted Publisher (once per package). Authentication is OIDC. No
NPM_TOKENis used or stored. Each package needs its own trusted publisher pointing at its own workflow file. On the package's…/accesspage (or for a not-yet-published package: org settings → "Packages" → "Add trusted publisher"), add:- Publisher: GitHub Actions
- Organization or user:
synergycodes - Repository:
workflowbuilder - Workflow filename:
release-sdk.ymlfor@workflowbuilder/sdk,release-ui.ymlfor@workflowbuilder/ui - Environment name: (leave empty)
The workflows already have
permissions: id-token: write, so once the trusted publisher is registered,pnpm publishon a scoped tag push exchanges the GitHub OIDC token for a short-lived npm credential. Provenance attestation is enabled via the--provenanceflag, so each published version links back to the exact workflow run and commit. -
Create the
releasebranch (first time only):git checkout -b release main git push -u origin releaseBranch protection (recommended once it stabilizes): require PR from
mainonly, require status checks (typecheck + tests) before merge.
Daily flow (every PR touching the SDK)
This part Claude (or any contributor) handles per change — not the maintainer.
-
Edit
packages/sdk/**and tests. -
pnpm --filter @workflowbuilder/sdk test && pnpm --filter @workflowbuilder/sdk typecheck. -
Add a changeset:
/wb.changeset patch "fix zustand store identity leak in useStore hook" /wb.changeset minor "add WorkflowBuilder.Toolbar compound subcomponent" /wb.changeset major "rename onSave prop to onPersist on WorkflowBuilder.Root"Skip the changeset only for changes that do not affect the published
dist/(e.g. internal tests, lint config, comments).Keep the body short. It becomes this change's CHANGELOG bullet at release time, reformatted into Keep a Changelog style (the maintainer strips the commit hash and the
feat:/fix:prefix and files it under Added / Changed / Fixed). One sentence for a fix, one or two for a feature. State what changed and the consumer-facing effect, name the public symbols touched, and stop. No rationale, no implementation walk-through, no internal file names. Reasoning belongs in the PR description or code comments, not the release notes. Breaking changes are the only exception: add aBreaking changes:list with migration steps (seeremove-nodeid-from-handles.md). -
Commit code + changeset together. Conventional Commits format is enforced by
.husky/commit-msg:git add packages/sdk/... .changeset/*.md git commit -m "fix(sdk): zustand store identity leak" -
Push, open PR to
main, get review, merge.
The changeset accumulates in .changeset/ on main until the next release.
Release procedure (maintainer)
Steps 1–6 are the human-driven path; step 7 is fully automated.
1. Open the release PR
git checkout main && git pull
git checkout -b release/vX.Y.Z # X.Y.Z chosen by changesets in step 2
In the branch, consume all pending changesets:
pnpm install --frozen-lockfile
pnpm changeset version
This:
- Reads every
.changeset/*.md, computes the highest bump per package. - Bumps
packages/sdk/package.jsonversion (so2.0.0 → 2.1.0if any minor changeset,2.0.0 → 2.0.1if only patches,2.0.0 → 3.0.0if any major). - Regenerates
packages/sdk/CHANGELOG.mdwith one section per consumed changeset, in raw Changesets format. Reformat it into Keep a Changelog style before committing (see "Reformat the generated CHANGELOG section" below). - Deletes the consumed
.changeset/*.mdfiles. - Touches
pnpm-lock.yamlif needed.
Pick the vX.Y.Z for the branch name from the new version in packages/sdk/package.json.
Reformat the generated CHANGELOG section
pnpm changeset version writes the new release in raw Changesets format: a bare ## X.Y.Z heading, ### Minor Changes / ### Patch Changes groupings, and each bullet prefixed with a commit hash and its Conventional-Commit type (5dddbff: feat: ...). The committed CHANGELOG uses Keep a Changelog instead, matching every prior release. Rewrite the generated block before committing:
- Heading. Use
## [X.Y.Z] - YYYY-MM-DDwith the release date, not the bare## X.Y.Z. - Sections. Replace
### Minor Changes/### Patch Changeswith### Added,### Changed,### Fixed,### Removed. Categorize by intent, not by semver bump. A new API goes under Added, a behavior change or rename under Changed, a bug fix under Fixed, a deletion under Removed. - Bullets. Drop the leading commit hash and the
feat:/fix:prefix. Write plain prose. Lead an Added bullet with the public symbol it introduces. Start a Fixed or Changed bullet with a capitalized verb. - Link reference. Add
[X.Y.Z]: https://www.npmjs.com/package/@workflowbuilder/sdk/v/X.Y.Zto the reference list at the bottom of the file, newest first.
Keep the # Changelog H1 as the only thing above the newest version heading. Do not add a preamble paragraph or an ## [Unreleased] placeholder there. Changesets always inserts the next release immediately after the H1, so anything between the H1 and the first ## heading gets pushed into that release's section and leaks into its GitHub Release notes.
Example. The generated block:
## 2.1.0
### Minor Changes
- fa207df: feat: add `edgeTemplates` prop on `<WorkflowBuilder.Root>` for custom edge renderers.
### Patch Changes
- 3b9f8fd: fix: re-measure node internals when `layoutDirection` changes.
becomes:
## [2.1.0] - 2026-06-16
### Added
- `edgeTemplates` prop on `<WorkflowBuilder.Root>` for custom edge renderers.
### Fixed
- Re-measure node internals when `layoutDirection` changes.
After reformatting, confirm the release-notes extraction is clean. The extractor in .github/workflows/release-sdk.yml matches both the bracketed heading and a bare ## X.Y.Z, so run it locally to see exactly what the GitHub Release body will contain:
VERSION=$(node -p "require('./packages/sdk/package.json').version")
awk -v v="$VERSION" '\$0 ~ ("^## \\[?" v "\\]?([ -]|$)"){flag=1;next}/^## /{flag=0}flag' packages/sdk/CHANGELOG.md
It should print the ### Added / ### Fixed bullets for this version and nothing else.
Then commit the version bump, reformatted CHANGELOG, and changeset deletions together:
git add -A
git commit -m "chore(sdk): release vX.Y.Z"
git push -u origin release/vX.Y.Z
Open a PR release/vX.Y.Z → release.
2. Pre-merge verification
In the PR diff you should see:
packages/sdk/package.json: version bumppackages/sdk/CHANGELOG.md: new Keep-a-Changelog section (dated## [X.Y.Z]heading,### Added/### Changed/### Fixedgroupings, link reference at the bottom), reformatted from the raw Changesets output.changeset/*.md: deletionspnpm-lock.yaml: small workspace dep update if a tracked package was bumped. The non-publishable internal packages are inignorein.changeset/config.json, but@workflowbuilder/sdkand@workflowbuilder/uiare tracked - bumping@workflowbuilder/uiupdates consumers that depend on it viaworkspace:*
Local smoke before approving:
pnpm --filter @workflowbuilder/sdk build:lib
cd packages/sdk
pnpm publish --dry-run --no-git-checks
Inspect the dry-run output. The tarball should contain:
package.jsonwithcatalog:references replaced by real versions. (workspace:*deps live indevDependenciesonly and are stripped bypnpm publish.)dist/index.js,dist/index.d.ts,dist/style.css.- The lazy icon chunks (~1500
dist/*.jsfiles). README.md,LICENSE,CHANGELOG.md.- No
src/,node_modules/,tsconfig*.json,vite.config.*, decision logs, or test files.
3. Merge the release PR
Merge release/vX.Y.Z into release (merge commit gives cleaner blame; pick one strategy and stick with it).
4. Tag the merge commit
After the merge lands on release:
git checkout release && git pull
git tag @workflowbuilder/sdk@X.Y.Z # scoped; matches packages/sdk/package.json exactly
git push origin @workflowbuilder/sdk@X.Y.Z
# For the UI package: git tag @workflowbuilder/ui@X.Y.Z && git push origin @workflowbuilder/ui@X.Y.Z
5. CI publishes automatically
The workflow .github/workflows/release-sdk.yml:
- Checks out the tag.
- Runs lint + typecheck + test on the SDK (defensive — if any fails, no publish).
- Builds the SDK.
- Verifies the tag version matches
packages/sdk/package.json(catches "pushed wrong tag"). - Checks if
@workflowbuilder/sdk@X.Y.Zis already on npm (idempotency — re-pushing tag won't fail). - Runs
pnpm publish --no-git-checks --access public. - Extracts the CHANGELOG section for this version and creates a GitHub Release with those notes.
Monitor at https://github.com/synergycodes/workflowbuilder/actions. If the workflow fails, see Troubleshooting below.
6. Sync back to main
After CI is green:
git checkout main && git pull
git merge release # fast-forward — release HEAD becomes main HEAD for the bump
git push origin main
This brings the version bump, regenerated CHANGELOG, and changeset deletions back to main so subsequent PRs start from a clean state.
7. Verify on npm
npm view @workflowbuilder/sdk
npm view @workflowbuilder/sdk@X.Y.Z
Both should show the new version. The npm page (https://www.npmjs.com/package/@workflowbuilder/sdk) updates within a minute.
Rollback
A published version cannot be overwritten on npm. Options when something went wrong:
-
Bad code but version is on npm: publish a
X.Y.Z+1patch with the fix. Optionallynpm deprecate @workflowbuilder/sdk@X.Y.Z "Use X.Y.Z+1 — <reason>"so consumers see a warning on install. -
Tag is on the wrong commit but version is not yet on npm (CI failed mid-publish, or you killed the workflow before publish step ran): delete and re-tag:
git tag -d @workflowbuilder/sdk@X.Y.Z git push origin :refs/tags/@workflowbuilder/sdk@X.Y.Z # … fix the underlying issue, then re-tag at the correct commit and push againThe
npm viewidempotency check in the workflow means a re-push after a partial failure is safe — if the publish step already succeeded, the next run will skip it gracefully. -
You merged the release PR but want to back out before tagging: revert the merge commit on
release(git revert -m 1 <merge-sha>). Master still has the pending changesets; they get consumed in the next attempt.
npm unpublish is restricted to releases less than 72 hours old and only when there are no dependents. Treat publish as one-way.
Troubleshooting CI failures
| Symptom | Cause | Fix |
|---|---|---|
npm error code E401 / OIDC token exchange failed in publish step | Trusted publisher not configured, or workflow filename / repo / org in the npm config doesn't match this workflow | On npmjs.com, verify the trusted publisher entry points at synergycodes/workflowbuilder with workflow filename release-sdk.yml and no environment name |
id-token permission errors | Job/workflow lost id-token: write (e.g. someone edited the workflow) | Restore permissions: id-token: write at the workflow level |
Tag version (X.Y.Z) does not match package.json version (Y.Y.Y) | Pushed tag before merging the release PR, or tagged the wrong commit | Delete tag (see Rollback), merge release PR first, re-tag |
404 Not Found - PUT https://registry.npmjs.org/@workflowbuilder/sdk | npm org doesn't exist or you're not a maintainer | Create the workflowbuilder org or get added as maintainer |
| Build fails: workspace dep resolution | Probably stale pnpm-lock.yaml after rename | Run pnpm install locally, commit lockfile, re-tag |
| Lint / typecheck / test step fails | Code that landed on release doesn't pass checks | Fix on main via PR, redo the release PR, re-tag at the new HEAD |
| Workflow says "already on npm — skipping publish" | Re-pushed tag after successful publish | Expected. No-op. CI still creates the GitHub Release. |
Why these decisions
-
pnpm publish, nevernpm publish—npmdoes not resolve pnpm'scatalog:protocol, would publish a brokenpackage.json. The--no-git-checksflag skips the "clean working tree" check (CI runs in detached HEAD on a tag — git considers that unclean). pnpm 10.17+ supports OIDC trusted publishing, which is why the rootpackageManageris pinned to that floor. -
OIDC Trusted Publisher, never a long-lived
NPM_TOKEN— the workflow exchanges a per-run GitHub OIDC token for a short-lived npm credential. Nothing to rotate, nothing to leak from CI logs. The trust is bound to the exactsynergycodes/workflowbuilderrepository plus this workflow file path; a fork can't publish, a different workflow in the same repo can't publish.--provenanceattaches the signed build attestation so consumers see "published from this commit, by this workflow run" on the npm page. -
Scoped tag format
@workflowbuilder/<pkg>@X.Y.Z— the repo now publishes two packages (@workflowbuilder/sdkand@workflowbuilder/ui), so each release tag is scoped to its package. This lets the two be released independently without tag collisions, and lets each workflow trigger on its own tag pattern. The earlier single-packagev*scheme (an ng-diagram convention for single-package monorepos) was retired when@workflowbuilder/uibecame publishable. Historicalv*tags stay untouched in git history — the change is forward-only. -
Dedicated
releasebranch —mainis "what we're building",releaseis "what's currently on npm". Each commit onreleasecorresponds to one published version. Why this over main-only:- "What's published" is visible as a branch in the UI (no
git tag --listscanning). - Branch protection can require approval on the release PR — second human gate before publish.
- Hotfix path is natural: branch from a
releasecommit (or tag), patch, mini-release PR. - Matches ng-diagram's flow exactly — patterns we can re-use.
- "What's published" is visible as a branch in the UI (no
-
Defensive lint/typecheck/test in workflow — catches "code landed on release in a broken state and nobody noticed until publish time". Costs a few minutes per release; saves a botched npm publish.
-
npm viewidempotency check — re-pushing a tag (after a workflow bug fix, say) shouldn't re-publish or fail loudly. Adopted from ng-diagram'spublish-npm.yml. -
GitHub-only, single remote — earlier we considered dual-pushing tags to Bitbucket (source-of-truth) + GitHub (mirror with CI). Org decision is GitHub-as-primary going forward, so single push to
origin(= GitHub) is enough. Workflow only runs on GitHub anyway.