Releases, versioning and support
How a Spillway release is cut, what the version number promises, how long a release is supported, how to upgrade, and how security issues and deprecations are handled. The short version for operators is in install.md; this is the policy behind it.
One version for everything
Every package in the workspace (@spillway-cms/core, admin, shared, blocks, sdk, client, astro, next, fleet, docs-tools) carries the same version, as does the root package.json, the SPILLWAY_VERSION constant in packages/shared/src/index.ts (what /healthz, the CLI banner, the module manifest check and the kit dependency check report) and the Docker image tag. A release is one commit, one tag and one changelog entry, whether it touches one package or all of them. The published npm packages therefore skip numbers when a release does not change them; that is intended.
Cutting a release
Releases are cut from main (a hotfix for a supported line is cut from its release/X.Y branch, below).
- Make sure
CHANGELOG.mdhas the notes under "Unreleased", in Keep a Changelog form (Added, Changed, Deprecated, Removed, Fixed, Security). Every pull request adds its line there;node scripts/changelog.mjs checkverifies the file's shape. - On a clean checkout, with CI green:
pnpm release patch # or minor, major, or an explicit version such as 1.0.0-rc.1scripts/release.mjsbumps the root and everypackages/*/package.jsonin lockstep, updatesSPILLWAY_VERSION, moves the Unreleased section under## [X.Y.Z] - <date>(and rewrites the compare links at the end of the file), regeneratesdocs/reference/(it carries the version), commits with a DCO sign-off and creates the annotated tagvX.Y.Z. Nothing is pushed.--dry-runshows the plan;--date=YYYY-MM-DDoverrides the date. - Push the branch and the tag: the script prints the exact command (
git push origin main vX.Y.Z). - The tag starts
.github/workflows/release.yml:verifychecks the tag,package.json,SPILLWAY_VERSIONand the changelog agree on the version, then builds, typechecks, tests, checks the generated docs and the dependency audit, and packs the npm tarballs without uploading.imagebuilds the image forlinux/amd64andlinux/arm64and pushes it to GHCR asghcr.io/<owner>/<repo>(the repository name lowercased, so it follows the repository if it moves) with the tagsvX.Y.Z,X.Yandlatest. A pre-release (1.0.0-rc.1) gets only its own tag. The job usesGITHUB_TOKENwithpackages: write; no secret to add.npmpublishes the MIT packages (@spillway-cms/shared,blocks,sdk,client,astro,next;scripts/publish-npm.mjsselects everypackages/*manifest whose license is MIT and that is not private) with provenance when theNPM_TOKENrepository secret exists. Without it the job logs a notice, runs a dry run and passes; add an npm automation token with publish rights on the@spillwayscope and re-run the workflow to publish. Versions already on the registry are skipped, so a re-run is safe. The AGPL packages (core,admin,fleet) are never published to npm; the image is their distribution.releasecreates the GitHub release for the tag with the changelog section for that version, the image tags and digest, and the npm package list. A version with a pre-release suffix is marked as a pre-release.
- Check the release page, pull the image once (
docker pull ghcr.io/<owner>/<repo>:vX.Y.Z) and, for a stable release, move therelease/X.Ybranch (see support, below).
workflow_dispatch on the Release workflow with the "dry run" box ticked does everything except push the image, publish to npm and create the release; use it to rehearse on a branch before the first real tag.
The first tag
The first release is v0.1.0 (decided 2026-11-06; nothing was published as 0.1.0 before). Its notes are one [0.1.0] entry in CHANGELOG.md that folds in everything built before it; once that is on main, tag it by hand: git tag -a v0.1.0 -m "Spillway v0.1.0" && git push origin v0.1.0. pnpm release is for every release after it; it refuses a version equal to the current one. The image is ghcr.io/kwall-llc/spillway-cms: the workflow names it after the repository, and docs/install.md, the fleet's default image in packages/fleet/src/types.ts and the README say the same, so a move to another owner needs those edits too.
Versioning
Spillway uses Semantic Versioning. Before 1.0 a minor version may break compatibility, and the changelog says so under "Changed" or "Removed" with the upgrade step. From 1.0:
- Major (
X): a breaking change in any of the contracts below. - Minor (
X.Y): new features, new endpoints, new blocks and fields, new migrations, deprecations. Upgrading within a major never requires changing a site's content, a module or a front end. - Patch (
X.Y.Z): fixes and security fixes only; no new migrations unless a fix needs one, and then only an additive one.
What counts as breaking
The contracts an operator, a module author or a front-end developer relies on. A change to any of these that is not backward compatible is a major version:
- The shared contract (
packages/shared/src/index.ts): the HTTP API under/apiand/api/public, the shapes of its requests and responses, the roles and the permission names, the export file format (format: spillway-export, version: 1), the MCP tools. Adding endpoints, optional fields and response properties is compatible; removing or renaming any of them, changing a type, tightening validation that previously accepted input, or changing the meaning of a permission is breaking. - Migrations and stored data: a migration that drops or rewrites columns in a way that an export from the previous version would no longer import, a change in how entity data or layouts are stored that an older front end could not read through the public API, or a change to
DATA_DIR's layout (spillway.db,media/,session-secret,sso-secret,backups/). - Block props (
packages/blocks): the names and types of each block's props are what a saved layout holds. Adding a prop with a default is compatible; removing or renaming one, or changing its type, breaks saved pages and is only done with a migration that rewrites layouts, in a major. - The module SDK (
packages/sdk):defineModule, the hook names and their payloads, the capabilities, the manifest format, the Wasm host interface. A module published for^1.0.0of core keeps working on every 1.x. - Environment variables and CLI commands documented in
docs/install.mdanddocs/reference/cli.md: removing or renaming one, or changing a default in a way that changes behavior on an existing install. - Rendered HTML is not a contract: markup and CSS of the core blocks may change in a minor, and a theme that targets class names should expect that. The CSS custom properties (theme tokens) are part of the theming contract and follow the rules above.
Supported Node and database versions can change in a minor (announced one minor ahead); the image always runs a supported Node.
Support policy
Applies from 1.0; until then only the newest release gets fixes, and main is the release.
- Long-term support. Each major version has one LTS line, its first minor (
1.0), supported for 18 months from its release with security fixes and fixes for data loss or startup failures. A later minor can be named the next LTS line when a major is far off; the changelog and this page say which line is LTS and until when. - Current minor. The newest minor of the current major receives every fix until the next minor is released, plus a two-month overlap so nobody has to upgrade the day a minor ships.
- Previous major. When 2.0 ships, the 1.x LTS line continues to its 18-month date, and nothing else from 1.x is maintained.
- Hotfixes for a supported line are cut from a
release/X.Ybranch (created from the tag when the first hotfix is needed), withpnpm release patchon that branch; the image gets theX.Ytag moved to the new patch. - Upgrade path. Upgrading from any supported release to the next minor (N to N+1) and from the LTS line to the next major is supported: migrations run on boot, the previous release's data is preserved, and the step is covered by CI (below). Skipping minors within a major works too (migrations are forward-only and cumulative) but is tested only as the sum of its single steps. Crossing a major from a release older than its LTS line is done in two steps, via the LTS.
- Downgrades are not supported. A build refuses to start on a database that a newer build has migrated (see Upgrades), and the way back is the backup taken before upgrading.
| Line | Status | Fixes until |
|---|---|---|
| 0.x | pre-release | the next release replaces it |
| 1.0 LTS | planned | 18 months after its release |
Upgrades
What the server does and what the operator does; install.md has the commands per deployment.
- Migrations run on boot.
createAppapplies every pending migration inpackages/core/src/db/migrations/index.tsbefore the server listens, each in its own transaction, recorded by name in_migrations.spillway migratedoes the same and exits;spillway migrate --statuslists what is applied, pending and unknown without changing anything. - Downgrade protection. If
_migrationsnames a migration this build does not know, the database was migrated by a newer Spillway; the server refuses to start andspillway migrateexits 1 with that message (SchemaNewerErrorindb/migrate.ts). Restore the backup to go back, or run the newer version. - Backups first.
spillway backupcopies a SQLite database consistently while the server runs (SQLite's online backup, verified withquick_check) intoDATA_DIR/backups/spillway-<timestamp>.db;media/is copied separately. Postgres usespg_dump. The upgrade section of the changelog says when a release needs more than that. - Migrations are forward-only and additive within a major: they add tables and columns and backfill them. Dropping or rewriting data is a major and the changelog's upgrade notes say what to check first.
- The upgrade check in CI (
upgradejob in.github/workflows/ci.yml,scripts/upgrade-check.mjs): on every push and pull request, CI takes the previous stable release (its GHCR image when it was pushed, otherwise a build of its git tag), creates a site with it (create-user, the higher-ed kit with sample content,export), then runs this build'sspillway backup,spillway migrateandspillway migrate --statuson that data directory, boots the server, checks/healthzreports the new version and/renders, exports again and compares entity and type ids, and finally runs the previous release'sspillway migrateagainst the upgraded database, which must fail when this build added migrations. Until the first tag exists the job prints a notice and passes. Locally,node scripts/upgrade-check.mjs --previous <built checkout>or--previous-image <ref>runs the same thing.
Security advisories
The reporting side is in SECURITY.md: private reports, an acknowledgment within 3 working days, status every 14 days.
- Fixes are developed in a private fork through a GitHub security advisory on the repository, which also issues the CVE. Reporters are credited unless they prefer not to be.
- A fix ships as a patch release on the current minor and on every supported LTS line, on the same day, with the changelog entry under "Security" describing the impact and the affected versions, and the advisory published alongside the release.
- Disclosure window: 90 days from the report, or the day the fix is released, whichever comes first. If a fix needs longer, the reporter is told why and a new date is agreed. A vulnerability already public is fixed and disclosed as fast as a build can be made.
- Dependency advisories: CI fails on high and critical
pnpm auditfindings; a vulnerable dependency with no fix available is pinned or replaced and noted in the changelog. - Operators who want fixes announced subscribe to the repository's releases and security advisories on GitHub.
Deprecations
- A deprecation is announced in a minor release under "Deprecated" in the changelog, in the docs for the thing deprecated, and where possible at runtime (a log line at boot for an environment variable or a CLI command, a
Deprecationheader on an endpoint, a notice in the admin for a block or field). The announcement names the replacement and the earliest version that removes the old way. - Nothing is removed before the next major, and nothing is removed less than six months after its deprecation was announced. Within an LTS line's 18 months nothing deprecated is removed at all.
- Deprecated blocks keep rendering until removal; a removal ships with a migration that rewrites saved layouts to the replacement.
- Deprecated module hooks keep firing until removal; the SDK marks them
@deprecatedso authors see it in their editor.
Where things are
CHANGELOG.md | the notes, Keep a Changelog form; node scripts/changelog.mjs section <version> prints one section |
scripts/release.mjs | pnpm release: bump, changelog, docs, commit, tag |
scripts/publish-npm.mjs | which packages go to npm and how |
scripts/upgrade-check.mjs | the upgrade check CI runs, usable locally |
.github/workflows/release.yml | the release workflow (tag push; manual dry run) |
.github/workflows/ci.yml | CI, including the upgrade job |
packages/core/src/db/migrate.ts | the migration runner and downgrade protection |
packages/core/src/db/backup.ts | spillway backup |
packages/core/src/test/upgrade.test.ts | the tests for the above |
Spillway 0.1.0 · Edit this page on GitHub