返回 CodeWhale
auto-close-harvested.yml
根目录 / .github / workflows / auto-close-harvested.yml
1 name: Auto-close harvested PRs
2
3 # When a commit on main contains a "Harvested from PR #N" line in its
4 # message, close PR #N with a templated thank-you that links back to
5 # the merged commit. Solves the long-standing problem where contributor
6 # PRs whose code lands via maintainer cherry-pick stay open and
7 # `CONFLICTING` forever, even though their fix is credited in the
8 # CHANGELOG.
9 #
10 # The expected commit-message convention is documented in
11 # CONTRIBUTING.md. Two patterns are recognised:
12 #
13 # * `Harvested from PR #1234 by @username` (preferred)
14 # * `harvested from #1234` (case-insensitive fallback)
15 #
16 # The first match's PR number is closed; multiple PRs can be closed
17 # per commit by repeating the line. The match runs on the commit
18 # body only, not on the subject line, so the subject can describe
19 # the change naturally without baking a number into it.
20
21 on:
22 push:
23 branches: [main]
24
25 permissions:
26 contents: read
27 pull-requests: write
28 issues: write
29
30 # Only one auto-close run at a time so two near-simultaneous main
31 # pushes can't both try to close the same PR (the second would just
32 # fail with "Pull request is already closed", harmless but noisy).
33 concurrency:
34 group: auto-close-harvested
35 cancel-in-progress: false
36
37 jobs:
38 close:
39 runs-on: ubuntu-latest
40 timeout-minutes: 10
41 steps:
42 - uses: actions/checkout@v7
43 with:
44 # We need at least the commits that this push introduced.
45 # fetch-depth: 0 is the simplest correct option; the
46 # alternative (fetching just `before..after`) is fragile
47 # when force-pushes happen.
48 fetch-depth: 0
49
50 - name: Close PRs referenced by harvested-from lines
51 env:
52 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 BEFORE_SHA: ${{ github.event.before }}
54 AFTER_SHA: ${{ github.event.after }}
55 shell: bash
56 run: |
57 set -euo pipefail
58
59 # The first push to a fresh branch has BEFORE_SHA = 0000…0000.
60 # In that case fall back to the latest commit only — we don't
61 # want to scan the entire history.
62 if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" || -z "${BEFORE_SHA:-}" ]]; then
63 RANGE="${AFTER_SHA}"
64 RANGE_ARGS=("-1" "${AFTER_SHA}")
65 else
66 RANGE="${BEFORE_SHA}..${AFTER_SHA}"
67 RANGE_ARGS=("${RANGE}")
68 fi
69 echo "Scanning commit range: ${RANGE}"
70
71 # `git log --format=%H%n%B%n--END--` separates commits with a
72 # sentinel so multi-line bodies don't get mangled.
73 mapfile -t commits < <(git log "${RANGE_ARGS[@]}" --format="%H")
74
75 if [[ ${#commits[@]} -eq 0 ]]; then
76 echo "No commits in range; nothing to do."
77 exit 0
78 fi
79
80 declare -A processed_prs=()
81
82 for sha in "${commits[@]}"; do
83 body="$(git log -1 --format=%B "${sha}")"
84 # Two patterns, both case-insensitive on the keyword:
85 # "Harvested from PR #1234 by @username" (preferred form)
86 # "harvested from #1234" (short form)
87 mapfile -t pr_numbers < <(
88 printf '%s\n' "${body}" \
89 | grep -oiE 'harvested from (pr )?#[0-9]+' \
90 | grep -oE '#[0-9]+' \
91 | tr -d '#' \
92 | sort -u || true
93 )
94
95 if [[ ${#pr_numbers[@]} -eq 0 ]]; then
96 continue
97 fi
98
99 short_sha="${sha:0:12}"
100 subject="$(git log -1 --format=%s "${sha}")"
101
102 for pr in "${pr_numbers[@]}"; do
103 key="${pr}-${sha}"
104 if [[ -n "${processed_prs[${key}]:-}" ]]; then
105 continue
106 fi
107 processed_prs[${key}]=1
108
109 # Idempotency: skip if the PR is already closed.
110 state="$(gh pr view "${pr}" --json state --jq .state 2>/dev/null || echo "MISSING")"
111 if [[ "${state}" == "CLOSED" || "${state}" == "MERGED" ]]; then
112 echo "PR #${pr} is already ${state}; skipping."
113 continue
114 fi
115 if [[ "${state}" == "MISSING" ]]; then
116 echo "::warning::PR #${pr} not found or inaccessible; skipping."
117 continue
118 fi
119
120 author="$(gh pr view "${pr}" --json author --jq '.author.login' 2>/dev/null || echo "")"
121 greeting="Hi"
122 if [[ -n "${author}" ]]; then
123 greeting="Thanks @${author}"
124 fi
125
126 # NOTE: this block intentionally avoids `<<EOF` heredocs.
127 # YAML's `|` block scalar requires consistent indentation,
128 # but heredoc bodies have to start at column 0 — those two
129 # constraints can't coexist in the same file. We assemble
130 # the body with `printf` + `\n` so every line of the
131 # message lives at the same indent as the surrounding
132 # shell code.
133 commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${sha}"
134 contributing_url="https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md"
135 body_text="$(printf '%s\n' \
136 "${greeting} — your contribution landed in [\`${short_sha}\`](${commit_url}) on \`main\`:" \
137 "" \
138 "> ${subject}" \
139 "" \
140 "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." \
141 "" \
142 "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." \
143 )"
144
145 echo "Closing PR #${pr} (harvested in ${short_sha})"
146 if ! gh pr close "${pr}" \
147 --repo "${GITHUB_REPOSITORY}" \
148 --comment "${body_text}"; then
149 echo "::warning::Failed to close PR #${pr}; continuing"
150 fi
151 done
152 done
153
154 echo "Auto-close pass complete."
155
155 lines YAML