commit-suggest.yaml #538
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |