From c4cd36acb1eab24f28c3c30d8689060458de735a Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Wed, 5 Aug 2026 17:35:40 +0000 Subject: [PATCH 1/7] test: exercise the auto-fix pull-request path end to end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 covers the push path. This adds the half only a real pull request can reach: the step that moves the runner off the PR merge ref and onto the PR head branch, its same-repository gating, and pushing the fix back so the pull request updates itself. Neither cpp-linter-action#443 nor cpp-linter#202 has to be merged — the action is consumed from its PR branch as a local action and its pinned cpp-linter version is rewritten in the runner's workspace. Scans only the files this pull request changes, because src/demo.cpp and src/demo.hpp are intentionally unformatted fixtures that auto-fix would otherwise repair and commit; the check asserts they stay untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012uXrN1wk5EaqK5GimN3deT --- .github/workflows/auto-fix-pr-e2e.yml | 182 ++++++++++++++++++++++++++ src/e2e_autofix_pr_demo.cpp | 2 + 2 files changed, 184 insertions(+) create mode 100644 .github/workflows/auto-fix-pr-e2e.yml create mode 100644 src/e2e_autofix_pr_demo.cpp diff --git a/.github/workflows/auto-fix-pr-e2e.yml b/.github/workflows/auto-fix-pr-e2e.yml new file mode 100644 index 0000000..8ece82b --- /dev/null +++ b/.github/workflows/auto-fix-pr-e2e.yml @@ -0,0 +1,182 @@ +name: auto-fix PR e2e + +# Phase 2 of the auto-fix end-to-end test: the pull-request path. +# +# Phase 1 (auto-fix-e2e.yml) covers the push path, where the action derives the +# branch from GITHUB_REF. This workflow covers what only a real PR can exercise: +# +# - the "Checkout PR branch for auto-fix push capability" step, which switches +# the runner off the PR *merge* ref and onto the PR *head* branch +# - the same-repository gating that gets applied to that step +# - pushing the fix back onto the PR head branch, so the PR updates itself +# +# As in phase 1, neither cpp-linter-action#443 nor cpp-linter#202 needs to be +# merged: the action is consumed from its PR branch as a local action, and the +# cpp-linter version it pins is rewritten in this runner's workspace. +# +# Workflows added by a pull request do run for `pull_request` events, so this +# file takes effect from the PR branch itself. + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + +env: + CPP_LINTER_REF: 'feature/auto-fix' + ACTION_DIR: .action-under-test + TEST_FILE: src/e2e_autofix_pr_demo.cpp + CLANG_VERSION: '18' + COMMIT_MSG: 'style: apply clang-format fixes' + +jobs: + auto-fix-pr: + # Only ever run for the dedicated e2e branch, never for real pull requests. + if: startsWith(github.head_ref, 'test/auto-fix-pr-e2e') + runs-on: ubuntu-latest + steps: + # `persist-credentials` stays enabled (the default) so the action can fetch + # the head branch and push the fix back to it. + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Check out the action under test + uses: actions/checkout@v7 + with: + repository: cpp-linter/cpp-linter-action + ref: feature/auto-fix + path: .action-under-test + persist-credentials: false + + - name: Point the action at the cpp-linter PR branch + run: | + set -euo pipefail + cd "$ACTION_DIR" + python3 - "$CPP_LINTER_REF" <<'PY' + import pathlib + import re + import sys + + ref = sys.argv[1] + spec = f"cpp-linter @ git+https://github.com/cpp-linter/cpp-linter.git@{ref}" + path = pathlib.Path("pyproject.toml") + text = path.read_text(encoding="utf-8") + patched, count = re.subn(r'"cpp-linter==[^"]+"', f'"{spec}"', text) + if count != 1: + sys.exit(f"expected exactly 1 cpp-linter pin, patched {count}") + path.write_text(patched, encoding="utf-8") + print(patched) + PY + rm -f uv.lock + + - name: Record the pre-fix state + run: | + set -euo pipefail + echo "::group::fixture before" + cat "$TEST_FILE" + echo "::endgroup::" + { + echo "MALFORMED_BLOB=$(git hash-object "$TEST_FILE")" + echo "PR_HEAD_SHA=${{ github.event.pull_request.head.sha }}" + } >> "$GITHUB_ENV" + + # `files-changed-only: true` is deliberate and load-bearing: src/demo.cpp + # and src/demo.hpp are intentionally unformatted fixtures of this repo, and + # scanning everything would make auto-fix "repair" and commit them too. + # clang-tidy is off so this measures the clang-format path only. + - name: Run cpp-linter with auto-fix + uses: ./.action-under-test + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: file + tidy-checks: '-*' + files-changed-only: true + lines-changed-only: false + ignore: build|.action-under-test + version: '18' + verbosity: debug + auto-fix: true + auto-fix-commit-msg: 'style: apply clang-format fixes' + + - name: Verify the PR branch was fixed and updated + run: | + set -uo pipefail + failed=0 + head_ref='${{ github.event.pull_request.head.ref }}' + + echo "::group::fixture after" + cat "$TEST_FILE" + echo "::endgroup::" + + # The action must have moved the runner onto the PR head branch. + current="$(git rev-parse --abbrev-ref HEAD)" + if [ "$current" != "$head_ref" ]; then + echo "::error title=Wrong branch::expected to be on '$head_ref', but HEAD is '$current'" + failed=1 + else + echo "PASS: runner switched from the merge ref onto '$head_ref'" + fi + + if [ "$(git hash-object "$TEST_FILE")" = "$MALFORMED_BLOB" ]; then + echo "::error title=Not reformatted::$TEST_FILE is unchanged; --fix did not rewrite it." + failed=1 + else + echo "PASS: fixture was reformatted" + fi + + head_sha="$(git rev-parse HEAD)" + subject="$(git log -1 --pretty=%s)" + if [ "$head_sha" = "$PR_HEAD_SHA" ]; then + echo "::error title=No commit::auto-fix produced no commit on the PR branch." + failed=1 + else + echo "PASS: auto-fix commit $head_sha" + echo " subject: $subject" + echo " author: $(git log -1 --pretty='%an <%ae>')" + fi + + if [ "$subject" != "$COMMIT_MSG" ]; then + echo "::error title=Wrong commit message::expected '$COMMIT_MSG', got '$subject'" + failed=1 + else + echo "PASS: commit message matches auto-fix-commit-msg" + fi + + fmt="$(command -v "clang-format-${CLANG_VERSION}" || command -v clang-format || true)" + if [ -n "$fmt" ]; then + if "$fmt" --style=file --dry-run --Werror "$TEST_FILE"; then + echo "PASS: committed fixture satisfies .clang-format" + else + echo "::error title=Still unformatted::the committed fixture still violates .clang-format" + failed=1 + fi + else + echo "note: clang-format not on PATH here; skipped the re-check" + fi + + # The repo's intentionally-unformatted fixtures must be left alone. + if ! git diff --quiet "$PR_HEAD_SHA" HEAD -- src/demo.cpp src/demo.hpp; then + echo "::error title=Collateral damage::auto-fix also rewrote this repo's intentional demo fixtures." + failed=1 + else + echo "PASS: src/demo.cpp and src/demo.hpp were left untouched" + fi + + # The fix has to be on the PR branch at the remote, not just locally. + git fetch -q origin "$head_ref" + if [ "$(git rev-parse FETCH_HEAD)" != "$head_sha" ]; then + echo "::error title=Not pushed::the auto-fix commit is not on origin/$head_ref" + failed=1 + else + echo "PASS: auto-fix commit is present on origin/$head_ref" + fi + + if [ "$failed" -eq 0 ]; then + echo "auto-fix PR e2e PASSED" + fi + exit "$failed" diff --git a/src/e2e_autofix_pr_demo.cpp b/src/e2e_autofix_pr_demo.cpp new file mode 100644 index 0000000..38b08a5 --- /dev/null +++ b/src/e2e_autofix_pr_demo.cpp @@ -0,0 +1,2 @@ +#include +int main( ){int x=0 ;for(;;){break;}printf("Hello from PR!\n") ;return x;} From b658f069e265dfc21995e790f4908242bfaf274e Mon Sep 17 00:00:00 2001 From: shenxianpeng <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:36:36 +0000 Subject: [PATCH 2/7] style: apply clang-format fixes --- src/e2e_autofix_pr_demo.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/e2e_autofix_pr_demo.cpp b/src/e2e_autofix_pr_demo.cpp index 38b08a5..8fec1fe 100644 --- a/src/e2e_autofix_pr_demo.cpp +++ b/src/e2e_autofix_pr_demo.cpp @@ -1,2 +1,10 @@ #include -int main( ){int x=0 ;for(;;){break;}printf("Hello from PR!\n") ;return x;} +int main() +{ + int x = 0; + for (;;) { + break; + } + printf("Hello from PR!\n"); + return x; +} From 8ca581fff43a72a4a05d1721fc58a0cd127f4333 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 20 Aug 2026 01:58:17 +0300 Subject: [PATCH 3/7] test: cover clang-tidy alongside auto-fix The previous run passed with `tidy-checks: '-*'`, which meant it only ever exercised the clang-format half. That hid a real defect: `--fix` rewrites the file in place, but clang-tidy reads line numbers off the file on disk while its `--line-filter` is keyed to the diff, so every diagnostic after a reflowed line drifted out of the filter. The job now lints twice with identical inputs -- once with auto-fix off to establish a baseline, once with it on -- and asserts the clang-tidy count is unchanged. Expressing it as a comparison rather than a hard-coded number keeps it stable across clang releases. The fixture is rewritten so formatting it expands 12 lines to 28, which is what makes the drift observable, and `lines-changed-only` is now true so the `--line-filter` is actually in play. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/auto-fix-pr-e2e.yml | 105 ++++++++++++++++++++++++-- src/e2e_autofix_pr_demo.cpp | 22 +++--- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/.github/workflows/auto-fix-pr-e2e.yml b/.github/workflows/auto-fix-pr-e2e.yml index 8ece82b..c052411 100644 --- a/.github/workflows/auto-fix-pr-e2e.yml +++ b/.github/workflows/auto-fix-pr-e2e.yml @@ -9,6 +9,7 @@ name: auto-fix PR e2e # the runner off the PR *merge* ref and onto the PR *head* branch # - the same-repository gating that gets applied to that step # - pushing the fix back onto the PR head branch, so the PR updates itself +# - that applying fixes does not disturb what clang-tidy reports # # As in phase 1, neither cpp-linter-action#443 nor cpp-linter#202 needs to be # merged: the action is consumed from its PR branch as a local action, and the @@ -30,6 +31,10 @@ env: TEST_FILE: src/e2e_autofix_pr_demo.cpp CLANG_VERSION: '18' COMMIT_MSG: 'style: apply clang-format fixes' + # Two long-standing checks with stable behaviour across clang releases. The + # fixture triggers both, and `-*` keeps the repo's .clang-tidy out of it so + # the counts below don't drift when that file changes. + TIDY_CHECKS: '-*,readability-magic-numbers,modernize-use-nullptr' jobs: auto-fix-pr: @@ -76,17 +81,48 @@ jobs: run: | set -euo pipefail echo "::group::fixture before" - cat "$TEST_FILE" + cat -n "$TEST_FILE" echo "::endgroup::" { echo "MALFORMED_BLOB=$(git hash-object "$TEST_FILE")" echo "PR_HEAD_SHA=${{ github.event.pull_request.head.sha }}" } >> "$GITHUB_ENV" + # Baseline: the same lint, with auto-fix off. This measures what + # clang-tidy reports against the file as the diff describes it, and is + # the reference the auto-fix run below has to reproduce. + # # `files-changed-only: true` is deliberate and load-bearing: src/demo.cpp # and src/demo.hpp are intentionally unformatted fixtures of this repo, and # scanning everything would make auto-fix "repair" and commit them too. - # clang-tidy is off so this measures the clang-format path only. + - name: Lint without auto-fix (baseline) + uses: ./.action-under-test + id: baseline + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: file + tidy-checks: ${{ env.TIDY_CHECKS }} + extra-args: '-std=c++17' + files-changed-only: true + lines-changed-only: true + ignore: build|.action-under-test + version: '18' + verbosity: debug + thread-comments: false + step-summary: false + auto-fix: false + + - name: The baseline must not have touched the fixture + run: | + set -euo pipefail + if [ "$(git hash-object "$TEST_FILE")" != "$MALFORMED_BLOB" ]; then + echo "::error title=Baseline mutated the fixture::auto-fix was off, but $TEST_FILE changed." + exit 1 + fi + echo "PASS: baseline left the fixture malformed, as expected" + + # The real run. Same inputs, auto-fix on. - name: Run cpp-linter with auto-fix uses: ./.action-under-test id: linter @@ -94,26 +130,73 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: style: file - tidy-checks: '-*' + tidy-checks: ${{ env.TIDY_CHECKS }} + extra-args: '-std=c++17' files-changed-only: true - lines-changed-only: false + lines-changed-only: true ignore: build|.action-under-test version: '18' verbosity: debug + thread-comments: false + step-summary: false auto-fix: true auto-fix-commit-msg: 'style: apply clang-format fixes' - name: Verify the PR branch was fixed and updated + env: + BASE_FORMAT: ${{ steps.baseline.outputs.clang-format-checks-failed }} + BASE_TIDY: ${{ steps.baseline.outputs.clang-tidy-checks-failed }} + FIX_FORMAT: ${{ steps.linter.outputs.clang-format-checks-failed }} + FIX_TIDY: ${{ steps.linter.outputs.clang-tidy-checks-failed }} run: | set -uo pipefail failed=0 head_ref='${{ github.event.pull_request.head.ref }}' echo "::group::fixture after" - cat "$TEST_FILE" + cat -n "$TEST_FILE" echo "::endgroup::" - # The action must have moved the runner onto the PR head branch. + echo "baseline: clang-format=$BASE_FORMAT clang-tidy=$BASE_TIDY" + echo "auto-fix: clang-format=$FIX_FORMAT clang-tidy=$FIX_TIDY" + + # --- sanity: the fixture has to actually exercise both tools --- + if [ "${BASE_FORMAT:-0}" -le 0 ]; then + echo "::error title=Fixture not malformed::the baseline found no clang-format issues, so this run proves nothing." + failed=1 + else + echo "PASS: baseline saw $BASE_FORMAT clang-format issue(s)" + fi + + if [ "${BASE_TIDY:-0}" -le 0 ]; then + echo "::error title=No tidy coverage::the baseline found no clang-tidy issues, so the comparison below proves nothing." + failed=1 + else + echo "PASS: baseline saw $BASE_TIDY clang-tidy diagnostic(s)" + fi + + # --- auto-fix cleared the format issues --- + if [ "${FIX_FORMAT:-1}" -ne 0 ]; then + echo "::error title=Format issues remain::auto-fix ran but still reports $FIX_FORMAT clang-format issue(s)." + failed=1 + else + echo "PASS: auto-fix cleared all clang-format issues" + fi + + # --- the point of the tidy coverage --- + # clang-format's `-i` rewrites the file. clang-tidy reports line + # numbers from the file on disk, but its --line-filter and the review + # comments built from its output are keyed to the diff. If the tools + # run in the wrong order, diagnostics past a reflowed line drift out + # of the filter and silently disappear. + if [ "${FIX_TIDY:-0}" -ne "${BASE_TIDY:-0}" ]; then + echo "::error title=Tidy diagnostics drifted::auto-fix changed the clang-tidy count from $BASE_TIDY to $FIX_TIDY. Applying format fixes must not affect what clang-tidy reports." + failed=1 + else + echo "PASS: clang-tidy still reports $FIX_TIDY diagnostic(s); auto-fix did not shift them" + fi + + # --- the pull-request plumbing --- current="$(git rev-parse --abbrev-ref HEAD)" if [ "$current" != "$head_ref" ]; then echo "::error title=Wrong branch::expected to be on '$head_ref', but HEAD is '$current'" @@ -147,6 +230,16 @@ jobs: echo "PASS: commit message matches auto-fix-commit-msg" fi + # Only the fixture may appear in the auto-fix commit. + touched="$(git diff --name-only "$PR_HEAD_SHA" HEAD)" + if [ "$touched" != "$TEST_FILE" ]; then + echo "::error title=Unexpected files committed::auto-fix committed more than the fixture:" + echo "$touched" + failed=1 + else + echo "PASS: the auto-fix commit contains only $TEST_FILE" + fi + fmt="$(command -v "clang-format-${CLANG_VERSION}" || command -v clang-format || true)" if [ -n "$fmt" ]; then if "$fmt" --style=file --dry-run --Werror "$TEST_FILE"; then diff --git a/src/e2e_autofix_pr_demo.cpp b/src/e2e_autofix_pr_demo.cpp index 8fec1fe..7d0cbdd 100644 --- a/src/e2e_autofix_pr_demo.cpp +++ b/src/e2e_autofix_pr_demo.cpp @@ -1,10 +1,12 @@ -#include -int main() -{ - int x = 0; - for (;;) { - break; - } - printf("Hello from PR!\n"); - return x; -} +// Deliberately malformed fixture for the auto-fix e2e test. +// +// Two properties matter here: +// 1. clang-format has plenty to fix, so auto-fix has something to commit. +// 2. Formatting it changes its line count (the one-liners below expand), +// which is what makes it able to catch clang-tidy diagnostics drifting +// off the diff when the tools run in the wrong order. +int accumulate( ){int a=10;int b=20;int c=30;int d=40;int e=50;return a+b+c+d+e;} + +int magic_user( ){int v=42;return v;} + +int * null_user( ){int * p=0;return p;} From a8d79e9a8f5a9385e377953a62b4a394fe7f306f Mon Sep 17 00:00:00 2001 From: shenxianpeng <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:59:15 +0000 Subject: [PATCH 4/7] style: apply clang-format fixes --- src/e2e_autofix_pr_demo.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/e2e_autofix_pr_demo.cpp b/src/e2e_autofix_pr_demo.cpp index 7d0cbdd..874fb59 100644 --- a/src/e2e_autofix_pr_demo.cpp +++ b/src/e2e_autofix_pr_demo.cpp @@ -5,8 +5,24 @@ // 2. Formatting it changes its line count (the one-liners below expand), // which is what makes it able to catch clang-tidy diagnostics drifting // off the diff when the tools run in the wrong order. -int accumulate( ){int a=10;int b=20;int c=30;int d=40;int e=50;return a+b+c+d+e;} +int accumulate() +{ + int a = 10; + int b = 20; + int c = 30; + int d = 40; + int e = 50; + return a + b + c + d + e; +} -int magic_user( ){int v=42;return v;} +int magic_user() +{ + int v = 42; + return v; +} -int * null_user( ){int * p=0;return p;} +int* null_user() +{ + int* p = 0; + return p; +} From b7df87fa3721f9dad91b7885daa1e92238e00626 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Wed, 19 Aug 2026 23:00:30 +0000 Subject: [PATCH 5/7] test: make the auto-fix PR check re-run safe Pushing the fix back onto the pull request raises a synchronize event, so this workflow runs again against its own auto-fix commit. The fixture is already clean by then and doing nothing is the correct behaviour, but the checks demanded another rewrite and another commit, so every second run failed -- as it just did on b658f06. Pick the assertions from the baseline's own clang-format count rather than from the fixture's contents, so this keeps working whatever the fixture is later rewritten to contain. The already-clean case now carries its own meaning: auto-fix must leave the file byte-identical and must not manufacture an empty commit. The clang-tidy drift comparison, the tidy coverage floor and the collateral-damage checks are unchanged and still run in both states. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012uXrN1wk5EaqK5GimN3deT --- .github/workflows/auto-fix-pr-e2e.yml | 94 +++++++++++++++++---------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/.github/workflows/auto-fix-pr-e2e.yml b/.github/workflows/auto-fix-pr-e2e.yml index c052411..a13cc8b 100644 --- a/.github/workflows/auto-fix-pr-e2e.yml +++ b/.github/workflows/auto-fix-pr-e2e.yml @@ -160,12 +160,21 @@ jobs: echo "baseline: clang-format=$BASE_FORMAT clang-tidy=$BASE_TIDY" echo "auto-fix: clang-format=$FIX_FORMAT clang-tidy=$FIX_TIDY" - # --- sanity: the fixture has to actually exercise both tools --- - if [ "${BASE_FORMAT:-0}" -le 0 ]; then - echo "::error title=Fixture not malformed::the baseline found no clang-format issues, so this run proves nothing." - failed=1 - else + # --- which of the two states is this run in? --- + # Pushing the fix raises a `synchronize` event, so this workflow runs a + # second time against its own auto-fix commit. The fixture is clean by + # then and doing nothing is the correct behaviour, so that run asserts + # idempotency instead of demanding another fix. The baseline's own + # clang-format count says which state we are in, so this stays true + # whatever the fixture is later rewritten to contain. + if [ "${BASE_FORMAT:-0}" -gt 0 ]; then + expect_fix=true echo "PASS: baseline saw $BASE_FORMAT clang-format issue(s)" + else + expect_fix=false + echo "NOTE: the fixture is already formatted, so this is the re-run" + echo " that auto-fix's own push triggered. Asserting that it is" + echo " a no-op rather than expecting another fix." fi if [ "${BASE_TIDY:-0}" -le 0 ]; then @@ -205,39 +214,58 @@ jobs: echo "PASS: runner switched from the merge ref onto '$head_ref'" fi - if [ "$(git hash-object "$TEST_FILE")" = "$MALFORMED_BLOB" ]; then - echo "::error title=Not reformatted::$TEST_FILE is unchanged; --fix did not rewrite it." - failed=1 - else - echo "PASS: fixture was reformatted" - fi - head_sha="$(git rev-parse HEAD)" subject="$(git log -1 --pretty=%s)" - if [ "$head_sha" = "$PR_HEAD_SHA" ]; then - echo "::error title=No commit::auto-fix produced no commit on the PR branch." - failed=1 - else - echo "PASS: auto-fix commit $head_sha" - echo " subject: $subject" - echo " author: $(git log -1 --pretty='%an <%ae>')" - fi - if [ "$subject" != "$COMMIT_MSG" ]; then - echo "::error title=Wrong commit message::expected '$COMMIT_MSG', got '$subject'" - failed=1 - else - echo "PASS: commit message matches auto-fix-commit-msg" - fi + if [ "$expect_fix" = "true" ]; then + if [ "$(git hash-object "$TEST_FILE")" = "$MALFORMED_BLOB" ]; then + echo "::error title=Not reformatted::$TEST_FILE is unchanged; --fix did not rewrite it." + failed=1 + else + echo "PASS: fixture was reformatted" + fi - # Only the fixture may appear in the auto-fix commit. - touched="$(git diff --name-only "$PR_HEAD_SHA" HEAD)" - if [ "$touched" != "$TEST_FILE" ]; then - echo "::error title=Unexpected files committed::auto-fix committed more than the fixture:" - echo "$touched" - failed=1 + if [ "$head_sha" = "$PR_HEAD_SHA" ]; then + echo "::error title=No commit::auto-fix produced no commit on the PR branch." + failed=1 + else + echo "PASS: auto-fix commit $head_sha" + echo " subject: $subject" + echo " author: $(git log -1 --pretty='%an <%ae>')" + fi + + if [ "$subject" != "$COMMIT_MSG" ]; then + echo "::error title=Wrong commit message::expected '$COMMIT_MSG', got '$subject'" + failed=1 + else + echo "PASS: commit message matches auto-fix-commit-msg" + fi + + # Only the fixture may appear in the auto-fix commit. + touched="$(git diff --name-only "$PR_HEAD_SHA" HEAD)" + if [ "$touched" != "$TEST_FILE" ]; then + echo "::error title=Unexpected files committed::auto-fix committed more than the fixture:" + echo "$touched" + failed=1 + else + echo "PASS: the auto-fix commit contains only $TEST_FILE" + fi else - echo "PASS: the auto-fix commit contains only $TEST_FILE" + # Idempotency: with nothing left to fix, auto-fix must not rewrite + # the file and must not manufacture an empty commit. + if [ "$(git hash-object "$TEST_FILE")" != "$MALFORMED_BLOB" ]; then + echo "::error title=Needless rewrite::auto-fix modified an already-formatted file." + failed=1 + else + echo "PASS: already-clean fixture was left byte-identical" + fi + + if [ "$head_sha" != "$PR_HEAD_SHA" ]; then + echo "::error title=Empty commit::auto-fix committed $head_sha despite having nothing to fix." + failed=1 + else + echo "PASS: no commit was created when there was nothing to fix" + fi fi fmt="$(command -v "clang-format-${CLANG_VERSION}" || command -v clang-format || true)" From 1748a81db205c40048c9e7ca5a22a10a13893c89 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 20 Aug 2026 02:11:32 +0300 Subject: [PATCH 6/7] test: cover format-review alongside auto-fix Asking for a clang-format review while auto-fix is on used to abort cpp-linter outright: AssertionError: FormatAdvice has no suggestions for A fixed file has nothing left to report, but the review pass still walks it and wants a patch to diff against. The combination is a natural one to reach for -- suggestions for what you have to fix by hand, auto-fix for what you don't -- so it is worth a check. Nothing gets posted: the file comes out clean and `no-lgtm` defaults to true, so the review is suppressed. The step running at all is the assertion. The fixture goes back to its malformed state so this run exercises the whole path rather than the already-clean re-run. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/auto-fix-pr-e2e.yml | 11 +++++++++++ src/e2e_autofix_pr_demo.cpp | 22 +++------------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/.github/workflows/auto-fix-pr-e2e.yml b/.github/workflows/auto-fix-pr-e2e.yml index a13cc8b..9cf04a6 100644 --- a/.github/workflows/auto-fix-pr-e2e.yml +++ b/.github/workflows/auto-fix-pr-e2e.yml @@ -24,6 +24,8 @@ on: permissions: contents: write + # `format-review` on the auto-fix run below needs this. + pull-requests: write env: CPP_LINTER_REF: 'feature/auto-fix' @@ -141,6 +143,15 @@ jobs: step-summary: false auto-fix: true auto-fix-commit-msg: 'style: apply clang-format fixes' + # Only this run asks for a clang-format review, and only this run can + # hit the bug it covers: a fixed file has no advice left to report, + # but the review pass still walks it and wants a patch to diff + # against. Handing it an empty one used to abort cpp-linter with + # `AssertionError: FormatAdvice has no suggestions for `, so + # this step failing at all is the assertion. Nothing is posted -- + # the file comes out clean, and `no-lgtm` defaults to true. + format-review: true + passive-reviews: true - name: Verify the PR branch was fixed and updated env: diff --git a/src/e2e_autofix_pr_demo.cpp b/src/e2e_autofix_pr_demo.cpp index 874fb59..7d0cbdd 100644 --- a/src/e2e_autofix_pr_demo.cpp +++ b/src/e2e_autofix_pr_demo.cpp @@ -5,24 +5,8 @@ // 2. Formatting it changes its line count (the one-liners below expand), // which is what makes it able to catch clang-tidy diagnostics drifting // off the diff when the tools run in the wrong order. -int accumulate() -{ - int a = 10; - int b = 20; - int c = 30; - int d = 40; - int e = 50; - return a + b + c + d + e; -} +int accumulate( ){int a=10;int b=20;int c=30;int d=40;int e=50;return a+b+c+d+e;} -int magic_user() -{ - int v = 42; - return v; -} +int magic_user( ){int v=42;return v;} -int* null_user() -{ - int* p = 0; - return p; -} +int * null_user( ){int * p=0;return p;} From 46eea4759134c2c98efefc12befb251a5b7dffaa Mon Sep 17 00:00:00 2001 From: shenxianpeng <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:14:14 +0000 Subject: [PATCH 7/7] style: apply clang-format fixes --- src/e2e_autofix_pr_demo.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/e2e_autofix_pr_demo.cpp b/src/e2e_autofix_pr_demo.cpp index 7d0cbdd..874fb59 100644 --- a/src/e2e_autofix_pr_demo.cpp +++ b/src/e2e_autofix_pr_demo.cpp @@ -5,8 +5,24 @@ // 2. Formatting it changes its line count (the one-liners below expand), // which is what makes it able to catch clang-tidy diagnostics drifting // off the diff when the tools run in the wrong order. -int accumulate( ){int a=10;int b=20;int c=30;int d=40;int e=50;return a+b+c+d+e;} +int accumulate() +{ + int a = 10; + int b = 20; + int c = 30; + int d = 40; + int e = 50; + return a + b + c + d + e; +} -int magic_user( ){int v=42;return v;} +int magic_user() +{ + int v = 42; + return v; +} -int * null_user( ){int * p=0;return p;} +int* null_user() +{ + int* p = 0; + return p; +}