SQSCANGHA-127 Rename downloaded file to .zip before extraction on Windows (#251)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julien HENRYandClaude Sonnet 4.6 authored and GitHub committed 2026-06-08 14:51:37 +02:00
1 parent 3581139216
commit 713881670b
4 files changed
+155 -3

No files matched your search

+13 -1
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';
@@ -4154,6 +4155,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.
*/
@@ -4202,7 +4212,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(
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sources":["../node_modules/semver/internal/constants.js","../node_modules/semver/internal/debug.js","../node_modules/semver/internal/re.js","../node_modules/semver/internal/parse-options.js","../node_modules/semver/internal/identifiers.js","../node_modules/semver/classes/semver.js","../node_modules/semver/functions/parse.js","../node_modules/semver/functions/valid.js","../node_modules/semver/functions/clean.js","../node_modules/semver/functions/inc.js","../node_modules/semver/functions/diff.js","../node_modules/semver/functions/major.js","../node_modules/semver/functions/minor.js","../node_modules/semver/functions/patch.js","../node_modules/semver/functions/prerelease.js","../node_modules/semver/functions/compare.js","../node_modules/semver/functions/rcompare.js","../node_modules/semver/functions/compare-loose.js","../node_modules/semver/functions/compare-build.js","../node_modules/semver/functions/sort.js","../node_modules/semver/functions/rsort.js","../node_modules/semver/functions/gt.js","../node_modules/semver/functions/lt.js","../node_modules/semver/functions/eq.js","../node_modules/semver/functions/neq.js","../node_modules/semver/functions/gte.js","../node_modules/semver/functions/lte.js","../node_modules/semver/functions/cmp.js","../node_modules/semver/functions/coerce.js","../node_modules/semver/internal/lrucache.js","../node_modules/semver/classes/range.js","../node_modules/semver/classes/comparator.js","../node_modules/semver/functions/satisfies.js","../node_modules/semver/ranges/to-comparators.js","../node_modules/semver/ranges/max-satisfying.js","../node_modules/semver/ranges/min-satisfying.js","../node_modules/semver/ranges/min-version.js","../node_modules/semver/ranges/valid.js","../node_modules/semver/ranges/outside.js","../node_modules/semver/ranges/gtr.js","../node_modules/semver/ranges/ltr.js","../node_modules/semver/ranges/intersects.js","../node_modules/semver/ranges/simplify.js","../node_modules/semver/ranges/subset.js","../node_modules/semver/index.js","../node_modules/@actions/tool-cache/lib/manifest.js","../node_modules/@actions/tool-cache/node_modules/@actions/exec/lib/toolrunner.js","../node_modules/@actions/tool-cache/node_modules/@actions/exec/lib/exec.js","../node_modules/@actions/tool-cache/lib/retry-helper.js","../node_modules/@actions/tool-cache/lib/tool-cache.js","../src/main/utils.js","../src/main/gpg-verification.js","../src/main/install-sonar-scanner.js","../node_modules/string-argv/index.js","../src/main/run-sonar-scanner.js","../src/main/sanity-checks.js","../src/main/index.js"],"sourcesContent":["'use strict'\n\n// Note: this is the semver.org version of the spec that it implements\n// Not necessarily the package version of this code.\nconst SEMVER_SPEC_VERSION = '2.0.0'\n\nconst MAX_LENGTH = 256\nconst MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||\n/* istanbul ignore next */ 9007199254740991\n\n// Max safe segment length for coercion.\nconst MAX_SAFE_COMPONENT_LENGTH = 16\n\n// Max safe length for a build identifier. The max length minus 6 characters for\n// the shortest version with a build 0.0.0+BUILD.\nconst MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6\n\nconst RELEASE_TYPES = [\n 'major',\n 'premajor',\n 'minor',\n 'preminor',\n 'patch',\n 'prepatch',\n 'prerelease',\n]\n\nmodule.exports = {\n MAX_LENGTH,\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_SAFE_INTEGER,\n RELEASE_TYPES,\n SEMVER_SPEC_VERSION,\n FLAG_INCLUDE_PRERELEASE: 0b001,\n FLAG_LOOSE: 0b010,\n}\n","'use strict'\n\nconst debug = (\n typeof process === 'object' &&\n process.env &&\n process.env.NODE_DEBUG &&\n /\\bsemver\\b/i.test(process.env.NODE_DEBUG)\n) ? (...args) => console.error('SEMVER', ...args)\n : () => {}\n\nmodule.exports = debug\n","'use strict'\n\nconst {\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_LENGTH,\n} = require('./constants')\nconst debug = require('./debug')\nexports = module.exports = {}\n\n// The actual regexps go on exports.re\nconst re = exports.re = []\nconst safeRe = exports.safeRe = []\nconst src = exports.src = []\nconst safeSrc = exports.safeSrc = []\nconst t = exports.t = {}\nlet R = 0\n\nconst LETTERDASHNUMBER = '[a-zA-Z0-9-]'\n\n// Replace some greedy regex tokens to prevent regex dos issues. These regex are\n// used internally via the safeRe object since all inputs in this library get\n// normalized first to trim and collapse all extra whitespace. The original\n// regexes are exported for userland consumption and lower level usage. A\n// future breaking change could export the safer regex only with a note that\n// all input should have extra whitespace removed.\nconst safeRegexReplacements = [\n ['\\\\s', 1],\n ['\\\\d', MAX_LENGTH],\n [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],\n]\n\nconst makeSafeRegex = (value) => {\n for (const [token, max] of safeRegexReplacements) {\n value = value\n .split(`${token}*`).join(`${token}{0,${max}}`)\n .split(`${token}+`).join(`${tokeLine truncated
{"version":3,"file":"index.js","sources":["../node_modules/semver/internal/constants.js","../node_modules/semver/internal/debug.js","../node_modules/semver/internal/re.js","../node_modules/semver/internal/parse-options.js","../node_modules/semver/internal/identifiers.js","../node_modules/semver/classes/semver.js","../node_modules/semver/functions/parse.js","../node_modules/semver/functions/valid.js","../node_modules/semver/functions/clean.js","../node_modules/semver/functions/inc.js","../node_modules/semver/functions/diff.js","../node_modules/semver/functions/major.js","../node_modules/semver/functions/minor.js","../node_modules/semver/functions/patch.js","../node_modules/semver/functions/prerelease.js","../node_modules/semver/functions/compare.js","../node_modules/semver/functions/rcompare.js","../node_modules/semver/functions/compare-loose.js","../node_modules/semver/functions/compare-build.js","../node_modules/semver/functions/sort.js","../node_modules/semver/functions/rsort.js","../node_modules/semver/functions/gt.js","../node_modules/semver/functions/lt.js","../node_modules/semver/functions/eq.js","../node_modules/semver/functions/neq.js","../node_modules/semver/functions/gte.js","../node_modules/semver/functions/lte.js","../node_modules/semver/functions/cmp.js","../node_modules/semver/functions/coerce.js","../node_modules/semver/internal/lrucache.js","../node_modules/semver/classes/range.js","../node_modules/semver/classes/comparator.js","../node_modules/semver/functions/satisfies.js","../node_modules/semver/ranges/to-comparators.js","../node_modules/semver/ranges/max-satisfying.js","../node_modules/semver/ranges/min-satisfying.js","../node_modules/semver/ranges/min-version.js","../node_modules/semver/ranges/valid.js","../node_modules/semver/ranges/outside.js","../node_modules/semver/ranges/gtr.js","../node_modules/semver/ranges/ltr.js","../node_modules/semver/ranges/intersects.js","../node_modules/semver/ranges/simplify.js","../node_modules/semver/ranges/subset.js","../node_modules/semver/index.js","../node_modules/@actions/tool-cache/lib/manifest.js","../node_modules/@actions/tool-cache/node_modules/@actions/exec/lib/toolrunner.js","../node_modules/@actions/tool-cache/node_modules/@actions/exec/lib/exec.js","../node_modules/@actions/tool-cache/lib/retry-helper.js","../node_modules/@actions/tool-cache/lib/tool-cache.js","../src/main/utils.js","../src/main/gpg-verification.js","../src/main/install-sonar-scanner.js","../node_modules/string-argv/index.js","../src/main/run-sonar-scanner.js","../src/main/sanity-checks.js","../src/main/index.js"],"sourcesContent":["'use strict'\n\n// Note: this is the semver.org version of the spec that it implements\n// Not necessarily the package version of this code.\nconst SEMVER_SPEC_VERSION = '2.0.0'\n\nconst MAX_LENGTH = 256\nconst MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||\n/* istanbul ignore next */ 9007199254740991\n\n// Max safe segment length for coercion.\nconst MAX_SAFE_COMPONENT_LENGTH = 16\n\n// Max safe length for a build identifier. The max length minus 6 characters for\n// the shortest version with a build 0.0.0+BUILD.\nconst MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6\n\nconst RELEASE_TYPES = [\n 'major',\n 'premajor',\n 'minor',\n 'preminor',\n 'patch',\n 'prepatch',\n 'prerelease',\n]\n\nmodule.exports = {\n MAX_LENGTH,\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_SAFE_INTEGER,\n RELEASE_TYPES,\n SEMVER_SPEC_VERSION,\n FLAG_INCLUDE_PRERELEASE: 0b001,\n FLAG_LOOSE: 0b010,\n}\n","'use strict'\n\nconst debug = (\n typeof process === 'object' &&\n process.env &&\n process.env.NODE_DEBUG &&\n /\\bsemver\\b/i.test(process.env.NODE_DEBUG)\n) ? (...args) => console.error('SEMVER', ...args)\n : () => {}\n\nmodule.exports = debug\n","'use strict'\n\nconst {\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_LENGTH,\n} = require('./constants')\nconst debug = require('./debug')\nexports = module.exports = {}\n\n// The actual regexps go on exports.re\nconst re = exports.re = []\nconst safeRe = exports.safeRe = []\nconst src = exports.src = []\nconst safeSrc = exports.safeSrc = []\nconst t = exports.t = {}\nlet R = 0\n\nconst LETTERDASHNUMBER = '[a-zA-Z0-9-]'\n\n// Replace some greedy regex tokens to prevent regex dos issues. These regex are\n// used internally via the safeRe object since all inputs in this library get\n// normalized first to trim and collapse all extra whitespace. The original\n// regexes are exported for userland consumption and lower level usage. A\n// future breaking change could export the safer regex only with a note that\n// all input should have extra whitespace removed.\nconst safeRegexReplacements = [\n ['\\\\s', 1],\n ['\\\\d', MAX_LENGTH],\n [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],\n]\n\nconst makeSafeRegex = (value) => {\n for (const [token, max] of safeRegexReplacements) {\n value = value\n .split(`${token}*`).join(`${token}{0,${max}}`)\n .split(`${token}+`).join(`${tokeLine truncated
@@ -20,6 +20,7 @@
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";
@@ -37,6 +38,15 @@ function mockUtils(t) {
});
}
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 = [];
@@ -46,6 +56,7 @@ describe("installSonarScanner", () => {
});
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -94,6 +105,7 @@ describe("installSonarScanner", () => {
});
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -140,6 +152,7 @@ describe("installSonarScanner", () => {
});
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -178,6 +191,7 @@ describe("installSonarScanner", () => {
const cacheDirFn = mock.fn(async () => "/tmp/cached");
mockUtils(t);
mockFsPromises(t);
t.mock.module("@actions/tool-cache", {
namedExports: {
@@ -217,6 +231,120 @@ describe("installSonarScanner", () => {
"tc.cacheDir should be called with semver-compatible version");
});
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();
+13 -1
View File
@@ -18,6 +18,7 @@
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 {
@@ -30,6 +31,15 @@ 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.
*/
@@ -78,7 +88,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(