Spillway CMSStructured content like Drupal. Page building like Puck. Setup like Ghost. AI that drafts, people who approve.
Menu

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

  1. Make sure CHANGELOG.md has 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 check verifies the file's shape.
  2. On a clean checkout, with CI green:
    pnpm release patch      # or minor, major, or an explicit version such as 1.0.0-rc.1
    

    scripts/release.mjs bumps the root and every packages/*/package.json in lockstep, updates SPILLWAY_VERSION, moves the Unreleased section under ## [X.Y.Z] - <date> (and rewrites the compare links at the end of the file), regenerates docs/reference/ (it carries the version), commits with a DCO sign-off and creates the annotated tag vX.Y.Z. Nothing is pushed. --dry-run shows the plan; --date=YYYY-MM-DD overrides the date.

  3. Push the branch and the tag: the script prints the exact command (git push origin main vX.Y.Z).
  4. The tag starts .github/workflows/release.yml:
    • verify checks the tag, package.json, SPILLWAY_VERSION and 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.
    • image builds the image for linux/amd64 and linux/arm64 and pushes it to GHCR as ghcr.io/<owner>/<repo> (the repository name lowercased, so it follows the repository if it moves) with the tags vX.Y.Z, X.Y and latest. A pre-release (1.0.0-rc.1) gets only its own tag. The job uses GITHUB_TOKEN with packages: write; no secret to add.
    • npm publishes the MIT packages (@spillway-cms/shared, blocks, sdk, client, astro, next; scripts/publish-npm.mjs selects every packages/* manifest whose license is MIT and that is not private) with provenance when the NPM_TOKEN repository 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 @spillway scope 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.
    • release creates 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.
  5. Check the release page, pull the image once (docker pull ghcr.io/<owner>/<repo>:vX.Y.Z) and, for a stable release, move the release/X.Y branch (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 /api and /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.0 of core keeps working on every 1.x.
  • Environment variables and CLI commands documented in docs/install.md and docs/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.Y branch (created from the tag when the first hotfix is needed), with pnpm release patch on that branch; the image gets the X.Y tag 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.
LineStatusFixes until
0.xpre-releasethe next release replaces it
1.0 LTSplanned18 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. createApp applies every pending migration in packages/core/src/db/migrations/index.ts before the server listens, each in its own transaction, recorded by name in _migrations. spillway migrate does the same and exits; spillway migrate --status lists what is applied, pending and unknown without changing anything.
  • Downgrade protection. If _migrations names a migration this build does not know, the database was migrated by a newer Spillway; the server refuses to start and spillway migrate exits 1 with that message (SchemaNewerError in db/migrate.ts). Restore the backup to go back, or run the newer version.
  • Backups first. spillway backup copies a SQLite database consistently while the server runs (SQLite's online backup, verified with quick_check) into DATA_DIR/backups/spillway-<timestamp>.db; media/ is copied separately. Postgres uses pg_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 (upgrade job 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's spillway backup, spillway migrate and spillway migrate --status on that data directory, boots the server, checks /healthz reports the new version and / renders, exports again and compares entity and type ids, and finally runs the previous release's spillway migrate against 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 audit findings; 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 Deprecation header 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 @deprecated so authors see it in their editor.

Where things are

CHANGELOG.mdthe notes, Keep a Changelog form; node scripts/changelog.mjs section <version> prints one section
scripts/release.mjspnpm release: bump, changelog, docs, commit, tag
scripts/publish-npm.mjswhich packages go to npm and how
scripts/upgrade-check.mjsthe upgrade check CI runs, usable locally
.github/workflows/release.ymlthe release workflow (tag push; manual dry run)
.github/workflows/ci.ymlCI, including the upgrade job
packages/core/src/db/migrate.tsthe migration runner and downgrade protection
packages/core/src/db/backup.tsspillway backup
packages/core/src/test/upgrade.test.tsthe tests for the above

Spillway 0.1.0 · Edit this page on GitHub

Was this page helpful?