The UluBit monorepo contains packages that are versioned and published independently. A release therefore belongs to a package rather than to the repository as a whole.
The process connects three things: the source state in Git, the package published to npm, and a package-specific tag and GitHub Release that make that relationship easy to find later.
This guide covers the manual release process used for packages such as @ulubit/foundations and @ulubit/ui.
Prepare the release state
The package implementation and its related documentation should be complete before the release version is set.
Each package being released gets its own version update:
{
"name": "@ulubit/foundations",
"version": "0.1.5"
}
Only packages that are actually part of the release need a new version. A Foundations release, for example, does not imply a UI release unless UI has also changed or its published dependency contract needs to change.
It is easier to understand and recover a release later when its changes are represented by an identifiable commit. Finished unrelated work can be committed separately before the release commit. If unrelated work is still unfinished, it can be stashed until the release is complete rather than being mixed into the release state.
Once the release changes and version updates are committed, the branch can be pushed and checked with:
git status -sb
At this point there should be no uncommitted file changes, and the branch should not be ahead of or behind its remote.
This is also the state expected by pnpm’s normal publish checks: the publish branch must be clean and up to date with its remote unless those checks are explicitly disabled.
Preview the packages
Before anything reaches npm, a dry run shows what pnpm is preparing to publish:
pnpm publish --dry-run
This is run from each package directory involved in the release.
The output provides a final check of the package name, version, registry, and files included in the artifact. For example:
@ulubit/foundations@0.1.5 → https://registry.npmjs.org/
A dry run performs the publish process without actually creating the package version in the registry.
Check workspace dependencies
Some UluBit packages depend on other packages from the same monorepo. UI, for example, can reference Foundations through pnpm’s workspace protocol:
{
"peerDependencies": {
"@ulubit/foundations": "workspace:^"
}
}
Inside the monorepo, this keeps the dependency tied to the local workspace package. That workspace declaration is not what npm consumers receive.
When pnpm packs or publishes a workspace package, it replaces workspace: dependencies with normal version ranges. A package with this kind of dependency is worth inspecting before publication because the packed manifest shows the dependency contract that consumers will actually receive.
The package archive can be created with:
pnpm pack
and its manifest inspected with:
tar -xOf ulubit-ui-<version>.tgz package/package.json
For example:
"@ulubit/foundations": "workspace:^"
should become a range based on the current Foundations version:
"@ulubit/foundations": "^0.1.5"
The temporary archive is no longer needed after that check:
rm ulubit-ui-<version>.tgz
Publish in dependency order
When several packages are being released together and one depends on another, the dependency needs to exist in npm before the dependent package is published.
For Foundations and UI, that relationship is:
@ulubit/foundations
↓
@ulubit/ui
Foundations can therefore be published first:
pnpm publish
Once that version is available in npm, UI can be published:
pnpm publish
Packages without a release in the current change are left alone.
Verify the published packages
A successful pnpm publish confirms that npm accepted the package, but it does not prove that the published artifact works correctly from the other side of the registry.
The release is therefore checked from a real consuming project rather than from the local monorepo workspace.
For Foundations and UI, UluBit Starter is the default integration check unless another project exercises the particular change more effectively.
For a combined Foundations and UI release, the published versions can be pulled into Starter with:
pnpm update @ulubit/foundations @ulubit/ui
The resolved versions can then be confirmed with:
pnpm list @ulubit/foundations @ulubit/ui
Once the expected versions are present, Starter’s normal validation and production build provide the integration check:
pnpm check
pnpm build
This verifies the packages as npm consumers receive them, rather than the local workspace copies used while developing them.
Tag the release source
Because packages in the monorepo have independent versions, their Git tags include the package name:
<package>-v<version>
For example:
foundations-v0.1.5
ui-v0.1.2
Before creating the tags, you can confirm that the latest commit is indeed the package release commit:
git log -1 --oneline
If it is, then the annotated tags can be created directly:
git tag -a foundations-v0.1.5 -m "foundations v0.1.5"
git tag -a ui-v0.1.2 -m "ui v0.1.2"
Several package tags can point to the same commit when those packages were released from the same repository state.
If the latest commit is no longer the release commit, the correct one can be found in the history:
git log --oneline
The tags can then be attached to that commit explicitly:
git tag -a foundations-v0.1.5 <release-commit> -m "foundations v0.1.5"
git tag -a ui-v0.1.2 <release-commit> -m "ui v0.1.2"
Before pushing, the tag targets can be checked with:
git show foundations-v0.1.5 --no-patch
git show ui-v0.1.2 --no-patch
The tags are then pushed to the remote repository:
git push origin foundations-v0.1.5
git push origin ui-v0.1.2
Each tag provides the durable link between a published package version and the exact Git state that produced it.
Record the release
Each package tag gets its own GitHub Release.
The release title uses the npm package name and version:
@ulubit/foundations 0.1.5
@ulubit/ui 0.1.2
The notes record the meaningful changes introduced by that package version.
This gives each release three complementary records: npm contains the installable artifact, the Git tag identifies its source state, and the GitHub Release provides the package-level change history.
The source repository can remain private while the npm packages themselves are public.
If publishing fails
A failed publish does not necessarily mean that another version is needed. The important distinction is whether npm actually created the package version.
Authentication can be checked with:
npm whoami
and renewed when necessary with:
npm login
If the version never reached the registry, the same version can be published again after the problem is resolved.
A Git-state failure is better resolved by returning the repository to the clean release state described earlier rather than routinely bypassing pnpm’s Git checks.
Likewise, an unexpected packed manifest is a reason to correct the package configuration before anything is published.
Release check
A release is complete when:
- the intended package versions exist on npm;
- workspace dependencies have become the intended published version ranges;
- a real consumer resolves the published packages;
- that consumer passes its normal validation and production build;
- each released package has a package-specific tag pointing to its release source;
- each tag has a corresponding GitHub Release.