Skip to content

ci: release from release branches and keep latest on tagged releases - #1822

Open
gdevenyi wants to merge 5 commits into
mainfrom
ci/release-branches
Open

gdevenyi wants to merge 5 commits into
mainfrom
ci/release-branches

Conversation

@gdevenyi

Copy link
Copy Markdown
Contributor

Why

The October 2026 Node.js security releases fix four High CVEs in Node 24 (TLS hostname checks, the Brotli decoder, dns), and the api and gateway images run Node. The published latest and 2.5.2 images were built on 2026-10-07 from 432dec8 with Node 24.21.0, so they are vulnerable.

Those images are also not the code at the v2.5.2 tag (3c3f081): every push to main re-pushed 2.5.2 and latest. This PR changes that, so that we can ship v2.5.3 from a release/2.5 branch built from the code that deployments run now.

What changed

ci: release branches, and latest only on tagged releases

  • .github/scripts/release.cjs now exports a pure planRelease({ ref, tags, version }). A run is a release only when v<version> is not a git tag. It reads git tags only, never GHCR or npm.
  • Every push to main pushes edge and sha-<7>. A release pushes <version> and <major>.<minor>, and also latest (GHCR, npm dist-tag, GitHub "latest release") only when its version is higher than every tag.
  • release/<major>.<minor> branches release on workflow_dispatch only. On a branch that is not the newest line, npm publishes under dist-tag release-<major>.<minor>. configure fails on another ref, on a version that is already tagged, or on a version that is not on the branch's line.
  • The GitHub release sets target_commitish: ${{ github.sha }}. Before this change, a new tag was created on the default branch.
  • ci.yaml also runs on PRs to release/**.
  • Docs: new .agents/docs/architecture/release-channels.md. cut-a-release.md gets a "From a release branch" section. The odc-release and odc-testing skills, testing-strategy.md, .env.template and the playground deployment doc are updated.

fix: Node 24.22.1

  • engines.node is ^24.22.1 || >=26.12.1. This is stricter than a new minimum: it also refuses the vulnerable 26.x before 26.12.1. pnpm only warns, because engine-strict is off.
  • main keeps FROM node:lts-krypton, which already resolves to 24.22.1, so the next build is patched. A release branch pins FROM node:<exact> (documented in the playbook and in release-channels.md).

After merge

The merge run pushes edge only, because v2.5.2 exists, so latest does not change. Next: add release/* to the release environment's deployment-branch policy, cut release/2.5, pin Node, bump to 2.5.3, and dispatch. That dispatch is the first run of publish-npm --tag, make_latest and target_commitish.

Verification

Run locally on Node 24.22.1:

  • pnpm lint: passes.
  • pnpm test:coverage: passes at 100%. New vitest project github with .github/scripts/__tests__/release.test.ts (10 tests; release.cjs has 100% statement and branch coverage).
  • pnpm test:e2e: 276 of 278 pass. The two failures are not related to this change: record-file-deletion.spec.ts gets a 503 because the local .env has STORAGE_ENABLED=false, and subject-detail.spec.ts timed out on a detached element, then passed 9 of 9 on a second run.
  • ./scripts/increment-version.ts --bump patch --yes dry run: the 2.5.3 changelog section generates from all 321 commits since v2.5.2 (I undid the dry run).

Exceptions:

  • No e2e spec: this changes the release workflow, which Playwright cannot run. The first real test of the release-only steps is the release/2.5 dispatch.
  • The coverage exclude entry '**/scripts/**' is narrowed to '{apps,packages,runtime}/*/scripts/**', because it also matched .github/scripts/. No file in the earlier coverage scope is under a scripts/ directory.
  • workspace-map.md gets no row for the github project, because .github is not a workspace.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

gdevenyi and others added 2 commits October 22, 2026 07:56
The release decision moves into a tested planRelease function in
.github/scripts/release.cjs. A run releases only when v<version> is not a
git tag yet. latest (GHCR, npm, GitHub) moves only with a release whose
version is higher than every tag. Every push to main builds edge and
sha-<short>. A release/<major>.<minor> branch releases on dispatch,
publishes <version> and <major>.<minor>, and uses the release-<major>.<minor>
npm dist-tag when it is not the newest line. The GitHub tag now goes on
the built commit. CI also runs on pull requests to release branches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G73CpTPk1EjKYeBAKPacit
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
@gdevenyi
gdevenyi requested a review from joshunrau as a code owner October 22, 2026 11:57
Comment thread .agents/docs/playbooks/cut-a-release.md Outdated
Comment thread package.json Outdated
Comment thread .agents/skills/odc-release/SKILL.md Outdated
…d branches

A release branch cut from a commit before release-branch support has the
old configure and ci.yaml, so the cut-a-release playbook now takes them
from main in the branch's first commit.

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

Copy link
Copy Markdown
Contributor Author

@thomasbeaudry this has a few impacts to existing deployments

  1. The use of :latest tag has been resulting in differing installed versions depending on when an install was run because it would get re-targeted on each new commit. Now it will stay pointed at the latest tagged release.
  2. Installed versions should point a particular tag.
  3. Staging should point at :latest so we can use that for intermediate testing.
  4. The new :edge tag should be use for the auto-deploying demo and for dev environments.

@thomasbeaudry thomasbeaudry left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two release-channel rollback cases that should be fixed before merging. Reviewed the merge result; lint passes and the github project passes all 10 tests with 100% coverage. Full workspace coverage/e2e and real publishing were not run; local Node was 24.22.0.

If delegated to Claude Code, Fable 5 is the skill-recommended size for changes spanning release planning and publishing.

Reviewed at commit 8e83c56.

Comment thread .github/scripts/release.cjs Outdated
Comment thread .github/workflows/release.yaml
planRelease decides from a tag snapshot, so overlapping runs on two refs
could both plan latest, and the one that finished last owned it, or they
could release one version from two commits. The concurrency group now
spans every ref and never cancels a running release. planRelease also
refuses an untagged version lower than a tag on its own line, so a late
retry cannot move the line tag back.

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

@thomasbeaudry thomasbeaudry left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two earlier findings are addressed in a9a35d7. The second pass found one additional npm channel recovery issue, described inline.

If delegated to Claude Code, this review skill recommends Fable 5 for changes spanning release planning and publishing.

Reviewed at commit a9a35d7.

Comment thread .github/workflows/release.yaml Outdated
echo "Publishing ${name}@${version}"
pnpm --filter "${name}" publish --no-git-checks
echo "Publishing ${name}@${version} under ${NPM_TAG}"
pnpm --filter "${name}" publish --no-git-checks --tag "${NPM_TAG}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reconcile the selected dist-tag for already-published npm versions

The new npmTag can change between whole-run retries, but --tag is only applied after the existing-version check has continued past published packages. For example, 2.5.3 can publish its packages under latest and then fail before creating v2.5.3. If 2.6.0 succeeds before the whole 2.5.3 run is retried, the retry correctly plans npmTag=release-2.5; however, every already-published 2.5.3 package is skipped, so release-2.5 is never created or advanced for those packages. The workflow can finish green while installs using that maintenance channel fail or get an older version. I reproduced this with the current planRelease and the exact Publish Packages shell block: exit 0, only npm view invoked, no channel update. Please reconcile or verify the planned dist-tag even when the tarball exists, or fail explicitly and require a fresh patch, and cover this retry scenario. See npm dist-tag documentation for the distinction between publishing and tagging an existing version.

Reviewed at commit a9a35d7.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 68aa1e3, with the explicit failure. A reconcile with npm dist-tag add would need the opt-in "Allow npm dist-tag" setting on all six trusted publishers, because OIDC trusted publishing cannot move a dist-tag otherwise.

The shell loop is now .github/scripts/publish-npm.cjs, run through actions/github-script. It skips a published version only while the planned npmTag points at it. Otherwise it throws:

@odc/a@2.5.3 is already published, but its release-2.5 dist-tag is on '2.5.2'; trusted publishing cannot move a dist-tag, so cut a fresh patch

Your scenario (2.5.3 published under latest, then 2.6.0 releases, then a whole-run retry of 2.5.3 that plans release-2.5) is the test should fail a re-run whose planned dist-tag is elsewhere, so the channel is never left behind silently in .github/scripts/__tests__/publish-npm.test.ts. The normal re-run, where the tag still points at the version, is covered by should skip a published version whose planned dist-tag already points at it. 15 tests in the github project, and both scripts have 100% coverage. The docs (release-channels.md, cut-a-release.md, the odc-release skill) state the rule and the recovery.

A re-run planned after a newer release can carry another npmTag than the
run that published. The publish loop skipped every published version, so
the release-<major>.<minor> channel stayed on an older version while the
run finished green. Trusted publishing cannot move a dist-tag, so the new
tested .github/scripts/publish-npm.cjs skips a published version only
while npmTag points at it, and otherwise fails with a request for a fresh
patch.

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

This branch has not been deployed

No deployments
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