Skip to content

fix(release): make pr mode tag on merge instead of on the pre-bump commit - #936

Merged
BryanFRD merged 1 commit into
mainfrom
fix/pr-mode-two-phase
Aug 25, 2026
Merged

BryanFRD merged 1 commit into
mainfrom
fix/pr-mode-two-phase

Conversation

@BryanFRD

Copy link
Copy Markdown
Contributor

Closes #934.

Splits releaseCommitMode: "pr" into the two phases it always needed.

Proposing. Compute the bump, write the files onto the release branch, open or update the PR. No tags, no GitHub releases. Every later commit on the target branch regenerates the branch, so the open PR keeps tracking the version that would ship now.

Finalising. After the PR merges, a chore(release): commit sits on the target branch carrying versions that no tag covers. That run tags exactly those versions and publishes the releases.

The versions are read from the version files, never recomputed. That is the part the issue flagged as load-bearing: gating the tags alone would have left the merge proposing 1.1.0 → 1.2.0 instead of tagging 1.1.0, and every merged release PR would have spawned another one a notch higher.

Shape

ReleasePlan gains finalizing, which resolves the mode to None for that run. One flag covers both halves: the commit step becomes a no-op because the files are already committed, and the tag and release steps run because the mode is no longer Pr.

finalize::merged_release_tags does the detection. For each package it reads the version from the first versioned file, computes the tag through the existing tag_for_version, and keeps it only when that tag is missing and a chore(release): commit appears in the window since the package's last tag. The second condition is what stops a hand-edited version file from being tagged as if it had been released.

Verified end to end

Against real repositories with a local bare remote, not just unit tests.

Proposing, no tags anywhere:

● app  1.0.0 → 1.1.0  (minor, from tag v1.0.0, over Cargo.toml)
  ✓ Updated Cargo.toml

✓ Pushed branch ferrflow/release-main

$ git ls-remote --tags origin
13d0d6b  refs/tags/v1.0.0

Finalising after the merge:

● app  1.1.0  (release merged, tagging now)
  ✓ Created tag v1.1.0

✓ Pushed tags
tag v1.1.0 commit:  cbe0c32 Merge pull request #1 from ferrflow/release-main
version at the tag: version = "1.1.0"

The tag now lands on the commit that carries the bump, and the version there matches the tag. A third run reports "Nothing to release", so it is idempotent.

Squash merge takes the same path, tagging the squash commit with 1.1.0 present.

And the symptom that started this, the PR never updating, is gone. A second feat!: landing on main before the merge regenerates the branch:

run1: ● app  1.0.0 → 1.1.0  (minor)
run1: ✓ Pushed branch ferrflow/release-main
run2: ● app  1.0.0 → 2.0.0  (major)
run2: ✓ Pushed branch ferrflow/release-main

Before this change run2 said "Nothing to release".

Tests

Five unit tests on the detection, including a commit that merely mentions chore(release): in its text and must not trigger finalisation, and prerelease detection from the version.

Four tests on the gating itself, through the existing forge harness: pr mode creates no tags locally, pushes none, and publishes no releases; finalising does all three; finalising tags the release commit rather than the one behind it; and commit mode still does everything in one pass.

1191 bin tests and 909 lib tests passing, clippy clean, wasm surface still builds.

Limits worth knowing

A package with no versionedFiles has no version to read, so it is not finalised this way. commit mode remains the answer for tag-only packages. Called out in the README rather than left to be discovered.

is_prerelease is derived by parsing the version, since the finalising run has no bump plan to inherit it from. Non-semver versions fall back to false.

Existing repositories

Anything already on pr mode has tags one commit behind their bump and releases published for PRs that may never merge. Those are not repaired by this change. IdleWarden/idlewarden#1 is the case from the issue and needs its 12 tags and 12 releases removed by hand before it can release cleanly again.

@ferrfleet ferrfleet Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The two-phase split (propose without tagging/publishing, finalize by reading committed versions rather than recomputing) is sound, and the chore(release):-in-window guard correctly avoids treating a hand-edited version file as a merged release. Mode gating in execute.rs (tag creation, tag push, release publish) is consistent for both the Pr-proposing path and the finalizing→None path. Good test coverage for the single-package cases described in the PR.

Two non-blocking observations:

Nit: In a monorepo, finalizing is a single global bool derived from !finalize_tags.is_empty() (mod.rs). If any package needs finalizing, bump_order is set to &[] for the whole run, so proposing/bumping for every other package is skipped entirely that run — not just tag/release creation for the finalizing package. Unless another commit lands on the target branch soon after, unrelated packages with pending commits won't get their release PR opened/updated until the next trigger. Might be worth scoping the skip to just the packages being finalized rather than gating the entire per-package loop.

Nit: finalize_tags is forced to empty whenever dry_run is true (mod.rs), and ferrflow check always calls run_release_logic with dry_run: true (check.rs). So ferrflow check right after a release PR merges will report "nothing to release" instead of previewing the pending finalize — easy to be confused by if anyone relies on check to see what the next real run will do.

Neither affects the core single-package flow this PR is fixing.

@github-actions

Copy link
Copy Markdown

SonarQube — 2 issue(s) introduite(s) par cette PR

  • CRITICAL src/monorepo/run/mod.rs L52 — Refactor this function to reduce its Cognitive Complexity from 205 to the 15 allowed. rust:S3776
  • CRITICAL src/monorepo/run/execute.rs L44 — Refactor this function to reduce its Cognitive Complexity from 25 to the 15 allowed. rust:S3776

2 issue(s) corrigée(s) sur les fichiers touchés.

Comparaison entre le projet bac à sable de cette PR et la branche par défaut : SonarQube Community n'analyse pas les PR, ce delta est calculé côté CI. Détail

@BryanFRD
BryanFRD merged commit 197aadb into main Aug 25, 2026
40 checks passed
@BryanFRD
BryanFRD deleted the fix/pr-mode-two-phase branch August 25, 2026 15:17
ferrflow Bot added a commit that referenced this pull request Aug 25, 2026
## [7.8.0] - 2026-08-25

### Features

- feat(config): support per-package config files via an include key (#932)

### Bug Fixes

- fix(release): make pr mode tag on merge instead of on the pre-bump commit (#936)
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.

fix(release): pr mode tags the pre-bump commit and then never updates the PR

1 participant