Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .github/actions/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,16 @@ gh workflow run release-candidate.yml --ref main \
```

The commit may be given as anything the checkout resolves — a full SHA, a
tag, `origin/main~4` — but it must be reachable from the default branch: a
release ships code that went through trunk. Note that `v*` release tags are
not reachable from it, because settlement commits the dated changelog on the
release branch; to release from an old release line, name a commit on the
default branch, not its tag.
tag, `origin/main~4`. It must be reachable from the default branch, **or**
sit on the latest `v*` release line (from the merge-base with the default
branch through that tag). Settlement leaves the tag off trunk, so a hotfix
of the current release is named as that tag (or its peeled SHA), not as a
commit further down the default branch.

A side branch that never landed on trunk is still refused. So is an older
release line that a higher `v*` tag has already passed: the version must
still beat the newest tag, and those commits are outside the latest-line
window.

Step 1 then works differently, and steps 3 and 4 are unchanged:

Expand All @@ -174,9 +179,10 @@ Step 1 then works differently, and steps 3 and 4 are unchanged:
next version must still be newer than `v1.2.3`.

The version must be newer than the newest `v*` tag in the repository, as
usual, so this releases an earlier commit on the current line. It does not
patch an older line that a higher tag has already passed; there is no
backport flow.
usual. That covers two cases: an earlier commit on the current default
branch, and a hotfix of the latest tag (new `release-vX.Y.Z` cut there,
candidate PR aimed at that branch). It still does not patch an older line
that a higher tag has already passed.

If an attempt fails after the branch was created — a broken `bump_command`,
say — re-dispatching the same version and commit reuses the branch, because
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/release-candidate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Family: [Release pipeline](../RELEASE.md).
| `stage` | yes | | `propose` (on workflow_dispatch) or `cut` (on the candidate PR merging). |
| `token` | yes | | Token that authors the PR and creates the branch. Must be a GitHub App installation token (e.g. the Maxwell app) so the PR triggers CI; the job's GITHUB_TOKEN does not. |
| `version` | no | | Version to release, X.Y.Z or vX.Y.Z. Required for `propose`. |
| `base_commit` | no | | The commit to release, for `propose`, when it must not be the tip of the default branch. Any commit-ish the checkout can resolve (a full SHA, a tag, `origin/main~3`), and it must be reachable from the default branch — a release still ships code that went through trunk. Empty (the default) is the trunk-first flow: the candidate PR merges into the default branch and `cut` creates the release branch at that merge commit. Set, `propose` creates the release branch at this commit and opens the candidate PR against that branch, because merging it into the default branch would take the commits this release is leaving out; the default branch is then untouched and `cut` never runs. The version must still be newer than the newest `v*` tag in the repository. |
| `base_commit` | no | | The commit to release, for `propose`, when it must not be the tip of the default branch. Any commit-ish the checkout can resolve (a full SHA, a tag, `origin/main~3`). It must be reachable from the default branch, or sit on the latest `v*` release line (from the merge-base with trunk through that tag), so a hotfix of the current tag works even though settlement left the tag off trunk. Empty (the default) is the trunk-first flow: the candidate PR merges into the default branch and `cut` creates the release branch at that merge commit. Set, `propose` creates the release branch at this commit and opens the candidate PR against that branch, because merging it into the default branch would take the commits this release is leaving out; the default branch is then untouched and `cut` never runs. The version must still be newer than the newest `v*` tag in the repository. |
| `version_file` | yes | | File holding the version (VERSION, Cargo.toml, pyproject.toml, package.json). |
| `version_pattern` | no | `plain` | How the version is stored in `version_file`: plain, toml or json. |
| `bump_command` | no | | Optional shell command run after `version_file` is rewritten, for anything else that must move with the version: lockfiles (`cargo update --workspace`), path-dependency versions, generated files. Runs with `OLD_VERSION` and `NEW_VERSION` in the environment, in the repository root, under `bash -euo pipefail`. Whatever it changes is committed with the bump. Install any toolchain it needs in the calling job before this action. |
Expand Down
29 changes: 25 additions & 4 deletions .github/actions/release-candidate/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ inputs:
description: >-
The commit to release, for `propose`, when it must not be the tip of the
default branch. Any commit-ish the checkout can resolve (a full SHA, a
tag, `origin/main~3`), and it must be reachable from the default branch
— a release still ships code that went through trunk. Empty (the
tag, `origin/main~3`). It must be reachable from the default branch, or
sit on the latest `v*` release line (from the merge-base with trunk
through that tag), so a hotfix of the current tag works even though
settlement left the tag off trunk. Empty (the
default) is the trunk-first flow: the candidate PR merges into the
default branch and `cut` creates the release branch at that merge commit.
Set, `propose` creates the release branch at this commit and opens the
Expand Down Expand Up @@ -188,8 +190,27 @@ runs:
exit 1
fi
if ! git merge-base --is-ancestor "$base" "origin/$default"; then
echo "::error::base_commit $base is not reachable from the default branch ($default); a release must ship code that landed on trunk"
exit 1
# Settlement leaves the latest tag off trunk (bump + dated
# changelog). A hotfix of that tag is still the current line,
# not a random side branch.
latest="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
on_release_line=0
if [[ -n "$latest" ]]; then
latest_sha="$(git rev-parse --verify --quiet "${latest}^{commit}" || true)"
if [[ -n "$latest_sha" ]]; then
mb="$(git merge-base "$latest_sha" "origin/$default" || true)"
if [[ -n "$mb" ]] \
&& git merge-base --is-ancestor "$mb" "$base" \
&& git merge-base --is-ancestor "$base" "$latest_sha"; then
on_release_line=1
fi
fi
fi
if [[ "$on_release_line" -ne 1 ]]; then
echo "::error::base_commit $base is not reachable from the default branch ($default); a release must ship code that landed on trunk"
exit 1
fi
echo "base commit $base is on the latest release line (${latest}), not on $default"
fi
echo "base commit: $BASE_COMMIT -> $base"
fi
Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/actions-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -614,9 +614,19 @@ jobs:
set -euo pipefail
source "$SH/helpers.sh"
refuse 1.1.0 "$(cat "$RUNNER_TEMP/unreachable")" "not reachable from the default branch"
refuse 1.1.0 v1.0.0 "not reachable from the default branch"
refuse 1.1.0 0000000000000000000000000000000000000000 "is not a commit in this checkout"
echo "an off-trunk commit, a release tag and nonsense are all refused"
echo "an off-trunk commit and nonsense are refused"

- name: The latest release tag is an allowed base
run: |
set -euo pipefail
source "$SH/helpers.sh"
latest="$(git -C "$REPO" rev-parse 'v1.0.0^{commit}')"
guard 1.1.0 v1.0.0
expect_output "base=$latest"
guard 1.1.0 "$latest"
expect_output "base=$latest"
echo "a hotfix of the current tag starts from that tag"

- name: The rules that were already there still apply
run: |
Expand Down
Loading