Skip to content

ci(release): support prerelease versions such as 1.0.0-alpha.1 - #156

Merged
daften merged 3 commits into
developfrom
claude/confident-bardeen-vzptx9
Oct 29, 2026
Merged

daften merged 3 commits into
developfrom
claude/confident-bardeen-vzptx9

Conversation

@daften

@daften daften commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

Prerequisite for releasing a 1.0.0-alpha.1 (or any prerelease). The release pipeline assumed every version is a plain X.Y.Z; tagging a prerelease today would have hijacked latest everywhere. Stable releases are unchanged, except that the new next channel follows them.

What was wrong (each verified, not assumed):

Where Behaviour before
publish.yml any v* tag publishes npm latest, Docker :latest/:X/:X.Y, and a stable GitHub Release
auto-release.yml npm version patch on 1.0.0-alpha.1 gives 1.0.0, so the next Dependabot security merge would publish a stable 1.0.0 unannounced
publish.yml nightly Number('0-alpha1') is NaN, giving version 1.0.NaN-nightly.<stamp>
changelog-guard.mjs baselineTag returns v1.0.0-alpha.1 once v1.0.0 exists (git version sort ranks the longer name first); a prerelease [X-pre]: link definition is swallowed into the oldest section; a prerelease section listed below its stable release is not flagged (NaN <= 0 is false)

The channel model

  • Closed vocabulary: a prerelease must be X.Y.Z-alpha.N, -beta.N or -rc.N (dot, number, no leading zero). Anything else (-alpha1, -dev.1, -foo.2, -1) fails the tag push before any output is written. The three stages sort in maturity order on their own (alpha < beta < rc < stable).
  • One prerelease channel, next (not one tag per stage, which goes stale): npm dist-tag next and Docker :next. next is the newest tagged release of any kind. A prerelease moves it; a stable release moves it only when it is newer than every existing v* tag, so @next never trails @latest.
  • next only moves forward: a stable hotfix on an old line (0.5.7 while 1.0.0-rc.1 exists) leaves it alone, and a prerelease older than an existing tag (1.0.0-rc.2 after 1.0.0) is refused.
  • Prerelease artifacts: npm next (never latest); Docker :X.Y.Z-pre.N and :next only (no :latest, :X, :X.Y); GitHub pre-release with make_latest: false.
  • Stable release: unchanged (latest, :latest, :X.Y.Z, :X.Y, :X), plus next when it is the newest tag.
  • Nightly is a separate channel (develop, never tagged) and keeps working.

What changes

  • scripts/release-version.mjs (new): classify, is-newest, nightly-base, assert-stable.
  • publish.yml: uses it for the tag channel, the update_next decision and the nightly base. New step after the npm publish moves the next dist-tag to a stable release. It is continue-on-error because it needs the opt-in Allow npm dist-tag permission on the package's trusted-publisher config (or a token); without it the release stays good and the run prints npm dist-tag add @daften/fireflyiii-mcp@X.Y.Z next.
  • auto-release.yml: fails loudly instead of bumping over a prerelease.
  • changelog-guard.mjs: semver-precedence compareVersions (exported), prerelease link definitions, and baselineTag re-sorted by precedence rather than git's version sort.
  • Tests: release-version.node-test.mjs (new, 60 automation tests in total) and prerelease cases in changelog-guard.node-test.mjs. The old guard accepted a mis-ordered prerelease section; the new one flags it.
  • AGENTS.md: "Prerelease releases" section. CHANGELOG.md: one ### Changed bullet.

Merge as a merge commit, never squash or rebase.

Verification

  • The real run: block of the setup step, extracted from publish.yml, was executed under bash -e against throwaway git repos with different existing tags: stable and prerelease tags produce the right prerelease/npm_tag/update_next; a hotfix below the newest tag gets update_next=false; a prerelease older than a stable tag, -foo.1 and -alpha1 exit 1.
  • Docker tag action behaviour confirmed from its docs and source: prereleases only extend {{version}} (never {{major}}/{{major}}.{{minor}}), latest=auto never adds latest for a prerelease, and an explicit type=raw entry still produces its tag.

Not verified (cannot be exercised before a real prerelease tag exists)

  • That softprops/action-gh-release accepts prerelease/make_latest as written, and that npm dist-tag add works from the trusted-publisher OIDC session once the permission is enabled (based on a GitHub changelog dated 2026-09-30 that could not be opened from the build environment). Watch the first prerelease run and the first stable run after it.
  • The nightly schedule and auto-release run the workflow file from main, so none of this takes effect on them until it ships in a release.

Type of change

  • feat (new tool or feature)
  • fix (bug fix)
  • refactor (code cleanup, no behavior change)
  • test (add or update tests)
  • chore (dependencies, config, scaffolding)
  • docs (documentation only)

Checklist

  • Tests added or updated
  • npm test passes locally (598 passed, 29 skipped); node --test tests/automation/*.node-test.mjs 60 passed
  • npx tsc --noEmit passes locally
  • README tool table updated (no tools changed)
  • CLAUDE.md updated (AGENTS.md updated instead)
  • Verified against the Firefly III OpenAPI spec (not API-touching)

🤖 Generated with Claude Code

https://claude.ai/code/session_012ESxuEjCeVsRAAxAb29Yjr

A v* tag always published to npm latest, the Docker latest/major/minor
tags and a stable GitHub Release; auto-release.yml's npm version patch
turns a prerelease into the stable release; the nightly version became
1.0.NaN-nightly.<stamp>; and changelog-guard ranked v1.0.0-alpha.1 above
v1.0.0 and could not order or link prerelease sections.

scripts/release-version.mjs now classifies a version for the workflows:
prereleases publish under their own dist-tag, push only :X.Y.Z-pre.N and
:pre to GHCR, create a GitHub pre-release, and make auto-release fail
loudly. Stable releases are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ESxuEjCeVsRAAxAb29Yjr
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 95.63% 1227 / 1283
🔵 Statements 95.28% 1393 / 1462
🔵 Functions 96.73% 355 / 367
🔵 Branches 93.07% 712 / 765
File CoverageNo changed files found.
Generated in workflow #367 for commit 78d58e2 by the Vitest Coverage Report Action

CodeQL (js/identity-replacement) flagged replacing the alpha.1 header with
itself; the test only needs the 1.0.0 section inserted below it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ESxuEjCeVsRAAxAb29Yjr
…a/rc

Prereleases must be X.Y.Z-(alpha|beta|rc).N and all publish to a single
next channel (npm dist-tag, Docker :next) instead of one tag per stage
that goes stale. next follows the newest tagged release of any kind: a
stable release that is the newest tag moves it too, it never moves
backwards, and a prerelease older than an existing tag is refused.

Moving the npm dist-tag after a stable publish is continue-on-error and
prints the manual command, since it needs an opt-in permission.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ESxuEjCeVsRAAxAb29Yjr
@daften
daften merged commit 6e49284 into develop Oct 29, 2026
10 checks passed
@daften daften mentioned this pull request Oct 29, 2026
3 of 12 tasks
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.

3 participants