Skip to content

ci: release from release/<major> branches and stop re-pushing release tags - #1901

Merged
joshunrau merged 6 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:ci/release-policy
Nov 10, 2026
Merged

joshunrau merged 6 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:ci/release-policy

Conversation

@joshunrau

Copy link
Copy Markdown
Collaborator

Implements the release and support policy decided in #1870 and supersedes #1822. A merge to main stops overwriting the published latest and 2.5.2 images, deployers get a published policy saying which versions receive which fixes, and a superseded major can ship fixes from a release/<major> branch.

Changes

  • A push to main publishes only edge and sha-<7> images. <version>, <major> and latest move only when the root version has no v* tag yet, which means after a version bump. latest and 2.5.2 stop tracking main.
  • A release pushes <version> and <major> (for example 2), and a release from main also moves latest and publishes npm. <major>.<minor> is never published.
  • A superseded major releases from release/<major> on dispatch, and only once a higher major is tagged. That run publishes images and a GitHub release not marked latest, and skips npm with a notice. CI now runs on pull requests to release/**.
  • One Release run at a time across every branch. A run waits for the one in flight instead of cancelling it. GitHub keeps only one waiting run, so a third run cancels the waiting one; if that was a release from main, the next push releases the version anyway.
  • The GitHub release tag goes on the commit the run built, not on the default branch's head.
  • .env.template defaults RELEASE_CHANNEL to 2 instead of latest. A .env generated from now on tracks the current major.
  • engines.node is ^24.22.1 || >=26.12.1, Gabriel's October 2026 Node security fix from ci: release from release branches and keep latest on tagged releases #1822, unchanged.
  • scripts/increment-version.ts enforces the policy:
    • It allows only a major bump once a breaking commit is in range.
    • On a superseded major it allows only a patch bump and refuses a breaking or feat commit.
    • A major bump rewrites RELEASE_CHANNEL and the end-of-support table.
  • New user docs page, Versioning and Support (docs/en/5-reference/5.10-versioning.md), covering:
    • what a version promises;
    • which versions get which fixes, for 18 months after a major is superseded;
    • which image tag to run;
    • installing from a release tag and upgrading by git checkout.
  • Other docs follow the new page. SECURITY.md links to it and points reporters at private vulnerability reporting. The deployment guide clones a release tag instead of main.

Decisions

  • latest and npm move only with a release from main. A branch release cannot move them, because planRelease refuses a release/<major> dispatch until a higher major is tagged. With that guarantee, ci: release from release branches and keep latest on tagged releases #1822's dist-tag reconciliation (publish-npm.cjs) is no longer needed.
  • A superseded major takes patch bumps only. The policy supports only its last minor, so a feat commit in range fails the bump instead of producing a new minor on the branch.
  • The support table is dated by month, using the month of the bump, not the exact release date. The release ships when the bump's PR merges, so a day-precise date would be wrong whenever the merge comes later.
  • The release decision reads git tags only, never GHCR or npm. This is kept from ci: release from release branches and keep latest on tagged releases #1822: a deleted image or package cannot wedge the next release.

Risks

  • No 2 image tag exists until the first release after this merges. Until then, a .env generated from main points at a tag GHCR does not have. Existing deployments keep RELEASE_CHANNEL=latest, which now moves only on a release. The fix is to release 2.6.0 soon after merging.
  • Most of the new workflow behaviour runs for the first time on a real release:
    • The first release after merging is the first run of the new tags, make_latest and target_commitish.
    • A release/<major> dispatch will first run after 3.0.0 ships.
    • Until then, planRelease's unit tests are the only check of that path.

Filed: #1896, #1900

Notes

I ran scripts/increment-version.ts by hand on throwaway state and reverted every run:

  • A major bump rewrote RELEASE_CHANNEL=3 and the table: it added 3 | 2026-11 and marked 2 superseded in 2026-11, supported until 2028-05.
  • With the row for 2 removed from the table, a major bump failed and wrote no file.
  • With a temporary local v3.0.0 tag, --bump minor and a feat commit were both refused.
  • Without that tag, --bump minor after a feat! commit was refused.

The docs site builds with the new page at /en/docs/reference/versioning/.

Adversarial review

Reviewed at commit f67a438. pnpm check: green.

Findings

  1. scripts/increment-version.ts:487 — a major bump writes .env.template and the support table only after every version file and both changelogs, so a missing RELEASE_CHANNEL line or table row leaves the tree half-bumped and a rerun bumps from the wrong version — compute and check both edits before the first write.
  2. scripts/increment-version.ts:212 — a superseded major offers and accepts a minor bump, letting a feature release onto a branch that takes fixes only — allow patch only, and refuse a feat commit in range as the breaking case is refused.
  3. scripts/increment-version.ts:191 — "superseded major" and version parsing are defined here and again in .github/scripts/release.cjs, two artifacts that must agree — export them from release.cjs and import them.
  4. scripts/increment-version.ts:411 — the fallback rule ?? 'it is not allowed here' can never run — make every restricted case carry its rule and drop the fallback.
  5. vitest.config.ts:24 — the narrowed coverage exclude {apps,packages,runtime}/*/scripts/** matches no file the coverage include admits — delete it.
  6. .github/scripts/__tests__/release.test.ts:101 — the entrypoint tests stub readFileSync and getExecOutput without checking their arguments, so reading the wrong file or listing the wrong tags would still pass — assert both calls.
  7. .github/scripts/release.cjs:5 — the ReleasePlan typedef says what each field is in doc comments — drop them and let release-channels.md's output table be the one explanation.

Fixed

    1. Both edits, including the prettier pass, are computed before any version field is written, and only written after the lockstep check.
    1. A superseded major allows only patch; a feat or breaking commit in range fails the run.
    1. release.cjs exports parseVersion and isSupersededMajor, planRelease uses the latter, and increment-version.ts imports both.
    1. allowBumps takes the requested bump and throws with the rule of the branch that restricted it; the fallback is gone.
    1. The exclude entry is deleted.
    1. The tests assert readFileSync('package.json', 'utf-8') and getExecOutput('git', ['tag', '--list', 'v*']).
    1. The descriptions are dropped, with a pointer to release-channels.md.

Attestation

I, Claude Opus 5.5, attest that the review block above reports the adversarial review as it ran on the commit named, with every finding it returned.

🤖 Generated with Claude Code

gdevenyi and others added 6 commits November 9, 2026 20:45
Node v24.22.1 fixes CVE-2026-105482, CVE-2026-105483, CVE-2026-106069 and
CVE-2026-111497 (High) and four Medium and Low CVEs: TLS hostname checks,
the Brotli decoder, dns names and keep-alive sockets. engines.node now
accepts only the patched 24 and 26 lines. The images build from the
floating node:lts-krypton tag, which already resolves to 24.22.1.

https://nodejs.org/en/blog/vulnerability/october-2026-security-releases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G73CpTPk1EjKYeBAKPacit
…e tags

Every push to main re-pushed latest and the current version tag, so both
pointed at whatever main was. The release decision is now a tested
planRelease in .github/scripts/release.cjs, which reads the root version
and the git tags and nothing else. A run releases only when v<version> is
not a tag yet. Every push to main pushes edge and sha-<7>; a release pushes
<version> and <major>, and a release from main also moves latest on GHCR
and GitHub and publishes npm.

A release/<major> branch releases on dispatch, only once a higher major is
tagged, so it can never plan latest, and it skips npm: the npm trusted
publisher and the release environment stay on main. One Release runs at a
time across every ref, an untagged version older than a tag on its line is
refused, and the GitHub tag goes on the commit the run built. CI also runs
on pull requests to release branches.

Refs DouglasNeuroInformatics#1870. Supersedes DouglasNeuroInformatics#1822, whose planRelease and tests this adapts.

Co-Authored-By: Gabriel A. Devenyi <gdevenyi@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new reference page says what a version promises, which versions receive
which fixes for how long, which image tag to run, and how to install from
and upgrade between release tags. It carries the end-of-support table that
increment-version.ts keeps current. SECURITY.md links to it instead of
promising fixes to the most recent version only, and points reporters at
private vulnerability reporting. The deployment guide clones a release tag
rather than main.

Refs DouglasNeuroInformatics#1870.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RELEASE_CHANNEL defaults to 2 instead of latest, so a new deployment
receives every release of its major, fixes included, and never crosses to
a new major on its own. increment-version.ts rewrites it on a major bump.

Refs DouglasNeuroInformatics#1870.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
increment-version.ts now takes the bump from a set the policy allows. Once
a breaking commit is in range it offers and accepts major only. On a
superseded major, one with a higher major tagged, it offers and accepts
patch only, refuses a breaking or feat commit in range outright, and names
the release/<major> branch as the base of the PR. It reads both version
parsing and that superseded check from .github/scripts/release.cjs, which
makes the same decision for the release workflow.

A major bump also rewrites RELEASE_CHANNEL in .env.template and adds the
new major to the support table, marking the previous one superseded with
its end of support. Both edits are computed before any file is written,
so a missing line or row fails the run with the tree untouched.

Refs DouglasNeuroInformatics#1870.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new architecture doc, release-channels.md, holds the model: the outputs
of planRelease, what each run publishes, release/<major> branches, the bump
rules and what a major bump rewrites. cut-a-release.md follows the new
jobs and adds the steps for a release branch, and testing-strategy.md
lists the github vitest project and the new CI and Release triggers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joshunrau
joshunrau merged commit f58e490 into DouglasNeuroInformatics:main Nov 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants