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
110 changes: 77 additions & 33 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,21 @@ concurrency:
cancel-in-progress: false

jobs:
# Reversible half: keep the Release PR current. Never gated.
# Reversible half: keep the Release PR current. Stands down only while a release is
# already pending, because until that one is tagged there is no release commit to stop
# the walk at and it would propose the same commits again in a second Release PR.
release-pr:
name: Release PR
needs: detect
Comment thread
mogita marked this conversation as resolved.
# No status function of its own would mean an implicit success(), so one transient
# gh api error inside detect would stop the reversible half too. Its own event and
# ref guard still has to be repeated here, because detect is skipped on the
# publish_tag path and this job must skip with it.
if: >-
!cancelled() &&
(github.event_name == 'push' || inputs.publish_tag == '') &&
(github.ref_name == 'main' || endsWith(github.ref_name, '.x'))
(github.ref_name == 'main' || endsWith(github.ref_name, '.x')) &&
needs.detect.outputs.pending != 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand All @@ -42,8 +51,9 @@ jobs:

# Tagging and the GitHub Release are irreversible, so the suite has to run before
# them, which means knowing a release is pending before the suite starts. The tag
# lands on the merged Release PR's merge commit, not on this branch's tip, so that
# commit is also what gets tested.
# lands on the merged Release PR's merge commit while the suite runs on this
# workflow's own commit, so `ready` also requires those to be the same commit. They
# are, on the path that matters: the push of that merge.
detect:
name: Detect pending release
if: >-
Expand All @@ -54,30 +64,81 @@ jobs:
permissions:
contents: read
pull-requests: read
Comment thread
mogita marked this conversation as resolved.
issues: read
outputs:
pending: ${{ steps.find.outputs.pending }}
sha: ${{ steps.find.outputs.sha }}
ready: ${{ steps.find.outputs.ready }}
steps:
- name: Find a merged Release PR waiting to be tagged
id: find
env:
GH_TOKEN: ${{ github.token }}
BASE: ${{ github.ref_name }}
HEAD_SHA: ${{ github.sha }}
run: |
sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls?state=closed&base=${GITHUB_REF_NAME}&sort=updated&direction=desc&per_page=50" \
--jq '[.[] | select(.merged_at != null and ([.labels[].name] | index("autorelease: pending")))] | .[0].merge_commit_sha // empty')"
pending=false
ready=false

# Query the label directly, and page: release-please applies the label when it
# opens the Release PR, not when it merges, so every Release PR closed without
# merging keeps it forever and holds a slot in this listing. One page would
# eventually stop containing the genuinely pending release, which reads as
# "nothing to release" and passes. merged_at comes back in the listing, so
# filtering on it here keeps the per-PR lookups below to real candidates.
nums="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues" \
-X GET -f state=closed -f labels='autorelease: pending' -f per_page=100 \
--jq '.[] | select(.pull_request.merged_at != null) | .number')"

# The base branch is not in that listing, so each candidate still needs a
# lookup: a hotfix branch can hold its own pending release, and taking the
# newest label match would drop this branch's release until the other clears.
num=""
sha=""
for n in $nums; do
# Under `bash -e` an unguarded assignment from a non-2xx would abort the
# step, which would fail detect and skip release-pr with it.
if ! sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${n}" \
--jq 'select(.base.ref == env.BASE) | .merge_commit_sha // empty')"; then
echo "::warning::Could not read PR #${n}; skipping it."
sha=""
continue
fi
if [ -n "$sha" ]; then
num="$n"
break
fi
done

if [ -z "$sha" ]; then
echo "No pending release."
echo "pending=false" >> "$GITHUB_OUTPUT"
exit 0
echo "No pending release on ${BASE}."
elif [ "$sha" != "$HEAD_SHA" ]; then
Comment thread
mogita marked this conversation as resolved.
# Reached when an earlier release run failed after the Release PR merged.
# Tagging $sha here would tag a tree this run never tested, and failing
# would redden every later push, so stand down and say why.
pending=true
echo "::warning::Release PR #${num} is still pending at ${sha}, which is not this run's commit ${HEAD_SHA}. Re-run the workflow run for ${sha} to finish that release."
Comment thread
mogita marked this conversation as resolved.
{
echo "### Release stuck"
echo
echo "Release PR #${num} merged at \`${sha}\` and was never tagged, so no Release PR will be opened or refreshed until it clears."
echo
echo "Re-run the \`Release\` run for \`${sha}\`. If that commit is genuinely broken, remove the \`autorelease: pending\` label from #${num} by hand and release forward."
} >> "$GITHUB_STEP_SUMMARY"
else
pending=true
ready=true
echo "Pending release #${num} will be tagged at ${sha}."
fi
echo "Pending release will be tagged at $sha."
echo "pending=true" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"

{
echo "pending=${pending}"
echo "ready=${ready}"
} >> "$GITHUB_OUTPUT"

test-unit:
name: Test (unit)
needs: detect
if: needs.detect.outputs.pending == 'true'
if: needs.detect.outputs.ready == 'true'
Comment thread
mogita marked this conversation as resolved.
uses: ./.github/workflows/run_tests.yml
with:
marker: 'not integration'
Expand All @@ -86,7 +147,7 @@ jobs:
test-integration:
name: Test (integration)
needs: detect
if: needs.detect.outputs.pending == 'true'
if: needs.detect.outputs.ready == 'true'
uses: ./.github/workflows/run_tests.yml
with:
marker: 'integration'
Expand All @@ -96,7 +157,7 @@ jobs:
release:
name: 馃殌 Tag and release
needs: [detect, test-unit, test-integration]
if: needs.detect.outputs.pending == 'true'
if: needs.detect.outputs.ready == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand All @@ -107,23 +168,6 @@ jobs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
# The suite ran against this workflow's own commit, while the tag lands on the
# merged Release PR's merge commit. They are the same commit on the path that
# gets here, a push of that merge. They diverge on a dispatch after the branch
# has moved, which would tag a tree nothing tested, so refuse instead. Recovery
# is "Re-run failed jobs" on the run for the merge itself.
- name: Refuse to tag a commit the suite did not run on
env:
TESTED: ${{ github.sha }}
PENDING: ${{ needs.detect.outputs.sha }}
run: |
if [ "$TESTED" != "$PENDING" ]; then
echo "::error::The pending release is tagged at $PENDING but this run tested $TESTED."
echo "::error::Re-run the workflow run for $PENDING instead of dispatching from the branch tip."
exit 1
fi
echo "Tagging $PENDING, which is the commit the suite ran on."

- uses: googleapis/release-please-action@v4
id: release
with:
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,15 @@ Releases are driven by [release-please](https://github.com/googleapis/release-pl
that merge commit, which is the commit the tag will point at. Only if that is green
does the workflow create the tag and the GitHub Release and publish to PyPI via
Trusted Publishing (OIDC). The order matters: a tag and a GitHub Release cannot be
withdrawn, a failed publish can be retried. If a later dispatch would tag a commit
this run did not test, it fails rather than tagging it.
withdrawn, a failed publish can be retried. If the commit waiting to be tagged is not
the one this run tested, the workflow stands down instead of tagging it, and says so in
the run summary.

While a merged Release PR is waiting to be tagged, no new Release PR is opened or
refreshed, so that release-please has a release commit to stop its walk at. If the suite
failed on that merge commit, use "Re-run failed jobs" on its `Release` run. If the commit
is genuinely broken, remove the `autorelease: pending` label from the merged Release PR
by hand, then release forward; nothing clears that state automatically.

To retry a publish that failed after the release was tagged, use "Re-run failed jobs" on
that workflow run. Once GitHub has retired the run, dispatch `Release` from `main` with
Expand Down
Loading