Deployment
This document describes how @xmachines/xmachines-js packages are built, versioned, and published to npm. All releases are automated through semantic-release running inside a GitLab CI pipeline. No manual npm publish commands are required for normal releases.
Deployment Targets
All @xmachines/* packages are published to the public npm registry (registry.npmjs.org). Every package uses "publishConfig": { "access": "public" } so scoped packages are accessible without an npm org subscription. See the Published Packages table for the full list.
| Target | Config File | Purpose |
|---|---|---|
| npm registry | scripts/publish-release.mjs, on the tag pipeline | Publish all public packages |
| GitLab Releases | scripts/publish-release.mjs, on the tag pipeline | Attach tarball artifacts to the GitLab release tag |
| GitLab CI | .gitlab-ci.yml | Trigger builds, tests, and releases on push / MR / tag |
The root package.json is marked "private": true and is never published to npm.
Published Packages
The following packages are published on every release (derived from the workspace scan in release.config.mjs — every non-private package that is not a demo):
| Package | Directory |
|---|---|
@xmachines/shared | packages/shared |
@xmachines/play | packages/play |
@xmachines/play-actor | packages/play-actor |
@xmachines/play-signals | packages/play-signals |
@xmachines/play-router | packages/play-router |
@xmachines/play-dom | packages/play-dom |
@xmachines/play-dom-router | packages/play-dom-router |
@xmachines/play-react | packages/play-react |
@xmachines/play-react-router | packages/play-react-router |
@xmachines/play-solid | packages/play-solid |
@xmachines/play-solid-router | packages/play-solid-router |
@xmachines/play-svelte | packages/play-svelte |
@xmachines/play-sveltekit-router | packages/play-sveltekit-router |
@xmachines/play-svelte-spa-router | packages/play-svelte-spa-router |
@xmachines/play-tanstack-react-router | packages/play-tanstack-react-router |
@xmachines/play-tanstack-solid-router | packages/play-tanstack-solid-router |
@xmachines/play-vue | packages/play-vue |
@xmachines/play-vue-router | packages/play-vue-router |
@xmachines/play-xstate | packages/play-xstate |
@xmachines/docs | packages/docs |
Example demo packages (under packages/*/examples/demo) are not published to npm ("npmPublish": false) but are still packed into release tarballs and attached as GitLab release assets for testing.
Each published package includes only the directories listed in its files field: dist, README.md, and LICENSE (or equivalent for packages without a compilation step).
Build Pipeline
CI Pipeline Stages
Pipeline runs are triggered by .gitlab-ci.yml under the following conditions:
| Trigger | Pipeline runs |
|---|---|
Push to main / rc / beta / alpha / pre | Full pipeline; the release job waits to be started |
| Merge request | Full pipeline, no release job |
| Web / API / trigger / parent pipeline | Full pipeline |
No rule admits a tag pipeline, so pushing a tag runs nothing.
The pipeline includes two reusable CI components, both pinned in .gitlab-ci.yml — which is the version to trust, rather than a number repeated here:
to-be-continuous/node/gitlab-ci-node— handles install, lint, build, test, and auditto-be-continuous/semantic-release/gitlab-ci-semrel— runssemantic-releaseon eligible branches
The node-build job runs with:
pnpm run build # vite build (JavaScript) && tsc --build (declarations)pnpm run test:coverage # vitest run --coverage (with JUnit + Cobertura reporters)Coverage is extracted from the node-build job via the regex:
/All files[^|]*\|[^|]*\s+([\d\.]+)/JUnit results are published as reports/junit.xml and Cobertura coverage as reports/coverage/cobertura-coverage.xml.
Release Job Detail
The semantic-release CI job is configured with:
semantic-release: id_tokens: NPM_ID_TOKEN: aud: "npm:registry.npmjs.org" artifacts: when: always expire_in: 1 week paths: - dist/releases/*.tgzThe job uses a GitLab OIDC id token (NPM_ID_TOKEN) scoped to npm:registry.npmjs.org rather than a static NPM_TOKEN.
The semantic-release command is called with --no-ci to allow it to run inside CI without triggering its own CI-environment mode.
Release Smoke Test Job
The release-pack-smoke job is defined in .gitlab-ci.yml and runs on the same trigger rules as other CI jobs (automatically on main pushes and tags, manually on MRs):
pnpm run buildnode scripts/release-pack-smoke.mjsscripts/release-pack-smoke.mjs:
- Asks
scripts/lib/release-packages.mjswhich packages a release publishes.publish-release.mjsandrelease.config.mjsask the same module, so the three cannot disagree. - For each publishable package: runs
npm pack --jsonin the package directory. - Creates a temporary directory, runs
npm init -y, and installs the local tarball with--ignore-scripts. - Asserts the install succeeds — any missing files, broken exports, or pack-time errors surface here.
- Cleans up temp directories and tarballs after each check.
This smoke test runs before the actual publish so packaging problems are caught in CI rather than on npm.
Semantic Release Process
Branch Model
Releases are driven by commit history on the release branches:
| Branch | Channel | Tag format | Behaviour |
|---|---|---|---|
main | stable (default) | v1.2.3 | Full production release |
rc | rc | v1.2.3-rc.N | Release candidate pre-release |
beta | beta | v1.2.3-beta.N | Beta pre-release |
alpha | alpha | v1.2.3-alpha.N | Alpha pre-release |
pre | pre | v1.2.3-pre.N | Pre-release |
The channel becomes the npm dist-tag verbatim, which is why none of them contains a slash: npm reads a slash in a version selector as a GitHub shorthand, so such a tag cannot be installed.
Tag format is v${version} (e.g., v1.0.0-beta.46).
Prepare Phase
Before any package is published, semantic-release runs a sequence of @semantic-release/exec prepare steps defined in release.config.mjs:
corepack enablepnpm install --frozen-lockfilenode scripts/set-workspace-versions.mjs ${nextRelease.version}pnpm run buildpnpm --filter @xmachines/docs run typedoc --gitRevision v${nextRelease.version}pnpm --filter @xmachines/docs run formatgit add -A packages/docs/apiStep by step:
- Enable corepack — activates the pinned pnpm version.
- Install —
pnpm install --frozen-lockfileinstalls the exact dependency versions recorded in the lockfile. - Sync versions —
scripts/set-workspace-versions.mjssets the same release version across everypackage.jsonin the workspace (root + all packages), including cross-package workspace dependency ranges. - Build — runs
vite build && tsc --buildto produce compileddist/output — JavaScript then declarations — in all packages. - Generate API docs — runs TypeDoc to regenerate
packages/docs/api/at the release git revision. - Format docs — runs
oxfmton the docs package to ensure consistent formatting. - Stage the generated docs —
git add -A packages/docs/api. The git plugin stages whatgit ls-files -m -oreports, and that never reports a DELETION. A page that TypeDoc removed, because an export was renamed or deleted, therefore stayed in git forever while the docs tarball packed from the working tree was correct.git add -Astages the removals, and the git plugin commits the index.
Publish Phase
semantic-release publishes nothing. It stops at the tag, and scripts/publish-release.mjs publishes from a pipeline that runs ON that tag:
node scripts/publish-release.mjsThe tag pipeline is what makes the provenance attestation meaningful. npm records the commit of the pipeline in the attestation. On a tag pipeline that commit IS the commit that the tag names, and it is the commit whose manifests declare this version. A publish from the branch pipeline attests the PARENT of the release commit instead: a revision that the tag does not name, and whose manifests still carry the previous version.
The script pins the internal dependencies, strips the source export condition, packs every workspace package into dist/releases/, publishes each publishable tarball with --provenance under the dist-tag of the version, and verifies that every one reached the registry. It then creates the GitLab release with the tarballs attached, and it restores the working tree.
The script is idempotent. It skips every package that the registry already serves at this version, so one retry of the job finishes a run that died partway — which is how the publish of 1.1.0 recovered from a credential that expired mid-run. Nothing to untag, nothing to force-push.
Post-Release Phase
This phase runs on the branch, BEFORE the tag pipeline publishes.
-
@semantic-release/gitcommits updated files back to the repository:CHANGELOG.mdpackage.json,pnpm-lock.yamlandpnpm-workspace.yaml(root)- The
package.jsonof every scanned package, and theREADME.mdof every one — the release stamps the version badge of a README at the same moment as the manifest packages/docs/api/**(generated API documentation)
The manifest list and the README list both come from the workspace scan, and not from a glob written by hand, so a package that a new
pnpm-workspacepattern adds cannot be published while its manifest stays behind in git. A test asserts that every scanned package appears in both lists.Commit message format:
chore(release): <version>The release notes are NOT interpolated into the message.
@semantic-release/gitrunsgit commit -m <message>, and the notes of the first stable release aggregate about 1600 commits — an-mvalue of several megabytes overflows the argument limit of the OS. The notes land inCHANGELOG.md, which the same commit carries.The message carries no
[skip ci]. The release commit needs no pipeline of its own, but the TAG that points at it does: that pipeline is where the publish happens. -
The git plugin pushes the commit and the tag. semantic-release stops there. The GitLab release entry, with every
dist/releases/*.tgzattached, is created later byscripts/publish-release.mjson the tag pipeline — after the publish succeeded, so a release entry never advertises a tarball that npm refused.
Version Synchronisation
All packages in the monorepo share a single version at all times. This is enforced during release by scripts/set-workspace-versions.mjs, which:
- Sets
versionin every workspacepackage.json(including the root) to the new release version. - Rewrites
dependencies,devDependencies,peerDependencies, andoptionalDependenciesranges for any@xmachines/*workspace package to the exact new version (no^or~prefix — a strict pinned version is written).
This means packages always depend on the exact same version of sibling packages that was released together.
Credentials and Protected Variables
| Variable | Source | Purpose |
|---|---|---|
NPM_ID_TOKEN | GitLab CI OIDC (auto-generated per job) | Authenticates semantic-release to npm registry via OIDC token exchange |
GL_TOKEN / GITLAB_TOKEN | GitLab CI group variable (unprotected) | Authenticates the git push back of semantic-release, and the GitLab release that publish-release.mjs creates |
CI_JOB_TOKEN | GitLab CI built-in | Used by the to-be-continuous components for GitLab API calls |
No static npm token is committed to the repository. The NPM_ID_TOKEN is a short-lived OIDC token generated for each CI job with audience npm:registry.npmjs.org.
To publish packages, a maintainer must:
- Have push access to a release-eligible branch (
main,rc,beta,alpha,pre). Onlymainis protected; see the branch model above for what changes ifGITLAB_TOKENis ever made a protected variable. - Ensure the GitLab project CI/CD variables include the required tokens.
Artifacts
Release tarballs are stored as GitLab CI artifacts for 1 week after each pipeline:
dist/releases/*.tgzThe same tarballs are attached to the GitLab Release entry as permanent downloadable assets. Tarballs are not committed to git (the dist/ directory is gitignored).
The API documentation (packages/docs/api/) is committed to git by the post-release git commit and is the only generated artifact tracked in version control.
Rollback Procedure
There is no automated rollback mechanism. To revert a release:
-
npm deprecate (preferred) — deprecate the bad version on npm so it is hidden from installs:
Terminal window npm deprecate @xmachines/play@<bad-version> "Deprecated: use <good-version>"Repeat for each affected package.
-
npm unpublish — unpublish the bad version if it was published within 72 hours and no dependents exist:
Terminal window npm unpublish @xmachines/play@<bad-version> -
Patch release — push a
fix:commit tomain(or the relevant branch) to trigger a patch version release via semantic-release. This is the preferred long-term approach for production issues. -
GitLab Release — delete or edit the GitLab Release entry via the GitLab UI or API to remove broken tarballs. This does not affect what is on npm.
Recovering a Failed Release
If the job dies partway (e.g. one package’s trusted publisher isn’t set up), some packages are published, the git tag is created, and the rest are missing. To finish the release:
- Delete the release tag. semantic-release creates the tag before publishing, so a re-run while it exists is a no-op (semantic-release#3178):
Terminal window glab api --method DELETE "projects/xmachin-es%2Fxmachines-js/repository/tags/vX.Y.Z" - Re-run the
semantic-releasejob.
The config detects a staged-but-untagged version (a recovery re-run) and automatically demotes any package already on npm at that version to pack-only — re-packing it for the release assets but not re-publishing it — so the re-run publishes only what’s missing, with no manual input. SEMREL_SKIP_STEPS (a regex matched against release step IDs) remains available as a manual override to force-skip specific steps.
First fix the underlying cause before re-running — e.g. configure the missing trusted publisher — otherwise the same packages fail again.
Local Release Dry-Run (Manual)
To preview what semantic-release would do without publishing:
# Install dependencies firstpnpm install --frozen-lockfile
# Run semantic-release in dry-run modepnpm exec semantic-release --dry-run --no-ciTo test package packaging locally (the same check run by release-pack-smoke in CI):
pnpm run buildnode scripts/release-pack-smoke.mjsThis packs every publishable package, installs each tarball in a temporary project, and verifies the install succeeds.
To manually version and pack a single package for local inspection:
# From the repo rootpnpm --filter @xmachines/play run buildpnpm --filter @xmachines/play packDo not run npm publish manually. All publishing must go through semantic-release in CI to keep CHANGELOG.md, git tags, and npm versions in sync.
Monitoring
The project does not include application runtime monitoring (no Sentry, Datadog, or OpenTelemetry) — it is a library monorepo, not a deployed service.
Post-release health checks:
- npm package availability — verify new versions appear on
https://www.npmjs.com/package/@xmachines/<pkg>after the pipeline completes. - GitLab pipeline status — monitor the
semantic-releasejob in the GitLab CI UI for the release commit. - GitLab Releases page — confirm the release tag and tarball assets appear at
https://gitlab.com/xmachin-es/xmachines-js/-/releases. packages/docs/api/commit — confirm the post-release git commit updated API docs on the target branch.