mirror of
https://github.com/SonarSource/sonarqube-scan-action.git
synced 2026-04-03 13:24:05 +03:00
SQSCANGHA-107 Make room for install-build-wrapper action
This commit is contained in:
committed by
Julien HENRY
parent
a64281002c
commit
a88c96d7e4
7
src/main/__tests__/mocks.js
Normal file
7
src/main/__tests__/mocks.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function mockCore(overrides = {}) {
|
||||
return {
|
||||
setFailed: (msg) => console.error(msg),
|
||||
warning: (msg) => console.log(msg),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
177
src/main/__tests__/sanity-checks.test.js
Normal file
177
src/main/__tests__/sanity-checks.test.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import mockfs from "mock-fs";
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it, mock } from "node:test";
|
||||
import {
|
||||
checkGradleProject,
|
||||
checkMavenProject,
|
||||
checkSonarToken,
|
||||
validateScannerVersion,
|
||||
} from "../sanity-checks.js";
|
||||
import { mockCore } from "./mocks.js";
|
||||
|
||||
describe("validateScannerVersion", () => {
|
||||
const expected =
|
||||
"Invalid scannerVersion format. Expected format: x.y.z.w (e.g., 7.1.0.4889)";
|
||||
|
||||
const validVersions = [undefined, "", "7.1.0.4889", "1.2.3.4"];
|
||||
|
||||
const invalidVersions = [
|
||||
"wrong",
|
||||
"4.2.",
|
||||
"7.1.0",
|
||||
"7.1.0.abc",
|
||||
"7.1.0.4889.5",
|
||||
"7.1",
|
||||
"7",
|
||||
"7.1.0.",
|
||||
".7.1.0.4889",
|
||||
"7..1.0.4889",
|
||||
"7.1..0.4889",
|
||||
"7.1.0..4889",
|
||||
"a.b.c.d",
|
||||
"7.1.0.4889-SNAPSHOT",
|
||||
"v7.1.0.4889",
|
||||
"7.1.0.4889.0.0",
|
||||
"-7.1.0.4889",
|
||||
"7.-1.0.4889",
|
||||
"7.1.-0.4889",
|
||||
"7.1.0.-4889",
|
||||
"7.1.0.4889 ",
|
||||
" 7.1.0.4889",
|
||||
"7.1.0.4889\n",
|
||||
"7,1,0,4889",
|
||||
];
|
||||
|
||||
validVersions.forEach((version) => {
|
||||
it(`accepts ${version}`, () => {
|
||||
assert.equal(validateScannerVersion(version), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
invalidVersions.forEach((version) =>
|
||||
it(`throws for ${version}`, () => {
|
||||
assert.throws(
|
||||
() => validateScannerVersion(version),
|
||||
{
|
||||
message: expected,
|
||||
},
|
||||
`should have thrown for ${version}`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe("checkSonarToken", () => {
|
||||
it("calls core.warning when SONAR_TOKEN is not set", () => {
|
||||
const warning = mock.fn();
|
||||
|
||||
checkSonarToken(mockCore({ warning }));
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Running this GitHub Action without SONAR_TOKEN is not recommended"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call core.warning when SONAR_TOKEN is set", () => {
|
||||
const warning = mock.fn();
|
||||
|
||||
checkSonarToken(mockCore({ warning }), "test-token");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkMavenProject", () => {
|
||||
it("calls core.warning when pom.xml exists", async () => {
|
||||
mockfs({ "/test/project/": { "pom.xml": "" } });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkMavenProject({ warning }, "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Maven project detected. Sonar recommends running the 'org.sonarsource.scanner.maven:sonar-maven-plugin:sonar' goal during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("does not call core.warning when pom.xml does not exist", async () => {
|
||||
mockfs({ "/test/project/": {} });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkMavenProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 0);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("handles project base dir with trailing slash", async () => {
|
||||
mockfs({ "/test/project/": { "pom.xml": "" } });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkMavenProject(mockCore({ warning }), "/test/project/");
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkGradleProject", () => {
|
||||
it("calls core.warning when build.gradle exists", async () => {
|
||||
mockfs({ "/test/project/": { "build.gradle": "" } });
|
||||
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("calls core.warning when build.gradle.kts exists", async () => {
|
||||
mockfs({ "/test/project/": { "build.gradle.kts": "" } });
|
||||
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("does not call core.warning when neither gradle file exists", async () => {
|
||||
mockfs({ "/test/project/": {} });
|
||||
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 0);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("handles project base dir with trailing slash", async () => {
|
||||
mockfs({ "/test/project/": { "build.gradle": "" } });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project/");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
});
|
||||
});
|
||||
81
src/main/__tests__/utils.test.js
Normal file
81
src/main/__tests__/utils.test.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
getPlatformFlavor,
|
||||
getScannerDownloadURL,
|
||||
scannerDirName,
|
||||
} from "../utils.js";
|
||||
|
||||
describe("getPlatformFlavor", () => {
|
||||
const supportedPlatforms = [
|
||||
{ platform: "linux", arch: "x64", expected: "linux-x64" },
|
||||
{ platform: "linux", arch: "arm64", expected: "linux-aarch64" },
|
||||
{ platform: "win32", arch: "x64", expected: "windows-x64" },
|
||||
{ platform: "darwin", arch: "x64", expected: "macosx-x64" },
|
||||
{ platform: "darwin", arch: "arm64", expected: "macosx-aarch64" },
|
||||
];
|
||||
|
||||
const unsupportedPlatforms = [
|
||||
{ platform: "linux", arch: "arm" },
|
||||
{ platform: "openbsd", arch: "x64" },
|
||||
{ platform: undefined, arch: "x64" },
|
||||
{ platform: "linux", arch: undefined },
|
||||
{ platform: null, arch: "x64" },
|
||||
{ platform: "linux", arch: null },
|
||||
];
|
||||
|
||||
supportedPlatforms.forEach(({ platform, arch, expected }) => {
|
||||
it(`returns ${expected} for ${platform} ${arch}`, () => {
|
||||
assert.equal(getPlatformFlavor(platform, arch), expected);
|
||||
});
|
||||
});
|
||||
|
||||
unsupportedPlatforms.forEach(({ platform, arch }) => {
|
||||
it(`throws for unsupported platform ${platform} ${arch}`, () => {
|
||||
assert.throws(
|
||||
() => getPlatformFlavor(platform, arch),
|
||||
{
|
||||
message: `Platform ${platform} ${arch} not supported`,
|
||||
},
|
||||
`should have thrown for ${platform} ${arch}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScannerDownloadURL", () => {
|
||||
it("generates correct URL without trailing slash", () => {
|
||||
const result = getScannerDownloadURL({
|
||||
scannerBinariesUrl:
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli",
|
||||
scannerVersion: "7.2.0.5079",
|
||||
flavor: "linux-x64",
|
||||
});
|
||||
assert.equal(
|
||||
result,
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-linux-x64.zip"
|
||||
);
|
||||
});
|
||||
|
||||
it("generates correct URL with trailing slash", () => {
|
||||
const result = getScannerDownloadURL({
|
||||
scannerBinariesUrl:
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/",
|
||||
scannerVersion: "7.2.0.5079",
|
||||
flavor: "linux-x64",
|
||||
});
|
||||
assert.equal(
|
||||
result,
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-linux-x64.zip"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scannerDirName", () => {
|
||||
it("handles special characters", () => {
|
||||
assert.equal(
|
||||
scannerDirName("7.2.0-SNAPSHOT", "linux_x64"),
|
||||
"sonar-scanner-7.2.0-SNAPSHOT-linux_x64"
|
||||
);
|
||||
});
|
||||
});
|
||||
59
src/main/install-sonar-scanner.js
Normal file
59
src/main/install-sonar-scanner.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import {
|
||||
getPlatformFlavor,
|
||||
getScannerDownloadURL,
|
||||
scannerDirName,
|
||||
} from "./utils";
|
||||
|
||||
const TOOLNAME = "sonar-scanner-cli";
|
||||
|
||||
/**
|
||||
* Download the Sonar Scanner CLI for the current environment and cache it.
|
||||
*/
|
||||
export async function installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
}) {
|
||||
const flavor = getPlatformFlavor(os.platform(), os.arch());
|
||||
|
||||
// Check if tool is already cached
|
||||
let toolDir = tc.find(TOOLNAME, scannerVersion, flavor);
|
||||
|
||||
if (!toolDir) {
|
||||
core.info(
|
||||
`Installing Sonar Scanner CLI ${scannerVersion} for ${flavor}...`
|
||||
);
|
||||
|
||||
const downloadUrl = getScannerDownloadURL({
|
||||
scannerBinariesUrl,
|
||||
scannerVersion,
|
||||
flavor,
|
||||
});
|
||||
|
||||
core.info(`Downloading from: ${downloadUrl}`);
|
||||
|
||||
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||
const extractedPath = await tc.extractZip(downloadPath);
|
||||
|
||||
// Find the actual scanner directory inside the extracted folder
|
||||
const scannerPath = path.join(
|
||||
extractedPath,
|
||||
scannerDirName(scannerVersion, flavor)
|
||||
);
|
||||
|
||||
toolDir = await tc.cacheDir(scannerPath, TOOLNAME, scannerVersion, flavor);
|
||||
|
||||
core.info(`Sonar Scanner CLI cached to: ${toolDir}`);
|
||||
} else {
|
||||
core.info(`Using cached Sonar Scanner CLI from: ${toolDir}`);
|
||||
}
|
||||
|
||||
// Add the bin directory to PATH
|
||||
const binDir = path.join(toolDir, "bin");
|
||||
core.addPath(binDir);
|
||||
|
||||
return toolDir;
|
||||
}
|
||||
152
src/main/run-sonar-scanner.js
Normal file
152
src/main/run-sonar-scanner.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { parseArgsStringToArgv } from "string-argv";
|
||||
|
||||
const KEYTOOL_MAIN_CLASS = "sun.security.tools.keytool.Main";
|
||||
const TRUSTSTORE_PASSWORD = "changeit"; // default password of the Java truststore!
|
||||
|
||||
export async function runSonarScanner(
|
||||
inputArgs,
|
||||
projectBaseDir,
|
||||
scannerDir,
|
||||
runnerEnv = {}
|
||||
) {
|
||||
const { runnerDebug, runnerOs, runnerTemp, sonarRootCert, sonarcloudUrl } =
|
||||
runnerEnv;
|
||||
|
||||
const scannerBin =
|
||||
runnerOs === "Windows" ? "sonar-scanner.bat" : "sonar-scanner";
|
||||
|
||||
const scannerArgs = [];
|
||||
|
||||
/**
|
||||
* Not sanitization is needed when populating scannerArgs.
|
||||
* @actions/exec will take care of sanitizing the args it receives.
|
||||
*/
|
||||
|
||||
if (sonarcloudUrl) {
|
||||
scannerArgs.push(`-Dsonar.scanner.sonarcloudUrl=${sonarcloudUrl}`);
|
||||
}
|
||||
|
||||
if (runnerDebug === "1") {
|
||||
scannerArgs.push("--debug");
|
||||
}
|
||||
|
||||
if (projectBaseDir) {
|
||||
scannerArgs.push(`-Dsonar.projectBaseDir=${projectBaseDir}`);
|
||||
}
|
||||
|
||||
// The SSL folder may exist on an uncleaned self-hosted runner
|
||||
const sslFolder = path.join(os.homedir(), ".sonar", "ssl");
|
||||
const truststoreFile = path.join(sslFolder, "truststore.p12");
|
||||
|
||||
const keytoolParams = {
|
||||
scannerDir,
|
||||
truststoreFile,
|
||||
};
|
||||
|
||||
if (fs.existsSync(truststoreFile)) {
|
||||
let aliasSonarIsPresent = true;
|
||||
|
||||
try {
|
||||
await checkSonarAliasInTruststore(keytoolParams);
|
||||
} catch (_) {
|
||||
aliasSonarIsPresent = false;
|
||||
core.info(
|
||||
`Existing Scanner truststore ${truststoreFile} does not contain 'sonar' alias`
|
||||
);
|
||||
}
|
||||
|
||||
if (aliasSonarIsPresent) {
|
||||
core.info(
|
||||
`Removing 'sonar' alias from already existing Scanner truststore: ${truststoreFile}`
|
||||
);
|
||||
await deleteSonarAliasFromTruststore(keytoolParams);
|
||||
}
|
||||
}
|
||||
|
||||
if (sonarRootCert) {
|
||||
core.info("Adding SSL certificate to the Scanner truststore");
|
||||
const tempCertPath = path.join(runnerTemp, "tmpcert.pem");
|
||||
|
||||
try {
|
||||
fs.unlinkSync(tempCertPath);
|
||||
} catch (_) {
|
||||
// File doesn't exist, ignore
|
||||
}
|
||||
|
||||
fs.writeFileSync(tempCertPath, sonarRootCert);
|
||||
fs.mkdirSync(sslFolder, { recursive: true });
|
||||
|
||||
await importCertificateToTruststore(keytoolParams, tempCertPath);
|
||||
|
||||
scannerArgs.push(
|
||||
`-Dsonar.scanner.truststorePassword=${TRUSTSTORE_PASSWORD}`
|
||||
);
|
||||
}
|
||||
|
||||
if (inputArgs) {
|
||||
/**
|
||||
* No sanitization, but it is parsing a string into an array of arguments in a safe way (= no command execution),
|
||||
* and with good enough support of quotes to support arguments containing spaces.
|
||||
*/
|
||||
const args = parseArgsStringToArgv(inputArgs);
|
||||
scannerArgs.push(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments are sanitized by `exec`
|
||||
*/
|
||||
await exec.exec(scannerBin, scannerArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use keytool for now, as SonarQube 10.6 and below doesn't support openssl generated keystores
|
||||
* keytool requires a password > 6 characters, so we won't use the default password 'sonar'
|
||||
*/
|
||||
function executeKeytoolCommand({
|
||||
scannerDir,
|
||||
truststoreFile,
|
||||
extraArgs,
|
||||
options = {},
|
||||
}) {
|
||||
const baseArgs = [
|
||||
KEYTOOL_MAIN_CLASS,
|
||||
"-storetype",
|
||||
"PKCS12",
|
||||
"-keystore",
|
||||
truststoreFile,
|
||||
"-storepass",
|
||||
TRUSTSTORE_PASSWORD,
|
||||
"-noprompt",
|
||||
"-trustcacerts",
|
||||
...extraArgs,
|
||||
];
|
||||
|
||||
return exec.exec(`${scannerDir}/jre/bin/java`, baseArgs, options);
|
||||
}
|
||||
|
||||
function importCertificateToTruststore(keytoolParams, certPath) {
|
||||
return executeKeytoolCommand({
|
||||
...keytoolParams,
|
||||
extraArgs: ["-importcert", "-alias", "sonar", "-file", certPath],
|
||||
});
|
||||
}
|
||||
|
||||
function checkSonarAliasInTruststore(keytoolParams) {
|
||||
return executeKeytoolCommand({
|
||||
...keytoolParams,
|
||||
extraArgs: ["-list", "-v", "-alias", "sonar"],
|
||||
options: { silent: true },
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSonarAliasFromTruststore(keytoolParams) {
|
||||
return executeKeytoolCommand({
|
||||
...keytoolParams,
|
||||
extraArgs: ["-delete", "-alias", "sonar"],
|
||||
});
|
||||
}
|
||||
44
src/main/sanity-checks.js
Normal file
44
src/main/sanity-checks.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import fs from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export function validateScannerVersion(version) {
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const versionRegex = /^\d+\.\d+\.\d+\.\d+$/;
|
||||
if (!versionRegex.test(version)) {
|
||||
throw new Error(
|
||||
"Invalid scannerVersion format. Expected format: x.y.z.w (e.g., 7.1.0.4889)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkSonarToken(core, sonarToken) {
|
||||
if (!sonarToken) {
|
||||
core.warning(
|
||||
"Running this GitHub Action without SONAR_TOKEN is not recommended"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkMavenProject(core, projectBaseDir) {
|
||||
const pomPath = join(projectBaseDir.replace(/\/$/, ""), "pom.xml");
|
||||
if (fs.existsSync(pomPath)) {
|
||||
core.warning(
|
||||
"Maven project detected. Sonar recommends running the 'org.sonarsource.scanner.maven:sonar-maven-plugin:sonar' goal during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkGradleProject(core, projectBaseDir) {
|
||||
const baseDir = projectBaseDir.replace(/\/$/, "");
|
||||
const gradlePath = join(baseDir, "build.gradle");
|
||||
const gradleKtsPath = join(baseDir, "build.gradle.kts");
|
||||
|
||||
if (fs.existsSync(gradlePath) || fs.existsSync(gradleKtsPath)) {
|
||||
core.warning(
|
||||
"Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/main/utils.js
Normal file
35
src/main/utils.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const platformFlavor = {
|
||||
linux: {
|
||||
x64: "linux-x64",
|
||||
arm64: "linux-aarch64",
|
||||
},
|
||||
win32: {
|
||||
x64: "windows-x64",
|
||||
},
|
||||
darwin: {
|
||||
x64: "macosx-x64",
|
||||
arm64: "macosx-aarch64",
|
||||
},
|
||||
};
|
||||
|
||||
export function getPlatformFlavor(platform, arch) {
|
||||
const flavor = platformFlavor[platform]?.[arch];
|
||||
|
||||
if (!flavor) {
|
||||
throw new Error(`Platform ${platform} ${arch} not supported`);
|
||||
}
|
||||
|
||||
return flavor;
|
||||
}
|
||||
|
||||
export function getScannerDownloadURL({
|
||||
scannerBinariesUrl,
|
||||
scannerVersion,
|
||||
flavor,
|
||||
}) {
|
||||
const trimURL = scannerBinariesUrl.replace(/\/$/, "");
|
||||
return `${trimURL}/sonar-scanner-cli-${scannerVersion}-${flavor}.zip`;
|
||||
}
|
||||
|
||||
export const scannerDirName = (version, flavor) =>
|
||||
`sonar-scanner-${version}-${flavor}`;
|
||||
Reference in New Issue
Block a user