Compare commits

...

26 Commits

Author SHA1 Message Date
github-actions[bot]
3367652b89 Rebuild 2026-10-22 13:56:10 +00:00
github-actions[bot]
0373b66365 Update changelog and version after v4.39.2 2026-10-22 13:56:02 +00:00
Michael B. Gale
875aaad85e Update PR check to point at a directory 2026-10-22 13:53:09 +00:00
Michael B. Gale
8750a1ac27 Allow check-diagnostics.ts to check multiple SARIF files 2026-10-22 13:53:09 +00:00
Michael B. Gale
a5a9628b13 Refactor top-level code into checkSarif 2026-10-22 13:53:09 +00:00
Michael B. Gale
1220252f8a Add e2e test 2026-10-22 13:53:09 +00:00
Michael B. Gale
50f89f07cf Use lstat instead of stat 2026-10-22 13:53:09 +00:00
Michael B. Gale
07e32fb9bf Accept paths containing only major version
Also fix unescaped dot patterns in regex
2026-10-22 13:53:09 +00:00
Michael B. Gale
873af8e3e6 Include URL to announcement in diagnostic and log messages 2026-10-22 13:53:09 +00:00
Michael B. Gale
764d26858c Don't perform check for non-traced analyses 2026-10-22 13:53:09 +00:00
Michael B. Gale
abdcb4f319 Refactor non-Swift-specific parts out into traced.ts 2026-10-22 13:53:09 +00:00
Michael B. Gale
c1667d908b Rename feature to SkipUnsupportedTracedAnalysis 2026-10-22 13:53:09 +00:00
Michael B. Gale
23960f8e78 Move xcodeVersion to platform.ts 2026-10-22 13:53:09 +00:00
Michael B. Gale
f095c07825 Use ActionConfig in diagnostics.ts 2026-10-22 13:53:09 +00:00
Michael B. Gale
aa307a6f1b Fix test creating diagnostic locally 2026-10-22 13:53:09 +00:00
Michael B. Gale
c8f25f7624 Check if Xcode version is unsupported 2026-10-22 13:53:09 +00:00
Michael B. Gale
08b50c6860 Add function to determine Xcode version based on symlink 2026-10-22 13:53:09 +00:00
Michael B. Gale
6159e39bb5 Add FF to fail if macOS version is unsupported 2026-10-22 13:53:09 +00:00
Michael B. Gale
17ce15702d Check if macOS version is unsupported 2026-10-22 13:53:09 +00:00
Michael B. Gale
0b06d85d48 Skip Swift checks if tools feature is enabled 2026-10-22 13:53:09 +00:00
Michael B. Gale
1c6a8ee831 Add swiftSupportsAllPlatforms to ToolsFeature 2026-10-22 13:53:09 +00:00
Michael B. Gale
010ab3e269 Use macOSVersion in isSwiftCompatible 2026-10-22 13:53:09 +00:00
Michael B. Gale
766f9e58bc Refactor swift check out of init-action.ts 2026-10-22 13:53:09 +00:00
Michael B. Gale
b52760bd92 Add macOSVersion helper 2026-10-22 13:53:09 +00:00
Michael B. Gale
5a1e877d2d Make getCodeQLForCmd use Env 2026-10-22 13:53:09 +00:00
Michael B. Gale
a6f0856a9c Add osRelease to BaseState 2026-10-22 13:53:09 +00:00
25 changed files with 2176 additions and 613 deletions

131
.github/workflows/__unsupported-macos.yml generated vendored Normal file
View File

@@ -0,0 +1,131 @@
# 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 - Unsupported macOS
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: unsupported-macos-${{github.ref}}
jobs:
unsupported-macos:
strategy:
fail-fast: false
matrix:
include:
- os: xcode-27
version: linked
- os: xcode-27
version: nightly-latest
name: Unsupported macOS
if: github.triggering_actor != 'dependabot[bot]'
permissions:
contents: read
security-events: read
timeout-minutes: 45
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.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: Check xcode_select_link
run: |
readlink /private/var/db/xcode_select_link
file -h /private/var/db/xcode_select_link
- uses: ./../action/init
id: init
with:
languages: swift
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Analyze
# We could run `analyze` here from `./../analyze`, but `getAnalyzeActionName` in `workflow.ts`
# can't cope with that unless we introduce a different testing environment. Since this step
# would just fail on `xcode-27` anyway, we just skip it instead.
if: false
uses: ./analyze
with:
category: '/test-codeql-version:${{ matrix.version }}'
env:
CODEQL_ACTION_SKIP_GENERATE_FAILED_SARIF: false
CODEQL_ACTION_SKIP_SARIF_UPLOAD: true
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
CODEQL_ACTION_TEST_MODE: true
unsupported-macos-sarif-checks:
if: github.triggering_actor != 'dependabot[bot]'
needs:
- unsupported-macos
permissions:
contents: read
security-events: read
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.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: Download workflow artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-mode-sarif-*
path: sarif
- name: List artifact contents
run: ls -laR ./sarif
- name: Check diagnostics appear in SARIF
env:
SARIF_PATH: "./sarif/"
DIAGNOSTIC_ID: "codeql-action/unsupported-traced-analysis-macos"
DIAGNOSTIC_NAME: "Traced analysis is not supported on this version of macOS"
EXPECTED_LOCATIONS: "0"
EXPECTED_MESSAGE: |
Traced analysis is not supported on macOS 27 or above.
run: npx tsx ./pr-checks/check-diagnostics.ts
env:
CODEQL_ACTION_TEST_MODE: true

View File

@@ -6,6 +6,10 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
No user facing changes.
## v4.39.2 - 22 Oct 2026
This release rolls back 4.39.1 due to issues with that release. It is identical to 0.0.0.
## 4.39.1 - 21 Oct 2026
- Update default CodeQL bundle version to [2.27.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.3). [#4256](https://github.com/github/codeql-action/pull/4256)
@@ -1307,3 +1311,4 @@ No user facing changes.
- Add this changelog file. [#507](https://github.com/github/codeql-action/pull/507)
- Improve grouping of analysis logs. Add a new log group containing a summary of metrics and diagnostics, if they were produced by CodeQL builtin queries. [#515](https://github.com/github/codeql-action/pull/515)
- Add metrics and diagnostics summaries from custom query suites to the analysis summary log group. [#532](https://github.com/github/codeql-action/pull/532)

1232
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

5
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.39.2",
"version": "4.39.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.39.2",
"version": "4.39.3",
"license": "MIT",
"workspaces": [
"pr-checks"
@@ -10488,6 +10488,7 @@
"@octokit/core": "^7.0.8",
"@octokit/plugin-paginate-rest": ">=16.0.0",
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"glob": "^13.0.6",
"lite-matter": "^0.1.2",
"mdast-util-from-markdown": "^2.1.0",
"semver": "^7.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.39.2",
"version": "4.39.3",
"private": true,
"description": "CodeQL action",
"scripts": {

View File

@@ -1,6 +1,8 @@
import fs from "fs";
import path from "path";
import * as core from "@actions/core";
import { globSync } from "glob";
import * as sarif from "../src/sarif";
@@ -34,59 +36,80 @@ function checkStatusPageNotification(n: sarif.Notification) {
}
}
function checkSarif(sarifFile: string) {
core.info(`Checking SARIF file at '${sarifFile}'`);
const log = JSON.parse(fs.readFileSync(sarifFile, "utf8")) as sarif.Log;
const run = log.runs[0];
if (run.invocations === undefined) {
throw new Error("run.invocations is undefined");
}
const toolExecutionNotifications =
run.invocations[0].toolExecutionNotifications;
if (toolExecutionNotifications === undefined) {
throw new Error("toolExecutionNotifications is undefined");
}
const statusPageNotifications = toolExecutionNotifications.filter(
(n) =>
n.descriptor?.id === DIAGNOSTIC_ID &&
n.properties?.visibility?.statusPage,
);
if (statusPageNotifications.length !== 1) {
core.setFailed(
"Expected exactly one status page reporting descriptor for this diagnostic in the " +
`'runs[].invocations[].toolExecutionNotifications[]' SARIF property, but found ` +
`${statusPageNotifications.length}. All notification reporting descriptors: ` +
`${JSON.stringify(toolExecutionNotifications)}.`,
);
}
checkStatusPageNotification(statusPageNotifications[0]);
const notifications = run.tool.driver.notifications;
if (notifications === undefined) {
throw new Error("notifications is undefined");
}
const diagnosticNotification = notifications.filter(
(n) =>
n.id === DIAGNOSTIC_ID &&
n.name === DIAGNOSTIC_ID &&
n.fullDescription?.text === DIAGNOSTIC_NAME,
);
if (diagnosticNotification.length !== 1) {
core.setFailed(
"Expected exactly one notification for this diagnostic in the " +
`'runs[].tool.driver.notifications[]' SARIF property, but found ` +
`${diagnosticNotification.length}. All notifications: ` +
`${JSON.stringify(notifications)}.`,
);
}
}
const sarifPath = process.env["SARIF_PATH"];
if (sarifPath === undefined) {
throw new Error("The path to a SARIF file must be set in 'SARIF_PATH'.");
}
const log = JSON.parse(fs.readFileSync(sarifPath, "utf8")) as sarif.Log;
const run = log.runs[0];
if (run.invocations === undefined) {
throw new Error("run.invocations is undefined");
}
const toolExecutionNotifications =
run.invocations[0].toolExecutionNotifications;
if (toolExecutionNotifications === undefined) {
throw new Error("toolExecutionNotifications is undefined");
}
const statusPageNotifications = toolExecutionNotifications.filter(
(n) =>
n.descriptor?.id === DIAGNOSTIC_ID && n.properties?.visibility?.statusPage,
);
if (statusPageNotifications.length !== 1) {
core.setFailed(
"Expected exactly one status page reporting descriptor for this diagnostic in the " +
`'runs[].invocations[].toolExecutionNotifications[]' SARIF property, but found ` +
`${statusPageNotifications.length}. All notification reporting descriptors: ` +
`${JSON.stringify(toolExecutionNotifications)}.`,
throw new Error(
"The path to a SARIF file or directory must be set in 'SARIF_PATH'.",
);
}
checkStatusPageNotification(statusPageNotifications[0]);
const notifications = run.tool.driver.notifications;
if (fs.statSync(sarifPath).isDirectory()) {
const sarifFiles = globSync(path.join(sarifPath, "**", "*.sarif"));
if (notifications === undefined) {
throw new Error("notifications is undefined");
}
if (sarifFiles.length < 1) {
throw new Error(`Unable to find any .sarif files in '${sarifPath}'.`);
}
const diagnosticNotification = notifications.filter(
(n) =>
n.id === DIAGNOSTIC_ID &&
n.name === DIAGNOSTIC_ID &&
n.fullDescription?.text === DIAGNOSTIC_NAME,
);
if (diagnosticNotification.length !== 1) {
core.setFailed(
"Expected exactly one notification for this diagnostic in the " +
`'runs[].tool.driver.notifications[]' SARIF property, but found ` +
`${diagnosticNotification.length}. All notifications: ` +
`${JSON.stringify(notifications)}.`,
);
for (const sarifFile of sarifFiles) {
checkSarif(sarifFile);
}
} else {
checkSarif(sarifPath);
}
core.info("Finished diagnostic export test");

View File

@@ -0,0 +1,63 @@
name: "Unsupported macOS"
description: "Tests that unsupported macOS versions are detected and result in appropriate diagnostics."
versions:
- linked
- nightly-latest
installNode: true
operatingSystems:
- os: macos
runner-image: xcode-27
env:
# Allow the failed SARIF file to be generated.
CODEQL_ACTION_SKIP_GENERATE_FAILED_SARIF: false
# But don't actually upload it.
CODEQL_ACTION_SKIP_SARIF_UPLOAD: true
# Mark telemetry for this workflow so it can be treated separately.
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
steps:
- name: Check xcode_select_link
run: |
readlink /private/var/db/xcode_select_link
file -h /private/var/db/xcode_select_link
- uses: ./../action/init
id: init
with:
languages: swift
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Analyze
# We could run `analyze` here from `./../analyze`, but `getAnalyzeActionName` in `workflow.ts`
# can't cope with that unless we introduce a different testing environment. Since this step
# would just fail on `xcode-27` anyway, we just skip it instead.
if: false
uses: ./analyze
with:
category: "/test-codeql-version:${{ matrix.version }}"
validationJobs:
unsupported-macos-sarif-checks:
checkout: true
installNode: true
steps:
- name: Download workflow artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-mode-sarif-*
path: sarif
- name: List artifact contents
run: ls -laR ./sarif
- name: Check diagnostics appear in SARIF
env:
SARIF_PATH: "./sarif/"
DIAGNOSTIC_ID: "codeql-action/unsupported-traced-analysis-macos"
DIAGNOSTIC_NAME: "Traced analysis is not supported on this version of macOS"
EXPECTED_LOCATIONS: "0"
EXPECTED_MESSAGE: |
Traced analysis is not supported on macOS 27 or above.
run: npx tsx ./pr-checks/check-diagnostics.ts

View File

@@ -9,6 +9,7 @@
"@octokit/core": "^7.0.8",
"@octokit/plugin-paginate-rest": ">=16.0.0",
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"glob": "^13.0.6",
"lite-matter": "^0.1.2",
"mdast-util-from-markdown": "^2.1.0",
"semver": "^7.8.5",

View File

@@ -124,6 +124,8 @@ interface JobSpecification {
type ValidationJobSpecification = JobSpecification & {
checkout?: boolean;
/** Optional condition for the validation job. */
if?: string;
};
/** Describes language/framework-specific steps and inputs. */
@@ -640,9 +642,14 @@ function generateValidationJob(
// Add the generated steps in front of the ones from the specification.
specSteps.items.unshift(...checkoutStep, ...steps);
const baseCondition = "github.triggering_actor != 'dependabot[bot]'";
const validationJob: Record<string, any> = {
name: jobSpecification.name,
if: "github.triggering_actor != 'dependabot[bot]'",
if:
jobSpecification.if !== undefined
? `${baseCondition} && ${jobSpecification.if}`
: baseCondition,
needs: [checkName],
permissions: {
contents: "read",

View File

@@ -1,3 +1,5 @@
import * as os from "os";
import * as core from "@actions/core";
import { ActionsEnv, getActionsEnv } from "./actions-util";
@@ -13,6 +15,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. */
@@ -23,6 +27,8 @@ export interface BaseState {
platform: NodeJS.Platform;
/** The architecture of the host. */
arch: NodeJS.Architecture;
/** The version of the operating system. */
osRelease: string;
}
/** Describes different state features that an Action may have. */
@@ -173,6 +179,7 @@ export async function runInActions(action: Action) {
startedAt,
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
logger,
env,
actions: actionsEnv,

View File

@@ -72,7 +72,7 @@ export const getOptionalInput = function (name: string): string | undefined {
* directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the
* value of `RUNNER_TEMP` otherwise.
*/
export function getTemporaryDirectory(env: Env = getEnv()): string {
export function getTemporaryDirectory(env: ReadOnlyEnv = getEnv()): string {
return (
env.getOptional(EnvVar.TEMP) ?? env.getRequired(ActionsEnvVars.RUNNER_TEMP)
);

View File

@@ -2,7 +2,7 @@ import * as fs from "fs";
import path from "path";
import { getTemporaryDirectory } from "../actions-util";
import { Env } from "../environment";
import { ReadOnlyEnv } from "../environment";
import * as json from "../json";
import { Logger } from "../logging";
@@ -51,7 +51,7 @@ export function resetCachedCodeQlVersion(): void {
* Returns the path to the temporary file that backs the
* on-disk cache of CLI responses between workflow steps.
*/
export function getCommandCacheFilePath(env: Env): string {
export function getCommandCacheFilePath(env: ReadOnlyEnv): string {
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
}

View File

@@ -18,7 +18,7 @@ import { CliError, wrapCliConfigurationError } from "./cli-errors";
import type { ActionConfig } from "./config/action-config";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
import { EnvVar, getEnv, exportEnvVar, Env, ReadOnlyEnv } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -499,13 +499,14 @@ export async function getCodeQLForCmd(
logger: Logger,
cmd: string,
checkVersion: boolean,
env: Env = getEnv(),
): Promise<CodeQL> {
const codeql: CodeQL = {
getPath() {
return cmd;
},
async getVersion() {
const cacheFilePath = outputCache.getCommandCacheFilePath(getEnv());
const cacheFilePath = outputCache.getCommandCacheFilePath(env);
let result = outputCache.getCachedCodeQlVersion(
logger,
cacheFilePath,
@@ -631,7 +632,7 @@ export async function getCodeQLForCmd(
}
},
async runAutobuild(config: Config, language: Language) {
applyAutobuildAzurePipelinesTimeoutFix();
applyAutobuildAzurePipelinesTimeoutFix(env);
const autobuildCmd = path.join(
await this.resolveExtractor(language),
@@ -641,8 +642,11 @@ export async function getCodeQLForCmd(
// Bump the verbosity of the autobuild command if we're in debug mode
if (config.debugMode) {
process.env[EnvVar.CLI_VERBOSITY] =
process.env[EnvVar.CLI_VERBOSITY] || EXTRACTION_DEBUG_MODE_VERBOSITY;
env.set(
EnvVar.CLI_VERBOSITY,
env.getOptional(EnvVar.CLI_VERBOSITY) ??
EXTRACTION_DEBUG_MODE_VERBOSITY,
);
}
// On macOS, System Integrity Protection (SIP) typically interferes with
@@ -674,7 +678,7 @@ export async function getCodeQLForCmd(
},
async extractUsingBuildMode(config: Config, language: Language) {
if (config.buildMode === BuildMode.Autobuild) {
applyAutobuildAzurePipelinesTimeoutFix();
applyAutobuildAzurePipelinesTimeoutFix(env);
}
try {
await runCli(cmd, [
@@ -806,7 +810,7 @@ export async function getCodeQLForCmd(
"--sarif-group-rules-by-pack",
"--sarif-include-query-help=always",
"--sublanguage-file-coverage",
...(await getJobRunUuidSarifOptions()),
...(await getJobRunUuidSarifOptions(env)),
...getExtraOptionsFromEnv(["database", "interpret-results"]),
];
if (sarifRunPropertyFlag !== undefined) {
@@ -1054,7 +1058,7 @@ export async function getCodeQLForCmd(
);
} else if (
checkVersion &&
process.env[EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING] !== "true" &&
env.getOptional(EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING) !== "true" &&
!(await util.codeQlVersionAtLeast(codeql, CODEQL_NEXT_MINIMUM_VERSION))
) {
const result = await codeql.getVersion();
@@ -1276,17 +1280,20 @@ function getExtractionVerbosityArguments(
* Without the fix, long build processes will timeout when pulling down Java packages
* https://developercommunity.visualstudio.com/content/problem/292284/maven-hosted-agent-connection-timeout.html
*/
function applyAutobuildAzurePipelinesTimeoutFix() {
const javaToolOptions = process.env["JAVA_TOOL_OPTIONS"] || "";
process.env["JAVA_TOOL_OPTIONS"] = [
...javaToolOptions.split(/\s+/),
"-Dhttp.keepAlive=false",
"-Dmaven.wagon.http.pool=false",
].join(" ");
function applyAutobuildAzurePipelinesTimeoutFix(env: Env) {
const javaToolOptions = env.getOptional("JAVA_TOOL_OPTIONS") ?? "";
env.set(
"JAVA_TOOL_OPTIONS",
[
...javaToolOptions.split(/\s+/),
"-Dhttp.keepAlive=false",
"-Dmaven.wagon.http.pool=false",
].join(" "),
);
}
async function getJobRunUuidSarifOptions() {
const jobRunUuid = process.env[EnvVar.JOB_RUN_UUID];
async function getJobRunUuidSarifOptions(env: ReadOnlyEnv) {
const jobRunUuid = env.getOptional(EnvVar.JOB_RUN_UUID);
return jobRunUuid ? [`--sarif-run-property=jobRunUuid=${jobRunUuid}`] : [];
}

View File

@@ -3,7 +3,7 @@ import path from "path";
import type { ActionState } from "./action-common";
import { getTemporaryDirectory } from "./actions-util";
import type { Config } from "./config-utils";
import type { ActionConfig } from "./config/action-config";
import { Language } from "./languages";
import { getActionsLogger, type Logger } from "./logging";
import {
@@ -177,7 +177,10 @@ export function getTempLanguageDiagnosticsPath(
}
/** Gets the path where diagnostics for {@link language} should be stored in the database. */
export function getDatabaseDiagnosticsPath(config: Config, language: Language) {
export function getDatabaseDiagnosticsPath(
config: ActionConfig<"dbLocation">,
language: Language,
) {
return getLanguageDiagnosticsPath(config.dbLocation, language);
}
@@ -190,7 +193,7 @@ export function getDatabaseDiagnosticsPath(config: Config, language: Language) {
* @param diagnostic The diagnostic message to add to the database.
*/
export function addDiagnostic(
config: Config,
config: ActionConfig<"dbLocation" | "tempDir">,
language: Language,
diagnostic: DiagnosticMessage,
logger: Logger = getActionsLogger(),
@@ -215,7 +218,7 @@ export function addDiagnostic(
/** Adds a diagnostic that is not specific to any language. */
export function addNoLanguageDiagnostic(
config: Config | undefined,
config: ActionConfig<"languages" | "dbLocation" | "tempDir"> | undefined,
diagnostic: DiagnosticMessage,
logger: Logger = getActionsLogger(),
) {
@@ -305,7 +308,7 @@ function writeDiagnosticFile(
*/
function writeDiagnostic(
action: ActionState<["Logger"]>,
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
diagnostic: DiagnosticMessage,
) {
@@ -348,7 +351,7 @@ export function logTemporaryDiagnostics(action: ActionState<["Logger"]>) {
/** Relocates all temporary diagnostics to the respective databases. */
export function flushDiagnostics(
action: ActionState<["Logger"]>,
config: Config,
config: ActionConfig<"dbLocation" | "languages">,
) {
const diagnosticsCount = temporaryDiagnostics.length;
action.logger.debug(

View File

@@ -12,4 +12,6 @@ export enum DocUrl {
SPECIFY_BUILD_STEPS_MANUALLY = "https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages#about-specifying-build-steps-manually",
SYSTEM_REQUIREMENTS = "https://codeql.github.com/docs/codeql-overview/system-requirements/",
TRACK_CODE_SCANNING_ALERTS_ACROSS_RUNS = "https://docs.github.com/en/code-security/reference/code-scanning/sarif-support-for-code-scanning#data-for-preventing-duplicated-alerts",
/** Blog post about traced analysis being unsupported on macOS 27 and with Xcode 27. */
UNSUPPORTED_TRACED_ANALYSIS_MACOS27 = "https://github.blog/changelog/2026-10-19-codeql-changes-for-macos-27-xcode-27-and-swift-6-4/",
}

View File

@@ -149,6 +149,8 @@ export enum Feature {
*/
PerLanguageBundles = "per_language_bundles_v2",
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Whether we should fail early if we detect that traced analysis is unsupported. */
SkipUnsupportedTracedAnalysis = "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. */
@@ -420,6 +422,11 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_START_PROXY_USE_FEATURES_RELEASE",
minimumVersion: undefined,
},
[Feature.SkipUnsupportedTracedAnalysis]: {
defaultValue: false,
envVar: "CODEQL_ACTION_SKIP_UNSUPPORTED_TRACED_ANALYSIS",
minimumVersion: undefined,
},
[Feature.ToolsRepositoryProperty]: {
defaultValue: false,
envVar: "CODEQL_ACTION_TOOLS_REPOSITORY_PROPERTY",

View File

@@ -62,6 +62,7 @@ import {
runDatabaseInitCluster,
} from "./init";
import { JavaEnvVars, BuiltInLanguage } from "./languages";
import { checkRunnerSupportsTracedAnalysis } from "./languages/traced";
import { Logger, withGroupAsync } from "./logging";
import {
downloadOverlayBaseDatabaseFromCache,
@@ -418,14 +419,11 @@ 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.`,
);
}
await checkRunnerSupportsTracedAnalysis(
actionStateWithFeatures,
config,
codeql,
);
if (repositoryPropertiesResult.isFailure()) {
addNoLanguageDiagnostic(

View File

@@ -0,0 +1,89 @@
import test from "ava";
import * as sinon from "sinon";
import { ActionsEnvVars } from "../environment";
import {
createTestConfig,
getCodeQLForTesting,
getTestEnv,
makeVersionInfo,
RecordingLogger,
setupTests,
} from "../testing-utils";
import { ToolsFeature } from "../tools-features";
import { withTmpDir } from "../util";
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) {
continue;
}
const codeql = await getCodeQLForTesting();
await t.notThrowsAsync(
isSwiftCompatible(
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,
),
);
}
});
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(
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,
),
);
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(
createTestConfig({
tempDir: tmpDir,
languages: [BuiltInLanguage.swift],
}),
codeql,
// A value other than `undefined` means we are on macOS.
"some-macos-version",
),
);
}));

42
src/languages/swift.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { SemVer } from "semver";
import { CodeQL } from "../codeql";
import type { ActionConfig } from "../config/action-config";
import { ToolsFeature } from "../tools-features";
import { ConfigurationError } from "../util";
import { BuiltInLanguage } from ".";
/**
* Determines whether we can run a Swift analysis on the current runner.
*
* @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.
*/
export async function isSwiftCompatible(
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)) {
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;
}
// 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.`,
);
}
}

View File

@@ -0,0 +1,482 @@
import fs from "fs";
import test from "ava";
import * as sinon from "sinon";
import { type CodeQL } from "../codeql";
import * as diagnostics from "../diagnostics";
import { ActionsEnvVars, type Env } from "../environment";
import { Feature } from "../feature-flags";
import type { Logger } from "../logging";
import { XCODE_SELECT_LINK_PATH } from "../platform";
import {
checkExpectedLogMessages,
checkUnexpectedLogMessages,
createFeatures,
createTestConfig,
getCodeQLForTesting,
getTestEnv,
initAllState,
makeVersionInfo,
RecordingLogger,
setupTests,
} from "../testing-utils";
import { BuildMode, withTmpDir } from "../util";
import { checkRunnerSupportsTracedAnalysis } from "./traced";
import { BuiltInLanguage } from ".";
setupTests(test);
const TEST_DEFAULT_TRACED_LANGUAGES = {
[BuiltInLanguage.swift]: true,
[BuiltInLanguage.go]: true,
};
async function getCodeQLWithTracedStub(
cmd: string,
logger: Logger,
env: Env,
tracedLanguages: Partial<
Record<BuiltInLanguage, boolean>
> = TEST_DEFAULT_TRACED_LANGUAGES,
): Promise<CodeQL> {
const codeql = await getCodeQLForTesting(cmd, logger, env);
sinon.stub(codeql, "isTracedLanguage").callsFake(async (language) => {
return tracedLanguages[language] === true;
});
return codeql;
}
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 getCodeQLWithTracedStub(
"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 doesn't throw for BMN`, async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLWithTracedStub(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
await t.notThrowsAsync(
checkRunnerSupportsTracedAnalysis(
initAllState({
platform: "darwin",
osRelease: "27.0.0",
env,
logger,
}),
createTestConfig({ buildMode: BuildMode.None }),
codeql,
),
);
}));
// Some languages not in `TEST_DEFAULT_TRACED_LANGUAGES`.
const nonTracedLanguages: BuiltInLanguage[] = [
BuiltInLanguage.javascript,
BuiltInLanguage.actions,
];
for (const nonTracedLanguage of nonTracedLanguages) {
test(`checkRunnerSupportsTracedAnalysis doesn't throw for ${nonTracedLanguage} on unsupported macOS`, async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLWithTracedStub(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
await t.notThrowsAsync(
checkRunnerSupportsTracedAnalysis(
initAllState({
platform: "darwin",
osRelease: "27.0.0",
env,
logger,
}),
createTestConfig({ languages: [nonTracedLanguage] }),
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 getCodeQLWithTracedStub(
"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 getCodeQLWithTracedStub(
"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<diagnostics.DiagnosticMessage>,
]);
}),
);
// `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 getCodeQLWithTracedStub(
"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<diagnostics.DiagnosticMessage>,
]);
}),
);
// `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 lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_26.0.app/Contents/Developer");
const codeql = await getCodeQLWithTracedStub(
"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(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
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 lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_27.0.app/Contents/Developer");
const codeql = await getCodeQLWithTracedStub(
"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(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
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<diagnostics.DiagnosticMessage>,
]);
}),
);
// `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 lstatSync = sinon
.stub(fs, "lstatSync")
.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 getCodeQLWithTracedStub(
"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(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
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<diagnostics.DiagnosticMessage>,
]);
}),
);

171
src/languages/traced.ts Normal file
View File

@@ -0,0 +1,171 @@
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 { DocUrl } from "../doc-url";
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,
`For more information, see [this announcement](${DocUrl.UNSUPPORTED_TRACED_ANALYSIS_MACOS27}).`,
].join(" "),
severity: skipUnsupportedTracedAnalysis ? "error" : "warning",
visibility: {
cliSummaryTable: true,
statusPage: true,
telemetry: true,
},
},
),
);
}
// Log messages generally don't render as markdown, so we include the URL verbatim there.
const logMessage = [
baseMessage,
`For more information, see ${DocUrl.UNSUPPORTED_TRACED_ANALYSIS_MACOS27}`,
].join(" ");
// 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(logMessage);
} else {
// This will also show up as a workflow annotation.
logger.warning(logMessage);
}
}
/**
* Decides whether {@link config} has any traced languages by looking for tracer
* configurations in the {@link codeql} CLI installation.
*/
export async function hasTracedLanguages(
config: ActionConfig<"languages">,
codeql: CodeQL,
): Promise<boolean> {
for (const language of config.languages) {
if (await codeql.isTracedLanguage(language)) {
return true;
}
}
return false;
}
/**
* 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);
// `build-mode: none` doesn't use the tracer, so it is always supported.
if (config.buildMode !== undefined && config.buildMode === "none") {
return;
}
// If none of the languages are traced, then we can skip the compatibility checks below as well.
if (!(await hasTracedLanguages(config, codeql))) {
return;
}
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,
);
}
}

View File

@@ -1,6 +1,23 @@
import test from "ava";
import fs from "fs";
import { BundlePlatform, getBundlePlatform } from "./platform";
import test from "ava";
import * as sinon from "sinon";
import {
BundlePlatform,
getBundlePlatform,
macOSVersion,
XCODE_SELECT_LINK_PATH,
xcodeVersion,
} from "./platform";
import {
checkExpectedLogMessages,
initAllState,
RecordingLogger,
setupTests,
} from "./testing-utils";
setupTests(test);
for (const [platform, arch, expected] of [
["linux", "x64", BundlePlatform.Linux64],
@@ -16,3 +33,221 @@ 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);
}
});
test.serial("xcodeVersion returns undefined if symlink doesn't exist", (t) => {
const logger = new RecordingLogger();
const lstatSync = sinon.stub(fs, "lstatSync").returns(undefined);
t.is(xcodeVersion(logger), undefined);
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
checkExpectedLogMessages(t, logger.messages, [
"Xcode is probably not installed.",
]);
});
test.serial("xcodeVersion returns undefined if lstatSync throws", (t) => {
const logger = new RecordingLogger();
const lstatSync = sinon.stub(fs, "lstatSync").throws(new Error("ENOENT"));
t.is(xcodeVersion(logger), undefined);
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: ENOENT",
]);
});
test.serial("xcodeVersion returns undefined if file is not a symlink", (t) => {
const logger = new RecordingLogger();
const lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => false } as fs.Stats);
t.is(xcodeVersion(logger), undefined);
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
checkExpectedLogMessages(t, logger.messages, [
"exists, but is not a symbolic link",
]);
});
test.serial(
"xcodeVersion returns undefined if resolving the symlink returns nothing",
(t) => {
const logger = new RecordingLogger();
const lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon.stub(fs, "readlinkSync").returns("");
t.is(xcodeVersion(logger), undefined);
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"unexpectedly returned nothing",
]);
},
);
test.serial(
"xcodeVersion returns undefined if resolved path doesn't include pattern",
(t) => {
const logger = new RecordingLogger();
const lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode.app/Contents/Developer");
t.is(xcodeVersion(logger), undefined);
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"does not contain expected pattern",
]);
},
);
test.serial("xcodeVersion returns undefined if match can't be parsed", (t) => {
const logger = new RecordingLogger();
const lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_00.0.app/Contents/Developer");
t.is(xcodeVersion(logger), undefined);
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
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.serial("xcodeVersion returns version from resolved path", (t) => {
const logger = new RecordingLogger();
const lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_16.4.app/Contents/Developer");
const result = xcodeVersion(logger);
if (t.truthy(result)) {
t.is(result.major, 16);
t.is(result.minor, 4);
}
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
});
test.serial("xcodeVersion returns major version from resolved path", (t) => {
const logger = new RecordingLogger();
const lstatSync = sinon
.stub(fs, "lstatSync")
.returns({ isSymbolicLink: () => true } as fs.Stats);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_16.app/Contents/Developer");
const result = xcodeVersion(logger);
if (t.truthy(result)) {
t.is(result.major, 16);
t.is(result.minor, 0);
}
t.is(lstatSync.callCount, 1);
t.deepEqual(lstatSync.args[0], [
XCODE_SELECT_LINK_PATH,
{ throwIfNoEntry: false },
]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
});

View File

@@ -1,3 +1,10 @@
import fs from "fs";
import * as semver from "semver";
import type { ActionState, Logger } from "./action-common";
import { getErrorMessage } from "./util";
/** Platform identifiers used in CodeQL bundle asset names. */
export enum BundlePlatform {
Linux64 = "linux64",
@@ -24,3 +31,100 @@ 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;
}
/** 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(
/(?<filename>Xcode_(?<majorMinor>\d+(\.\d+)?)\.app)/,
);
/**
* 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): 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.lstatSync(XCODE_SELECT_LINK_PATH, {
throwIfNoEntry: false,
});
if (stats === undefined) {
logger.debug(
`'${XCODE_SELECT_LINK_PATH}' does not exist. Xcode is probably not installed.`,
);
return undefined;
}
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;
}
}

View File

@@ -1,4 +1,5 @@
import { TextDecoder } from "node:util";
import * as os from "os";
import path from "path";
import * as github from "@actions/github";
@@ -34,6 +35,7 @@ import { joinMessageStrings, Loggable, LoggableError, 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,
@@ -272,6 +274,7 @@ export function initAllState(
startedAt: new Date(),
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
logger: new RecordingLogger(),
env,
actions: getTestActionsEnv(env),
@@ -911,7 +914,7 @@ export function mockLanguagesInRepo(languages: string[]) {
*/
export const makeVersionInfo = (
version: string,
features?: { [name: string]: boolean },
features?: { [key in ToolsFeature]?: boolean },
overlayVersion?: number,
): VersionInfo => ({
version,
@@ -1052,6 +1055,7 @@ export function makeTestToken(length: number = 36) {
export async function getCodeQLForTesting(
cmd = "codeql-for-testing",
logger: Logger = new RecordingLogger(),
env: Env = getEnv(),
): Promise<codeql.CodeQL> {
return codeql.getCodeQLForCmd(logger, cmd, false);
return codeql.getCodeQLForCmd(logger, cmd, false, env);
}

View File

@@ -6,6 +6,7 @@ export enum ToolsFeature {
BundleSupportsOverlay = "bundleSupportsOverlay",
IndirectTracingSupportsStaticBinaries = "indirectTracingSupportsStaticBinaries",
SuppressesMissingFileBaselineWarning = "suppressesMissingFileBaselineWarning",
SwiftSupportsAllPlatforms = "swiftSupportsAllPlatforms",
}
/**