Repository navigation
Description
@gdevenyi @thomasbeaudry: this follows up on #1822 and my comment there about a hybrid approach. I worked through a full release and support policy. Before #1822 gets reworked to match it, I'd like your thoughts, especially on anything you would push back on and on the two open questions at the end.
Why
- Every merge to
mainstill re-pusheslatestand2.5.2. The most recent was today, in run 42063797447.mainis 564 commits pastv2.5.2(48feat, 101fix). A deployment on either tag runsmainfrom whenever it last pulled, and the actualv2.5.2code is no longer published under any image tag. SECURITY.mdpromises security updates for "the most recent version" only.- Major numbers have marked big features, not compatibility breaks. The 2.0.0 changelog calls it "a non-breaking upgrade."
- The deployment guide has deployers
git clonemain, so the compose file they run is unreleased code.
Proposal
1. What a version number promises
A change that breaks any of these is a new major:
- Stored data. An upgrade boots against the existing MongoDB and object storage and reads every existing record, with no manual step.
- Deployment config. This covers
.envvariables, compose services, volumes and ports. A new variable needs a default that keeps the old behaviour. Renaming or removing a variable, or making it required, is breaking. It follows that the newest image of a major works with the compose file from any earlier release of that major. - Stored instruments. Every bundle already in a database keeps rendering, and its records keep exporting.
- The documented HTTP API (
spec.json). Additions are fine; removing or changing anything is breaking. - Export formats. Column and field names.
- Access. An upgrade never widens or narrows what an existing user can see or do, unless an admin acts.
- The public API and CLI of each published npm package.
Outside the contract: UI layout and workflows, undocumented endpoints, internal workspaces, and the gateway↔api protocol. The gateway and api must run the same version.
2. Which lines are supported
- The current major is supported in its newest minor only. To get a fix, upgrade to the newest minor, which §1 makes safe.
- Every superseded major keeps its last minor supported for 36 months after the next major ships.
- The policy applies going forward only. 1.x is not covered.
3. What each line gets
| Fix | Current major (newest minor) | Newest superseded major | Older superseded majors |
|---|---|---|---|
| Security | ✓ | ✓ | ✓ |
| Data integrity | ✓ | ✓ | ✓ |
| Critical bug | ✓ | ✓ | — |
| Everything else | ✓ | — | — |
-
Security means either of two things.
- A vulnerability that lets someone, in a documented deployment configuration (the compose stack and its documented options, opt-in ones included), do any of these:
- read data they aren't authorized to see;
- create, change or delete data they aren't authorized to;
- act as another user or gain permissions they weren't given;
- run code on the server or in another user's browser.
- Or a Critical-rated CVE in anything a deployment runs: the base image, OS packages, the runtime npm dependencies of api, gateway and web, and the third-party images the compose stack pins (MongoDB, RustFS, Caddy).
A CVSS score alone qualifies nothing, and availability-only issues (DoS, resource exhaustion, crashes) never get a backport. They're fixed on the current line in its next ordinary release.
- A vulnerability that lets someone, in a documented deployment configuration (the compose stack and its documented options, opt-in ones included), do any of these:
-
Data integrity means a bug that silently loses, corrupts or misattributes recorded data, or exports it wrong.
-
Critical bug means the stack fails to start, or a core workflow is unusable with no workaround. The core workflows are administering an instrument, collecting a remote assignment, managing subjects and users, and exporting data.
-
Maintainers judge reachability and whether a workaround exists. The backport PR states which category it falls under.
-
If a fix can't be backported without breaking the line (for example, the fix exists only in the dependency's next major), we publish an advisory saying the line is affected and recommending an upgrade, rather than skipping it quietly.
4. Image tags
| Tag | Moves when | For |
|---|---|---|
<version> |
never (immutable) | production deployments that upgrade by hand |
<major> |
every release on that major, including backports | the documented default: fixes and features, never a breaking change |
latest |
every release of the newest major | demos and quick starts; crosses majors, so not for production |
edge |
every push to main |
the demo instance and dev environments |
sha-<7> |
never | pinning an exact main build |
<major>.<minor>is dropped. Under §2 it would stop getting fixes the day the next minor ships, and nothing would tell the people tracking it..env.templatedefaultsRELEASE_CHANNELto the current major.increment-version.tsrewrites it on a major bump.- Staging tracks
<major>, so it runs what production will run.
5. Which compose file a deployment runs
- Deploy from a release tag, never from
main. Install withgit clone --branch v<version>, and upgrade withgit fetch --tags && git checkout v<newer>.mongo/,rustfs/,sqlite/and.envare git-ignored, so switching refs never touches data or config. - A per-major branch that deployers can
git pullwould be safer, but CI would need to push to a protected branch with a token we don't hold today. That's a later improvement if people keep pullingmain.
6. npm packages
- The version numbers stay in lockstep with the platform, but npm publishes only from
main. That makes every npm publish alatestpublish. - A release from a branch ships images and a GitHub release, and logs that it is skipping npm. Authors targeting an older major install
@^<major>. - The
releaseenvironment staysmainonly, so the npm trusted publisher never has to trust a branch.
7. Breaking changes
- Every breaking change is marked
!, with aBREAKING CHANGE:footer written for deployers that says what they have to do. The footer becomes the changelog entry, so a major's upgrade notes come from the commits themselves. - Merging a
!commit tomainis the decision that the next release frommainis a major. There is no cadence cap, andmaindoesn't have to stay releasable as a minor. increment-version.tsrefuses a bump lower than the recommended one once a breaking commit is in range, and refuses any!commit on a release branch.- Automatic detection is follow-up work, with an issue for each check:
- an OpenAPI diff against the last tag;
- an
.env.templateand compose diff against the last tag; - export-format snapshots;
- an upgrade test that boots the previous release's data with the new images.
Out of scope: release cadence and gating. Releases stay at my discretion, as now.
What this means right now
- No
release/2.5branch and no 2.5.3. Under §2, 2.5 isn't a separate line, and a branch cut fromv2.5.2would rebuild code almost no deployment runs. - Once ci: release from release branches and keep latest on tagged releases #1822 has landed, the next release is cut from
main.- Before bumping, audit the commits since
v2.5.2against §1. A first pass over.env.template, compose and the Prisma schema found nothing breaking:BIND_ADDRESShas a default,PROJECT_ROOTleft the template, and the schema only gained optional fields and new models. The HTTP API, export formats and access are still unchecked. Permission sets need the closest look. - If nothing is breaking: release 2.6.0. Its notes tell deployers to set
RELEASE_CHANNEL=2and check outv2.6.0. - If something is breaking: release 3.0.0, with upgrade notes. 2.x then gets no maintenance line, because every 2.x image tag points at moving code and there is no stable 2.x build to support. The policy would start with 3.x.
- Before bumping, audit the commits since
- Confirm the Node version in the build log before announcing the CVE fix.
mainbuildsFROM node:lts-krypton, which should already resolve to 24.22.1.
What changes in #1822
| Area | #1822 today | Under this proposal |
|---|---|---|
| Image tags | <version>, <major>.<minor>, latest, edge, sha-<7> |
<version>, <major>, latest, edge, sha-<7> |
| Release branches | release/<major>.<minor> |
release/<major>; planRelease checks the version's major against the branch |
| npm | publishes from branches under release-<major>.<minor>; publish-npm.cjs reconciles dist-tags |
publishes only from main; a branch release logs that it skips npm; dist-tag logic removed |
release environment |
add release/* after merge |
stays main only |
| "After merge" plan | cut release/2.5, ship 2.5.3 |
dropped; the next release is 2.6.0 or 3.0.0 from main |
Node fix: commit |
in the PR | unchanged |
release-channels.md |
describes the PR's model | rewritten to match this policy |
These would land as separate small PRs, independent of the workflow:
increment-version.ts: refuse a lower bump, refuse!commits on a release branch, and rewriteRELEASE_CHANNELon a major bump..env.templatedefaulting toRELEASE_CHANNEL=2, and the deployment guide switching to deploying from a tag.- A user-facing versioning and support page, and
SECURITY.md. - The detection issues listed in §7.
Open questions
- How should this land? I'd revise ci: release from release branches and keep latest on tagged releases #1822 in place with the table above, then merge it before the next release. Merging as-is would add a dist-tag scheme, a reconciliation script and environment instructions that this proposal then deletes. The cost is that
latestkeeps moving with every merge until the revision lands. The alternative is to merge ci: release from release branches and keep latest on tagged releases #1822 now and do the table as a follow-up. That's safe only if the follow-up lands before the next release, because until a release happens the merge run only pushesedge. - Where do deployers read the policy? Not discussed yet. Options are a docs page with a table of end-of-life dates, the "Supported Versions" section of
SECURITY.md, and whether advisories go through GitHub Security Advisories.
Activity
joshunrau commentedon Nov 5, 2026
My recommendations on the two open questions
1. How this lands: revise #1822 in place
publish-npm.cjsand its tests, thenpmTagoutput, therelease-<major>.<minor>dist-tag, and the step that addsrelease/*to thereleaseenvironment. Two renames:<major>.<minor>becomes<major>, andrelease/<major>.<minor>becomesrelease/<major>.planReleaseshould refuse arelease/<major>dispatch while no higher major is tagged. Under §2 that branch only exists once its successor has shipped. With the check, a branch release can never planlatest, and the race from the first review can't happen even in principle.v2.5.2, then cut 2.6.0 or 3.0.0 frommain.Why I prefer this to merging #1822 now:
release-channels.md, the playbook and theodc-releaseskill) that would be wrong onmainuntil the follow-up arrived.latestkeeps moving with every merge until the revision lands, but it has been doing that for six weeks, and a revision should take days.If the revision stalls, the fallback is to merge #1822 as-is and land the table as a follow-up before the next release. Until a release happens, the merge run only pushes
edge, so that is safe.2. Where deployers read the policy
docs/en/5-reference/5.10-versioning.md("Versioning and support"), covers §1–§5 of the issue, written for deployers. It ends with an end-of-life table: one row per major, giving its release date, its successor's release date and the end of its support. It's English only, like the rest of the reference section. The page also tells deployers to use Watch → Custom → Releases to hear about new versions.increment-version.tscan write the row on a major bump, the same way it rewritesRELEASE_CHANNEL, so the table can't fall behind.SECURITY.mdstates the rule in two sentences and links to the page rather than copying the table, so the dates exist in one place. It should also point reporters at GitHub's private vulnerability reporting, which is already enabled on this repo but whichSECURITY.mddoesn't mention.thomasbeaudry commentedon Nov 5, 2026
Overall this is good, just a few suggestions;
Open questions:
gdevenyi commentedon Nov 7, 2026
I agree with Tom's changes here, I would cut 36 months to 12-24.
Definitely we need to make explicit upgrade recommendations as we are regularly generating new releases, there needs to be some info for sysadmins to decide if they "need to".
One thing not covered here, how do we handle "large" version jumps? Do they need to be incremental?
joshunrau commentedon Nov 10, 2026
Decisions
Thanks both. Here is where each point landed.
<major>stays the documented default, and<version>stays available for deployments that upgrade by hand. Every release is in the changelog, so admins can decide when to update. @thomasbeaudry, the new workflow should also limit accidental breakages: a breaking change has to be marked!, which makes the next release a major, andincrement-version.tsrefuses a lower bump once one is in range.How this lands: a new PR supersedes #1822. It implements the policy above, including stopping merges to
mainfrom overwritinglatestand<version>. Where deployers read the policy: as recommended above, one docs page with the end-of-life table,SECURITY.mdlinking to it, and a GitHub Security Advisory for each Security fix.Per-release upgrade notices, rollback and breaking data changes are open in #1896.
ci: release from main and release/<major>, and stop re-pushing releas…
docs: publish the versioning and support policy
feat: track the current major release in a generated .env
chore: enforce the support policy when bumping the version