返回 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 steps:
41 - uses: actions/checkout@v7
42 with:
43 # We need at least the commits that this push introduced.
44 # fetch-depth: 0 is the simplest correct option; the
45 # alternative (fetching just `before..after`) is fragile
46 # when force-pushes happen.
47 fetch-depth: 0
48
49 - name: Close PRs referenced by harvested-from lines
50 env:
51 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 BEFORE_SHA: ${{ github.event.before }}
53 AFTER_SHA: ${{ github.event.after }}
54 shell: bash
55 run: |
56 set -euo pipefail
57
58 # The first push to a fresh branch has BEFORE_SHA = 0000…0000.
59 # In that case fall back to the latest commit only — we don't
60 # want to scan the entire history.
61 if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" || -z "${BEFORE_SHA:-}" ]]; then
62 RANGE="${AFTER_SHA}"
63 RANGE_ARGS=("-1" "${AFTER_SHA}")
64 else
65 RANGE="${BEFORE_SHA}..${AFTER_SHA}"
66 RANGE_ARGS=("${RANGE}")
67 fi
68 echo "Scanning commit range: ${RANGE}"
69
70 # `git log --format=%H%n%B%n--END--` separates commits with a
71 # sentinel so multi-line bodies don't get mangled.
72 mapfile -t commits < <(git log "${RANGE_ARGS[@]}" --format="%H")
73
74 if [[ ${#commits[@]} -eq 0 ]]; then
75 echo "No commits in range; nothing to do."
76 exit 0
77 fi
78
79 declare -A processed_prs=()
80
81 for sha in "${commits[@]}"; do
82 body="$(git log -1 --format=%B "${sha}")"
83 # Two patterns, both case-insensitive on the keyword:
84 # "Harvested from PR #1234 by @username" (preferred form)
85 # "harvested from #1234" (short form)
86 mapfile -t pr_numbers < <(
87 printf '%s\n' "${body}" \
88 | grep -oiE 'harvested from (pr )?#[0-9]+' \
89 | grep -oE '#[0-9]+' \
90 | tr -d '#' \
91 | sort -u || true
92 )
93
94 if [[ ${#pr_numbers[@]} -eq 0 ]]; then
95 continue
96 fi
97
98 short_sha="${sha:0:12}"
99 subject="$(git log -1 --format=%s "${sha}")"
100
101 for pr in "${pr_numbers[@]}"; do
102 key="${pr}-${sha}"
103 if [[ -n "${processed_prs[${key}]:-}" ]]; then
104 continue
105 fi
106 processed_prs[${key}]=1
107
108 # Idempotency: skip if the PR is already closed.
109 state="$(gh pr view "${pr}" --json state --jq .state 2>/dev/null || echo "MISSING")"
110 if [[ "${state}" == "CLOSED" || "${state}" == "MERGED" ]]; then
111 echo "PR #${pr} is already ${state}; skipping."
112 continue
113 fi
114 if [[ "${state}" == "MISSING" ]]; then
115 echo "::warning::PR #${pr} not found or inaccessible; skipping."
116 continue
117 fi
118
119 author="$(gh pr view "${pr}" --json author --jq '.author.login' 2>/dev/null || echo "")"
120 greeting="Hi"
121 if [[ -n "${author}" ]]; then
122 greeting="Thanks @${author}"
123 fi
124
125 # NOTE: this block intentionally avoids `<<EOF` heredocs.
126 # YAML's `|` block scalar requires consistent indentation,
127 # but heredoc bodies have to start at column 0 — those two
128 # constraints can't coexist in the same file. We assemble
129 # the body with `printf` + `\n` so every line of the
130 # message lives at the same indent as the surrounding
131 # shell code.
132 commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${sha}"
133 contributing_url="https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md"
134 body_text="$(printf '%s\n' \
135 "${greeting} — your contribution landed in [\`${short_sha}\`](${commit_url}) on \`main\`:" \
136 "" \
137 "> ${subject}" \
138 "" \
139 "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." \
140 "" \
141 "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." \
142 )"
143
144 echo "Closing PR #${pr} (harvested in ${short_sha})"
145 if ! gh pr close "${pr}" \
146 --repo "${GITHUB_REPOSITORY}" \
147 --comment "${body_text}"; then
148 echo "::warning::Failed to close PR #${pr}; continuing"
149 fi
150 done
151 done
152
153 echo "Auto-close pass complete."
154
154 lines YAML