Skip to content

feat(release-candidate): release a commit that is not the trunk tip - #45

Merged
Troublor merged 1 commit into
mainfrom
william/feat/candidate-from-commit
Sep 11, 2026
Merged

feat(release-candidate): release a commit that is not the trunk tip#45
Troublor merged 1 commit into
mainfrom
william/feat/candidate-from-commit

Conversation

@Troublor

Copy link
Copy Markdown
Collaborator

main moves while a release is being prepared. Until now the pipeline could
only ship its tip: propose is guarded to the default branch, the candidate PR
merges there, and cut creates the release branch at that merge commit.

This adds a base_commit input to release-candidate, propose only.

What it does

gh workflow run release-candidate.yml --ref main -f version=1.2.3 -f base_commit=<sha>

  • Resolves the commit-ish (full SHA, tag, origin/main~4) and refuses
    anything the default branch cannot reach
    , so a release still ships code that
    went through trunk. Note this rules out v* tags, which sit off trunk once
    settlement commits the dated changelog on the release branch.
  • Creates release-v1.2.3 at that commit itself and checks it out, so the bump
    and the changelog entry are drafted over what actually ships, not over the
    trunk tip.
  • Opens the candidate PR against that branch. It cannot target the default
    branch: merging it there would take the commits the release is deliberately
    leaving out.
  • cut then has nothing to do, and does not even fire (the template's
    pull_request trigger filters on the default branch). It also short-circuits
    with a notice if a consumer's workflow has no such filter.

release-settle is unchanged, as asked: the release-branch tip it wants is the
merge of that candidate PR.

Consequences, documented

  • The default branch is left untouched. Its version file keeps the old
    version; it gains no changelog entry until the next candidate, which syncs it
    back from the tag the way it already does after any release.
  • The version must still be newer than the newest v* tag, so this releases an
    earlier commit on the current line. It is not a backport flow.
  • Re-dispatching after a failed attempt reuses a release branch still sitting on
    exactly the requested commit; any other mismatch is refused.
  • That candidate PR is governed by the release branch ruleset, so it needs its
    approving review and a squash merge.

Unchanged when base_commit is empty

base: on create-pull-request falls back to the checked-out branch when the
input is empty (source),
and every existing guard rule is kept in order.

Testing

New release-candidate job in actions-test.yml. It extracts the shipped
steps from action.yml
rather than copying them, and runs them against a
throwaway repository shaped like a settled release line (tag off trunk, an
unreachable side commit), with a fake gh for the default-branch lookup and the
ref creation. It covers: the trunk path unchanged; SHA and relative-ref
resolution; refusal of off-trunk commits, release tags and nonsense; the
pre-existing default-branch and version rules; the branch actually cut at the
commit with the notes stopping there; and re-dispatch reuse.

Verified locally that the job fails when the ancestry guard is removed, and when
a step under test is renamed.

🤖 Generated with Claude Code

`main` moves while a release is being prepared, and until now the pipeline
had no way to ship anything but its tip: propose is guarded to the default
branch, the candidate PR merges there, and cut creates the release branch at
that merge commit.

`base_commit` names the commit to release instead. Propose resolves it (any
commit-ish), refuses anything the default branch cannot reach so a release
still ships code that went through trunk, creates `release-vX.Y.Z` at that
commit itself, and opens the candidate PR against that branch — it cannot
target the default branch, because merging it there would take the commits
the release is deliberately leaving out. Checking the branch out first also
means the bump and the changelog entry are drafted over what actually ships.
Cut then has nothing to do, and settle is untouched: the release branch tip
it wants is the merge of that PR.

The default branch is left alone, so its version file keeps the old version
and the next candidate syncs the changelog entry back from the tag, the way
it already does after any release. Re-dispatching after a failed attempt
reuses a release branch still sitting on exactly the requested commit;
anything else is refused.

The trunk-first flow is unchanged when `base_commit` is empty: the PR's base
falls back to the checked-out branch and the guard keeps every rule it had.

Tested in actions-test.yml by extracting the shipped steps from action.yml
and running them against a throwaway repository shaped like a settled
release line, with a fake `gh`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Troublor
Troublor merged commit d5a0095 into main Sep 11, 2026
9 checks passed
@Troublor
Troublor deleted the william/feat/candidate-from-commit branch September 11, 2026 08:52
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.

1 participant