From 8a453f4290cdd51c73a216ecb643ed98656e17f6 Mon Sep 17 00:00:00 2001 From: aspencer Date: Mon, 3 Aug 2026 09:45:17 -0700 Subject: [PATCH 1/3] feat(ci): add reusable SonarQube manual-scan workflow Add workflow_call workflow with quality_profile input, optional coverage artifact download from a prior run, and Sonar scanner cache. DEVEX-1727 Co-authored-by: Cursor --- .github/workflows/sonarqube-manual.yaml | 95 +++++++++++++++++++++++++ README.md | 35 +++++++++ 2 files changed, 130 insertions(+) create mode 100644 .github/workflows/sonarqube-manual.yaml diff --git a/.github/workflows/sonarqube-manual.yaml b/.github/workflows/sonarqube-manual.yaml new file mode 100644 index 0000000..109d5ec --- /dev/null +++ b/.github/workflows/sonarqube-manual.yaml @@ -0,0 +1,95 @@ +name: SonarQube Manual Scan + +on: + workflow_call: + inputs: + quality_profile: + description: SonarCloud quality profile name (e.g. Sonar way) + required: true + type: string + ref: + description: Branch, tag, or SHA to analyze (defaults to caller ref) + required: false + type: string + default: "" + fetch_depth: + description: Git fetch depth (0 = full history for Sonar SCM) + required: false + type: number + default: 0 + runs_on: + required: false + type: string + default: ubuntu-latest-8-cores + sonar_scanner_opts: + required: false + type: string + default: -Xmx4000m + coverage_workflow_run_id: + description: Workflow run ID to download coverage artifacts from (optional) + required: false + type: string + default: "" + coverage_artifact_pattern: + description: Artifact name pattern when downloading coverage (e.g. coverage*) + required: false + type: string + default: coverage* + coverage_files_to_rewrite: + description: Comma-separated coverage.xml paths to rewrite for Sonar container paths + required: false + type: string + default: "" + secrets: + sonar_token: + required: true + +permissions: + contents: read + actions: read + +jobs: + sonarqube: + runs-on: ${{ inputs.runs_on }} + name: SonarQube Manual Scan (${{ inputs.quality_profile }}) + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} + fetch-depth: ${{ inputs.fetch_depth }} + + - name: Cache SonarQube scanner data + uses: actions/cache@v5 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar-${{ hashFiles('sonar-project.properties') }} + restore-keys: | + ${{ runner.os }}-sonar- + + - name: Download coverage artifacts + if: inputs.coverage_workflow_run_id != '' + uses: actions/download-artifact@v4 + with: + run-id: ${{ inputs.coverage_workflow_run_id }} + pattern: ${{ inputs.coverage_artifact_pattern }} + path: ./ + merge-multiple: true + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fix code coverage paths + if: inputs.coverage_files_to_rewrite != '' + run: | + IFS=',' read -ra FILES <<< "${{ inputs.coverage_files_to_rewrite }}" + for file in "${FILES[@]}"; do + if [ -f "$file" ]; then + sed -i 's@'"$GITHUB_WORKSPACE"'@/github/workspace@g' "$file" + fi + done + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v6.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.sonar_token }} + SONAR_SCANNER_OPTS: "${{ inputs.sonar_scanner_opts }} -Dsonar.qualityprofile=${{ inputs.quality_profile }}" diff --git a/README.md b/README.md index 5a86cfe..37c85c6 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ # .github + +## Reusable workflows + +### SonarQube manual scan (`sonarqube-manual.yaml`) + +On-demand SonarCloud analysis with a configurable quality profile. Intended for security-heavy profiles or full scans without slowing every PR. + +**Consumer setup** — add a `workflow_dispatch` workflow in your repo: + +```yaml +jobs: + sonarqube: + uses: encodium/.github/.github/workflows/sonarqube-manual.yaml@main + with: + quality_profile: ${{ inputs.quality_profile }} + ref: ${{ inputs.ref }} + coverage_workflow_run_id: ${{ inputs.coverage_workflow_run_id }} + coverage_files_to_rewrite: coverage/coverage.xml # repo-specific paths + secrets: + sonar_token: ${{ secrets.SONAR_TOKEN }} +``` + +**Inputs** + +| Input | Required | Description | +| --- | --- | --- | +| `quality_profile` | yes | SonarCloud profile name (e.g. `Sonar way`) | +| `ref` | no | Branch/tag/SHA; defaults to caller ref | +| `coverage_workflow_run_id` | no | GitHub Actions run ID to pull coverage artifacts from | +| `coverage_artifact_pattern` | no | Defaults to `coverage*` | +| `coverage_files_to_rewrite` | no | Comma-separated `coverage.xml` paths for `/github/workspace` rewrite | + +**Run from GitHub UI:** Actions → your repo's manual Sonar workflow → Run workflow. + +For coverage, pass the run ID from a recent unit-test workflow (Actions → PHP Unit Tests → copy run ID from the URL). From 28e40cc636a12c4028533a77f80502d229b2d1ae Mon Sep 17 00:00:00 2001 From: aspencer Date: Mon, 3 Aug 2026 10:06:46 -0700 Subject: [PATCH 2/3] fix(ci): address Bugbot review for sonarqube-manual workflow Switch quality profiles via SonarCloud API instead of unsupported scanner args, remove Docker-era coverage path rewrite, and require an explicit coverage artifact pattern when downloading from a prior run. DEVEX-1727 Co-authored-by: Cursor --- .github/workflows/sonarqube-manual.yaml | 72 ++++++++++++++++++------- README.md | 17 ++++-- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/.github/workflows/sonarqube-manual.yaml b/.github/workflows/sonarqube-manual.yaml index 109d5ec..2b999cf 100644 --- a/.github/workflows/sonarqube-manual.yaml +++ b/.github/workflows/sonarqube-manual.yaml @@ -7,6 +7,11 @@ on: description: SonarCloud quality profile name (e.g. Sonar way) required: true type: string + sonar_language: + description: SonarCloud language key for the quality profile (e.g. php, java) + required: false + type: string + default: php ref: description: Branch, tag, or SHA to analyze (defaults to caller ref) required: false @@ -22,6 +27,7 @@ on: type: string default: ubuntu-latest-8-cores sonar_scanner_opts: + description: JVM options for the Sonar scanner (e.g. -Xmx4000m) required: false type: string default: -Xmx4000m @@ -31,12 +37,7 @@ on: type: string default: "" coverage_artifact_pattern: - description: Artifact name pattern when downloading coverage (e.g. coverage*) - required: false - type: string - default: coverage* - coverage_files_to_rewrite: - description: Comma-separated coverage.xml paths to rewrite for Sonar container paths + description: Artifact name pattern when downloading coverage (required with coverage_workflow_run_id) required: false type: string default: "" @@ -67,8 +68,40 @@ jobs: restore-keys: | ${{ runner.os }}-sonar- + - name: Switch SonarCloud quality profile + id: sonar_profile + env: + SONAR_TOKEN: ${{ secrets.sonar_token }} + run: | + set -euo pipefail + + if [ ! -f sonar-project.properties ]; then + echo "sonar-project.properties not found in repository root" >&2 + exit 1 + fi + + PROJECT_KEY=$(grep -E '^sonar\.projectKey=' sonar-project.properties | head -1 | cut -d= -f2- | tr -d ' \r') + ORGANIZATION=$(grep -E '^sonar\.organization=' sonar-project.properties | head -1 | cut -d= -f2- | tr -d ' \r') + LANGUAGE="${{ inputs.sonar_language }}" + PROFILE="${{ inputs.quality_profile }}" + + RESPONSE=$(curl -sf -u "${SONAR_TOKEN}:" \ + "https://sonarcloud.io/api/qualityprofiles/search?project=${PROJECT_KEY}&organization=${ORGANIZATION}") + PREVIOUS=$(echo "$RESPONSE" | jq -r --arg lang "$LANGUAGE" \ + '.profiles[] | select(.language == $lang) | .name' | head -1) + + echo "previous_profile=${PREVIOUS}" >> "$GITHUB_OUTPUT" + echo "project_key=${PROJECT_KEY}" >> "$GITHUB_OUTPUT" + echo "organization=${ORGANIZATION}" >> "$GITHUB_OUTPUT" + + curl -sf -u "${SONAR_TOKEN}:" -X POST \ + "https://sonarcloud.io/api/qualityprofiles/add_project" \ + --data-urlencode "project=${PROJECT_KEY}" \ + --data-urlencode "qualityProfile=${PROFILE}" \ + --data-urlencode "language=${LANGUAGE}" + - name: Download coverage artifacts - if: inputs.coverage_workflow_run_id != '' + if: inputs.coverage_workflow_run_id != '' && inputs.coverage_artifact_pattern != '' uses: actions/download-artifact@v4 with: run-id: ${{ inputs.coverage_workflow_run_id }} @@ -77,19 +110,22 @@ jobs: merge-multiple: true github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Fix code coverage paths - if: inputs.coverage_files_to_rewrite != '' - run: | - IFS=',' read -ra FILES <<< "${{ inputs.coverage_files_to_rewrite }}" - for file in "${FILES[@]}"; do - if [ -f "$file" ]; then - sed -i 's@'"$GITHUB_WORKSPACE"'@/github/workspace@g' "$file" - fi - done - - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.sonar_token }} - SONAR_SCANNER_OPTS: "${{ inputs.sonar_scanner_opts }} -Dsonar.qualityprofile=${{ inputs.quality_profile }}" + SONAR_SCANNER_OPTS: ${{ inputs.sonar_scanner_opts }} + + - name: Restore SonarCloud quality profile + if: always() && steps.sonar_profile.outputs.previous_profile != '' + env: + SONAR_TOKEN: ${{ secrets.sonar_token }} + run: | + set -euo pipefail + + curl -sf -u "${SONAR_TOKEN}:" -X POST \ + "https://sonarcloud.io/api/qualityprofiles/add_project" \ + --data-urlencode "project=${{ steps.sonar_profile.outputs.project_key }}" \ + --data-urlencode "qualityProfile=${{ steps.sonar_profile.outputs.previous_profile }}" \ + --data-urlencode "language=${{ inputs.sonar_language }}" diff --git a/README.md b/README.md index 37c85c6..f4ba414 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ On-demand SonarCloud analysis with a configurable quality profile. Intended for security-heavy profiles or full scans without slowing every PR. +SonarCloud does not support selecting a quality profile via scanner arguments. This workflow temporarily assigns the requested profile to the project using the SonarCloud API, runs the scan, then restores the previous profile. + **Consumer setup** — add a `workflow_dispatch` workflow in your repo: ```yaml @@ -16,7 +18,7 @@ jobs: quality_profile: ${{ inputs.quality_profile }} ref: ${{ inputs.ref }} coverage_workflow_run_id: ${{ inputs.coverage_workflow_run_id }} - coverage_files_to_rewrite: coverage/coverage.xml # repo-specific paths + coverage_artifact_pattern: coverage* # repo-specific; see below secrets: sonar_token: ${{ secrets.SONAR_TOKEN }} ``` @@ -26,11 +28,18 @@ jobs: | Input | Required | Description | | --- | --- | --- | | `quality_profile` | yes | SonarCloud profile name (e.g. `Sonar way`) | +| `sonar_language` | no | Language key for profile assignment (default: `php`) | | `ref` | no | Branch/tag/SHA; defaults to caller ref | | `coverage_workflow_run_id` | no | GitHub Actions run ID to pull coverage artifacts from | -| `coverage_artifact_pattern` | no | Defaults to `coverage*` | -| `coverage_files_to_rewrite` | no | Comma-separated `coverage.xml` paths for `/github/workspace` rewrite | +| `coverage_artifact_pattern` | no | Required with `coverage_workflow_run_id`; repo-specific glob | + +**Coverage artifact patterns** (set both `coverage_workflow_run_id` and `coverage_artifact_pattern`): + +| Repo workflow | Typical pattern | +| --- | --- | +| `encodium/common` unit tests | `coverage*` | +| `php-unit-test.yaml` reusable workflow | `phpunit-code-coverage-report` or `*coverage*` | **Run from GitHub UI:** Actions → your repo's manual Sonar workflow → Run workflow. -For coverage, pass the run ID from a recent unit-test workflow (Actions → PHP Unit Tests → copy run ID from the URL). +For coverage, pass the run ID from a recent unit-test workflow (Actions → workflow run → copy ID from the URL). From 4547cf33baeb773c802ba50ed54d5b1d6d13d19c Mon Sep 17 00:00:00 2001 From: aspencer Date: Mon, 3 Aug 2026 10:14:45 -0700 Subject: [PATCH 3/3] refactor(ci): simplify sonarqube-manual to security-only scan Drop coverage artifact download and revert README changes. This workflow is for on-demand security profile scans only. DEVEX-1727 Co-authored-by: Cursor --- .github/workflows/sonarqube-manual.yaml | 22 ------------- README.md | 44 ------------------------- 2 files changed, 66 deletions(-) diff --git a/.github/workflows/sonarqube-manual.yaml b/.github/workflows/sonarqube-manual.yaml index 2b999cf..4e67656 100644 --- a/.github/workflows/sonarqube-manual.yaml +++ b/.github/workflows/sonarqube-manual.yaml @@ -31,23 +31,12 @@ on: required: false type: string default: -Xmx4000m - coverage_workflow_run_id: - description: Workflow run ID to download coverage artifacts from (optional) - required: false - type: string - default: "" - coverage_artifact_pattern: - description: Artifact name pattern when downloading coverage (required with coverage_workflow_run_id) - required: false - type: string - default: "" secrets: sonar_token: required: true permissions: contents: read - actions: read jobs: sonarqube: @@ -92,7 +81,6 @@ jobs: echo "previous_profile=${PREVIOUS}" >> "$GITHUB_OUTPUT" echo "project_key=${PROJECT_KEY}" >> "$GITHUB_OUTPUT" - echo "organization=${ORGANIZATION}" >> "$GITHUB_OUTPUT" curl -sf -u "${SONAR_TOKEN}:" -X POST \ "https://sonarcloud.io/api/qualityprofiles/add_project" \ @@ -100,16 +88,6 @@ jobs: --data-urlencode "qualityProfile=${PROFILE}" \ --data-urlencode "language=${LANGUAGE}" - - name: Download coverage artifacts - if: inputs.coverage_workflow_run_id != '' && inputs.coverage_artifact_pattern != '' - uses: actions/download-artifact@v4 - with: - run-id: ${{ inputs.coverage_workflow_run_id }} - pattern: ${{ inputs.coverage_artifact_pattern }} - path: ./ - merge-multiple: true - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@v6.0.0 env: diff --git a/README.md b/README.md index f4ba414..5a86cfe 100644 --- a/README.md +++ b/README.md @@ -1,45 +1 @@ # .github - -## Reusable workflows - -### SonarQube manual scan (`sonarqube-manual.yaml`) - -On-demand SonarCloud analysis with a configurable quality profile. Intended for security-heavy profiles or full scans without slowing every PR. - -SonarCloud does not support selecting a quality profile via scanner arguments. This workflow temporarily assigns the requested profile to the project using the SonarCloud API, runs the scan, then restores the previous profile. - -**Consumer setup** — add a `workflow_dispatch` workflow in your repo: - -```yaml -jobs: - sonarqube: - uses: encodium/.github/.github/workflows/sonarqube-manual.yaml@main - with: - quality_profile: ${{ inputs.quality_profile }} - ref: ${{ inputs.ref }} - coverage_workflow_run_id: ${{ inputs.coverage_workflow_run_id }} - coverage_artifact_pattern: coverage* # repo-specific; see below - secrets: - sonar_token: ${{ secrets.SONAR_TOKEN }} -``` - -**Inputs** - -| Input | Required | Description | -| --- | --- | --- | -| `quality_profile` | yes | SonarCloud profile name (e.g. `Sonar way`) | -| `sonar_language` | no | Language key for profile assignment (default: `php`) | -| `ref` | no | Branch/tag/SHA; defaults to caller ref | -| `coverage_workflow_run_id` | no | GitHub Actions run ID to pull coverage artifacts from | -| `coverage_artifact_pattern` | no | Required with `coverage_workflow_run_id`; repo-specific glob | - -**Coverage artifact patterns** (set both `coverage_workflow_run_id` and `coverage_artifact_pattern`): - -| Repo workflow | Typical pattern | -| --- | --- | -| `encodium/common` unit tests | `coverage*` | -| `php-unit-test.yaml` reusable workflow | `phpunit-code-coverage-report` or `*coverage*` | - -**Run from GitHub UI:** Actions → your repo's manual Sonar workflow → Run workflow. - -For coverage, pass the run ID from a recent unit-test workflow (Actions → workflow run → copy ID from the URL).