Compare commits

..

4 Commits

Author SHA1 Message Date
Julien HENRY c9d327c024 SQSCANGHA-84 Remove outdated wget/curl references
The action was refactored to use Node.js (@actions/tool-cache) for
downloads, which doesn't rely on wget or curl. Update the README and
QA workflow to reflect this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 17:25:27 +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
11 changed files with 507 additions and 42 deletions
+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."
+45 -24
View File
@@ -245,9 +245,9 @@ jobs:
- name: Assert Sonar Scanner CLI was not executed - name: Assert Sonar Scanner CLI was not executed
run: | run: |
./test/assertFileDoesntExist ./output.properties ./test/assertFileDoesntExist ./output.properties
scannerBinariesUrlIsEscapedWithWget: scannerBinariesUrlCommandInjectionTest:
name: > name: >
'scannerBinariesUrl' is escaped with wget so special chars are not injected in the download command 'scannerBinariesUrl' does not allow command injection via semicolons
runs-on: github-ubuntu-latest-s runs-on: github-ubuntu-latest-s
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -266,22 +266,14 @@ jobs:
- name: Assert file.txt does not exist - name: Assert file.txt does not exist
run: | run: |
./test/assertFileDoesntExist "$RUNNER_TEMP/sonarscanner/file.txt" ./test/assertFileDoesntExist "$RUNNER_TEMP/sonarscanner/file.txt"
scannerBinariesUrlIsEscapedWithCurl: scannerBinariesUrlCommandInjectionWithSpacesTest:
name: > name: >
'scannerBinariesUrl' is escaped with curl so special chars are not injected in the download command 'scannerBinariesUrl' does not allow command injection via spaces and quotes
runs-on: github-ubuntu-latest-s runs-on: github-ubuntu-latest-s
steps: steps:
- 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: Remove wget
run: sudo apt-get remove -y wget
- name: Assert wget is not available
run: |
if command -v wget 2>&1 >/dev/null
then
exit 1
fi
- name: Run action with scannerBinariesUrl - name: Run action with scannerBinariesUrl
id: runTest id: runTest
uses: ./ uses: ./
@@ -451,7 +443,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 +453,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
@@ -472,22 +464,14 @@ jobs:
run: | run: |
./test/assertFileContains ./output.properties "sonar.host.url=mirror.sonarcloud.io" ./test/assertFileContains ./output.properties "sonar.host.url=mirror.sonarcloud.io"
./test/assertFileContains ./output.properties "sonar.scanner.sonarcloudUrl=mirror.sonarcloud.io" ./test/assertFileContains ./output.properties "sonar.scanner.sonarcloudUrl=mirror.sonarcloud.io"
curlPerformsRedirect: scannerBinariesUrlRedirectFollowed:
name: > name: >
curl performs redirect when scannerBinariesUrl returns 3xx scannerBinariesUrl redirect (3xx) is followed
runs-on: github-ubuntu-latest-s runs-on: github-ubuntu-latest-s
steps: steps:
- 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: Remove wget
run: sudo apt-get remove -y wget
- name: Assert wget is not available
run: |
if command -v wget 2>&1 >/dev/null
then
exit 1
fi
- name: Generate SSL certificates for nginx - name: Generate SSL certificates for nginx
run: ./generate-ssl.sh run: ./generate-ssl.sh
working-directory: .github/qa-nginx-redirecting working-directory: .github/qa-nginx-redirecting
@@ -827,3 +811,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
- scannerBinariesUrlCommandInjectionTest
- scannerBinariesUrlCommandInjectionWithSpacesTest
- dontFailGradleTest
- dontFailGradleKotlinTest
- dontFailMavenTest
- runAnalysisTest
- runnerDebugUsedTest
- runAnalysisWithCacheTest
- overrideSonarcloudUrlTest
- scannerBinariesUrlRedirectFollowed
- 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."
+16 -2
View File
@@ -200,6 +200,20 @@ 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` #### `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: 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:
@@ -469,11 +483,11 @@ See also [example configurations of C++ projects for SonarQube Server](https://g
When running the action in a self-hosted runner or container, please ensure that the following programs are installed: When running the action in a self-hosted runner or container, please ensure that the following programs are installed:
* **curl** or **wget**
* **unzip**
* **gpg** * **gpg**
* **dirmngr** * **dirmngr**
Note: `gpg` and `dirmngr` are only required for GPG signature verification (enabled by default). They can be omitted when setting `skipSignatureVerification: true`.
### Additional information ### Additional information
The `sonarqube-scan-action/install-build-wrapper` action installs `coreutils` if run on macOS. The `sonarqube-scan-action/install-build-wrapper` action installs `coreutils` if run on macOS.
+7
View File
@@ -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
+26 -5
View File
@@ -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);
@@ -4140,6 +4147,7 @@ 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());
@@ -4160,7 +4168,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)");
@@ -4170,7 +4178,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}`
@@ -4489,10 +4497,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 };
} }
/** /**
@@ -4528,16 +4540,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
+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,207 @@
/*
* 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 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`),
},
});
}
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 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");
});
});
+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,
}); });
+5 -4
View File
@@ -24,8 +24,8 @@ import {
getPlatformFlavor, getPlatformFlavor,
getScannerDownloadURL, getScannerDownloadURL,
scannerDirName, scannerDirName,
} from "./utils"; } from "./utils.js";
import { verifySignature } from "./gpg-verification"; import { verifySignature } from "./gpg-verification.js";
const TOOLNAME = "sonar-scanner-cli"; const TOOLNAME = "sonar-scanner-cli";
@@ -35,6 +35,7 @@ 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());
@@ -55,7 +56,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 +66,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}`