Skip to content

commit-suggest.yaml #532

commit-suggest.yaml

commit-suggest.yaml #532

# 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 '[![Apply patch](https://img.shields.io/badge/Apply%%20patch-%%2Fapply--patch-2ea44f?style=for-the-badge&logo=github)]'
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