-
Notifications
You must be signed in to change notification settings - Fork 9
181 lines (166 loc) · 7.51 KB
/
Copy pathcommit-suggest.yaml
File metadata and controls
181 lines (166 loc) · 7.51 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
# Posts the formatting patch produced by the `rcc` workflow
# as a comment on the pull request it came from.
#
# SECURITY -- `workflow_run` is a privileged trigger.
# It runs from the default branch of the BASE repository
# with a token that can write to it,
# and it fires for `rcc` runs of pull requests from forks.
# Everything reachable from `github.event.workflow_run` is therefore
# attacker-controlled data, not trusted input:
#
# * `head_branch` is a fork branch name, and `git check-ref-format`
# permits `"`, `` ` ``, `;` and `$(...)` in branch names.
# * `head_commit.message`, repository descriptions and similar fields
# are free text and may contain quotes.
# * The `changes-patch` artifact was produced by a run
# that executed the fork's code, so its contents are arbitrary.
#
# Consequently no field of the event is ever interpolated with `${{ }}`
# into a shell script; values are passed through the environment
# so the shell treats them as inert data.
# The pull request head is deliberately NOT checked out:
# this job only needs the artifact, and not checking out
# avoids placing a credentialed `.git/config`
# next to attacker-controlled files.
#
# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
name: commit-suggest.yaml
on:
workflow_run:
workflows: ["rcc"]
types:
- completed
# Deny everything by default; the job opts back into the minimum it needs.
# This matters more here than in most workflows: `workflow_run` runs from the
# default branch with a token that can write to this repository, on runs that
# belong to a pull request from a fork.
permissions: {}
jobs:
commit-suggest:
runs-on: ubuntu-26.04
if: github.event.workflow_run.event == 'pull_request'
permissions:
# Required by actions/download-artifact to read another run's artifacts.
# The workflow did not previously request this, so the download could
# only ever have failed -- silently, under `continue-on-error: true`.
actions: read
# `contents: read` is deliberately absent: the pull request checkout is
# gone, and nothing else in this job reads the repository.
# Required to post the suggestion comment
pull-requests: write
steps:
- name: Download artifact
uses: actions/download-artifact@v6
with:
name: changes-patch
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
continue-on-error: true
- name: Check if artifact exists
id: check-artifact
run: |
if [ -f changes.patch ]; then
echo "has_diff=true" >> $GITHUB_OUTPUT
else
echo "has_diff=false" >> $GITHUB_OUTPUT
echo "No changes-patch artifact found"
fi
shell: bash
- name: Find PR number for branch from correct head repository
id: find-pr
if: steps.check-artifact.outputs.has_diff == 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }}
run: |
set -euo pipefail
# `--arg` keeps the owner login out of the jq program text,
# and `"${HEAD_BRANCH}"` keeps the branch name out of the shell's
# parsing -- see the security note at the top of this file.
pr_number=$(
gh pr list \
--repo "${GITHUB_REPOSITORY}" \
--head "${HEAD_BRANCH}" \
--state open \
--json number,headRepositoryOwner |
jq -r --arg owner "${HEAD_OWNER}" \
'[.[] | select(.headRepositoryOwner.login == $owner) | .number][0] // empty'
) || pr_number=""
# Belt and braces: only ever emit a plain integer downstream.
if ! printf '%s' "${pr_number}" | grep -qE '^[0-9]+$'; then
echo "No matching open pull request found"
pr_number=""
fi
echo "pr_number=${pr_number}" >> "${GITHUB_OUTPUT}"
shell: bash
- name: Generate comment body
if: steps.check-artifact.outputs.has_diff == 'true' && steps.find-pr.outputs.pr_number != ''
env:
RUN_ID: ${{ github.event.workflow_run.id }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
run: |
set -euo pipefail
# A GitHub comment is capped at 65536 characters, and the patch is
# attacker-controlled, so cap what we embed and say so when we do.
max_bytes=40000
truncated=false
if [ "$(wc -c < changes.patch)" -gt "${max_bytes}" ]; then
head -c "${max_bytes}" changes.patch > patch.txt
truncated=true
else
cp changes.patch patch.txt
fi
# Pick a fence longer than the longest run of backticks in the patch.
# Otherwise a crafted patch could close the code block early and
# inject arbitrary Markdown into a comment authored by github-actions.
longest=$(
{ grep -o '`\+' patch.txt || true; } |
awk '{ if (length($0) > n) n = length($0) } END { print n + 0 }'
)
if [ "${longest}" -lt 3 ]; then
fence_len=3
else
fence_len=$((longest + 1))
fi
fence=$(printf '`%.0s' $(seq 1 "${fence_len}"))
{
printf '## Formatting suggestions available\n\n'
printf 'A patch file with formatting suggestions has been generated. '
printf 'You can apply it using one of these methods:\n\n'
printf '### Method 1: Apply via gh CLI\n\n'
printf '%s\n' '```bash'
printf '# Download and apply the patch directly\n'
printf 'gh run download %s --repo %s --name changes-patch && patch -p1 < changes.patch && rm changes.patch\n' \
"${RUN_ID}" "${REPO}"
printf '%s\n\n' '```'
printf 'Repo owners can also apply the patch automatically. '
printf 'Click the button to jump to the comment box, then post:\n\n'
printf '%s\n' '```'
printf '/apply-patch\n'
printf '%s\n\n' '```'
printf '[]'
printf '(https://github.com/%s/pull/%s#new_comment_field)\n\n' "${REPO}" "${PR_NUMBER}"
printf '### Method 2: View the patch\n\n'
printf '<details>\n'
printf '<summary>Click to see the patch contents</summary>\n\n'
printf '%sdiff\n' "${fence}"
cat patch.txt
printf '\n%s\n\n' "${fence}"
if [ "${truncated}" = "true" ]; then
printf '_Patch truncated at %s bytes; download the artifact for the full diff._\n\n' "${max_bytes}"
fi
printf '</details>\n\n'
printf -- '---\n'
printf '*This comment was automatically generated by the commit-suggester workflow.*\n'
} > comment.md
shell: bash
- name: Post or update comment
if: steps.check-artifact.outputs.has_diff == 'true' && steps.find-pr.outputs.pr_number != ''
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
with:
pr-number: ${{ steps.find-pr.outputs.pr_number }}
file-path: comment.md
comment-tag: formatting-suggestions
mode: recreate