Skip to content

Releases & versioning

A release is a vX.Y.Z-tagged image publish. Rather than every app owner tagging by hand and drifting, MajNet gives one consistent scheme across every app: the bot cuts the tag, CI builds the image, and you promote it to production — all through git.

From an app’s Releases section, Cut release picks the next version for you. The bot reads the last release and computes the next semver — you choose the bump:

  • Patch / Minor / Major — explicit (the menu shows the exact next version).
  • Auto — derived from the conventional-commit messages since the last release. By default feat: → minor, fix: → patch, and a ! or BREAKING CHANGE → major; every other type (chore, docs, refactor, …) is ignored, so it neither forces a patch nor clutters the changelog. The strongest signal across the commits wins. If nothing releasable landed since the last release, there’s simply no candidate to cut.

The bot creates the vX.Y.Z tag on the app repo’s main (through git, App- signed) — no local tagging, no inconsistency between repos.

The bump map is configurable per app (release.bumps in the ops repo’s project.yaml): map any commit type to major / minor / patch — e.g. add perf: patch — while a breaking change always wins as major.

  1. CI builds it. The tag triggers the managed release workflow, which builds and pushes the image tagged vX.Y.Z, pinned by digest.
  2. The bot records it. The registry publish is picked up and the release appears in the app’s Releases list. stable auto-tracks the newest tag.
  3. Promote to production. Promote pins the chosen version’s digest into the project’s ops repo (production.yaml) via a render PR — merging it deploys. Production only ever runs a version you explicitly promoted.

A monorepo — one repository hosting several apps — can release each app on its own version and cadence instead of one repo-wide vX.Y.Z covering everything. Opt an app in by giving it a release: block in the ops repo’s project.yaml (written for you from the dashboard’s release settings — no hand-editing, no PR):

apps:
- name: sideline-server
repo: sideline
release:
scope: sideline # per-app tags @sideline/[email protected]
autorelease: auto # off | patch | auto
paths: [applications/server/**]

With a scope set, cutting that app creates a scoped git tag @<scope>/<leaf>@vX.Y.Z (the Changesets convention), and each app tracks its own last version. The image tag stays the plain version, so nothing downstream changes — only the git tag is scoped. Without a release: block an app keeps the repo-wide vX.Y.Z scheme, so this is fully backward compatible.

Two extra behaviours make per-app releases first-class:

  • Path-scoped changelogs. With paths, an app’s changelog and auto bump consider only the commits that touched those paths — a busy monorepo never inflates one app’s changelog with a sibling’s changes.
  • The bump lands in the repo. A release commits the bumped <dir>/package.json version and a prepended <dir>/CHANGELOG.md entry straight to main (one commit, no PR), then tags it — so the repo’s own version files stay in sync with the release.

An app with autorelease: patch or autorelease: auto cuts itself automatically on every push to main that touches its paths: patch always bumps the patch level, auto derives the bump from the conventional commits. It reuses the same tag → CI path as a manual cut, and release commits are skipped so a release never re-triggers itself. Manual Cut release still works alongside it.

The fleet-wide Releases page (View all → from the top-bar Releases popover) lists every app with an unreleased change as a candidate. Expand a row to review its computed version + changelog and release it inline, or tick several and Release N selected to release them in one action.

A bulk release is batched per repository: all of a monorepo’s selected apps land in one version-bump-and-changelog commit with one tag each, so sibling releases never race each other for the same main — the whole repo releases atomically.

A release isn’t instant — after you cut it, the version is committed and tagged, CI builds the image, the publish is recorded, and stable starts tracking it. The Releases page shows each in-flight release as a live stepper so you can watch it move through those stages instead of guessing:

committing → tagging → building → published → tracked

Each stage lights up as it completes, and if one fails — a rejected tag, a stuck CI build — it’s marked there with the reason, so you can see exactly where a release stalled. This is especially handy for a bulk release, where several apps advance at once.

Every app serves a standard /info endpoint with its build metadata; the reconciler scrapes it at deploy time, so the dashboard, deploy events, and render PRs all show the version running per environment — not an opaque digest. Images are always pinned by digest under the hood; the version is the human-facing handle.