-
-
Notifications
You must be signed in to change notification settings - Fork 11.2k
183 lines (162 loc) · 7.43 KB
/
Copy pathincomplete-prs.yml
File metadata and controls
183 lines (162 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
name: Close incomplete pull requests
on:
# `pull_request_target` has a write token, so this workflow must only ever run trusted
# base-branch code and must never checkout or execute pull request head code.
pull_request_target:
types:
- opened
- edited
- reopened
permissions: {}
defaults:
run:
shell: bash -euo pipefail {0}
concurrency:
group: "incomplete-pr-${{ github.event.pull_request.number }}"
cancel-in-progress: true
jobs:
manage:
# Restrict this write-token workflow to Homebrew/brew. The first step also
# fails if a repository checkout has occurred.
if: >-
github.repository == 'Homebrew/brew' &&
github.event.pull_request.user.login != 'BrewTestBot' &&
github.event.pull_request.user.login != 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
# Read the trusted base-branch pull request template through the API; write only
# the issue comment and pull request state needed here.
contents: read
issues: write
pull-requests: write
env:
GH_TOKEN: ${{ github.token }}
INCOMPLETE_TEMPLATE_COMMENT_MARKER: "<!-- incomplete-pr-template -->"
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TEMPLATE_URL: ${{ github.server_url }}/${{ github.repository }}/blob/main/.github/PULL_REQUEST_TEMPLATE.md
steps:
- name: Verify no checkout
run: |
if git -C "${GITHUB_WORKSPACE:?}" rev-parse --is-inside-work-tree &>/dev/null
then
echo "Refusing to run after a repository checkout in ${GITHUB_WORKSPACE}." >&2
exit 1
fi
- name: Write pull request body
# Do not add a checkout here. `pull_request_target` has a write token, so this
# step must only use inline trusted code and API responses from `main`.
env:
# Bind PR-controlled strings as environment variables instead of interpolating
# them into shell code.
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# This workflow intentionally uses `pull_request_target` so it can close and
# reopen forked pull requests. Keep this self-contained and never execute
# pull request code from this step.
mkdir -p "${RUNNER_TEMP:?}/incomplete-prs"
printf "%s" "${PR_BODY}" >"${RUNNER_TEMP}/incomplete-prs/pr-body"
- name: Fetch pull request template
run: |
gh api "repos/${GITHUB_REPOSITORY:?}/contents/.github/PULL_REQUEST_TEMPLATE.md?ref=main" \
--jq ".content" |
base64 --decode >"${RUNNER_TEMP:?}/incomplete-prs/template"
- name: Check pull request template
id: template
run: |
complete_template="$(
ruby - \
"${RUNNER_TEMP}/incomplete-prs/pr-body" \
"${RUNNER_TEMP}/incomplete-prs/template" <<'RUBY'
CHECKBOX_MARKER = /\A- \[[ xX]\] /
CHECKED_CHECKBOX_MARKER = /\A- \[[xX]\] /
NORMALISED_CHECKBOX_MARKER = '- [ ] '
REQUIRED_TEMPLATE_PERCENTAGE = 75
PERCENTAGE_SCALE = 100
def lines(path)
File.read(path, mode: 'rb')
.encode('UTF-8', invalid: :replace, undef: :replace)
.lines(chomp: true)
end
def normalise_lines(path)
lines(path).each_with_object([]) do |line, normalised_lines|
line = line.strip.sub(CHECKBOX_MARKER, NORMALISED_CHECKBOX_MARKER)
normalised_lines << line unless line.empty?
end.uniq
end
pr_body_path = ARGV.fetch(0)
template_path = ARGV.fetch(1)
pr_lines = normalise_lines(pr_body_path)
template_lines = normalise_lines(template_path)
matching_template_lines = template_lines.count { |line| pr_lines.include?(line) }
scaled_matching_percentage = matching_template_lines * PERCENTAGE_SCALE
scaled_required_percentage = template_lines.count * REQUIRED_TEMPLATE_PERCENTAGE
preserves_template = scaled_matching_percentage >= scaled_required_percentage
has_checked_checkbox = lines(pr_body_path).any? { |line| line.match?(CHECKED_CHECKBOX_MARKER) }
has_non_template_content = (pr_lines - template_lines).any?
puts preserves_template && (has_checked_checkbox || has_non_template_content)
RUBY
)"
case "${complete_template}" in
true | false) ;;
*)
echo "Unexpected template completion result: ${complete_template}" >&2
exit 1
;;
esac
echo "complete_template=${complete_template}" >>"${GITHUB_OUTPUT:?}"
- name: Find incomplete template comment
id: comments
if: >-
(github.event.pull_request.state == 'closed' &&
steps.template.outputs.complete_template == 'true') ||
(github.event.pull_request.state != 'closed' &&
steps.template.outputs.complete_template == 'false')
run: |
comment_ids="$(
gh api --paginate "repos/${GITHUB_REPOSITORY:?}/issues/${PR_NUMBER:?}/comments" \
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"${INCOMPLETE_TEMPLATE_COMMENT_MARKER:?}\"))) | .id"
)"
if [[ -n "${comment_ids}" ]]
then
echo "has_incomplete_template_comment=true" >>"${GITHUB_OUTPUT:?}"
else
echo "has_incomplete_template_comment=false" >>"${GITHUB_OUTPUT:?}"
fi
- name: Find pull request closer
id: closer
if: >-
github.event.pull_request.state == 'closed' &&
steps.template.outputs.complete_template == 'true'
run: |
closed_by="$(gh api "repos/${GITHUB_REPOSITORY:?}/issues/${PR_NUMBER:?}" --jq ".closed_by.login // \"\"")"
echo "closed_by=${closed_by}" >>"${GITHUB_OUTPUT:?}"
- name: Reopen completed pull request
if: >-
github.event.pull_request.state == 'closed' &&
steps.template.outputs.complete_template == 'true' &&
steps.closer.outputs.closed_by == 'github-actions[bot]' &&
steps.comments.outputs.has_incomplete_template_comment == 'true'
run: |
gh api --method PATCH "repos/${GITHUB_REPOSITORY:?}/pulls/${PR_NUMBER:?}" \
-f state=open
- name: Comment on incomplete pull request
if: >-
github.event.pull_request.state != 'closed' &&
steps.template.outputs.complete_template == 'false' &&
steps.comments.outputs.has_incomplete_template_comment != 'true'
run: |
gh api --method POST "repos/${GITHUB_REPOSITORY:?}/issues/${PR_NUMBER:?}/comments" \
--raw-field body="$(
cat <<COMMENT
${INCOMPLETE_TEMPLATE_COMMENT_MARKER}
Thanks for your pull request. This has been closed because it appears to use an incomplete or outdated pull request template.
Please edit the pull request body to fill in the current [pull request template](${PR_TEMPLATE_URL:?}). This workflow will reopen the pull request automatically once the template is complete.
COMMENT
)"
- name: Close incomplete pull request
if: >-
github.event.pull_request.state != 'closed' &&
steps.template.outputs.complete_template == 'false'
run: |
gh api --method PATCH "repos/${GITHUB_REPOSITORY:?}/pulls/${PR_NUMBER:?}" \
-f state=closed