From 07703dbb7a05044de8d010c8b3b02016bc43d2f0 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:08:34 +0000 Subject: [PATCH 1/4] docs: add a recipe for PR comments on pull requests from forks --- .../examples/fork-PR-comments-lint.yml | 47 ++++++++++++++ .../examples/fork-PR-comments-post.yml | 63 +++++++++++++++++++ README.md | 3 +- docs/examples/index.md | 34 ++++++++++ docs/permissions.md | 19 ++++++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/examples/fork-PR-comments-lint.yml create mode 100644 .github/workflows/examples/fork-PR-comments-post.yml diff --git a/.github/workflows/examples/fork-PR-comments-lint.yml b/.github/workflows/examples/fork-PR-comments-lint.yml new file mode 100644 index 00000000..4db61139 --- /dev/null +++ b/.github/workflows/examples/fork-PR-comments-lint.yml @@ -0,0 +1,47 @@ +name: cpp-linter # (1)! +on: + pull_request: + branches: [main, master, develop] + paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake'] + +permissions: {} + +jobs: + cpp-linter: + runs-on: ubuntu-latest + permissions: # (2)! + contents: read + pull-requests: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # ... optionally setup build env to create a compilation database + + - uses: cpp-linter/cpp-linter-action@v2 + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: 'file' # Use .clang-format config file. (3) + tidy-checks: '' # Use .clang-tidy config file. (4) + # No thread-comments here: this job's token is read-only. (5) + summary-output-file: ${{ runner.temp }}/cpp-linter-comment/comment.md + + - name: Save the pull request number + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + mkdir -p "$RUNNER_TEMP/cpp-linter-comment" + echo "$PR_NUMBER" > "$RUNNER_TEMP/cpp-linter-comment/pr-number.txt" + + - uses: actions/upload-artifact@v7 + with: + name: cpp-linter-comment + path: ${{ runner.temp }}/cpp-linter-comment/ + retention-days: 1 + + - name: Fail fast?! + if: steps.linter.outputs.checks-failed > 0 + run: exit 1 diff --git a/.github/workflows/examples/fork-PR-comments-post.yml b/.github/workflows/examples/fork-PR-comments-post.yml new file mode 100644 index 00000000..54db5cb8 --- /dev/null +++ b/.github/workflows/examples/fork-PR-comments-post.yml @@ -0,0 +1,63 @@ +name: cpp-linter comment +on: + workflow_run: + workflows: [cpp-linter] # (1)! + types: [completed] + +permissions: {} + +jobs: + comment: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + permissions: + actions: read # (2)! + pull-requests: write + steps: + # No checkout: this job never runs code from the pull request. (3) + - uses: actions/download-artifact@v8 + id: download + continue-on-error: true # The lint run may have stopped before uploading. + with: + name: cpp-linter-comment + path: ${{ runner.temp }}/cpp-linter-comment + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Post or update the comment + if: steps.download.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + run: | + dir="$RUNNER_TEMP/cpp-linter-comment" + # The artifact was made while linting the pull request: use it only as data. + pr="$(tr -cd '0-9' < "$dir/pr-number.txt" | head -c 10)" + if [ -z "$pr" ] || [ ! -f "$dir/comment.md" ]; then + echo "::warning::The artifact has no pull request number or no comment" + exit 0 + fi + # Only comment on the pull request whose head the lint run checked. (4) + if [ "$(gh api "repos/$GH_REPO/pulls/$pr" --jq .head.sha)" != "$HEAD_SHA" ]; then + echo "::notice::Pull request #$pr has new commits since this run; not commenting" + exit 0 + fi + marker='' + body="$RUNNER_TEMP/body.md" + if [ "$(wc -c < "$dir/comment.md")" -gt 60000 ]; then + # A comment holds at most 65536 characters. + printf '%s\nToo many findings to show here. See the [workflow run](%s).\n' "$marker" "$RUN_URL" > "$body" + else + { echo "$marker"; cat "$dir/comment.md"; } > "$body" + fi + # Update the comment from an earlier run instead of adding another one. + id="$(gh api --paginate "repos/$GH_REPO/issues/$pr/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id" \ + | head -n 1)" + if [ -n "$id" ]; then + gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F "body=@$body" > /dev/null + else + gh api "repos/$GH_REPO/issues/$pr/comments" -F "body=@$body" > /dev/null + fi diff --git a/README.md b/README.md index 3b0fe29e..4d7b15ce 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,8 @@ jobs: - The action does not fail the job by itself; the last step does, using the `checks-failed` output. - Pull requests from forks get a read-only token: annotations still appear, but reviews are not - posted, and `thread-comments` would fail the step. Draft pull requests get no review. + posted, and `thread-comments` would fail the step. To comment on them anyway, see the + [PR comments from forks recipe][recipes-doc]. Draft pull requests get no review. ## Usage diff --git a/docs/examples/index.md b/docs/examples/index.md index e0679327..54561f2f 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -3,6 +3,7 @@ [tidy-checks]: ../inputs-outputs.md#tidy-checks [thread-comments]: ../inputs-outputs.md#thread-comments [auto-fix]: ../inputs-outputs.md#auto-fix +[summary-output-file]: ../inputs-outputs.md#summary-output-file # Recipes @@ -46,3 +47,36 @@ Here are some example workflows to get started quickly. 2. See also [`style`][style] 3. See also [`tidy-checks`][tidy-checks] 4. See also [`thread-comments`][thread-comments] + +=== "PR comments from forks" + + The token of a pull request from a fork is read-only, so the action cannot comment + there. Split the work into two workflows: the first lints with a read-only token and + uploads the comment as an artifact; the second runs on `workflow_run` with + `pull-requests: write` and posts it. See [pull requests from forks](../permissions.md#pull-requests-from-forks). + + `.github/workflows/cpp-linter.yml` + + ``` yaml + --8<-- ".github/workflows/examples/fork-PR-comments-lint.yml" + ``` + + 1. The second workflow finds this one by its `name`. + 2. Read-only, as for any pull request from a fork. + 3. See also [`style`][style] + 4. See also [`tidy-checks`][tidy-checks] + 5. See also [`thread-comments`][thread-comments] and [`summary-output-file`][summary-output-file] + + `.github/workflows/cpp-linter-comment.yml` + + ``` yaml + --8<-- ".github/workflows/examples/fork-PR-comments-post.yml" + ``` + + 1. Must match the `name` of the first workflow. GitHub runs this workflow from the + default branch only, so it takes effect after it is merged there. + 2. Needed to download the artifact of the first workflow's run. + 3. This workflow has a write token, so it must not check out or run anything from the + pull request. It only reads two files from the artifact. + 4. The artifact can be written by the pull request's code, so the pull request number in + it is checked against the commit that the first workflow linted. diff --git a/docs/permissions.md b/docs/permissions.md index 5d84fb7b..406f6797 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -77,6 +77,25 @@ The [`tidy-review`](inputs-outputs.md#tidy-review), [`format-review`](inputs-out pull-requests: write ``` +## Pull requests from forks + +GitHub gives a pull request from a fork a read-only `GITHUB_TOKEN`, whatever the +`permissions` of the workflow say. File annotations, the step summary and the outputs +still work. Reviews are not posted, and [`thread-comments`](inputs-outputs.md#thread-comments) +fails the step with `403 Resource not accessible by integration`. + +To comment on those pull requests too, split the work into two workflows: one lints with +the read-only token and uploads the comment as an artifact, the other runs on +`workflow_run` with `pull-requests: write` and posts it. See the +[PR comments from forks](examples/index.md) recipe. + +!!! warning "Do not lint pull requests in `pull_request_target`" + + `pull_request_target` runs with a write token and the repository's secrets. If that + workflow checks out the pull request's code and builds it, for example to create a + compilation database with CMake, code from the pull request runs with that token. + Lint in `pull_request`, and post the comment from `workflow_run`. + ## Auto-fix The [`auto-fix`](inputs-outputs.md#auto-fix) feature requires `contents: write` permission From d723a18ee8bcd0e07647ee3fd7b6a0347cb95df0 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:10:05 +0000 Subject: [PATCH 2/4] docs: say that reviews also need a write token on fork pull requests --- README.md | 4 ++-- docs/permissions.md | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d7b15ce..9f3b16e6 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,8 @@ jobs: both. `auto-fix` commits the clang-format fixes to the branch and needs `contents: write`. - The action does not fail the job by itself; the last step does, using the `checks-failed` output. -- Pull requests from forks get a read-only token: annotations still appear, but reviews are not - posted, and `thread-comments` would fail the step. To comment on them anyway, see the +- Pull requests from forks get a read-only token: annotations still appear, but `thread-comments` + and reviews need a write token and would fail the step. To comment on them anyway, see the [PR comments from forks recipe][recipes-doc]. Draft pull requests get no review. ## Usage diff --git a/docs/permissions.md b/docs/permissions.md index 406f6797..36a95fce 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -81,8 +81,9 @@ The [`tidy-review`](inputs-outputs.md#tidy-review), [`format-review`](inputs-out GitHub gives a pull request from a fork a read-only `GITHUB_TOKEN`, whatever the `permissions` of the workflow say. File annotations, the step summary and the outputs -still work. Reviews are not posted, and [`thread-comments`](inputs-outputs.md#thread-comments) -fails the step with `403 Resource not accessible by integration`. +still work. [`thread-comments`](inputs-outputs.md#thread-comments) and the pull request +reviews need `pull-requests: write`, so they fail the step with +`403 Resource not accessible by integration`. To comment on those pull requests too, split the work into two workflows: one lints with the read-only token and uploads the comment as an artifact, the other runs on From 2f5d11755b5c0c07776553a02345c28a0d1aa58b Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:21:39 +0000 Subject: [PATCH 3/4] docs: note that pull_request_target is blocked by default from December 2 --- docs/permissions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/permissions.md b/docs/permissions.md index 36a95fce..b3712ded 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -97,6 +97,12 @@ the read-only token and uploads the comment as an artifact, the other runs on compilation database with CMake, code from the pull request runs with that token. Lint in `pull_request`, and post the comment from `workflow_run`. + From December 2, 2026, GitHub + [blocks `pull_request_target` by default](https://github.blog/changelog/2026-11-02-pull_request_target-blocked-by-default-december-2/) + in public repositories: such workflows do not run unless an Actions policy allows the + event. A workflow that posts cpp-linter comments that way stops commenting on that date. + The recipe above does not use `pull_request_target`. + ## Auto-fix The [`auto-fix`](inputs-outputs.md#auto-fix) feature requires `contents: write` permission From 13eefc216323bc2878dd1c0fde10664e1d2dc4bd Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:39:26 +0000 Subject: [PATCH 4/4] docs: fork PR comments are skipped, not failed; harden the recipe In git-bot-feedback 0.5.1 a 403 on a fork pull request is logged and the step still passes. Also check the head repository, guard a missing PR number file, serialize posts per branch, keep the artifact 7 days, and note the private-repository write-token setting. --- .../workflows/examples/fork-PR-comments-lint.yml | 2 +- .../workflows/examples/fork-PR-comments-post.yml | 16 +++++++++++++--- README.md | 5 +++-- docs/examples/index.md | 5 +++-- docs/permissions.md | 7 ++++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/examples/fork-PR-comments-lint.yml b/.github/workflows/examples/fork-PR-comments-lint.yml index 4db61139..c4d70ca2 100644 --- a/.github/workflows/examples/fork-PR-comments-lint.yml +++ b/.github/workflows/examples/fork-PR-comments-lint.yml @@ -40,7 +40,7 @@ jobs: with: name: cpp-linter-comment path: ${{ runner.temp }}/cpp-linter-comment/ - retention-days: 1 + retention-days: 7 - name: Fail fast?! if: steps.linter.outputs.checks-failed > 0 diff --git a/.github/workflows/examples/fork-PR-comments-post.yml b/.github/workflows/examples/fork-PR-comments-post.yml index 54db5cb8..ce766981 100644 --- a/.github/workflows/examples/fork-PR-comments-post.yml +++ b/.github/workflows/examples/fork-PR-comments-post.yml @@ -6,6 +6,11 @@ on: permissions: {} +# Post one run at a time per branch, so two runs cannot both add a new comment. +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + jobs: comment: if: github.event.workflow_run.event == 'pull_request' @@ -30,17 +35,22 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} RUN_URL: ${{ github.event.workflow_run.html_url }} run: | dir="$RUNNER_TEMP/cpp-linter-comment" # The artifact was made while linting the pull request: use it only as data. - pr="$(tr -cd '0-9' < "$dir/pr-number.txt" | head -c 10)" - if [ -z "$pr" ] || [ ! -f "$dir/comment.md" ]; then + if [ ! -f "$dir/pr-number.txt" ] || [ ! -f "$dir/comment.md" ]; then echo "::warning::The artifact has no pull request number or no comment" exit 0 fi + pr="$(tr -cd '0-9' < "$dir/pr-number.txt" | head -c 10)" + if [ -z "$pr" ]; then + echo "::warning::The artifact has no pull request number" + exit 0 + fi # Only comment on the pull request whose head the lint run checked. (4) - if [ "$(gh api "repos/$GH_REPO/pulls/$pr" --jq .head.sha)" != "$HEAD_SHA" ]; then + if [ "$(gh api "repos/$GH_REPO/pulls/$pr" --jq '.head.sha + " " + .head.repo.full_name')" != "$HEAD_SHA $HEAD_REPO" ]; then echo "::notice::Pull request #$pr has new commits since this run; not commenting" exit 0 fi diff --git a/README.md b/README.md index 9f3b16e6..b665ae0c 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,9 @@ jobs: - The action does not fail the job by itself; the last step does, using the `checks-failed` output. - Pull requests from forks get a read-only token: annotations still appear, but `thread-comments` - and reviews need a write token and would fail the step. To comment on them anyway, see the - [PR comments from forks recipe][recipes-doc]. Draft pull requests get no review. + and reviews need a write token, so they are not posted (the log shows a 403 error). To comment + on them anyway, see the [PR comments from forks recipe][recipes-doc]. Draft pull requests get no + review. ## Usage diff --git a/docs/examples/index.md b/docs/examples/index.md index 54561f2f..735581d5 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -78,5 +78,6 @@ Here are some example workflows to get started quickly. 2. Needed to download the artifact of the first workflow's run. 3. This workflow has a write token, so it must not check out or run anything from the pull request. It only reads two files from the artifact. - 4. The artifact can be written by the pull request's code, so the pull request number in - it is checked against the commit that the first workflow linted. + 4. Anyone who opens a pull request can change the first workflow and so the artifact. + The pull request number in it is checked against the commit and the fork that the + first workflow linted. diff --git a/docs/permissions.md b/docs/permissions.md index b3712ded..8e8e9acc 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -80,9 +80,10 @@ The [`tidy-review`](inputs-outputs.md#tidy-review), [`format-review`](inputs-out ## Pull requests from forks GitHub gives a pull request from a fork a read-only `GITHUB_TOKEN`, whatever the -`permissions` of the workflow say. File annotations, the step summary and the outputs -still work. [`thread-comments`](inputs-outputs.md#thread-comments) and the pull request -reviews need `pull-requests: write`, so they fail the step with +`permissions` of the workflow say, unless a private or internal repository turns on +**Send write tokens to workflows from pull requests**. File annotations, the step summary +and the outputs still work. [`thread-comments`](inputs-outputs.md#thread-comments) and the +pull request reviews need `pull-requests: write`, so they are not posted, and the log shows `403 Resource not accessible by integration`. To comment on those pull requests too, split the work into two workflows: one lints with