Compare commits

..

2 Commits

Author SHA1 Message Date
Julien HENRY b24f324433 Use mise 2026-06-04 16:43:59 +02:00
Julien HENRY ec7d7e54b2 SQSCANGHA-127 Rename downloaded file to .zip before extraction on Windows
PowerShell 5.1, used on some Windows GitHub Actions runners, requires
Expand-Archive to receive a file with a .zip extension. The @actions/tool-cache
downloadTool function saves to a temp path without any extension, causing
extraction to fail on PS 5.1. Rename the file to add .zip before calling
extractZip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:43:18 +02:00
6 changed files with 126 additions and 70 deletions
+15 -17
View File
@@ -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';
@@ -3862,19 +3863,6 @@ function getScannerDownloadURL({
const scannerDirName = (version, flavor) =>
`sonar-scanner-${version}-${flavor}`;
/**
* Converts a 4-part version string (e.g. "8.0.1.6346") to a SemVer 2.0 compatible
* string (e.g. "8.0.1-build.6346") for use with GitHub's tool-cache library,
* which requires SemVer-compliant version strings.
*/
function toSemVer(version) {
const parts = version.split(".");
if (parts.length === 4) {
return `${parts[0]}.${parts[1]}.${parts[2]}-build.${parts[3]}`;
}
return version;
}
/*
* sonarqube-scan-action
* Copyright (C) 2025 SonarSource SA
@@ -4154,6 +4142,15 @@ 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.
*/
@@ -4164,10 +4161,9 @@ async function installSonarScanner({
skipSignatureVerification = false,
}) {
const flavor = getPlatformFlavor(os$1.platform(), os$1.arch());
const semVerVersion = toSemVer(scannerVersion);
// Check if tool is already cached
let toolDir = find(TOOLNAME, semVerVersion, flavor);
let toolDir = find(TOOLNAME, scannerVersion, flavor);
if (!toolDir) {
info(
@@ -4202,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(
@@ -4210,7 +4208,7 @@ async function installSonarScanner({
scannerDirName(scannerVersion, flavor)
);
toolDir = await cacheDir(scannerPath, TOOLNAME, semVerVersion, flavor);
toolDir = await cacheDir(scannerPath, TOOLNAME, scannerVersion, flavor);
info(`Sonar Scanner CLI cached to: ${toolDir}`);
} else {
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -20,9 +20,9 @@
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 SCANNER_SEMVER_VERSION = "6.2.0-build.4584";
const BINARIES_URL = "https://my.artifactory.example.com/sonar-scanner-cli";
const BINARY_DOWNLOAD_URL = `${BINARIES_URL}/sonar-scanner-cli-${SCANNER_VERSION}-linux-x64.zip`;
@@ -32,7 +32,15 @@ function mockUtils(t) {
getPlatformFlavor: mock.fn(() => "linux-x64"),
getScannerDownloadURL: mock.fn(() => BINARY_DOWNLOAD_URL),
scannerDirName: mock.fn(() => `sonar-scanner-${SCANNER_VERSION}-linux-x64`),
toSemVer: mock.fn(() => SCANNER_SEMVER_VERSION),
},
});
}
function mockFsPromises(t) {
t.mock.module("node:fs/promises", {
namedExports: {
...nodeFsPromises,
rename: mock.fn(async () => {}),
},
});
}
@@ -46,6 +54,7 @@ describe("installSonarScanner", () => {
});
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -94,6 +103,7 @@ describe("installSonarScanner", () => {
});
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -140,6 +150,7 @@ describe("installSonarScanner", () => {
});
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -173,18 +184,30 @@ describe("installSonarScanner", () => {
assert.equal(downloadCalls[0].auth, "Bearer mytoken");
});
it("should use semver-compatible version for tool-cache find and cacheDir", async (t) => {
const findFn = mock.fn(() => null);
const cacheDirFn = mock.fn(async () => "/tmp/cached");
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: findFn,
downloadTool: mock.fn(async () => "/tmp/downloaded"),
extractZip: mock.fn(async () => "/tmp/extracted"),
cacheDir: cacheDirFn,
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"),
},
});
@@ -203,18 +226,76 @@ describe("installSonarScanner", () => {
});
const { installSonarScanner } = await import(
`../install-sonar-scanner.js?test=semver-version`
`../install-sonar-scanner.js?test=rename-zip`
);
await installSonarScanner({
scannerVersion: SCANNER_VERSION,
scannerBinariesUrl: BINARIES_URL,
skipSignatureVerification: true,
});
assert.equal(findFn.mock.calls[0].arguments[1], SCANNER_SEMVER_VERSION,
"tc.find should be called with semver-compatible version");
assert.equal(cacheDirFn.mock.calls[0].arguments[2], SCANNER_SEMVER_VERSION,
"tc.cacheDir should be called with semver-compatible version");
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) => {
-20
View File
@@ -22,7 +22,6 @@ import {
getPlatformFlavor,
getScannerDownloadURL,
scannerDirName,
toSemVer,
} from "../utils.js";
describe("getPlatformFlavor", () => {
@@ -98,22 +97,3 @@ describe("scannerDirName", () => {
);
});
});
describe("toSemVer", () => {
it("converts 4-part version to semver pre-release format", () => {
assert.equal(toSemVer("8.0.1.6346"), "8.0.1-build.6346");
});
it("leaves 3-part semver version unchanged", () => {
assert.equal(toSemVer("8.0.1"), "8.0.1");
});
it("leaves version with pre-release identifier unchanged", () => {
assert.equal(toSemVer("7.2.0-SNAPSHOT"), "7.2.0-SNAPSHOT");
});
it("converts different 4-part versions correctly", () => {
assert.equal(toSemVer("6.2.0.4584"), "6.2.0-build.4584");
assert.equal(toSemVer("8.1.0.6389"), "8.1.0-build.6389");
});
});
+15 -5
View File
@@ -18,18 +18,27 @@
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,
toSemVer,
} 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.
*/
@@ -40,10 +49,9 @@ export async function installSonarScanner({
skipSignatureVerification = false,
}) {
const flavor = getPlatformFlavor(os.platform(), os.arch());
const semVerVersion = toSemVer(scannerVersion);
// Check if tool is already cached
let toolDir = tc.find(TOOLNAME, semVerVersion, flavor);
let toolDir = tc.find(TOOLNAME, scannerVersion, flavor);
if (!toolDir) {
core.info(
@@ -78,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(
@@ -86,7 +96,7 @@ export async function installSonarScanner({
scannerDirName(scannerVersion, flavor)
);
toolDir = await tc.cacheDir(scannerPath, TOOLNAME, semVerVersion, flavor);
toolDir = await tc.cacheDir(scannerPath, TOOLNAME, scannerVersion, flavor);
core.info(`Sonar Scanner CLI cached to: ${toolDir}`);
} else {
-13
View File
@@ -51,16 +51,3 @@ export function getScannerDownloadURL({
export const scannerDirName = (version, flavor) =>
`sonar-scanner-${version}-${flavor}`;
/**
* Converts a 4-part version string (e.g. "8.0.1.6346") to a SemVer 2.0 compatible
* string (e.g. "8.0.1-build.6346") for use with GitHub's tool-cache library,
* which requires SemVer-compliant version strings.
*/
export function toSemVer(version) {
const parts = version.split(".");
if (parts.length === 4) {
return `${parts[0]}.${parts[1]}.${parts[2]}-build.${parts[3]}`;
}
return version;
}