Version Checking
How SveltyCMS handles version display, update checking, and the automated release pipeline.
On this page
SveltyCMS uses a simple two-branch model with automated releases triggered when CI passes on main.
Branch Model
| Branch | Purpose | Releases |
|---|---|---|
next |
Active development — features, fixes, and experiments land here first | No |
main |
Production — merged from next when stable |
Yes (auto on CI pass) |
Release Pipeline
next or main: pre-push gate passes → push
│
▼
main: CI runs (whitebox → build → DB matrix → bench → E2E)
│
▼ (CI passes)
auto-release:
1. reads version from package.json
2. if that version's tag already exists and this commit is ahead of it,
scripts/version.ts patch writes the next version and the workflow
commits and pushes it
3. creates the git tag for the version now in package.json
4. GitHub Release with auto-generated notes (independent of npm)
5. npm publish (best-effort — npm never blocks the release)
the created tag is the package.json version ✅
a checkout that already is that tag is skipped ✅
Deriving the next version
bun run version:bump runs scripts/version.ts. It reads the released version from the highest v* git tag reachable from the current branch (falling back to package.json when no tag exists yet), inspects the commits since that tag, and writes the next version back into package.json. Every run prints the source it used, the commit range it inspected, and the proposed version before anything is written — --dry-run stops right after that report. An explicit x.y.z argument (optionally v-prefixed) sets that version instead: the manifest is written as given, no commit is inspected, and the same refusals below still apply.
bun run version:bump # auto policy (default)
bun run version:bump patch # force a patch bump
bun run version:bump minor # force a minor bump
bun run version:bump major # force a major bump
bun run version:bump 0.0.10 # set exactly this version (no commit detection)
bun run version:bump --dry-run # print the decision, change nothing
Preparing the bump on push
The pre-push gate can run the derivation for you. bun run git push --release arms a release step in
.githooks/pre-push (kind optional: --release=patch|minor|major, default auto); once all nine checks
pass, the hook runs scripts/version.ts, commits the manifest as
chore(release): bump version to vX.Y.Z and prints the push that publishes it. The flag is consumed by
scripts/git-safe.ts, which exports SVELTY_RELEASE for the hook — git never forwards hook arguments.
The manual equivalent is bun run gate --release.
Three properties keep it safe to automate: it runs after the full gate (a version that ships for a
tree the gate never validated is the stray bump §8 of AGENTS.md warns about); the release commit
cannot travel inside the push that triggered it, because git resolves the pushed refs before the hook
runs — so the bump stays local and reversible (git reset --hard HEAD~1) until you push again; and
re-running is a no-op, since the derivation is stable within one release cycle. It is skipped on the
docs-only and --only-failures paths, and a version.ts refusal fails the hook (the push is blocked)
rather than leaving behind a push that looks released but is not.
Check the proposal before the second push.
autoderives from the highest reachablev*tag, which is not alwaysgit describe’s nearest tag. A test tagv0.2.0(5 July 2026, manifest0.0.7) used to outrankv0.1.2and propose0.3.0; it was deleted on 2026-09-30 and was never onorigin. The bump commit stays local until you push it, so a wrong proposal costs onegit reset— or pass an explicit target (bun run version:bump 0.1.3) when the range containsfeatcommits but the release should stay a patch on the shipped line.
Auto policy (default) — Conventional Commits subjects mapped to SemVer, highest rule first:
Commits since the latest v* tag |
Result |
|---|---|
BREAKING CHANGE footer or type!: subject while major === 0 |
minor (SemVer §4) |
The same markers once major >= 1 |
major (SemVer §8) |
feat subject — new backwards-compatible functionality |
minor (SemVer §7) |
Everything else (fix, perf, refactor, docs, chore, ci, test, build, style, revert, merge commits) |
patch (SemVer §6) |
SemVer §7 defines MINOR as new backwards-compatible functionality, so a feat is never a patch. SemVer §4 (0.y.z may change anything) is why a breaking commit is a minor while major === 0 and a major afterwards (§8); §6 keeps pure fix/refactor/docs ranges at patch. A range that mixes types follows the highest rule that applies — breaking beats feat, feat beats everything else — so feat!: on 1.x is a major release even beside fix commits. patch, minor or major as an argument overrides detection, while an explicit x.y.z literal sets that version verbatim without inspecting commits.
Single source of truth
Two rules keep the tag and the manifest in lockstep (as of September 2026): the release workflow tags whatever version it finds in package.json, and scripts/version.ts derives the next package.json version from the latest v* tag — it reads git, not the hand-edited manifest, so the two cannot drift apart. Because the script refuses to derive a version that already carries a tag, re-releasing an existing version fails fast locally instead of the runner quietly skipping tag creation.
Two further safety rails (as of September 2026): the script refuses to run while package.json has uncommitted changes (--dry-run still previews the decision and prints a warning), and the script itself never creates a commit or a tag. Auto Release commits the bump after CI passes on main. The local pre-push release step also commits one, and only after an explicit --release request and a full green gate.
The same refusals cover an explicit target: a literal that is not MAJOR.MINOR.PATCH, a version that already carries a v* tag, or one lower than package.json (a downgrade). Because the derived proposal comes from the tag rather than the manifest, it can equal package.json — that means the pending version is the next release above the newest tag, so a patch bump reports nothing to write and the script says so, pointing at the explicit form (bun run version:bump 0.0.10) for any other release.
The version is written when CI passes on main. If package.json still names a version that already has a tag, and the commit CI validated is ahead of that tag, Auto Release runs scripts/version.ts auto — the same SemVer policy as the table above — commits chore(release): bump version to vX.Y.Z, pushes that commit to main, and tags it. So a range containing a feat becomes a minor (0.2.0 → 0.3.0), and a pure fix/perf range becomes a patch (0.2.0 → 0.2.1). A checkout that is already the tagged release logs Release skipped and exits green. bun run git push --release prepares that local decision before CI.
A manual trigger remains at Actions → Auto Release → Run workflow, and pushing a v* tag directly (git tag v0.0.10 && git push origin v0.0.10) triggers the same publish steps from the tag name.
npm token caveat: the GitHub Release (tag + release notes) is created before npm publish and never blocked by it. If
NPM_TOKENis missing or invalid, the run logs a warning and skips publishing — the release still exists, so the in-app update check (/api/system/version/check) keeps working.
Version Pipeline Components
| Component | Path | Role |
|---|---|---|
| Version Service | src/services/core/version-service.ts |
Reads local version from package.json, checks GitHub Releases API for newer versions |
| Version Handler | src/routes/api/[...path]/handlers/system.ts |
REST endpoints /api/system/version (public: installed version) and /api/system/version/check (authenticated: update check) |
| VersionCheck Component | src/components/version-check.svelte |
Admin UI badge — displays current version; the badge background carries the status (primary / warning / surface / error) |
| Version Tests | tests/unit/api/version.test.ts |
Unit tests for compareVersions (SemVer incl. prerelease precedence) and checkForUpdates — these import the real implementation, not a copy |
| Auto Release | .github/workflows/auto-release.yaml |
CI pass on main (or manual v* tag push / dispatch) → tag, GitHub Release, npm publish |
Note:
src/utils/media/version-history.tsis a separate module for tracking media file revisions (create/update/replace diffs) — not related to CMS release versioning.
API Endpoints
Check for Updates
GET /api/system/version/check
Queries the GitHub Releases API and compares against the installed version from package.json.
Response (update available):
{
"success": true,
"data": {
"currentVersion": "0.0.9",
"latestVersion": "0.0.10",
"updateAvailable": true,
"checkedAt": "2026-07-08T12:00:00.000Z"
}
}
Response (up to date):
{
"success": true,
"data": {
"currentVersion": "0.0.10",
"latestVersion": "0.0.10",
"updateAvailable": false,
"checkedAt": "2026-07-08T12:00:00.000Z"
}
}
If GitHub is unreachable, the endpoint returns latestVersion: null with updateAvailable: false and an error field.
Admin UI
The VersionCheck component (src/components/version-check.svelte) appears in the admin interface and:
- Reads
PKG_VERSIONfrompublicEnv(loaded frompackage.jsonat server boot viasettings-service.ts→+layout.server.ts) - Fetches
/api/system/version/checkevery hour to compare against the latest GitHub release (not on/setup— see below) - Shows a clickable badge linking to the releases page
| Badge State | Meaning |
|---|---|
Green (primary) |
You are up to date (also the /setup install screen, where the check cannot run) |
Yellow (warning) |
A newer version is available (e.g. “Update to v0.0.10 recommended”), or GitHub answered with an error |
Grey (surface) |
Waiting for the check, or no verdict (not authorized / no data) |
Red (error) |
The check request itself failed (network / timeout) — an error alert is shown |
The update check is authenticated by design (system:read via the fail-closed dispatcher, pinned by
tests/integration/api/version.test.ts → “should require authentication”): the endpoint performs an outbound
GitHub lookup, so an anonymous caller must not be able to trigger it. The installed version keeps its own public
endpoint (/api/system/version). /setup mounts the badge before a session exists, so the component does not call
the endpoint at all and presents the installed version in the brand-green (primary) colour with no status
dot and no error alert — a pre-auth request could only answer 401, and a grey “unavailable” badge described
the check rather than the version. /login keeps a fixed neutral transparent pill that never repaints (status colour, dots included, are gone there), and the authenticated surfaces
(sidebar, config) keep the real check (src/components/version-check.svelte).
Automated Upgrades
SveltyCMS includes an automated upgrade CLI:
bun run scripts/upgrade.ts
The script:
- Fetches the latest release from GitHub
- Merges changes into your local branch
- Refreshes dependencies (
bun install)
Always run
bun run check && bun run test:unitafter an upgrade to ensure system integrity.
File Version History
For media file version tracking (distinct from CMS release versioning), SveltyCMS uses src/utils/media/version-history.ts which provides:
createVersion()— records file create/update/replace actions with change diffscompareMediaVersions()— compares two file versions for content and metadata changesdetectChanges()— deep-diff comparison between old and new file metadatagetVersionStats()— aggregates version counts, sizes, and user activity
This powers the media gallery’s version history viewer — not the CMS release cycle.
Related
- Upgrading SveltyCMS — Manual upgrade process
- System Settings — Configure your SveltyCMS instance
- Git Workflow — Branch strategy and CI gates