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@v4 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 `< ${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."