Compare commits

..

15 Commits

Author SHA1 Message Date
Julien HENRY 0368e05ab1 Fix dist source maps with correct relative paths
Built from worktree with local node_modules so source map paths match
CI build environment (../node_modules/ instead of ../../../../node_modules/).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 17:00:05 +02:00
Julien HENRY a30e39dd44 Use mise 2026-06-04 16:41:27 +02:00
Julien HENRY 751b8e8c7a SQSCANGHA-135 Fix scanner binaries always re-downloaded due to incompatible 4-part version
GitHub's tool-cache library uses semver.clean() to look up cached tools, which
returns null for 4-part version strings like "8.0.1.6346". This caused
findAllVersions() to filter out any cached directory, resulting in a cache miss
on every run.

The fix converts the 4-part version to a semver pre-release format
(e.g. "8.0.1-build.6346") for tool-cache operations, while keeping the original
version string for download URLs and zip extraction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:35:44 +02:00
Julien HENRY b243e5198f SQSCANGHA-88 Deprecate the SONARCLOUD_URL env variable support
Emit a warning when SONARCLOUD_URL is set, directing users to either
pass nothing, use SONAR_REGION=us for the US region, or pass
-Dsonar.scanner.sonarcloudUrl and -Dsonar.scanner.apiBaseUrl via args
for advanced needs. Backward compatibility is preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:34:28 +02:00
Julien HENRY 375c3f5c03 SQSCANGHA-149 Add scannerBinariesAuthHeader input for authenticated binary downloads
Organisations using private Artifactory mirrors require authentication to
download the SonarScanner CLI. This adds an optional scannerBinariesAuthHeader
input whose value is forwarded as the Authorization HTTP header to both the
binary and GPG signature downloads via tc.downloadTool's built-in auth
parameter. No new dependencies are introduced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 14:19:55 +02:00
Julien HENRY 9c783232fe SQSCANGHA-144 Add gate jobs to QA workflows for branch protection
Add a non-matrix gate job to qa-main, qa-deprecated-c-cpp, and
qa-install-build-wrapper workflows. Each gate job depends on all
other jobs in its workflow and provides a single stable check context
that can be used in GitHub branch protection required status checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 09:32:35 +02:00
SonarTech 7006c4492b Update SonarScanner CLI to 8.1.0.6389 2026-05-19 09:24:23 +02:00
dependabot[bot] edd319f284 NO-JIRA Bump actions/setup-node from 6.3.0 to 6.4.0 (#234)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 09:23:47 +02:00
dependabot[bot] e050aa9e69 NO-JIRA Bump actions/cache from 5.0.4 to 5.0.5 (#231)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 09:23:25 +02:00
dependabot[bot] 6cd3d8f2ae NO-JIRA Bump madhead/semver-utils from 4.3.0 to 5.0.0
Bumps [madhead/semver-utils](https://github.com/madhead/semver-utils) from 4.3.0 to 5.0.0.
- [Release notes](https://github.com/madhead/semver-utils/releases)
- [Commits](https://github.com/madhead/semver-utils/compare/36d1e0ed361bd7b4b77665de8093092eaeabe6ba...4cf918affe9106ea59f86c6250e5ec4570ac4389)

---
updated-dependencies:
- dependency-name: madhead/semver-utils
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 09:20:24 +02:00
Julien HENRY 56568530ed SQSCANGHA-146 Add proxy support for GPG keyserver access (#244)
Co-authored-by: Marius Boden <marius.boden@xebia.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:11:36 +02:00
Claire Villard c444753899 SQSCANGHA-140 Add the missing requirements in README.md (#243) 2026-05-11 12:13:30 +02:00
Antoine Vinot 59db25f34e SQSCANGHA-145 Set skipSignatureVerification default value to false (#241) 2026-04-29 14:23:12 +02:00
Pavel Mikula ca30b65f4e SQSCANGHA-143 SubmitReview: Use Vault token (#238) 2026-04-29 11:16:25 +02:00
Antoine Vinot c7ee0f9df9 SQSCANGHA-140 Set skipSignatureVerification default value to true to avoid breaking change (#240)
Co-authored-by: Gustavo Cunha <dev@gustavocunha.dev>
2026-04-29 10:13:05 +02:00
28 changed files with 923 additions and 57840 deletions
+2 -2
View File
@@ -10,7 +10,6 @@ jobs:
runs-on: github-ubuntu-latest-s runs-on: github-ubuntu-latest-s
permissions: permissions:
id-token: write id-token: write
pull-requests: read
# For external PR, ticket should be moved manually # For external PR, ticket should be moved manually
if: | if: |
github.event.pull_request.head.repo.full_name == github.repository github.event.pull_request.head.repo.full_name == github.repository
@@ -21,10 +20,11 @@ jobs:
uses: SonarSource/vault-action-wrapper@v3 uses: SonarSource/vault-action-wrapper@v3
with: with:
secrets: | secrets: |
development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN;
development/kv/data/jira user | JIRA_USER; development/kv/data/jira user | JIRA_USER;
development/kv/data/jira token | JIRA_TOKEN; development/kv/data/jira token | JIRA_TOKEN;
- uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2 - uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }}
jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
+14
View File
@@ -88,3 +88,17 @@ jobs:
BINARY: ${{ steps.run-action.outputs.build-wrapper-binary }} BINARY: ${{ steps.run-action.outputs.build-wrapper-binary }}
run: | run: |
("$BINARY" || true) | grep "build-wrapper, version " ("$BINARY" || true) | grep "build-wrapper, version "
qa-gate:
name: QA Deprecated C and C++ - gate
runs-on: ubuntu-latest
needs: [output-test]
if: always()
steps:
- name: Check all jobs passed
run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or were cancelled."
exit 1
fi
echo "All checks passed."
@@ -70,3 +70,17 @@ jobs:
BINARY: ${{ steps.run-action.outputs.build-wrapper-binary }} BINARY: ${{ steps.run-action.outputs.build-wrapper-binary }}
run: | run: |
("$BINARY" || true) | grep "build-wrapper, version " ("$BINARY" || true) | grep "build-wrapper, version "
qa-gate:
name: QA Install Build Wrapper - gate
runs-on: ubuntu-latest
needs: [output-test]
if: always()
steps:
- name: Check all jobs passed
run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or were cancelled."
exit 1
fi
echo "All checks passed."
+40 -3
View File
@@ -433,7 +433,7 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: SonarQube Cache - name: SonarQube Cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
path: ${{ github.workspace }}/.sonar/cache path: ${{ github.workspace }}/.sonar/cache
key: ${{ runner.os }}-${{ runner.arch }}-sonar key: ${{ runner.os }}-${{ runner.arch }}-sonar
@@ -451,7 +451,7 @@ jobs:
./test/assertFileExists ./test/example-project/.scannerwork/report-task.txt ./test/assertFileExists ./test/example-project/.scannerwork/report-task.txt
overrideSonarcloudUrlTest: overrideSonarcloudUrlTest:
name: > name: >
'SONARCLOUD_URL' is used Deprecated 'SONARCLOUD_URL' still works and emits a deprecation warning
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -461,7 +461,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Run action with SONARCLOUD_URL - name: Run action with deprecated SONARCLOUD_URL
uses: ./ uses: ./
with: with:
args: -Dsonar.scanner.apiBaseUrl=api.mirror.sonarcloud.io -Dsonar.scanner.internal.dumpToFile=./output.properties args: -Dsonar.scanner.apiBaseUrl=api.mirror.sonarcloud.io -Dsonar.scanner.internal.dumpToFile=./output.properties
@@ -827,3 +827,40 @@ jobs:
run: | run: |
echo "Action with invalid scannerVersion should have failed but succeeded" echo "Action with invalid scannerVersion should have failed but succeeded"
exit 1 exit 1
qa-gate:
name: QA Main - gate
runs-on: ubuntu-latest
needs:
- noInputsTest
- argsInputTest
- argsInputInjectionTest
- backtickCommandInjectionTest
- dollarSymbolCommandInjectionTest
- otherCommandInjectionVariantsTest
- projectBaseDirInputTest
- scannerVersionTest
- scannerBinariesUrlTest
- scannerBinariesUrlIsEscapedWithWget
- scannerBinariesUrlIsEscapedWithCurl
- dontFailGradleTest
- dontFailGradleKotlinTest
- dontFailMavenTest
- runAnalysisTest
- runnerDebugUsedTest
- runAnalysisWithCacheTest
- overrideSonarcloudUrlTest
- curlPerformsRedirect
- useSslCertificate
- analysisWithSslCertificate
- updateTruststoreWhenPresent
- scannerVersionValidationTest
if: always()
steps:
- name: Check all jobs passed
run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or were cancelled."
exit 1
fi
echo "All checks passed."
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e #v6.4.0
with: with:
node-version: "24" node-version: "24"
cache: "npm" cache: "npm"
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Parse semver - name: Parse semver
uses: madhead/semver-utils@36d1e0ed361bd7b4b77665de8093092eaeabe6ba # v4.3.0 uses: madhead/semver-utils@4cf918affe9106ea59f86c6250e5ec4570ac4389 # v5.0.0
id: version id: version
with: with:
version: ${{ github.ref_name }} version: ${{ github.ref_name }}
+35
View File
@@ -200,6 +200,39 @@ This can be useful when the runner executing the action is self-hosted and has r
scannerBinariesUrl: https://my.custom.binaries.url.com/Distribution/sonar-scanner-cli/ scannerBinariesUrl: https://my.custom.binaries.url.com/Distribution/sonar-scanner-cli/
``` ```
#### `scannerBinariesAuthHeader`
If the server specified by `scannerBinariesUrl` requires authentication, you can provide an `Authorization` header value using the `scannerBinariesAuthHeader` option.
The value is passed directly as the `Authorization` HTTP header, so you must include the scheme (e.g. `Bearer`, `Basic`):
```yaml
- uses: SonarSource/sonarqube-scan-action@<action version>
with:
scannerBinariesUrl: https://my.custom.binaries.url.com/Distribution/sonar-scanner-cli/
scannerBinariesAuthHeader: ${{ secrets.BINARIES_AUTH_HEADER }}
```
Store the full header value (e.g. `Bearer mytoken`) in the GitHub secret to avoid exposing credentials.
#### `skipSignatureVerification`
By default, the action verifies the OpenPGP signature of the SonarScanner CLI binary before executing it. You can disable this verification using the `skipSignatureVerification` option:
```yaml
- uses: SonarSource/sonarqube-scan-action@<action version>
with:
skipSignatureVerification: true
```
> [!NOTE]
> Signature verification requires `gpg` and `dirmngr` to be installed on the runner. GitHub-hosted runners include both, but some self-hosted runners or containers may not.
>
> If your runner accesses the internet through a proxy, the action automatically picks up the `HTTPS_PROXY` or `https_proxy` environment variable when fetching the public key from the keyserver. `HTTP_PROXY` is intentionally not used as a fallback, since keyservers are accessed over TLS (`hkps://`).
>
> **Version history:**
> - Introduced in **v7.2** with a default value of `true` to avoid breaking existing workflows on runners without `dirmngr`.
> - Changed to `false` by default in **v8** (breaking change). If your runner does not have `gpg` or `dirmngr` installed, set this option to `true` explicitly.
More information about possible analysis parameters can be found: More information about possible analysis parameters can be found:
* in the [Analysis parameters page](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/analysis-parameters/) of the SonarQube Server documentation * in the [Analysis parameters page](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/analysis-parameters/) of the SonarQube Server documentation
* in the [Analysis parameters page](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/analysis-parameters/) of the SonarQube Cloud documentation * in the [Analysis parameters page](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/analysis-parameters/) of the SonarQube Cloud documentation
@@ -452,6 +485,8 @@ When running the action in a self-hosted runner or container, please ensure that
* **curl** or **wget** * **curl** or **wget**
* **unzip** * **unzip**
* **gpg**
* **dirmngr**
### Additional information ### Additional information
+8 -1
View File
@@ -19,7 +19,7 @@ inputs:
description: Version of the Sonar Scanner CLI to use description: Version of the Sonar Scanner CLI to use
required: false required: false
# to be kept in sync with sonar-scanner-version # to be kept in sync with sonar-scanner-version
default: 8.0.1.6346 default: 8.1.0.6389
scannerBinariesUrl: scannerBinariesUrl:
description: URL to download the Sonar Scanner CLI binaries from description: URL to download the Sonar Scanner CLI binaries from
required: false required: false
@@ -28,6 +28,13 @@ inputs:
description: Skip GPG signature verification (not recommended for security) description: Skip GPG signature verification (not recommended for security)
required: false required: false
default: "false" default: "false"
scannerBinariesAuthHeader:
description: >
Authorization header value to use when downloading the SonarScanner CLI binaries
(e.g. 'Bearer mytoken' or 'Basic base64creds'). Use this when scannerBinariesUrl
points to a private server that requires authentication.
required: false
default: ""
runs: runs:
using: node24 using: node24
main: dist/index.js main: dist/index.js
-30514
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-27277
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -31829,5 +31829,5 @@ function requireExec () {
var execExports = requireExec(); var execExports = requireExec();
export { HttpClient as H, info as a, isDebug as b, cp as c, debug as d, exists as e, execExports as f, warning as g, addPath as h, isRooted as i, getInput as j, getBooleanInput as k, core as l, mkdirP as m, setOutput as n, startGroup as o, endGroup as p, rmRF as r, setFailed as s, which as w }; export { HttpClient as H, info as a, isDebug as b, cp as c, debug as d, exists as e, execExports as f, warning as g, addPath as h, isRooted as i, setFailed as j, getInput as k, getBooleanInput as l, mkdirP as m, core as n, setOutput as o, startGroup as p, endGroup as q, rmRF as r, setSecret as s, which as w };
//# sourceMappingURL=exec-zlpfwmpH.js.map //# sourceMappingURL=exec-BeYcktvA.js.map
File diff suppressed because one or more lines are too long
+65 -8
View File
@@ -1,4 +1,4 @@
import { i as isRooted, w as which, e as exists, a as info, d as debug, m as mkdirP, c as cp, H as HttpClient, r as rmRF, b as isDebug, f as execExports, g as warning, h as addPath, s as setFailed, j as getInput, k as getBooleanInput, l as core } from './exec-zlpfwmpH.js'; import { i as isRooted, w as which, e as exists, a as info, d as debug, m as mkdirP, c as cp, H as HttpClient, r as rmRF, b as isDebug, f as execExports, g as warning, s as setSecret, h as addPath, j as setFailed, k as getInput, l as getBooleanInput, n as core } from './exec-BeYcktvA.js';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
@@ -3503,6 +3503,13 @@ function downloadToolAttempt(url, dest, auth, headers) {
const http = new HttpClient(userAgent, [], { const http = new HttpClient(userAgent, [], {
allowRetries: false allowRetries: false
}); });
if (auth) {
debug('set auth');
if (headers === undefined) {
headers = {};
}
headers.authorization = auth;
}
const response = yield http.get(url, headers); const response = yield http.get(url, headers);
if (response.message.statusCode !== 200) { if (response.message.statusCode !== 200) {
const err = new HTTPError(response.message.statusCode); const err = new HTTPError(response.message.statusCode);
@@ -3855,6 +3862,19 @@ function getScannerDownloadURL({
const scannerDirName = (version, flavor) => const scannerDirName = (version, flavor) =>
`sonar-scanner-${version}-${flavor}`; `sonar-scanner-${version}-${flavor}`;
/**
* Converts a 4-part version string (e.g. "8.0.1.6346") to a SemVer 2.0 compatible
* string (e.g. "8.0.1-build.6346") for use with GitHub's tool-cache library,
* which requires SemVer-compliant version strings.
*/
function toSemVer(version) {
const parts = version.split(".");
if (parts.length === 4) {
return `${parts[0]}.${parts[1]}.${parts[2]}-build.${parts[3]}`;
}
return version;
}
/* /*
* sonarqube-scan-action * sonarqube-scan-action
* Copyright (C) 2025 SonarSource SA * Copyright (C) 2025 SonarSource SA
@@ -3977,6 +3997,18 @@ function setupGpgHome() {
return gpgHome; return gpgHome;
} }
/**
* Detects HTTPS proxy from environment variables.
* Checks both upper and lower case variants (HTTPS_PROXY, https_proxy).
* Only HTTPS proxy is used since keyservers use hkps:// (TLS).
* HTTP_PROXY is intentionally not used as a fallback to avoid routing
* HTTPS traffic through a proxy not intended for TLS connections.
* @returns {string|undefined} Proxy URL or undefined if not set
*/
function getProxyFromEnv() {
return process.env.HTTPS_PROXY || process.env.https_proxy;
}
/** /**
* Attempts to import a public key from a specific keyserver * Attempts to import a public key from a specific keyserver
* @param {string} gpgHome - Path to GPG home directory * @param {string} gpgHome - Path to GPG home directory
@@ -3988,6 +4020,15 @@ function setupGpgHome() {
async function tryImportKey(gpgHome, keyFingerprint, keyserver) { async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
const gpgCommand = getGpgCommand(); const gpgCommand = getGpgCommand();
const gpgHomePath = convertToUnixPath(gpgHome); const gpgHomePath = convertToUnixPath(gpgHome);
const proxyUrl = getProxyFromEnv();
if (proxyUrl) {
// The URL may carry credentials (e.g. http://user:pass@proxy:8080).
// Register it as a secret so future logging (here or downstream) is
// automatically redacted
setSecret(proxyUrl);
info("Using HTTPS_PROXY for keyserver access");
}
await execExports.exec( await execExports.exec(
gpgCommand, gpgCommand,
@@ -3997,6 +4038,7 @@ async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
"--batch", "--batch",
"--keyserver", "--keyserver",
keyserver, keyserver,
...(proxyUrl ? ["--keyserver-options", `http-proxy=${proxyUrl}`] : []),
"--recv-keys", "--recv-keys",
keyFingerprint, keyFingerprint,
], ],
@@ -4118,12 +4160,14 @@ const TOOLNAME = "sonar-scanner-cli";
async function installSonarScanner({ async function installSonarScanner({
scannerVersion, scannerVersion,
scannerBinariesUrl, scannerBinariesUrl,
scannerBinariesAuthHeader,
skipSignatureVerification = false, skipSignatureVerification = false,
}) { }) {
const flavor = getPlatformFlavor(os$1.platform(), os$1.arch()); const flavor = getPlatformFlavor(os$1.platform(), os$1.arch());
const semVerVersion = toSemVer(scannerVersion);
// Check if tool is already cached // Check if tool is already cached
let toolDir = find(TOOLNAME, scannerVersion, flavor); let toolDir = find(TOOLNAME, semVerVersion, flavor);
if (!toolDir) { if (!toolDir) {
info( info(
@@ -4138,7 +4182,7 @@ async function installSonarScanner({
info(`Downloading from: ${downloadUrl}`); info(`Downloading from: ${downloadUrl}`);
const downloadPath = await downloadTool(downloadUrl); const downloadPath = await downloadTool(downloadUrl, undefined, scannerBinariesAuthHeader);
if (skipSignatureVerification) { if (skipSignatureVerification) {
warning("⚠ Skipping GPG signature verification (not recommended)"); warning("⚠ Skipping GPG signature verification (not recommended)");
@@ -4148,7 +4192,7 @@ async function installSonarScanner({
let signaturePath; let signaturePath;
try { try {
signaturePath = await downloadTool(signatureUrl); signaturePath = await downloadTool(signatureUrl, undefined, scannerBinariesAuthHeader);
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Failed to download signature file from ${signatureUrl}: ${error.message}` `Failed to download signature file from ${signatureUrl}: ${error.message}`
@@ -4166,7 +4210,7 @@ async function installSonarScanner({
scannerDirName(scannerVersion, flavor) scannerDirName(scannerVersion, flavor)
); );
toolDir = await cacheDir(scannerPath, TOOLNAME, scannerVersion, flavor); toolDir = await cacheDir(scannerPath, TOOLNAME, semVerVersion, flavor);
info(`Sonar Scanner CLI cached to: ${toolDir}`); info(`Sonar Scanner CLI cached to: ${toolDir}`);
} else { } else {
@@ -4467,10 +4511,14 @@ function getInputs() {
const args = getInput("args"); const args = getInput("args");
const projectBaseDir = getInput("projectBaseDir"); const projectBaseDir = getInput("projectBaseDir");
const scannerBinariesUrl = getInput("scannerBinariesUrl"); const scannerBinariesUrl = getInput("scannerBinariesUrl");
const scannerBinariesAuthHeader = getInput("scannerBinariesAuthHeader") || undefined;
if (scannerBinariesAuthHeader) {
setSecret(scannerBinariesAuthHeader);
}
const scannerVersion = getInput("scannerVersion"); const scannerVersion = getInput("scannerVersion");
const skipSignatureVerification = getBooleanInput("skipSignatureVerification"); const skipSignatureVerification = getBooleanInput("skipSignatureVerification");
return { args, projectBaseDir, scannerBinariesUrl, scannerVersion, skipSignatureVerification }; return { args, projectBaseDir, scannerBinariesUrl, scannerBinariesAuthHeader, scannerVersion, skipSignatureVerification };
} }
/** /**
@@ -4506,16 +4554,25 @@ function runSanityChecks(inputs) {
async function run() { async function run() {
try { try {
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, skipSignatureVerification } = const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, scannerBinariesAuthHeader, skipSignatureVerification } =
getInputs(); getInputs();
const runnerEnv = getEnvVariables(); const runnerEnv = getEnvVariables();
const { sonarToken } = runnerEnv; const { sonarToken, sonarcloudUrl } = runnerEnv;
if (sonarcloudUrl) {
warning(
"The SONARCLOUD_URL environment variable is deprecated and will be removed in a future version. " +
"Regular users should not set it; use SONAR_REGION=us for the US region. " +
"For advanced needs, pass -Dsonar.scanner.sonarcloudUrl and -Dsonar.scanner.apiBaseUrl via the args input."
);
}
runSanityChecks({ projectBaseDir, scannerVersion, sonarToken }); runSanityChecks({ projectBaseDir, scannerVersion, sonarToken });
const scannerDir = await installSonarScanner({ const scannerDir = await installSonarScanner({
scannerVersion, scannerVersion,
scannerBinariesUrl, scannerBinariesUrl,
scannerBinariesAuthHeader,
skipSignatureVerification, skipSignatureVerification,
}); });
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
import { f as execExports, h as addPath, a as info, n as setOutput, s as setFailed, o as startGroup, p as endGroup } from './exec-zlpfwmpH.js'; import { f as execExports, h as addPath, a as info, o as setOutput, j as setFailed, p as startGroup, q as endGroup } from './exec-BeYcktvA.js';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import 'os'; import 'os';
+2
View File
@@ -0,0 +1,2 @@
[tools]
node = "24"
+15 -1
View File
@@ -16,9 +16,23 @@
// along with this program; if not, write to the Free Software Foundation, // along with this program; if not, write to the Free Software Foundation,
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import { readFileSync } from "node:fs";
import commonjs from "@rollup/plugin-commonjs"; import commonjs from "@rollup/plugin-commonjs";
import { nodeResolve } from "@rollup/plugin-node-resolve"; import { nodeResolve } from "@rollup/plugin-node-resolve";
// Ensures CRLF line endings from a Windows checkout don't leak into the
// bundle or the source map's sourcesContent, so the build is reproducible
// across operating systems. Uses `load` rather than `transform` so the
// normalized text is also what Rollup embeds in sourcesContent.
const normalizeLineEndings = {
name: "normalize-line-endings",
load(id) {
if (id.startsWith("\0") || id.includes("?")) return null;
const code = readFileSync(id, "utf8");
return code.includes("\r") ? code.replaceAll("\r\n", "\n") : null;
},
};
const config = { const config = {
input: [ input: [
"src/main/index.js", "src/main/index.js",
@@ -30,7 +44,7 @@ const config = {
format: "es", format: "es",
sourcemap: true, sourcemap: true,
}, },
plugins: [commonjs(), nodeResolve({ preferBuiltins: true })], plugins: [normalizeLineEndings, commonjs(), nodeResolve({ preferBuiltins: true })],
}; };
export default config; export default config;
+11 -11
View File
@@ -1,11 +1,11 @@
sonar-scanner-version=8.0.1.6346 sonar-scanner-version=8.1.0.6389
sonar-scanner-url-windows-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-windows-x64.zip sonar-scanner-url-windows-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-windows-x64.zip
sonar-scanner-sha-windows-x64=52b35b24be4ce5ec2e2933b32683db45db139581c46945546d9739b0c8866231 sonar-scanner-sha-windows-x64=73f0e71928673d5b2f39bb86213342a30e51a14c8eec345164016bb29c8df8ee
sonar-scanner-url-linux-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-linux-x64.zip sonar-scanner-url-linux-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-linux-x64.zip
sonar-scanner-sha-linux-x64=4bd40bf8411ed104853e94a3746ec92bc92845fde2b27dbf5c33fb5cfa8ecbe9 sonar-scanner-sha-linux-x64=bb8f709f9cb73352f8d1260a3b3c506c0f41146754bc630762c126d795499d0b
sonar-scanner-url-linux-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-linux-aarch64.zip sonar-scanner-url-linux-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-linux-aarch64.zip
sonar-scanner-sha-linux-aarch64=ae2b062ed6d640ab9014ab576042385d54c910857de952f5cb2592d2a2d7c8d8 sonar-scanner-sha-linux-aarch64=5e1c9328f4e261838de778c9e586ee608cca45ff7f0538108642219214628ba5
sonar-scanner-url-macosx-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-macosx-x64.zip sonar-scanner-url-macosx-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-macosx-x64.zip
sonar-scanner-sha-macosx-x64=aa9065347ba834ff6f3d461183eb40a67a321e6996206875fd257e8e7d5745b2 sonar-scanner-sha-macosx-x64=8afc8bbff9008434e53b31cb681333ff643b999f84ca537db573d0fae8883cdc
sonar-scanner-url-macosx-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-macosx-aarch64.zip sonar-scanner-url-macosx-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-macosx-aarch64.zip
sonar-scanner-sha-macosx-aarch64=2d65d49c327ec8ca5ec7c6dc2af17749f5b43c596fd906501bba5a0b09edc5e2 sonar-scanner-sha-macosx-aarch64=20d12be4081896b337cd873d98ebd3d554be666086a45e31dd84a12ef51c3688
@@ -20,8 +20,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs"; import * as fs from "node:fs";
import {afterEach, describe, it, mock} from "node:test"; import { afterEach, describe, it, mock } from "node:test";
import {setupGpgHome,} from "../gpg-verification.js"; import { getProxyFromEnv, setupGpgHome, } from "../gpg-verification.js";
/** /**
* Helper function to create a temporary GPG home directory for testing. * Helper function to create a temporary GPG home directory for testing.
@@ -38,6 +38,14 @@ function createTrackedGpgHome(tempDirs) {
describe("gpg-verification with mocked exec", () => { describe("gpg-verification with mocked exec", () => {
let tempDirs = []; let tempDirs = [];
const proxyVars = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
function clearProxyEnv() {
for (const v of proxyVars) {
delete process.env[v];
}
}
afterEach(() => { afterEach(() => {
// Clean up temporary directories // Clean up temporary directories
tempDirs.forEach((dir) => { tempDirs.forEach((dir) => {
@@ -483,4 +491,215 @@ describe("gpg-verification with mocked exec", () => {
); );
}); });
}); });
describe("getProxyFromEnv", () => {
afterEach(() => {
clearProxyEnv();
});
it("should return undefined when no proxy is set", () => {
clearProxyEnv();
assert.equal(getProxyFromEnv(), undefined);
});
it("should prefer HTTPS_PROXY over https_proxy", () => {
clearProxyEnv();
process.env.HTTPS_PROXY = "http://proxy-https:8080";
process.env.https_proxy = "http://proxy-lower:8080";
assert.equal(getProxyFromEnv(), "http://proxy-https:8080");
});
it("should use https_proxy (lowercase)", () => {
clearProxyEnv();
process.env.https_proxy = "http://proxy-lower:8080";
assert.equal(getProxyFromEnv(), "http://proxy-lower:8080");
});
it("should not fall back to HTTP_PROXY", () => {
clearProxyEnv();
process.env.HTTP_PROXY = "http://proxy-http:3128";
assert.equal(getProxyFromEnv(), undefined);
});
it("should not fall back to http_proxy (lowercase)", () => {
clearProxyEnv();
process.env.http_proxy = "http://proxy-lower-http:3128";
assert.equal(getProxyFromEnv(), undefined);
});
it("should ignore HTTP_PROXY when only HTTP proxy is configured", () => {
clearProxyEnv();
process.env.HTTP_PROXY = "http://http-only-proxy:3128";
process.env.http_proxy = "http://http-only-proxy:3128";
assert.equal(getProxyFromEnv(), undefined);
});
});
describe("tryImportKey with proxy", () => {
afterEach(() => {
clearProxyEnv();
});
it("should not pass --keyserver-options when no proxy env is set", async (t) => {
clearProxyEnv();
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=no-proxy");
const gpgHome = createTrackedGpgHome(tempDirs);
const keyserver = "hkps://keyserver.ubuntu.com";
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
await importSonarSourceKey(gpgHome, keyFingerprint, keyserver);
assert.equal(execCalls.length, 1);
const args = execCalls[0].args;
assert.ok(!args.includes("--keyserver-options"), "Should NOT include --keyserver-options");
});
it("should use HTTPS_PROXY when set", async (t) => {
clearProxyEnv();
process.env.HTTPS_PROXY = "http://corporate-proxy:8080";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-https-upper");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
const optIdx = args.indexOf("--keyserver-options");
assert.ok(optIdx !== -1, "Should include --keyserver-options");
assert.equal(args[optIdx + 1], "http-proxy=http://corporate-proxy:8080");
});
it("should use https_proxy (lowercase) when set", async (t) => {
clearProxyEnv();
process.env.https_proxy = "http://lowercase-proxy:3128";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-https-lower");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
const optIdx = args.indexOf("--keyserver-options");
assert.ok(optIdx !== -1);
assert.equal(args[optIdx + 1], "http-proxy=http://lowercase-proxy:3128");
});
it("should not use proxy when only HTTP_PROXY is set", async (t) => {
clearProxyEnv();
process.env.HTTP_PROXY = "http://http-only-proxy:9090";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-http-upper");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
assert.ok(!args.includes("--keyserver-options"), "Should NOT include --keyserver-options when only HTTP_PROXY is set");
});
it("should not use proxy when only http_proxy (lowercase) is set", async (t) => {
clearProxyEnv();
process.env.http_proxy = "http://last-resort-proxy:1080";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-http-lower");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
assert.ok(!args.includes("--keyserver-options"), "Should NOT include --keyserver-options when only http_proxy is set");
});
it("should prefer HTTPS_PROXY over https_proxy and ignore HTTP variants", async (t) => {
clearProxyEnv();
process.env.HTTPS_PROXY = "http://preferred:8080";
process.env.https_proxy = "http://not-this-one:8080";
process.env.HTTP_PROXY = "http://also-not:3128";
process.env.http_proxy = "http://nope:1080";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-precedence");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
const optIdx = args.indexOf("--keyserver-options");
assert.ok(optIdx !== -1);
assert.equal(args[optIdx + 1], "http-proxy=http://preferred:8080");
});
});
}); });
+153
View File
@@ -0,0 +1,153 @@
/*
* sonarqube-scan-action
* Copyright (C) 2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import assert from "node:assert/strict";
import { describe, it, mock } from "node:test";
function mockDependencies(t, { getInputFn, setSecretFn }) {
t.mock.module("@actions/core", {
namedExports: {
getInput: getInputFn,
getBooleanInput: mock.fn(() => false),
setSecret: setSecretFn,
setFailed: mock.fn(),
info: mock.fn(),
warning: mock.fn(),
},
});
t.mock.module("../install-sonar-scanner.js", {
namedExports: { installSonarScanner: mock.fn(async () => "/scanner") },
});
t.mock.module("../run-sonar-scanner.js", {
namedExports: { runSonarScanner: mock.fn(async () => {}) },
});
t.mock.module("../sanity-checks.js", {
namedExports: {
validateScannerVersion: mock.fn(),
checkSonarToken: mock.fn(),
checkMavenProject: mock.fn(),
checkGradleProject: mock.fn(),
},
});
}
describe("SONARCLOUD_URL deprecation", () => {
it("should warn when SONARCLOUD_URL is set", async (t) => {
const warningFn = mock.fn();
const getInputFn = mock.fn(() => "");
t.mock.module("@actions/core", {
namedExports: {
getInput: getInputFn,
getBooleanInput: mock.fn(() => false),
setSecret: mock.fn(),
setFailed: mock.fn(),
info: mock.fn(),
warning: warningFn,
},
});
t.mock.module("../install-sonar-scanner.js", {
namedExports: { installSonarScanner: mock.fn(async () => "/scanner") },
});
t.mock.module("../run-sonar-scanner.js", {
namedExports: { runSonarScanner: mock.fn(async () => {}) },
});
t.mock.module("../sanity-checks.js", {
namedExports: {
validateScannerVersion: mock.fn(),
checkSonarToken: mock.fn(),
checkMavenProject: mock.fn(),
checkGradleProject: mock.fn(),
},
});
process.env.SONARCLOUD_URL = "mirror.sonarcloud.io";
t.after(() => delete process.env.SONARCLOUD_URL);
await import("../index.js?test=deprecation-warning");
assert.equal(warningFn.mock.calls.length, 1);
assert.match(
warningFn.mock.calls[0].arguments[0],
/SONARCLOUD_URL.*deprecated/
);
});
it("should not warn when SONARCLOUD_URL is not set", async (t) => {
const warningFn = mock.fn();
const getInputFn = mock.fn(() => "");
t.mock.module("@actions/core", {
namedExports: {
getInput: getInputFn,
getBooleanInput: mock.fn(() => false),
setSecret: mock.fn(),
setFailed: mock.fn(),
info: mock.fn(),
warning: warningFn,
},
});
t.mock.module("../install-sonar-scanner.js", {
namedExports: { installSonarScanner: mock.fn(async () => "/scanner") },
});
t.mock.module("../run-sonar-scanner.js", {
namedExports: { runSonarScanner: mock.fn(async () => {}) },
});
t.mock.module("../sanity-checks.js", {
namedExports: {
validateScannerVersion: mock.fn(),
checkSonarToken: mock.fn(),
checkMavenProject: mock.fn(),
checkGradleProject: mock.fn(),
},
});
delete process.env.SONARCLOUD_URL;
await import("../index.js?test=no-deprecation-warning");
assert.equal(warningFn.mock.calls.length, 0);
});
});
describe("getInputs", () => {
it("should mask scannerBinariesAuthHeader using setSecret when provided", async (t) => {
const setSecretFn = mock.fn();
const getInputFn = mock.fn((name) => name === "scannerBinariesAuthHeader" ? "Bearer mytoken" : "");
mockDependencies(t, { getInputFn, setSecretFn });
await import("../index.js?test=set-secret");
assert.equal(setSecretFn.mock.calls.length, 1);
assert.equal(setSecretFn.mock.calls[0].arguments[0], "Bearer mytoken");
});
it("should not call setSecret when scannerBinariesAuthHeader is not provided", async (t) => {
const setSecretFn = mock.fn();
const getInputFn = mock.fn(() => "");
mockDependencies(t, { getInputFn, setSecretFn });
await import("../index.js?test=no-set-secret");
assert.equal(setSecretFn.mock.calls.length, 0);
});
});
@@ -0,0 +1,253 @@
/*
* sonarqube-scan-action
* Copyright (C) 2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import assert from "node:assert/strict";
import { describe, it, mock } from "node:test";
const SCANNER_VERSION = "6.2.0.4584";
const SCANNER_SEMVER_VERSION = "6.2.0-build.4584";
const BINARIES_URL = "https://my.artifactory.example.com/sonar-scanner-cli";
const BINARY_DOWNLOAD_URL = `${BINARIES_URL}/sonar-scanner-cli-${SCANNER_VERSION}-linux-x64.zip`;
function mockUtils(t) {
t.mock.module("../utils.js", {
namedExports: {
getPlatformFlavor: mock.fn(() => "linux-x64"),
getScannerDownloadURL: mock.fn(() => BINARY_DOWNLOAD_URL),
scannerDirName: mock.fn(() => `sonar-scanner-${SCANNER_VERSION}-linux-x64`),
toSemVer: mock.fn(() => SCANNER_SEMVER_VERSION),
},
});
}
describe("installSonarScanner", () => {
it("should forward scannerBinariesAuthHeader to both binary and signature downloads", async (t) => {
const downloadCalls = [];
const downloadToolFn = mock.fn(async (url, dest, auth) => {
downloadCalls.push({ url, auth });
return `/tmp/downloaded-${downloadCalls.length}`;
});
mockUtils(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
find: mock.fn(() => null),
downloadTool: downloadToolFn,
extractZip: mock.fn(async () => "/tmp/extracted"),
cacheDir: mock.fn(async () => "/tmp/cached"),
},
});
t.mock.module("@actions/core", {
namedExports: {
info: mock.fn(),
warning: mock.fn(),
addPath: mock.fn(),
},
});
t.mock.module("../gpg-verification.js", {
namedExports: {
verifySignature: mock.fn(async () => {}),
},
});
const { installSonarScanner } = await import(
`../install-sonar-scanner.js?test=auth-header`
);
await installSonarScanner({
scannerVersion: SCANNER_VERSION,
scannerBinariesUrl: BINARIES_URL,
scannerBinariesAuthHeader: "Bearer mytoken",
});
assert.equal(downloadCalls.length, 2, "Should download binary and signature");
assert.equal(downloadCalls[0].auth, "Bearer mytoken", "Binary download should use auth header");
assert.equal(downloadCalls[1].auth, "Bearer mytoken", "Signature download should use auth header");
assert.ok(downloadCalls[1].url.endsWith(".asc"), "Second download should be the signature");
});
it("should not set auth header when scannerBinariesAuthHeader is not provided", async (t) => {
const downloadCalls = [];
const downloadToolFn = mock.fn(async (url, dest, auth) => {
downloadCalls.push({ url, auth });
return `/tmp/downloaded-${downloadCalls.length}`;
});
mockUtils(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
find: mock.fn(() => null),
downloadTool: downloadToolFn,
extractZip: mock.fn(async () => "/tmp/extracted"),
cacheDir: mock.fn(async () => "/tmp/cached"),
},
});
t.mock.module("@actions/core", {
namedExports: {
info: mock.fn(),
warning: mock.fn(),
addPath: mock.fn(),
},
});
t.mock.module("../gpg-verification.js", {
namedExports: {
verifySignature: mock.fn(async () => {}),
},
});
const { installSonarScanner } = await import(
`../install-sonar-scanner.js?test=no-auth-header`
);
await installSonarScanner({
scannerVersion: SCANNER_VERSION,
scannerBinariesUrl: BINARIES_URL,
});
assert.equal(downloadCalls.length, 2);
assert.equal(downloadCalls[0].auth, undefined, "Binary download should have no auth header");
assert.equal(downloadCalls[1].auth, undefined, "Signature download should have no auth header");
});
it("should skip signature download when skipSignatureVerification is true", async (t) => {
const downloadCalls = [];
const downloadToolFn = mock.fn(async (url, dest, auth) => {
downloadCalls.push({ url, auth });
return `/tmp/downloaded-${downloadCalls.length}`;
});
mockUtils(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
find: mock.fn(() => null),
downloadTool: downloadToolFn,
extractZip: mock.fn(async () => "/tmp/extracted"),
cacheDir: mock.fn(async () => "/tmp/cached"),
},
});
t.mock.module("@actions/core", {
namedExports: {
info: mock.fn(),
warning: mock.fn(),
addPath: mock.fn(),
},
});
const { installSonarScanner } = await import(
`../install-sonar-scanner.js?test=skip-sig`
);
await installSonarScanner({
scannerVersion: SCANNER_VERSION,
scannerBinariesUrl: BINARIES_URL,
scannerBinariesAuthHeader: "Bearer mytoken",
skipSignatureVerification: true,
});
assert.equal(downloadCalls.length, 1, "Should only download binary, not signature");
assert.equal(downloadCalls[0].auth, "Bearer mytoken");
});
it("should use semver-compatible version for tool-cache find and cacheDir", async (t) => {
const findFn = mock.fn(() => null);
const cacheDirFn = mock.fn(async () => "/tmp/cached");
mockUtils(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
find: findFn,
downloadTool: mock.fn(async () => "/tmp/downloaded"),
extractZip: mock.fn(async () => "/tmp/extracted"),
cacheDir: cacheDirFn,
},
});
t.mock.module("@actions/core", {
namedExports: {
info: mock.fn(),
warning: mock.fn(),
addPath: mock.fn(),
},
});
t.mock.module("../gpg-verification.js", {
namedExports: {
verifySignature: mock.fn(async () => {}),
},
});
const { installSonarScanner } = await import(
`../install-sonar-scanner.js?test=semver-version`
);
await installSonarScanner({
scannerVersion: SCANNER_VERSION,
scannerBinariesUrl: BINARIES_URL,
});
assert.equal(findFn.mock.calls[0].arguments[1], SCANNER_SEMVER_VERSION,
"tc.find should be called with semver-compatible version");
assert.equal(cacheDirFn.mock.calls[0].arguments[2], SCANNER_SEMVER_VERSION,
"tc.cacheDir should be called with semver-compatible version");
});
it("should use cached tool when available and skip download", async (t) => {
const downloadToolFn = mock.fn();
mockUtils(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
find: mock.fn(() => "/tmp/cached-tool"),
downloadTool: downloadToolFn,
extractZip: mock.fn(),
cacheDir: mock.fn(),
},
});
t.mock.module("@actions/core", {
namedExports: {
info: mock.fn(),
warning: mock.fn(),
addPath: mock.fn(),
},
});
const { installSonarScanner } = await import(
`../install-sonar-scanner.js?test=cached`
);
await installSonarScanner({
scannerVersion: SCANNER_VERSION,
scannerBinariesUrl: BINARIES_URL,
});
assert.equal(downloadToolFn.mock.calls.length, 0, "Should not download when cached");
});
});
+20
View File
@@ -22,6 +22,7 @@ import {
getPlatformFlavor, getPlatformFlavor,
getScannerDownloadURL, getScannerDownloadURL,
scannerDirName, scannerDirName,
toSemVer,
} from "../utils.js"; } from "../utils.js";
describe("getPlatformFlavor", () => { describe("getPlatformFlavor", () => {
@@ -97,3 +98,22 @@ describe("scannerDirName", () => {
); );
}); });
}); });
describe("toSemVer", () => {
it("converts 4-part version to semver pre-release format", () => {
assert.equal(toSemVer("8.0.1.6346"), "8.0.1-build.6346");
});
it("leaves 3-part semver version unchanged", () => {
assert.equal(toSemVer("8.0.1"), "8.0.1");
});
it("leaves version with pre-release identifier unchanged", () => {
assert.equal(toSemVer("7.2.0-SNAPSHOT"), "7.2.0-SNAPSHOT");
});
it("converts different 4-part versions correctly", () => {
assert.equal(toSemVer("6.2.0.4584"), "6.2.0-build.4584");
assert.equal(toSemVer("8.1.0.6389"), "8.1.0-build.6389");
});
});
+22
View File
@@ -125,6 +125,18 @@ export function setupGpgHome() {
return gpgHome; return gpgHome;
} }
/**
* Detects HTTPS proxy from environment variables.
* Checks both upper and lower case variants (HTTPS_PROXY, https_proxy).
* Only HTTPS proxy is used since keyservers use hkps:// (TLS).
* HTTP_PROXY is intentionally not used as a fallback to avoid routing
* HTTPS traffic through a proxy not intended for TLS connections.
* @returns {string|undefined} Proxy URL or undefined if not set
*/
export function getProxyFromEnv() {
return process.env.HTTPS_PROXY || process.env.https_proxy;
}
/** /**
* Attempts to import a public key from a specific keyserver * Attempts to import a public key from a specific keyserver
* @param {string} gpgHome - Path to GPG home directory * @param {string} gpgHome - Path to GPG home directory
@@ -136,6 +148,15 @@ export function setupGpgHome() {
async function tryImportKey(gpgHome, keyFingerprint, keyserver) { async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
const gpgCommand = getGpgCommand(); const gpgCommand = getGpgCommand();
const gpgHomePath = convertToUnixPath(gpgHome); const gpgHomePath = convertToUnixPath(gpgHome);
const proxyUrl = getProxyFromEnv();
if (proxyUrl) {
// The URL may carry credentials (e.g. http://user:pass@proxy:8080).
// Register it as a secret so future logging (here or downstream) is
// automatically redacted
core.setSecret(proxyUrl);
core.info("Using HTTPS_PROXY for keyserver access");
}
await exec.exec( await exec.exec(
gpgCommand, gpgCommand,
@@ -145,6 +166,7 @@ async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
"--batch", "--batch",
"--keyserver", "--keyserver",
keyserver, keyserver,
...(proxyUrl ? ["--keyserver-options", `http-proxy=${proxyUrl}`] : []),
"--recv-keys", "--recv-keys",
keyFingerprint, keyFingerprint,
], ],
+19 -6
View File
@@ -17,14 +17,14 @@
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import * as core from "@actions/core"; import * as core from "@actions/core";
import { installSonarScanner } from "./install-sonar-scanner"; import { installSonarScanner } from "./install-sonar-scanner.js";
import { runSonarScanner } from "./run-sonar-scanner"; import { runSonarScanner } from "./run-sonar-scanner.js";
import { import {
checkGradleProject, checkGradleProject,
checkMavenProject, checkMavenProject,
checkSonarToken, checkSonarToken,
validateScannerVersion, validateScannerVersion,
} from "./sanity-checks"; } from "./sanity-checks.js";
/** /**
* Inputs are defined in action.yml * Inputs are defined in action.yml
@@ -33,10 +33,14 @@ function getInputs() {
const args = core.getInput("args"); const args = core.getInput("args");
const projectBaseDir = core.getInput("projectBaseDir"); const projectBaseDir = core.getInput("projectBaseDir");
const scannerBinariesUrl = core.getInput("scannerBinariesUrl"); const scannerBinariesUrl = core.getInput("scannerBinariesUrl");
const scannerBinariesAuthHeader = core.getInput("scannerBinariesAuthHeader") || undefined;
if (scannerBinariesAuthHeader) {
core.setSecret(scannerBinariesAuthHeader);
}
const scannerVersion = core.getInput("scannerVersion"); const scannerVersion = core.getInput("scannerVersion");
const skipSignatureVerification = core.getBooleanInput("skipSignatureVerification"); const skipSignatureVerification = core.getBooleanInput("skipSignatureVerification");
return { args, projectBaseDir, scannerBinariesUrl, scannerVersion, skipSignatureVerification }; return { args, projectBaseDir, scannerBinariesUrl, scannerBinariesAuthHeader, scannerVersion, skipSignatureVerification };
} }
/** /**
@@ -72,16 +76,25 @@ function runSanityChecks(inputs) {
async function run() { async function run() {
try { try {
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, skipSignatureVerification } = const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, scannerBinariesAuthHeader, skipSignatureVerification } =
getInputs(); getInputs();
const runnerEnv = getEnvVariables(); const runnerEnv = getEnvVariables();
const { sonarToken } = runnerEnv; const { sonarToken, sonarcloudUrl } = runnerEnv;
if (sonarcloudUrl) {
core.warning(
"The SONARCLOUD_URL environment variable is deprecated and will be removed in a future version. " +
"Regular users should not set it; use SONAR_REGION=us for the US region. " +
"For advanced needs, pass -Dsonar.scanner.sonarcloudUrl and -Dsonar.scanner.apiBaseUrl via the args input."
);
}
runSanityChecks({ projectBaseDir, scannerVersion, sonarToken }); runSanityChecks({ projectBaseDir, scannerVersion, sonarToken });
const scannerDir = await installSonarScanner({ const scannerDir = await installSonarScanner({
scannerVersion, scannerVersion,
scannerBinariesUrl, scannerBinariesUrl,
scannerBinariesAuthHeader,
skipSignatureVerification, skipSignatureVerification,
}); });
+9 -6
View File
@@ -24,8 +24,9 @@ import {
getPlatformFlavor, getPlatformFlavor,
getScannerDownloadURL, getScannerDownloadURL,
scannerDirName, scannerDirName,
} from "./utils"; toSemVer,
import { verifySignature } from "./gpg-verification"; } from "./utils.js";
import { verifySignature } from "./gpg-verification.js";
const TOOLNAME = "sonar-scanner-cli"; const TOOLNAME = "sonar-scanner-cli";
@@ -35,12 +36,14 @@ const TOOLNAME = "sonar-scanner-cli";
export async function installSonarScanner({ export async function installSonarScanner({
scannerVersion, scannerVersion,
scannerBinariesUrl, scannerBinariesUrl,
scannerBinariesAuthHeader,
skipSignatureVerification = false, skipSignatureVerification = false,
}) { }) {
const flavor = getPlatformFlavor(os.platform(), os.arch()); const flavor = getPlatformFlavor(os.platform(), os.arch());
const semVerVersion = toSemVer(scannerVersion);
// Check if tool is already cached // Check if tool is already cached
let toolDir = tc.find(TOOLNAME, scannerVersion, flavor); let toolDir = tc.find(TOOLNAME, semVerVersion, flavor);
if (!toolDir) { if (!toolDir) {
core.info( core.info(
@@ -55,7 +58,7 @@ export async function installSonarScanner({
core.info(`Downloading from: ${downloadUrl}`); core.info(`Downloading from: ${downloadUrl}`);
const downloadPath = await tc.downloadTool(downloadUrl); const downloadPath = await tc.downloadTool(downloadUrl, undefined, scannerBinariesAuthHeader);
if (skipSignatureVerification) { if (skipSignatureVerification) {
core.warning("⚠ Skipping GPG signature verification (not recommended)"); core.warning("⚠ Skipping GPG signature verification (not recommended)");
@@ -65,7 +68,7 @@ export async function installSonarScanner({
let signaturePath; let signaturePath;
try { try {
signaturePath = await tc.downloadTool(signatureUrl); signaturePath = await tc.downloadTool(signatureUrl, undefined, scannerBinariesAuthHeader);
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Failed to download signature file from ${signatureUrl}: ${error.message}` `Failed to download signature file from ${signatureUrl}: ${error.message}`
@@ -83,7 +86,7 @@ export async function installSonarScanner({
scannerDirName(scannerVersion, flavor) scannerDirName(scannerVersion, flavor)
); );
toolDir = await tc.cacheDir(scannerPath, TOOLNAME, scannerVersion, flavor); toolDir = await tc.cacheDir(scannerPath, TOOLNAME, semVerVersion, flavor);
core.info(`Sonar Scanner CLI cached to: ${toolDir}`); core.info(`Sonar Scanner CLI cached to: ${toolDir}`);
} else { } else {
+13
View File
@@ -51,3 +51,16 @@ export function getScannerDownloadURL({
export const scannerDirName = (version, flavor) => export const scannerDirName = (version, flavor) =>
`sonar-scanner-${version}-${flavor}`; `sonar-scanner-${version}-${flavor}`;
/**
* Converts a 4-part version string (e.g. "8.0.1.6346") to a SemVer 2.0 compatible
* string (e.g. "8.0.1-build.6346") for use with GitHub's tool-cache library,
* which requires SemVer-compliant version strings.
*/
export function toSemVer(version) {
const parts = version.split(".");
if (parts.length === 4) {
return `${parts[0]}.${parts[1]}.${parts[2]}-build.${parts[3]}`;
}
return version;
}