From f2412f892de56c0fe31217c8d945a3b194c5c9c1 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 23:09:58 +0100 Subject: [PATCH 01/12] Add e2e test --- .../workflows/__diagnostics-without-db.yml | 84 +++++++++++++++++++ pr-checks/checks/diagnostics-without-db.yml | 25 ++++++ pr-checks/diagnostics-without-db.ts | 40 +++++++++ 3 files changed, 149 insertions(+) create mode 100644 .github/workflows/__diagnostics-without-db.yml create mode 100644 pr-checks/checks/diagnostics-without-db.yml create mode 100644 pr-checks/diagnostics-without-db.ts diff --git a/.github/workflows/__diagnostics-without-db.yml b/.github/workflows/__diagnostics-without-db.yml new file mode 100644 index 0000000000..e7e633ea71 --- /dev/null +++ b/.github/workflows/__diagnostics-without-db.yml @@ -0,0 +1,84 @@ +# Warning: This file is generated automatically, and should not be modified. +# Instead, please modify the template in the pr-checks directory and run: +# pr-checks/sync.sh +# to regenerate this file. + +name: PR Check - Diagnostic export without database +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GO111MODULE: auto +on: + push: + branches: + - main + - releases/v* + pull_request: {} + merge_group: + types: + - checks_requested + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + inputs: {} + workflow_call: + inputs: {} +defaults: + run: + shell: bash +concurrency: + cancel-in-progress: ${{ github.event_name == 'pull_request' || false }} + group: diagnostics-without-db-${{github.ref}} +jobs: + diagnostics-without-db: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + version: linked + name: Diagnostic export without database + if: github.triggering_actor != 'dependabot[bot]' + permissions: + contents: read + security-events: read + timeout-minutes: 45 + runs-on: ${{ matrix.os }} + steps: + # We deliberately need to make this step fail before the database is initialised, but after + # some diagnostic has been produced. The simplest way to do that right now is to force the + # use of a nightly release and then fail because we are attempting to analyse Swift on + # a non-macOS runner. + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20.x + cache: npm + - name: Install newer npm + run: npm install -g npm@11.19.1 + - name: Install dependencies + run: npm ci + - name: Prepare test + id: prepare-test + uses: ./.github/actions/prepare-test + with: + version: ${{ matrix.version }} + use-all-platform-bundle: 'false' + setup-kotlin: 'true' + - name: Initialise failing analysis + id: init + uses: ./../action/init + continue-on-error: true + env: + CODEQL_ACTION_FORCE_NIGHTLY: true + with: + languages: swift + tools: ${{ steps.prepare-test.outputs.tools-url }} + + - name: Check diagnostics appear on disk + env: + SARIF_PATH: '${{ runner.temp }}/codeql-action-temp-diagnostics/' + run: npx tsx ./pr-checks/diagnostics-without-db.ts + env: + CODEQL_ACTION_TEST_MODE: true diff --git a/pr-checks/checks/diagnostics-without-db.yml b/pr-checks/checks/diagnostics-without-db.yml new file mode 100644 index 0000000000..2d8eff72c0 --- /dev/null +++ b/pr-checks/checks/diagnostics-without-db.yml @@ -0,0 +1,25 @@ +name: "Diagnostic export without database" +description: "Tests that temporary diagnostics are correctly included in the failed SARIF." +versions: + # Overriden by the FF below. + - linked +installNode: true +steps: + # We deliberately need to make this step fail before the database is initialised, but after + # some diagnostic has been produced. The simplest way to do that right now is to force the + # use of a nightly release and then fail because we are attempting to analyse Swift on + # a non-macOS runner. + - name: Initialise failing analysis + id: init + uses: ./../action/init + continue-on-error: true + env: + CODEQL_ACTION_FORCE_NIGHTLY: true + with: + languages: swift + tools: ${{ steps.prepare-test.outputs.tools-url }} + + - name: Check diagnostics appear on disk + env: + SARIF_PATH: "${{ runner.temp }}/codeql-action-temp-diagnostics/" + run: npx tsx ./pr-checks/diagnostics-without-db.ts diff --git a/pr-checks/diagnostics-without-db.ts b/pr-checks/diagnostics-without-db.ts new file mode 100644 index 0000000000..b7e99c939e --- /dev/null +++ b/pr-checks/diagnostics-without-db.ts @@ -0,0 +1,40 @@ +import * as fs from "fs"; +import * as path from "path"; + +import * as core from "@actions/core"; + +import type * as diagnostics from "../src/diagnostics"; + +const DIAGNOSTIC_ID = "codeql-action/forced-nightly-cli"; +const sarifDirectory = process.env["SARIF_PATH"]; + +if (sarifDirectory === undefined) { + throw new Error("SARIF_PATH is undefined."); +} + +const paths = fs.readdirSync(sarifDirectory, { + recursive: true, + encoding: "utf-8", +}); + +let found = false; +for (const diagnosticPath of paths) { + if (path.extname(diagnosticPath) !== ".json") { + core.info(`Skipping ${diagnosticPath}.`); + continue; + } + core.info(`Found ${diagnosticPath}.`); + + const contents = JSON.parse( + fs.readFileSync(path.resolve(sarifDirectory, diagnosticPath), "utf-8"), + ) as diagnostics.DiagnosticMessage; + + if (contents.source.id === DIAGNOSTIC_ID) { + found = true; + break; + } +} + +if (!found) { + throw new Error(`Didn't find '${DIAGNOSTIC_ID}' diagnostic.`); +} From 520eff7a220d53cfb6e3cdcd9a82d2dbd805134c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 23:38:11 +0100 Subject: [PATCH 02/12] Fix `flushDiagnostics` not ensuring directories exist --- lib/entry-points.js | 13 +++++++++++-- src/diagnostics.test.ts | 3 +-- src/diagnostics.ts | 16 +++++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 54362e40d0..7376558e1d 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -173070,6 +173070,7 @@ function flushDiagnostics(action, config) { action.logger.debug( `Moving ${diagnosticsCount} diagnostic(s) to their respective databases.` ); + const failedFlushDiagnostics = []; for (const temporary of temporaryDiagnostics) { const directory = getDatabaseDiagnosticsPath( config, @@ -173077,9 +173078,17 @@ function flushDiagnostics(action, config) { ); const filename = import_path2.default.basename(temporary.path); const destination = import_path2.default.join(directory, filename); - action.fs.renameSync(temporary.path, destination); + try { + action.fs.mkdirSync(directory, { recursive: true }); + action.fs.renameSync(temporary.path, destination); + } catch (err) { + action.logger.warning( + `Failed to rename '${temporary.path}' to '${destination}': ${getErrorMessage(err)}` + ); + failedFlushDiagnostics.push(temporary); + } } - temporaryDiagnostics = []; + temporaryDiagnostics = failedFlushDiagnostics; } function makeTelemetryDiagnostic(id, name, attributes, tags) { return makeDiagnostic(id, name, { diff --git a/src/diagnostics.test.ts b/src/diagnostics.test.ts index e721dfc529..7d898defc7 100644 --- a/src/diagnostics.test.ts +++ b/src/diagnostics.test.ts @@ -88,9 +88,8 @@ test("addDiagnostic writes temporary diagnostics and flushDiagnostics moves them ); }); - // addDiagnostic t.is(existsSync.callCount, 1); - t.is(mkdirSync.callCount, 1); + t.is(mkdirSync.callCount, 2); t.is(writeFileSync.callCount, 1); t.is(writeFileSync.args[0].length, 2); t.is(writeFileSync.args[0][1], JSON.stringify(diagnostic)); diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 3563e41f4f..a29ef25ec3 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -7,7 +7,7 @@ import type { Config } from "./config-utils"; import type { FileSystem } from "./fs"; import { Language } from "./languages"; import { getActionsLogger, type Logger } from "./logging"; -import { getCodeQLDatabasePath } from "./util"; +import { getCodeQLDatabasePath, getErrorMessage } from "./util"; /** * Known tags for diagnostics. There is currently only "internal-error", @@ -304,6 +304,7 @@ export function flushDiagnostics( `Moving ${diagnosticsCount} diagnostic(s) to their respective databases.`, ); + const failedFlushDiagnostics: TemporaryDiagnostic[] = []; for (const temporary of temporaryDiagnostics) { // If `temporary.language` is `undefined`, then we didn't have a `config` at the time that // `addNoLanguageDiagnostic` was called. In that case, we arbitrarily choose the first @@ -315,11 +316,20 @@ export function flushDiagnostics( const filename = path.basename(temporary.path); const destination = path.join(directory, filename); - action.fs.renameSync(temporary.path, destination); + try { + action.fs.mkdirSync(directory, { recursive: true }); + action.fs.renameSync(temporary.path, destination); + } catch (err) { + action.logger.warning( + `Failed to rename '${temporary.path}' to '${destination}': ${getErrorMessage(err)}`, + ); + + failedFlushDiagnostics.push(temporary); + } } // Reset the temporary diagnostics arrays. - temporaryDiagnostics = []; + temporaryDiagnostics = failedFlushDiagnostics; } /** From b2d253eaa144a31a52afb563e7746c4f712e5d8e Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 10:48:24 +0100 Subject: [PATCH 03/12] Add `macOSVersion` helper --- src/platform.test.ts | 40 +++++++++++++++++++++++++++++++++++++++- src/platform.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/platform.test.ts b/src/platform.test.ts index c8a6a7955d..803e28b903 100644 --- a/src/platform.test.ts +++ b/src/platform.test.ts @@ -1,6 +1,7 @@ import test from "ava"; -import { BundlePlatform, getBundlePlatform } from "./platform"; +import { BundlePlatform, getBundlePlatform, macOSVersion } from "./platform"; +import { initAllState } from "./testing-utils"; for (const [platform, arch, expected] of [ ["linux", "x64", BundlePlatform.Linux64], @@ -16,3 +17,40 @@ for (const [platform, arch, expected] of [ t.is(getBundlePlatform(platform, arch), expected); }); } + +const platforms: NodeJS.Platform[] = ["linux", "win32", "freebsd"]; +for (const platform of platforms) { + test(`macOSVersion returns undefined on ${platform}`, (t) => { + t.is(macOSVersion(initAllState({ platform })), undefined); + }); +} + +test("macOSVersion returns raw string if semver parsing fails", (t) => { + const invalidSemVer = "sealOS-2026"; + t.is( + macOSVersion( + initAllState({ platform: "darwin", osRelease: invalidSemVer }), + ), + invalidSemVer, + ); +}); + +test("macOSVersion returns semver if parsing succeeds", (t) => { + const validSemVer = "27.0.1"; + + const version = macOSVersion( + initAllState({ platform: "darwin", osRelease: validSemVer }), + ); + + // Check that `version` is not undefined and narrow the type; throws if undefined. + if (t.truthy(version)) { + // Check that it's also not just a string. + t.not(typeof version, "string"); + + // Should be an object with the expected properties. + t.is(typeof version, "object"); + t.is(version["major"], 27); + t.is(version["minor"], 0); + t.is(version["patch"], 1); + } +}); diff --git a/src/platform.ts b/src/platform.ts index 1cc085d6ab..59785d3124 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,3 +1,7 @@ +import * as semver from "semver"; + +import type { ActionState } from "./action-common"; + /** Platform identifiers used in CodeQL bundle asset names. */ export enum BundlePlatform { Linux64 = "linux64", @@ -24,3 +28,28 @@ export function getBundlePlatform( return undefined; } } + +/** + * Tries to determine the version of macOS. + * + * @returns + * The version as either a semantic version object, the raw version string + * if it is not a semantic version, or `undefined` if we are not on macOS. + */ +export function macOSVersion( + action: ActionState<["Base"]>, +): semver.SemVer | string | undefined { + // Skip if we are not running on macOS. + if (action.platform !== "darwin") { + return undefined; + } + + // Try to parse the OS version string. + const version = semver.parse(action.osRelease); + + if (version === null) { + return action.osRelease; + } + + return version; +} From 8ac4f4481b01aabd3781c90aca0ae359c7f25801 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 11:01:20 +0100 Subject: [PATCH 04/12] Refactor swift check out of `init-action.ts` --- lib/entry-points.js | 178 +++++++++++++++++++----------------- src/init-action.ts | 10 +- src/languages/swift.test.ts | 43 +++++++++ src/languages/swift.ts | 28 ++++++ 4 files changed, 165 insertions(+), 94 deletions(-) create mode 100644 src/languages/swift.test.ts create mode 100644 src/languages/swift.ts diff --git a/lib/entry-points.js b/lib/entry-points.js index 7376558e1d..d1fb87199b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -26953,7 +26953,7 @@ var require_parse2 = __commonJS({ "node_modules/semver/functions/parse.js"(exports2, module2) { "use strict"; var SemVer = require_semver(); - var parse3 = (version, options, throwErrors = false) => { + var parse4 = (version, options, throwErrors = false) => { if (version instanceof SemVer) { return version; } @@ -26966,7 +26966,7 @@ var require_parse2 = __commonJS({ throw er; } }; - module2.exports = parse3; + module2.exports = parse4; } }); @@ -26974,9 +26974,9 @@ var require_parse2 = __commonJS({ var require_valid = __commonJS({ "node_modules/semver/functions/valid.js"(exports2, module2) { "use strict"; - var parse3 = require_parse2(); + var parse4 = require_parse2(); var valid4 = (version, options) => { - const v = parse3(version, options); + const v = parse4(version, options); return v ? v.version : null; }; module2.exports = valid4; @@ -26987,9 +26987,9 @@ var require_valid = __commonJS({ var require_clean = __commonJS({ "node_modules/semver/functions/clean.js"(exports2, module2) { "use strict"; - var parse3 = require_parse2(); + var parse4 = require_parse2(); var clean3 = (version, options) => { - const s = parse3(version.trim().replace(/^[=v]+/, ""), options); + const s = parse4(version.trim().replace(/^[=v]+/, ""), options); return s ? s.version : null; }; module2.exports = clean3; @@ -27024,10 +27024,10 @@ var require_inc = __commonJS({ var require_diff = __commonJS({ "node_modules/semver/functions/diff.js"(exports2, module2) { "use strict"; - var parse3 = require_parse2(); + var parse4 = require_parse2(); var diff = (version1, version2) => { - const v1 = parse3(version1, null, true); - const v2 = parse3(version2, null, true); + const v1 = parse4(version1, null, true); + const v2 = parse4(version2, null, true); const comparison = v1.compare(v2); if (comparison === 0) { return null; @@ -27098,9 +27098,9 @@ var require_patch = __commonJS({ var require_prerelease = __commonJS({ "node_modules/semver/functions/prerelease.js"(exports2, module2) { "use strict"; - var parse3 = require_parse2(); + var parse4 = require_parse2(); var prerelease = (version, options) => { - const parsed = parse3(version, options); + const parsed = parse4(version, options); return parsed && parsed.prerelease.length ? parsed.prerelease : null; }; module2.exports = prerelease; @@ -27286,7 +27286,7 @@ var require_coerce = __commonJS({ "node_modules/semver/functions/coerce.js"(exports2, module2) { "use strict"; var SemVer = require_semver(); - var parse3 = require_parse2(); + var parse4 = require_parse2(); var { safeRe: re, t } = require_re(); var coerce3 = (version, options) => { if (version instanceof SemVer) { @@ -27321,7 +27321,7 @@ var require_coerce = __commonJS({ const patch = match2[4] || "0"; const prerelease = options.includePrerelease && match2[5] ? `-${match2[5]}` : ""; const build2 = options.includePrerelease && match2[6] ? `+${match2[6]}` : ""; - return parse3(`${major}.${minor}.${patch}${prerelease}${build2}`, options); + return parse4(`${major}.${minor}.${patch}${prerelease}${build2}`, options); }; module2.exports = coerce3; } @@ -27331,7 +27331,7 @@ var require_coerce = __commonJS({ var require_truncate = __commonJS({ "node_modules/semver/functions/truncate.js"(exports2, module2) { "use strict"; - var parse3 = require_parse2(); + var parse4 = require_parse2(); var constants = require_constants6(); var SemVer = require_semver(); var truncate = (version, truncation, options) => { @@ -27343,7 +27343,7 @@ var require_truncate = __commonJS({ }; var cloneInputVersion = (version, options) => { const versionStringToParse = version instanceof SemVer ? version.version : version; - return parse3(versionStringToParse, options); + return parse4(versionStringToParse, options); }; var doTruncation = (version, truncation) => { if (isPrerelease(truncation)) { @@ -28387,7 +28387,7 @@ var require_semver2 = __commonJS({ var constants = require_constants6(); var SemVer = require_semver(); var identifiers = require_identifiers(); - var parse3 = require_parse2(); + var parse4 = require_parse2(); var valid4 = require_valid(); var clean3 = require_clean(); var inc = require_inc(); @@ -28426,7 +28426,7 @@ var require_semver2 = __commonJS({ var simplifyRange = require_simplify(); var subset = require_subset(); module2.exports = { - parse: parse3, + parse: parse4, valid: valid4, clean: clean3, inc, @@ -32109,9 +32109,9 @@ var require_minimatch = __commonJS({ throw new TypeError("pattern is too long"); } }; - Minimatch2.prototype.parse = parse3; + Minimatch2.prototype.parse = parse4; var SUBPARSE = {}; - function parse3(pattern, isSub) { + function parse4(pattern, isSub) { assertValidPattern2(pattern); var options = this.options; if (pattern === "**") { @@ -33561,8 +33561,8 @@ var require_semver3 = __commonJS({ } } var i; - exports2.parse = parse3; - function parse3(version, options) { + exports2.parse = parse4; + function parse4(version, options) { if (!options || typeof options !== "object") { options = { loose: !!options, @@ -33590,12 +33590,12 @@ var require_semver3 = __commonJS({ } exports2.valid = valid4; function valid4(version, options) { - var v = parse3(version, options); + var v = parse4(version, options); return v ? v.version : null; } exports2.clean = clean3; function clean3(version, options) { - var s = parse3(version.trim().replace(/^[=v]+/, ""), options); + var s = parse4(version.trim().replace(/^[=v]+/, ""), options); return s ? s.version : null; } exports2.SemVer = SemVer; @@ -33831,8 +33831,8 @@ var require_semver3 = __commonJS({ if (eq(version1, version2)) { return null; } else { - var v1 = parse3(version1); - var v2 = parse3(version2); + var v1 = parse4(version1); + var v2 = parse4(version2); var prefix = ""; if (v1.prerelease.length || v2.prerelease.length) { prefix = "pre"; @@ -34538,7 +34538,7 @@ var require_semver3 = __commonJS({ } exports2.prerelease = prerelease; function prerelease(version, options) { - var parsed = parse3(version, options); + var parsed = parse4(version, options); return parsed && parsed.prerelease.length ? parsed.prerelease : null; } exports2.intersects = intersects; @@ -34575,7 +34575,7 @@ var require_semver3 = __commonJS({ if (match2 === null) { return null; } - return parse3(match2[2] + "." + (match2[3] || "0") + "." + (match2[4] || "0"), options); + return parse4(match2[2] + "." + (match2[3] || "0") + "." + (match2[4] || "0"), options); } } }); @@ -34719,7 +34719,7 @@ var require_cacheUtils = __commonJS({ var crypto3 = __importStar2(require("crypto")); var fs33 = __importStar2(require("fs")); var path30 = __importStar2(require("path")); - var semver12 = __importStar2(require_semver3()); + var semver13 = __importStar2(require_semver3()); var util4 = __importStar2(require("util")); var constants_1 = require_constants7(); var versionSalt = "1.0"; @@ -34812,7 +34812,7 @@ var require_cacheUtils = __commonJS({ function getCompressionMethod() { return __awaiter2(this, void 0, void 0, function* () { const versionOutput = yield getVersion("zstd", ["--quiet"]); - const version = semver12.clean(versionOutput); + const version = semver13.clean(versionOutput); core29.debug(`zstd version: ${version}`); if (versionOutput === "") { return constants_1.CompressionMethod.Gzip; @@ -37490,7 +37490,7 @@ var require_ms = __commonJS({ options = options || {}; var type = typeof val; if (type === "string" && val.length > 0) { - return parse3(val); + return parse4(val); } else if (type === "number" && isFinite(val)) { return options.long ? fmtLong(val) : fmtShort(val); } @@ -37498,7 +37498,7 @@ var require_ms = __commonJS({ "val is not a non-empty string or a valid number. val=" + JSON.stringify(val) ); }; - function parse3(str) { + function parse4(str) { str = String(str); if (str.length > 100) { return; @@ -42967,11 +42967,11 @@ var require_tracingPolicy = __commonJS({ var import_log = require_log3(); var import_core_util = require_commonjs4(); var import_restError = require_restError3(); - var import_util58 = require_internal3(); + var import_util59 = require_internal3(); var tracingPolicyName2 = "tracingPolicy"; function tracingPolicy2(options = {}) { const userAgentPromise = (0, import_userAgent.getUserAgentValue)(options.userAgentPrefix); - const sanitizer = new import_util58.Sanitizer({ + const sanitizer = new import_util59.Sanitizer({ additionalAllowedQueryParameters: options.additionalAllowedQueryParameters }); const tracingClient = tryCreateTracingClient(); @@ -45399,7 +45399,7 @@ var require_deserializationPolicy = __commonJS({ return result; } async function deserializeResponseBody(jsonContentTypes, xmlContentTypes, response, options, parseXML) { - const parsedResponse = await parse3(jsonContentTypes, xmlContentTypes, response, options, parseXML); + const parsedResponse = await parse4(jsonContentTypes, xmlContentTypes, response, options, parseXML); if (!shouldDeserializeResponse(parsedResponse)) { return parsedResponse; } @@ -45500,7 +45500,7 @@ var require_deserializationPolicy = __commonJS({ } return { error: error3, shouldReturnResponse: false }; } - async function parse3(jsonContentTypes, xmlContentTypes, operationResponse, opts, parseXML) { + async function parse4(jsonContentTypes, xmlContentTypes, operationResponse, opts, parseXML) { if (!operationResponse.request.streamResponseStatusCodes?.has(operationResponse.status) && operationResponse.bodyAsText) { const text = operationResponse.bodyAsText; const contentType = operationResponse.headers.get("Content-Type") || ""; @@ -46597,11 +46597,11 @@ var require_response2 = __commonJS({ }); module2.exports = __toCommonJS2(response_exports); var import_core_rest_pipeline = require_commonjs6(); - var import_util58 = require_util9(); + var import_util59 = require_util9(); var originalResponse = /* @__PURE__ */ Symbol("Original FullOperationResponse"); function toCompatResponse2(response, options) { - let request3 = (0, import_util58.toWebResourceLike)(response.request); - let headers = (0, import_util58.toHttpHeadersLike)(response.headers); + let request3 = (0, import_util59.toWebResourceLike)(response.request); + let headers = (0, import_util59.toHttpHeadersLike)(response.headers); if (options?.createProxy) { return new Proxy(response, { get(target, prop, receiver) { @@ -46642,7 +46642,7 @@ var require_response2 = __commonJS({ return { ...compatResponse, headers, - request: (0, import_util58.toPipelineRequest)(compatResponse.request) + request: (0, import_util59.toPipelineRequest)(compatResponse.request) }; } } @@ -46749,7 +46749,7 @@ var require_requestPolicyFactoryPolicy = __commonJS({ requestPolicyFactoryPolicyName: () => requestPolicyFactoryPolicyName2 }); module2.exports = __toCommonJS2(requestPolicyFactoryPolicy_exports); - var import_util58 = require_util9(); + var import_util59 = require_util9(); var import_response = require_response2(); var HttpPipelineLogLevel2 = /* @__PURE__ */ ((HttpPipelineLogLevel22) => { HttpPipelineLogLevel22[HttpPipelineLogLevel22["ERROR"] = 1] = "ERROR"; @@ -46773,14 +46773,14 @@ var require_requestPolicyFactoryPolicy = __commonJS({ async sendRequest(request3, next) { let httpPipeline = { async sendRequest(httpRequest) { - const response2 = await next((0, import_util58.toPipelineRequest)(httpRequest)); + const response2 = await next((0, import_util59.toPipelineRequest)(httpRequest)); return (0, import_response.toCompatResponse)(response2, { createProxy: true }); } }; for (const factory of orderedFactories) { httpPipeline = factory.create(httpPipeline, mockRequestPolicyOptions); } - const webResourceLike = (0, import_util58.toWebResourceLike)(request3, { createProxy: true }); + const webResourceLike = (0, import_util59.toWebResourceLike)(request3, { createProxy: true }); const response = await httpPipeline.sendRequest(webResourceLike); return (0, import_response.toPipelineResponse)(response); } @@ -46815,12 +46815,12 @@ var require_httpClientAdapter = __commonJS({ }); module2.exports = __toCommonJS2(httpClientAdapter_exports); var import_response = require_response2(); - var import_util58 = require_util9(); + var import_util59 = require_util9(); function convertHttpClient2(requestPolicyClient) { return { sendRequest: async (request3) => { const response = await requestPolicyClient.sendRequest( - (0, import_util58.toWebResourceLike)(request3, { createProxy: true }) + (0, import_util59.toWebResourceLike)(request3, { createProxy: true }) ); return (0, import_response.toPipelineResponse)(response); } @@ -46858,7 +46858,7 @@ var require_commonjs9 = __commonJS({ disableKeepAlivePolicyName: () => import_disableKeepAlivePolicy.disableKeepAlivePolicyName, requestPolicyFactoryPolicyName: () => import_requestPolicyFactoryPolicy.requestPolicyFactoryPolicyName, toCompatResponse: () => import_response.toCompatResponse, - toHttpHeadersLike: () => import_util58.toHttpHeadersLike + toHttpHeadersLike: () => import_util59.toHttpHeadersLike }); module2.exports = __toCommonJS2(src_exports); var import_extendedClient = require_extendedClient(); @@ -46866,7 +46866,7 @@ var require_commonjs9 = __commonJS({ var import_requestPolicyFactoryPolicy = require_requestPolicyFactoryPolicy(); var import_disableKeepAlivePolicy = require_disableKeepAlivePolicy(); var import_httpClientAdapter = require_httpClientAdapter(); - var import_util58 = require_util9(); + var import_util59 = require_util9(); } }); @@ -106346,7 +106346,7 @@ var require_manifest = __commonJS({ exports2._findMatch = _findMatch; exports2._getOsVersion = _getOsVersion; exports2._readLinuxVersionFile = _readLinuxVersionFile; - var semver12 = __importStar2(require_semver2()); + var semver13 = __importStar2(require_semver2()); var core_1 = require_core(); var os8 = require("os"); var cp = require("child_process"); @@ -106360,7 +106360,7 @@ var require_manifest = __commonJS({ for (const candidate of candidates) { const version = candidate.version; (0, core_1.debug)(`check ${version} satisfies ${versionSpec}`); - if (semver12.satisfies(version, versionSpec) && (!stable || candidate.stable === stable)) { + if (semver13.satisfies(version, versionSpec) && (!stable || candidate.stable === stable)) { file = candidate.files.find((item) => { (0, core_1.debug)(`${item.arch}===${archFilter} && ${item.platform}===${platFilter}`); let chk = item.arch === archFilter && item.platform === platFilter; @@ -106369,7 +106369,7 @@ var require_manifest = __commonJS({ if (osVersion === item.platform_version) { chk = true; } else { - chk = semver12.satisfies(osVersion, item.platform_version); + chk = semver13.satisfies(osVersion, item.platform_version); } } return chk; @@ -106629,7 +106629,7 @@ var require_tool_cache = __commonJS({ var os8 = __importStar2(require("os")); var path30 = __importStar2(require("path")); var httpm = __importStar2(require_lib()); - var semver12 = __importStar2(require_semver2()); + var semver13 = __importStar2(require_semver2()); var stream2 = __importStar2(require("stream")); var util4 = __importStar2(require("util")); var assert_1 = require("assert"); @@ -106902,7 +106902,7 @@ var require_tool_cache = __commonJS({ } function cacheDir2(sourceDir, tool, version, arch2) { return __awaiter2(this, void 0, void 0, function* () { - version = semver12.clean(version) || version; + version = semver13.clean(version) || version; arch2 = arch2 || os8.arch(); core29.debug(`Caching tool ${tool} ${version} ${arch2}`); core29.debug(`source dir: ${sourceDir}`); @@ -106920,7 +106920,7 @@ var require_tool_cache = __commonJS({ } function cacheFile(sourceFile, targetFile, tool, version, arch2) { return __awaiter2(this, void 0, void 0, function* () { - version = semver12.clean(version) || version; + version = semver13.clean(version) || version; arch2 = arch2 || os8.arch(); core29.debug(`Caching tool ${tool} ${version} ${arch2}`); core29.debug(`source file: ${sourceFile}`); @@ -106950,7 +106950,7 @@ var require_tool_cache = __commonJS({ } let toolPath = ""; if (versionSpec) { - versionSpec = semver12.clean(versionSpec) || ""; + versionSpec = semver13.clean(versionSpec) || ""; const cachePath = path30.join(_getCacheDirectory(), toolName, versionSpec, arch2); core29.debug(`checking cache: ${cachePath}`); if (fs33.existsSync(cachePath) && fs33.existsSync(`${cachePath}.complete`)) { @@ -107030,7 +107030,7 @@ var require_tool_cache = __commonJS({ } function _createToolPath(tool, version, arch2) { return __awaiter2(this, void 0, void 0, function* () { - const folderPath = path30.join(_getCacheDirectory(), tool, semver12.clean(version) || version, arch2 || ""); + const folderPath = path30.join(_getCacheDirectory(), tool, semver13.clean(version) || version, arch2 || ""); core29.debug(`destination ${folderPath}`); const markerPath = `${folderPath}.complete`; yield io9.rmRF(folderPath); @@ -107040,15 +107040,15 @@ var require_tool_cache = __commonJS({ }); } function _completeToolPath(tool, version, arch2) { - const folderPath = path30.join(_getCacheDirectory(), tool, semver12.clean(version) || version, arch2 || ""); + const folderPath = path30.join(_getCacheDirectory(), tool, semver13.clean(version) || version, arch2 || ""); const markerPath = `${folderPath}.complete`; fs33.writeFileSync(markerPath, ""); core29.debug("finished caching tool"); } function isExplicitVersion(versionSpec) { - const c = semver12.clean(versionSpec) || ""; + const c = semver13.clean(versionSpec) || ""; core29.debug(`isExplicit: ${c}`); - const valid4 = semver12.valid(c) != null; + const valid4 = semver13.valid(c) != null; core29.debug(`explicit? ${valid4}`); return valid4; } @@ -107056,14 +107056,14 @@ var require_tool_cache = __commonJS({ let version = ""; core29.debug(`evaluating ${versions.length} versions`); versions = versions.sort((a, b) => { - if (semver12.gt(a, b)) { + if (semver13.gt(a, b)) { return 1; } return -1; }); for (let i = versions.length - 1; i >= 0; i--) { const potential = versions[i]; - const satisfied = semver12.satisfies(potential, versionSpec); + const satisfied = semver13.satisfies(potential, versionSpec); if (satisfied) { version = potential; break; @@ -136593,7 +136593,7 @@ var require_dist_node2 = __commonJS({ return template.replace(/\/$/, ""); } } - function parse3(options) { + function parse4(options) { let method = options.method.toUpperCase(); let url2 = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}"); let headers = Object.assign({}, options.headers); @@ -136657,7 +136657,7 @@ var require_dist_node2 = __commonJS({ ); } function endpointWithDefaults2(defaults3, route, options) { - return parse3(merge2(defaults3, route, options)); + return parse4(merge2(defaults3, route, options)); } function withDefaults4(oldDefaults, newDefaults) { const DEFAULTS22 = merge2(oldDefaults, newDefaults); @@ -136666,7 +136666,7 @@ var require_dist_node2 = __commonJS({ DEFAULTS: DEFAULTS22, defaults: withDefaults4.bind(null, DEFAULTS22), merge: merge2.bind(null, DEFAULTS22), - parse: parse3 + parse: parse4 }); } var endpoint2 = withDefaults4(null, DEFAULTS2); @@ -140898,7 +140898,7 @@ var require_binary = __commonJS({ }); return stream2; }; - exports2.parse = function parse3(buffer) { + exports2.parse = function parse4(buffer) { var self2 = words(function(bytes, cb) { return function(name) { if (offset + bytes <= buffer.length) { @@ -175269,7 +175269,7 @@ var path13 = __toESM(require("path")); var import_perf_hooks4 = require("perf_hooks"); var toolcache3 = __toESM(require_tool_cache()); var import_fast_deep_equal = __toESM(require_fast_deep_equal()); -var semver10 = __toESM(require_semver2()); +var semver11 = __toESM(require_semver2()); // src/codeql-bundle.ts function getCodeQLBundleName(compressionMethod, platform2, language) { @@ -175598,9 +175598,10 @@ async function getCodeQlVersionsForOverlayBaseDatabases(rawLanguages, logger2) { } // src/per-language-bundles.ts -var semver7 = __toESM(require_semver2()); +var semver8 = __toESM(require_semver2()); // src/platform.ts +var semver7 = __toESM(require_semver2()); function getBundlePlatform(platform2 = process.platform, arch2 = process.arch) { switch (platform2) { case "win32": @@ -175705,7 +175706,7 @@ async function getPerLanguageBundleLanguage({ if (cliVersion2 === void 0) { return explain("the requested CLI version is unknown"); } - if (!semver7.gte(cliVersion2, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION)) { + if (!semver8.gte(cliVersion2, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION)) { return explain( `the requested CodeQL version ${cliVersion2} is older than ${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}, which is the first version for which per-language bundles are published` ); @@ -175731,7 +175732,7 @@ var stream = __toESM(require("stream")); var import_toolrunner = __toESM(require_toolrunner()); var io4 = __toESM(require_io()); var toolcache = __toESM(require_tool_cache()); -var semver8 = __toESM(require_semver2()); +var semver9 = __toESM(require_semver2()); var MIN_REQUIRED_BSD_TAR_VERSION = "3.4.3"; var MIN_REQUIRED_GNU_TAR_VERSION = "1.31"; async function getTarVersion() { @@ -175773,9 +175774,9 @@ async function isZstdAvailable(logger2) { case "gnu": return { available: foundZstdBinary && // GNU tar only uses major and minor version numbers - semver8.gte( - semver8.coerce(version), - semver8.coerce(MIN_REQUIRED_GNU_TAR_VERSION) + semver9.gte( + semver9.coerce(version), + semver9.coerce(MIN_REQUIRED_GNU_TAR_VERSION) ), foundZstdBinary, version: tarVersion @@ -175784,7 +175785,7 @@ async function isZstdAvailable(logger2) { return { available: foundZstdBinary && // Do a loose comparison since these version numbers don't contain // a patch version number. - semver8.gte(version, MIN_REQUIRED_BSD_TAR_VERSION), + semver9.gte(version, MIN_REQUIRED_BSD_TAR_VERSION), foundZstdBinary, version: tarVersion }; @@ -175893,7 +175894,7 @@ var core11 = __toESM(require_core()); var import_http_client = __toESM(require_lib()); var toolcache2 = __toESM(require_tool_cache()); var import_follow_redirects = __toESM(require_follow_redirects()); -var semver9 = __toESM(require_semver2()); +var semver10 = __toESM(require_semver2()); var STREAMING_HIGH_WATERMARK_BYTES = 4 * 1024 * 1024; var STREAMING_STALL_TIMEOUT_MS = 5 * 60 * 1e3; var TOOLCACHE_TOOL_NAME = "CodeQL"; @@ -176019,7 +176020,7 @@ function getToolcacheToolDirectory(env) { ); } function getToolcacheVersionDirectoryName(version) { - return semver9.clean(version) || version; + return semver10.clean(version) || version; } function getToolcacheDirectory(version) { return path12.join( @@ -176199,13 +176200,13 @@ function tryGetTagNameFromUrl(url2, logger2) { return match2[1]; } function convertToSemVer(version, logger2) { - if (!semver10.valid(version)) { + if (!semver11.valid(version)) { logger2.debug( `Bundle version ${version} is not in SemVer format. Will treat it as pre-release 0.0.0-${version}.` ); version = `0.0.0-${version}`; } - const s = semver10.clean(version); + const s = semver11.clean(version); if (!s) { throw new Error(`Bundle version ${version} is not in SemVer format.`); } @@ -176421,7 +176422,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, othe url2 = toolsInput; if (tagName) { const bundleVersion3 = tryGetBundleVersionFromTagName(tagName, logger2); - if (bundleVersion3 !== void 0 && semver10.valid(bundleVersion3)) { + if (bundleVersion3 !== void 0 && semver11.valid(bundleVersion3)) { cliVersion2 = convertToSemVer(bundleVersion3, logger2); } } @@ -176803,7 +176804,7 @@ async function downloadCodeQLBundle(action, source, apiDetails, tarVersion, temp async function useZstdBundle(cliVersion2, tarSupportsZstd) { return ( // In testing, gzip performs better than zstd on Windows. - process.platform !== "win32" && tarSupportsZstd && semver10.gte(cliVersion2, CODEQL_VERSION_ZSTD_BUNDLE) + process.platform !== "win32" && tarSupportsZstd && semver11.gte(cliVersion2, CODEQL_VERSION_ZSTD_BUNDLE) ); } function getTempExtractionDir(tempDir) { @@ -176857,7 +176858,7 @@ async function getLatestNightlyBundle(action, rawLanguages, otherLanguagePacksRe } } function getLatestToolcacheVersion(logger2) { - const allVersions = toolcache3.findAllVersions("CodeQL").sort((a, b) => semver10.compare(b, a)); + const allVersions = toolcache3.findAllVersions("CodeQL").sort((a, b) => semver11.compare(b, a)); logger2.debug( `Found the following versions of the CodeQL tools in the toolcache: ${JSON.stringify( allVersions @@ -185980,7 +185981,7 @@ var fs30 = __toESM(require("fs")); var path25 = __toESM(require("path")); var core19 = __toESM(require_core()); var io7 = __toESM(require_io()); -var semver11 = __toESM(require_semver2()); +var semver12 = __toESM(require_semver2()); // src/config/inputs.ts async function getToolsInput(action, repositoryProperties) { @@ -186016,6 +186017,15 @@ async function getToolsInput(action, repositoryProperties) { return void 0; } +// src/languages/swift.ts +function isSwiftCompatible(action, config) { + if (config.languages.includes("swift" /* swift */) && action.platform !== "darwin") { + throw new ConfigurationError( + `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.` + ); + } +} + // src/status-report/tools-download.ts function createInitToolsDownloadFields(report, toolsFeatureFlagsValid) { const fields = { ...report?.perLanguage }; @@ -186484,12 +186494,12 @@ async function run3(actionState) { const experimental = "2.19.3"; const publicPreview = "2.22.1"; const actualVer = (await codeql.getVersion()).version; - if (semver11.lt(actualVer, experimental)) { + if (semver12.lt(actualVer, experimental)) { throw new ConfigurationError( `Rust analysis is supported by CodeQL CLI version ${experimental} or higher, but found version ${actualVer}` ); } - if (semver11.lt(actualVer, publicPreview)) { + if (semver12.lt(actualVer, publicPreview)) { actionState.env.export("CODEQL_ENABLE_EXPERIMENTAL_FEATURES" /* EXPERIMENTAL_FEATURES */, "true"); logger2.info("Experimental Rust analysis enabled"); } @@ -186534,11 +186544,7 @@ async function run3(actionState) { enableFileCoverageInformation: fileCoverageResult.enabled, logger: logger2 }); - if (config.languages.includes("swift" /* swift */) && process.platform !== "darwin") { - throw new ConfigurationError( - `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.` - ); - } + isSwiftCompatible(actionState, config); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( config, diff --git a/src/init-action.ts b/src/init-action.ts index 3ede802503..322b99003b 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -56,6 +56,7 @@ import { runDatabaseInitCluster, } from "./init"; import { JavaEnvVars, BuiltInLanguage } from "./languages"; +import { isSwiftCompatible } from "./languages/swift"; import { Logger, withGroupAsync } from "./logging"; import { downloadOverlayBaseDatabaseFromCache, @@ -417,14 +418,7 @@ async function run( logger, }); - if ( - config.languages.includes(BuiltInLanguage.swift) && - process.platform !== "darwin" - ) { - throw new ConfigurationError( - `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`, - ); - } + isSwiftCompatible(actionState, config); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts new file mode 100644 index 0000000000..b0ddc89a32 --- /dev/null +++ b/src/languages/swift.test.ts @@ -0,0 +1,43 @@ +import test from "ava"; + +import { createTestConfig, initAllState } from "../testing-utils"; + +import { isSwiftCompatible } from "./swift"; + +import { BuiltInLanguage } from "."; + +test("isSwiftCompatible doesn't throw for non-Swift languages", (t) => { + for (const language of Object.values(BuiltInLanguage)) { + if (language === BuiltInLanguage.swift) { + continue; + } + + t.notThrows(() => + isSwiftCompatible( + initAllState(), + createTestConfig({ languages: [language] }), + ), + ); + } +}); + +test("isSwiftCompatible doesn't throw for Swift on darwin", (t) => { + t.notThrows(() => { + isSwiftCompatible( + initAllState({ platform: "darwin" }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + ); + }); +}); + +const nonDarwinPlatforms: NodeJS.Platform[] = ["linux", "win32"]; +for (const nonDarwinPlatform of nonDarwinPlatforms) { + test(`isSwiftCompatible throws for Swift on ${nonDarwinPlatform}`, (t) => { + t.throws(() => { + isSwiftCompatible( + initAllState({ platform: nonDarwinPlatform }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + ); + }); + }); +} diff --git a/src/languages/swift.ts b/src/languages/swift.ts new file mode 100644 index 0000000000..501e7a0274 --- /dev/null +++ b/src/languages/swift.ts @@ -0,0 +1,28 @@ +import { ActionState } from "../action-common"; +import { Config } from "../config-utils"; +import { ConfigurationError } from "../util"; + +import { BuiltInLanguage } from "."; + +/** + * Determines whether we can run a Swift analysis on the current runner. + * + * @param action The Action state. + * @param config The Action configuration. + * + * @throws {ConfigurationError} If Swift analysis is not possible on the current runner. + * @returns True if we can run a Swift analysis. + */ +export function isSwiftCompatible( + action: ActionState<["Base"]>, + config: Config, +) { + if ( + config.languages.includes(BuiltInLanguage.swift) && + action.platform !== "darwin" + ) { + throw new ConfigurationError( + `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`, + ); + } +} From d6fd306480ddc6d0ea1b7b1ed6331882b2ef4e8f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 11:35:54 +0100 Subject: [PATCH 05/12] Use `macOSVersion` in `isSwiftCompatible` --- lib/entry-points.js | 22 +++++++++++++++++++++- src/languages/swift.test.ts | 18 +++++++++++++++++- src/languages/swift.ts | 24 +++++++++++++++++++----- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index d1fb87199b..767a6a7085 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -175614,6 +175614,16 @@ function getBundlePlatform(platform2 = process.platform, arch2 = process.arch) { return void 0; } } +function macOSVersion(action) { + if (action.platform !== "darwin") { + return void 0; + } + const version = semver7.parse(action.osRelease); + if (version === null) { + return action.osRelease; + } + return version; +} // src/per-language-bundles.ts var MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION = "2.27.1"; @@ -186019,11 +186029,21 @@ async function getToolsInput(action, repositoryProperties) { // src/languages/swift.ts function isSwiftCompatible(action, config) { - if (config.languages.includes("swift" /* swift */) && action.platform !== "darwin") { + if (!config.languages.includes("swift" /* swift */)) { + return; + } + const version = macOSVersion(action); + if (version === void 0) { throw new ConfigurationError( `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.` ); } + if (typeof version === "string") { + action.logger.warning( + `Unable to determine version of macOS, got: ${version}` + ); + return; + } } // src/status-report/tools-download.ts diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index b0ddc89a32..ecbaae76fa 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -1,6 +1,11 @@ import test from "ava"; -import { createTestConfig, initAllState } from "../testing-utils"; +import { + checkExpectedLogMessages, + createTestConfig, + initAllState, + RecordingLogger, +} from "../testing-utils"; import { isSwiftCompatible } from "./swift"; @@ -41,3 +46,14 @@ for (const nonDarwinPlatform of nonDarwinPlatforms) { }); }); } + +test("isSwiftCompatible warns if version string is not a semver", (t) => { + const logger = new RecordingLogger(); + isSwiftCompatible( + initAllState({ logger, platform: "darwin", osRelease: "unexpected" }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + ); + checkExpectedLogMessages(t, logger.messages, [ + "Unable to determine version of macOS, got: unexpected", + ]); +}); diff --git a/src/languages/swift.ts b/src/languages/swift.ts index 501e7a0274..fa6517c6df 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -1,5 +1,6 @@ import { ActionState } from "../action-common"; import { Config } from "../config-utils"; +import { macOSVersion } from "../platform"; import { ConfigurationError } from "../util"; import { BuiltInLanguage } from "."; @@ -14,15 +15,28 @@ import { BuiltInLanguage } from "."; * @returns True if we can run a Swift analysis. */ export function isSwiftCompatible( - action: ActionState<["Base"]>, + action: ActionState<["Base", "Logger"]>, config: Config, ) { - if ( - config.languages.includes(BuiltInLanguage.swift) && - action.platform !== "darwin" - ) { + // The checks are not relevant if we are not trying to analyse Swift. + if (!config.languages.includes(BuiltInLanguage.swift)) { + return; + } + + // Try to get the macOS version. + const version = macOSVersion(action); + + // If `version` is undefined, then we are not on macOS. + if (version === undefined) { throw new ConfigurationError( `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`, ); } + // If we got a string, we are on macOS but couldn't parse the version string. + if (typeof version === "string") { + action.logger.warning( + `Unable to determine version of macOS, got: ${version}`, + ); + return; + } } From 6afbf5f5d5cbfdaa8b59d43025d51604b23b3284 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 14:41:26 +0100 Subject: [PATCH 06/12] Add `swiftSupportsAllPlatforms` to `ToolsFeature` --- src/tools-features.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools-features.ts b/src/tools-features.ts index 4931be65ba..2c0124e4ab 100644 --- a/src/tools-features.ts +++ b/src/tools-features.ts @@ -8,6 +8,7 @@ export enum ToolsFeature { BundleSupportsOverlay = "bundleSupportsOverlay", IndirectTracingSupportsStaticBinaries = "indirectTracingSupportsStaticBinaries", SuppressesMissingFileBaselineWarning = "suppressesMissingFileBaselineWarning", + SwiftSupportsAllPlatforms = "swiftSupportsAllPlatforms", } /** From 3a8c4dbb8d9e25d0290f8c7098d006daf8a9771a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 15:25:46 +0100 Subject: [PATCH 07/12] Skip Swift checks if tools feature is enabled --- lib/entry-points.js | 7 +- src/init-action.ts | 2 +- src/languages/swift.test.ts | 123 ++++++++++++++++++++++++++++-------- src/languages/swift.ts | 12 +++- src/testing-utils.ts | 3 +- 5 files changed, 116 insertions(+), 31 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 767a6a7085..a0ebe4c1e4 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -186028,10 +186028,13 @@ async function getToolsInput(action, repositoryProperties) { } // src/languages/swift.ts -function isSwiftCompatible(action, config) { +async function isSwiftCompatible(action, config, codeql) { if (!config.languages.includes("swift" /* swift */)) { return; } + if (await codeql.supportsFeature("swiftSupportsAllPlatforms" /* SwiftSupportsAllPlatforms */)) { + return; + } const version = macOSVersion(action); if (version === void 0) { throw new ConfigurationError( @@ -186564,7 +186567,7 @@ async function run3(actionState) { enableFileCoverageInformation: fileCoverageResult.enabled, logger: logger2 }); - isSwiftCompatible(actionState, config); + await isSwiftCompatible(actionState, config, codeql); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( config, diff --git a/src/init-action.ts b/src/init-action.ts index 322b99003b..973e2f60df 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -418,7 +418,7 @@ async function run( logger, }); - isSwiftCompatible(actionState, config); + await isSwiftCompatible(actionState, config, codeql); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index ecbaae76fa..745f631b5a 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -1,59 +1,130 @@ import test from "ava"; +import * as sinon from "sinon"; +import { getCodeQLForTesting } from "../codeql"; +import { ActionsEnvVars } from "../environment"; import { checkExpectedLogMessages, createTestConfig, + getTestEnv, initAllState, + makeVersionInfo, RecordingLogger, } from "../testing-utils"; +import { ToolsFeature } from "../tools-features"; +import { withTmpDir } from "../util"; import { isSwiftCompatible } from "./swift"; import { BuiltInLanguage } from "."; -test("isSwiftCompatible doesn't throw for non-Swift languages", (t) => { +test("isSwiftCompatible doesn't throw for non-Swift languages", async (t) => { for (const language of Object.values(BuiltInLanguage)) { if (language === BuiltInLanguage.swift) { continue; } - t.notThrows(() => + const codeql = await getCodeQLForTesting(); + await t.notThrowsAsync( isSwiftCompatible( initAllState(), createTestConfig({ languages: [language] }), + codeql, ), ); } }); -test("isSwiftCompatible doesn't throw for Swift on darwin", (t) => { - t.notThrows(() => { - isSwiftCompatible( - initAllState({ platform: "darwin" }), - createTestConfig({ languages: [BuiltInLanguage.swift] }), +test("isSwiftCompatible doesn't throw for Swift if CLI supports swiftSupportsAllPlatforms", async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env); + const supportsFeature = sinon + .stub(codeql, "supportsFeature") + .withArgs(ToolsFeature.SwiftSupportsAllPlatforms) + .resolves(true); + + await t.notThrowsAsync( + isSwiftCompatible( + initAllState({ platform: "darwin", env, logger }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + codeql, + ), ); - }); -}); -const nonDarwinPlatforms: NodeJS.Platform[] = ["linux", "win32"]; -for (const nonDarwinPlatform of nonDarwinPlatforms) { - test(`isSwiftCompatible throws for Swift on ${nonDarwinPlatform}`, (t) => { - t.throws(() => { + t.is(supportsFeature.callCount, 1); + t.deepEqual(supportsFeature.args[0], [ + ToolsFeature.SwiftSupportsAllPlatforms, + ]); + })); + +test("isSwiftCompatible doesn't throw for Swift on darwin", async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + await t.notThrowsAsync( isSwiftCompatible( - initAllState({ platform: nonDarwinPlatform }), + initAllState({ platform: "darwin", env, logger }), createTestConfig({ languages: [BuiltInLanguage.swift] }), + codeql, + ), + ); + })); + +const nonDarwinPlatforms: NodeJS.Platform[] = ["linux", "win32"]; +for (const nonDarwinPlatform of nonDarwinPlatforms) { + test(`isSwiftCompatible throws for Swift on ${nonDarwinPlatform}`, async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const codeql = await getCodeQLForTesting( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + await t.throwsAsync( + isSwiftCompatible( + initAllState({ platform: nonDarwinPlatform, env, logger }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + codeql, + ), ); - }); - }); + })); } -test("isSwiftCompatible warns if version string is not a semver", (t) => { - const logger = new RecordingLogger(); - isSwiftCompatible( - initAllState({ logger, platform: "darwin", osRelease: "unexpected" }), - createTestConfig({ languages: [BuiltInLanguage.swift] }), - ); - checkExpectedLogMessages(t, logger.messages, [ - "Unable to determine version of macOS, got: unexpected", - ]); -}); +test("isSwiftCompatible warns if version string is not a semver", async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + await isSwiftCompatible( + initAllState({ + logger, + platform: "darwin", + osRelease: "unexpected", + env, + }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + codeql, + ); + + checkExpectedLogMessages(t, logger.messages, [ + "Unable to determine version of macOS, got: unexpected", + ]); + })); diff --git a/src/languages/swift.ts b/src/languages/swift.ts index fa6517c6df..3b6d70959a 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -1,6 +1,8 @@ import { ActionState } from "../action-common"; +import { CodeQL } from "../codeql"; import { Config } from "../config-utils"; import { macOSVersion } from "../platform"; +import { ToolsFeature } from "../tools-features"; import { ConfigurationError } from "../util"; import { BuiltInLanguage } from "."; @@ -14,15 +16,23 @@ import { BuiltInLanguage } from "."; * @throws {ConfigurationError} If Swift analysis is not possible on the current runner. * @returns True if we can run a Swift analysis. */ -export function isSwiftCompatible( +export async function isSwiftCompatible( action: ActionState<["Base", "Logger"]>, config: Config, + codeql: CodeQL, ) { // The checks are not relevant if we are not trying to analyse Swift. if (!config.languages.includes(BuiltInLanguage.swift)) { return; } + // Skip the checks if the `swiftSupportsAllPlatforms` feature is supported by the CLI. + // This is a forward-looking measure that allows a future CLI update to disable these + // platform checks in the Action when they shouldn't be enforced anymore. + if (await codeql.supportsFeature(ToolsFeature.SwiftSupportsAllPlatforms)) { + return; + } + // Try to get the macOS version. const version = macOSVersion(action); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 4f1d0625ad..69b5f7f140 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -36,6 +36,7 @@ import { Logger } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { getBundlePlatform } from "./platform"; import { ActionName } from "./status-report"; +import { ToolsFeature } from "./tools-features"; import { DEFAULT_DEBUG_ARTIFACT_NAME, DEFAULT_DEBUG_DATABASE_NAME, @@ -875,7 +876,7 @@ export function mockLanguagesInRepo(languages: string[]) { */ export const makeVersionInfo = ( version: string, - features?: { [name: string]: boolean }, + features?: { [key in ToolsFeature]?: boolean }, overlayVersion?: number, ): VersionInfo => ({ version, From 02a4e818be9f3ebe775c62d0cf2ddd9ac41c8850 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 15:51:31 +0100 Subject: [PATCH 08/12] Check if macOS version is unsupported --- lib/entry-points.js | 31 +++++++++++++++++++ src/languages/swift.test.ts | 61 +++++++++++++++++++++++++++++++++++++ src/languages/swift.ts | 41 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/lib/entry-points.js b/lib/entry-points.js index a0ebe4c1e4..28b24d8dcf 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -186028,6 +186028,8 @@ async function getToolsInput(action, repositoryProperties) { } // src/languages/swift.ts +var SWIFT_TRACED_UNSUPPORTED_MACOS = 27; +var SWIFT_TRACED_UNSUPPORTED_XCODE = 27; async function isSwiftCompatible(action, config, codeql) { if (!config.languages.includes("swift" /* swift */)) { return; @@ -186047,6 +186049,35 @@ async function isSwiftCompatible(action, config, codeql) { ); return; } + if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { + const baseMessage = [ + `Traced Swift analysis is not supported on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, + `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, + `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.` + ].join(" "); + action.logger.warning(baseMessage); + addDiagnostic( + config, + "swift" /* swift */, + makeDiagnostic( + "codeql-action/unsupported-traced-swift-analysis-macos", + "Traced Swift analysis is not supported on this version of macOS", + { + attributes: { + languages: config.languages, + macOSVersion: version.toString() + }, + markdownMessage: baseMessage, + severity: "warning", + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true + } + } + ) + ); + } } // src/status-report/tools-download.ts diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index 745f631b5a..1527756828 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -2,6 +2,7 @@ import test from "ava"; import * as sinon from "sinon"; import { getCodeQLForTesting } from "../codeql"; +import * as diagnostics from "../diagnostics"; import { ActionsEnvVars } from "../environment"; import { checkExpectedLogMessages, @@ -10,6 +11,7 @@ import { initAllState, makeVersionInfo, RecordingLogger, + setupTests, } from "../testing-utils"; import { ToolsFeature } from "../tools-features"; import { withTmpDir } from "../util"; @@ -18,6 +20,8 @@ import { isSwiftCompatible } from "./swift"; import { BuiltInLanguage } from "."; +setupTests(test); + test("isSwiftCompatible doesn't throw for non-Swift languages", async (t) => { for (const language of Object.values(BuiltInLanguage)) { if (language === BuiltInLanguage.swift) { @@ -128,3 +132,60 @@ test("isSwiftCompatible warns if version string is not a semver", async (t) => "Unable to determine version of macOS, got: unexpected", ]); })); + +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "isSwiftCompatible logs and adds diagnostic if macOS version is unsupported", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const codeql = await getCodeQLForTesting( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ languages: [BuiltInLanguage.swift] }); + await isSwiftCompatible( + initAllState({ + logger, + platform: "darwin", + osRelease: "27.0.0", + env, + }), + config, + codeql, + ); + + checkExpectedLogMessages(t, logger.messages, [ + "Traced Swift analysis is not supported on macOS 27", + ]); + + t.is(addDiagnostic.callCount, 1); + t.like(addDiagnostic.args[0], [ + config, + BuiltInLanguage.swift, + { + attributes: { + languages: [BuiltInLanguage.swift], + macOSVersion: "27.0.0", + }, + source: { + id: "codeql-action/unsupported-traced-swift-analysis-macos", + name: "Traced Swift analysis is not supported on this version of macOS", + }, + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + } satisfies Partial, + ]); + }), +); diff --git a/src/languages/swift.ts b/src/languages/swift.ts index 3b6d70959a..21172951b3 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -1,12 +1,19 @@ import { ActionState } from "../action-common"; import { CodeQL } from "../codeql"; import { Config } from "../config-utils"; +import { addDiagnostic, makeDiagnostic } from "../diagnostics"; import { macOSVersion } from "../platform"; import { ToolsFeature } from "../tools-features"; import { ConfigurationError } from "../util"; import { BuiltInLanguage } from "."; +/** macOS 27 and above do not support traced extraction for Swift. */ +export const SWIFT_TRACED_UNSUPPORTED_MACOS = 27; + +/** Xcode 27 and above do not support traced extraction for Swift. */ +export const SWIFT_TRACED_UNSUPPORTED_XCODE = 27; + /** * Determines whether we can run a Swift analysis on the current runner. * @@ -49,4 +56,38 @@ export async function isSwiftCompatible( ); return; } + + if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { + const baseMessage = [ + `Traced Swift analysis is not supported on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, + `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, + `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.`, + ].join(" "); + + // Log the message. This will also show up as a workflow annotation. + action.logger.warning(baseMessage); + + // Create a diagnostic that will show up on the TSP. + addDiagnostic( + config, + BuiltInLanguage.swift, + makeDiagnostic( + "codeql-action/unsupported-traced-swift-analysis-macos", + "Traced Swift analysis is not supported on this version of macOS", + { + attributes: { + languages: config.languages, + macOSVersion: version.toString(), + }, + markdownMessage: baseMessage, + severity: "warning", + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + }, + ), + ); + } } From b458b9f6be0b128444dd79215b05e505f1845511 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 16:01:15 +0100 Subject: [PATCH 09/12] Add FF to fail if macOS version is unsupported --- lib/entry-points.js | 18 +++++++++-- src/feature-flags.ts | 7 ++++ src/init-action.ts | 2 +- src/languages/swift.test.ts | 64 +++++++++++++++++++++++++++++++++++++ src/languages/swift.ts | 21 +++++++++--- 5 files changed, 103 insertions(+), 9 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 28b24d8dcf..663fe98f2b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -172157,6 +172157,11 @@ var featureConfig = { envVar: "CODEQL_ACTION_START_PROXY_USE_FEATURES_RELEASE", minimumVersion: void 0 }, + ["swift_skip_unsupported_traced_analysis" /* SwiftSkipUnsupportedTracedAnalysis */]: { + defaultValue: false, + envVar: "CODEQL_ACTION_SWIFT_SKIP_UNSUPPORTED_TRACED_ANALYSIS", + minimumVersion: void 0 + }, ["tools_repository_property" /* ToolsRepositoryProperty */]: { defaultValue: false, envVar: "CODEQL_ACTION_TOOLS_REPOSITORY_PROPERTY", @@ -186049,13 +186054,15 @@ async function isSwiftCompatible(action, config, codeql) { ); return; } + const skipUnsupportedTracedAnalysis = await action.features.getValue( + "swift_skip_unsupported_traced_analysis" /* SwiftSkipUnsupportedTracedAnalysis */ + ); if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { const baseMessage = [ `Traced Swift analysis is not supported on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.` ].join(" "); - action.logger.warning(baseMessage); addDiagnostic( config, "swift" /* swift */, @@ -186068,7 +186075,7 @@ async function isSwiftCompatible(action, config, codeql) { macOSVersion: version.toString() }, markdownMessage: baseMessage, - severity: "warning", + severity: skipUnsupportedTracedAnalysis ? "error" : "warning", visibility: { cliSummaryTable: true, statusPage: true, @@ -186077,6 +186084,11 @@ async function isSwiftCompatible(action, config, codeql) { } ) ); + if (skipUnsupportedTracedAnalysis) { + throw new ConfigurationError(baseMessage); + } else { + action.logger.warning(baseMessage); + } } } @@ -186598,7 +186610,7 @@ async function run3(actionState) { enableFileCoverageInformation: fileCoverageResult.enabled, logger: logger2 }); - await isSwiftCompatible(actionState, config, codeql); + await isSwiftCompatible(actionStateWithFeatures, config, codeql); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( config, diff --git a/src/feature-flags.ts b/src/feature-flags.ts index 59b50e2dfd..8db78b1ddf 100644 --- a/src/feature-flags.ts +++ b/src/feature-flags.ts @@ -170,6 +170,8 @@ export enum Feature { */ PerLanguageBundles = "per_language_bundles_v2", QaTelemetryEnabled = "qa_telemetry_enabled", + /** Whether we should fail early if we detect that traced Swift analysis is unsupported. */ + SwiftSkipUnsupportedTracedAnalysis = "swift_skip_unsupported_traced_analysis", /** Routes (some) API requests through the registry proxy. */ ProxyApiRequests = "proxy_api_requests", /** Note that this currently only disables baseline file coverage information. */ @@ -466,6 +468,11 @@ export const featureConfig = { envVar: "CODEQL_ACTION_START_PROXY_USE_FEATURES_RELEASE", minimumVersion: undefined, }, + [Feature.SwiftSkipUnsupportedTracedAnalysis]: { + defaultValue: false, + envVar: "CODEQL_ACTION_SWIFT_SKIP_UNSUPPORTED_TRACED_ANALYSIS", + minimumVersion: undefined, + }, [Feature.ToolsRepositoryProperty]: { defaultValue: false, envVar: "CODEQL_ACTION_TOOLS_REPOSITORY_PROPERTY", diff --git a/src/init-action.ts b/src/init-action.ts index 973e2f60df..b0053718a7 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -418,7 +418,7 @@ async function run( logger, }); - await isSwiftCompatible(actionState, config, codeql); + await isSwiftCompatible(actionStateWithFeatures, config, codeql); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index 1527756828..c9e90ad453 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -4,8 +4,10 @@ import * as sinon from "sinon"; import { getCodeQLForTesting } from "../codeql"; import * as diagnostics from "../diagnostics"; import { ActionsEnvVars } from "../environment"; +import { Feature } from "../feature-flags"; import { checkExpectedLogMessages, + createFeatures, createTestConfig, getTestEnv, initAllState, @@ -176,6 +178,68 @@ test.serial( languages: [BuiltInLanguage.swift], macOSVersion: "27.0.0", }, + severity: "warning", + source: { + id: "codeql-action/unsupported-traced-swift-analysis-macos", + name: "Traced Swift analysis is not supported on this version of macOS", + }, + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + } satisfies Partial, + ]); + }), +); + +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "isSwiftCompatible throws if macOS version is unsupported and FF is enabled", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const features = createFeatures([ + Feature.SwiftSkipUnsupportedTracedAnalysis, + ]); + + const codeql = await getCodeQLForTesting( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ languages: [BuiltInLanguage.swift] }); + await t.throwsAsync( + isSwiftCompatible( + initAllState({ + logger, + platform: "darwin", + osRelease: "27.0.0", + env, + features, + }), + config, + codeql, + ), + ); + + t.is(addDiagnostic.callCount, 1); + t.like(addDiagnostic.args[0], [ + config, + BuiltInLanguage.swift, + { + attributes: { + languages: [BuiltInLanguage.swift], + macOSVersion: "27.0.0", + }, + severity: "error", source: { id: "codeql-action/unsupported-traced-swift-analysis-macos", name: "Traced Swift analysis is not supported on this version of macOS", diff --git a/src/languages/swift.ts b/src/languages/swift.ts index 21172951b3..e73e40e597 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -2,6 +2,7 @@ import { ActionState } from "../action-common"; import { CodeQL } from "../codeql"; import { Config } from "../config-utils"; import { addDiagnostic, makeDiagnostic } from "../diagnostics"; +import { Feature } from "../feature-flags"; import { macOSVersion } from "../platform"; import { ToolsFeature } from "../tools-features"; import { ConfigurationError } from "../util"; @@ -24,7 +25,7 @@ export const SWIFT_TRACED_UNSUPPORTED_XCODE = 27; * @returns True if we can run a Swift analysis. */ export async function isSwiftCompatible( - action: ActionState<["Base", "Logger"]>, + action: ActionState<["Base", "Logger", "FeatureFlags"]>, config: Config, codeql: CodeQL, ) { @@ -57,6 +58,9 @@ export async function isSwiftCompatible( return; } + const skipUnsupportedTracedAnalysis = await action.features.getValue( + Feature.SwiftSkipUnsupportedTracedAnalysis, + ); if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { const baseMessage = [ `Traced Swift analysis is not supported on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, @@ -64,9 +68,6 @@ export async function isSwiftCompatible( `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.`, ].join(" "); - // Log the message. This will also show up as a workflow annotation. - action.logger.warning(baseMessage); - // Create a diagnostic that will show up on the TSP. addDiagnostic( config, @@ -80,7 +81,7 @@ export async function isSwiftCompatible( macOSVersion: version.toString(), }, markdownMessage: baseMessage, - severity: "warning", + severity: skipUnsupportedTracedAnalysis ? "error" : "warning", visibility: { cliSummaryTable: true, statusPage: true, @@ -89,5 +90,15 @@ export async function isSwiftCompatible( }, ), ); + + // Throw an error to abort the analysis if the FF is enabled or log the message. + if (skipUnsupportedTracedAnalysis) { + // ConfigurationErrors are converted to the "aborted" status by the exception handler + // in `init-action.ts` that guards the call to `isSwiftCompatible`. + throw new ConfigurationError(baseMessage); + } else { + // This will also show up as a workflow annotation. + action.logger.warning(baseMessage); + } } } From 39546d5de7b267791e6e013fca0c90812c634866 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 17:22:49 +0100 Subject: [PATCH 10/12] Add function to determine Xcode version based on symlink --- lib/entry-points.js | 56 ++++++------- src/action-common.ts | 2 + src/languages/swift.test.ts | 151 +++++++++++++++++++++++++++++++++++- src/languages/swift.ts | 76 +++++++++++++++++- 4 files changed, 256 insertions(+), 29 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 663fe98f2b..0f8bb756dd 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -27288,7 +27288,7 @@ var require_coerce = __commonJS({ var SemVer = require_semver(); var parse4 = require_parse2(); var { safeRe: re, t } = require_re(); - var coerce3 = (version, options) => { + var coerce4 = (version, options) => { if (version instanceof SemVer) { return version; } @@ -27323,7 +27323,7 @@ var require_coerce = __commonJS({ const build2 = options.includePrerelease && match2[6] ? `+${match2[6]}` : ""; return parse4(`${major}.${minor}.${patch}${prerelease}${build2}`, options); }; - module2.exports = coerce3; + module2.exports = coerce4; } }); @@ -28409,7 +28409,7 @@ var require_semver2 = __commonJS({ var gte8 = require_gte(); var lte2 = require_lte(); var cmp = require_cmp(); - var coerce3 = require_coerce(); + var coerce4 = require_coerce(); var truncate = require_truncate(); var Comparator = require_comparator(); var Range2 = require_range(); @@ -28448,7 +28448,7 @@ var require_semver2 = __commonJS({ gte: gte8, lte: lte2, cmp, - coerce: coerce3, + coerce: coerce4, truncate, Comparator, Range: Range2, @@ -34547,8 +34547,8 @@ var require_semver3 = __commonJS({ r2 = new Range2(r2, options); return r1.intersects(r2); } - exports2.coerce = coerce3; - function coerce3(version, options) { + exports2.coerce = coerce4; + function coerce4(version, options) { if (version instanceof SemVer) { return version; } @@ -34719,7 +34719,7 @@ var require_cacheUtils = __commonJS({ var crypto3 = __importStar2(require("crypto")); var fs33 = __importStar2(require("fs")); var path30 = __importStar2(require("path")); - var semver13 = __importStar2(require_semver3()); + var semver14 = __importStar2(require_semver3()); var util4 = __importStar2(require("util")); var constants_1 = require_constants7(); var versionSalt = "1.0"; @@ -34812,7 +34812,7 @@ var require_cacheUtils = __commonJS({ function getCompressionMethod() { return __awaiter2(this, void 0, void 0, function* () { const versionOutput = yield getVersion("zstd", ["--quiet"]); - const version = semver13.clean(versionOutput); + const version = semver14.clean(versionOutput); core29.debug(`zstd version: ${version}`); if (versionOutput === "") { return constants_1.CompressionMethod.Gzip; @@ -37599,7 +37599,7 @@ var require_common = __commonJS({ function setup(env) { createDebug.debug = createDebug; createDebug.default = createDebug; - createDebug.coerce = coerce3; + createDebug.coerce = coerce4; createDebug.disable = disable; createDebug.enable = enable; createDebug.enabled = enabled; @@ -37754,7 +37754,7 @@ var require_common = __commonJS({ } return false; } - function coerce3(val) { + function coerce4(val) { if (val instanceof Error) { return val.stack || val.message; } @@ -106346,7 +106346,7 @@ var require_manifest = __commonJS({ exports2._findMatch = _findMatch; exports2._getOsVersion = _getOsVersion; exports2._readLinuxVersionFile = _readLinuxVersionFile; - var semver13 = __importStar2(require_semver2()); + var semver14 = __importStar2(require_semver2()); var core_1 = require_core(); var os8 = require("os"); var cp = require("child_process"); @@ -106360,7 +106360,7 @@ var require_manifest = __commonJS({ for (const candidate of candidates) { const version = candidate.version; (0, core_1.debug)(`check ${version} satisfies ${versionSpec}`); - if (semver13.satisfies(version, versionSpec) && (!stable || candidate.stable === stable)) { + if (semver14.satisfies(version, versionSpec) && (!stable || candidate.stable === stable)) { file = candidate.files.find((item) => { (0, core_1.debug)(`${item.arch}===${archFilter} && ${item.platform}===${platFilter}`); let chk = item.arch === archFilter && item.platform === platFilter; @@ -106369,7 +106369,7 @@ var require_manifest = __commonJS({ if (osVersion === item.platform_version) { chk = true; } else { - chk = semver13.satisfies(osVersion, item.platform_version); + chk = semver14.satisfies(osVersion, item.platform_version); } } return chk; @@ -106629,7 +106629,7 @@ var require_tool_cache = __commonJS({ var os8 = __importStar2(require("os")); var path30 = __importStar2(require("path")); var httpm = __importStar2(require_lib()); - var semver13 = __importStar2(require_semver2()); + var semver14 = __importStar2(require_semver2()); var stream2 = __importStar2(require("stream")); var util4 = __importStar2(require("util")); var assert_1 = require("assert"); @@ -106902,7 +106902,7 @@ var require_tool_cache = __commonJS({ } function cacheDir2(sourceDir, tool, version, arch2) { return __awaiter2(this, void 0, void 0, function* () { - version = semver13.clean(version) || version; + version = semver14.clean(version) || version; arch2 = arch2 || os8.arch(); core29.debug(`Caching tool ${tool} ${version} ${arch2}`); core29.debug(`source dir: ${sourceDir}`); @@ -106920,7 +106920,7 @@ var require_tool_cache = __commonJS({ } function cacheFile(sourceFile, targetFile, tool, version, arch2) { return __awaiter2(this, void 0, void 0, function* () { - version = semver13.clean(version) || version; + version = semver14.clean(version) || version; arch2 = arch2 || os8.arch(); core29.debug(`Caching tool ${tool} ${version} ${arch2}`); core29.debug(`source file: ${sourceFile}`); @@ -106950,7 +106950,7 @@ var require_tool_cache = __commonJS({ } let toolPath = ""; if (versionSpec) { - versionSpec = semver13.clean(versionSpec) || ""; + versionSpec = semver14.clean(versionSpec) || ""; const cachePath = path30.join(_getCacheDirectory(), toolName, versionSpec, arch2); core29.debug(`checking cache: ${cachePath}`); if (fs33.existsSync(cachePath) && fs33.existsSync(`${cachePath}.complete`)) { @@ -107030,7 +107030,7 @@ var require_tool_cache = __commonJS({ } function _createToolPath(tool, version, arch2) { return __awaiter2(this, void 0, void 0, function* () { - const folderPath = path30.join(_getCacheDirectory(), tool, semver13.clean(version) || version, arch2 || ""); + const folderPath = path30.join(_getCacheDirectory(), tool, semver14.clean(version) || version, arch2 || ""); core29.debug(`destination ${folderPath}`); const markerPath = `${folderPath}.complete`; yield io9.rmRF(folderPath); @@ -107040,15 +107040,15 @@ var require_tool_cache = __commonJS({ }); } function _completeToolPath(tool, version, arch2) { - const folderPath = path30.join(_getCacheDirectory(), tool, semver13.clean(version) || version, arch2 || ""); + const folderPath = path30.join(_getCacheDirectory(), tool, semver14.clean(version) || version, arch2 || ""); const markerPath = `${folderPath}.complete`; fs33.writeFileSync(markerPath, ""); core29.debug("finished caching tool"); } function isExplicitVersion(versionSpec) { - const c = semver13.clean(versionSpec) || ""; + const c = semver14.clean(versionSpec) || ""; core29.debug(`isExplicit: ${c}`); - const valid4 = semver13.valid(c) != null; + const valid4 = semver14.valid(c) != null; core29.debug(`explicit? ${valid4}`); return valid4; } @@ -107056,14 +107056,14 @@ var require_tool_cache = __commonJS({ let version = ""; core29.debug(`evaluating ${versions.length} versions`); versions = versions.sort((a, b) => { - if (semver13.gt(a, b)) { + if (semver14.gt(a, b)) { return 1; } return -1; }); for (let i = versions.length - 1; i >= 0; i--) { const potential = versions[i]; - const satisfied = semver13.satisfies(potential, versionSpec); + const satisfied = semver14.satisfies(potential, versionSpec); if (satisfied) { version = potential; break; @@ -185996,7 +185996,7 @@ var fs30 = __toESM(require("fs")); var path25 = __toESM(require("path")); var core19 = __toESM(require_core()); var io7 = __toESM(require_io()); -var semver12 = __toESM(require_semver2()); +var semver13 = __toESM(require_semver2()); // src/config/inputs.ts async function getToolsInput(action, repositoryProperties) { @@ -186033,6 +186033,10 @@ async function getToolsInput(action, repositoryProperties) { } // src/languages/swift.ts +var semver12 = __toESM(require_semver2()); +var XCODE_APP_FILENAME_PATTERN = new RegExp( + /(?Xcode_(?\d+.\d+).app)/ +); var SWIFT_TRACED_UNSUPPORTED_MACOS = 27; var SWIFT_TRACED_UNSUPPORTED_XCODE = 27; async function isSwiftCompatible(action, config, codeql) { @@ -186560,12 +186564,12 @@ async function run3(actionState) { const experimental = "2.19.3"; const publicPreview = "2.22.1"; const actualVer = (await codeql.getVersion()).version; - if (semver12.lt(actualVer, experimental)) { + if (semver13.lt(actualVer, experimental)) { throw new ConfigurationError( `Rust analysis is supported by CodeQL CLI version ${experimental} or higher, but found version ${actualVer}` ); } - if (semver12.lt(actualVer, publicPreview)) { + if (semver13.lt(actualVer, publicPreview)) { actionState.env.export("CODEQL_ENABLE_EXPERIMENTAL_FEATURES" /* EXPERIMENTAL_FEATURES */, "true"); logger2.info("Experimental Rust analysis enabled"); } diff --git a/src/action-common.ts b/src/action-common.ts index 5163259266..4209163dad 100644 --- a/src/action-common.ts +++ b/src/action-common.ts @@ -17,6 +17,8 @@ import { } from "./status-report"; import { getEnv, getErrorMessage, wrapError } from "./util"; +export type { Logger } from "./logging"; + /** Base state that is available to an Action on startup. */ export interface BaseState { /** The name of the Action. */ diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index c9e90ad453..12364da2df 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -1,3 +1,5 @@ +import * as fs from "fs"; + import test from "ava"; import * as sinon from "sinon"; @@ -5,6 +7,7 @@ import { getCodeQLForTesting } from "../codeql"; import * as diagnostics from "../diagnostics"; import { ActionsEnvVars } from "../environment"; import { Feature } from "../feature-flags"; +import { FileSystem } from "../fs"; import { checkExpectedLogMessages, createFeatures, @@ -18,12 +21,158 @@ import { import { ToolsFeature } from "../tools-features"; import { withTmpDir } from "../util"; -import { isSwiftCompatible } from "./swift"; +import { + isSwiftCompatible, + XCODE_SELECT_LINK_PATH, + xcodeVersion, +} from "./swift"; import { BuiltInLanguage } from "."; setupTests(test); +type RequiredFS = FileSystem<"statSync" | "readlinkSync">; + +test("xcodeVersion returns undefined if symlink doesn't exist", (t) => { + const logger = new RecordingLogger(); + + const stubbedFs: RequiredFS = { + statSync: fs.statSync, + readlinkSync: fs.readlinkSync, + }; + const statSync = sinon + .stub(stubbedFs, "statSync") + .throws(new Error("ENOENT")); + + t.is(xcodeVersion(logger, stubbedFs), undefined); + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkExpectedLogMessages(t, logger.messages, [ + "Unable to determine Xcode version: ENOENT", + ]); +}); + +test("xcodeVersion returns undefined if file is not a symlink", (t) => { + const logger = new RecordingLogger(); + + const stubbedFs: RequiredFS = { + statSync: fs.statSync, + readlinkSync: fs.readlinkSync, + }; + const statSync = sinon + .stub(stubbedFs, "statSync") + .returns({ isSymbolicLink: () => false } as fs.Stats); + + t.is(xcodeVersion(logger, stubbedFs), undefined); + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkExpectedLogMessages(t, logger.messages, [ + "exists, but is not a symbolic link", + ]); +}); + +test("xcodeVersion returns undefined if resolving the symlink returns nothing", (t) => { + const logger = new RecordingLogger(); + + const stubbedFs: RequiredFS = { + statSync: fs.statSync, + readlinkSync: fs.readlinkSync, + }; + const statSync = sinon + .stub(stubbedFs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon.stub(stubbedFs, "readlinkSync").returns(""); + + t.is(xcodeVersion(logger, stubbedFs), undefined); + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkExpectedLogMessages(t, logger.messages, [ + "unexpectedly returned nothing", + ]); +}); + +test("xcodeVersion returns undefined if resolved path doesn't include pattern", (t) => { + const logger = new RecordingLogger(); + + const stubbedFs: RequiredFS = { + statSync: fs.statSync, + readlinkSync: fs.readlinkSync, + }; + const statSync = sinon + .stub(stubbedFs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon + .stub(stubbedFs, "readlinkSync") + .returns("/Applications/Xcode.app/Contents/Developer"); + + t.is(xcodeVersion(logger, stubbedFs), undefined); + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkExpectedLogMessages(t, logger.messages, [ + "does not contain expected pattern", + ]); +}); + +test("xcodeVersion returns undefined if match can't be parsed", (t) => { + const logger = new RecordingLogger(); + + const stubbedFs: RequiredFS = { + statSync: fs.statSync, + readlinkSync: fs.readlinkSync, + }; + const statSync = sinon + .stub(stubbedFs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon + .stub(stubbedFs, "readlinkSync") + .returns("/Applications/Xcode_00.0.app/Contents/Developer"); + + t.is(xcodeVersion(logger, stubbedFs), undefined); + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkExpectedLogMessages(t, logger.messages, [ + "Couldn't parse '00.0' as a semantic version.", + ]); +}); + +test("xcodeVersion returns version from resolved path", (t) => { + const logger = new RecordingLogger(); + + const stubbedFs: RequiredFS = { + statSync: fs.statSync, + readlinkSync: fs.readlinkSync, + }; + const statSync = sinon + .stub(stubbedFs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon + .stub(stubbedFs, "readlinkSync") + .returns("/Applications/Xcode_16.4.app/Contents/Developer"); + + const result = xcodeVersion(logger, stubbedFs); + + if (t.truthy(result)) { + t.is(result.major, 16); + t.is(result.minor, 4); + } + + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); +}); + test("isSwiftCompatible doesn't throw for non-Swift languages", async (t) => { for (const language of Object.values(BuiltInLanguage)) { if (language === BuiltInLanguage.swift) { diff --git a/src/languages/swift.ts b/src/languages/swift.ts index e73e40e597..e6683a3308 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -1,20 +1,92 @@ -import { ActionState } from "../action-common"; +import * as nodefs from "fs"; + +import * as semver from "semver"; + +import { ActionState, Logger } from "../action-common"; import { CodeQL } from "../codeql"; import { Config } from "../config-utils"; import { addDiagnostic, makeDiagnostic } from "../diagnostics"; import { Feature } from "../feature-flags"; +import { FileSystem } from "../fs"; import { macOSVersion } from "../platform"; import { ToolsFeature } from "../tools-features"; -import { ConfigurationError } from "../util"; +import { ConfigurationError, getErrorMessage } from "../util"; import { BuiltInLanguage } from "."; +/** The static path we check for a symbolic link to the (dynamic) Xcode location. */ +export const XCODE_SELECT_LINK_PATH = "/private/var/db/xcode_select_link"; + +/** The pattern we expect to find in the Xcode path. */ +export const XCODE_APP_FILENAME_PATTERN = new RegExp( + /(?Xcode_(?\d+.\d+).app)/, +); + /** macOS 27 and above do not support traced extraction for Swift. */ export const SWIFT_TRACED_UNSUPPORTED_MACOS = 27; /** Xcode 27 and above do not support traced extraction for Swift. */ export const SWIFT_TRACED_UNSUPPORTED_XCODE = 27; +/** + * Tries to determine the version of Xcode that is installed. + * + * @param logger The logger to use. + * @returns The Xcode version or `undefined` if it couldn't be determined. + */ +export function xcodeVersion( + logger: Logger, + fs: FileSystem<"statSync" | "readlinkSync"> = nodefs, +): semver.SemVer | undefined { + try { + // Stat the expected symbolic link to check that it exists and is a symbolic link. + // The `readlinkSync` call below returns an empty string in either case and so + // this check allows us to distinguish between the two cases. + const stats = fs.statSync(XCODE_SELECT_LINK_PATH); + + if (!stats.isSymbolicLink()) { + logger.warning( + `${XCODE_SELECT_LINK_PATH} exists, but is not a symbolic link.`, + ); + return undefined; + } + + // Read what the symbolic link points to. + const xcodePath = fs.readlinkSync(XCODE_SELECT_LINK_PATH); + + if (xcodePath === "") { + logger.warning( + `Resolving ${XCODE_SELECT_LINK_PATH} unexpectedly returned nothing.`, + ); + return undefined; + } + + // Try to extract the version from the path. + const matchResult = xcodePath.match(XCODE_APP_FILENAME_PATTERN); + + if (matchResult?.groups === undefined) { + logger.warning( + `Xcode path '${xcodePath}' does not contain expected pattern.`, + ); + return undefined; + } + + const majorMinor = matchResult.groups["majorMinor"]; + const version = semver.coerce(majorMinor); + + if (version === null) { + logger.warning(`Couldn't parse '${majorMinor}' as a semantic version.`); + return undefined; + } + return version; + } catch (err) { + logger.warning( + `Unable to determine Xcode version: ${getErrorMessage(err)}`, + ); + return undefined; + } +} + /** * Determines whether we can run a Swift analysis on the current runner. * From a4bb2b4672df72bdd35611a8b159380602edbe04 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 17:52:55 +0100 Subject: [PATCH 11/12] Make `FS` available to `isSwiftCompatible` --- src/languages/swift.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/swift.ts b/src/languages/swift.ts index e6683a3308..3964bd700c 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -97,7 +97,7 @@ export function xcodeVersion( * @returns True if we can run a Swift analysis. */ export async function isSwiftCompatible( - action: ActionState<["Base", "Logger", "FeatureFlags"]>, + action: ActionState<["Base", "Logger", "FeatureFlags", "FS"]>, config: Config, codeql: CodeQL, ) { From 8baf1db3949412281d22936d930cb754977beac8 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 18:11:41 +0100 Subject: [PATCH 12/12] Check if Xcode version is unsupported --- lib/entry-points.js | 124 +++++++++++----- src/languages/swift.test.ts | 287 +++++++++++++++++++++++++++++------- src/languages/swift.ts | 132 +++++++++++------ 3 files changed, 413 insertions(+), 130 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 0f8bb756dd..ae68201b62 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -186034,11 +186034,83 @@ async function getToolsInput(action, repositoryProperties) { // src/languages/swift.ts var semver12 = __toESM(require_semver2()); +var XCODE_SELECT_LINK_PATH = "/private/var/db/xcode_select_link"; var XCODE_APP_FILENAME_PATTERN = new RegExp( /(?Xcode_(?\d+.\d+).app)/ ); var SWIFT_TRACED_UNSUPPORTED_MACOS = 27; var SWIFT_TRACED_UNSUPPORTED_XCODE = 27; +function xcodeVersion(logger2, fs33) { + try { + const stats = fs33.statSync(XCODE_SELECT_LINK_PATH); + if (!stats.isSymbolicLink()) { + logger2.warning( + `${XCODE_SELECT_LINK_PATH} exists, but is not a symbolic link.` + ); + return void 0; + } + const xcodePath = fs33.readlinkSync(XCODE_SELECT_LINK_PATH); + if (xcodePath === "") { + logger2.warning( + `Resolving ${XCODE_SELECT_LINK_PATH} unexpectedly returned nothing.` + ); + return void 0; + } + const matchResult = xcodePath.match(XCODE_APP_FILENAME_PATTERN); + if (matchResult?.groups === void 0) { + logger2.warning( + `Xcode path '${xcodePath}' does not contain expected pattern.` + ); + return void 0; + } + const majorMinor = matchResult.groups["majorMinor"]; + const version = semver12.coerce(majorMinor); + if (version === null) { + logger2.warning(`Couldn't parse '${majorMinor}' as a semantic version.`); + return void 0; + } + return version; + } catch (err) { + logger2.warning( + `Unable to determine Xcode version: ${getErrorMessage(err)}` + ); + return void 0; + } +} +function handleUnsupportedVersion(logger2, config, skipUnsupportedTracedAnalysis, product, version) { + const baseMessage = [ + `Traced Swift analysis is not supported on ${product} ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, + `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, + `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.` + ].join(" "); + const attributeName = product === "macOS" ? "macOSVersion" : "xcodeVersion"; + addDiagnostic( + config, + "swift" /* swift */, + makeDiagnostic( + `codeql-action/unsupported-traced-swift-analysis-${product.toLowerCase()}`, + `Traced Swift analysis is not supported on this version of ${product}`, + { + attributes: { + languages: config.languages, + [attributeName]: version.toString() + }, + markdownMessage: baseMessage, + severity: skipUnsupportedTracedAnalysis ? "error" : "warning", + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true + } + } + ) + ); + if (skipUnsupportedTracedAnalysis) { + throw new ConfigurationError(baseMessage); + } else { + logger2.warning(baseMessage); + } +} async function isSwiftCompatible(action, config, codeql) { if (!config.languages.includes("swift" /* swift */)) { return; @@ -186052,47 +186124,31 @@ async function isSwiftCompatible(action, config, codeql) { `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.` ); } + const skipUnsupportedTracedAnalysis = await action.features.getValue( + "swift_skip_unsupported_traced_analysis" /* SwiftSkipUnsupportedTracedAnalysis */ + ); if (typeof version === "string") { action.logger.warning( `Unable to determine version of macOS, got: ${version}` ); - return; + } else if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { + handleUnsupportedVersion( + action.logger, + config, + skipUnsupportedTracedAnalysis, + "macOS", + version + ); } - const skipUnsupportedTracedAnalysis = await action.features.getValue( - "swift_skip_unsupported_traced_analysis" /* SwiftSkipUnsupportedTracedAnalysis */ - ); - if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { - const baseMessage = [ - `Traced Swift analysis is not supported on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, - `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, - `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.` - ].join(" "); - addDiagnostic( + const xcodeVer = xcodeVersion(action.logger, action.fs); + if (xcodeVer !== void 0 && xcodeVer.major >= SWIFT_TRACED_UNSUPPORTED_XCODE) { + handleUnsupportedVersion( + action.logger, config, - "swift" /* swift */, - makeDiagnostic( - "codeql-action/unsupported-traced-swift-analysis-macos", - "Traced Swift analysis is not supported on this version of macOS", - { - attributes: { - languages: config.languages, - macOSVersion: version.toString() - }, - markdownMessage: baseMessage, - severity: skipUnsupportedTracedAnalysis ? "error" : "warning", - visibility: { - cliSummaryTable: true, - statusPage: true, - telemetry: true - } - } - ) + skipUnsupportedTracedAnalysis, + "Xcode", + xcodeVer ); - if (skipUnsupportedTracedAnalysis) { - throw new ConfigurationError(baseMessage); - } else { - action.logger.warning(baseMessage); - } } } diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index 12364da2df..93a3abc3f9 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -10,6 +10,7 @@ import { Feature } from "../feature-flags"; import { FileSystem } from "../fs"; import { checkExpectedLogMessages, + checkUnexpectedLogMessages, createFeatures, createTestConfig, getTestEnv, @@ -33,16 +34,41 @@ setupTests(test); type RequiredFS = FileSystem<"statSync" | "readlinkSync">; -test("xcodeVersion returns undefined if symlink doesn't exist", (t) => { - const logger = new RecordingLogger(); - +/** + * Sets up a suitable mock `FileSystem` for use with `xcodeVersion`. + * + * @param statSyncResult The result of `statSync`. + * @param readlinkSyncResult The result of `readlinkSync`. + * + * @returns The mocked `FileSystem` and stubs. + */ +function mockFs( + statSyncResult: boolean | Error, + readlinkSyncResult: string = "", +) { const stubbedFs: RequiredFS = { statSync: fs.statSync, readlinkSync: fs.readlinkSync, }; - const statSync = sinon - .stub(stubbedFs, "statSync") - .throws(new Error("ENOENT")); + const statSync = sinon.stub(stubbedFs, "statSync"); + + if (typeof statSyncResult === "boolean") { + statSync.returns({ isSymbolicLink: () => statSyncResult } as fs.Stats); + } else { + statSync.throws(new Error("ENOENT")); + } + + const readlinkSync = sinon + .stub(stubbedFs, "readlinkSync") + .returns(readlinkSyncResult); + + return { stubbedFs, statSync, readlinkSync }; +} + +test("xcodeVersion returns undefined if symlink doesn't exist", (t) => { + const logger = new RecordingLogger(); + + const { stubbedFs, statSync } = mockFs(new Error("ENOENT")); t.is(xcodeVersion(logger, stubbedFs), undefined); t.is(statSync.callCount, 1); @@ -56,13 +82,7 @@ test("xcodeVersion returns undefined if symlink doesn't exist", (t) => { test("xcodeVersion returns undefined if file is not a symlink", (t) => { const logger = new RecordingLogger(); - const stubbedFs: RequiredFS = { - statSync: fs.statSync, - readlinkSync: fs.readlinkSync, - }; - const statSync = sinon - .stub(stubbedFs, "statSync") - .returns({ isSymbolicLink: () => false } as fs.Stats); + const { stubbedFs, statSync } = mockFs(false); t.is(xcodeVersion(logger, stubbedFs), undefined); t.is(statSync.callCount, 1); @@ -76,14 +96,7 @@ test("xcodeVersion returns undefined if file is not a symlink", (t) => { test("xcodeVersion returns undefined if resolving the symlink returns nothing", (t) => { const logger = new RecordingLogger(); - const stubbedFs: RequiredFS = { - statSync: fs.statSync, - readlinkSync: fs.readlinkSync, - }; - const statSync = sinon - .stub(stubbedFs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon.stub(stubbedFs, "readlinkSync").returns(""); + const { stubbedFs, statSync, readlinkSync } = mockFs(true); t.is(xcodeVersion(logger, stubbedFs), undefined); t.is(statSync.callCount, 1); @@ -99,16 +112,10 @@ test("xcodeVersion returns undefined if resolving the symlink returns nothing", test("xcodeVersion returns undefined if resolved path doesn't include pattern", (t) => { const logger = new RecordingLogger(); - const stubbedFs: RequiredFS = { - statSync: fs.statSync, - readlinkSync: fs.readlinkSync, - }; - const statSync = sinon - .stub(stubbedFs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon - .stub(stubbedFs, "readlinkSync") - .returns("/Applications/Xcode.app/Contents/Developer"); + const { stubbedFs, statSync, readlinkSync } = mockFs( + true, + "/Applications/Xcode.app/Contents/Developer", + ); t.is(xcodeVersion(logger, stubbedFs), undefined); t.is(statSync.callCount, 1); @@ -124,16 +131,10 @@ test("xcodeVersion returns undefined if resolved path doesn't include pattern", test("xcodeVersion returns undefined if match can't be parsed", (t) => { const logger = new RecordingLogger(); - const stubbedFs: RequiredFS = { - statSync: fs.statSync, - readlinkSync: fs.readlinkSync, - }; - const statSync = sinon - .stub(stubbedFs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon - .stub(stubbedFs, "readlinkSync") - .returns("/Applications/Xcode_00.0.app/Contents/Developer"); + const { stubbedFs, statSync, readlinkSync } = mockFs( + true, + "/Applications/Xcode_00.0.app/Contents/Developer", + ); t.is(xcodeVersion(logger, stubbedFs), undefined); t.is(statSync.callCount, 1); @@ -149,16 +150,10 @@ test("xcodeVersion returns undefined if match can't be parsed", (t) => { test("xcodeVersion returns version from resolved path", (t) => { const logger = new RecordingLogger(); - const stubbedFs: RequiredFS = { - statSync: fs.statSync, - readlinkSync: fs.readlinkSync, - }; - const statSync = sinon - .stub(stubbedFs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon - .stub(stubbedFs, "readlinkSync") - .returns("/Applications/Xcode_16.4.app/Contents/Developer"); + const { stubbedFs, statSync, readlinkSync } = mockFs( + true, + "/Applications/Xcode_16.4.app/Contents/Developer", + ); const result = xcodeVersion(logger, stubbedFs); @@ -402,3 +397,193 @@ test.serial( ]); }), ); + +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "isSwiftCompatible doesn't add a diagnostic if Xcode version is supported", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const { stubbedFs, statSync, readlinkSync } = mockFs( + true, + "/Applications/Xcode_26.0.app/Contents/Developer", + ); + + const codeql = await getCodeQLForTesting( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ languages: [BuiltInLanguage.swift] }); + await isSwiftCompatible( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + fs: stubbedFs as FileSystem, + }), + config, + codeql, + ); + + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkUnexpectedLogMessages(t, logger.messages, [ + "Traced Swift analysis is not supported on Xcode 27", + ]); + + t.is(addDiagnostic.callCount, 0); + }), +); + +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "isSwiftCompatible logs and adds diagnostic if Xcode version is unsupported", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const { stubbedFs, statSync, readlinkSync } = mockFs( + true, + "/Applications/Xcode_27.0.app/Contents/Developer", + ); + + const codeql = await getCodeQLForTesting( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ languages: [BuiltInLanguage.swift] }); + await isSwiftCompatible( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + fs: stubbedFs as FileSystem, + }), + config, + codeql, + ); + + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkExpectedLogMessages(t, logger.messages, [ + "Traced Swift analysis is not supported on Xcode 27", + ]); + + t.is(addDiagnostic.callCount, 1); + t.like(addDiagnostic.args[0], [ + config, + BuiltInLanguage.swift, + { + attributes: { + languages: [BuiltInLanguage.swift], + xcodeVersion: "27.0.0", + }, + severity: "warning", + source: { + id: "codeql-action/unsupported-traced-swift-analysis-xcode", + name: "Traced Swift analysis is not supported on this version of Xcode", + }, + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + } satisfies Partial, + ]); + }), +); + +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "isSwiftCompatible throws if Xcode version is unsupported and FF is enabled", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + + const { stubbedFs, statSync, readlinkSync } = mockFs( + true, + "/Applications/Xcode_27.0.app/Contents/Developer", + ); + + const features = createFeatures([ + Feature.SwiftSkipUnsupportedTracedAnalysis, + ]); + + const codeql = await getCodeQLForTesting( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ languages: [BuiltInLanguage.swift] }); + await t.throwsAsync( + isSwiftCompatible( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + features, + fs: stubbedFs as FileSystem, + }), + config, + codeql, + ), + ); + + t.is(statSync.callCount, 1); + t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + t.is(addDiagnostic.callCount, 1); + t.like(addDiagnostic.args[0], [ + config, + BuiltInLanguage.swift, + { + attributes: { + languages: [BuiltInLanguage.swift], + xcodeVersion: "27.0.0", + }, + severity: "error", + source: { + id: "codeql-action/unsupported-traced-swift-analysis-xcode", + name: "Traced Swift analysis is not supported on this version of Xcode", + }, + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + } satisfies Partial, + ]); + }), +); diff --git a/src/languages/swift.ts b/src/languages/swift.ts index 3964bd700c..e6341f6c02 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -1,5 +1,3 @@ -import * as nodefs from "fs"; - import * as semver from "semver"; import { ActionState, Logger } from "../action-common"; @@ -36,7 +34,7 @@ export const SWIFT_TRACED_UNSUPPORTED_XCODE = 27; */ export function xcodeVersion( logger: Logger, - fs: FileSystem<"statSync" | "readlinkSync"> = nodefs, + fs: FileSystem<"statSync" | "readlinkSync">, ): semver.SemVer | undefined { try { // Stat the expected symbolic link to check that it exists and is a symbolic link. @@ -87,6 +85,66 @@ export function xcodeVersion( } } +/** + * Creates a diagnostic indicating that `version` of `product` is unsupported for traced Swift analysis. + * Depending on `skipUnsupportedTracedAnalysis`, this function then either throws a {@link ConfigurationError} + * or logs the problem as a warning. + * + * @param logger The logger to use. + * @param config The CodeQL Action configuration. + * @param skipUnsupportedTracedAnalysis Whether this is a fatal error. + * @param product The product that the version is unsupported of. + * @param version The unsupported version. + */ +function handleUnsupportedVersion( + logger: Logger, + config: Config, + skipUnsupportedTracedAnalysis: boolean, + product: "macOS" | "Xcode", + version: semver.SemVer, +) { + const baseMessage = [ + `Traced Swift analysis is not supported on ${product} ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, + `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, + `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.`, + ].join(" "); + + const attributeName = product === "macOS" ? "macOSVersion" : "xcodeVersion"; + + // Create a diagnostic that will show up on the TSP. + addDiagnostic( + config, + BuiltInLanguage.swift, + makeDiagnostic( + `codeql-action/unsupported-traced-swift-analysis-${product.toLowerCase()}`, + `Traced Swift analysis is not supported on this version of ${product}`, + { + attributes: { + languages: config.languages, + [attributeName]: version.toString(), + }, + markdownMessage: baseMessage, + severity: skipUnsupportedTracedAnalysis ? "error" : "warning", + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + }, + ), + ); + + // Throw an error to abort the analysis if the FF is enabled or log the message. + if (skipUnsupportedTracedAnalysis) { + // ConfigurationErrors are converted to the "aborted" status by the exception handler + // in `init-action.ts` that guards the call to `isSwiftCompatible`. + throw new ConfigurationError(baseMessage); + } else { + // This will also show up as a workflow annotation. + logger.warning(baseMessage); + } +} + /** * Determines whether we can run a Swift analysis on the current runner. * @@ -122,55 +180,39 @@ export async function isSwiftCompatible( `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`, ); } - // If we got a string, we are on macOS but couldn't parse the version string. + + const skipUnsupportedTracedAnalysis = await action.features.getValue( + Feature.SwiftSkipUnsupportedTracedAnalysis, + ); if (typeof version === "string") { + // If we got a string, we are on macOS but couldn't parse the version string. action.logger.warning( `Unable to determine version of macOS, got: ${version}`, ); - return; + } else if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { + handleUnsupportedVersion( + action.logger, + config, + skipUnsupportedTracedAnalysis, + "macOS", + version, + ); } - const skipUnsupportedTracedAnalysis = await action.features.getValue( - Feature.SwiftSkipUnsupportedTracedAnalysis, - ); - if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { - const baseMessage = [ - `Traced Swift analysis is not supported on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`, - `Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`, - `and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.`, - ].join(" "); - - // Create a diagnostic that will show up on the TSP. - addDiagnostic( + // Determining whether the Xcode version is supported only makes sense on macOS, so we only do it + // after determining that we are running on macOS. + const xcodeVer = xcodeVersion(action.logger, action.fs); + + if ( + xcodeVer !== undefined && + xcodeVer.major >= SWIFT_TRACED_UNSUPPORTED_XCODE + ) { + handleUnsupportedVersion( + action.logger, config, - BuiltInLanguage.swift, - makeDiagnostic( - "codeql-action/unsupported-traced-swift-analysis-macos", - "Traced Swift analysis is not supported on this version of macOS", - { - attributes: { - languages: config.languages, - macOSVersion: version.toString(), - }, - markdownMessage: baseMessage, - severity: skipUnsupportedTracedAnalysis ? "error" : "warning", - visibility: { - cliSummaryTable: true, - statusPage: true, - telemetry: true, - }, - }, - ), + skipUnsupportedTracedAnalysis, + "Xcode", + xcodeVer, ); - - // Throw an error to abort the analysis if the FF is enabled or log the message. - if (skipUnsupportedTracedAnalysis) { - // ConfigurationErrors are converted to the "aborted" status by the exception handler - // in `init-action.ts` that guards the call to `isSwiftCompatible`. - throw new ConfigurationError(baseMessage); - } else { - // This will also show up as a workflow annotation. - action.logger.warning(baseMessage); - } } }