From 6d7405ebd69d1e58cc6d0c063e95906426ac4ae1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 06:32:38 +0000 Subject: [PATCH] ci(spotbugs): publish one SARIF run per code-scanning category The weekly SpotBugs job gathered every module's spotbugsSarif.json into one directory and uploaded it under the single category `spotbugs`. Each run names the same SpotBugs tool driver and carried no automation id, so code scanning saw seven runs it could not tell apart in one delivery and refused the upload: "The CodeQL Action does not support uploading multiple SARIF runs with the same category." Stamp each file's runs with the module's own automationDetails.id (`spotbugs//`) while collecting them, which is what the category input sets and what code scanning keys an analysis on. upload-sarif fills that id in only where none is set, so the per-module ids survive and the category input is no longer needed. The module reports stay in one upload and one job; the findings keep their repository-relative paths, so attribution is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L3wQdJ9u1p1KkcHM7wfc1n --- .github/workflows/spotbugs.yml | 15 +++++++++++++-- AGENTS.md | 8 ++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/spotbugs.yml b/.github/workflows/spotbugs.yml index abca5e07..6a4967f2 100644 --- a/.github/workflows/spotbugs.yml +++ b/.github/workflows/spotbugs.yml @@ -46,18 +46,29 @@ jobs: # Each module writes its own target/spotbugsSarif.json. upload-sarif reads # a directory only for files named *.sarif, so they are gathered under one # directory and renamed per module to keep the findings attributable. + # + # Every module's run carries the same SpotBugs tool driver, so code + # scanning would see seven indistinguishable runs in one delivery and + # reject the upload ("multiple SARIF runs with the same category"). The + # run's category is its automationDetails.id, so each file gets the + # module's own — which upload-sarif then leaves alone, since it only fills + # in an id where none is set. One category per module, one run per + # category. run: | mkdir -p target/sarif find . -path '*/target/spotbugsSarif.json' -print0 | while IFS= read -r -d '' report; do module=$(echo "${report}" | sed -e 's|^\./||' -e 's|/target/spotbugsSarif\.json$||' -e 's|/|-|g') - cp "${report}" "target/sarif/${module}.sarif" + jq --arg id "spotbugs/${module}/" '.runs[].automationDetails.id = $id' \ + "${report}" > "target/sarif/${module}.sarif" done echo "Collected $(find target/sarif -name '*.sarif' | wc -l) SARIF reports." - name: Publish SARIF to code scanning + # No category input: it would only apply to runs that have no + # automationDetails.id of their own, and the step above gives every run + # the per-module id that keeps the delivery's runs distinct. uses: github/codeql-action/upload-sarif@988661ebb5e81487b3fb31b2185d2856c0a10679 # v4 with: sarif_file: target/sarif - category: spotbugs - name: Upload SpotBugs reports uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: diff --git a/AGENTS.md b/AGENTS.md index 50119968..57788079 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -779,8 +779,12 @@ module's `*IT`s stay unrun until it gets its own copy. Run them with has read it, and it is **report-only**: the tree carries findings today, so a `check` goal would fail every build instead of surfacing them. `spotbugs.yml` publishes the SARIF to the code-scanning tab, which is where the findings are - meant to be triaged. The same two generated-code modules opt out with - `spotbugs.skip`, for the same reason they set `jacoco.skip` and `pitest.skip`. + meant to be triaged. It publishes each module's report under its own category, + `spotbugs//`, written into the run's `automationDetails.id` before the + upload: every module's run names the same SpotBugs driver, and code scanning + rejects a delivery holding two runs it cannot tell apart. The same two + generated-code modules opt out with `spotbugs.skip`, for the same reason they + set `jacoco.skip` and `pitest.skip`. A finding that has been decided about rather than fixed is excluded in the module's own filter file, wired in from the module pom — today only