Compare commits

...

34 Commits

Author SHA1 Message Date
Henry Mercer
c050421152 Merge pull request #4284 from github/update-bundle/codeql-bundle-v2.27.4
Update default bundle to 2.27.4
2026-11-02 16:43:58 +00:00
Henry Mercer
750fe8feea Merge branch 'main' into update-bundle/codeql-bundle-v2.27.4 2026-11-02 16:16:46 +00:00
github-actions[bot]
adfcd5ffb8 Add changelog note 2026-11-02 16:15:59 +00:00
github-actions[bot]
2ef3b62201 Update default bundle to codeql-bundle-v2.27.4 2026-11-02 16:15:52 +00:00
Michael B. Gale
b35c6c6aae Merge pull request #4251 from github/mbg/refactor-util/errors
Refactor a few things out of `util.ts`
2026-11-02 16:00:32 +00:00
Michael B. Gale
ac9f257611 Merge remote-tracking branch 'origin/main' into mbg/refactor-util/errors 2026-11-02 14:28:50 +00:00
Michael B. Gale
52cf185226 Merge pull request #4278 from github/mbg/macos27/xcode-detection
Add `xcrun`-based fallback for Xcode version detection
2026-11-02 12:37:03 +00:00
Michael B. Gale
95dd6bfbc4 Fix new test not providing typical input 2026-11-02 12:24:01 +00:00
Michael B. Gale
120993f723 Convert diagnostic ID to kebab case 2026-10-30 18:06:04 +00:00
Michael B. Gale
f1396bd6e2 Fix incorrect xcodeVer check 2026-10-30 17:47:22 +00:00
Michael B. Gale
8d18caadc8 Distinguish between Xcode and SDK version in messages 2026-10-30 17:09:47 +00:00
Michael B. Gale
410d706e43 Combine product name and version into ProductVersion type 2026-10-30 16:58:46 +00:00
Michael B. Gale
fb9d45340b Merge remote-tracking branch 'origin/main' into mbg/refactor-util/errors 2026-10-30 16:50:30 +00:00
Michael B. Gale
ce1de8daea Run Xcode check on self-hosted runners 2026-10-30 16:44:04 +00:00
Michael B. Gale
9410c4a524 Merge remote-tracking branch 'origin/main' into mbg/macos27/xcode-detection 2026-10-30 16:22:57 +00:00
Michael B. Gale
80d7b74bf4 Rename xcodeVersionFromSdk and improve comments/log messages 2026-10-30 16:18:06 +00:00
Michael B. Gale
efea0445d3 Add xcodeVersionFromXcodeBuild 2026-10-30 16:14:50 +00:00
Michael B. Gale
8f2596eca1 Remove xcodeVersion and inline fallback logic in checkRunnerSupportsTracedAnalysis 2026-10-30 15:51:13 +00:00
Michael B. Gale
1fb93080c2 Fix test name 2026-10-30 15:38:28 +00:00
Michael B. Gale
be54d791a1 Add xcrun-based fallback for Xcode version detection 2026-10-30 14:59:05 +00:00
Michael B. Gale
4a6767cdd0 Rename xcodeVersion to xcodeVersionFromLink 2026-10-30 14:47:45 +00:00
Michael B. Gale
5c1005e0ae Merge branch 'main' into mbg/refactor-util/errors 2026-10-21 16:58:03 +01:00
Michael B. Gale
c29909a87f Improve typing of resolveFunction 2026-10-21 16:57:16 +01:00
Michael B. Gale
44f1c73c56 Fix resolveFunction call 2026-10-21 16:57:16 +01:00
Michael B. Gale
7def52843a Move initializeEnvironment out of util.ts 2026-10-21 16:57:16 +01:00
Michael B. Gale
774bc191bf Remove unused type parameters from Env classes 2026-10-21 16:57:15 +01:00
Michael B. Gale
dc2ed81cc7 Remove InstanceURL from util.ts 2026-10-16 22:04:05 +01:00
Michael B. Gale
02283e9542 Move more GitHub-related definitions from util.ts to github.ts 2026-10-16 21:54:02 +01:00
Michael B. Gale
86bc4448d3 Mark additional imports in util.ts as type-only 2026-10-16 21:52:19 +01:00
Michael B. Gale
9c72af5b2b Move codeQlVersionAtLeast out of util.ts and into CodeQL 2026-10-16 21:50:31 +01:00
Michael B. Gale
929f5c0db4 Move isDiskConfigurationError out of codeql.ts 2026-10-16 21:50:09 +01:00
Michael B. Gale
a76419a632 Move wrapError and getErrorMessage out of util.ts 2026-10-16 21:46:16 +01:00
Michael B. Gale
071544a67a Move HTTPError out of util.ts 2026-10-16 21:40:48 +01:00
Michael B. Gale
8bdea824e2 Move ConfigurationError out of util.ts 2026-10-16 21:39:54 +01:00
31 changed files with 1158 additions and 536 deletions

View File

@@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
No user facing changes.
- Update default CodeQL bundle version to [2.27.4](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.4). [#4284](https://github.com/github/codeql-action/pull/4284)
## 4.39.1 - 21 Oct 2026

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.27.3",
"cliVersion": "2.27.3",
"priorBundleVersion": "codeql-bundle-v2.27.2",
"priorCliVersion": "2.27.2"
"bundleVersion": "codeql-bundle-v2.27.4",
"cliVersion": "2.27.4",
"priorBundleVersion": "codeql-bundle-v2.27.3",
"priorCliVersion": "2.27.3"
}

571
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@ import {
} from "./actions-util";
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { RecordingLogger, setupTests } from "./testing-utils";
import { initializeEnvironment, withTmpDir } from "./util";
import { getTestEnv, RecordingLogger, setupTests } from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
@@ -227,8 +227,9 @@ test.serial(
);
test.serial("initializeEnvironment", (t) => {
initializeEnvironment("1.2.3");
t.deepEqual(process.env[EnvVar.VERSION], "1.2.3");
const env = getTestEnv();
env.initialise("1.2.3");
t.deepEqual(env.getOptional(EnvVar.VERSION), "1.2.3");
});
test.serial("fixCodeQualityCategory", (t) => {

View File

@@ -233,7 +233,7 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
let databaseUploadResults: DatabaseUploadResult[] = [];
try {
util.initializeEnvironment(actionsUtil.getActionVersion());
action.env.initialise(actionsUtil.getActionVersion());
// Make inputs accessible in the `post` step, details at
// https://github.com/github/codeql-action/issues/2553

View File

@@ -7,7 +7,7 @@ import { ProxyAgent } from "undici";
import * as actionsUtil from "./actions-util";
import * as api from "./api-client";
import { DO_NOT_RETRY_STATUSES } from "./api-client";
import { ActionsEnvVars, RegistryProxyVars } from "./environment";
import { ActionsEnvVars, RegistryProxyVars, getEnv } from "./environment";
import { GITHUB_DOTCOM_API_URL, GITHUB_DOTCOM_URL } from "./github";
import {
callee,
@@ -21,7 +21,7 @@ import * as util from "./util";
setupTests(test);
test.beforeEach(() => {
util.initializeEnvironment(actionsUtil.getActionVersion());
getEnv().initialise(actionsUtil.getActionVersion());
});
test.serial("getApiClient", async (t) => {

View File

@@ -10,7 +10,7 @@ import { getGitHubVersion } from "./api-client";
import { determineAutobuildLanguages, runAutobuild } from "./autobuild";
import { getCodeQL } from "./codeql";
import { Config, getConfig } from "./config-utils";
import { EnvVar } from "./environment";
import { EnvVar, type Env } from "./environment";
import { Language } from "./languages";
import { Logger } from "./logging";
import {
@@ -26,7 +26,6 @@ import {
checkDiskUsage,
checkGitHubVersionInRange,
ConfigurationError,
initializeEnvironment,
wrapError,
} from "./util";
@@ -38,6 +37,7 @@ interface AutobuildStatusReport extends StatusReportBase {
}
async function sendCompletedStatusReport(
env: Env,
config: Config | undefined,
logger: Logger,
startedAt: Date,
@@ -45,7 +45,7 @@ async function sendCompletedStatusReport(
failingLanguage?: string,
cause?: Error,
) {
initializeEnvironment(getActionVersion());
env.initialise(getActionVersion());
const status = getActionsStatus(cause, failingLanguage);
const statusReportBase = await createStatusReportBase(
@@ -129,6 +129,7 @@ async function run({
`We were unable to automatically build your code. Please replace the call to the autobuild action with your custom build steps. ${error.message}`,
);
await sendCompletedStatusReport(
env,
config,
logger,
startedAt,
@@ -141,7 +142,13 @@ async function run({
env.export(EnvVar.AUTOBUILD_DID_COMPLETE_SUCCESSFULLY, "true");
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
await sendCompletedStatusReport(
env,
config,
logger,
startedAt,
languages ?? [],
);
}
/** Defines the `autobuild` Action. */

View File

@@ -21,6 +21,7 @@ import {
import type { Config } from "./config-utils";
import * as defaults from "./defaults.json";
import { DocUrl } from "./doc-url";
import { getEnv } from "./environment";
import { BuiltInLanguage } from "./languages";
import { ToolsSource } from "./setup-codeql";
import {
@@ -38,14 +39,13 @@ import {
} from "./testing-utils";
import { ToolsDownloadStatusReport } from "./tools-download";
import * as util from "./util";
import { initializeEnvironment } from "./util";
setupTests(test);
let stubConfig: Config;
test.beforeEach(() => {
initializeEnvironment("1.2.3");
getEnv().initialise("1.2.3");
stubConfig = createTestConfig({
languages: [BuiltInLanguage.cpp],

View File

@@ -4,6 +4,7 @@ import * as path from "path";
import * as core from "@actions/core";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as yaml from "js-yaml";
import * as semver from "semver";
import {
CommandInvocationError,
@@ -19,6 +20,7 @@ import type { ActionConfig } from "./config/action-config";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar, getEnv, Env, ReadOnlyEnv } from "./environment";
import { getErrorMessage, isDiskConfigurationError } from "./errors";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -34,7 +36,9 @@ import { ToolsDownloadStatusReport } from "./tools-download";
import { ToolsFeature, isSupportedToolsFeature } from "./tools-features";
import { shouldEnableIndirectTracing } from "./tracer-config";
import * as util from "./util";
import { BuildMode, CleanupLevel, getErrorMessage } from "./util";
import { BuildMode, CleanupLevel } from "./util";
export { isDiskConfigurationError } from "./errors";
type Options = Array<string | number | boolean>;
@@ -70,6 +74,11 @@ export interface CodeQL {
* Get a string containing the semver version of the CodeQL executable.
*/
getVersion(): Promise<VersionInfo>;
/**
* Determines whether the CLI installation represented by this {@link CodeQL} instance
* has at least the {@link requiredVersion}.
*/
isVersionAtLeast(requiredVersion: string): Promise<boolean>;
/**
* Print version information about CodeQL.
*/
@@ -275,26 +284,6 @@ const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-10-16";
/** The CLI verbosity level to use for extraction in debug mode. */
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
/**
* Decides whether `e` is a disk-related error outside of our control
* that should be classified as a `ConfigurationError`.
*
* @param e The error to check.
* @returns True if the error should be treated as a `ConfigurationError` or false if not.
*/
export function isDiskConfigurationError(e: unknown): boolean {
if (!(e instanceof Error)) {
return false;
}
return (
// out of disk space
e.message.includes("ENOSPC") ||
// access denied
e.message.includes("EACCES")
);
}
/**
* Set up CodeQL CLI access.
*
@@ -403,9 +392,9 @@ export function setCodeQL(codeql: Partial<CodeQL>): void {
cachedCodeQL = createStubCodeQL(codeql);
}
function resolveFunction<T>(
function resolveFunction<K extends keyof CodeQL, T extends CodeQL[K]>(
partialCodeql: Partial<CodeQL>,
methodName: string,
methodName: K,
defaultImplementation?: T,
): T {
if (typeof partialCodeql[methodName] !== "function") {
@@ -415,7 +404,7 @@ function resolveFunction<T>(
const dummyMethod = () => {
throw new Error(`CodeQL ${methodName} method not correctly defined`);
};
return dummyMethod as T;
return dummyMethod as unknown as T;
}
return partialCodeql[methodName] as T;
}
@@ -432,6 +421,19 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
getVersion: resolveFunction(partialCodeql, "getVersion", async () => ({
version: "1.0.0",
})),
isVersionAtLeast: resolveFunction(
partialCodeql,
"isVersionAtLeast",
async (requiredVersion) => {
return (
!!partialCodeql.getVersion &&
semver.gte(
(await partialCodeql.getVersion()).version,
requiredVersion,
)
);
},
),
printVersion: resolveFunction(partialCodeql, "printVersion"),
supportsFeature: resolveFunction(
partialCodeql,
@@ -525,6 +527,9 @@ export async function getCodeQLForCmd(
}
return result;
},
async isVersionAtLeast(requiredVersion: string) {
return semver.gte((await this.getVersion()).version, requiredVersion);
},
async printVersion() {
// Reuse the cached version information rather than invoking the CLI again.
core.info(JSON.stringify(await this.getVersion(), null, 2));
@@ -1062,7 +1067,7 @@ export async function getCodeQLForCmd(
// CodeQL object is created.
if (
checkVersion &&
!(await util.codeQlVersionAtLeast(codeql, CODEQL_MINIMUM_VERSION))
!(await codeql.isVersionAtLeast(CODEQL_MINIMUM_VERSION))
) {
throw new util.ConfigurationError(
`Expected a CodeQL CLI with version at least ${CODEQL_MINIMUM_VERSION} but got version ${
@@ -1072,7 +1077,7 @@ export async function getCodeQLForCmd(
} else if (
checkVersion &&
env.getOptional(EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING) !== "true" &&
!(await util.codeQlVersionAtLeast(codeql, CODEQL_NEXT_MINIMUM_VERSION))
!(await codeql.isVersionAtLeast(CODEQL_NEXT_MINIMUM_VERSION))
) {
const result = await codeql.getVersion();
core.warning(

View File

@@ -82,7 +82,6 @@ import {
GitHubVersion,
ConfigurationError,
BuildMode,
codeQlVersionAtLeast,
cloneObject,
isDefined,
checkDiskUsage,
@@ -654,10 +653,7 @@ async function runnerHasSufficientMemory(
logger: Logger,
): Promise<boolean> {
if (
await codeQlVersionAtLeast(
codeql,
CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE,
)
await codeql.isVersionAtLeast(CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE)
) {
logger.debug(
`Skipping memory check for overlay analysis because CodeQL version is at least ${CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE}.`,
@@ -901,7 +897,7 @@ async function validateOverlayDatabaseMode(
);
return new Failure(OverlayDisabledReason.IncompatibleBuildMode);
}
if (!(await codeQlVersionAtLeast(codeql, CODEQL_OVERLAY_MINIMUM_VERSION))) {
if (!(await codeql.isVersionAtLeast(CODEQL_OVERLAY_MINIMUM_VERSION))) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`the CodeQL CLI is older than ${CODEQL_OVERLAY_MINIMUM_VERSION}. ` +

View File

@@ -15,6 +15,7 @@ import {
cleanupAndUploadDatabases,
uploadBundledDatabase,
} from "./database-upload";
import { getEnv } from "./environment";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { BuiltInLanguage } from "./languages";
@@ -31,18 +32,12 @@ import {
setupActionsVars,
setupTests,
} from "./testing-utils";
import {
CleanupLevel,
GitHubVariant,
HTTPError,
initializeEnvironment,
withTmpDir,
} from "./util";
import { CleanupLevel, GitHubVariant, HTTPError, withTmpDir } from "./util";
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
getEnv().initialise("1.2.3");
});
const testRepoName: RepositoryNwo = { owner: "github", repo: "example" };

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.27.3",
"cliVersion": "2.27.3",
"priorBundleVersion": "codeql-bundle-v2.27.2",
"priorCliVersion": "2.27.2"
"bundleVersion": "codeql-bundle-v2.27.4",
"cliVersion": "2.27.4",
"priorBundleVersion": "codeql-bundle-v2.27.3",
"priorCliVersion": "2.27.3"
}

View File

@@ -397,6 +397,18 @@ export class Env extends ReadOnlyEnv {
}
}
/**
* Set some initial environment variables that we can set even without
* knowing what version of CodeQL we're running.
*/
public initialise(version: string) {
this.export(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
this.export(EnvVar.FEATURE_SANDWICH, "false");
this.export(EnvVar.FEATURE_SARIF_COMBINE, "true");
this.export(EnvVar.FEATURE_WILL_UPLOAD, "true");
this.export(EnvVar.VERSION, version);
}
/** Gets a value indicating whether `set` was called at least once. */
public hasChanged(): boolean {
return this.changed;

88
src/errors/index.ts Normal file
View File

@@ -0,0 +1,88 @@
import * as json from "../json";
/**
* If {@link error} is not already an instance of {@link Error}, then this function constructs
* an {@link Error} with a string representation of {@link error} as the message.
*/
export function wrapError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
/**
* Returns an appropriate message for the error.
*
* If the error is an {@link Error} instance, this returns the error message without
* an `Error: ` prefix.
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Approximates whether {@link error} is like a {@link NodeJS.ErrnoException}.
* We only typically care about the `code` property, so that's what we check for here.
*/
export function isErrnoException(
error: unknown,
): error is NodeJS.ErrnoException {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string"
);
}
/**
* Decides whether `e` is a disk-related error outside of our control
* that should be classified as a `ConfigurationError`.
*
* @param e The error to check.
* @returns True if the error should be treated as a `ConfigurationError` or false if not.
*/
export function isDiskConfigurationError(e: unknown): boolean {
if (!(e instanceof Error)) {
return false;
}
return (
// out of disk space
e.message.includes("ENOSPC") ||
// access denied
e.message.includes("EACCES")
);
}
/**
* An Error class that indicates an error that occurred due to
* a misconfiguration of the action or the CodeQL CLI.
*/
export class ConfigurationError extends Error {}
/** Minimal representation of errors resulting from HTTP requests. */
export class HTTPError extends Error {
public status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
/**
* Tries to convert {@link arg} to a {@link HTTPError}, but returns `undefined` if
* {@link arg} is not like a {@link HTTPError}.
*/
export function asHTTPError(arg: any): HTTPError | undefined {
if (!json.isObject<any>(arg) || !json.isString(arg.message)) {
return undefined;
}
if (Number.isInteger(arg.status)) {
return new HTTPError(arg.message, arg.status as number);
}
// See https://github.com/actions/toolkit/blob/acb230b99a46ed33a3f04a758cd68b47b9a82908/packages/tool-cache/src/tool-cache.ts#L19
if (Number.isInteger(arg.httpStatusCode)) {
return new HTTPError(arg.message, arg.httpStatusCode as number);
}
return undefined;
}

View File

@@ -4,6 +4,7 @@ import * as path from "path";
import test from "ava";
import * as defaults from "./defaults.json";
import { getEnv } from "./environment";
import {
Feature,
featureConfig,
@@ -26,12 +27,12 @@ import {
setupTests,
stubFeatureFlagApiEndpoint,
} from "./testing-utils";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";
import { GitHubVariant, withTmpDir } from "./util";
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
getEnv().initialise("1.2.3");
});
test.serial(

View File

@@ -560,7 +560,7 @@ class OfflineFeatures implements FeatureEnablement {
// Never use this feature if the CLI version explicitly can't support it.
const minimumVersion = config.minimumVersion;
if (codeql && minimumVersion) {
if (!(await util.codeQlVersionAtLeast(codeql, minimumVersion))) {
if (!(await codeql.isVersionAtLeast(minimumVersion))) {
this.logger.debug(
`Feature ${feature} is disabled because the CodeQL CLI version is older than the minimum ` +
`version ${minimumVersion}.`,

View File

@@ -8,7 +8,7 @@ import {
LoggedMessage,
setupTests,
} from "../testing-utils";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "../util";
import { getEnv, GitHubVariant, withTmpDir } from "../util";
import {
assertAllFeaturesHaveDefaultValues,
@@ -18,7 +18,7 @@ import {
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
getEnv().initialise("1.2.3");
});
test("OfflineFeatures makes no API requests", async (t) => {

View File

@@ -1,5 +1,19 @@
import { ActionsEnvVars, type ReadOnlyEnv } from "./environment";
import { ConfigurationError } from "./util";
import { ConfigurationError } from "./errors";
export enum GitHubVariant {
/** [GitHub.com](https://github.com) */
DOTCOM = "GitHub.com",
/** [GitHub Enterprise Server](https://docs.github.com/en/enterprise-server@latest/admin/overview/about-github-enterprise-server) */
GHES = "GitHub Enterprise Server",
/** [GitHub Enterprise Cloud with data residency](https://docs.github.com/en/enterprise-cloud@latest/admin/data-residency/about-github-enterprise-cloud-with-data-residency) */
GHEC_DR = "GitHub Enterprise Cloud with data residency",
}
export type GitHubVersion =
| { type: GitHubVariant.DOTCOM }
| { type: GitHubVariant.GHEC_DR }
| { type: GitHubVariant.GHES; version: string };
/** A string representing a GitHub instance address. */
export type InstanceURL = string;

View File

@@ -89,12 +89,10 @@ import {
checkDiskUsage,
checkForTimeout,
checkGitHubVersionInRange,
codeQlVersionAtLeast,
DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME,
getCodeQLMemoryLimit,
getThreadsFlagValue,
initializeEnvironment,
ConfigurationError,
wrapError,
checkActionVersion,
@@ -227,7 +225,7 @@ async function run(
let toolsVersion: string;
try {
initializeEnvironment(getActionVersion());
actionState.env.initialise(getActionVersion());
// Make inputs accessible in the `post` step.
persistInputs();
@@ -685,7 +683,7 @@ async function run(
`${EnvVar.JAVA_EXTRACTOR_MINIMIZE_DEPENDENCY_JARS} is already set to '${process.env[EnvVar.JAVA_EXTRACTOR_MINIMIZE_DEPENDENCY_JARS]}', so the Action will not override it.`,
);
} else if (
(await codeQlVersionAtLeast(codeql, CODEQL_VERSION_JAR_MINIMIZATION)) &&
(await codeql.isVersionAtLeast(CODEQL_VERSION_JAR_MINIMIZATION)) &&
config.dependencyCachingEnabled &&
config.buildMode === BuildMode.None &&
config.languages.includes(BuiltInLanguage.java)

View File

@@ -1,5 +1,8 @@
import fs from "fs";
import type { ExecOptions } from "@actions/exec";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import test from "ava";
import * as sinon from "sinon";
@@ -344,38 +347,6 @@ test.serial(
}),
);
test("checkRunnerSupportsTracedAnalysis doesn't perform Xcode check on self-hosted runners", async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
env.set(ActionsEnvVars.RUNNER_ENVIRONMENT, "self-hosted");
const features = createFeatures([Feature.CheckUnsupportedTracedAnalysis]);
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: "26.0.0",
env,
features,
}),
createTestConfig({ tempDir: tmpDir, languages: [BuiltInLanguage.go] }),
codeql,
);
checkExpectedLogMessages(t, logger.messages, [
"Skipping traced analysis compatibility check for Xcode because we are on a self-hosted runner.",
]);
}));
// `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",
@@ -426,6 +397,86 @@ test.serial(
}),
);
test.serial(
"checkRunnerSupportsTracedAnalysis attempts fallbacks if needed",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const features = createFeatures([Feature.CheckUnsupportedTracedAnalysis]);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("Not found"));
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
const exec = sinon.stub<[], Promise<number>>().callsFake(async () => {
throw new Error("Error to force next fallback");
});
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon
.stub(toolrunner, "ToolRunner")
.callsFake(() => {
return runner;
});
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({
tempDir: tmpDir,
languages: [BuiltInLanguage.swift],
});
await checkRunnerSupportsTracedAnalysis(
initAllState({
logger,
platform: "darwin",
osRelease: "26.0.0",
env,
features,
}),
config,
codeql,
);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(which.callCount, 2);
t.deepEqual(which.args[0], ["xcrun", true]);
t.deepEqual(which.args[1], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 2);
t.is(exec.callCount, 2);
t.is(toolrunnerStub.args[0][0], "/path/to/xcrun");
t.deepEqual(toolrunnerStub.args[0][1], ["xcodebuild", "-version"]);
t.is(toolrunnerStub.args[1][0], "/path/to/xcrun");
t.deepEqual(toolrunnerStub.args[1][1], [
"--sdk",
"macosx",
"--show-sdk-version",
]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: Not found",
"Unable to determine Xcode version from xcodebuild: Error to force next fallback",
"Unable to determine Xcode SDK version: Error to force next fallback",
]);
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",
@@ -496,6 +547,103 @@ test.serial(
}),
);
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
test.serial(
"checkRunnerSupportsTracedAnalysis logs and adds diagnostic if Xcode SDK version is unsupported",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const features = createFeatures([Feature.CheckUnsupportedTracedAnalysis]);
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("Not found"));
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
let stdoutListener: ((data: Buffer) => void) | undefined = undefined;
const exec = sinon.stub<[], Promise<number>>().callsFake(async () => {
if (stdoutListener !== undefined) {
stdoutListener(Buffer.from("27.5"));
return 0;
} else {
throw new Error("Error to force next fallback");
}
});
const runner = sinon.createStubInstance(toolrunner.ToolRunner, {
exec,
});
const toolrunnerStub = sinon
.stub(toolrunner, "ToolRunner")
.callsFake((_cmd, args: string[], opts: ExecOptions) => {
if (args[0] !== "xcodebuild") {
stdoutListener = opts.listeners?.stdout;
}
return runner;
});
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({
tempDir: tmpDir,
languages: [BuiltInLanguage.swift],
});
await checkRunnerSupportsTracedAnalysis(
initAllState({
logger,
platform: "darwin",
osRelease: "26.0.0",
env,
features,
}),
config,
codeql,
);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(which.callCount, 2);
t.is(toolrunnerStub.callCount, 2);
t.is(exec.callCount, 2);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version from xcodebuild",
"Traced analysis is not supported on Xcode SDK 27",
]);
t.is(addDiagnostic.callCount, 1);
t.like(addDiagnostic.args[0], [
config,
BuiltInLanguage.swift,
{
attributes: {
languages: [BuiltInLanguage.swift],
xcodeSdkVersion: "27.5.0",
},
severity: "warning",
source: {
id: "codeql-action/unsupported-traced-analysis-xcode-sdk",
name: "Traced analysis is not supported on this version of Xcode SDK",
},
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",

View File

@@ -1,13 +1,17 @@
import * as semver from "semver";
import type { ActionState, Logger } from "../action-common";
import { isSelfHostedRunner } from "../actions-util";
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 { darwinVersion, xcodeVersion } from "../platform";
import {
darwinVersion,
xcodeVersionFromLink,
xcodeSdkVersion,
xcodeVersionFromXcodeBuild,
} from "../platform";
import { ConfigurationError } from "../util";
import { checkSwiftCompatibility } from "./swift";
@@ -18,6 +22,27 @@ export const UNSUPPORTED_DARWIN_FOR_TRACED = 27;
/** Xcode 27 and above do not support traced extraction. */
export const UNSUPPORTED_XCODE_FOR_TRACED = 27;
/** Names of products that we check the versions of here. */
type ProductName = "macOS" | "Xcode" | "Xcode SDK";
/** A version along with the product it relates to. */
type ProductVersion = {
product: ProductName;
version: semver.SemVer;
};
/** Gets the appropriate attribute name for the diagnostic based on {@link product}. */
function diagnosticAttributeName(product: ProductName): string {
switch (product) {
case "macOS":
return "darwinVersion";
case "Xcode":
return "xcodeVersion";
case "Xcode SDK":
return "xcodeSdkVersion";
}
}
/**
* Creates a diagnostic indicating that `version` of `product` is unsupported for traced analysis.
* Depending on `skipUnsupportedTracedAnalysis`, this function then either throws a {@link ConfigurationError}
@@ -27,28 +52,30 @@ export const UNSUPPORTED_XCODE_FOR_TRACED = 27;
* @param config The CodeQL Action configuration.
* @param tracedLanguages Traced languages that are configured for analysis.
* @param abortUnsupportedTracedAnalysis Whether this is a fatal error.
* @param product The product that the version is unsupported of.
* @param version The unsupported version.
* @param productVersion A description of the product and its version that is unsupported.
*/
function handleUnsupportedVersion(
logger: Logger,
config: ActionConfig<"dbLocation" | "tempDir">,
tracedLanguages: string[],
abortUnsupportedTracedAnalysis: boolean,
product: "macOS" | "Xcode",
version: semver.SemVer,
productVersion: ProductVersion,
) {
const { product, version } = productVersion;
const unsupportedVersion =
product === "macOS"
? UNSUPPORTED_DARWIN_FOR_TRACED
: UNSUPPORTED_XCODE_FOR_TRACED;
: // We use the same constant for both Xcode and the Xcode SDK since we treat them
// as equivalent indicators for whether traced analysis is unsupported.
UNSUPPORTED_XCODE_FOR_TRACED;
const baseMessage = [
`Traced analysis is not supported on ${product} ${unsupportedVersion} or above.`,
`Configure your analysis to run on macOS ${UNSUPPORTED_DARWIN_FOR_TRACED - 1} or below`,
`and Xcode ${UNSUPPORTED_XCODE_FOR_TRACED - 1} or below.`,
].join(" ");
const attributeName = product === "macOS" ? "darwinVersion" : "xcodeVersion";
const attributeName = diagnosticAttributeName(product);
// Create a diagnostic that will show up on the TSP.
for (const language of tracedLanguages) {
@@ -56,7 +83,7 @@ function handleUnsupportedVersion(
config,
language,
makeDiagnostic(
`codeql-action/unsupported-traced-analysis-${product.toLowerCase()}`,
`codeql-action/unsupported-traced-analysis-${product.toLowerCase().replaceAll(" ", "-")}`,
`Traced analysis is not supported on this version of ${product}`,
{
attributes: {
@@ -177,8 +204,7 @@ export async function checkRunnerSupportsTracedAnalysis(
config,
tracedLanguages,
abortUnsupportedTracedAnalysis,
"macOS",
version,
{ product: "macOS", version },
);
// Return early so that we don't end up producing more-or-less the same log message/diagnostic
@@ -186,32 +212,39 @@ export async function checkRunnerSupportsTracedAnalysis(
return;
}
// Skip if we are on a self-hosted runner since the current Xcode version detection
// is only validated on hosted runners.
if (isSelfHostedRunner(action.env)) {
action.logger.debug(
"Skipping traced analysis compatibility check for Xcode because we are on a self-hosted runner.",
);
return;
// 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. We first try to obtain the version of Xcode itself
// starting with the cheapest approach and then call out to an external program. If neither works, we try
// to obtain the version from the SDK instead. Since either Xcode or the SDK at version 27 or above is
// incompatible, either is fine as we only check the major version below.
const xcodeVer =
xcodeVersionFromLink(action.logger) ??
(await xcodeVersionFromXcodeBuild(action.logger));
let xcodeProduct: ProductVersion | undefined = undefined;
if (xcodeVer !== undefined) {
xcodeProduct = { product: "Xcode", version: xcodeVer };
} else {
// Fallback to checking the SDK version, which may be available even if a full Xcode installation is not.
const xcodeSdkVer = await xcodeSdkVersion(action.logger);
xcodeProduct =
xcodeSdkVer !== undefined
? { product: "Xcode SDK", version: xcodeSdkVer }
: undefined;
}
// 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
xcodeProduct !== undefined &&
xcodeProduct.version.major >= UNSUPPORTED_XCODE_FOR_TRACED
) {
handleUnsupportedVersion(
action.logger,
config,
tracedLanguages,
abortUnsupportedTracedAnalysis,
"Xcode",
xcodeVer,
xcodeProduct,
);
} else if (xcodeVer === undefined) {
} else if (xcodeProduct === undefined) {
action.logger.info(
"Unable to determine Xcode version. Skipping Xcode compatibility check.",
);

View File

@@ -1,5 +1,8 @@
import fs from "fs";
import type { ExecOptions } from "@actions/exec/lib/interfaces";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import test from "ava";
import * as sinon from "sinon";
@@ -8,7 +11,9 @@ import {
getBundlePlatform,
darwinVersion,
XCODE_SELECT_LINK_PATH,
xcodeVersion,
xcodeVersionFromLink,
xcodeSdkVersion,
xcodeVersionFromXcodeBuild,
} from "./platform";
import {
checkExpectedLogMessages,
@@ -71,46 +76,52 @@ test("darwinVersion returns semver if parsing succeeds", (t) => {
}
});
test.serial("xcodeVersion returns undefined if symlink doesn't exist", (t) => {
const logger = new RecordingLogger();
test.serial(
"xcodeVersionFromLink returns undefined if symlink doesn't exist",
(t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("ENOENT: no such file or directory"));
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("ENOENT: no such file or directory"));
t.is(xcodeVersion(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(xcodeVersionFromLink(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: ENOENT: no such file or directory",
]);
});
test.serial("xcodeVersion returns undefined if file is not a symlink", (t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("EINVAL: invalid argument"));
t.is(xcodeVersion(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: EINVAL: invalid argument",
]);
});
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: ENOENT: no such file or directory",
]);
},
);
test.serial(
"xcodeVersion returns undefined if resolving the symlink returns nothing",
"xcodeVersionFromLink returns undefined if file is not a symlink",
(t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("EINVAL: invalid argument"));
t.is(xcodeVersionFromLink(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: EINVAL: invalid argument",
]);
},
);
test.serial(
"xcodeVersionFromLink returns undefined if resolving the symlink returns nothing",
(t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon.stub(fs, "readlinkSync").returns("");
t.is(xcodeVersion(logger), undefined);
t.is(xcodeVersionFromLink(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
@@ -121,7 +132,7 @@ test.serial(
);
test.serial(
"xcodeVersion returns undefined if resolved path doesn't include pattern",
"xcodeVersionFromLink returns undefined if resolved path doesn't include pattern",
(t) => {
const logger = new RecordingLogger();
@@ -129,7 +140,7 @@ test.serial(
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode.app/Contents/Developer");
t.is(xcodeVersion(logger), undefined);
t.is(xcodeVersionFromLink(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
@@ -139,30 +150,33 @@ test.serial(
},
);
test.serial("xcodeVersion returns undefined if match can't be parsed", (t) => {
const logger = new RecordingLogger();
test.serial(
"xcodeVersionFromLink returns undefined if match can't be parsed",
(t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_00.0.app/Contents/Developer");
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_00.0.app/Contents/Developer");
t.is(xcodeVersion(logger), undefined);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(xcodeVersionFromLink(logger), undefined);
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.",
]);
});
checkExpectedLogMessages(t, logger.messages, [
"Couldn't parse '00.0' as a semantic version.",
]);
},
);
test.serial("xcodeVersion returns version from resolved path", (t) => {
test.serial("xcodeVersionFromLink returns version from resolved path", (t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_16.4.app/Contents/Developer");
const result = xcodeVersion(logger);
const result = xcodeVersionFromLink(logger);
if (t.truthy(result)) {
t.is(result.major, 16);
@@ -173,26 +187,29 @@ test.serial("xcodeVersion returns version from resolved path", (t) => {
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
});
test.serial("xcodeVersion returns major version from resolved path", (t) => {
const logger = new RecordingLogger();
test.serial(
"xcodeVersionFromLink returns major version from resolved path",
(t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_16.app/Contents/Developer");
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_16.app/Contents/Developer");
const result = xcodeVersion(logger);
const result = xcodeVersionFromLink(logger);
if (t.truthy(result)) {
t.is(result.major, 16);
t.is(result.minor, 0);
}
if (t.truthy(result)) {
t.is(result.major, 16);
t.is(result.minor, 0);
}
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
});
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
},
);
test.serial(
"xcodeVersion allows more version components in resolved path",
"xcodeVersionFromLink allows more version components in resolved path",
(t) => {
const logger = new RecordingLogger();
@@ -200,7 +217,7 @@ test.serial(
.stub(fs, "readlinkSync")
.returns("/Applications/Xcode_16.5.9.app/Contents/Developer");
const result = xcodeVersion(logger);
const result = xcodeVersionFromLink(logger);
if (t.truthy(result)) {
t.is(result.major, 16);
@@ -211,3 +228,135 @@ test.serial(
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
},
);
test.serial(
"xcodeVersionFromXcodeBuild returns undefined for non-zero exit code",
async (t) => {
const logger = new RecordingLogger();
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
const exec = sinon.stub<[], Promise<number>>().resolves(1);
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon.stub(toolrunner, "ToolRunner").returns(runner);
await t.notThrowsAsync(async () => {
const result = await xcodeVersionFromXcodeBuild(logger);
t.is(result, undefined);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 1);
t.is(exec.callCount, 1);
checkExpectedLogMessages(t, logger.messages, [
"xcodebuild returned exit code 1",
]);
});
},
);
test.serial(
"xcodeVersionFromXcodeBuild extracts version from xcrun stdout",
async (t) => {
const logger = new RecordingLogger();
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
let stdoutListener: ((data: Buffer) => void) | undefined = undefined;
const exec = sinon.stub<[], Promise<number>>().callsFake(async () => {
if (stdoutListener !== undefined) {
stdoutListener(Buffer.from("Xcode 26.5"));
stdoutListener(Buffer.from("Build version 26F9"));
}
return 0;
});
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon
.stub(toolrunner, "ToolRunner")
.callsFake((_cmd, _args, opts: ExecOptions) => {
stdoutListener = opts.listeners?.stdout;
return runner;
});
await t.notThrowsAsync(async () => {
const result = await xcodeVersionFromXcodeBuild(logger);
t.not(result, undefined);
t.is(result?.major, 26);
t.is(result?.minor, 5);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 1);
t.is(exec.callCount, 1);
});
},
);
test.serial(
"xcodeSdkVersion returns undefined for non-zero exit code",
async (t) => {
const logger = new RecordingLogger();
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
const exec = sinon.stub<[], Promise<number>>().resolves(1);
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon.stub(toolrunner, "ToolRunner").returns(runner);
await t.notThrowsAsync(async () => {
const result = await xcodeSdkVersion(logger);
t.is(result, undefined);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 1);
t.is(exec.callCount, 1);
checkExpectedLogMessages(t, logger.messages, [
"xcrun returned exit code 1",
]);
});
},
);
test.serial("xcodeSdkVersion extracts version from xcrun stdout", async (t) => {
const logger = new RecordingLogger();
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
let stdoutListener: ((data: Buffer) => void) | undefined = undefined;
const exec = sinon.stub<[], Promise<number>>().callsFake(async () => {
if (stdoutListener !== undefined) {
stdoutListener(Buffer.from("26.5"));
}
return 0;
});
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon
.stub(toolrunner, "ToolRunner")
.callsFake((_cmd, _args, opts: ExecOptions) => {
stdoutListener = opts.listeners?.stdout;
return runner;
});
await t.notThrowsAsync(async () => {
const result = await xcodeSdkVersion(logger);
t.not(result, undefined);
t.is(result?.major, 26);
t.is(result?.minor, 5);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 1);
t.is(exec.callCount, 1);
});
});

View File

@@ -1,5 +1,7 @@
import fs from "fs";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import * as semver from "semver";
import type { ActionState, Logger } from "./action-common";
@@ -68,12 +70,15 @@ export const XCODE_APP_FILENAME_PATTERN = new RegExp(
);
/**
* Tries to determine the version of Xcode that is installed.
* Tries to determine the version of Xcode that is installed from the resolved symbolic link
* at {@link XCODE_SELECT_LINK_PATH}.
*
* @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 {
export function xcodeVersionFromLink(
logger: Logger,
): semver.SemVer | undefined {
try {
// Read what the symbolic link points to. This will throw if `XCODE_SELECT_LINK_PATH` does not
// exist (`ENOENT`) or is not a symbolic link (`EINVAL`).
@@ -109,3 +114,132 @@ export function xcodeVersion(logger: Logger): semver.SemVer | undefined {
return undefined;
}
}
/**
* Tries to determine the version of Xcode that is installed by running `xcrun xcodebuild -version`.
*
* @param logger The logger to use.
* @returns The Xcode version or `undefined` if it couldn't be determined.
*/
export async function xcodeVersionFromXcodeBuild(
logger: Logger,
): Promise<semver.SemVer | undefined> {
try {
const xcrun = await io.which("xcrun", true);
let stdout = "";
let stderr = "";
const exitCode = await new toolrunner.ToolRunner(
xcrun,
["xcodebuild", "-version"],
{
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data: Buffer) => {
stdout += data.toString("utf-8");
},
stderr: (data: Buffer) => {
stderr += data.toString("utf-8");
},
},
},
).exec();
if (exitCode !== 0) {
logger.debug(`xcodebuild returned exit code ${exitCode}`);
logger.debug(`stdout:\n${stdout}`);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
// The output contains a couple of lines, but we only care about the product version.
const match = stdout.match(/^Xcode (\d+(\.\d+)?)/);
if (match === null) {
logger.debug(
`Output doesn't appear to contain the Xcode product version.`,
);
logger.debug(`stdout:\n${stdout}`);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
// We expect the output to only be a version string. Try to parse it into a semver.
const version = semver.coerce(match[1].trim());
if (version === null) {
logger.debug(
`xcodebuild's output couldn't be parsed as a version:\n${stdout}`,
);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
return version;
} catch (err) {
logger.debug(
`Unable to determine Xcode version from xcodebuild: ${getErrorMessage(err)}`,
);
return undefined;
}
}
/**
* Tries to determine the version of the Xcode SDK that is installed by running
* `xcrun --sdk macosx --show-sdk-version`.
*
* @param logger The logger to use.
* @returns The Xcode SDK version or `undefined` if it couldn't be determined.
*/
export async function xcodeSdkVersion(
logger: Logger,
): Promise<semver.SemVer | undefined> {
try {
const xcrun = await io.which("xcrun", true);
let stdout = "";
let stderr = "";
const exitCode = await new toolrunner.ToolRunner(
xcrun,
["--sdk", "macosx", "--show-sdk-version"],
{
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data: Buffer) => {
stdout += data.toString("utf-8");
},
stderr: (data: Buffer) => {
stderr += data.toString("utf-8");
},
},
},
).exec();
if (exitCode !== 0) {
logger.debug(`xcrun returned exit code ${exitCode}`);
logger.debug(`stdout:\n${stdout}`);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
// We expect the output to only be a version string. Try to parse it into a semver.
const version = semver.coerce(stdout.trim());
if (version === null) {
logger.debug(
`xcrun's output couldn't be parsed as a version:\n${stdout}`,
);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
return version;
} catch (err) {
logger.debug(
`Unable to determine Xcode SDK version: ${getErrorMessage(err)}`,
);
return undefined;
}
}

View File

@@ -38,7 +38,6 @@ import {
checkDiskUsage,
checkForTimeout,
checkGitHubVersionInRange,
initializeEnvironment,
ConfigurationError,
wrapError,
checkActionVersion,
@@ -108,7 +107,7 @@ async function run(
let toolsVersion: string;
try {
initializeEnvironment(getActionVersion());
actionState.env.initialise(getActionVersion());
const apiDetails = {
auth: getRequiredInput("token"),

View File

@@ -11,7 +11,13 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as api from "./api-client";
import * as diagnostics from "./diagnostics";
import { ActionsEnvVars, Env, EnvVar, ReadOnlyEnv } from "./environment";
import {
ActionsEnvVars,
Env,
EnvVar,
getEnv,
ReadOnlyEnv,
} from "./environment";
import { Feature } from "./feature-flags";
import { BuiltInLanguage } from "./languages";
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
@@ -37,18 +43,12 @@ import {
setupTests,
} from "./testing-utils";
import * as toolsDownload from "./tools-download";
import {
getErrorMessage,
GitHubVariant,
HTTPError,
initializeEnvironment,
withTmpDir,
} from "./util";
import { getErrorMessage, GitHubVariant, HTTPError, withTmpDir } from "./util";
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
getEnv().initialise("1.2.3");
});
function stubDownloadAndExtract() {

View File

@@ -6,7 +6,6 @@ import * as sinon from "sinon";
import * as actionsUtil from "../actions-util";
import { ActionsEnvVars } from "../environment";
import { callee, getTestEnv, setupTests } from "../testing-utils";
import { initializeEnvironment } from "../util";
import { getCodeQLActionRepository } from "./repository";
@@ -16,7 +15,7 @@ test.serial("getCodeQLActionRepository", async (t) => {
const initialEnv = getTestEnv({
[ActionsEnvVars.RUNNER_TEMP]: path.dirname(__dirname),
});
initializeEnvironment("1.2.3", initialEnv);
initialEnv.initialise("1.2.3");
const target = callee(getCodeQLActionRepository)
.withArgs()

View File

@@ -2,13 +2,12 @@ import { ActionState } from "../action-common";
import { isRunningLocalAction } from "../actions-util";
import * as api from "../api-client";
import { ActionsEnvVars } from "../environment";
import { GITHUB_DOTCOM_URL } from "../github";
import { GITHUB_DOTCOM_URL, type InstanceURL } from "../github";
import {
isRepositoryNwoStr,
parseRepositoryNwo,
type RepositoryNwoStr,
} from "../repository";
import { type InstanceURL } from "../util";
/** The NWO of the standard CodeQL Action repository. */
export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";

View File

@@ -10,6 +10,7 @@ import { https } from "follow-redirects";
import nock from "nock";
import * as sinon from "sinon";
import * as errors from "./errors";
import * as tar from "./tar";
import { RecordingLogger, setupTests } from "./testing-utils";
import { downloadAndExtract } from "./tools-download";
@@ -163,7 +164,7 @@ test.serial(
test.serial(
"downloadAndExtract handles an unknown status as a non-HTTP error",
async (t) => {
const asHTTPError = sinon.spy(util, "asHTTPError");
const asHTTPError = sinon.spy(errors, "asHTTPError");
await util.withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");

View File

@@ -10,17 +10,18 @@ import * as analyses from "./analyses";
import { AnalysisKind, CodeQuality, CodeScanning } from "./analyses";
import * as api from "./api-client";
import * as diffUtils from "./diff-informed-analysis-utils";
import { getEnv } from "./environment";
import { Logger } from "./logging";
import * as sarif from "./sarif";
import { RecordingLogger, setupTests } from "./testing-utils";
import * as uploadLib from "./upload-lib";
import { UploadPayload } from "./upload-lib/types";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";
import { GitHubVariant, withTmpDir } from "./util";
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
getEnv().initialise("1.2.3");
});
test.serial("validateSarifFileSchema - valid", (t) => {

View File

@@ -24,7 +24,6 @@ import {
ConfigurationError,
checkActionVersion,
checkDiskUsage,
initializeEnvironment,
shouldSkipSarifUpload,
wrapError,
} from "./util";
@@ -55,11 +54,13 @@ async function sendSuccessStatusReport(
}
}
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
async function run(action: ActionState<["Base", "Env", "Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = action.logger;
try {
initializeEnvironment(getActionVersion());
action.env.initialise(getActionVersion());
const gitHubVersion = await getGitHubVersion();
checkActionVersion(getActionVersion(), gitHubVersion);
@@ -79,7 +80,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
const startingStatusReportBase = await createStatusReportBase(
ActionName.UploadSarif,
"starting",
startedAt,
action.startedAt,
undefined,
await checkDiskUsage(logger),
logger,
@@ -137,7 +138,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
// so we can't wait for that here.
}
await sendSuccessStatusReport(
startedAt,
action.startedAt,
codeScanningResult?.statusReport || {},
logger,
);
@@ -153,7 +154,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
const errorStatusReportBase = await createStatusReportBase(
ActionName.UploadSarif,
getActionsStatus(error),
startedAt,
action.startedAt,
undefined,
await checkDiskUsage(logger),
logger,

View File

@@ -20,20 +20,22 @@ import {
getRequiredEnvParam,
exportEnvVar,
getEnv,
type Env,
type ReadOnlyEnv,
} from "./environment";
import * as json from "./json";
import { Language } from "./languages";
import { Logger } from "./logging";
import { ConfigurationError, getErrorMessage, wrapError } from "./errors";
import { GitHubVariant, type GitHubVersion } from "./github";
import type { Language } from "./languages";
import type { Logger } from "./logging";
// Re-export for backwards compatibility to avoid updating a lot of imports elsewhere.
export * from "./errors";
export {
getRequiredEnvParam,
getOptionalEnvVar,
getEnv,
isInTestMode,
} from "./environment";
export { GitHubVariant, type GitHubVersion } from "./github";
/**
* The name of the file containing the base database OIDs, as stored in the
@@ -47,9 +49,6 @@ const BASE_DATABASE_OIDS_FILE_NAME = "base-database-oids.json";
*/
const BROKEN_VERSIONS = ["0.0.0-20211207"];
/** A string representing a GitHub instance. */
export type InstanceURL = string;
/**
* Default name of the debugging artifact.
*/
@@ -470,20 +469,6 @@ const CODEQL_ACTION_WARNED_ABOUT_VERSION_ENV_VAR =
let hasBeenWarnedAboutVersion = false;
export enum GitHubVariant {
/** [GitHub.com](https://github.com) */
DOTCOM = "GitHub.com",
/** [GitHub Enterprise Server](https://docs.github.com/en/enterprise-server@latest/admin/overview/about-github-enterprise-server) */
GHES = "GitHub Enterprise Server",
/** [GitHub Enterprise Cloud with data residency](https://docs.github.com/en/enterprise-cloud@latest/admin/data-residency/about-github-enterprise-cloud-with-data-residency) */
GHEC_DR = "GitHub Enterprise Cloud with data residency",
}
export type GitHubVersion =
| { type: GitHubVariant.DOTCOM }
| { type: GitHubVariant.GHEC_DR }
| { type: GitHubVariant.GHES; version: string };
export function checkGitHubVersionInRange(
version: GitHubVersion,
logger: Logger,
@@ -552,54 +537,6 @@ export function assertNever(value: never): never {
throw new ExhaustivityCheckingError(value);
}
/**
* Set some initial environment variables that we can set even without
* knowing what version of CodeQL we're running.
*/
export function initializeEnvironment(version: string, env: Env = getEnv()) {
env.export(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
env.export(EnvVar.FEATURE_SANDWICH, "false");
env.export(EnvVar.FEATURE_SARIF_COMBINE, "true");
env.export(EnvVar.FEATURE_WILL_UPLOAD, "true");
env.export(EnvVar.VERSION, version);
}
export class HTTPError extends Error {
public status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
/**
* An Error class that indicates an error that occurred due to
* a misconfiguration of the action or the CodeQL CLI.
*/
export class ConfigurationError extends Error {}
export function asHTTPError(arg: any): HTTPError | undefined {
if (!json.isObject<any>(arg) || !json.isString(arg.message)) {
return undefined;
}
if (Number.isInteger(arg.status)) {
return new HTTPError(arg.message, arg.status as number);
}
// See https://github.com/actions/toolkit/blob/acb230b99a46ed33a3f04a758cd68b47b9a82908/packages/tool-cache/src/tool-cache.ts#L19
if (Number.isInteger(arg.httpStatusCode)) {
return new HTTPError(arg.message, arg.httpStatusCode as number);
}
return undefined;
}
export async function codeQlVersionAtLeast(
codeql: CodeQL,
requiredVersion: string,
): Promise<boolean> {
return semver.gte((await codeql.getVersion()).version, requiredVersion);
}
export function getBaseDatabaseOidsFilePath(
config: ActionConfig<"dbLocation">,
): string {
@@ -886,35 +823,6 @@ export function parseMatrixInput(
}
}
export function wrapError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
/**
* Returns an appropriate message for the error.
*
* If the error is an `Error` instance, this returns the error message without
* an `Error: ` prefix.
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Approximates whether {@link error} is like a {@link NodeJS.ErrnoException}.
* We only typically care about the `code` property, so that's what we check for here.
*/
export function isErrnoException(
error: unknown,
): error is NodeJS.ErrnoException {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string"
);
}
export function prettyPrintPack(pack: Pack) {
return `${pack.name}${pack.version ? `@${pack.version}` : ""}${
pack.path ? `:${pack.path}` : ""