mirror of
https://github.com/SonarSource/sonarqube-scan-action.git
synced 2026-06-05 09:00:31 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b24f324433 | |||
| ec7d7e54b2 | |||
| b243e5198f | |||
| 375c3f5c03 | |||
| 9c783232fe | |||
| 7006c4492b | |||
| edd319f284 | |||
| e050aa9e69 | |||
| 6cd3d8f2ae | |||
| 56568530ed | |||
| c444753899 | |||
| 59db25f34e | |||
| ca30b65f4e |
@@ -10,7 +10,6 @@ jobs:
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: read
|
||||
# For external PR, ticket should be moved manually
|
||||
if: |
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
@@ -21,10 +20,11 @@ jobs:
|
||||
uses: SonarSource/vault-action-wrapper@v3
|
||||
with:
|
||||
secrets: |
|
||||
development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN;
|
||||
development/kv/data/jira user | JIRA_USER;
|
||||
development/kv/data/jira token | JIRA_TOKEN;
|
||||
- uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2
|
||||
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-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
|
||||
|
||||
@@ -88,3 +88,17 @@ jobs:
|
||||
BINARY: ${{ steps.run-action.outputs.build-wrapper-binary }}
|
||||
run: |
|
||||
("$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 }}
|
||||
run: |
|
||||
("$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."
|
||||
|
||||
@@ -433,7 +433,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: SonarQube Cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sonar/cache
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-sonar
|
||||
@@ -451,7 +451,7 @@ jobs:
|
||||
./test/assertFileExists ./test/example-project/.scannerwork/report-task.txt
|
||||
overrideSonarcloudUrlTest:
|
||||
name: >
|
||||
'SONARCLOUD_URL' is used
|
||||
Deprecated 'SONARCLOUD_URL' still works and emits a deprecation warning
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -461,7 +461,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with SONARCLOUD_URL
|
||||
- name: Run action with deprecated SONARCLOUD_URL
|
||||
uses: ./
|
||||
with:
|
||||
args: -Dsonar.scanner.apiBaseUrl=api.mirror.sonarcloud.io -Dsonar.scanner.internal.dumpToFile=./output.properties
|
||||
@@ -827,3 +827,40 @@ jobs:
|
||||
run: |
|
||||
echo "Action with invalid scannerVersion should have failed but succeeded"
|
||||
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."
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e #v6.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Parse semver
|
||||
uses: madhead/semver-utils@36d1e0ed361bd7b4b77665de8093092eaeabe6ba # v4.3.0
|
||||
uses: madhead/semver-utils@4cf918affe9106ea59f86c6250e5ec4570ac4389 # v5.0.0
|
||||
id: version
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
@@ -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/
|
||||
```
|
||||
|
||||
#### `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:
|
||||
* 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
|
||||
@@ -452,6 +485,8 @@ When running the action in a self-hosted runner or container, please ensure that
|
||||
|
||||
* **curl** or **wget**
|
||||
* **unzip**
|
||||
* **gpg**
|
||||
* **dirmngr**
|
||||
|
||||
### Additional information
|
||||
|
||||
|
||||
+10
-3
@@ -19,15 +19,22 @@ inputs:
|
||||
description: Version of the Sonar Scanner CLI to use
|
||||
required: false
|
||||
# to be kept in sync with sonar-scanner-version
|
||||
default: 8.0.1.6346
|
||||
default: 8.1.0.6389
|
||||
scannerBinariesUrl:
|
||||
description: URL to download the Sonar Scanner CLI binaries from
|
||||
required: false
|
||||
default: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli
|
||||
skipSignatureVerification:
|
||||
description: Skip GPG signature verification (defaults to true temporarily while dirmngr dependency is resolved; set to false to enable verification)
|
||||
description: Skip GPG signature verification (not recommended for security)
|
||||
required: false
|
||||
default: "true"
|
||||
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:
|
||||
using: node24
|
||||
main: dist/index.js
|
||||
|
||||
Vendored
-30514
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-27277
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -31829,5 +31829,5 @@ function 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 };
|
||||
//# sourceMappingURL=exec-zlpfwmpH.js.map
|
||||
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-BeYcktvA.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+62
-7
@@ -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 fs from 'fs';
|
||||
import * as os from 'os';
|
||||
@@ -10,6 +10,7 @@ import { ok } from 'assert';
|
||||
import 'string_decoder';
|
||||
import * as events from 'events';
|
||||
import { setTimeout as setTimeout$1 } from 'timers';
|
||||
import * as fs$2 from 'node:fs/promises';
|
||||
import * as os$1 from 'node:os';
|
||||
import * as path$1 from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
@@ -3503,6 +3504,13 @@ function downloadToolAttempt(url, dest, auth, headers) {
|
||||
const http = new HttpClient(userAgent, [], {
|
||||
allowRetries: false
|
||||
});
|
||||
if (auth) {
|
||||
debug('set auth');
|
||||
if (headers === undefined) {
|
||||
headers = {};
|
||||
}
|
||||
headers.authorization = auth;
|
||||
}
|
||||
const response = yield http.get(url, headers);
|
||||
if (response.message.statusCode !== 200) {
|
||||
const err = new HTTPError(response.message.statusCode);
|
||||
@@ -3977,6 +3985,18 @@ function setupGpgHome() {
|
||||
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
|
||||
* @param {string} gpgHome - Path to GPG home directory
|
||||
@@ -3988,6 +4008,15 @@ function setupGpgHome() {
|
||||
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
|
||||
const gpgCommand = getGpgCommand();
|
||||
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(
|
||||
gpgCommand,
|
||||
@@ -3997,6 +4026,7 @@ async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
|
||||
"--batch",
|
||||
"--keyserver",
|
||||
keyserver,
|
||||
...(proxyUrl ? ["--keyserver-options", `http-proxy=${proxyUrl}`] : []),
|
||||
"--recv-keys",
|
||||
keyFingerprint,
|
||||
],
|
||||
@@ -4112,12 +4142,22 @@ function cleanupGpgHome(gpgHome) {
|
||||
|
||||
const TOOLNAME = "sonar-scanner-cli";
|
||||
|
||||
async function ensureZipExtension(filePath) {
|
||||
if (filePath.endsWith(".zip")) {
|
||||
return filePath;
|
||||
}
|
||||
const zipPath = `${filePath}.zip`;
|
||||
await fs$2.rename(filePath, zipPath);
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the Sonar Scanner CLI for the current environment and cache it.
|
||||
*/
|
||||
async function installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
scannerBinariesAuthHeader,
|
||||
skipSignatureVerification = false,
|
||||
}) {
|
||||
const flavor = getPlatformFlavor(os$1.platform(), os$1.arch());
|
||||
@@ -4138,7 +4178,7 @@ async function installSonarScanner({
|
||||
|
||||
info(`Downloading from: ${downloadUrl}`);
|
||||
|
||||
const downloadPath = await downloadTool(downloadUrl);
|
||||
const downloadPath = await downloadTool(downloadUrl, undefined, scannerBinariesAuthHeader);
|
||||
|
||||
if (skipSignatureVerification) {
|
||||
warning("⚠ Skipping GPG signature verification (not recommended)");
|
||||
@@ -4148,7 +4188,7 @@ async function installSonarScanner({
|
||||
|
||||
let signaturePath;
|
||||
try {
|
||||
signaturePath = await downloadTool(signatureUrl);
|
||||
signaturePath = await downloadTool(signatureUrl, undefined, scannerBinariesAuthHeader);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to download signature file from ${signatureUrl}: ${error.message}`
|
||||
@@ -4158,7 +4198,9 @@ async function installSonarScanner({
|
||||
await verifySignature(downloadPath, signaturePath);
|
||||
}
|
||||
|
||||
const extractedPath = await extractZip(downloadPath);
|
||||
// PowerShell 5.1 (used on some Windows agents) requires the .zip extension for Expand-Archive
|
||||
const extractInput = await ensureZipExtension(downloadPath);
|
||||
const extractedPath = await extractZip(extractInput);
|
||||
|
||||
// Find the actual scanner directory inside the extracted folder
|
||||
const scannerPath = path$1.join(
|
||||
@@ -4467,10 +4509,14 @@ function getInputs() {
|
||||
const args = getInput("args");
|
||||
const projectBaseDir = getInput("projectBaseDir");
|
||||
const scannerBinariesUrl = getInput("scannerBinariesUrl");
|
||||
const scannerBinariesAuthHeader = getInput("scannerBinariesAuthHeader") || undefined;
|
||||
if (scannerBinariesAuthHeader) {
|
||||
setSecret(scannerBinariesAuthHeader);
|
||||
}
|
||||
const scannerVersion = getInput("scannerVersion");
|
||||
const skipSignatureVerification = getBooleanInput("skipSignatureVerification");
|
||||
|
||||
return { args, projectBaseDir, scannerBinariesUrl, scannerVersion, skipSignatureVerification };
|
||||
return { args, projectBaseDir, scannerBinariesUrl, scannerBinariesAuthHeader, scannerVersion, skipSignatureVerification };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4506,16 +4552,25 @@ function runSanityChecks(inputs) {
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, skipSignatureVerification } =
|
||||
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, scannerBinariesAuthHeader, skipSignatureVerification } =
|
||||
getInputs();
|
||||
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 });
|
||||
|
||||
const scannerDir = await installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
scannerBinariesAuthHeader,
|
||||
skipSignatureVerification,
|
||||
});
|
||||
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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 path from 'path';
|
||||
import 'os';
|
||||
|
||||
+15
-1
@@ -16,9 +16,23 @@
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
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 = {
|
||||
input: [
|
||||
"src/main/index.js",
|
||||
@@ -30,7 +44,7 @@ const config = {
|
||||
format: "es",
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [commonjs(), nodeResolve({ preferBuiltins: true })],
|
||||
plugins: [normalizeLineEndings, commonjs(), nodeResolve({ preferBuiltins: true })],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
+11
-11
@@ -1,11 +1,11 @@
|
||||
sonar-scanner-version=8.0.1.6346
|
||||
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-sha-windows-x64=52b35b24be4ce5ec2e2933b32683db45db139581c46945546d9739b0c8866231
|
||||
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-sha-linux-x64=4bd40bf8411ed104853e94a3746ec92bc92845fde2b27dbf5c33fb5cfa8ecbe9
|
||||
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-sha-linux-aarch64=ae2b062ed6d640ab9014ab576042385d54c910857de952f5cb2592d2a2d7c8d8
|
||||
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-sha-macosx-x64=aa9065347ba834ff6f3d461183eb40a67a321e6996206875fd257e8e7d5745b2
|
||||
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-sha-macosx-aarch64=2d65d49c327ec8ca5ec7c6dc2af17749f5b43c596fd906501bba5a0b09edc5e2
|
||||
sonar-scanner-version=8.1.0.6389
|
||||
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=73f0e71928673d5b2f39bb86213342a30e51a14c8eec345164016bb29c8df8ee
|
||||
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=bb8f709f9cb73352f8d1260a3b3c506c0f41146754bc630762c126d795499d0b
|
||||
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=5e1c9328f4e261838de778c9e586ee608cca45ff7f0538108642219214628ba5
|
||||
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=8afc8bbff9008434e53b31cb681333ff643b999f84ca537db573d0fae8883cdc
|
||||
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=20d12be4081896b337cd873d98ebd3d554be666086a45e31dd84a12ef51c3688
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import {afterEach, describe, it, mock} from "node:test";
|
||||
import {setupGpgHome,} from "../gpg-verification.js";
|
||||
import { afterEach, describe, it, mock } from "node:test";
|
||||
import { getProxyFromEnv, setupGpgHome, } from "../gpg-verification.js";
|
||||
|
||||
/**
|
||||
* Helper function to create a temporary GPG home directory for testing.
|
||||
@@ -38,6 +38,14 @@ function createTrackedGpgHome(tempDirs) {
|
||||
describe("gpg-verification with mocked exec", () => {
|
||||
let tempDirs = [];
|
||||
|
||||
const proxyVars = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
||||
|
||||
function clearProxyEnv() {
|
||||
for (const v of proxyVars) {
|
||||
delete process.env[v];
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directories
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,334 @@
|
||||
/*
|
||||
* 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";
|
||||
import nodeFsPromises from "node:fs/promises";
|
||||
|
||||
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`),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function mockFsPromises(t) {
|
||||
t.mock.module("node:fs/promises", {
|
||||
namedExports: {
|
||||
...nodeFsPromises,
|
||||
rename: mock.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
mockFsPromises(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);
|
||||
mockFsPromises(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);
|
||||
mockFsPromises(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 rename downloaded file to add .zip extension before extraction", async (t) => {
|
||||
const renameCalls = [];
|
||||
const extractZipCalls = [];
|
||||
|
||||
mockUtils(t);
|
||||
|
||||
t.mock.module("node:fs/promises", {
|
||||
namedExports: {
|
||||
...nodeFsPromises,
|
||||
rename: mock.fn(async (src, dest) => {
|
||||
renameCalls.push({ src, dest });
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
t.mock.module("@actions/tool-cache", {
|
||||
namedExports: {
|
||||
find: mock.fn(() => null),
|
||||
downloadTool: mock.fn(async () => "/tmp/downloaded-file"),
|
||||
extractZip: mock.fn(async (p) => {
|
||||
extractZipCalls.push(p);
|
||||
return "/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=rename-zip`
|
||||
);
|
||||
|
||||
await installSonarScanner({
|
||||
scannerVersion: SCANNER_VERSION,
|
||||
scannerBinariesUrl: BINARIES_URL,
|
||||
skipSignatureVerification: true,
|
||||
});
|
||||
|
||||
assert.equal(renameCalls.length, 1, "Should rename downloaded file");
|
||||
assert.equal(renameCalls[0].src, "/tmp/downloaded-file");
|
||||
assert.equal(renameCalls[0].dest, "/tmp/downloaded-file.zip");
|
||||
assert.equal(extractZipCalls.length, 1, "Should call extractZip once");
|
||||
assert.equal(extractZipCalls[0], "/tmp/downloaded-file.zip", "Should extract the renamed file");
|
||||
});
|
||||
|
||||
it("should not rename downloaded file when it already has .zip extension", async (t) => {
|
||||
const renameCalls = [];
|
||||
const extractZipCalls = [];
|
||||
|
||||
mockUtils(t);
|
||||
|
||||
t.mock.module("node:fs/promises", {
|
||||
namedExports: {
|
||||
...nodeFsPromises,
|
||||
rename: mock.fn(async (src, dest) => {
|
||||
renameCalls.push({ src, dest });
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
t.mock.module("@actions/tool-cache", {
|
||||
namedExports: {
|
||||
find: mock.fn(() => null),
|
||||
downloadTool: mock.fn(async () => "/tmp/downloaded-file.zip"),
|
||||
extractZip: mock.fn(async (p) => {
|
||||
extractZipCalls.push(p);
|
||||
return "/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-rename-zip`
|
||||
);
|
||||
|
||||
await installSonarScanner({
|
||||
scannerVersion: SCANNER_VERSION,
|
||||
scannerBinariesUrl: BINARIES_URL,
|
||||
skipSignatureVerification: true,
|
||||
});
|
||||
|
||||
assert.equal(renameCalls.length, 0, "Should not rename when already .zip");
|
||||
assert.equal(extractZipCalls.length, 1, "Should call extractZip once");
|
||||
assert.equal(extractZipCalls[0], "/tmp/downloaded-file.zip", "Should extract original file");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -125,6 +125,18 @@ export function setupGpgHome() {
|
||||
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
|
||||
* @param {string} gpgHome - Path to GPG home directory
|
||||
@@ -136,6 +148,15 @@ export function setupGpgHome() {
|
||||
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
|
||||
const gpgCommand = getGpgCommand();
|
||||
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(
|
||||
gpgCommand,
|
||||
@@ -145,6 +166,7 @@ async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
|
||||
"--batch",
|
||||
"--keyserver",
|
||||
keyserver,
|
||||
...(proxyUrl ? ["--keyserver-options", `http-proxy=${proxyUrl}`] : []),
|
||||
"--recv-keys",
|
||||
keyFingerprint,
|
||||
],
|
||||
|
||||
+19
-6
@@ -17,14 +17,14 @@
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { installSonarScanner } from "./install-sonar-scanner";
|
||||
import { runSonarScanner } from "./run-sonar-scanner";
|
||||
import { installSonarScanner } from "./install-sonar-scanner.js";
|
||||
import { runSonarScanner } from "./run-sonar-scanner.js";
|
||||
import {
|
||||
checkGradleProject,
|
||||
checkMavenProject,
|
||||
checkSonarToken,
|
||||
validateScannerVersion,
|
||||
} from "./sanity-checks";
|
||||
} from "./sanity-checks.js";
|
||||
|
||||
/**
|
||||
* Inputs are defined in action.yml
|
||||
@@ -33,10 +33,14 @@ function getInputs() {
|
||||
const args = core.getInput("args");
|
||||
const projectBaseDir = core.getInput("projectBaseDir");
|
||||
const scannerBinariesUrl = core.getInput("scannerBinariesUrl");
|
||||
const scannerBinariesAuthHeader = core.getInput("scannerBinariesAuthHeader") || undefined;
|
||||
if (scannerBinariesAuthHeader) {
|
||||
core.setSecret(scannerBinariesAuthHeader);
|
||||
}
|
||||
const scannerVersion = core.getInput("scannerVersion");
|
||||
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() {
|
||||
try {
|
||||
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, skipSignatureVerification } =
|
||||
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, scannerBinariesAuthHeader, skipSignatureVerification } =
|
||||
getInputs();
|
||||
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 });
|
||||
|
||||
const scannerDir = await installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
scannerBinariesAuthHeader,
|
||||
skipSignatureVerification,
|
||||
});
|
||||
|
||||
|
||||
@@ -18,23 +18,34 @@
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
getPlatformFlavor,
|
||||
getScannerDownloadURL,
|
||||
scannerDirName,
|
||||
} from "./utils";
|
||||
import { verifySignature } from "./gpg-verification";
|
||||
} from "./utils.js";
|
||||
import { verifySignature } from "./gpg-verification.js";
|
||||
|
||||
const TOOLNAME = "sonar-scanner-cli";
|
||||
|
||||
async function ensureZipExtension(filePath) {
|
||||
if (filePath.endsWith(".zip")) {
|
||||
return filePath;
|
||||
}
|
||||
const zipPath = `${filePath}.zip`;
|
||||
await fs.rename(filePath, zipPath);
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the Sonar Scanner CLI for the current environment and cache it.
|
||||
*/
|
||||
export async function installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
scannerBinariesAuthHeader,
|
||||
skipSignatureVerification = false,
|
||||
}) {
|
||||
const flavor = getPlatformFlavor(os.platform(), os.arch());
|
||||
@@ -55,7 +66,7 @@ export async function installSonarScanner({
|
||||
|
||||
core.info(`Downloading from: ${downloadUrl}`);
|
||||
|
||||
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||
const downloadPath = await tc.downloadTool(downloadUrl, undefined, scannerBinariesAuthHeader);
|
||||
|
||||
if (skipSignatureVerification) {
|
||||
core.warning("⚠ Skipping GPG signature verification (not recommended)");
|
||||
@@ -65,7 +76,7 @@ export async function installSonarScanner({
|
||||
|
||||
let signaturePath;
|
||||
try {
|
||||
signaturePath = await tc.downloadTool(signatureUrl);
|
||||
signaturePath = await tc.downloadTool(signatureUrl, undefined, scannerBinariesAuthHeader);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to download signature file from ${signatureUrl}: ${error.message}`
|
||||
@@ -75,7 +86,9 @@ export async function installSonarScanner({
|
||||
await verifySignature(downloadPath, signaturePath);
|
||||
}
|
||||
|
||||
const extractedPath = await tc.extractZip(downloadPath);
|
||||
// PowerShell 5.1 (used on some Windows agents) requires the .zip extension for Expand-Archive
|
||||
const extractInput = await ensureZipExtension(downloadPath);
|
||||
const extractedPath = await tc.extractZip(extractInput);
|
||||
|
||||
// Find the actual scanner directory inside the extracted folder
|
||||
const scannerPath = path.join(
|
||||
|
||||
Reference in New Issue
Block a user