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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 36 additions & 94 deletions .github/workflows/code_quality.yml
Original file line number Diff line number Diff line change
@@ -1,60 +1,61 @@
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) - 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:
Comment thread
kploch marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
push:
branches:
- main
- 'releases/*'
Comment thread
kploch marked this conversation as resolved.

jobs:
qodana:
Comment thread
kploch marked this conversation as resolved.
# 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.
# 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
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
Comment thread
kploch marked this conversation as resolved.
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.
Expand All @@ -64,84 +65,25 @@ 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
# `../mrploch-development/...` imports and the `../ploch-common/...` project
# references resolve to paths that do not exist inside the container and the
# clones are effectively invisible. Mounting them at /data/<name> makes
# /data/project/../<name> 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
Comment thread
kploch marked this conversation as resolved.
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 }}
Comment thread
kploch marked this conversation as resolved.
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."
8 changes: 7 additions & 1 deletion qodana.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading