diff --git a/lib/entry-points.js b/lib/entry-points.js index fdeb36815..2ee662740 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -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_util61 = require_internal3(); + var import_util62 = require_internal3(); var tracingPolicyName2 = "tracingPolicy"; function tracingPolicy2(options = {}) { const userAgentPromise = (0, import_userAgent.getUserAgentValue)(options.userAgentPrefix); - const sanitizer = new import_util61.Sanitizer({ + const sanitizer = new import_util62.Sanitizer({ additionalAllowedQueryParameters: options.additionalAllowedQueryParameters }); const tracingClient = tryCreateTracingClient(); @@ -46597,11 +46597,11 @@ var require_response2 = __commonJS({ }); module2.exports = __toCommonJS2(response_exports); var import_core_rest_pipeline = require_commonjs6(); - var import_util61 = require_util9(); + var import_util62 = require_util9(); var originalResponse = /* @__PURE__ */ Symbol("Original FullOperationResponse"); function toCompatResponse2(response, options) { - let request3 = (0, import_util61.toWebResourceLike)(response.request); - let headers = (0, import_util61.toHttpHeadersLike)(response.headers); + let request3 = (0, import_util62.toWebResourceLike)(response.request); + let headers = (0, import_util62.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_util61.toPipelineRequest)(compatResponse.request) + request: (0, import_util62.toPipelineRequest)(compatResponse.request) }; } } @@ -46749,7 +46749,7 @@ var require_requestPolicyFactoryPolicy = __commonJS({ requestPolicyFactoryPolicyName: () => requestPolicyFactoryPolicyName2 }); module2.exports = __toCommonJS2(requestPolicyFactoryPolicy_exports); - var import_util61 = require_util9(); + var import_util62 = 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_util61.toPipelineRequest)(httpRequest)); + const response2 = await next((0, import_util62.toPipelineRequest)(httpRequest)); return (0, import_response.toCompatResponse)(response2, { createProxy: true }); } }; for (const factory of orderedFactories) { httpPipeline = factory.create(httpPipeline, mockRequestPolicyOptions); } - const webResourceLike = (0, import_util61.toWebResourceLike)(request3, { createProxy: true }); + const webResourceLike = (0, import_util62.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_util61 = require_util9(); + var import_util62 = require_util9(); function convertHttpClient2(requestPolicyClient) { return { sendRequest: async (request3) => { const response = await requestPolicyClient.sendRequest( - (0, import_util61.toWebResourceLike)(request3, { createProxy: true }) + (0, import_util62.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_util61.toHttpHeadersLike + toHttpHeadersLike: () => import_util62.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_util61 = require_util9(); + var import_util62 = require_util9(); } }); @@ -186219,55 +186219,62 @@ async function getToolsInput(action, repositoryProperties) { } // src/languages/swift.ts -var SWIFT_TRACED_UNSUPPORTED_MACOS = 27; -var SWIFT_TRACED_UNSUPPORTED_XCODE = 27; -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) { +async function isSwiftCompatible(config, codeql, macosVersion) { if (!config.languages.includes("swift" /* swift */)) { return; } if (await codeql.supportsFeature("swiftSupportsAllPlatforms" /* SwiftSupportsAllPlatforms */)) { return; } - const version = macOSVersion(action); - if (version === void 0) { + if (macosVersion === void 0) { throw new ConfigurationError( `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.` ); } +} + +// src/languages/traced.ts +var UNSUPPORTED_MACOS_FOR_TRACED = 27; +var UNSUPPORTED_XCODE_FOR_TRACED = 27; +function handleUnsupportedVersion(logger2, config, skipUnsupportedTracedAnalysis, product, version) { + const baseMessage = [ + `Traced analysis is not supported on ${product} ${UNSUPPORTED_MACOS_FOR_TRACED} or above.`, + `Configure your analysis to run on macOS ${UNSUPPORTED_MACOS_FOR_TRACED - 1} or below`, + `and XCode ${UNSUPPORTED_XCODE_FOR_TRACED - 1} or below.` + ].join(" "); + const attributeName = product === "macOS" ? "macOSVersion" : "xcodeVersion"; + for (const language of config.languages) { + addDiagnostic( + config, + language, + makeDiagnostic( + `codeql-action/unsupported-traced-analysis-${product.toLowerCase()}`, + `Traced 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 checkRunnerSupportsTracedAnalysis(action, config, codeql) { + const version = macOSVersion(action); + await isSwiftCompatible(config, codeql, version); const skipUnsupportedTracedAnalysis = await action.features.getValue( "skip_unsupported_traced_analysis" /* SkipUnsupportedTracedAnalysis */ ); @@ -186275,7 +186282,7 @@ async function isSwiftCompatible(action, config, codeql) { action.logger.warning( `Unable to determine version of macOS, got: ${version}` ); - } else if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { + } else if (version !== void 0 && version.major >= UNSUPPORTED_MACOS_FOR_TRACED) { handleUnsupportedVersion( action.logger, config, @@ -186285,7 +186292,7 @@ async function isSwiftCompatible(action, config, codeql) { ); } const xcodeVer = xcodeVersion(action.logger); - if (xcodeVer !== void 0 && xcodeVer.major >= SWIFT_TRACED_UNSUPPORTED_XCODE) { + if (xcodeVer !== void 0 && xcodeVer.major >= UNSUPPORTED_XCODE_FOR_TRACED) { handleUnsupportedVersion( action.logger, config, @@ -186815,7 +186822,11 @@ async function run3(actionState) { enableFileCoverageInformation: fileCoverageResult.enabled, logger: logger2 }); - await isSwiftCompatible(actionStateWithFeatures, config, codeql); + await checkRunnerSupportsTracedAnalysis( + actionStateWithFeatures, + config, + codeql + ); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( config, diff --git a/src/init-action.ts b/src/init-action.ts index cf102d76a..f0f377c4f 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -62,7 +62,7 @@ import { runDatabaseInitCluster, } from "./init"; import { JavaEnvVars, BuiltInLanguage } from "./languages"; -import { isSwiftCompatible } from "./languages/swift"; +import { checkRunnerSupportsTracedAnalysis } from "./languages/traced"; import { Logger, withGroupAsync } from "./logging"; import { downloadOverlayBaseDatabaseFromCache, @@ -419,7 +419,11 @@ async function run( logger, }); - await isSwiftCompatible(actionStateWithFeatures, config, codeql); + await checkRunnerSupportsTracedAnalysis( + actionStateWithFeatures, + config, + codeql, + ); if (repositoryPropertiesResult.isFailure()) { addNoLanguageDiagnostic( diff --git a/src/languages/swift.test.ts b/src/languages/swift.test.ts index d42c45b59..3bdcf1fa2 100644 --- a/src/languages/swift.test.ts +++ b/src/languages/swift.test.ts @@ -1,20 +1,11 @@ -import fs from "fs"; - import test from "ava"; import * as sinon from "sinon"; import { getCodeQLForTesting } from "../codeql"; -import * as diagnostics from "../diagnostics"; import { ActionsEnvVars } from "../environment"; -import { Feature } from "../feature-flags"; -import { XCODE_SELECT_LINK_PATH } from "../platform"; import { - checkExpectedLogMessages, - checkUnexpectedLogMessages, - createFeatures, createTestConfig, getTestEnv, - initAllState, makeVersionInfo, RecordingLogger, setupTests, @@ -37,9 +28,11 @@ test("isSwiftCompatible doesn't throw for non-Swift languages", async (t) => { const codeql = await getCodeQLForTesting(); await t.notThrowsAsync( isSwiftCompatible( - initAllState(), createTestConfig({ languages: [language] }), codeql, + // A value of `undefined` should make `isSwiftCompatible` throw for Swift by default + // since it means we are on a platform other than macOS + undefined, ), ); } @@ -59,9 +52,11 @@ test("isSwiftCompatible doesn't throw for Swift if CLI supports swiftSupportsAll await t.notThrowsAsync( isSwiftCompatible( - initAllState({ platform: "darwin", env, logger }), createTestConfig({ languages: [BuiltInLanguage.swift] }), codeql, + // A value of `undefined` should make `isSwiftCompatible` throw for Swift by default + // since it means we are on a platform other than macOS + undefined, ), ); @@ -82,370 +77,13 @@ test("isSwiftCompatible doesn't throw for Swift on darwin", async (t) => await t.notThrowsAsync( isSwiftCompatible( - initAllState({ platform: "darwin", env, logger }), createTestConfig({ tempDir: tmpDir, languages: [BuiltInLanguage.swift], }), codeql, + // A value other than `undefined` means we are on macOS. + "some-macos-version", ), ); })); - -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", 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", - ]); - })); - -// `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", - }, - 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.SkipUnsupportedTracedAnalysis]); - - 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", - }, - 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 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 statSync = sinon - .stub(fs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .returns("/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, - }), - 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 statSync = sinon - .stub(fs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .returns("/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, - }), - 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 statSync = sinon - .stub(fs, "statSync") - .returns({ isSymbolicLink: () => true } as fs.Stats); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .returns("/Applications/Xcode_27.0.app/Contents/Developer"); - - const features = createFeatures([Feature.SkipUnsupportedTracedAnalysis]); - - 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, - }), - 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 42a9b68d4..34932fe78 100644 --- a/src/languages/swift.ts +++ b/src/languages/swift.ts @@ -1,95 +1,25 @@ -import * as semver from "semver"; +import type { 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 { macOSVersion, xcodeVersion } from "../platform"; +import type { ActionConfig } from "../config/action-config"; 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; - -/** - * 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. * - * @param action The Action state. * @param config The Action configuration. + * @param codeql The {@link CodeQL} CLI instance. + * @param macosVersion The version of macOS, if any. * * @throws {ConfigurationError} If Swift analysis is not possible on the current runner. - * @returns True if we can run a Swift analysis. */ export async function isSwiftCompatible( - action: ActionState<["Base", "Logger", "FeatureFlags"]>, - config: Config, + config: ActionConfig<"languages">, codeql: CodeQL, + macosVersion: SemVer | string | undefined, ) { // The checks are not relevant if we are not trying to analyse Swift. if (!config.languages.includes(BuiltInLanguage.swift)) { @@ -103,48 +33,10 @@ export async function isSwiftCompatible( return; } - // Try to get the macOS version. - const version = macOSVersion(action); - - // If `version` is undefined, then we are not on macOS. - if (version === undefined) { + // If `macosVersion` is undefined, then we are not on macOS and Swift analysis is unsupported. + if (macosVersion === undefined) { throw new ConfigurationError( `Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`, ); } - - const skipUnsupportedTracedAnalysis = await action.features.getValue( - Feature.SkipUnsupportedTracedAnalysis, - ); - 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}`, - ); - } else if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) { - handleUnsupportedVersion( - action.logger, - config, - skipUnsupportedTracedAnalysis, - "macOS", - version, - ); - } - - // 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); - - if ( - xcodeVer !== undefined && - xcodeVer.major >= SWIFT_TRACED_UNSUPPORTED_XCODE - ) { - handleUnsupportedVersion( - action.logger, - config, - skipUnsupportedTracedAnalysis, - "Xcode", - xcodeVer, - ); - } } diff --git a/src/languages/traced.test.ts b/src/languages/traced.test.ts new file mode 100644 index 000000000..3f4a530d3 --- /dev/null +++ b/src/languages/traced.test.ts @@ -0,0 +1,386 @@ +import fs from "fs"; + +import test from "ava"; +import * as sinon from "sinon"; + +import { getCodeQLForTesting } from "../codeql"; +import * as diagnostics from "../diagnostics"; +import { ActionsEnvVars } from "../environment"; +import { Feature } from "../feature-flags"; +import { XCODE_SELECT_LINK_PATH } from "../platform"; +import { + checkExpectedLogMessages, + checkUnexpectedLogMessages, + createFeatures, + createTestConfig, + getTestEnv, + initAllState, + makeVersionInfo, + RecordingLogger, + setupTests, +} from "../testing-utils"; +import { withTmpDir } from "../util"; + +import { checkRunnerSupportsTracedAnalysis } from "./traced"; + +import { BuiltInLanguage } from "."; + +setupTests(test); + +const nonDarwinPlatforms: NodeJS.Platform[] = ["linux", "win32"]; +for (const nonDarwinPlatform of nonDarwinPlatforms) { + test(`checkRunnerSupportsTracedAnalysis 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( + checkRunnerSupportsTracedAnalysis( + initAllState({ platform: nonDarwinPlatform, env, logger }), + createTestConfig({ languages: [BuiltInLanguage.swift] }), + codeql, + ), + ); + })); +} + +test("checkRunnerSupportsTracedAnalysis 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 checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "unexpected", + env, + }), + createTestConfig({ languages: [BuiltInLanguage.go] }), + codeql, + ); + + checkExpectedLogMessages(t, logger.messages, [ + "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( + "checkRunnerSupportsTracedAnalysis 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 checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "27.0.0", + env, + }), + config, + codeql, + ); + + checkExpectedLogMessages(t, logger.messages, [ + "Traced 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", + }, + severity: "warning", + source: { + id: "codeql-action/unsupported-traced-analysis-macos", + name: "Traced 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( + "checkRunnerSupportsTracedAnalysis 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.SkipUnsupportedTracedAnalysis]); + + 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( + checkRunnerSupportsTracedAnalysis( + 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-analysis-macos", + name: "Traced 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( + "checkRunnerSupportsTracedAnalysis 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 statSync = sinon + .stub(fs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .returns("/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 checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + }), + 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 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( + "checkRunnerSupportsTracedAnalysis 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 statSync = sinon + .stub(fs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .returns("/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 checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + }), + 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 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-analysis-xcode", + name: "Traced 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( + "checkRunnerSupportsTracedAnalysis 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 statSync = sinon + .stub(fs, "statSync") + .returns({ isSymbolicLink: () => true } as fs.Stats); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .returns("/Applications/Xcode_27.0.app/Contents/Developer"); + + const features = createFeatures([Feature.SkipUnsupportedTracedAnalysis]); + + 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( + checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + features, + }), + 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-analysis-xcode", + name: "Traced analysis is not supported on this version of Xcode", + }, + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + } satisfies Partial, + ]); + }), +); diff --git a/src/languages/traced.ts b/src/languages/traced.ts new file mode 100644 index 000000000..1eaa04dd8 --- /dev/null +++ b/src/languages/traced.ts @@ -0,0 +1,134 @@ +import * as semver from "semver"; + +import type { ActionState, Logger } from "../action-common"; +import type { CodeQL } from "../codeql"; +import type { ActionConfig } from "../config/action-config"; +import { addDiagnostic, makeDiagnostic } from "../diagnostics"; +import { Feature } from "../feature-flags"; +import { macOSVersion, xcodeVersion } from "../platform"; +import { ConfigurationError } from "../util"; + +import { isSwiftCompatible } from "./swift"; + +/** macOS 27 and above do not support traced extraction. */ +export const UNSUPPORTED_MACOS_FOR_TRACED = 27; + +/** Xcode 27 and above do not support traced extraction. */ +export const UNSUPPORTED_XCODE_FOR_TRACED = 27; + +/** + * Creates a diagnostic indicating that `version` of `product` is unsupported for traced 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: ActionConfig<"languages" | "dbLocation" | "tempDir">, + skipUnsupportedTracedAnalysis: boolean, + product: "macOS" | "Xcode", + version: semver.SemVer, +) { + const baseMessage = [ + `Traced analysis is not supported on ${product} ${UNSUPPORTED_MACOS_FOR_TRACED} or above.`, + `Configure your analysis to run on macOS ${UNSUPPORTED_MACOS_FOR_TRACED - 1} or below`, + `and XCode ${UNSUPPORTED_XCODE_FOR_TRACED - 1} or below.`, + ].join(" "); + + const attributeName = product === "macOS" ? "macOSVersion" : "xcodeVersion"; + + // Create a diagnostic that will show up on the TSP. + for (const language of config.languages) { + addDiagnostic( + config, + language, + makeDiagnostic( + `codeql-action/unsupported-traced-analysis-${product.toLowerCase()}`, + `Traced 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 `checkRunnerSupportsTracedAnalysis`. + throw new ConfigurationError(baseMessage); + } else { + // This will also show up as a workflow annotation. + logger.warning(baseMessage); + } +} + +/** + * Determines whether we can run a traced analysis on the current runner. + * + * @throws {ConfigurationError} If traced analysis is not possible on the current runner. + */ +export async function checkRunnerSupportsTracedAnalysis( + action: ActionState<["Base", "Logger", "FeatureFlags"]>, + config: ActionConfig<"languages" | "dbLocation" | "tempDir">, + codeql: CodeQL, +) { + // Try to get the macOS version. + const version = macOSVersion(action); + + // Perform some initial, Swift-specific checks. + await isSwiftCompatible(config, codeql, version); + + const skipUnsupportedTracedAnalysis = await action.features.getValue( + Feature.SkipUnsupportedTracedAnalysis, + ); + 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}`, + ); + } else if ( + version !== undefined && + version.major >= UNSUPPORTED_MACOS_FOR_TRACED + ) { + handleUnsupportedVersion( + action.logger, + config, + skipUnsupportedTracedAnalysis, + "macOS", + version, + ); + } + + // 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); + + if ( + xcodeVer !== undefined && + xcodeVer.major >= UNSUPPORTED_XCODE_FOR_TRACED + ) { + handleUnsupportedVersion( + action.logger, + config, + skipUnsupportedTracedAnalysis, + "Xcode", + xcodeVer, + ); + } +}