Versioning & Releases
SUDIGITAL follows Semantic Versioning (MAJOR.MINOR.PATCH). The root package.json version is the single source of truth for the whole monorepo, and every app shows the running version in its sidebar footer.
Bump rules
The project is still on 0.y.z, so the pre-1.0 rule applies: 0.y behaves as the major number and z is the patch number. The table below is what release-please produces today; the standard SemVer table resumes at 1.0.0.
| Change | Bump while 0.y.z | Bump from 1.0.0 |
|---|---|---|
| Breaking change to a shipped surface | MINOR (0.9.2 → 0.10.0) | MAJOR |
| Backwards-compatible feature | PATCH (0.9.2 → 0.9.3) | MINOR |
| Backwards-compatible fix | PATCH | PATCH |
Pre-1.0 bumps are release-please's bump-minor-pre-major and bump-patch-for-minor-pre-major options, both enabled in release-please-config.json. Never hand-edit the version to work around them.
Pre-1.0 policy
Semantic Versioning reserves 0.y.z for initial development: anything MAY change at any time and the public API SHOULD NOT be considered stable. The same specification's FAQ adds that software already used in production "should probably already be 1.0.0".
SUDIGITAL ships to production, but its API, on-chain contracts and payment surfaces are still moving, so it stays pre-1.0 deliberately — and follows the convention the wider 0.x ecosystem uses (Rust crates, cargo, and release-please's own pre-major mode): while the version is below 1.0.0, 0.y acts as the major number, so a backwards-compatible feature is a PATCH and only a breaking change moves y.
That preserves three properties:
- A
MINORbump always means "a breaking change happened", the same discipline a1.xproject applies toMAJOR. - Features no longer inflate the minor number, so the project stops drifting toward
1.0.0by accident —1.0.0stays an explicit stability declaration. - The mapping only affects releases cut after the change lands; past tags and changelog entries are untouched.
Rejected alternative: declare 1.0.0 now and keep feat → MINOR. It signals a stable API, which the contract and payment surfaces are not yet ready to promise, so it is deferred until the ecosystem is frozen.
Commit types drive the bump
release-please only reads Conventional Commit prefixes, so the commit type and the Notion scope matter:
| Commit prefix | Bump while 0.y.z | Bump from 1.0.0 |
|---|---|---|
feat(SUDI-xxx): | PATCH | MINOR |
fix(SUDI-xxx): | PATCH | PATCH |
perf(SUDI-xxx): | PATCH | PATCH |
revert(SUDI-xxx): | PATCH | PATCH |
feat(SUDI-xxx)!: / BREAKING CHANGE: footer | MINOR | MAJOR |
refactor, chore, docs, test, ci, build, style | none | none |
PR Check enforces it: a PR whose squash subject — the PR title, or a one-commit PR's commit headline — is not type(SUDI-<n>): description with one of those eleven types is red. edited is in the trigger list, so a retitle clears it without a push. The hidden types (refactor, chore, ci, docs, test, build, style) never produce a release on their own, so a refactor-only batch cuts no new version and waits for the next feat, fix, perf or revert. That is expected, not a broken pipeline.
Branch names keep their human prefixes (feature/, bugfix/, hotfix/, improvement/, chore/) — only the commit type follows Conventional Commits. The workflow triggers on push: dev, so it is active on the integration branch; dev → main only carries a version that has already been released.
Release flow
Versioning needs no label — the commit types decide the bump. Deploying the development environment is what the deploy label gates. release-please runs on every push to dev and maintains one open version PR per component. The PR is a proposal, not a release: merging it is what cuts the version. A new PR number only appears after the previous one is merged.
- Feature work merges into
dev. A PR merged intodevdeploys the development environment (deploy.yml) when it carries thedeploylabel, or when its head branch is arelease-please--branch. - When a
feat/fixhas landed since the last release, release-please opens achore(release): X.Y.ZPR ondev, then force-updates that same PR on every later push — it does not open a new PR per merge. The weekly release train merges it (or you merge it out of band), which bumps the rootpackage.jsonversion, updatesCHANGELOG.md, records the version in.release-please-manifest.json, and tagsvX.Y.Z. Achore/ci/docs-only merge cuts no release (standard). Merging the version PR also redeploys dev — that merge carries nodeploylabel, it is recognised by itsrelease-please--head branch. devis released intomainthrough a manually approved PR, which deploys production (deploy-prod.yml).
Versioning itself has no label — release-please derives the bump from the commit types. Deploy has one: deploy for a feature PR, release-please-- for a version PR. The dev -> main release is squash-merged like any other PR, so main gains a commit dev never contains and nothing is ever merged back: main takes all of dev's latest, and its tree is the tree of a commit on dev. PR Check enforces that — a dev -> main release passes only while main's tree is the tree of a commit on dev, which every squash of dev satisfies. A hotfix committed straight onto main breaks it: cherry-pick it onto dev and revert it on main through the route that landed it, so main holds only released content again — never a merge of main into dev. The PR Checkversion-monotonic job blocks any PR that would lower its target branch's version.
Closing or skipping a version PR
Closing a version PR releases nothing: no tag, no changelog entry, no manifest write. The next push to dev (or the next Monday train) recreates it, because the version was never consumed. That means you cannot "close 0.9.6 and merge 0.9.7" — the recreated PR proposes 0.9.6 again, and 0.9.7 cannot exist until 0.9.6 is actually released.
Versions are immutable and monotonic: never skip, reuse, or lower one. version-monotonic enforces the ordering. SemVer does not mandate a release cadence, so skipping a number would still parse, but it breaks the changelog and the "a version means these changes shipped together" contract. If a release must be deferred, close the PR and let a later cut include the work under the next number; never renumber by hand.
Release cadence
A version bump is not the same event as a deploy, so releases are cut on a schedule rather than on every merge. The version PR itself is not a release: it is a continuously updated proposal, and the cadence is set by when the train merges it. Left unmanaged — merging each version PR as soon as it appears — the history shows 22 tags across 7 days, 2026-10-07 → 2026-10-13, three of them on 2026-10-13, including five-release days on 2026-10-08 and 2026-10-11, leaving CHANGELOG.md as a per-merge log instead of release notes.
Industry practice splits two ways:
- Continuous delivery — one version per deploy. Normal for internal services with no external consumers, where the changelog is a build log nobody tracks.
- Release train — a fixed cut (weekly, biweekly) that batches whatever is ready since the last one. The norm for anything with external consumers; Node.js, Angular, Kubernetes and React all release on a schedule rather than on every merge, so a version number means "these changes shipped together".
SUDIGITAL ships to production with public contracts, payments and an API, so it uses a release train: .github/workflows/release-train.yml merges the pending version PR every Monday at 09:00 UTC, then dispatches the release finalisation (the vX.Y.Z tag and GitHub Release) and the development deploy.
- Feature PRs still merge into
devcontinuously — only the version cut is scheduled. - The train waits for any in-flight release-please run, then merges the pending
autorelease: pendingPR. If none exists it dispatches release-please once and re-checks; if there is nothing releasable it exits cleanly and ships nothing. - An out-of-band release runs the same workflow manually (
workflow_dispatch), so a hotfix never has to wait for Monday. Merging a version PR by hand is an out-of-band release and should be reserved for hotfixes. - The repository default branch is
dev, soschedulefires from the branch the version PR targets. It would only fire frommainif the default branch moved.
Where the shown version comes from
The released version is owned by release-please. release-please runs on dev and its chore(release): X.Y.Z PR (see Release flow) bumps the root package.json, updates CHANGELOG.md, tags vX.Y.Z, and writes the released version to .release-please-manifest.json; the dev → main release carries it to production.
tooling/build-info.ts reads that manifest (inlined as __APP_VERSION__); it never computes a version itself, so the footer always shows the released SemVer:
- Local (
vite, development) → the released version, e.g.v0.1.0 - Development (
dev) → the released version - Production (
main) → the released version
The commit is shown separately as __APP_COMMIT__. If the manifest is unavailable the build falls back to the root package.json version, then 0.0.0.
Non-Conventional commit types (bugfix:, improvement:, …) make release-please skip a bump. Nothing enforces the convention automatically — write Conventional Commits by hand.
Environments
| Environment | Branch | Host | Footer version |
|---|---|---|---|
| Local | — | localhost | vX.Y.Z |
| Development | dev | dev.*.sudigital.com | vX.Y.Z |
| Production | main | *.sudigital.com | vX.Y.Z |
The sidebar footer renders v<version> · <environment> (<commit>) so it is always clear which version of which environment is in front of you.