Skip to content

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.

ChangeBump while 0.y.zBump from 1.0.0
Breaking change to a shipped surfaceMINOR (0.9.2 → 0.10.0)MAJOR
Backwards-compatible featurePATCH (0.9.2 → 0.9.3)MINOR
Backwards-compatible fixPATCHPATCH

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 MINOR bump always means "a breaking change happened", the same discipline a 1.x project applies to MAJOR.
  • Features no longer inflate the minor number, so the project stops drifting toward 1.0.0 by accident — 1.0.0 stays 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 prefixBump while 0.y.zBump from 1.0.0
feat(SUDI-xxx):PATCHMINOR
fix(SUDI-xxx):PATCHPATCH
perf(SUDI-xxx):PATCHPATCH
revert(SUDI-xxx):PATCHPATCH
feat(SUDI-xxx)!: / BREAKING CHANGE: footerMINORMAJOR
refactor, chore, docs, test, ci, build, stylenonenone

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. release-please triggers on push: main; dev is the integration branch. A dev → main promotion is merged as a merge commit, which is what lets release-please see the underlying feat/fix commits on main.

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

  1. Feature work merges into dev. A PR merged into dev deploys the development environment (deploy.yml) when it carries the deploy label.
  2. When a release is wanted, dev is promoted to main with a merge commit (never a squash). The merge commit carries the feat/fix commits that release-please parses.
  3. release-please opens a chore(main): release X.Y.Z PR on main, then force-updates that same PR on every later push — it does not open a new PR per merge. Merging it bumps the root package.json version, updates CHANGELOG.md, records the version in .release-please-manifest.json, and tags vX.Y.Z. A chore/ci/docs-only promotion cuts no release (standard).
  4. The release workflow dispatches deploy-prod.yml for the tagged commit, so production deploys exactly the release. It then opens and merge-commits a main → dev sync PR, making the release commit an ancestor of dev and keeping the next promotion free of release-file conflicts, and dispatches deploy.yml for dev so the development footer shows the new version.

Versioning itself has no label — release-please derives the bump from the commit types. Deploy has one: deploy for a feature PR. The dev → main promotion is merged with a merge commit, and the release workflow owns the main → dev sync. A hotfix committed straight onto main breaks the tree guard: cherry-pick it onto dev so it is tested there and the next release sync carries the same content to both branches. The PR Check version-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 main 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 when a promotion is made rather than on every feature merge. The version PR itself is not a release: it is a continuously updated proposal, and the cadence is set by when dev is promoted to main. Left unmanaged — promoting and 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: dev is promoted to main on a weekly boundary by convention, and only the promotion is deliberate.

  • Feature PRs still merge into dev continuously — only the promotion is scheduled.
  • Merging the version PR is the release; release-please tags on that merge, not on a timer. A promotion with no feat/fix cuts no version and deploys nothing until the next one.
  • An out-of-band release promotes dev to main immediately, so a hotfix never has to wait for the next boundary. workflow_dispatch on deploy-prod.yml remains the manual deploy route.

Where the shown version comes from ​

The released version is owned by release-please. release-please runs on main and its chore(main): 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 release workflow merges that commit back into dev, so both branches show the same version.

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 ​

EnvironmentBranchHostFooter version
Local—localhostvX.Y.Z
Developmentdevdev.*.sudigital.comvX.Y.Z
Productionmain*.sudigital.comvX.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.

One backend. Three products. One token.