From 4130326b8b51ea8c26cc5b1a8989fd105799c19e Mon Sep 17 00:00:00 2001 From: Krzysztof Ploch Date: Sat, 12 Sep 2026 13:34:04 +0200 Subject: [PATCH 1/3] ci(qodana): Switch to the Community .NET linter to clear the licence block jetbrains/qodana-dotnet ("Qodana for .NET") requires a paid Qodana licence that the mrploch organisation's Community plan does not include, so every recorded scan failed a licence check before the solution was even opened. ploch-common hit the same wall and resolved it by switching to jetbrains/qodana-cdnet (Qodana Community for .NET), which has been green on every push/PR there since 2026-09-04. Same linter tag applied here. Left the workflow on workflow_dispatch pending a confirmed green run with the new linter before re-enabling pull_request/push triggers. Refs: #151 --- .github/workflows/code_quality.yml | 21 ++++++++++----------- qodana.yaml | 8 +++++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 6586460a..2a0bc68a 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -1,20 +1,19 @@ name: Qodana # -# KNOWN BLOCKER - this workflow cannot currently produce results, and the cause is a -# licence, not a configuration defect. Dispatch 33428446669 on this branch (the first run -# ever to get past solution loading) failed with: +# FORMER BLOCKER (issue #151) - every scan used to fail a licence check before the +# solution was even opened: # # Your Qodana Cloud organization has Community license that doesn't support # "Qodana for .NET" linter, please try one of the community linters instead # -# The check happens before the solution is opened, so no SARIF can be produced no matter -# how the scan is configured. Everything below - the correct solution file, the sibling -# clones and their container mounts, the private-feed credentials, the exclude paths - is -# necessary for a scan that runs, but none of it is sufficient while the linter itself is -# unlicensed. Resolving that (a paid Qodana licence, or moving to a community linter) is -# tracked separately; see issue #138. The observability wiring below is what surfaced this -# at all: the job stays green, emits warnings and reports the scan outcome instead of -# failing silently. +# qodana.yaml pointed at `jetbrains/qodana-dotnet`, the paid "Qodana for .NET" linter, +# which the mrploch organisation's Community licence does not cover. Switched to +# `jetbrains/qodana-cdnet` (Community .NET), the same linter ploch-common uses +# successfully on every push/PR. Left on workflow_dispatch until a scan with the new +# linter is confirmed to actually complete and upload a SARIF; once that is verified, +# add `pull_request` / `push` triggers to match ploch-common and this workflow's +# non-blocking observability scaffolding (continue-on-error, the SARIF-existence gate, +# the outcome warning) can be simplified away. on: workflow_dispatch: diff --git a/qodana.yaml b/qodana.yaml index 0e4b63d2..740a93e3 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -1,5 +1,11 @@ version: "1.0" -linter: jetbrains/qodana-dotnet:latest +# jetbrains/qodana-dotnet is the paid "Qodana for .NET" linter, which the mrploch +# organisation's Community licence does not cover (issue #151) - every scan failed a +# licence check before the solution was even opened. jetbrains/qodana-cdnet is the free +# Community .NET linter and is what ploch-common uses successfully (its +# qodana_code_quality.yml has been green on every push/PR since 2026-09-04). Same linter +# tag pinned here for consistency across the two repos. +linter: jetbrains/qodana-cdnet:2026.1 dotnet: # This was `Ploch.Common.sln` - a solution file that has never existed in this repository; # it was copied verbatim from a sibling repo. Qodana could not open it, which is why the From b32905279703c3bc1049d3ecda2a0bcd67a8efbd Mon Sep 17 00:00:00 2001 From: Krzysztof Ploch Date: Sat, 12 Sep 2026 13:39:11 +0200 Subject: [PATCH 2/3] ci(qodana): Drop the code-scanning upload, mirror ploch-common's structure Dispatch 34691387171 confirmed the Community linter switch clears the licence block (Qodana Scan step itself now succeeds), but then failed at the github/codeql-action/upload-sarif step with "GITHUB_REF environment variable must be set" and nothing in this repo's own configuration can fix that context-detection error. ploch-common's working qodana_code_quality.yml never had this step: Qodana reports directly to Qodana Cloud via QODANA_TOKEN, which is also what registers/configures the project there in the first place. Removed the upload-sarif step and the SARIF-existence/outcome scaffolding it needed, added QODANA_ENDPOINT to match ploch-common, re-enabled pull_request/push triggers, and trimmed permissions to contents: read now that security-events: write is no longer needed. Refs: #151 --- .github/workflows/code_quality.yml | 113 ++++++----------------------- 1 file changed, 24 insertions(+), 89 deletions(-) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 2a0bc68a..012629a8 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -8,52 +8,46 @@ name: Qodana # # qodana.yaml pointed at `jetbrains/qodana-dotnet`, the paid "Qodana for .NET" linter, # which the mrploch organisation's Community licence does not cover. Switched to -# `jetbrains/qodana-cdnet` (Community .NET), the same linter ploch-common uses -# successfully on every push/PR. Left on workflow_dispatch until a scan with the new -# linter is confirmed to actually complete and upload a SARIF; once that is verified, -# add `pull_request` / `push` triggers to match ploch-common and this workflow's -# non-blocking observability scaffolding (continue-on-error, the SARIF-existence gate, -# the outcome warning) can be simplified away. +# `jetbrains/qodana-cdnet` (Community .NET) - confirmed by dispatch 34691387171 to get +# past the licence check, restore, build and produce a report. +# +# Also dropped the github/codeql-action/upload-sarif step that same run then failed on +# ("GITHUB_REF environment variable must be set") and the bespoke SARIF-existence/outcome +# scaffolding it needed. ploch-common's working qodana_code_quality.yml never had that +# step either - Qodana reports straight to Qodana Cloud via QODANA_TOKEN, which is also +# what registers the project there (it showed "not configured" on qodana.cloud only +# because no scan had ever completed with a licensed linter to report in). Re-add a +# GitHub code-scanning upload only if that is genuinely wanted in addition to Qodana +# Cloud, with `ref`/`sha` passed explicitly to satisfy upload-sarif's context detection. on: workflow_dispatch: + pull_request: + push: + branches: + - main + - 'releases/*' jobs: qodana: # A cold container scan of this solution is not a fast job: a plain # `dotnet build -c Release` of Ploch.Data.slnx takes ~12 minutes locally, and Qodana # additionally pulls the linter image, restores, and runs InspectCode over everything. - # A job-level timeout CANCELS the job, and cancellation skips `if: always()` steps - - # which would silently defeat the SARIF observability this workflow exists for - # (issue #138). Tighten this only once a successful dispatch has reported a real - # duration. - timeout-minutes: 60 + timeout-minutes: 30 runs-on: ubuntu-latest - # Least privilege (issue #128). Declaring any permissions block drops every scope not - # listed to none, so each entry below is load-bearing: - # contents: read - checkout of this repository. Nothing in this workflow - # pushes, which is also why the checkout below sets - # persist-credentials: false. - # security-events: write - required by github/codeql-action/upload-sarif below; - # without it the SARIF upload fails with 403 and the Qodana - # results never reach code scanning. - # `pull-requests: write` is deliberately NOT granted: the trigger is - # workflow_dispatch-only, so the Qodana action's PR-comment path can never engage. - # Re-add it if and when a pull_request trigger is introduced. permissions: contents: read - security-events: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} # check out the actual PR commit, not the merge commit + fetch-depth: 0 # full history is required for pull request analysis # Nothing in this workflow pushes, so the job token has no reason to stay in # .git/config. persist-credentials: false # Ploch.Data.slnx references three projects under ../ploch-common by relative path, # and Directory.Packages.props imports five .props files from ../mrploch-development - # unconditionally. Without these clones the solution cannot be evaluated at all, so - # pointing Qodana at the right solution file is necessary but not sufficient. The + # unconditionally. Without these clones the solution cannot be evaluated at all. The # clone depths deliberately match build-dotnet.yml and release.yml: mrploch-development # supplies only .props files and is never built, but ploch-common IS built and uses # Nerdbank.GitVersioning, which cannot compute commit height from a shallow clone. @@ -63,14 +57,9 @@ jobs: run: git clone https://github.com/mrploch/ploch-common.git ../ploch-common - name: 'Qodana Scan' - id: qodana uses: JetBrains/qodana-action@10be11607eb323a180e2b76b26c9c5cdceac3e77 # v2026.2.1 - # The scan is deliberately non-blocking so that a Qodana failure still lets the - # upload-sarif step below run and publish whatever SARIF was produced. Without this - # the job aborts here and the upload never becomes observable (issue #138). The - # step's own outcome is surfaced by the warning step that follows. - continue-on-error: true with: + pr-mode: false # Qodana analyses the project inside a container that bind-mounts only the project # directory at /data/project. The two sibling clones above live one level ABOVE # that directory on the runner, so without these extra mounts the unconditional @@ -78,69 +67,15 @@ jobs: # references resolve to paths that do not exist inside the container and the # clones are effectively invisible. Mounting them at /data/ makes # /data/project/../ resolve exactly as it does on the runner. - # - # No `--baseline` here on purpose. The previous value was - # `--baseline,./qodana.sarif.json`, pointing at a 4 MB SARIF checked in at the - # repository root that was produced by QDNET 231 against a *different* solution - # (Ploch.Common). A baseline from another project cannot suppress anything - # meaningful here and only risks failing the scan, so the run is unbaselined. - # Reinstate a baseline only after a green scan of this repository has produced one. args: -v,${{ github.workspace }}/../ploch-common:/data/ploch-common,-v,${{ github.workspace }}/../mrploch-development:/data/mrploch-development env: - # The secret is named QODANA_TOKEN_1681345019 in this repository; there is no secret - # called QODANA_TOKEN at either repository or organisation level, so the previous - # reference expanded to an empty string and the scan ran unauthenticated. The - # environment variable must keep its name — that is what the action reads. QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1681345019 }} + QODANA_ENDPOINT: 'https://qodana.cloud' # Ploch.Data.slnx includes five SampleApp projects whose `Ploch.*` PackageReferences # are source-mapped in NuGet.Config exclusively to the authenticated GitHub Packages - # feed, so a container with no credentials cannot restore them. Run 33428446669 - # confirmed this from the scanner's own diagnostics: "private NuGet feed detected - # (NuGet.Config). Please set QODANA_NUGET_USER, QODANA_NUGET_PASSWORD, - # QODANA_NUGET_URL and QODANA_NUGET_NAME". These are the vendor-supported variables - # for exactly that case, so they are used here rather than a bespoke solution filter. - # NOT YET PROVEN TO WORK: run 33429634184 still logged that warning with all four - # set, and the licence failure below kills the run before restore, so there is no - # way to tell whether the credentials are actually consumed. Re-check this the - # first time a scan gets past the licence check (issue #151). + # feed, so a container with no credentials cannot restore them. These are the + # vendor-supported variables for exactly that case (issue #151). QODANA_NUGET_USER: kploch QODANA_NUGET_PASSWORD: ${{ secrets.GH_PACKAGES_TOKEN }} QODANA_NUGET_URL: https://nuget.pkg.github.com/mrploch/index.json QODANA_NUGET_NAME: github - - # A scan that fails early enough - bad configuration, authentication, a container that - # never starts - produces no SARIF at all, and upload-sarif errors on a missing file. - # That would turn the whole point of `if: always()` into a different failure, so the - # upload is gated on the file actually existing and its absence is reported instead. - - name: Check whether a SARIF report was produced - id: sarif - if: always() - env: - SARIF: ${{ runner.temp }}/qodana/results/qodana.sarif.json - run: | - if [ -f "$SARIF" ]; then - echo "found=true" >> "$GITHUB_OUTPUT" - echo "SARIF report found at $SARIF" - else - echo "found=false" >> "$GITHUB_OUTPUT" - echo "::warning title=No SARIF produced::The Qodana scan produced no SARIF report at $SARIF, so there is nothing to upload to code scanning. Check the 'Qodana Scan' step log." - fi - - # `if: always()` is what makes this step observable at all. Until it was added the scan - # failed on every recorded run and this step was skipped, so no SARIF had ever reached - # code scanning and the pinned action version was unexercised (issue #138). Uploading - # a partial SARIF from a failed scan is strictly better than uploading nothing. - - name: Upload SARIF to code scanning - if: ${{ always() && steps.sarif.outputs.found == 'true' }} - uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 - with: - sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json - - # Keeps a failed scan visible rather than hiding it behind a green job. Not always(): - # that would fire a spurious warning on a run the user cancelled deliberately. - - name: Warn if the Qodana scan did not succeed - if: ${{ !cancelled() && steps.qodana.outcome != 'success' }} - env: - QODANA_OUTCOME: ${{ steps.qodana.outcome }} - run: | - echo "::warning title=Qodana scan did not succeed::The Qodana Scan step reported '$QODANA_OUTCOME'. Any SARIF it managed to produce has still been uploaded. Check the 'Qodana Scan' step log." From 488cad06fcd1304bb8f9458f75a83abd74d6d2dd Mon Sep 17 00:00:00 2001 From: Krzysztof Ploch Date: Sat, 12 Sep 2026 14:11:21 +0200 Subject: [PATCH 3/3] ci(qodana): Skip fork pull requests to avoid a guaranteed-empty-secret scan Multiple PR #190 reviewers (Sourcery, codeant-ai, qodo-code-review, CodeRabbit) flagged that a fork's pull_request event never receives repository secrets, so QODANA_TOKEN and QODANA_NUGET_PASSWORD would both expand to empty strings on a fork PR: the scan can't report to Qodana Cloud and can't restore the SampleApp projects from the authenticated GitHub Packages feed. Added a job-level guard skipping pull_request events whose head repo isn't this one. Refs: #151 --- .github/workflows/code_quality.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 012629a8..9b5c87f2 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -34,6 +34,14 @@ jobs: # additionally pulls the linter image, restores, and runs InspectCode over everything. timeout-minutes: 30 runs-on: ubuntu-latest + # A fork's `pull_request` event never receives repository secrets (GitHub strips them + # by design), so on a fork PR QODANA_TOKEN and QODANA_NUGET_PASSWORD would both expand + # to empty strings: the scan can't report to Qodana Cloud and can't restore the + # SampleApp projects from the authenticated GitHub Packages feed either. Skipping fork + # PRs here avoids a scan that is guaranteed to fail on missing credentials, and avoids + # ever handing PR-controlled MSBuild/restore code a live token from a fork's context + # (flagged by Sourcery, codeant-ai, qodo-code-review and CodeRabbit on PR #190). + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository permissions: contents: read steps: