diff --git a/.gitea/workflows/sonarqube.yml b/.gitea/workflows/sonarqube.yml index d2c7310..252637f 100755 --- a/.gitea/workflows/sonarqube.yml +++ b/.gitea/workflows/sonarqube.yml @@ -200,8 +200,25 @@ jobs: # sonar.qualitygate.wait, so a degraded run marks the dashboard and is # overwritten by the next good one rather than blocking anything; and the # job fails regardless, so no run in this state reads as clean. + # Not on pull requests. SonarQube Community has no branch analysis: every + # scan published under a project key replaces that project's single + # analysis, whatever revision it came from. So a pull request scan + # overwrote the dashboard's picture of main with the branch, silently, and + # the new-code period, gate result, coverage and hotspot list then all + # described whatever was scanned last with nothing saying which revision + # that was. #197 caught it in the act — the dashboard describing a feature + # branch while reporting hotspot line numbers that landed on a blank line + # in main. + # + # scripts/scan-local.sh already refuses to do this, defaulting to a + # scratch key for exactly this reason. CI walked into the hazard that + # script guards against; now the two tell the same story. + # + # The suites above still run on pull requests, which is where their value + # is. Only publishing is restricted. - name: SonarQube Scan id: scan + if: github.event_name != 'pull_request' continue-on-error: true uses: sonarsource/sonarqube-scan-action@v4 env: @@ -238,8 +255,11 @@ jobs: # every path, so it cannot fail the job anyway, and guarding it would # oblige it to appear in the gate below — which exists to fail the job, # the opposite of what a report should do. See #261. + # Skipped alongside the scan on pull requests. With nothing published, this + # would report main's numbers under a pull request's log, which is noise + # at best and misread as the branch's own at worst. - name: Report SonarQube measures - if: always() + if: always() && github.event_name != 'pull_request' env: SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/backend/tests/unit/workflowGate.test.ts b/backend/tests/unit/workflowGate.test.ts index f8062d5..531960c 100644 --- a/backend/tests/unit/workflowGate.test.ts +++ b/backend/tests/unit/workflowGate.test.ts @@ -29,6 +29,7 @@ interface Step { name: string; id: string | null; guarded: boolean; + condition: string | null; } /** @@ -51,7 +52,7 @@ function parseSteps(source: string): Step[] { for (const line of source.split(/\r?\n/)) { const named = /^ {6}- name: (.+?)\s*$/.exec(line); if (named?.[1] !== undefined) { - current = { name: named[1], id: null, guarded: false }; + current = { name: named[1], id: null, guarded: false, condition: null }; steps.push(current); continue; } @@ -61,6 +62,9 @@ function parseSteps(source: string): Step[] { if (id?.[1] !== undefined) current.id = id[1]; if (/^ {8}continue-on-error: true\s*$/.test(line)) current.guarded = true; + + const condition = /^ {8}if: (.+?)\s*$/.exec(line); + if (condition?.[1] !== undefined) current.condition = condition[1]; } return steps; @@ -181,3 +185,35 @@ describe('sonarqube.yml fails at the end rather than part way through', () => { expect(unguarded.map((step) => step.name)).toEqual([]); }); }); + +/** + * The guard for #197. + * + * SonarQube Community has no branch analysis: every scan published under a + * project key replaces that project's single analysis, whatever revision it came + * from. Publishing from a pull request therefore overwrites the dashboard's + * picture of main with the branch — silently, because nothing on the dashboard + * says which revision it describes. It was caught only because hotspot line + * numbers landed on a blank line. + * + * scripts/scan-local.sh has always refused to do this, defaulting to a scratch + * key. This asserts CI refuses too, because the failure leaves no trace and the + * `if:` is one line for somebody to drop. + */ +describe('publishing is restricted to non-pull-request runs', () => { + it.each(['SonarQube Scan', 'Report SonarQube measures'])('%s does not run on a pull request', (name) => { + const step = steps.find((candidate) => candidate.name === name); + + expect(step).toBeDefined(); + expect(step?.condition).toContain("github.event_name != 'pull_request'"); + }); + + // The suites are the reason pull requests run this workflow at all. Gating + // them would turn a fix for a reporting problem into a loss of every check. + it.each(['unit', 'integration', 'e2e'])('the %s suite still runs on pull requests', (id) => { + const step = steps.find((candidate) => candidate.id === id); + + expect(step).toBeDefined(); + expect(step?.condition ?? '').not.toContain('pull_request'); + }); +});