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
81 changes: 56 additions & 25 deletions .github/scripts/bot-pull-request.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# bot-pull-request.sh spec-sync <head branch> [passed|failed]
# bot-pull-request.sh release
# bot-pull-request.sh catch-up [<head branch>]
# bot-pull-request.sh report <result of the CI passed job>
#
# spec-sync without passed|failed reads the conclusion of the latest finished Spec sync run on
# main. catch-up starts every workflow that runs release-please (.github/workflows/*.yml using
Expand All @@ -15,9 +16,11 @@
#
# A pull request opened with GITHUB_TOKEN gets its pull_request CI run only after a maintainer
# approves it (this repository is public). workflow_dispatch is exempt, so the script starts
# ci.yml on the pull request's branch: its checks land on the head commit, which is what the
# required "CI passed" check on main reads. It then turns on auto-merge (squash), so GitHub
# merges only once the required checks pass.
# ci.yml on the pull request's branch. GitHub leaves the check runs of a manual run off the pull
# request, so ci.yml's report job (mode report, in that run) sets a commit status "CI passed" on
# the head commit, which the pull request shows and main's required check reads, and then runs
# catch-up for the merge. The script turns on auto-merge (squash), so GitHub merges only once
# the required check passes.
#
# A pull request waits for a person (label needs-alex, one comment, auto-merge off) when:
# - a spec sync failed a check (usually a missing openapi/overlay.json entry), is a draft, or
Expand All @@ -29,12 +32,12 @@
# request labelled do-not-merge, one a person labelled needs-alex, and one whose auto-merge a
# person turned off.
#
# Needs GH_TOKEN (the workflow token with actions, contents, issues and pull-requests write),
# GITHUB_REPOSITORY, gh and jq.
# Needs GH_TOKEN (the workflow token with actions, contents, issues and pull-requests write;
# statuses write for report), GITHUB_REPOSITORY, gh and jq.
set -euo pipefail

usage() {
echo "usage: bot-pull-request.sh spec-sync <head branch> [passed|failed] | release | catch-up [<head branch>]" >&2
echo "usage: bot-pull-request.sh spec-sync <head branch> [passed|failed] | release | catch-up [<head branch>] | report <result>" >&2
exit 2
}

Expand All @@ -45,6 +48,31 @@ hold_label="needs-alex"
person_hold_label="do-not-merge"
bot_email="41898282+github-actions[bot]@users.noreply.github.com"

# Starts the release workflows on main for a newest commit that has no run of them, after
# waiting (at most three minutes) for the auto-merge of the open pull request from <branch>.
catch_up() {
local branch="$1" waiting sha
if [ -n "$branch" ]; then
for _ in $(seq 1 18); do
waiting="$(gh pr list --repo "$repo" --base "$base" --head "$branch" --state open \
--json autoMergeRequest --jq '[.[] | select(.autoMergeRequest != null)] | length')"
[ "$waiting" -eq 0 ] && break
sleep 10
done
fi
sha="$(gh api "repos/${repo}/commits/${base}" --jq '.sha')"
grep -l 'googleapis/release-please-action' .github/workflows/*.yml | while read -r file; do
workflow="$(basename "$file")"
runs="$(gh api "repos/${repo}/actions/workflows/${workflow}/runs?head_sha=${sha}&per_page=1" --jq '.total_count')"
if [ "$runs" -eq 0 ]; then
gh workflow run "$workflow" --repo "$repo" --ref "$base"
echo "Started ${workflow} on ${base} (${sha:0:10}): no run of it had started for that commit."
else
echo "${workflow} already ran on ${base} (${sha:0:10})."
fi
done
}

case "$kind" in
spec-sync)
branch="${2:-}"
Expand All @@ -68,26 +96,29 @@ case "$kind" in
--json number,headRefName --jq '.[] | select(.headRefName | startswith("release-please--")) | .number')"
;;
catch-up)
branch="${2:-}"
if [ -n "$branch" ]; then
for _ in $(seq 1 18); do
waiting="$(gh pr list --repo "$repo" --base "$base" --head "$branch" --state open \
--json autoMergeRequest --jq '[.[] | select(.autoMergeRequest != null)] | length')"
[ "$waiting" -eq 0 ] && break
sleep 10
done
catch_up "${2:-}"
exit 0
;;
report)
result="${2:-}"
sha="${GITHUB_SHA:?GITHUB_SHA is not set}"
ref="${GITHUB_REF_NAME:?GITHUB_REF_NAME is not set}"
case "$result" in
success | failure) ;;
*)
echo "The CI passed job ended with '${result}'; no status reported."
exit 0
;;
esac
gh api "repos/${repo}/statuses/${sha}" -f state="$result" -f context="CI passed" \
-f description="ci.yml run ${GITHUB_RUN_ID:-} (workflow_dispatch): ${result}" \
-f target_url="${GITHUB_SERVER_URL:-https://github.com}/${repo}/actions/runs/${GITHUB_RUN_ID:-}" > /dev/null
echo "Set the commit status CI passed=${result} on ${sha:0:10} (${ref})."
if [ "$result" = success ]; then
case "$ref" in
chore/spec-sync | release-please--*) catch_up "$ref" ;;
esac
fi
sha="$(gh api "repos/${repo}/commits/${base}" --jq '.sha')"
grep -l 'googleapis/release-please-action' .github/workflows/*.yml | while read -r file; do
workflow="$(basename "$file")"
runs="$(gh api "repos/${repo}/actions/workflows/${workflow}/runs?head_sha=${sha}&per_page=1" --jq '.total_count')"
if [ "$runs" -eq 0 ]; then
gh workflow run "$workflow" --repo "$repo" --ref "$base"
echo "Started ${workflow} on ${base} (${sha:0:10}): no run of it had started for that commit."
else
echo "${workflow} already ran on ${base} (${sha:0:10})."
fi
done
exit 0
;;
*) usage ;;
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,31 @@ jobs:
fi
done
echo "Every CI job passed."

# A pull request of the automation gets its CI from a manual run (bot-pull-requests.yml), and
# GitHub leaves a manual run's check runs off the pull request. This job sets the result as
# the commit status "CI passed" on the commit, which the pull request shows and main's
# required check reads, then starts the release workflows once auto-merge has merged it.
report:
name: Report CI passed as a commit status
if: always() && github.event_name == 'workflow_dispatch'
needs: ci-passed
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: write # start the release workflows after the merge
contents: read
pull-requests: read
statuses: write # the CI passed commit status
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out the scripts and workflows
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github
- name: Set the commit status
env:
RESULT: ${{ needs.ci-passed.result }}
run: bash .github/scripts/bot-pull-request.sh report "$RESULT"
2 changes: 1 addition & 1 deletion docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Ruby: `net/http` from the standard library, `jwt` only for Authentication.
- Release workflows publish with trusted publishing where the registry supports it (PyPI, npm with provenance, RubyGems and NuGet), Maven Central through the Central Portal with signed artifacts (token and signing key in the `maven-central` environment), Packagist through its GitHub hook, and Go module tags announced to proxy.golang.org.
- 1.0.0 shipped on 2026-10-29 with Alex's approval, and the repositories are public.
- Every release after that publishes straight from the repository, with no person in the loop for API changes (Alex, 2026-11-07). `.github/workflows/bot-pull-requests.yml` starts CI on the spec sync and release-please pull requests and turns on auto-merge with squash, so each merges once the required `CI passed` check passes. Merging the release pull request creates the vX.Y.Z tag and publishes, and the release workflow then waits until the registry serves the version, or fails.
- A pull request opened with `GITHUB_TOKEN` gets its `pull_request` CI run only after a maintainer approves it (the repositories are public), so that workflow starts `ci.yml` with `workflow_dispatch`, whose checks land on the same head commit. A merge made by that auto-merge starts no push workflow, so it also starts the release workflows on `main` when the newest commit has no run of them.
- A pull request opened with `GITHUB_TOKEN` gets its `pull_request` CI run only after a maintainer approves it (the repositories are public), so that workflow starts `ci.yml` with `workflow_dispatch`. GitHub leaves the check runs of a manual run off the pull request, so that run's `report` job sets its result as the commit status `CI passed` on the head commit, which the pull request shows and the required check reads. A merge made by that auto-merge starts no push workflow, so the `report` job, and `bot-pull-requests.yml` every 30 minutes, start the release workflows on `main` when the newest commit has no run of them.
- A person decides, and the pull request gets the `needs-alex` label instead of auto-merge, when a spec sync failed a check, when a pull request asks for a major version (a release pull request for a new major version, or a sync commit with `!` or a `BREAKING CHANGE:` footer), or when it carries commits from people. The `do-not-merge` label, or turning auto-merge off by hand, keeps any of these pull requests from merging.
- A release created with `GITHUB_TOKEN` does not trigger other workflows, so a repository that publishes from a separate `release.yml` has release-please start it with `workflow_dispatch` for the new tag. No personal access token is stored.

Expand Down
Loading