Skip to content

feat(sonarcloud): decouple sonar.python.version from the build version - #291

Merged
williaby merged 6 commits into
mainfrom
feat/sonar-python-version-input
Aug 6, 2026
Merged

feat(sonarcloud): decouple sonar.python.version from the build version#291
williaby merged 6 commits into
mainfrom
feat/sonar-python-version-input

Conversation

@williaby

@williaby williaby commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

-Dsonar.python.version was wired to the python-version input, which also feeds actions/setup-python. setup-python accepts exactly one version, so a caller could never declare a support range to the analyzer, only the single version CI happens to build with.

That is not cosmetic. Sonar gates version-specific rules on all declared versions:

// PythonVersionUtils.areSourcePythonVersionsGreaterOrEqualThan
return !sourcePythonVersions.isEmpty() && sourcePythonVersions.stream()
  .allMatch(version -> version.compare(required.major(), required.minor()) >= 0);

Declaring one modern version therefore opens every version gate, and the analyzer raises rules for syntax the project cannot legally use.

Observed impact

On cyo-adventure, 5 PEP 695 findings (python:S6794 x4, python:S6796 x1) against a codebase whose requires-python is >=3.11 and whose compatibility matrix still runs a 3.11 leg, where type X = ... is a SyntaxError. Both checks (TypeAliasAnnotationCheck, GenericFunctionTypeParameterCheck) call that helper with Version.V_312, and both rules carry the documented exception:

This rule will only raise an issue when the Python version of the analyzed project is set to 3.12 or higher.

Those 5 were on their way to being accepted by hand as false positives. They are neither false nor positives, just the answer to a question nobody meant to ask.

This is latent for every org repo whose requires-python is wider than its CI Python version, and it distorts every version-gated rule, not only PEP 695.

Change

Adds an optional sonar-python-version input for the source support range, defaulting to empty and falling back to python-version:

-Dsonar.python.version=${{ inputs.sonar-python-version != '' && inputs.sonar-python-version || inputs.python-version }}

Callers whose requires-python is wider than their CI version should set it, e.g. sonar-python-version: '3.11,3.12,3.13,3.14'.

Compatibility

Backward compatible. With the empty default the expression resolves to inputs.python-version, so every existing caller produces a byte-identical scanner invocation. No caller is required to change.

Verification

  • YAML parses; the new input registers with default: '', required: false
  • actionlint: no new findings at the changed lines (the SC2086/SC2129 reports are pre-existing, in run: blocks this PR does not touch)
  • Pre-commit clean, including yamllint and commitizen
  • The value lands in the scan action's with: args:, not a run: block, so it introduces no shell interpolation

Generated with Claude Code

Summary by CodeRabbit

  • Enhancements

    • Improved Python analysis configuration by separating the build environment version from the source-supported versions.
    • Added an optional setting for specifying multiple supported Python versions.
    • Automatically uses the configured build version when no supported versions are provided.
    • Added validation for supported-version formatting, with clear failure reporting for invalid values.
  • Documentation

    • Clarified configuration options, supported version formats, precedence rules, and maintenance guidance for SonarCloud analysis.

``-Dsonar.python.version`` was wired to the ``python-version`` input, which
also feeds ``actions/setup-python``. Because setup-python accepts exactly one
version, a caller could never declare a support RANGE to the analyzer, only
the single version CI happens to build with.

That is not cosmetic. Sonar gates version-specific rules on ALL declared
versions: ``PythonVersionUtils.areSourcePythonVersionsGreaterOrEqualThan`` is
an ``allMatch`` over the parsed set. Declaring one modern version therefore
opens every version gate, and the analyzer raises rules for syntax the project
cannot legally use. Observed on cyo-adventure: 5 PEP 695 findings
(``python:S6794`` x4, ``python:S6796`` x1) against a codebase whose
``requires-python`` is ``>=3.11`` and whose compatibility matrix still runs a
3.11 leg, where ``type X = ...`` is a SyntaxError. Both checks call that
helper with ``Version.V_312``. The findings were on their way to being
accepted by hand as false positives; they are neither false nor positives,
just the answer to a question nobody meant to ask.

Adds an optional ``sonar-python-version`` input for the source support range
(``'3.11,3.12,3.13,3.14'``), defaulting to empty and falling back to
``python-version``, so every existing caller is byte-identical. Callers whose
``requires-python`` is wider than their CI version should set it.

- YAML parses; the new input registers with ``default: ''``, ``required: false``
- actionlint: no new findings at the changed lines (the SC2086/SC2129 reports
  are pre-existing, in ``run:`` blocks this commit does not touch)
- The value lands in the scan action's ``with: args:``, not a ``run:`` block,
  so it introduces no shell interpolation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 01:59
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@williaby, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ef98019-3452-4be6-b086-256ba66fcc03

📥 Commits

Reviewing files that changed from the base of the PR and between a9fe594 and f101aa3.

📒 Files selected for processing (2)
  • .github/workflows/python-sonarcloud.yml
  • docs/workflows/python-sonarcloud.md
📝 Walkthrough

Walkthrough

The reusable workflow separates the Python interpreter used for setup and scanning from the Python versions supported by the source. It validates the supported-version list, passes it to SonarCloud, and documents the configuration.

Changes

SonarCloud Python version configuration

Layer / File(s) Summary
Workflow input, validation, and scanner wiring
.github/workflows/python-sonarcloud.yml
The workflow defines separate inputs, resolves the optional source-version list, validates MAJOR.MINOR values, and passes the validated list to SonarCloud.
Documentation and template guidance
docs/workflows/python-sonarcloud.md, workflow-templates/python-sonarcloud.yml
The documentation and template comments describe supported-version lists, validation, precedence, and SonarCloud formatting.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowInput
  participant VersionResolution
  participant SonarCloudScan
  WorkflowInput->>VersionResolution: provide Python version inputs
  VersionResolution->>VersionResolution: resolve and validate source versions
  VersionResolution->>SonarCloudScan: pass sonar.python.version
Loading

Possibly related PRs

Suggested reviewers: copilot, byronwilliamscpa

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem, impact, implementation, compatibility, and verification, but omits several template sections such as Related Issue, Type of Change, and Checklist. Add the required template sections and complete the relevant issue, change-type, testing, checklist, and additional-context items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: separating SonarCloud's Python version from the build version.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sonar-python-version-input

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the reusable SonarCloud workflow to allow callers to declare the Python version range their source code supports independently from the single Python version used to run CI and the analysis. This avoids SonarPython enabling version-gated rules for syntax not actually supported by the project.

Changes:

  • Added an optional sonar-python-version input (default empty) to represent the supported source version range.
  • Updated the Sonar scan arguments so -Dsonar.python.version uses sonar-python-version when provided, otherwise falls back to python-version.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/python-sonarcloud.yml:
- Around line 71-90: Update the workflow documentation for the inputs around
python-version: describe python-version as the single version used by
actions/setup-python, document the sonar-python-version comma-separated
source-version input, and state that an empty sonar-python-version falls back to
python-version.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f2eb86f5-18c9-49c5-845e-0aa620a25882

📥 Commits

Reviewing files that changed from the base of the PR and between 7d12f54 and a7a741b.

📒 Files selected for processing (1)
  • .github/workflows/python-sonarcloud.yml

Comment thread .github/workflows/python-sonarcloud.yml
@williaby

williaby commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

PR Review

PREMISE QUESTION: the premise is sound and the lever is the right one, but the change ships without its caller-facing docs, leaves an identically-defective sibling template unfixed, and has one unguarded input-format footgun.

Gates: 34 CI checks all SUCCESS or SKIPPED, SonarCloud quality gate passed (0 new issues, 0 hotspots), Qlty green. Copilot: overview only, zero findings. CodeRabbit: 1 actionable comment (finding 1 below).

Findings: 0 Critical, 3 Important, 4 Suggested, 3 Informational.

Verified clean

The load-bearing technical claims check out against primary sources, worth stating plainly:

  • PythonVersionUtils.areSourcePythonVersionsGreaterOrEqualThan is allMatch, and python:S6794 / python:S6796 gate on Version.V_312 through that helper, with both rule descriptions carrying the documented 3.12+ exception. Confirmed against sonar-python source.
  • sonar.python.version accepts a comma-separated list. Confirmed against SonarSource docs.
  • The != '' && x || y idiom is safe in this exact shape: the GHA falsy set is false, 0, -0, "", '', null, where 0 is the number, so the strings '0' and 'false' are truthy and cannot fall through. It also matches precedent one line below it (line 414) and in python-qlty-coverage.yml:165.
  • Backward compatibility holds; empty default resolves to inputs.python-version.
  • The PR body's lint claims are accurate, verified empirically against this head SHA: SC2086 and line-length findings sit at lines 197/260/263/266/334/426/484, none touched here. New line 410 is 131 chars, under the 160 limit.
  • No regression: git log -S "sonar-python-version" --all returns only this commit.
  • RAD markers not required; the change touches none of the four mandatory categories.

Important

1. The caller-facing inputs table omits the new input and still carries the description string this PR replaced.
docs/workflows/python-sonarcloud.md#L60 reads | python-version | string | '3.12' | Python version for analysis |, the pre-PR wording, and no sonar-python-version row exists in the table (lines 58-71), so the feature is undiscoverable from the docs. The properties-file example at line 196 still shows sonar.python.version=3.12, and the note at line 210 says workflow inputs override the properties file, so a caller setting both gets their properties value silently ignored with that interaction undocumented. Confirmed that no automated check enforces doc/input parity: nothing parses workflow_call.inputs, and mkdocs build --strict validates links only, at info severity. This will not be caught later.

2. A value in SonarSource's own documented format silently breaks the scan.
Sonar documents the canonical example as sonar.python.version=2.7, 3.7, 3.8, 3.9, with spaces after the commas. That form cannot work at line 410: the value is interpolated into an args: > folded block, so a space splits the argument, yielding -Dsonar.python.version=3.11, plus a stray 3.12 token. The PR's own example correctly uses no spaces, but nothing enforces or warns about it, and a caller who copies the format from Sonar's documentation, the obvious place to look, gets a misparse rather than an error. The 11-line description never mentions the constraint. Neither bot caught this.

3. The gallery starter workflow hardcodes the same value, so this is an incomplete remediation of its own stated problem.
workflow-templates/python-sonarcloud.yml#L100 hardcodes -Dsonar.python.version=3.12. That file is a consumer-facing starter-workflow gallery entry (per its .properties.json sidecar) which invokes the scanner directly and never calls the reusable workflow, so a repo adopting it still eats S6794/S6796 against a 3.11-supporting codebase, exactly the defect this PR describes as latent org-wide. It has no inputs, so it cannot take the fallback; the fix is a cookiecutter variable or at minimum a comment. docs/compliance/audits/2026-05-29/03-architecture.md:17 already records this template-versus-reusable divergence.

Suggested and Informational (summary)

  1. scripts/check-python-versions.sh both cannot see the new input's default: and would falsely DRIFT-fail a caller-style with: line containing an out-of-policy version. Both verified with fixtures, but latent everywhere and live nowhere: the script exists only in this repo, runs only from self-test.yml:136, is absent from all seven consumer clones, no org reusable workflow invokes it, and there is no in-repo caller of this workflow.
  2. The 3.14 in the description example sits outside the docs/python-versions.md supported marker, but that marker is the stale artifact: Python 3.14 reached stable release in October 2025, and by the policy's own rule it should already be listed. The policy also constrains only what a matrix or input default actively selects, and this default is empty.
  3. Because the gate is allMatch, over-declaring the range silently suppresses legitimate findings. A caller who defensively writes a floor below their real one loses every gated rule with no signal. Worth one sentence recommending an accurate range rather than a conservative one.
  4. The description bakes PythonVersionUtils.areSourcePythonVersionsGreaterOrEqualThan into permanent workflow config, where it will rot on any upstream rename, against a file-wide convention of one-line descriptions. The rationale belongs in the workflow doc.
  5. additional-sonar-args (line 415) already offered a path, since duplicate -D properties resolve last-wins. The named input is the better design; only the justification is missing from the PR body.
  6. Open PRs chore(deps)!: Update GitHub Actions (major) #290 and chore(deps): Update GitHub Actions #286 both touch this file and the template at unrelated lines. Textual conflict unlikely; whichever merges last needs a rebase.
  7. Merging remediates zero repos. Six of seven local callers declare a requires-python floor below the 3.12 analysis version and none sets the new input. Not a defect, since the input is explicitly optional, but the value of the change lands entirely in follow-up work.

Recommended action

  1. Add the docs row and the format constraint together (findings 1 and 2), and move the PythonVersionUtils rationale there from the input description (finding 7).
  2. Decide the template's fate (finding 3). Leaving it silently hardcoded is the one thing that undercuts this PR's stated scope.
  3. Merge is safe on the code as far as verifiable: green gates, confirmed backward compatibility, confirmed technical premise, no regression, idiom matches in-repo precedent.
  4. Separate follow-ups: refresh the docs/python-versions.md marker for 3.14; teach check-python-versions.sh about comma-separated inputs before any in-repo caller appears; then set sonar-python-version in the six affected consumer repos, which is where the PEP 695 false positives actually get fixed.

SonarQube: 0 issues and 0 hotspots queued for auto-fix.

Copilot review was requested by the org ruleset and submitted with no findings.

🤖 Generated with Claude Code

The scan step passes sonar.python.version inside a folded block scalar
(args: >), where YAML joins lines with spaces and the scanner then splits
the result on whitespace. A value containing a space became two scanner
arguments and the version list was silently truncated. SonarSource's own
documentation writes the property as "3.11, 3.12", so a caller copying the
vendor format hit that misparse with no validation and no warning.

Resolve the value in a bash step instead: strip whitespace so both
spellings work, then reject anything that is not a comma-separated list of
MAJOR.MINOR versions. The regex doubles as the GITHUB_OUTPUT shape guard
(PR #234), since a newline in a caller value could otherwise append extra
key=value lines and forge unrelated step outputs. Both inputs reach the
run: body through env:, never through template expansion.

Also narrow the description example from 3.14 to 3.13 so it matches this
repo's python-versions:supported marker and cannot trip the policy gate in
scripts/check-python-versions.sh.

Verified: 11 guard cases (vendor spacing, messy whitespace, empty
fallback, both-empty, patch version, range operator, command
substitution, trailing comma, newline injection) behave as designed;
actionlint rc=0; yamllint adds no new warnings; qlty clean.
The inputs table still described python-version as "Python version for
analysis" and did not list sonar-python-version at all, so callers had no
way to learn the input exists or why they would set it. CodeRabbit flagged
the same gap on PR #291.

Add the missing row, retitle python-version to say it is the single
actions/setup-python version, and add a "Declaring supported source
versions" section covering the allMatch gating rule that makes the input
necessary, the accepted format, and the inverse hazard: declaring versions
below the real requires-python floor silently suppresses rules that should
fire.

Also correct the sonar-project.properties example, which showed a single
version and did not mention that the workflow always passes
-Dsonar.python.version on the command line, so a sonar.python.version key
in that file is ignored.
The standalone starter workflow invokes the scanner directly and hardcodes
-Dsonar.python.version=3.12, so it carries the same defect the reusable
workflow just fixed: adopters whose requires-python is wider than 3.12 get
version-gated rules raised against syntax they cannot use.

The template has no workflow_call inputs to widen, so document the
adjustment at the value itself, including the folded-scalar constraint that
each value stay space-free.
Copilot AI review requested due to automatic review settings August 6, 2026 03:30
@williaby

williaby commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes applied

Three signed commits pushed (a7a741b..a9fe594). Resolves the three Important findings from the review above, including @coderabbitai's actionable comment.

Finding Commit What changed
Caller docs did not mention the new input (CodeRabbit's comment) a44f770 Added the sonar-python-version row, retitled python-version as the single actions/setup-python version, added a Declaring supported source versions section, corrected the properties example
Vendor-documented 3.11, 3.12 spacing silently misparsed 3da29ec New Resolve sonar.python.version step strips whitespace and shape-guards the value
Starter template still hardcoded 3.12 a9fe594 Documented at the value; the template has no workflow_call inputs to widen

Why the guard step exists

args: > is a folded block scalar, so YAML joins its lines with spaces and the scanner splits the result on whitespace. A value containing a space became two scanner arguments and the version list was silently truncated. SonarSource's own documentation writes this property as 3.11, 3.12, so a caller copying the vendor format hit that misparse with no validation and no warning.

The step now normalizes whitespace (both spellings work) and rejects anything that is not a comma-separated list of MAJOR.MINOR versions. The regex doubles as the $GITHUB_OUTPUT shape guard per #234: a newline in a caller value could otherwise append extra key=value lines and forge unrelated step outputs. Both inputs reach the run: body through env:, never through template expansion.

Verification

  • 11 guard cases exercised locally: vendor spacing, messy whitespace, empty fallback, both-empty, patch version, range operator, command substitution, trailing comma, newline injection. All behave as designed; nothing reaches $GITHUB_OUTPUT on a reject.
  • actionlint rc=0. The 5 remaining yamllint line-length warnings are byte-identical to base (pre-existing); the new step's longest line is 154 of 160.
  • pre-commit run --all-files 11/11 passed. qlty check --upstream origin/main reports "No issues".
  • scripts/check-python-versions.sh rc=0. The description example was narrowed from 3.14 to 3.13 so it matches this repo's python-versions:supported marker.

Two items deliberately left out

  1. Should 3.14 join the supported set? It has been stable since October 2025, and docs/python-versions.md says stable releases belong in the set, so the marker is arguably the stale artifact. That is a policy edit in its own PR, not a change to make here.
  2. This PR still remediates zero consumer repos. 6 of 7 callers have a requires-python floor below 3.12 and none set the new input. The input has to be added caller-side, which is 6 separate PRs.

Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- PR diff summary ---'
git diff --stat origin/main...HEAD

echo
echo '--- Changed documentation sections ---'
sed -n '50,95p' docs/workflows/python-sonarcloud.md
sed -n '185,245p' docs/workflows/python-sonarcloud.md

echo
echo '--- Changed workflow section ---'
sed -n '390,465p' .github/workflows/python-sonarcloud.yml

echo
echo '--- Relevant diff ---'
git diff --unified=35 origin/main...HEAD -- \
  docs/workflows/python-sonarcloud.md \
  .github/workflows/python-sonarcloud.yml \
  workflow-templates/python-sonarcloud.yml

Length of output: 32755


@williaby Confirmed. The documentation now exposes sonar-python-version, defines its fallback and format, and explains the sonar-project.properties precedence behavior.

The workflow also normalizes documented comma spacing, validates the version list before it writes $GITHUB_OUTPUT, and passes only the validated value to the scanner. The template now states the manual change required for its hard-coded value.

This addresses my caller-documentation finding.
🐇 ✅


✏️ Learnings added
Learnt from: williaby
URL: https://github.com/ByronWilliamsCPA/.github/pull/291

Timestamp: 2026-08-06T03:31:05.714Z
Learning: In `.github/workflows/python-sonarcloud.yml`, the optional `sonar-python-version` reusable-workflow input declares the Python `MAJOR.MINOR` versions supported by the source. The workflow removes surrounding whitespace, requires a comma-separated version list, falls back to `python-version` when empty, and passes the validated result as `-Dsonar.python.version`. Callers must set this input instead of `sonar-project.properties` because the workflow command-line property overrides the properties-file value.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

docs/workflows/python-sonarcloud.md:118

  • Docs describe stripping only surrounding whitespace, but the workflow implementation strips all whitespace from the value. Update the wording to reflect the actual behavior so callers understand that any spaces, tabs, or newlines are removed before validation.
**Format.** Comma-separated `MAJOR.MINOR` values. Surrounding whitespace is tolerated and
stripped, so both `3.11,3.12` and SonarSource's own documented `3.11, 3.12` spacing work.

.github/workflows/python-sonarcloud.yml:82

  • The description says whitespace is stripped only around commas, but the resolver step removes all whitespace characters (VERSION="${RAW//[[:space:]]/}"). Consider updating the input description to match the actual behavior.
          Whitespace around commas is stripped, so SonarSource's own documented
          "3.11, 3.12" spacing is accepted; any other shape fails the job with an
          explicit message instead of reaching the scanner malformed.

…commas

Copilot's re-review flagged that the docs promised whitespace was stripped
"around commas" while the resolver stripped whitespace everywhere. The claim
was wrong in the caller's favour: stripping first meant "3 .11" was silently
normalized to "3.11" instead of being reported as the typo it is, so a
mistyped value could quietly analyze against a version the caller never
declared.

Validate before stripping, with whitespace permitted only where it carries
no meaning: around the commas and at the ends. SonarSource's documented
"3.11, 3.12" spacing still passes; "3 .11", "3. 11", "3.1 1", and a bare
space used as a separator now fail with the same explicit message as any
other malformed shape.

The GITHUB_OUTPUT shape guard is unaffected. The accepted set is still only
digits, dots, commas, and whitespace, and stripping the whitespace leaves no
newline, no '=', and no shell metacharacter.

Verified: 18 cases, extracted from the workflow body itself so the test
cannot drift from the shipped code. 8 pass cases (including tab and newline
as inter-comma whitespace) and 10 reject cases (the 4 new whitespace-inside-
version cases, patch version, range operator, command substitution, trailing
comma, both-empty, newline injection). actionlint findings are byte-identical
to base at 46; yamllint line-length warnings unchanged at 5; qlty clean;
check-python-versions.sh rc=0.
Copilot AI review requested due to automatic review settings August 6, 2026 03:43
@williaby

williaby commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up: Copilot's suppressed finding, and a correction

Copilot was right, and the fix went into the implementation

Copilot's re-review reported "no new comments" but listed 2 suppressed ones. Both were the
same point, and it holds: the docs promised whitespace was stripped around the commas,
while ${RAW//[[:space:]]/} stripped it everywhere.

That gap was in the caller's favour in the worst way. Because the old guard stripped first
and validated second, sonar-python-version: "3 .11" was silently normalized to 3.11 and
accepted, so a typo would quietly analyze the project against a version nobody declared.

Resolved in c150019 by tightening the implementation rather than loosening the docs.
Validation now runs on the raw value, permitting whitespace only where it carries no
meaning (around the commas, at the ends), and stripping happens after:

if [[ ! "$RAW" =~ ^[[:space:]]*[0-9]+\.[0-9]+([[:space:]]*,[[:space:]]*[0-9]+\.[0-9]+)*[[:space:]]*$ ]]; then
  ...
fi
VERSION="${RAW//[[:space:]]/}"

Newly rejected, previously silently normalized: 3 .11, 3. 11,3.12, 3.1 1, and a bare
space used as a separator (3.11 3.12). SonarSource's own documented 3.11, 3.12 spacing
still passes, which was the whole point of tolerating whitespace in the first place.

The GITHUB_OUTPUT shape guard is unaffected. Tightening a regex can only narrow the
accepted set, and the set is still digits, dots, commas and whitespace, so after stripping
there is no newline, no =, and no shell metacharacter.

Correction: my earlier actionlint claim was wrong

An earlier comment on this PR states actionlint rc=0. That is not true, and the error was
mine: I read the exit code through a | head pipeline, so $? was head's status rather
than actionlint's. actionlint exits 1 on this workflow file, and did so before this PR
as well.

Re-measured properly, by diffing normalized output instead of trusting an exit code:

findings
base (a7a741b) 46
this branch (c150019) 46

Byte-identical after stripping line numbers, and none of the 46 falls in the new step's
line range. So the original conclusion stands (this change introduces no new actionlint
finding), but it now rests on evidence rather than on a mismeasured exit code.

Verification on the new head

  • 18 guard cases, all passing. The guard body is extracted from the workflow YAML by script,
    so the test cannot drift from the shipped code. 8 accept (vendor spacing, tab, newline
    after comma, empty falling back to python-version), 10 reject. Every reject exits 1 with
    nothing written to $GITHUB_OUTPUT.
  • pre-commit run --all-files: 12/12 passed
  • qlty check --upstream origin/main: no issues
  • scripts/check-python-versions.sh: rc=0
  • yamllint: 5 line-length warnings, byte-identical to base

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.github/workflows/python-sonarcloud.yml:438

  • When sonar-python-version is empty, this step validates python-version as MAJOR.MINOR only. That makes python-version: '3.12.1' (valid for actions/setup-python) fail the job even though setup succeeds. If the intent is backward compatibility and python-version continues to feed setup-python, consider normalizing an optional patch component when falling back to python-version, while keeping strict validation for the explicit sonar-python-version list.
          if [ -n "$SONAR_PYTHON_VERSION" ]; then
            RAW="$SONAR_PYTHON_VERSION"
            FROM_INPUT="sonar-python-version"
          else
            RAW="$PYTHON_VERSION"
            FROM_INPUT="python-version (sonar-python-version was empty)"
          fi

Copilot caught a backward-compatibility break I introduced in c150019. The
strict MAJOR.MINOR list validation was applied to both inputs, but the two have
different contracts. sonar-python-version is a sonar.python.version list and
nothing else. python-version feeds actions/setup-python, whose accepted syntax
is far wider: '3.12.1', '3.13.0-rc.1', '>=3.11', and 'pypy3.10' are all legal
there and all analyzed fine before this step existed. Validating them against a
Sonar list shape hard-failed the job for callers who never opted into
sonar.python.version at all.

Split the paths. The explicit input keeps strict validation. The fallback
extracts the leading MAJOR.MINOR, since that is all sonar.python.version
accepts, and only fails when no minor version can be derived ('3.x', 'pypy',
empty), where the message points at sonar-python-version rather than guessing a
version the caller never declared. A narrowed value is logged as narrowed, so
the normalization is never silent, and the fallback now also emits a notice
that a single build version may be too narrow.

The GITHUB_OUTPUT shape guard survives the split: both branches assign VERSION
from digits and dots only, either a whitespace-stripped match of the strict
list regex or two numeric BASH_REMATCH groups.

Verified: 26 cases, extracted from the workflow body so the test cannot drift
from shipped code. 16 explicit-input cases (6 accept including vendor spacing,
tab and newline; 10 reject including the injection and whitespace-inside-version
cases) and 10 fallback cases (6 accept including the 3.12.1 regression Copilot
found, 4 reject). Rejects write nothing to GITHUB_OUTPUT. pre-commit clean,
qlty clean, check-python-versions.sh rc=0, actionlint 46 findings matching base
with none in the step's line range.
Copilot AI review requested due to automatic review settings August 6, 2026 03:54
@williaby

williaby commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Copilot's patch-version finding was right, and it was a break I introduced

Copilot's latest review again reads "no new comments" with 1 suppressed, and the suppressed
one is a real backward-compatibility break in c150019:

When sonar-python-version is empty, this step validates python-version as MAJOR.MINOR
only. That makes python-version: '3.12.1' fail the job even though setup succeeds.

Correct. I applied one validation shape to two inputs that have different contracts.
sonar-python-version means a sonar.python.version list and nothing else, so strict is
right there. python-version feeds actions/setup-python, which legitimately accepts
3.12.1, 3.13.0-rc.1, >=3.11, 3.x, and pypy3.10. All of those analyzed fine before
this step existed, and validating them against a Sonar list shape hard-failed the job for
callers who never opted into the new input at all.

Fixed in f101aa3 by splitting the paths:

Path Behaviour
sonar-python-version set strict list validation, unchanged
falling back to python-version extract the leading MAJOR.MINOR

So 3.12.1 resolves to 3.12, >=3.11 to 3.11, pypy3.10 to 3.10. Only a value with
no derivable minor (3.x, pypy, empty) fails, and that message points at
sonar-python-version rather than guessing a version the caller never declared. A narrowed
value is logged as narrowed, so the normalization is never silent.

The GITHUB_OUTPUT shape guard survives the split. Both branches assign VERSION from
digits and dots only: either a whitespace-stripped match of the strict list regex, or two
numeric BASH_REMATCH groups.

Verification

26 cases, extracted from the workflow body by script so the test cannot drift from shipped
code. 16 explicit-input (6 accept, 10 reject) and 10 fallback (6 accept, 4 reject). Every
reject exits 1 with nothing written to $GITHUB_OUTPUT. pre-commit clean, qlty check --upstream origin/main clean, check-python-versions.sh rc=0, actionlint 46 findings
matching base with none in the step's line range.

Note on the previous run

qlty-gate failed on f101aa3's parent for an unrelated infrastructure reason: qlty could
not download trufflehog@3.95.5 from GitHub releases and exited 99 before analyzing
anything. This push re-triggers it.

@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

.github/workflows/python-sonarcloud.yml:448

  • sonar-python-version is documented as having whitespace at the ends stripped, but the script treats a whitespace-only value as non-empty and then fails validation. This makes an accidental value like ' ' behave differently than empty, even though stripping would yield empty and should fall back to python-version. Consider treating the whitespace-stripped value as the emptiness check, while still validating the original string to keep rejecting whitespace inside a version ("3 .11").
          if [ -n "$SONAR_PYTHON_VERSION" ]; then
            # The explicit input is a sonar.python.version list and nothing else, so hold it to
            # exactly that shape. Validate BEFORE stripping, so whitespace is tolerated only
            # where it is meaningless: around the commas and at the ends. That accepts
            # SonarSource's documented "3.11, 3.12" spacing while still rejecting whitespace

.github/workflows/python-sonarcloud.yml:463

  • The PR description claims the default empty sonar-python-version path produces a byte-identical scanner invocation by passing inputs.python-version through unchanged. The implementation now normalizes python-version to a leading MAJOR.MINOR for sonar.python.version, which changes the argument for callers using values like 3.12.1, >=3.11, or pypy3.10 (even though that normalization is likely desirable). Consider updating the PR description to reflect the normalization behavior.
            if [[ "$PYTHON_VERSION" =~ ([0-9]+)\.([0-9]+) ]]; then
              VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
            else

@williaby
williaby added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 4bd2d7c Aug 6, 2026
35 checks passed
@williaby
williaby deleted the feat/sonar-python-version-input branch August 6, 2026 04:01
williaby added a commit to ByronWilliamsCPA/rag-processor that referenced this pull request Sep 3, 2026
sonar.python.version was 3.12, the single version CI builds with, while
requires-python is ">=3.11,<3.15". SonarPython gates version-specific rules on
ALL declared versions (PythonVersionUtils.areSourcePythonVersionsGreaterOrEqualThan
is allMatch), so declaring only 3.12 made every 3.12-gated rule fire against code
that must still run on 3.11.

That produced 2 live false positives today: S6794 and S6796 urging PEP 695
"type X = ..." aliases, which are a SyntaxError on 3.11. Declaring the full range
makes allMatch(>=3.12) false, so the rules correctly stay silent until the floor
moves.

Sets the new sonar-python-version input (ByronWilliamsCPA/.github#291) and bumps
the uses: pin to the commit that introduces it, since the input does not exist in
the previously pinned revision and passing an undefined input fails the reusable
workflow at startup.

Also syncs sonar-project.properties. CI overrides that key via -D, so it does not
affect the pipeline, but SonarLint in the IDE reads it and would otherwise keep
raising the same false positives locally that CI no longer reports.

python-version stays 3.12: it feeds actions/setup-python and is the version the
project builds and runs tests with, which is a separate concern from the versions
the source must remain compatible with.

Pre-commit: all hooks pass except pydoclint, which fails identically on untouched
main (DOC105/109/110 in fuzz/*.py). This change touches no Python, so it is
scoped out with SKIP=pydoclint rather than bypassed.
williaby added a commit to ByronWilliamsCPA/rag-processor that referenced this pull request Sep 4, 2026
sonar.python.version was 3.12, the single version CI builds with, while
requires-python is ">=3.11,<3.15". SonarPython gates version-specific rules on
ALL declared versions (PythonVersionUtils.areSourcePythonVersionsGreaterOrEqualThan
is allMatch), so declaring only 3.12 made every 3.12-gated rule fire against code
that must still run on 3.11.

That produced 2 live false positives today: S6794 and S6796 urging PEP 695
"type X = ..." aliases, which are a SyntaxError on 3.11. Declaring the full range
makes allMatch(>=3.12) false, so the rules correctly stay silent until the floor
moves.

Sets the new sonar-python-version input (ByronWilliamsCPA/.github#291) and bumps
the uses: pin to the commit that introduces it, since the input does not exist in
the previously pinned revision and passing an undefined input fails the reusable
workflow at startup.

Also syncs sonar-project.properties. CI overrides that key via -D, so it does not
affect the pipeline, but SonarLint in the IDE reads it and would otherwise keep
raising the same false positives locally that CI no longer reports.

python-version stays 3.12: it feeds actions/setup-python and is the version the
project builds and runs tests with, which is a separate concern from the versions
the source must remain compatible with.

Pre-commit: all hooks pass except pydoclint, which fails identically on untouched
main (DOC105/109/110 in fuzz/*.py). This change touches no Python, so it is
scoped out with SKIP=pydoclint rather than bypassed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants