From d37c45e3cace320c4b0cff3beddb867c3c4603b9 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Wed, 14 Oct 2026 19:56:33 -0400 Subject: [PATCH 1/4] CORE-6123: release run_sonar_scan 2.0.0 with coverage by pattern Callers merge per-package coverage in an extra job and pass a conditional artifact name. With coverage-artifact-pattern (default coverage-*) the workflow downloads every coverage- artifact itself, so callers need neither. A pattern that matches nothing downloads nothing without failing (download-artifact v7.0.0 and v8.0.2), so the job warns and adds a summary line instead of guarding the step. A single match lands directly in the path, so the README recommends the lcov wildcard .sonar-coverage/**/lcov.info, which SonarJS supports and which covers both layouts. The job becomes scan / "Scan" for the CI standard's check names, and the dependabot/renovate skip moves into the job. Co-Authored-By: Claude Opus 5.5 --- .../workflows/CHANGELOGS/run_sonar_scan.md | 23 ++++ .github/workflows/READMES/run_sonar_scan.md | 103 ++++++++++-------- .github/workflows/run_sonar_scan.yml | 68 +++++++++--- 3 files changed, 138 insertions(+), 56 deletions(-) diff --git a/.github/workflows/CHANGELOGS/run_sonar_scan.md b/.github/workflows/CHANGELOGS/run_sonar_scan.md index 0af1c9e..b58f7cc 100644 --- a/.github/workflows/CHANGELOGS/run_sonar_scan.md +++ b/.github/workflows/CHANGELOGS/run_sonar_scan.md @@ -2,6 +2,29 @@ All notable changes to the **run_sonar_scan** callable workflow are documented in this file. +## 2.0.0 + +### Added + +- `coverage-artifact-pattern` input (default `coverage-*`). Every same-run artifact whose name matches + is downloaded into `coverage-artifact-path`, one directory per artifact when more than one matches, + so callers no longer need a coverage merge job or a conditional artifact-name output. +- A warning and a job summary line when no coverage artifact matches; the scan still runs. +- A job summary listing the downloaded coverage files. + +### Changed + +- **Breaking:** the job is renamed from `sonar-scan` ("Run SonarQube scan") to `scan` ("Scan"), so the + check reads ` / Scan`. +- **Breaking:** `coverage-artifact-path` defaults to `.sonar-coverage` instead of `.`. +- The job skips runs triggered by `dependabot[bot]` or `renovate[bot]`; callers can drop that condition. +- Workflow permissions default to none, and the job requests only `contents: read`. +- The checkout no longer persists credentials. + +### Removed + +- **Breaking:** the `coverage-artifact-name` input. Use `coverage-artifact-pattern`. + ## 1.3.0 ### Added diff --git a/.github/workflows/READMES/run_sonar_scan.md b/.github/workflows/READMES/run_sonar_scan.md index 775c927..603af70 100644 --- a/.github/workflows/READMES/run_sonar_scan.md +++ b/.github/workflows/READMES/run_sonar_scan.md @@ -1,70 +1,78 @@ # Run SonarQube Scan -This reusable workflow checks out the ref that triggered the caller, or a specified commit or ref, and runs a SonarQube scan with full Git history. Callers can optionally download a coverage artifact produced earlier in the same workflow run before the scan starts. +This reusable workflow checks out the ref that triggered the caller, or a specified commit or ref, +downloads the coverage artifacts produced earlier in the same workflow run, and runs a SonarQube scan +with full Git history. Runs triggered by `dependabot[bot]` or `renovate[bot]` skip the scan. ## Prerequisites -The consuming repository must include its SonarQube configuration, such as a `sonar-project.properties` file, or otherwise provide configuration supported by the scan action. +The consuming repository must include its SonarQube configuration, such as a `sonar-project.properties` +file, or otherwise provide configuration supported by the scan action. ## Usage ```yaml jobs: - sonar-scan: - uses: OpenSesame/core-github-actions/.github/workflows/run_sonar_scan.yml@workflows/run_sonar_scan/1.3.0 + sonar: + name: Sonar + needs: [ci-context, app-ci] + if: ${{ !cancelled() && needs.ci-context.outputs.should-run-ci == 'true' }} + uses: OpenSesame/core-github-actions/.github/workflows/run_sonar_scan.yml@ # workflows/run_sonar_scan/2.0.0 + permissions: + contents: read # Required to check out the code to scan. + with: + commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} ``` -To scan a different commit, tag, or branch, pass `commit-identifier`: +The check is reported as `Sonar / Scan`. + +## Coverage + +Upload each package's lcov report as an artifact named `coverage-`, including in +single-package repositories: ```yaml -with: - commit-identifier: ${{ github.sha }} +- name: Upload coverage + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-user-management-api + path: lambdas/user-management-api/coverage/lcov.info + if-no-files-found: error ``` -To import coverage produced by another job, upload the report as an artifact and make the Sonar job depend on the test job: +The workflow downloads every artifact whose name matches `coverage-artifact-pattern` (default +`coverage-*`) into `coverage-artifact-path` (default `.sonar-coverage`): -```yaml -jobs: - unit-tests: - runs-on: ubuntu-26.04 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install dependencies - run: npm ci - - - name: Run tests with coverage - run: npm test -- --coverage - - - name: Upload coverage - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: unit-test-coverage - path: coverage/lcov.info - if-no-files-found: error - - sonar-scan: - needs: unit-tests - uses: OpenSesame/core-github-actions/.github/workflows/run_sonar_scan.yml@ - with: - coverage-artifact-name: unit-test-coverage - coverage-artifact-path: .coverage - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} +- When two or more artifacts match, each is downloaded into its own directory: + `.sonar-coverage/coverage-/lcov.info`. +- When exactly one artifact matches, `actions/download-artifact` places its files directly in the path: + `.sonar-coverage/lcov.info`. +- When no artifact matches, the download succeeds with nothing downloaded. The job emits a warning and a + job summary line, and the scan runs without coverage. + +Point SonarQube at every layout with one wildcard, the same value in every repository: + +```properties +sonar.javascript.lcov.reportPaths=.sonar-coverage/**/lcov.info ``` -This downloads the report to `.coverage/lcov.info`. Configure SonarQube to read that location, for example with `sonar.javascript.lcov.reportPaths=.coverage/lcov.info`. The workflow does not run tests or generate coverage. If `coverage-artifact-name` is set but the artifact does not exist, the download step fails the job. +SonarJS resolves this property with `WildcardPattern`, where `**/` matches zero or more directories, so +the pattern covers both the single-artifact and multi-artifact layouts. SonarJS also matches each +lcov `SF:` path to a project file by path suffix, so a report generated inside +`lambdas/` with paths like `SF:src/handler.ts` maps to `lambdas//src/handler.ts`. + +Set `coverage-artifact-pattern: ''` to skip the coverage download entirely. The workflow does not run +tests or generate coverage. ## Inputs -| Input | Type | Required | Default | Description | -| ------------------------ | ------ | -------- | --------------------- | --------------------------------------------------------------------------- | -| `commit-identifier` | string | No | Triggering ref or SHA | Commit SHA, tag, or branch to check out and scan. | -| `coverage-artifact-name` | string | No | `''` | Name of a coverage artifact uploaded earlier in the same workflow run. | -| `coverage-artifact-path` | string | No | `.` | Directory into which the coverage artifact is downloaded before the scan. | +| Input | Type | Required | Default | Description | +| --------------------------- | ------ | -------- | --------------------- | -------------------------------------------------------------------------------------- | +| `commit-identifier` | string | No | Triggering ref or SHA | Commit SHA, tag, or branch to check out and scan. | +| `coverage-artifact-pattern` | string | No | `coverage-*` | Glob of same-run artifact names to download before the scan. Empty skips the download. | +| `coverage-artifact-path` | string | No | `.sonar-coverage` | Directory into which matching coverage artifacts are downloaded. | ## Secrets @@ -72,6 +80,14 @@ This downloads the report to `.coverage/lcov.info`. Configure SonarQube to read | ------------- | -------- | ---------------------------------------------- | | `SONAR_TOKEN` | Yes | Token used to authenticate the SonarQube scan. | +## Permissions + +| Permission | Reason | +| ---------- | ----------------------------------- | +| `contents` | `read`: check out the code to scan. | + +Downloading artifacts from the same workflow run needs no extra permission. + The workflow job runs on Ubuntu 26.04. ## Contribution @@ -82,3 +98,4 @@ The workflow job runs on Ubuntu 26.04. ## References - [SonarQube scan action](https://github.com/SonarSource/sonarqube-scan-action) +- [actions/download-artifact](https://github.com/actions/download-artifact) diff --git a/.github/workflows/run_sonar_scan.yml b/.github/workflows/run_sonar_scan.yml index d40dfe1..ede30cc 100644 --- a/.github/workflows/run_sonar_scan.yml +++ b/.github/workflows/run_sonar_scan.yml @@ -8,42 +8,84 @@ on: type: string required: false default: '' - coverage-artifact-name: - description: Name of a coverage artifact uploaded earlier in the same workflow run + coverage-artifact-pattern: + description: Glob of coverage artifact names uploaded earlier in the same workflow run; empty skips the download type: string required: false - default: '' + default: coverage-* coverage-artifact-path: - description: Directory into which the coverage artifact is downloaded + description: Directory into which matching coverage artifacts are downloaded type: string required: false - default: '.' + default: .sonar-coverage secrets: SONAR_TOKEN: required: true -permissions: - contents: read +permissions: {} + +defaults: + run: + shell: bash jobs: - sonar-scan: - name: Run SonarQube scan + scan: + name: Scan + if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]' }} runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the code to scan. steps: - - name: Checkout + - name: Check out code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.commit-identifier }} fetch-depth: 0 + persist-credentials: false - - name: Download coverage artifact - if: ${{ inputs.coverage-artifact-name != '' }} + - name: Download coverage artifacts + if: ${{ inputs.coverage-artifact-pattern != '' }} uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - name: ${{ inputs.coverage-artifact-name }} + pattern: ${{ inputs.coverage-artifact-pattern }} path: ${{ inputs.coverage-artifact-path }} + - name: Report coverage files + if: ${{ inputs.coverage-artifact-pattern != '' }} + env: + COVERAGE_PATTERN: ${{ inputs.coverage-artifact-pattern }} + COVERAGE_PATH: ${{ inputs.coverage-artifact-path }} + run: | + set -Eeuo pipefail + + files="" + if [[ -d "$COVERAGE_PATH" ]]; then + files=$(find "$COVERAGE_PATH" -type f | sort) + fi + + if [[ -z "$files" ]]; then + echo "::warning title=Coverage::No coverage artifact matched '$COVERAGE_PATTERN'; Sonar will report no coverage" + { + echo "## Coverage" + echo "" + echo "⚠️ No coverage artifact matched \`$COVERAGE_PATTERN\`; Sonar will report no coverage." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + echo "Coverage files downloaded:" + echo "$files" + { + echo "## Coverage" + echo "" + echo "Artifacts matching \`$COVERAGE_PATTERN\` downloaded to \`$COVERAGE_PATH\`:" + echo "" + while IFS= read -r file; do + echo "- \`$file\`" + done <<<"$files" + } >> "$GITHUB_STEP_SUMMARY" + - name: Run SonarQube scan uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0 env: From 6cb0b971d2040885329c2734fe1d2f6231f936fc Mon Sep 17 00:00:00 2001 From: ssvoss Date: Mon, 19 Oct 2026 19:22:58 -0400 Subject: [PATCH 2/4] CORE-6123: only report downloaded lcov files as Sonar coverage The coverage report listed every file under coverage-artifact-path, so a caller passing "." would see repository files reported as coverage and get no warning, and an artifact without lcov.info counted as coverage. The job now fails if the path already has content before the download, and reports only lcov.info files. The README claimed SonarJS maps package-relative SF: paths by suffix. It resolves them against the report's directory first, and this workflow moves reports into .sonar-coverage/, so it falls back to a suffix match that guesses when two packages share a path (SonarJS CoverageSensorDuplicateSuffixPathReproducerTest). The README now asks callers for repository-relative paths via the lcov reporter's projectRoot option. Co-Authored-By: Claude Opus 5.5 --- .../workflows/CHANGELOGS/run_sonar_scan.md | 5 ++- .github/workflows/READMES/run_sonar_scan.md | 31 ++++++++++++++++--- .github/workflows/run_sonar_scan.yml | 16 ++++++++-- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CHANGELOGS/run_sonar_scan.md b/.github/workflows/CHANGELOGS/run_sonar_scan.md index b58f7cc..7ab1149 100644 --- a/.github/workflows/CHANGELOGS/run_sonar_scan.md +++ b/.github/workflows/CHANGELOGS/run_sonar_scan.md @@ -9,7 +9,10 @@ All notable changes to the **run_sonar_scan** callable workflow are documented i - `coverage-artifact-pattern` input (default `coverage-*`). Every same-run artifact whose name matches is downloaded into `coverage-artifact-path`, one directory per artifact when more than one matches, so callers no longer need a coverage merge job or a conditional artifact-name output. -- A warning and a job summary line when no coverage artifact matches; the scan still runs. +- A warning and a job summary line when no downloaded artifact contains an `lcov.info`; the scan still + runs. +- A check that fails the job when `coverage-artifact-path` already exists with content in the + checkout, so repository files are never reported as coverage. - A job summary listing the downloaded coverage files. ### Changed diff --git a/.github/workflows/READMES/run_sonar_scan.md b/.github/workflows/READMES/run_sonar_scan.md index 603af70..aa0eb0c 100644 --- a/.github/workflows/READMES/run_sonar_scan.md +++ b/.github/workflows/READMES/run_sonar_scan.md @@ -49,8 +49,10 @@ The workflow downloads every artifact whose name matches `coverage-artifact-patt `.sonar-coverage/coverage-/lcov.info`. - When exactly one artifact matches, `actions/download-artifact` places its files directly in the path: `.sonar-coverage/lcov.info`. -- When no artifact matches, the download succeeds with nothing downloaded. The job emits a warning and a - job summary line, and the scan runs without coverage. +- When no artifact matches, or the matching artifacts contain no `lcov.info`, the job emits a warning + and a job summary line, and the scan runs without coverage. +- `coverage-artifact-path` must not already exist with content in the checkout; the job fails if it + does, so repository files are never mistaken for downloaded coverage. Point SonarQube at every layout with one wildcard, the same value in every repository: @@ -59,9 +61,28 @@ sonar.javascript.lcov.reportPaths=.sonar-coverage/**/lcov.info ``` SonarJS resolves this property with `WildcardPattern`, where `**/` matches zero or more directories, so -the pattern covers both the single-artifact and multi-artifact layouts. SonarJS also matches each -lcov `SF:` path to a project file by path suffix, so a report generated inside -`lambdas/` with paths like `SF:src/handler.ts` maps to `lambdas//src/handler.ts`. +the pattern covers both the single-artifact and multi-artifact layouts. + +### Repository-relative `SF:` paths + +Write each lcov report with paths relative to the repository root, such as +`SF:lambdas/user-management-api/src/handler.ts`. By default Jest (Istanbul) writes paths relative to +the directory the tests run in (`SF:src/handler.ts`). SonarJS can only resolve those against the +report's own directory, and this workflow moves every report into `.sonar-coverage/`. It then falls +back to matching by path suffix and guesses when two packages share a path such as `src/index.ts`, so +coverage can land on the wrong file. Set the lcov reporter's `projectRoot` in each package's Jest +config: + +```js +// lambdas/user-management-api/jest.config.js +const path = require('node:path'); + +module.exports = { + coverageReporters: [['lcov', { projectRoot: path.resolve(__dirname, '../..') }], 'text'], +}; +``` + +Single-package repositories that run Jest from the repository root already get root-relative paths. Set `coverage-artifact-pattern: ''` to skip the coverage download entirely. The workflow does not run tests or generate coverage. diff --git a/.github/workflows/run_sonar_scan.yml b/.github/workflows/run_sonar_scan.yml index ede30cc..a2b49c1 100644 --- a/.github/workflows/run_sonar_scan.yml +++ b/.github/workflows/run_sonar_scan.yml @@ -44,6 +44,16 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Check coverage path is empty + if: ${{ inputs.coverage-artifact-pattern != '' }} + env: + COVERAGE_PATH: ${{ inputs.coverage-artifact-path }} + run: | + if [[ -e "$COVERAGE_PATH" && -n "$(ls -A "$COVERAGE_PATH" 2>/dev/null)" ]]; then + echo "::error title=Coverage::coverage-artifact-path '$COVERAGE_PATH' already exists in the checkout and is not empty. Use a directory that only holds downloaded coverage, such as the default .sonar-coverage." + exit 1 + fi + - name: Download coverage artifacts if: ${{ inputs.coverage-artifact-pattern != '' }} uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 @@ -61,15 +71,15 @@ jobs: files="" if [[ -d "$COVERAGE_PATH" ]]; then - files=$(find "$COVERAGE_PATH" -type f | sort) + files=$(find "$COVERAGE_PATH" -type f -name lcov.info | sort) fi if [[ -z "$files" ]]; then - echo "::warning title=Coverage::No coverage artifact matched '$COVERAGE_PATTERN'; Sonar will report no coverage" + echo "::warning title=Coverage::No lcov.info found in artifacts matching '$COVERAGE_PATTERN'; Sonar will report no coverage" { echo "## Coverage" echo "" - echo "⚠️ No coverage artifact matched \`$COVERAGE_PATTERN\`; Sonar will report no coverage." + echo "⚠️ No \`lcov.info\` found in artifacts matching \`$COVERAGE_PATTERN\`; Sonar will report no coverage." } >> "$GITHUB_STEP_SUMMARY" exit 0 fi From bc05947e60b12b5a9c94a362024c710edd734952 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Mon, 19 Oct 2026 19:26:19 -0400 Subject: [PATCH 3/4] CORE-6123: base the lcov projectRoot rule on where Jest runs Repository-relative SF: paths are needed whenever Jest runs outside the repository root, not only in multi-package repos. identity-auth has one package but runs Jest from lambdas/auth-api. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/READMES/run_sonar_scan.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/READMES/run_sonar_scan.md b/.github/workflows/READMES/run_sonar_scan.md index aa0eb0c..0fd8dad 100644 --- a/.github/workflows/READMES/run_sonar_scan.md +++ b/.github/workflows/READMES/run_sonar_scan.md @@ -69,9 +69,9 @@ Write each lcov report with paths relative to the repository root, such as `SF:lambdas/user-management-api/src/handler.ts`. By default Jest (Istanbul) writes paths relative to the directory the tests run in (`SF:src/handler.ts`). SonarJS can only resolve those against the report's own directory, and this workflow moves every report into `.sonar-coverage/`. It then falls -back to matching by path suffix and guesses when two packages share a path such as `src/index.ts`, so -coverage can land on the wrong file. Set the lcov reporter's `projectRoot` in each package's Jest -config: +back to matching by path suffix and guesses when more than one file ends with that path, such as +`src/index.ts` in two directories, so coverage can land on the wrong file. Set the lcov reporter's `projectRoot` in the Jest config of every +package whose tests don't run from the repository root: ```js // lambdas/user-management-api/jest.config.js @@ -82,7 +82,9 @@ module.exports = { }; ``` -Single-package repositories that run Jest from the repository root already get root-relative paths. +This applies whenever Jest runs outside the repository root, including single-package repositories +whose code lives in a subdirectory (for example `lambdas/auth-api`). Only repositories that run Jest +from the root already get root-relative paths. Set `coverage-artifact-pattern: ''` to skip the coverage download entirely. The workflow does not run tests or generate coverage. From 3af603f22298da42a3c441428fc97d46b1893f38 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Mon, 19 Oct 2026 19:48:17 -0400 Subject: [PATCH 4/4] CORE-6123: trim the run_sonar_scan README to setup steps Engineers need the call, the three coverage setup steps and the inputs. The download layouts and SonarJS path-resolution rationale stay in the PR and commit history. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/READMES/run_sonar_scan.md | 122 ++++++-------------- 1 file changed, 35 insertions(+), 87 deletions(-) diff --git a/.github/workflows/READMES/run_sonar_scan.md b/.github/workflows/READMES/run_sonar_scan.md index 0fd8dad..63208e1 100644 --- a/.github/workflows/READMES/run_sonar_scan.md +++ b/.github/workflows/READMES/run_sonar_scan.md @@ -1,13 +1,7 @@ # Run SonarQube Scan -This reusable workflow checks out the ref that triggered the caller, or a specified commit or ref, -downloads the coverage artifacts produced earlier in the same workflow run, and runs a SonarQube scan -with full Git history. Runs triggered by `dependabot[bot]` or `renovate[bot]` skip the scan. - -## Prerequisites - -The consuming repository must include its SonarQube configuration, such as a `sonar-project.properties` -file, or otherwise provide configuration supported by the scan action. +Runs a SonarQube scan with full Git history, importing coverage artifacts from the same workflow run. +Skips runs by `dependabot[bot]` and `renovate[bot]`. Requires the repo's `sonar-project.properties`. ## Usage @@ -30,95 +24,49 @@ The check is reported as `Sonar / Scan`. ## Coverage -Upload each package's lcov report as an artifact named `coverage-`, including in -single-package repositories: - -```yaml -- name: Upload coverage - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: coverage-user-management-api - path: lambdas/user-management-api/coverage/lcov.info - if-no-files-found: error -``` - -The workflow downloads every artifact whose name matches `coverage-artifact-pattern` (default -`coverage-*`) into `coverage-artifact-path` (default `.sonar-coverage`): - -- When two or more artifacts match, each is downloaded into its own directory: - `.sonar-coverage/coverage-/lcov.info`. -- When exactly one artifact matches, `actions/download-artifact` places its files directly in the path: - `.sonar-coverage/lcov.info`. -- When no artifact matches, or the matching artifacts contain no `lcov.info`, the job emits a warning - and a job summary line, and the scan runs without coverage. -- `coverage-artifact-path` must not already exist with content in the checkout; the job fails if it - does, so repository files are never mistaken for downloaded coverage. +1. Upload each package's lcov report as an artifact named `coverage-`: -Point SonarQube at every layout with one wildcard, the same value in every repository: + ```yaml + - name: Upload coverage + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-user-management-api + path: lambdas/user-management-api/coverage/lcov.info + if-no-files-found: error + ``` -```properties -sonar.javascript.lcov.reportPaths=.sonar-coverage/**/lcov.info -``` +2. If Jest runs outside the repo root, make lcov paths repo-relative so Sonar maps coverage to the + right files: -SonarJS resolves this property with `WildcardPattern`, where `**/` matches zero or more directories, so -the pattern covers both the single-artifact and multi-artifact layouts. + ```js + // lambdas/user-management-api/jest.config.js + module.exports = { + coverageReporters: [ + ['lcov', { projectRoot: require('node:path').resolve(__dirname, '../..') }], + 'text', + ], + }; + ``` -### Repository-relative `SF:` paths +3. Set the same report path in every repo: -Write each lcov report with paths relative to the repository root, such as -`SF:lambdas/user-management-api/src/handler.ts`. By default Jest (Istanbul) writes paths relative to -the directory the tests run in (`SF:src/handler.ts`). SonarJS can only resolve those against the -report's own directory, and this workflow moves every report into `.sonar-coverage/`. It then falls -back to matching by path suffix and guesses when more than one file ends with that path, such as -`src/index.ts` in two directories, so coverage can land on the wrong file. Set the lcov reporter's `projectRoot` in the Jest config of every -package whose tests don't run from the repository root: + ```properties + sonar.javascript.lcov.reportPaths=.sonar-coverage/**/lcov.info + ``` -```js -// lambdas/user-management-api/jest.config.js -const path = require('node:path'); - -module.exports = { - coverageReporters: [['lcov', { projectRoot: path.resolve(__dirname, '../..') }], 'text'], -}; -``` - -This applies whenever Jest runs outside the repository root, including single-package repositories -whose code lives in a subdirectory (for example `lambdas/auth-api`). Only repositories that run Jest -from the root already get root-relative paths. - -Set `coverage-artifact-pattern: ''` to skip the coverage download entirely. The workflow does not run -tests or generate coverage. +If no `lcov.info` is downloaded, the job warns and scans without coverage. ## Inputs -| Input | Type | Required | Default | Description | -| --------------------------- | ------ | -------- | --------------------- | -------------------------------------------------------------------------------------- | -| `commit-identifier` | string | No | Triggering ref or SHA | Commit SHA, tag, or branch to check out and scan. | -| `coverage-artifact-pattern` | string | No | `coverage-*` | Glob of same-run artifact names to download before the scan. Empty skips the download. | -| `coverage-artifact-path` | string | No | `.sonar-coverage` | Directory into which matching coverage artifacts are downloaded. | - -## Secrets - -| Secret | Required | Description | -| ------------- | -------- | ---------------------------------------------- | -| `SONAR_TOKEN` | Yes | Token used to authenticate the SonarQube scan. | - -## Permissions +| Input | Default | Description | +| --------------------------- | --------------------- | ------------------------------------------------------------------------ | +| `commit-identifier` | Triggering ref or SHA | Commit SHA, tag, or branch to scan. | +| `coverage-artifact-pattern` | `coverage-*` | Same-run artifact names to download. Empty skips coverage. | +| `coverage-artifact-path` | `.sonar-coverage` | Download directory. Must not already exist with content in the checkout. | -| Permission | Reason | -| ---------- | ----------------------------------- | -| `contents` | `read`: check out the code to scan. | - -Downloading artifacts from the same workflow run needs no extra permission. - -The workflow job runs on Ubuntu 26.04. +Secret `SONAR_TOKEN` is required. ## Contribution -- Update the workflow, README, and changelog together. -- Create a PR and set a version label following the [versioning instructions](../../../VERSIONING.md). - -## References - -- [SonarQube scan action](https://github.com/SonarSource/sonarqube-scan-action) -- [actions/download-artifact](https://github.com/actions/download-artifact) +Update the workflow, README, and changelog together, and set a version label per +[VERSIONING.md](../../../VERSIONING.md).