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
8 changes: 5 additions & 3 deletions .github/actions/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ 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
it is still sitting on exactly that commit with nothing merged into it. Any
other mismatch is refused, and the branch has to be deleted by hand.
say — re-dispatching the same version and commit reuses the branch. If
hotfix PRs have already landed on it, propose sits on the tip (it does not
reset to `base_commit`) and skips the version bump when the version file
already reads the requested version. A branch that does not contain
`base_commit` is refused.

## Installing it in a repository

Expand Down
5 changes: 3 additions & 2 deletions .github/actions/release-candidate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ Family: [Release pipeline](../RELEASE.md).
1. Guard (propose) *(only if `inputs.stage == 'propose'`)*
1. Cut the release branch at the base commit (propose) *(only if `inputs.stage == 'propose' && inputs.base_commit != ''`)*
1. Bump version file *(only if `inputs.stage == 'propose'`)*
1. Run bump command *(only if `inputs.stage == 'propose' && inputs.bump_command != ''`)*
1. Draft changelog *(only if `inputs.stage == 'propose' && inputs.changelog_file != ''`)*
1. Run bump command *(only if `inputs.stage == 'propose' && inputs.bump_command != '' && steps.bump.outputs.skipped != 'true'`)*
1. Draft changelog *(only if `inputs.stage == 'propose' && inputs.changelog_file != '' && steps.bump.outputs.skipped != 'true'`)*
1. Remove git auth before opening the PR *(only if `inputs.stage == 'propose'`)*
1. Drop a stale candidate branch *(only if `inputs.stage == 'propose'`)*
1. Open candidate PR — `peter-evans/create-pull-request` *(only if `inputs.stage == 'propose'`)*
Expand All @@ -66,6 +66,7 @@ Family: [Release pipeline](../RELEASE.md).
- `base_commit $BASE_COMMIT is not a commit in this checkout; check the repository out with fetch-depth: 0`
- `base_commit $base is not reachable from the default branch ($default); a release must ship code that landed on trunk`
- `branch $branch already exists`
- `branch $branch already exists at $existing, which does not contain base_commit $base`
- `after bump_command, $FILE reads $actual, expected $NEW_VERSION`
- `cut runs on a merged candidate PR (pull_request closed, merged == true)`
- `candidate PR #$PR_NUMBER was opened by`
Expand Down
49 changes: 36 additions & 13 deletions .github/actions/release-candidate/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,21 +214,32 @@ runs:
fi
echo "base commit: $BASE_COMMIT -> $base"
fi
# The release branch must not exist yet. One exception: with an
# explicit commit, propose creates that branch before it opens the PR,
# so a re-dispatch after a failed attempt finds its own branch still
# sitting on the requested commit, with nothing merged into it.
# The release branch must not exist yet, except with an explicit
# commit: propose creates it, so a re-dispatch may find it still on
# that commit, or already ahead with hotfix PRs. Sit on the tip in
# that case (do not reset to base_commit). A branch that does not
# contain base_commit is someone else's and is refused.
existing="$(git ls-remote --heads origin "refs/heads/$branch" | cut -f1)"
checkout="$base"
if [[ -n "$existing" ]]; then
if [[ -n "$base" && "$existing" == "$base" ]]; then
if [[ -z "$base" ]]; then
echo "::error::branch $branch already exists"; exit 1
fi
git fetch --quiet origin "+refs/heads/$branch:refs/remotes/origin/$branch"
if [[ "$existing" == "$base" ]]; then
echo "::notice title=Reusing the release branch::$branch already exists at $base, from an earlier attempt at this candidate"
elif git merge-base --is-ancestor "$base" "$existing"; then
echo "::notice title=Reusing the release branch::$branch already has commits on top of $base; proposing from $existing"
checkout="$existing"
else
echo "::error::branch $branch already exists"; exit 1
echo "::error::branch $branch already exists at $existing, which does not contain base_commit $base"
exit 1
fi
fi
{
echo "base=$base"
echo "branch=$branch"
echo "checkout=$checkout"
} >> "$GITHUB_OUTPUT"

# With an explicit base commit the candidate PR cannot target the default
Expand All @@ -244,15 +255,19 @@ runs:
GH_TOKEN: ${{ inputs.token }}
BRANCH: ${{ steps.guard.outputs.branch }}
BASE: ${{ steps.guard.outputs.base }}
CHECKOUT: ${{ steps.guard.outputs.checkout }}
run: |
set -euo pipefail
checkout="${CHECKOUT:-$BASE}"
if ! git ls-remote --exit-code --heads origin "refs/heads/$BRANCH" >/dev/null; then
gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/heads/$BRANCH" -f sha="$BASE" >/dev/null
echo "created $BRANCH at $BASE"
gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/heads/$BRANCH" -f sha="$checkout" >/dev/null
echo "created $BRANCH at $checkout"
else
git fetch --quiet origin "+refs/heads/$BRANCH:refs/remotes/origin/$BRANCH"
fi
git checkout --quiet -B "$BRANCH" "$BASE"
git checkout --quiet -B "$BRANCH" "$checkout"
echo "release_branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "candidate PR will target $BRANCH"
echo "candidate PR will target $BRANCH at $checkout"

- name: Bump version file
if: inputs.stage == 'propose'
Expand All @@ -265,13 +280,21 @@ runs:
TOOLS: ${{ github.action_path }}/../release-tools/release_tools.py
run: |
set -euo pipefail
old="$(python3 "$TOOLS" bump-file "$FILE" "$PATTERN" "$VERSION")"
old="$(python3 "$TOOLS" read-file "$FILE" "$PATTERN")"
if [[ "$old" == "$VERSION" ]]; then
echo "old=$old" >> "$GITHUB_OUTPUT"
echo "skipped=true" >> "$GITHUB_OUTPUT"
echo "$FILE already $VERSION; skipping bump"
exit 0
fi
python3 "$TOOLS" bump-file "$FILE" "$PATTERN" "$VERSION" >/dev/null
echo "old=$old" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"
echo "$FILE: $old -> $VERSION"
git diff --stat

- name: Run bump command
if: inputs.stage == 'propose' && inputs.bump_command != ''
if: inputs.stage == 'propose' && inputs.bump_command != '' && steps.bump.outputs.skipped != 'true'
shell: bash
env:
OLD_VERSION: ${{ steps.bump.outputs.old }}
Expand All @@ -292,7 +315,7 @@ runs:
git diff --stat

- name: Draft changelog
if: inputs.stage == 'propose' && inputs.changelog_file != ''
if: inputs.stage == 'propose' && inputs.changelog_file != '' && steps.bump.outputs.skipped != 'true'
id: changelog
shell: bash
env:
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/actions-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,38 @@ jobs:
base="$(cat "$RUNNER_TEMP/base")"
guard 1.1.0 "$base"
expect_output "base=$base"
expect_output "checkout=$base"
refuse 1.1.0 "origin/main~1" "already exists"
refuse 1.1.0 "" "already exists"
echo "the same commit is reused; a different one, and the trunk flow, are refused"

- name: A hotfix branch that already moved is reused at its tip
run: |
set -euo pipefail
source "$SH/helpers.sh"
base="$(cat "$RUNNER_TEMP/base")"
git -C "$REPO" checkout --quiet release-v1.1.0
git -C "$REPO" commit --quiet --allow-empty -m "fix: already on the release branch"
git -C "$REPO" push --quiet origin release-v1.1.0
tip="$(git -C "$REPO" rev-parse HEAD)"
git -C "$REPO" checkout --quiet main
guard 1.1.0 "$base"
expect_output "base=$base"
expect_output "checkout=$tip"
: > "$OUT"
( cd "$REPO" && env \
GH_TOKEN=fake \
GITHUB_REPOSITORY=megaeth-labs/fake \
GITHUB_OUTPUT="$OUT" \
BRANCH=release-v1.1.0 \
BASE="$base" \
CHECKOUT="$tip" \
bash "$SH/cut-branch.sh" )
expect_output "release_branch=release-v1.1.0"
[[ "$(git -C "$REPO" rev-parse HEAD)" == "$tip" ]] \
|| { echo "HEAD was reset to the base commit, dropping hotfix commits"; exit 1; }
echo "propose sits on $tip, not $base"

# The generated parts of every action README (inputs, outputs, steps,
# errors) and the catalogue come from action.yml; stale docs fail here.
docs:
Expand Down
Loading