Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/examples/fork-PR-comments-lint.yml
Original file line number Diff line number Diff line change
@@ -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/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
retention-days: 7

- name: Fail fast?!
if: steps.linter.outputs.checks-failed > 0
run: exit 1
73 changes: 73 additions & 0 deletions .github/workflows/examples/fork-PR-comments-post.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: cpp-linter comment
on:
workflow_run:
workflows: [cpp-linter] # (1)!
types: [completed]

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'
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 }}
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.
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.repo.full_name')" != "$HEAD_SHA $HEAD_REPO" ]; then
echo "::notice::Pull request #$pr has new commits since this run; not commenting"
exit 0
fi
marker='<!-- cpp-linter-action workflow_run comment -->'
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ 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. Draft pull requests get no review.
- Pull requests from forks get a read-only token: annotations still appear, but `thread-comments`
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

Expand Down
35 changes: 35 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -46,3 +47,37 @@ 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. 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.
27 changes: 27 additions & 0 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ 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, 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
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`.

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
Expand Down
Loading