Merge pull request #5724 from Hmbown/fix/ci-shared-base-read-guard-20… #1503
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto-close harvested PRs | |
| # When a commit on main contains a "Harvested from PR #N" line in its | |
| # message, close PR #N with a templated thank-you that links back to | |
| # the merged commit. Solves the long-standing problem where contributor | |
| # PRs whose code lands via maintainer cherry-pick stay open and | |
| # `CONFLICTING` forever, even though their fix is credited in the | |
| # CHANGELOG. | |
| # | |
| # The expected commit-message convention is documented in | |
| # CONTRIBUTING.md. Two patterns are recognised: | |
| # | |
| # * `Harvested from PR #1234 by @username` (preferred) | |
| # * `harvested from #1234` (case-insensitive fallback) | |
| # | |
| # The first match's PR number is closed; multiple PRs can be closed | |
| # per commit by repeating the line. The match runs on the commit | |
| # body only, not on the subject line, so the subject can describe | |
| # the change naturally without baking a number into it. | |
| on: | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| # Only one auto-close run at a time so two near-simultaneous main | |
| # pushes can't both try to close the same PR (the second would just | |
| # fail with "Pull request is already closed", harmless but noisy). | |
| concurrency: | |
| group: auto-close-harvested | |
| cancel-in-progress: false | |
| jobs: | |
| close: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # We need at least the commits that this push introduced. | |
| # fetch-depth: 0 is the simplest correct option; the | |
| # alternative (fetching just `before..after`) is fragile | |
| # when force-pushes happen. | |
| fetch-depth: 0 | |
| - name: Close PRs referenced by harvested-from lines | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| AFTER_SHA: ${{ github.event.after }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # The first push to a fresh branch has BEFORE_SHA = 0000…0000. | |
| # In that case fall back to the latest commit only — we don't | |
| # want to scan the entire history. | |
| if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" || -z "${BEFORE_SHA:-}" ]]; then | |
| RANGE="${AFTER_SHA}" | |
| RANGE_ARGS=("-1" "${AFTER_SHA}") | |
| else | |
| RANGE="${BEFORE_SHA}..${AFTER_SHA}" | |
| RANGE_ARGS=("${RANGE}") | |
| fi | |
| echo "Scanning commit range: ${RANGE}" | |
| # `git log --format=%H%n%B%n--END--` separates commits with a | |
| # sentinel so multi-line bodies don't get mangled. | |
| mapfile -t commits < <(git log "${RANGE_ARGS[@]}" --format="%H") | |
| if [[ ${#commits[@]} -eq 0 ]]; then | |
| echo "No commits in range; nothing to do." | |
| exit 0 | |
| fi | |
| declare -A processed_prs=() | |
| for sha in "${commits[@]}"; do | |
| body="$(git log -1 --format=%B "${sha}")" | |
| # Two patterns, both case-insensitive on the keyword: | |
| # "Harvested from PR #1234 by @username" (preferred form) | |
| # "harvested from #1234" (short form) | |
| mapfile -t pr_numbers < <( | |
| printf '%s\n' "${body}" \ | |
| | grep -oiE 'harvested from (pr )?#[0-9]+' \ | |
| | grep -oE '#[0-9]+' \ | |
| | tr -d '#' \ | |
| | sort -u || true | |
| ) | |
| if [[ ${#pr_numbers[@]} -eq 0 ]]; then | |
| continue | |
| fi | |
| short_sha="${sha:0:12}" | |
| subject="$(git log -1 --format=%s "${sha}")" | |
| for pr in "${pr_numbers[@]}"; do | |
| key="${pr}-${sha}" | |
| if [[ -n "${processed_prs[${key}]:-}" ]]; then | |
| continue | |
| fi | |
| processed_prs[${key}]=1 | |
| # Idempotency: skip if the PR is already closed. | |
| state="$(gh pr view "${pr}" --json state --jq .state 2>/dev/null || echo "MISSING")" | |
| if [[ "${state}" == "CLOSED" || "${state}" == "MERGED" ]]; then | |
| echo "PR #${pr} is already ${state}; skipping." | |
| continue | |
| fi | |
| if [[ "${state}" == "MISSING" ]]; then | |
| echo "::warning::PR #${pr} not found or inaccessible; skipping." | |
| continue | |
| fi | |
| author="$(gh pr view "${pr}" --json author --jq '.author.login' 2>/dev/null || echo "")" | |
| greeting="Hi" | |
| if [[ -n "${author}" ]]; then | |
| greeting="Thanks @${author}" | |
| fi | |
| # NOTE: this block intentionally avoids `<<EOF` heredocs. | |
| # YAML's `|` block scalar requires consistent indentation, | |
| # but heredoc bodies have to start at column 0 — those two | |
| # constraints can't coexist in the same file. We assemble | |
| # the body with `printf` + `\n` so every line of the | |
| # message lives at the same indent as the surrounding | |
| # shell code. | |
| commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${sha}" | |
| contributing_url="https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md" | |
| body_text="$(printf '%s\n' \ | |
| "${greeting} — your contribution landed in [\`${short_sha}\`](${commit_url}) on \`main\`:" \ | |
| "" \ | |
| "> ${subject}" \ | |
| "" \ | |
| "Closing this PR now that the code is on \`main\`. Credit lives in the commit message and (where applicable) the \`CHANGELOG.md\` entry for the next release. Apologies for not closing this at the time of the merge — the auto-close workflow is new in v0.8.31." \ | |
| "" \ | |
| "If you want to land more work and would prefer your future PRs merge cleanly without a harvest step, the [\`CONTRIBUTING.md\`](${contributing_url}) doc has a short note on what makes a contribution mergeable as-is." \ | |
| )" | |
| echo "Closing PR #${pr} (harvested in ${short_sha})" | |
| if ! gh pr close "${pr}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --comment "${body_text}"; then | |
| echo "::warning::Failed to close PR #${pr}; continuing" | |
| fi | |
| done | |
| done | |
| echo "Auto-close pass complete." |