From e2e0e86967302cc26227ee4404f95fc4813d3cee Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 8 Jun 2026 19:29:36 +0300 Subject: [PATCH 01/23] feat: add auto-fix option to automatically format and commit clang-format fixes Adds two new inputs: - auto-fix: runs cpp-linter --fix and auto-commits changes - fix-commit-msg: custom commit message (default: 'style: apply styling format fix') The auto-fix workflow: 1. Runs cpp-linter with --fix (applies clang-format -i) 2. If there are changed files, commits and pushes as github-actions[bot] 3. Push failures degrade to a warning Closes #439 --- .github/workflows/examples/auto-fix.yml | 27 +++++++++++++++ README.md | 31 +++++++++++++++++ action.yml | 46 +++++++++++++++++++++++++ docs/action.yml | 5 +++ docs/examples/index.md | 11 ++++++ docs/permissions.md | 12 +++++++ 6 files changed, 132 insertions(+) create mode 100644 .github/workflows/examples/auto-fix.yml diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml new file mode 100644 index 00000000..6d0df6fa --- /dev/null +++ b/.github/workflows/examples/auto-fix.yml @@ -0,0 +1,27 @@ +name: cpp-linter (auto-fix) +on: + pull_request: + branches: [main, master, develop] + paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake'] + +jobs: + cpp-linter: + runs-on: ubuntu-latest + permissions: + contents: write # needed for auto-fix commits + pull-requests: write + steps: + - uses: actions/checkout@v5 + + - uses: cpp-linter/cpp-linter-action@v2 + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: 'file' # Use .clang-format config file + tidy-checks: '-*' # disable clang-tidy + auto-fix: 'true' # auto-apply clang-format fixes + + - name: Fail fast?! + if: steps.linter.outputs.clang-format-checks-failed > 0 + run: exit 1 diff --git a/README.md b/README.md index a3a4a318..0d48b798 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,37 @@ For all explanations of our available input parameters and output variables, see See also our [example recipes][recipes-doc]. +### Auto-fix clang-format issues + +You can enable automatic fixing of clang-format issues by setting `auto-fix: 'true'`. +When enabled, the action will: + +1. Run clang-format detection as usual +2. Apply `clang-format -i` to fix any files with style issues +3. Commit and push the formatted changes back to the PR branch + +```yaml + steps: + - uses: actions/checkout@v5 + - uses: cpp-linter/cpp-linter-action@v2 + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: 'file' + auto-fix: 'true' # automatically fix format issues +``` + +> [!TIP] +> To prevent the auto-fix commit from triggering another CI run, include a +> `[skip ci]` (or `[ci skip]`, `[no ci]`, etc.) tag in your custom commit message: +> +> ```yaml +> with: +> auto-fix: 'true' +> fix-commit-msg: 'style: apply styling format fix [skip ci]' +> ``` + ## Used By

diff --git a/action.yml b/action.yml index 1076a995..3298c81d 100644 --- a/action.yml +++ b/action.yml @@ -209,6 +209,24 @@ inputs: Set this option to `true` to prevent Pull Request reviews from approving or requesting changes. default: 'false' required: false + auto-fix: + description: | + Set this option to `true` to automatically apply clang-format fixes + and commit them back to the PR branch. + + When enabled, cpp-linter runs with ``--fix``, which applies + ``clang-format -i`` on files with style issues. After that, + a new commit is pushed to the PR branch with the formatted changes. + + This option has no effect on clang-tidy issues. + default: 'false' + required: false + fix-commit-msg: + description: | + Custom commit message for the auto-fix commit. + Only used when ``auto-fix`` is ``true``. + default: 'style: apply styling format fix' + required: false jobs: description: | The number of jobs to run in parallel. @@ -457,6 +475,9 @@ runs: '--jobs=${{ inputs.jobs }}' '--summary-output-file=${{ inputs.summary-output-file }}' ] + if '${{ inputs.auto-fix }}' == 'true' { + $args = ($args | append ['--fix']) + } mut uv_args = [run --no-sync --project $action_path --directory (pwd)] let gh_action_debug = $env | get --optional 'ACTIONS_STEP_DEBUG' @@ -482,3 +503,28 @@ runs: print $"\n(ansi purple)Running cpp-linter(ansi reset)" ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args cpp-linter ...$args + + - name: Auto-commit clang-format fixes + if: inputs.auto-fix == 'true' || inputs.auto-fix == true + shell: nu {0} + run: | + let has_changes = (^git diff --exit-code) | complete | $in.exit_code != 0 + if $has_changes { + ^git config user.name 'github-actions[bot]' + ^git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + ^git add -A + let commit_msg = if ('${{ inputs.fix-commit-msg }}' | is-empty) { + 'style: apply styling format fix' + } else { + '${{ inputs.fix-commit-msg }}' + } + ^git commit -m $"($commit_msg)" + let push_result = (^git push) | complete + if $push_result.exit_code != 0 { + print $"(ansi yellow)::warning title=Auto-fix push failed::($push_result.stderr)(ansi reset)" + } else { + print $"(ansi green)Auto-fix commit pushed successfully(ansi reset)" + } + } else { + print $"(ansi green)No formatting changes to commit(ansi reset)" + } diff --git a/docs/action.yml b/docs/action.yml index 84b047d8..aea6820f 100644 --- a/docs/action.yml +++ b/docs/action.yml @@ -47,6 +47,11 @@ inputs: passive-reviews: minimum-version: '2.12.0' required-permission: 'pull-requests: write #pull-request-reviews' + auto-fix: + minimum-version: '2.19.0' + required-permission: 'contents: write #auto-fix' + fix-commit-msg: + minimum-version: '2.19.0' jobs: minimum-version: '2.11.0' cache-enable: diff --git a/docs/examples/index.md b/docs/examples/index.md index a36646e0..e0679327 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -2,11 +2,22 @@ [style]: ../inputs-outputs.md#style [tidy-checks]: ../inputs-outputs.md#tidy-checks [thread-comments]: ../inputs-outputs.md#thread-comments +[auto-fix]: ../inputs-outputs.md#auto-fix # Recipes Here are some example workflows to get started quickly. +=== "auto-fix clang-format" + + ``` yaml + --8<-- ".github/workflows/examples/auto-fix.yml" + ``` + + 1. See also [`auto-fix`][auto-fix] + 2. See also [`style`][style] + 3. See also [`tidy-checks`][tidy-checks] + === "only clang-tidy" ``` yaml diff --git a/docs/permissions.md b/docs/permissions.md index 3495c392..bdcf4a74 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -78,3 +78,15 @@ The [`tidy-review`](inputs-outputs.md#tidy-review), [`format-review`](inputs-out permissions: pull-requests: write ``` + +## Auto-fix + +The [`auto-fix`](inputs-outputs.md#auto-fix) feature requires the following permission +in addition to any other permissions needed for other features: + +```yaml + permissions: + contents: write # (1)! +``` + +1. Needed to commit and push the formatted changes back to the PR branch. From 9ecd4def2a346f56f10a4d53a17425bbf15c402f Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 8 Jun 2026 21:07:11 +0300 Subject: [PATCH 02/23] refactor: rename fix-commit-msg to auto-fix-commit-msg for consistency --- README.md | 2 +- action.yml | 6 +++--- docs/action.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0d48b798..5e2da890 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ When enabled, the action will: > ```yaml > with: > auto-fix: 'true' -> fix-commit-msg: 'style: apply styling format fix [skip ci]' +> auto-fix-commit-msg: 'style: apply styling format fix [skip ci]' > ``` ## Used By diff --git a/action.yml b/action.yml index 3298c81d..de9b44f1 100644 --- a/action.yml +++ b/action.yml @@ -221,7 +221,7 @@ inputs: This option has no effect on clang-tidy issues. default: 'false' required: false - fix-commit-msg: + auto-fix-commit-msg: description: | Custom commit message for the auto-fix commit. Only used when ``auto-fix`` is ``true``. @@ -513,10 +513,10 @@ runs: ^git config user.name 'github-actions[bot]' ^git config user.email '41898282+github-actions[bot]@users.noreply.github.com' ^git add -A - let commit_msg = if ('${{ inputs.fix-commit-msg }}' | is-empty) { + let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply styling format fix' } else { - '${{ inputs.fix-commit-msg }}' + '${{ inputs.auto-fix-commit-msg }}' } ^git commit -m $"($commit_msg)" let push_result = (^git push) | complete diff --git a/docs/action.yml b/docs/action.yml index aea6820f..4f161fa4 100644 --- a/docs/action.yml +++ b/docs/action.yml @@ -50,7 +50,7 @@ inputs: auto-fix: minimum-version: '2.19.0' required-permission: 'contents: write #auto-fix' - fix-commit-msg: + auto-fix-commit-msg: minimum-version: '2.19.0' jobs: minimum-version: '2.11.0' From b5aa8fc0eb35d0707798b6e862219591a163f71c Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 22 Jul 2026 19:42:03 +0300 Subject: [PATCH 03/23] fix: address review feedback for auto-fix implementation - Use GITHUB_ACTOR/GITHUB_ACTOR_ID for git commit author instead of hardcoded github-actions[bot] - Add git update-index -q --refresh before checking for changes (prevents stat-dirty false positives) - Use git diff-index instead of git diff for more reliable change detection - Add PR branch checkout step before cpp-linter runs (fixes detached HEAD push problem) - Use explicit push refspec HEAD:refs/heads/ for reliable push targeting - Fix ANSI escape syntax in warning output Based on analysis of wearerequired/lint-action's proven approach. --- action.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index de9b44f1..a3fd3ce6 100644 --- a/action.yml +++ b/action.yml @@ -443,6 +443,18 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args ...$cmd } + - name: Checkout PR branch for auto-fix push capability + if: (inputs.auto-fix == 'true' || inputs.auto-fix == true) && github.event_name == 'pull_request' + shell: nu {0} + run: | + let head_ref = $env.GITHUB_HEAD_REF + if ($head_ref | is-not-empty) { + print $"(ansi purple)Fetching PR branch \"($head_ref)\" for auto-fix(ansi reset)" + ^git fetch origin --depth=1 $"($head_ref):refs/remotes/origin/($head_ref)" + ^git checkout --force -B $head_ref $"refs/remotes/origin/($head_ref)" + print $"(ansi green)Switched to PR branch \"($head_ref)\"(ansi reset)" + } + - name: Run cpp-linter id: cpp-linter shell: nu {0} @@ -508,10 +520,14 @@ runs: if: inputs.auto-fix == 'true' || inputs.auto-fix == true shell: nu {0} run: | - let has_changes = (^git diff --exit-code) | complete | $in.exit_code != 0 + # Refresh index first so stat-only differences don't create false positives + ^git update-index -q --refresh + let has_changes = (^git diff-index --name-status --exit-code HEAD -- | complete | $in.exit_code == 1) if $has_changes { - ^git config user.name 'github-actions[bot]' - ^git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + let actor_name = $env.GITHUB_ACTOR + let actor_id = $env.GITHUB_ACTOR_ID + ^git config user.name $"($actor_name)" + ^git config user.email $"($actor_id)+($actor_name)@users.noreply.github.com" ^git add -A let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply styling format fix' @@ -519,9 +535,10 @@ runs: '${{ inputs.auto-fix-commit-msg }}' } ^git commit -m $"($commit_msg)" - let push_result = (^git push) | complete + let branch = $env.GITHUB_HEAD_REF | default $env.GITHUB_REF_NAME + let push_result = (^git push origin $"HEAD:refs/heads/($branch)") | complete if $push_result.exit_code != 0 { - print $"(ansi yellow)::warning title=Auto-fix push failed::($push_result.stderr)(ansi reset)" + print $"::warning title=Auto-fix push failed::(ansi yellow)($push_result.stderr)(ansi reset)" } else { print $"(ansi green)Auto-fix commit pushed successfully(ansi reset)" } From 90ef5773c39f16acf883b48442ae58eea5f678be Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 22 Jul 2026 21:07:58 +0300 Subject: [PATCH 04/23] fix: change args from let to mut for reassignment compatibility Fixes Nu parser error: Error: nu::parser::assignment_requires_mutable_variable '' needs to be a mutable variable to support append ['--fix'] Use 'mut args' instead of 'let args' --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index a3fd3ce6..b4f1d963 100644 --- a/action.yml +++ b/action.yml @@ -463,7 +463,7 @@ runs: $env.UV_INSTALL_DIR = $action_path | path join 'bin' $env.UV_CACHE_DIR = $env.RUNNER_TEMP | path join 'cpp-linter-action-cache' - let args = [ + mut args = [ '--style=${{ inputs.style }}' '--extensions=${{ inputs.extensions }}' '--tidy-checks=${{ inputs.tidy-checks }}' From 355cbfab5990002e9d6be8b5b16ef7ffe791efc0 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 22 Jul 2026 23:28:05 +0300 Subject: [PATCH 05/23] refactor: use git -c for commit author instead of global git config Apply 2bndy5's review suggestion: use single git -c user.name=X -c user.email=Y commit instead of separate git config + git commit. This avoids polluting the global git configuration in the CI environment. --- action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index b4f1d963..a9fce08f 100644 --- a/action.yml +++ b/action.yml @@ -526,15 +526,15 @@ runs: if $has_changes { let actor_name = $env.GITHUB_ACTOR let actor_id = $env.GITHUB_ACTOR_ID - ^git config user.name $"($actor_name)" - ^git config user.email $"($actor_id)+($actor_name)@users.noreply.github.com" + let git_user = $"user.name=($actor_name)" + let git_email = $"user.email=($actor_id)+($actor_name)@users.noreply.github.com" ^git add -A let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply styling format fix' } else { '${{ inputs.auto-fix-commit-msg }}' } - ^git commit -m $"($commit_msg)" + ^git -c $git_user -c $git_email commit -m $"($commit_msg)" let branch = $env.GITHUB_HEAD_REF | default $env.GITHUB_REF_NAME let push_result = (^git push origin $"HEAD:refs/heads/($branch)") | complete if $push_result.exit_code != 0 { From f84c0ed3aed11b489080cbc08f2d1bfd288d4643 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 22 Jul 2026 23:34:44 +0300 Subject: [PATCH 06/23] docs: document CI re-triggering limitation and fork PR incompatibility - docs/permissions.md: Add warning about GITHUB_TOKEN not triggering CI on auto-fix commits, with PAT workaround. Add warning about third-party fork PRs where auto-fix cannot work. - action.yml: Add intelligent fork-detection in push failure warning (checks for 403/refused/permission errors and shows context-aware message referencing docs). - .github/workflows/examples/auto-fix.yml: Add comment about PAT option for CI re-triggering. --- .github/workflows/examples/auto-fix.yml | 3 +++ action.yml | 7 ++++++- docs/permissions.md | 27 ++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index 6d0df6fa..63a992d3 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -12,6 +12,9 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 + # For auto-fix commits to trigger new CI runs, use a PAT instead: + # with: + # token: ${{ secrets.MY_PAT }} - uses: cpp-linter/cpp-linter-action@v2 id: linter diff --git a/action.yml b/action.yml index a9fce08f..0fc26512 100644 --- a/action.yml +++ b/action.yml @@ -538,7 +538,12 @@ runs: let branch = $env.GITHUB_HEAD_REF | default $env.GITHUB_REF_NAME let push_result = (^git push origin $"HEAD:refs/heads/($branch)") | complete if $push_result.exit_code != 0 { - print $"::warning title=Auto-fix push failed::(ansi yellow)($push_result.stderr)(ansi reset)" + let stderr_lower = ($push_result.stderr | str downcase) + if ($stderr_lower | str contains "403") or ($stderr_lower | str contains "refused") or ($stderr_lower | str contains "not have permission") { + print $"::warning title=Auto-fix push failed::This action does not have permission to push to this branch. When using auto-fix on pull_request events from a third-party fork, the GITHUB_TOKEN cannot write to the fork repository. See docs/permissions.md for details.(ansi reset)" + } else { + print $"::warning title=Auto-fix push failed::(ansi yellow)($push_result.stderr)(ansi reset)" + } } else { print $"(ansi green)Auto-fix commit pushed successfully(ansi reset)" } diff --git a/docs/permissions.md b/docs/permissions.md index bdcf4a74..b5a86be8 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -81,7 +81,7 @@ The [`tidy-review`](inputs-outputs.md#tidy-review), [`format-review`](inputs-out ## Auto-fix -The [`auto-fix`](inputs-outputs.md#auto-fix) feature requires the following permission +The [`auto-fix`](inputs-outputs.md#auto-fix) feature requires `contents: write` permission in addition to any other permissions needed for other features: ```yaml @@ -90,3 +90,28 @@ in addition to any other permissions needed for other features: ``` 1. Needed to commit and push the formatted changes back to the PR branch. + +!!! warning "CI re-triggering with auto-fix" + + The default `GITHUB_TOKEN` **cannot** trigger new CI runs when pushing + a commit. If you need the auto-fix commit to trigger CI checks + (e.g. to verify the fix builds clean), use a personal access token + (PAT) with `contents: write` scope: + + ```yaml + - uses: actions/checkout@v5 + with: + token: ${{ secrets.MY_PAT }} + ``` + + When using the default `GITHUB_TOKEN`, you can include `[skip ci]` in + the auto-fix commit message to avoid unnecessary CI runs on the + fix commit itself. See the [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg) input. + +!!! warning "Pull requests from third-party forks" + + Auto-fix does not work on pull requests from third-party forks. The + `GITHUB_TOKEN` lacks write permission to the fork repository, and + `git push` to the fork's branch is not possible. Consider + restricting `auto-fix` to `push` events or pull requests from the + same repository. From 883fdfc2a2e91ad5102a86cf4488b34180b78935 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 22 Jul 2026 23:37:38 +0300 Subject: [PATCH 07/23] feat: add auto-fix-git-user and auto-fix-git-email inputs Add configurable git identity for auto-fix commits to address 2bndy5's concern about commit author reflecting the token owner. - auto-fix-git-user: custom git username for the auto-fix commit (defaults to GITHUB_ACTOR) - auto-fix-git-email: custom git email for the auto-fix commit (defaults to GITHUB_ACTOR_ID+GITHUB_ACTOR@users.noreply.github.com) - When empty, falls back to GITHUB_ACTOR/GITHUB_ACTOR_ID based values --- action.yml | 28 ++++++++++++++++++++++++++-- docs/action.yml | 4 ++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 0fc26512..931b7b3f 100644 --- a/action.yml +++ b/action.yml @@ -227,6 +227,20 @@ inputs: Only used when ``auto-fix`` is ``true``. default: 'style: apply styling format fix' required: false + auto-fix-git-user: + description: |- + Git username for the auto-fix commit. + Defaults to ``$GITHUB_ACTOR`` (the user who triggered the workflow). + Only used when ``auto-fix`` is ``true``. + default: '' + required: false + auto-fix-git-email: + description: |- + Git email for the auto-fix commit. + Defaults to ``$GITHUB_ACTOR_ID+$GITHUB_ACTOR@users.noreply.github.com``. + Only used when ``auto-fix`` is ``true``. + default: '' + required: false jobs: description: | The number of jobs to run in parallel. @@ -526,8 +540,18 @@ runs: if $has_changes { let actor_name = $env.GITHUB_ACTOR let actor_id = $env.GITHUB_ACTOR_ID - let git_user = $"user.name=($actor_name)" - let git_email = $"user.email=($actor_id)+($actor_name)@users.noreply.github.com" + let git_user_name = if ('${{ inputs.auto-fix-git-user }}' | is-empty) { + $actor_name + } else { + '${{ inputs.auto-fix-git-user }}' + } + let git_user_email = if ('${{ inputs.auto-fix-git-email }}' | is-empty) { + $"($actor_id)+($actor_name)@users.noreply.github.com" + } else { + '${{ inputs.auto-fix-git-email }}' + } + let git_user = $"user.name=($git_user_name)" + let git_email = $"user.email=($git_user_email)" ^git add -A let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply styling format fix' diff --git a/docs/action.yml b/docs/action.yml index 4f161fa4..7badc478 100644 --- a/docs/action.yml +++ b/docs/action.yml @@ -52,6 +52,10 @@ inputs: required-permission: 'contents: write #auto-fix' auto-fix-commit-msg: minimum-version: '2.19.0' + auto-fix-git-user: + minimum-version: '2.19.0' + auto-fix-git-email: + minimum-version: '2.19.0' jobs: minimum-version: '2.11.0' cache-enable: From 43b49052ec33ee18dd73fdaa277807db2c883787 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Wed, 22 Jul 2026 23:44:41 +0300 Subject: [PATCH 08/23] chore: Apply suggestions from code review Co-authored-by: Xianpeng Shen --- .github/workflows/examples/auto-fix.yml | 2 +- README.md | 2 +- docs/permissions.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index 63a992d3..9aeda61b 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -11,7 +11,7 @@ jobs: contents: write # needed for auto-fix commits pull-requests: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 # For auto-fix commits to trigger new CI runs, use a PAT instead: # with: # token: ${{ secrets.MY_PAT }} diff --git a/README.md b/README.md index 5e2da890..827a90af 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ When enabled, the action will: ```yaml steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: cpp-linter/cpp-linter-action@v2 id: linter env: diff --git a/docs/permissions.md b/docs/permissions.md index b5a86be8..d6e6a752 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -99,7 +99,7 @@ in addition to any other permissions needed for other features: (PAT) with `contents: write` scope: ```yaml - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: token: ${{ secrets.MY_PAT }} ``` From 88d5c80a5f709e7d59d7a522eb177917ac96af29 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 24 Jul 2026 05:24:34 +0300 Subject: [PATCH 09/23] chore: let auto-fix-git-user/email defaults reference env vars directly --- action.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/action.yml b/action.yml index 931b7b3f..ac1c61b4 100644 --- a/action.yml +++ b/action.yml @@ -230,14 +230,14 @@ inputs: auto-fix-git-user: description: |- Git username for the auto-fix commit. - Defaults to ``$GITHUB_ACTOR`` (the user who triggered the workflow). + Defaults to the value of ``$GITHUB_ACTOR``. Only used when ``auto-fix`` is ``true``. default: '' required: false auto-fix-git-email: description: |- Git email for the auto-fix commit. - Defaults to ``$GITHUB_ACTOR_ID+$GITHUB_ACTOR@users.noreply.github.com``. + Defaults to the value of ``$GITHUB_ACTOR_ID+$GITHUB_ACTOR@users.noreply.github.com``. Only used when ``auto-fix`` is ``true``. default: '' required: false @@ -538,15 +538,13 @@ runs: ^git update-index -q --refresh let has_changes = (^git diff-index --name-status --exit-code HEAD -- | complete | $in.exit_code == 1) if $has_changes { - let actor_name = $env.GITHUB_ACTOR - let actor_id = $env.GITHUB_ACTOR_ID let git_user_name = if ('${{ inputs.auto-fix-git-user }}' | is-empty) { - $actor_name + $env.GITHUB_ACTOR } else { '${{ inputs.auto-fix-git-user }}' } let git_user_email = if ('${{ inputs.auto-fix-git-email }}' | is-empty) { - $"($actor_id)+($actor_name)@users.noreply.github.com" + $"($env.GITHUB_ACTOR_ID)+($env.GITHUB_ACTOR)@users.noreply.github.com" } else { '${{ inputs.auto-fix-git-email }}' } From fbfbf5596f90eee66d98bfca149f298fd08c760b Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 24 Jul 2026 15:26:32 +0300 Subject: [PATCH 10/23] fix(auto-fix): gate to same-repo PRs, avoid stray commits Address review feedback on the auto-fix flow: - Gate the PR-branch checkout and the auto-commit/push steps to same-repository pull requests. Fork PRs previously hard-failed the checkout (the fork's branch isn't on origin) and could not push; now they are skipped with a warning. - Guard against tag refs: compute the destination branch from the PR head ref or a pushed branch ref, and skip otherwise so we never push HEAD to refs/heads/ and create a stray branch. - Stage only tracked modifications with `git add -u` instead of `git add -A`, so unrelated untracked/generated files are not swept into the auto-fix commit. - docs/permissions.md: clarify the write permission is for the actions/checkout token, and correct the `[skip ci]` guidance (it only matters for PAT/App-token pushes, since the default GITHUB_TOKEN push does not trigger CI anyway). - Example workflow: downgrade `pull-requests: write` to `read` (the example uses no review/thread-comment feature; `read` is still needed for files-changed-only on pull_request events). --- .github/workflows/examples/auto-fix.yml | 2 +- action.yml | 42 ++++++++++++++++++++++--- docs/permissions.md | 23 ++++++++------ 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index 9aeda61b..72a39d59 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write # needed for auto-fix commits - pull-requests: write + pull-requests: read # needed to list changed files on pull_request events steps: - uses: actions/checkout@v7 # For auto-fix commits to trigger new CI runs, use a PAT instead: diff --git a/action.yml b/action.yml index ac1c61b4..a0a07887 100644 --- a/action.yml +++ b/action.yml @@ -458,7 +458,10 @@ runs: } - name: Checkout PR branch for auto-fix push capability - if: (inputs.auto-fix == 'true' || inputs.auto-fix == true) && github.event_name == 'pull_request' + if: >- + (inputs.auto-fix == 'true' || inputs.auto-fix == true) + && github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository shell: nu {0} run: | let head_ref = $env.GITHUB_HEAD_REF @@ -469,6 +472,15 @@ runs: print $"(ansi green)Switched to PR branch \"($head_ref)\"(ansi reset)" } + - name: Warn that auto-fix is skipped on forked pull requests + if: >- + (inputs.auto-fix == 'true' || inputs.auto-fix == true) + && github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name != github.repository + shell: nu {0} + run: | + print "::warning title=Auto-fix skipped::auto-fix cannot push to a third-party fork's branch, so no formatting commit was made. Apply clang-format fixes from within the fork or run cpp-linter locally." + - name: Run cpp-linter id: cpp-linter shell: nu {0} @@ -531,9 +543,28 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args cpp-linter ...$args - name: Auto-commit clang-format fixes - if: inputs.auto-fix == 'true' || inputs.auto-fix == true + if: >- + (inputs.auto-fix == 'true' || inputs.auto-fix == true) + && (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) shell: nu {0} run: | + # Determine the destination branch up-front: the PR head ref, or the + # pushed branch. Bail out on tag refs (or anything that is not a branch) + # so we never push HEAD to refs/heads/ and create a stray branch. + let head_ref = $env.GITHUB_HEAD_REF + let branch = if ($head_ref | is-not-empty) { + $head_ref + } else if ($env.GITHUB_REF | str starts-with "refs/heads/") { + $env.GITHUB_REF_NAME + } else { + "" + } + if ($branch | is-empty) { + print $"::notice title=Auto-fix skipped::($env.GITHUB_REF) is not a branch or pull request ref; skipping auto-fix commit." + exit 0 + } + # Refresh index first so stat-only differences don't create false positives ^git update-index -q --refresh let has_changes = (^git diff-index --name-status --exit-code HEAD -- | complete | $in.exit_code == 1) @@ -550,19 +581,20 @@ runs: } let git_user = $"user.name=($git_user_name)" let git_email = $"user.email=($git_user_email)" - ^git add -A + # Stage only tracked modifications (clang-format edits existing sources). + # Avoid `git add -A`, which would also sweep in unrelated untracked files. + ^git add -u let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply styling format fix' } else { '${{ inputs.auto-fix-commit-msg }}' } ^git -c $git_user -c $git_email commit -m $"($commit_msg)" - let branch = $env.GITHUB_HEAD_REF | default $env.GITHUB_REF_NAME let push_result = (^git push origin $"HEAD:refs/heads/($branch)") | complete if $push_result.exit_code != 0 { let stderr_lower = ($push_result.stderr | str downcase) if ($stderr_lower | str contains "403") or ($stderr_lower | str contains "refused") or ($stderr_lower | str contains "not have permission") { - print $"::warning title=Auto-fix push failed::This action does not have permission to push to this branch. When using auto-fix on pull_request events from a third-party fork, the GITHUB_TOKEN cannot write to the fork repository. See docs/permissions.md for details.(ansi reset)" + print $"::warning title=Auto-fix push failed::This action does not have permission to push to this branch. Ensure the token used by actions/checkout has `contents: write` (branch protection rules may also block the push). See docs/permissions.md for details." } else { print $"::warning title=Auto-fix push failed::(ansi yellow)($push_result.stderr)(ansi reset)" } diff --git a/docs/permissions.md b/docs/permissions.md index d6e6a752..edd1de96 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -89,14 +89,15 @@ in addition to any other permissions needed for other features: contents: write # (1)! ``` -1. Needed to commit and push the formatted changes back to the PR branch. +1. Needed by the token used in `actions/checkout` to commit and push the + formatted changes back to the branch. !!! warning "CI re-triggering with auto-fix" The default `GITHUB_TOKEN` **cannot** trigger new CI runs when pushing a commit. If you need the auto-fix commit to trigger CI checks (e.g. to verify the fix builds clean), use a personal access token - (PAT) with `contents: write` scope: + (PAT) with `contents: write` scope on the checkout step: ```yaml - uses: actions/checkout@v7 @@ -104,14 +105,16 @@ in addition to any other permissions needed for other features: token: ${{ secrets.MY_PAT }} ``` - When using the default `GITHUB_TOKEN`, you can include `[skip ci]` in - the auto-fix commit message to avoid unnecessary CI runs on the - fix commit itself. See the [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg) input. + Conversely, if you use a PAT or GitHub App token (whose pushes **do** + trigger CI) but do not want the auto-fix commit itself to start a new + run, include `[skip ci]` in the commit message via the + [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg) input. + With the default `GITHUB_TOKEN`, `[skip ci]` is unnecessary since the + push does not trigger CI anyway. !!! warning "Pull requests from third-party forks" - Auto-fix does not work on pull requests from third-party forks. The - `GITHUB_TOKEN` lacks write permission to the fork repository, and - `git push` to the fork's branch is not possible. Consider - restricting `auto-fix` to `push` events or pull requests from the - same repository. + Auto-fix is automatically skipped for pull requests from third-party + forks: the `GITHUB_TOKEN` cannot push to the fork's branch, so the + action emits a warning and makes no commit. Use `auto-fix` on `push` + events or on pull requests from the same repository. From cd071f66f71891da71983289c1e98a67156953b0 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 24 Jul 2026 20:18:28 +0300 Subject: [PATCH 11/23] chore: update auto-fix-commit-msg default value I would like to change the default clang-format commit message and leave "refactor: apply clang-tidy fixes" for clang-tidy in the futhure if we also support auto-fix for it Co-authored-by: Xianpeng Shen --- README.md | 2 +- action.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 827a90af..4a069f34 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ When enabled, the action will: > ```yaml > with: > auto-fix: 'true' -> auto-fix-commit-msg: 'style: apply styling format fix [skip ci]' +> auto-fix-commit-msg: 'style: apply clang-format fixes [skip ci]' > ``` ## Used By diff --git a/action.yml b/action.yml index a0a07887..f51152b3 100644 --- a/action.yml +++ b/action.yml @@ -225,7 +225,7 @@ inputs: description: | Custom commit message for the auto-fix commit. Only used when ``auto-fix`` is ``true``. - default: 'style: apply styling format fix' + default: 'style: apply clang-format fixes' required: false auto-fix-git-user: description: |- @@ -585,7 +585,7 @@ runs: # Avoid `git add -A`, which would also sweep in unrelated untracked files. ^git add -u let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { - 'style: apply styling format fix' + 'style: apply clang-format fixes' } else { '${{ inputs.auto-fix-commit-msg }}' } From 148523a8489379c311b4a514ebcc42bd819da6d1 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 20 Aug 2026 02:29:41 +0300 Subject: [PATCH 12/23] docs: correct what auto-fix does to CI runs and to unchanged lines Two places said something the implementation does not do. The README told everyone to tag the commit with `[skip ci]` to stop it re-running CI, but the example above it checks out with the default `GITHUB_TOKEN` -- which cannot start a workflow run at all, so there was nothing to skip. docs/permissions.md already had this right and the two contradicted each other. The tip now names the token that makes `[skip ci]` meaningful and points at the permissions page for both setups. `auto-fix`'s own description implied it rewrites whole files. It does not: cpp-linter assembles range-aware args, so `lines-changed-only` narrows what gets reformatted. That matters, because with it enabled a file can come back from auto-fix still failing a whole-file `.clang-format` check -- worth saying out loud rather than leaving to be discovered. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 9 +++++++-- action.yml | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a069f34..dca0f5ea 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [io-doc]: https://cpp-linter.github.io/cpp-linter-action/inputs-outputs [recipes-doc]: https://cpp-linter.github.io/cpp-linter-action/examples +[permissions-doc]: https://cpp-linter.github.io/cpp-linter-action/permissions [format-annotations-preview]: https://raw.githubusercontent.com/cpp-linter/cpp-linter-action/main/docs/images/annotations-clang-format.png [tidy-annotations-preview]: https://raw.githubusercontent.com/cpp-linter/cpp-linter-action/main/docs/images/annotations-clang-tidy.png @@ -89,14 +90,18 @@ When enabled, the action will: ``` > [!TIP] -> To prevent the auto-fix commit from triggering another CI run, include a -> `[skip ci]` (or `[ci skip]`, `[no ci]`, etc.) tag in your custom commit message: +> The default `GITHUB_TOKEN` cannot start new workflow runs, so the auto-fix +> commit does not re-run your CI. Check out with a PAT or GitHub App token if you +> want it to — and then, to keep a particular auto-fix commit from re-running CI +> anyway, tag its message with `[skip ci]` (or `[ci skip]`, `[no ci]`, etc.): > > ```yaml > with: > auto-fix: 'true' > auto-fix-commit-msg: 'style: apply clang-format fixes [skip ci]' > ``` +> +> See [our documented permissions][permissions-doc] for both setups. ## Used By diff --git a/action.yml b/action.yml index f51152b3..bd6d006c 100644 --- a/action.yml +++ b/action.yml @@ -218,6 +218,11 @@ inputs: ``clang-format -i`` on files with style issues. After that, a new commit is pushed to the PR branch with the formatted changes. + Fixes respect [`lines-changed-only`](#lines-changed-only): only the lines + it selects are reformatted. With it enabled, a file can come out of + auto-fix still not satisfying `.clang-format` as a whole, because the + lines outside the diff are left as they were. + This option has no effect on clang-tidy issues. default: 'false' required: false From 7aae5aae05eba6c900a767eb83a68275aeb6c11f Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Sep 2026 09:33:38 +0300 Subject: [PATCH 13/23] docs: describe the GitHub App token setup for auto-fix - permissions.md: new "GitHub App token" section (App permissions, create-github-app-token, pass the token to checkout and cpp-linter); the CI re-trigger note points at it instead of recommending a PAT - fork note: fork pull requests get no secrets, so an App token or PAT cannot help there; auto-fix stays skipped - README tip and example workflow updated to match - docs/action.yml: auto-fix inputs land in 2.22.0, not 2.19.0 --- .github/workflows/examples/auto-fix.yml | 6 +-- README.md | 11 ++-- docs/action.yml | 8 +-- docs/permissions.md | 71 ++++++++++++++++++------- 4 files changed, 64 insertions(+), 32 deletions(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index 72a39d59..66b6d197 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -12,9 +12,9 @@ jobs: pull-requests: read # needed to list changed files on pull_request events steps: - uses: actions/checkout@v7 - # For auto-fix commits to trigger new CI runs, use a PAT instead: - # with: - # token: ${{ secrets.MY_PAT }} + # Pushes made with the default GITHUB_TOKEN do not start new workflow + # runs. To have the auto-fix commit re-checked by CI, check out and run + # the action with a GitHub App token; see the permissions docs. - uses: cpp-linter/cpp-linter-action@v2 id: linter diff --git a/README.md b/README.md index dca0f5ea..6d6b42ab 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ [io-doc]: https://cpp-linter.github.io/cpp-linter-action/inputs-outputs [recipes-doc]: https://cpp-linter.github.io/cpp-linter-action/examples [permissions-doc]: https://cpp-linter.github.io/cpp-linter-action/permissions +[app-token-doc]: https://cpp-linter.github.io/cpp-linter-action/permissions/#github-app-token [format-annotations-preview]: https://raw.githubusercontent.com/cpp-linter/cpp-linter-action/main/docs/images/annotations-clang-format.png [tidy-annotations-preview]: https://raw.githubusercontent.com/cpp-linter/cpp-linter-action/main/docs/images/annotations-clang-tidy.png @@ -90,10 +91,10 @@ When enabled, the action will: ``` > [!TIP] -> The default `GITHUB_TOKEN` cannot start new workflow runs, so the auto-fix -> commit does not re-run your CI. Check out with a PAT or GitHub App token if you -> want it to — and then, to keep a particular auto-fix commit from re-running CI -> anyway, tag its message with `[skip ci]` (or `[ci skip]`, `[no ci]`, etc.): +> Commits pushed with the default `GITHUB_TOKEN` do not start new workflow runs, +> so CI does not re-check the auto-fix commit. To change that, check out and run +> the action with a [GitHub App token][app-token-doc]. To keep a particular +> auto-fix commit from re-running CI, add `[skip ci]` to its message: > > ```yaml > with: @@ -101,7 +102,7 @@ When enabled, the action will: > auto-fix-commit-msg: 'style: apply clang-format fixes [skip ci]' > ``` > -> See [our documented permissions][permissions-doc] for both setups. +> See [our documented permissions][permissions-doc] for the required scopes. ## Used By diff --git a/docs/action.yml b/docs/action.yml index 7badc478..5455b24e 100644 --- a/docs/action.yml +++ b/docs/action.yml @@ -48,14 +48,14 @@ inputs: minimum-version: '2.12.0' required-permission: 'pull-requests: write #pull-request-reviews' auto-fix: - minimum-version: '2.19.0' + minimum-version: '2.22.0' required-permission: 'contents: write #auto-fix' auto-fix-commit-msg: - minimum-version: '2.19.0' + minimum-version: '2.22.0' auto-fix-git-user: - minimum-version: '2.19.0' + minimum-version: '2.22.0' auto-fix-git-email: - minimum-version: '2.19.0' + minimum-version: '2.22.0' jobs: minimum-version: '2.11.0' cache-enable: diff --git a/docs/permissions.md b/docs/permissions.md index edd1de96..639d180d 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -94,27 +94,58 @@ in addition to any other permissions needed for other features: !!! warning "CI re-triggering with auto-fix" - The default `GITHUB_TOKEN` **cannot** trigger new CI runs when pushing - a commit. If you need the auto-fix commit to trigger CI checks - (e.g. to verify the fix builds clean), use a personal access token - (PAT) with `contents: write` scope on the checkout step: + Commits pushed with the default `GITHUB_TOKEN` do not start new workflow + runs, so CI does not re-check the auto-fix commit. If you need that, push + with a [GitHub App token](#github-app-token) or a personal access token + that has `contents: write`. - ```yaml - - uses: actions/checkout@v7 - with: - token: ${{ secrets.MY_PAT }} - ``` - - Conversely, if you use a PAT or GitHub App token (whose pushes **do** - trigger CI) but do not want the auto-fix commit itself to start a new - run, include `[skip ci]` in the commit message via the - [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg) input. - With the default `GITHUB_TOKEN`, `[skip ci]` is unnecessary since the - push does not trigger CI anyway. + If your token does trigger CI and you want to keep a particular auto-fix + commit from starting a run, add `[skip ci]` to + [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg). !!! warning "Pull requests from third-party forks" - Auto-fix is automatically skipped for pull requests from third-party - forks: the `GITHUB_TOKEN` cannot push to the fork's branch, so the - action emits a warning and makes no commit. Use `auto-fix` on `push` - events or on pull requests from the same repository. + Auto-fix is skipped for pull requests from forks. The `GITHUB_TOKEN` + cannot push to the fork's branch, and workflows triggered by fork pull + requests receive no secrets, so an App token or PAT is not available there + either. The action prints a warning and makes no commit. Use `auto-fix` on + `push` events or on pull requests from the same repository. + +## GitHub App token + +A token minted from a GitHub App you own replaces the default `GITHUB_TOKEN` +for every feature on this page. Pushes made with it start workflow runs, and +comments and reviews are posted under the App's name instead of +`github-actions[bot]`. + +1. [Register a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) + with the repository permissions **Contents: Read and write** and + **Pull requests: Read and write**, then install it on the repository. +2. Store the App ID as a repository variable and the private key as a secret. +3. Mint the token at the start of the job and pass it to both `actions/checkout` + and cpp-linter: + +```yaml + steps: + - uses: actions/create-github-app-token@v3 + id: app-token + with: + app-id: ${{ vars.CPP_LINTER_APP_ID }} + private-key: ${{ secrets.CPP_LINTER_APP_PRIVATE_KEY }} + - uses: actions/checkout@v7 + with: + token: ${{ steps.app-token.outputs.token }} # (1)! + - uses: cpp-linter/cpp-linter-action@v2 + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # (2)! + with: + style: 'file' + auto-fix: 'true' +``` + +1. The auto-fix commit is pushed with this token, so the push triggers your + other workflows. +2. Thread comments and pull request reviews are posted with this token. + +The job's `permissions` block only applies to `GITHUB_TOKEN`; the App token's +permissions come from the App's settings. From b064aec38ef5e657aac977a4c62f4f6047d0dd3d Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Sun, 13 Sep 2026 19:28:01 +0300 Subject: [PATCH 14/23] Apply batched suggestions from code review Co-authored-by: Brendan <2bndy5@gmail.com> --- .github/workflows/examples/auto-fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index 66b6d197..aeda08d3 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -7,7 +7,7 @@ on: jobs: cpp-linter: runs-on: ubuntu-latest - permissions: + permissions: # explicit permissions granted to the `secrets.GITHUB_TOKEN` contents: write # needed for auto-fix commits pull-requests: read # needed to list changed files on pull_request events steps: From 1678b6a6898ae89681405f26894da2798e7bd14d Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Sep 2026 20:34:08 +0300 Subject: [PATCH 15/23] docs: surface the GitHub App token chapter in the README Give the GitHub App token setup its own section under Usage instead of only a passing mention in the auto-fix tip -- it applies to every feature and needs no server to host webhooks. Also add the relative link refs for permissions-doc and app-token-doc to docs/index.md, which includes the README; without them those links rendered as literal text on the docs homepage. --- README.md | 10 ++++++++++ docs/index.md | 2 ++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 6d6b42ab..3fb0ab06 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ When enabled, the action will: > > See [our documented permissions][permissions-doc] for the required scopes. +### Use your own GitHub App + +Every feature above can run with a token minted from a GitHub App that you own +instead of the default `GITHUB_TOKEN`. Comments and reviews are then posted +under your App's name rather than `github-actions[bot]`, and commits pushed by +`auto-fix` do start new workflow runs. The token is minted inside the job, so +there is no server or webhook handling to host. + +See [GitHub App token][app-token-doc] for the setup steps. + ## Used By

diff --git a/docs/index.md b/docs/index.md index 9a55fa3f..d5ecfe83 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,8 @@ [io-doc]: inputs-outputs.md [recipes-doc]: examples/index.md +[permissions-doc]: permissions.md +[app-token-doc]: permissions.md#github-app-token [format-annotations-preview]: images/annotations-clang-format.png [tidy-annotations-preview]: images/annotations-clang-tidy.png From 3dcf220c17d2a849774b17c16044fbb5d0fa2c1c Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Sun, 13 Sep 2026 20:40:22 +0300 Subject: [PATCH 16/23] Apply batched suggestions from code review Co-authored-by: Brendan <2bndy5@gmail.com> --- action.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index bd6d006c..02a3fa70 100644 --- a/action.yml +++ b/action.yml @@ -218,10 +218,8 @@ inputs: ``clang-format -i`` on files with style issues. After that, a new commit is pushed to the PR branch with the formatted changes. - Fixes respect [`lines-changed-only`](#lines-changed-only): only the lines - it selects are reformatted. With it enabled, a file can come out of - auto-fix still not satisfying `.clang-format` as a whole, because the - lines outside the diff are left as they were. + Fixes respect [`lines-changed-only`](#lines-changed-only): Only the + changed lines are reformatted accordingly. This option has no effect on clang-tidy issues. default: 'false' From da9a8d4436b6cd7826132905083882a04f196967 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Sun, 13 Sep 2026 22:14:31 +0300 Subject: [PATCH 17/23] Apply batched suggestions from code review Co-authored-by: Xianpeng Shen --- docs/action.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/action.yml b/docs/action.yml index 5455b24e..a9146157 100644 --- a/docs/action.yml +++ b/docs/action.yml @@ -48,14 +48,14 @@ inputs: minimum-version: '2.12.0' required-permission: 'pull-requests: write #pull-request-reviews' auto-fix: - minimum-version: '2.22.0' + minimum-version: '2.23.0' required-permission: 'contents: write #auto-fix' auto-fix-commit-msg: - minimum-version: '2.22.0' + minimum-version: '2.23.0' auto-fix-git-user: - minimum-version: '2.22.0' + minimum-version: '2.23.0' auto-fix-git-email: - minimum-version: '2.22.0' + minimum-version: '2.23.0' jobs: minimum-version: '2.11.0' cache-enable: From e1bbeb80315b75a77bdfcb10019554bbe1d96377 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 14 Sep 2026 01:23:24 +0300 Subject: [PATCH 18/23] fix(auto-fix): address review: no forced checkout, source-only commits Re-applies the action.yml part of 8580162, which was dropped when the branch was rewritten. - On pull_request events check out the PR head commit only when HEAD is the merge commit, without --force; a dirty tree or a failed checkout skips auto-fix with a warning instead (actions/checkout provides refs/pull/N/merge, so committing there would push that merge into the branch) - Stage only modified files matching the configured `extensions` and count `git ls-files --modified` output instead of parsing an exit code --- action.yml | 53 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/action.yml b/action.yml index 02a3fa70..28f22f1d 100644 --- a/action.yml +++ b/action.yml @@ -460,19 +460,41 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args ...$cmd } - - name: Checkout PR branch for auto-fix push capability + - name: Check out the pull request head for auto-fix + id: auto-fix-head if: >- (inputs.auto-fix == 'true' || inputs.auto-fix == true) && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository shell: nu {0} run: | - let head_ref = $env.GITHUB_HEAD_REF - if ($head_ref | is-not-empty) { - print $"(ansi purple)Fetching PR branch \"($head_ref)\" for auto-fix(ansi reset)" - ^git fetch origin --depth=1 $"($head_ref):refs/remotes/origin/($head_ref)" - ^git checkout --force -B $head_ref $"refs/remotes/origin/($head_ref)" - print $"(ansi green)Switched to PR branch \"($head_ref)\"(ansi reset)" + # On pull_request events, actions/checkout provides the merge commit + # (refs/pull/N/merge), not the branch. A commit made on top of it would + # carry that merge into the PR branch, so format the head commit instead. + # Nothing is forced: a dirty working tree or a failed checkout skips + # auto-fix with a warning. + let head_sha = '${{ github.event.pull_request.head.sha }}' + let current = (^git rev-parse HEAD | str trim) + if $current == $head_sha { + print $"(ansi green)Pull request head ($head_sha) is already checked out(ansi reset)" + exit 0 + } + let dirty = (^git status --porcelain --untracked-files=no | str trim) + if ($dirty | is-not-empty) { + print "::warning title=Auto-fix skipped::The working tree has uncommitted changes to tracked files, so the pull request head was not checked out and no formatting commit was made. Make sure earlier steps leave tracked files unchanged, or check out the pull request head commit yourself." + $"skip=true\n" | save --append $env.GITHUB_OUTPUT + exit 0 + } + print $"(ansi purple)Checking out pull request head ($head_sha) for auto-fix(ansi reset)" + let fetched = (^git fetch origin --depth=1 $head_sha | complete) + let checked_out = if $fetched.exit_code == 0 { + ^git checkout --detach $head_sha | complete + } else { + $fetched + } + if $checked_out.exit_code != 0 { + print $"::warning title=Auto-fix skipped::Could not check out the pull request head commit ($head_sha), so no formatting commit was made: ($checked_out.stderr | str trim)" + $"skip=true\n" | save --append $env.GITHUB_OUTPUT } - name: Warn that auto-fix is skipped on forked pull requests @@ -550,6 +572,7 @@ runs: (inputs.auto-fix == 'true' || inputs.auto-fix == true) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + && steps.auto-fix-head.outputs.skip != 'true' shell: nu {0} run: | # Determine the destination branch up-front: the PR head ref, or the @@ -568,10 +591,16 @@ runs: exit 0 } - # Refresh index first so stat-only differences don't create false positives + # Refresh the index first so stat-only differences don't create false positives ^git update-index -q --refresh - let has_changes = (^git diff-index --name-status --exit-code HEAD -- | complete | $in.exit_code == 1) - if $has_changes { + # Only source files can have been touched by clang-format, so limit the + # commit to the configured extensions. Anything else modified by earlier + # steps stays out of it. + let pathspecs = ('${{ inputs.extensions }}' | split row ',' | each { |ext| $"*.($ext | str trim)" }) + let changed = (^git ls-files --modified -- ...$pathspecs | lines | where { |line| ($line | str trim | is-not-empty) }) + if ($changed | is-not-empty) { + print $"(ansi purple)Committing ($changed | length) formatted file\(s\)(ansi reset)" + for file in $changed { print $" ($file)" } let git_user_name = if ('${{ inputs.auto-fix-git-user }}' | is-empty) { $env.GITHUB_ACTOR } else { @@ -584,9 +613,7 @@ runs: } let git_user = $"user.name=($git_user_name)" let git_email = $"user.email=($git_user_email)" - # Stage only tracked modifications (clang-format edits existing sources). - # Avoid `git add -A`, which would also sweep in unrelated untracked files. - ^git add -u + ^git add -- ...$changed let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply clang-format fixes' } else { From d6664a040356d0df44469f51aa2ffd9c9b1aafa8 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 14 Sep 2026 12:31:34 +0300 Subject: [PATCH 19/23] fix(auto-fix): stop checking out the PR head; require it from the workflow On pull_request events actions/checkout provides refs/pull/N/merge, so the action used to switch the workspace to the head commit itself. That silently changes the tree every later step sees. Drop the step and make the workflow responsible instead, the way git-auto-commit-action does: document `ref: ${{ github.event.pull_request.head.sha }}` on actions/checkout (head.sha rather than head_ref so fork PRs still check out), and have the commit step verify HEAD == head.sha on pull_request, skipping with a warning that names the missing input otherwise. --- .github/workflows/examples/auto-fix.yml | 4 ++ README.md | 6 +++ action.yml | 56 ++++++++----------------- docs/permissions.md | 16 +++++++ 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index aeda08d3..433518a0 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -12,6 +12,10 @@ jobs: pull-requests: read # needed to list changed files on pull_request events steps: - uses: actions/checkout@v7 + with: + # auto-fix commits on the pull request's head commit; the default + # checkout is the merge commit (empty on push events, so harmless). + ref: ${{ github.event.pull_request.head.sha }} # Pushes made with the default GITHUB_TOKEN do not start new workflow # runs. To have the auto-fix commit re-checked by CI, check out and run # the action with a GitHub App token; see the permissions docs. diff --git a/README.md b/README.md index 3fb0ab06..8e237e12 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ When enabled, the action will: ```yaml steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} # the PR head, not the merge commit - uses: cpp-linter/cpp-linter-action@v2 id: linter env: @@ -90,6 +92,10 @@ When enabled, the action will: auto-fix: 'true' # automatically fix format issues ``` +On `pull_request` events `actions/checkout` checks out the merge commit by default. Auto-fix +commits on the pull request's head commit, so set `ref` as above; without it the action prints +a warning and makes no commit. + > [!TIP] > Commits pushed with the default `GITHUB_TOKEN` do not start new workflow runs, > so CI does not re-check the auto-fix commit. To change that, check out and run diff --git a/action.yml b/action.yml index 28f22f1d..e07176e1 100644 --- a/action.yml +++ b/action.yml @@ -221,6 +221,11 @@ inputs: Fixes respect [`lines-changed-only`](#lines-changed-only): Only the changed lines are reformatted accordingly. + On `pull_request` events, check out the pull request's head commit + (`ref: github.event.pull_request.head.sha` in `actions/checkout`). + The default checkout is the merge commit, and auto-fix is skipped with + a warning when that is what is checked out. + This option has no effect on clang-tidy issues. default: 'false' required: false @@ -460,43 +465,6 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args ...$cmd } - - name: Check out the pull request head for auto-fix - id: auto-fix-head - if: >- - (inputs.auto-fix == 'true' || inputs.auto-fix == true) - && github.event_name == 'pull_request' - && github.event.pull_request.head.repo.full_name == github.repository - shell: nu {0} - run: | - # On pull_request events, actions/checkout provides the merge commit - # (refs/pull/N/merge), not the branch. A commit made on top of it would - # carry that merge into the PR branch, so format the head commit instead. - # Nothing is forced: a dirty working tree or a failed checkout skips - # auto-fix with a warning. - let head_sha = '${{ github.event.pull_request.head.sha }}' - let current = (^git rev-parse HEAD | str trim) - if $current == $head_sha { - print $"(ansi green)Pull request head ($head_sha) is already checked out(ansi reset)" - exit 0 - } - let dirty = (^git status --porcelain --untracked-files=no | str trim) - if ($dirty | is-not-empty) { - print "::warning title=Auto-fix skipped::The working tree has uncommitted changes to tracked files, so the pull request head was not checked out and no formatting commit was made. Make sure earlier steps leave tracked files unchanged, or check out the pull request head commit yourself." - $"skip=true\n" | save --append $env.GITHUB_OUTPUT - exit 0 - } - print $"(ansi purple)Checking out pull request head ($head_sha) for auto-fix(ansi reset)" - let fetched = (^git fetch origin --depth=1 $head_sha | complete) - let checked_out = if $fetched.exit_code == 0 { - ^git checkout --detach $head_sha | complete - } else { - $fetched - } - if $checked_out.exit_code != 0 { - print $"::warning title=Auto-fix skipped::Could not check out the pull request head commit ($head_sha), so no formatting commit was made: ($checked_out.stderr | str trim)" - $"skip=true\n" | save --append $env.GITHUB_OUTPUT - } - - name: Warn that auto-fix is skipped on forked pull requests if: >- (inputs.auto-fix == 'true' || inputs.auto-fix == true) @@ -572,7 +540,6 @@ runs: (inputs.auto-fix == 'true' || inputs.auto-fix == true) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - && steps.auto-fix-head.outputs.skip != 'true' shell: nu {0} run: | # Determine the destination branch up-front: the PR head ref, or the @@ -591,6 +558,19 @@ runs: exit 0 } + # On pull_request events actions/checkout provides the merge commit + # (refs/pull/N/merge) unless the workflow sets `ref` to the head SHA. + # A commit made on the merge commit would carry that merge into the + # branch, so only commit when the head commit is what is checked out. + let head_sha = '${{ github.event.pull_request.head.sha }}' + if ($head_sha | is-not-empty) { + let current = (^git rev-parse HEAD | str trim) + if $current != $head_sha { + print $"::warning title=Auto-fix skipped::HEAD is ($current), not the pull request head ($head_sha). actions/checkout checks out the merge commit by default; set its `ref` input to the pull request head SHA \(github.event.pull_request.head.sha\) so auto-fix can commit on the branch. See https://cpp-linter.github.io/cpp-linter-action/permissions/#auto-fix" + exit 0 + } + } + # Refresh the index first so stat-only differences don't create false positives ^git update-index -q --refresh # Only source files can have been touched by clang-format, so limit the diff --git a/docs/permissions.md b/docs/permissions.md index 639d180d..859e4958 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -92,6 +92,21 @@ in addition to any other permissions needed for other features: 1. Needed by the token used in `actions/checkout` to commit and push the formatted changes back to the branch. +!!! info "Check out the pull request head" + + On `pull_request` events `actions/checkout` provides the merge commit + (`refs/pull/N/merge`), not the branch. A commit made on it would carry that + merge into the pull request, so auto-fix only commits when the head commit + is checked out, and prints a warning otherwise: + + ```yaml + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + ``` + + The expression is empty on `push` events, so the same step works for both. + !!! warning "CI re-triggering with auto-fix" Commits pushed with the default `GITHUB_TOKEN` do not start new workflow @@ -135,6 +150,7 @@ comments and reviews are posted under the App's name instead of - uses: actions/checkout@v7 with: token: ${{ steps.app-token.outputs.token }} # (1)! + ref: ${{ github.event.pull_request.head.sha }} - uses: cpp-linter/cpp-linter-action@v2 env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # (2)! From ae9f3d05fc343674337f707348666197de684305 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Wed, 16 Sep 2026 13:41:54 +0300 Subject: [PATCH 20/23] refactor(auto-fix): fold the fork warning into the commit step, trim docs - one auto-fix step instead of two: the fork check is an early exit in the commit step, so the condition logic lives in one place - push failures print stderr plus the permissions hint instead of guessing the cause from the message text - shorter input description; README no longer restates it as a list; the two permissions-page warnings are one admonition --- README.md | 14 ++++------ action.yml | 63 +++++++++++++++++---------------------------- docs/permissions.md | 22 ++++++---------- 3 files changed, 36 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 8e237e12..073fb707 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,8 @@ See also our [example recipes][recipes-doc]. ### Auto-fix clang-format issues -You can enable automatic fixing of clang-format issues by setting `auto-fix: 'true'`. -When enabled, the action will: - -1. Run clang-format detection as usual -2. Apply `clang-format -i` to fix any files with style issues -3. Commit and push the formatted changes back to the PR branch +Set `auto-fix: 'true'` and the action applies `clang-format -i` to the files with style +issues and commits the result to the branch: ```yaml steps: @@ -92,9 +88,9 @@ When enabled, the action will: auto-fix: 'true' # automatically fix format issues ``` -On `pull_request` events `actions/checkout` checks out the merge commit by default. Auto-fix -commits on the pull request's head commit, so set `ref` as above; without it the action prints -a warning and makes no commit. +On `pull_request` events `actions/checkout` checks out the merge commit by default; auto-fix +needs the head commit, so set `ref` as above. Without it the action prints a warning and makes +no commit. > [!TIP] > Commits pushed with the default `GITHUB_TOKEN` do not start new workflow runs, diff --git a/action.yml b/action.yml index e07176e1..b45836bf 100644 --- a/action.yml +++ b/action.yml @@ -211,20 +211,15 @@ inputs: required: false auto-fix: description: | - Set this option to `true` to automatically apply clang-format fixes - and commit them back to the PR branch. + Set this option to `true` to apply clang-format fixes (`clang-format -i`) + and commit them to the branch. Fixes respect + [`lines-changed-only`](#lines-changed-only): Only the changed lines are + reformatted accordingly. - When enabled, cpp-linter runs with ``--fix``, which applies - ``clang-format -i`` on files with style issues. After that, - a new commit is pushed to the PR branch with the formatted changes. - - Fixes respect [`lines-changed-only`](#lines-changed-only): Only the - changed lines are reformatted accordingly. - - On `pull_request` events, check out the pull request's head commit - (`ref: github.event.pull_request.head.sha` in `actions/checkout`). - The default checkout is the merge commit, and auto-fix is skipped with - a warning when that is what is checked out. + On `pull_request` events check out the pull request's head commit + (`ref: github.event.pull_request.head.sha` in `actions/checkout`); the + default checkout is the merge commit, and auto-fix then skips with a + warning. Pull requests from forks are skipped as well. This option has no effect on clang-tidy issues. default: 'false' @@ -465,15 +460,6 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args ...$cmd } - - name: Warn that auto-fix is skipped on forked pull requests - if: >- - (inputs.auto-fix == 'true' || inputs.auto-fix == true) - && github.event_name == 'pull_request' - && github.event.pull_request.head.repo.full_name != github.repository - shell: nu {0} - run: | - print "::warning title=Auto-fix skipped::auto-fix cannot push to a third-party fork's branch, so no formatting commit was made. Apply clang-format fixes from within the fork or run cpp-linter locally." - - name: Run cpp-linter id: cpp-linter shell: nu {0} @@ -536,15 +522,17 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args cpp-linter ...$args - name: Auto-commit clang-format fixes - if: >- - (inputs.auto-fix == 'true' || inputs.auto-fix == true) - && (github.event_name != 'pull_request' - || github.event.pull_request.head.repo.full_name == github.repository) + if: inputs.auto-fix == 'true' || inputs.auto-fix == true shell: nu {0} run: | - # Determine the destination branch up-front: the PR head ref, or the - # pushed branch. Bail out on tag refs (or anything that is not a branch) - # so we never push HEAD to refs/heads/ and create a stray branch. + # The token cannot push to a third-party fork's branch. + if '${{ github.event_name }}' == 'pull_request' and '${{ github.event.pull_request.head.repo.full_name }}' != '${{ github.repository }}' { + print "::warning title=Auto-fix skipped::auto-fix cannot push to a third-party fork's branch, so no formatting commit was made. Apply clang-format fixes from within the fork or run cpp-linter locally." + exit 0 + } + + # Destination branch: the PR head ref, or the pushed branch. Tags and + # other refs are skipped so HEAD never lands on refs/heads/. let head_ref = $env.GITHUB_HEAD_REF let branch = if ($head_ref | is-not-empty) { $head_ref @@ -571,11 +559,10 @@ runs: } } - # Refresh the index first so stat-only differences don't create false positives + # Refresh stat info so untouched files don't show as modified, then stage + # only source files (the configured extensions): anything else an earlier + # step modified stays out of the commit. ^git update-index -q --refresh - # Only source files can have been touched by clang-format, so limit the - # commit to the configured extensions. Anything else modified by earlier - # steps stays out of it. let pathspecs = ('${{ inputs.extensions }}' | split row ',' | each { |ext| $"*.($ext | str trim)" }) let changed = (^git ls-files --modified -- ...$pathspecs | lines | where { |line| ($line | str trim | is-not-empty) }) if ($changed | is-not-empty) { @@ -599,15 +586,11 @@ runs: } else { '${{ inputs.auto-fix-commit-msg }}' } - ^git -c $git_user -c $git_email commit -m $"($commit_msg)" + ^git -c $git_user -c $git_email commit -m $commit_msg let push_result = (^git push origin $"HEAD:refs/heads/($branch)") | complete if $push_result.exit_code != 0 { - let stderr_lower = ($push_result.stderr | str downcase) - if ($stderr_lower | str contains "403") or ($stderr_lower | str contains "refused") or ($stderr_lower | str contains "not have permission") { - print $"::warning title=Auto-fix push failed::This action does not have permission to push to this branch. Ensure the token used by actions/checkout has `contents: write` (branch protection rules may also block the push). See docs/permissions.md for details." - } else { - print $"::warning title=Auto-fix push failed::(ansi yellow)($push_result.stderr)(ansi reset)" - } + let reason = ($push_result.stderr | str replace --all "\n" " " | str trim) + print $"::warning title=Auto-fix push failed::($reason) The token used by actions/checkout needs `contents: write` and branch protection must allow the push; see https://cpp-linter.github.io/cpp-linter-action/permissions/#auto-fix" } else { print $"(ansi green)Auto-fix commit pushed successfully(ansi reset)" } diff --git a/docs/permissions.md b/docs/permissions.md index 859e4958..5327ab2f 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -107,24 +107,18 @@ in addition to any other permissions needed for other features: The expression is empty on `push` events, so the same step works for both. -!!! warning "CI re-triggering with auto-fix" +!!! warning "Limits" Commits pushed with the default `GITHUB_TOKEN` do not start new workflow - runs, so CI does not re-check the auto-fix commit. If you need that, push + runs, so CI does not re-check the auto-fix commit. To change that, push with a [GitHub App token](#github-app-token) or a personal access token - that has `contents: write`. + that has `contents: write`; add `[skip ci]` to + [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg) if a + particular auto-fix commit should not start a run. - If your token does trigger CI and you want to keep a particular auto-fix - commit from starting a run, add `[skip ci]` to - [`auto-fix-commit-msg`](./inputs-outputs.md#auto-fix-commit-msg). - -!!! warning "Pull requests from third-party forks" - - Auto-fix is skipped for pull requests from forks. The `GITHUB_TOKEN` - cannot push to the fork's branch, and workflows triggered by fork pull - requests receive no secrets, so an App token or PAT is not available there - either. The action prints a warning and makes no commit. Use `auto-fix` on - `push` events or on pull requests from the same repository. + Pull requests from forks are skipped with a warning: `GITHUB_TOKEN` cannot + push to the fork's branch, and fork pull requests receive no secrets, so an + App token or PAT is not available there either. ## GitHub App token From 8feb22c6fc72acab3a7ec79d71b67727ac1f6e7e Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 24 Aug 2026 23:18:53 +0300 Subject: [PATCH 21/23] test: run auto-fix in the self-test Adds a job that runs the action with auto-fix on the demo sources and asserts the resulting commit: message, author and email from the inputs, only files under docs/examples/demo, a clean tree afterwards and zero clang-format findings. The job has no push credentials, so the push is rejected by design and the last step checks the commit never reached the PR branch. --- .github/workflows/self-test.yml | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index 1f37930e..79515749 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -81,3 +81,102 @@ jobs: echo "clang-format checks-failed: ${{ steps.linter.outputs.clang-format-checks-failed }}" # for actual deployment # run: exit 1 + + test-auto-fix: + # Runs the real action with auto-fix and checks the commit it makes. The + # job never gets push credentials (persist-credentials: false, contents: + # read), so the push is rejected on purpose and the demo sources stay + # mis-formatted for the other jobs; the last step proves nothing left the + # runner. pull_request only: on a push the target branch would be main. + if: github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: read + runs-on: ubuntu-latest + env: + EXPECTED_MSG: 'style: apply clang-format fixes [self-test]' + EXPECTED_AUTHOR: 'cpp-linter-self-test ' + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} # auto-fix commits on the PR head + + - name: Self test auto-fix + uses: ./ + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: file + auto-fix: 'true' + auto-fix-commit-msg: ${{ env.EXPECTED_MSG }} + auto-fix-git-user: cpp-linter-self-test + auto-fix-git-email: self-test@users.noreply.github.com + tidy-checks: '-*' # no clang-tidy, no compilation database needed + files-changed-only: false + lines-changed-only: false + ignore: build|venv + version: '16' + verbosity: debug + thread-comments: false + file-annotations: false + step-summary: false + + - name: Assert auto-fix committed the formatting changes + run: | + set -euo pipefail + + echo "::group::Tip commit after auto-fix" + git --no-pager log -1 --pretty=fuller --stat + echo "::endgroup::" + + subject="$(git log -1 --pretty=%s)" + if [ "$subject" != "$EXPECTED_MSG" ]; then + echo "::error title=auto-fix::tip commit is '$subject', expected the auto-fix commit '$EXPECTED_MSG'" + exit 1 + fi + + author="$(git log -1 --pretty='%an <%ae>')" + if [ "$author" != "$EXPECTED_AUTHOR" ]; then + echo "::error title=auto-fix::commit author is '$author', expected '$EXPECTED_AUTHOR'" + exit 1 + fi + + changed="$(git show --pretty=format: --name-only HEAD | sed '/^$/d')" + if [ -z "$changed" ]; then + echo "::error title=auto-fix::the auto-fix commit is empty; clang-format changed nothing" + exit 1 + fi + + if stray="$(printf '%s\n' "$changed" | grep -v '^docs/examples/demo/')"; then + echo "::error title=auto-fix::commit touched files outside docs/examples/demo:" + printf '%s\n' "$stray" + exit 1 + fi + + if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + echo "::error title=auto-fix::tracked files are still modified after the commit:" + git status --porcelain --untracked-files=no + exit 1 + fi + + failed='${{ steps.linter.outputs.clang-format-checks-failed }}' + if [ "${failed:-0}" != "0" ]; then + echo "::error title=auto-fix::clang-format still reports $failed issue(s) after auto-fix" + exit 1 + fi + + echo "auto-fix committed $(printf '%s\n' "$changed" | wc -l) file(s) as expected" + + - name: Assert the commit never reached the PR branch + run: | + set -euo pipefail + + git fetch --no-tags --depth=1 origin "$GITHUB_HEAD_REF" + if [ "$(git rev-parse HEAD)" = "$(git rev-parse FETCH_HEAD)" ]; then + echo "::error title=auto-fix::the auto-fix commit reached the PR branch; this job must never push" + exit 1 + fi + echo "PR branch is untouched, as expected" From c9db6079f6d396aecbc9035a7b2398317f5ff36f Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 18 Sep 2026 10:14:15 +0300 Subject: [PATCH 22/23] fix(auto-fix): check out the PR head in the action, count changes after staging Restore the step that switches from the pull_request merge commit to the head commit, so workflows can keep the default actions/checkout. It only acts when HEAD is the merge commit, never forces, and fetches the head commit only when it is missing: a --depth fetch truncates a full clone. Stage first and count from `git status --short`, as suggested in review. With a .gitattributes LF/CRLF rule the old check listed a file that `git add` normalized away, and `git commit` then failed the step. The pathspecs are resolved to modified tracked files first because `git add` aborts on a pathspec that matches nothing, and so that untracked sources are not committed. --- .github/workflows/examples/auto-fix.yml | 4 -- README.md | 7 +-- action.yml | 68 +++++++++++++++++++------ docs/permissions.md | 17 ++----- 4 files changed, 59 insertions(+), 37 deletions(-) diff --git a/.github/workflows/examples/auto-fix.yml b/.github/workflows/examples/auto-fix.yml index 433518a0..aeda08d3 100644 --- a/.github/workflows/examples/auto-fix.yml +++ b/.github/workflows/examples/auto-fix.yml @@ -12,10 +12,6 @@ jobs: pull-requests: read # needed to list changed files on pull_request events steps: - uses: actions/checkout@v7 - with: - # auto-fix commits on the pull request's head commit; the default - # checkout is the merge commit (empty on push events, so harmless). - ref: ${{ github.event.pull_request.head.sha }} # Pushes made with the default GITHUB_TOKEN do not start new workflow # runs. To have the auto-fix commit re-checked by CI, check out and run # the action with a GitHub App token; see the permissions docs. diff --git a/README.md b/README.md index 073fb707..2ffe562f 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,6 @@ issues and commits the result to the branch: ```yaml steps: - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.sha }} # the PR head, not the merge commit - uses: cpp-linter/cpp-linter-action@v2 id: linter env: @@ -88,9 +86,8 @@ issues and commits the result to the branch: auto-fix: 'true' # automatically fix format issues ``` -On `pull_request` events `actions/checkout` checks out the merge commit by default; auto-fix -needs the head commit, so set `ref` as above. Without it the action prints a warning and makes -no commit. +On `pull_request` events `actions/checkout` checks out the merge commit, so the action switches +the workspace to the pull request's head commit before it lints and commits. > [!TIP] > Commits pushed with the default `GITHUB_TOKEN` do not start new workflow runs, diff --git a/action.yml b/action.yml index b45836bf..926f4009 100644 --- a/action.yml +++ b/action.yml @@ -216,10 +216,10 @@ inputs: [`lines-changed-only`](#lines-changed-only): Only the changed lines are reformatted accordingly. - On `pull_request` events check out the pull request's head commit - (`ref: github.event.pull_request.head.sha` in `actions/checkout`); the - default checkout is the merge commit, and auto-fix then skips with a - warning. Pull requests from forks are skipped as well. + On `pull_request` events the action checks out the pull request's head + commit before linting: the default checkout is the merge commit, and a fix + committed on it would carry that merge into the branch. Pull requests from + forks are skipped. This option has no effect on clang-tidy issues. default: 'false' @@ -460,6 +460,35 @@ runs: ^$'($env.UV_INSTALL_DIR)/uv' ...$uv_args ...$cmd } + - name: Check out the pull request head for auto-fix + if: >- + (inputs.auto-fix == 'true' || inputs.auto-fix == true) + && github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + shell: nu {0} + run: | + # On pull_request events actions/checkout provides the merge commit + # (refs/pull/N/merge), detached. A fix committed on top of it would carry + # that merge into the PR branch, so format the head commit instead. + # Nothing is forced: git refuses to switch over conflicting local changes. + # Any other checkout (e.g. the workflow set `ref`) is left alone. + if (^git rev-parse HEAD | str trim) != $env.GITHUB_SHA { + exit 0 + } + let head_sha = '${{ github.event.pull_request.head.sha }}' + print $"(ansi purple)Checking out pull request head ($head_sha) for auto-fix(ansi reset)" + # The default shallow clone only holds the merge commit. Fetch the head + # commit only when it is missing: --depth would truncate a full clone. + if (^git cat-file -e $head_sha | complete).exit_code != 0 { + let fetched = (^git fetch origin --depth=1 $head_sha | complete) + if $fetched.exit_code != 0 { print $fetched.stderr } + } + let checked_out = (^git checkout --detach $head_sha | complete) + if $checked_out.exit_code != 0 { + let reason = ($checked_out.stderr | str replace --all "\n" " " | str trim) + print $"::warning title=Auto-fix checkout failed::Could not check out the pull request head ($head_sha): ($reason)" + } + - name: Run cpp-linter id: cpp-linter shell: nu {0} @@ -546,25 +575,33 @@ runs: exit 0 } - # On pull_request events actions/checkout provides the merge commit - # (refs/pull/N/merge) unless the workflow sets `ref` to the head SHA. - # A commit made on the merge commit would carry that merge into the - # branch, so only commit when the head commit is what is checked out. + # Only commit on the pull request head (see the checkout step above). let head_sha = '${{ github.event.pull_request.head.sha }}' if ($head_sha | is-not-empty) { let current = (^git rev-parse HEAD | str trim) if $current != $head_sha { - print $"::warning title=Auto-fix skipped::HEAD is ($current), not the pull request head ($head_sha). actions/checkout checks out the merge commit by default; set its `ref` input to the pull request head SHA \(github.event.pull_request.head.sha\) so auto-fix can commit on the branch. See https://cpp-linter.github.io/cpp-linter-action/permissions/#auto-fix" + print $"::warning title=Auto-fix skipped::HEAD is ($current), not the pull request head ($head_sha), so no formatting commit was made. auto-fix runs on `push` and `pull_request` events; see https://cpp-linter.github.io/cpp-linter-action/permissions/#auto-fix" exit 0 } } - # Refresh stat info so untouched files don't show as modified, then stage - # only source files (the configured extensions): anything else an earlier - # step modified stays out of the commit. - ^git update-index -q --refresh - let pathspecs = ('${{ inputs.extensions }}' | split row ',' | each { |ext| $"*.($ext | str trim)" }) - let changed = (^git ls-files --modified -- ...$pathspecs | lines | where { |line| ($line | str trim | is-not-empty) }) + # Stage only source files (the configured extensions), so anything else an + # earlier step modified stays out of the commit. `git add` aborts on a + # pathspec that matches nothing, which most of the default extensions do, + # so resolve them to modified tracked files first. Untracked sources + # (generated code, CMake's compiler-id files) stay out as well. + let path_specs = ('${{ inputs.extensions }}' | split row ',' | each { |ext| $"*.($ext | str trim)" }) + let modified = (^git ls-files --modified -- ...$path_specs | lines) + if ($modified | is-not-empty) { + ^git add -- ...$modified + } + # Count what got staged: .gitattributes rules (LF/CRLF) can normalize a + # modified file back to its committed content. + let changed = ( + ^git status --short --untracked-files=no -- ...$path_specs + | lines + | each { |line| $line | str substring 3.. } + ) if ($changed | is-not-empty) { print $"(ansi purple)Committing ($changed | length) formatted file\(s\)(ansi reset)" for file in $changed { print $" ($file)" } @@ -580,7 +617,6 @@ runs: } let git_user = $"user.name=($git_user_name)" let git_email = $"user.email=($git_user_email)" - ^git add -- ...$changed let commit_msg = if ('${{ inputs.auto-fix-commit-msg }}' | is-empty) { 'style: apply clang-format fixes' } else { diff --git a/docs/permissions.md b/docs/permissions.md index 5327ab2f..23285c43 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -92,20 +92,14 @@ in addition to any other permissions needed for other features: 1. Needed by the token used in `actions/checkout` to commit and push the formatted changes back to the branch. -!!! info "Check out the pull request head" +!!! info "The action checks out the pull request head" On `pull_request` events `actions/checkout` provides the merge commit (`refs/pull/N/merge`), not the branch. A commit made on it would carry that - merge into the pull request, so auto-fix only commits when the head commit - is checked out, and prints a warning otherwise: - - ```yaml - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.sha }} - ``` - - The expression is empty on `push` events, so the same step works for both. + merge into the pull request, so with `auto-fix` the action checks out the + pull request's head commit before it lints. Steps that run after the action + see that commit plus the auto-fix commit. If git refuses the checkout + because of local changes, auto-fix is skipped with a warning. !!! warning "Limits" @@ -144,7 +138,6 @@ comments and reviews are posted under the App's name instead of - uses: actions/checkout@v7 with: token: ${{ steps.app-token.outputs.token }} # (1)! - ref: ${{ github.event.pull_request.head.sha }} - uses: cpp-linter/cpp-linter-action@v2 env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # (2)! From b4e10340931c7cb5cc62f265af1ee37b09c1433e Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 18 Sep 2026 10:14:15 +0300 Subject: [PATCH 23/23] test: run the auto-fix self-test on the default checkout Drop `ref` so the job covers the action's own checkout of the PR head, and skip the job on fork pull requests, where auto-fix makes no commit. --- .github/workflows/self-test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index 79515749..6ffeee1e 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -88,7 +88,10 @@ jobs: # read), so the push is rejected on purpose and the demo sources stay # mis-formatted for the other jobs; the last step proves nothing left the # runner. pull_request only: on a push the target branch would be main. - if: github.event_name == 'pull_request' + # Not on forks: auto-fix skips them, so there would be no commit to check. + if: >- + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository permissions: contents: read pull-requests: read @@ -101,7 +104,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha }} # auto-fix commits on the PR head - name: Self test auto-fix uses: ./