Compare commits

..

14 Commits

Author SHA1 Message Date
Michael B. Gale
b6de93bcd7 Check if Xcode version is unsupported 2026-10-09 18:11:41 +01:00
Michael B. Gale
bf94034f3c Make FS available to isSwiftCompatible 2026-10-09 17:52:55 +01:00
Michael B. Gale
436656b463 Add FS state feature 2026-10-09 17:50:50 +01:00
Michael B. Gale
130da64af0 Add function to determine Xcode version based on symlink 2026-10-09 17:23:00 +01:00
Michael B. Gale
6745303b3d Add fs.ts to abstract over fs 2026-10-09 17:14:37 +01:00
Michael B. Gale
d09e9c0eb9 Add FF to fail if macOS version is unsupported 2026-10-09 16:01:15 +01:00
Michael B. Gale
ff832f1444 Check if macOS version is unsupported 2026-10-09 15:58:26 +01:00
Michael B. Gale
047473c122 Skip Swift checks if tools feature is enabled 2026-10-09 15:25:46 +01:00
Michael B. Gale
08bb1f303d Make getCodeQLForCmd use Env 2026-10-09 15:13:42 +01:00
Michael B. Gale
b904de6d18 Add swiftSupportsAllPlatforms to ToolsFeature 2026-10-09 14:41:26 +01:00
Michael B. Gale
807d4743d0 Use macOSVersion in isSwiftCompatible 2026-10-09 11:35:54 +01:00
Michael B. Gale
788a22022b Refactor swift check out of init-action.ts 2026-10-09 11:26:35 +01:00
Michael B. Gale
ed72893659 Add macOSVersion helper 2026-10-09 10:48:24 +01:00
Michael B. Gale
acf4e5b3ac Add osRelease to BaseState 2026-10-09 10:41:40 +01:00
39 changed files with 2260 additions and 1313 deletions

View File

@@ -1,4 +1,3 @@
import pkg from "./package.json" with { type: "json" };
globalThis.__CODEQL_ACTION_VERSION__ = pkg.version;
globalThis.__CODEQL_ACTION_TEST_ENV__ = "unit-test";

View File

@@ -78,7 +78,7 @@ const UPLOAD_LIB_SRC = "./src/upload-lib";
*
* The virtual module additionally re-exports `upload-lib` under the `uploadLib` namespace so that
* external consumers can access it via the small `lib/upload-lib.js` stub emitted below.
*
*
* A tiny stub file is emitted for each Action entrypoint, and one for `upload-lib`. Each stub
* imports the shared bundle and calls/re-exports from the respective entry point.
*
@@ -212,7 +212,6 @@ const context = await esbuild.context({
target: ["node20"],
define: {
__CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version),
__CODEQL_ACTION_TEST_ENV__: JSON.stringify(""),
},
metafile: true,
});

View File

@@ -140,17 +140,6 @@ export default [
"no-async-foreach/no-async-foreach": "error",
"no-sequences": "error",
"no-shadow": "off",
// A basic check that we don't use `exportVariable` from `@actions/core`.
"no-restricted-syntax": [
"error",
{
selector: "MemberExpression[property.name='exportVariable']",
message:
"Use the `export` method of an `Env` instance or `exportEnvVar` from `environment.ts` instead.",
},
],
// This is overly restrictive with unsetting `EnvVar`s
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/no-shadow": "error",
@@ -168,15 +157,6 @@ export default [
],
},
},
{
files: ["src/environment.ts"],
// We allow `exportVariable` from `@actions/core` to be used in this file
// since it defines the wrapper around it that other modules use.
rules: {
"no-restricted-syntax": "off",
},
},
{
files: ["**/*.ts", "**/*.js"],

2184
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

10
package-lock.json generated
View File

@@ -58,7 +58,7 @@
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-github": "^6.1.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^65.0.1",
"eslint-plugin-jsdoc": "^64.5.4",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.12.0",
@@ -5381,15 +5381,15 @@
}
},
"node_modules/eslint-plugin-jsdoc": {
"version": "65.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-65.0.1.tgz",
"integrity": "sha512-7IvB+ZS71WCw5tGKh6f6D5k55qtXKdrGTnIpQTBKWdpyZ5IfhGHfh0i6ArWlCJ4JEtz22YRk5Bqu1MvbOlpMKQ==",
"version": "64.5.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.5.4.tgz",
"integrity": "sha512-xfRhXPSSWT612muJ6XTvxuVJ8zYHv99qNgBqPVqF28aSqoy4s4XbSkrWXnYA2hl5ejC02yEpk6fqkj3BmJ6Xbg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@es-joy/jsdoccomment": "~0.98.0",
"@es-joy/resolve.exports": "1.2.0",
"@typescript-eslint/utils": "^8.70.1",
"@typescript-eslint/utils": "^8.70.0",
"are-docs-informative": "^0.1.1",
"comment-parser": "1.4.9",
"debug": "^4.4.3",

View File

@@ -66,7 +66,7 @@
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-github": "^6.1.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^65.0.1",
"eslint-plugin-jsdoc": "^64.5.4",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.12.0",

View File

@@ -154,6 +154,26 @@ await describe("isValidChangenoteFile", async () => {
assert.equal(isValidChangenoteFile("non-existent-file.md"), false);
});
await it("rejects invalid filename", async () => {
await withTmpFile(
"fix-bug.md",
"---\ncategory: fix\n---\n- Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), false);
},
);
});
await it("rejects missing frontmatter", async () => {
await withTmpFile(
"2026-01-01-fix-bug.md",
"- Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), false);
},
);
});
await it("rejects invalid Markdown", async () => {
await withTmpFile(
"2026-01-01-fix-bug.md",

View File

@@ -1,4 +1,5 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { matter } from "lite-matter";
import type { List, ListItem } from "mdast";
@@ -94,7 +95,21 @@ export function isValidChangenoteFile(filename: string): boolean {
return false;
}
const { content } = matter(fileData);
const { data: frontmatter, content } = matter(fileData);
if (!isValidChangenoteFilename(path.basename(filename))) {
isValid = false;
console.error(
`${filename}: invalid filename; must match pattern YYYY-MM-DD-id.md`,
);
}
if (!hasValidChangenoteCategory(frontmatter)) {
isValid = false;
const categories = Object.keys(VALID_CHANGE_NOTE_CATEGORIES).join(", ");
console.error(
`${filename}: invalid category; must be one of: ${categories}`,
);
}
if (!isValidChangenoteContent(content)) {
isValid = false;
console.error(

View File

@@ -23,8 +23,7 @@ predicate isSafeForDefaultSetup(string envVar) {
"GITHUB_BASE_REF", "GITHUB_EVENT_NAME", "GITHUB_JOB", "GITHUB_RUN_ATTEMPT", "GITHUB_RUN_ID",
"GITHUB_SHA", "GITHUB_REPOSITORY", "GITHUB_SERVER_URL", "GITHUB_TOKEN", "GITHUB_WORKFLOW",
"GITHUB_WORKSPACE", "GOFLAGS", "ImageVersion", "JAVA_TOOL_OPTIONS", "RUNNER_ARCH",
"RUNNER_ENVIRONMENT", "RUNNER_NAME", "RUNNER_OS", "RUNNER_TEMP", "RUNNER_TOOL_CACHE",
"NODE_ENV"
"RUNNER_ENVIRONMENT", "RUNNER_NAME", "RUNNER_OS", "RUNNER_TEMP", "RUNNER_TOOL_CACHE"
]
}

View File

@@ -1,9 +1,13 @@
import * as fs from "fs";
import * as os from "os";
import * as core from "@actions/core";
import { ActionsEnv, getActionsEnv } from "./actions-util";
import type { ApiClient } from "./api-client";
import { Env, ReadOnlyEnv } from "./environment";
import type { FeatureEnablement } from "./feature-flags";
import type { FileSystem } from "./fs";
import { getActionsLogger, Logger } from "./logging";
import {
ActionName,
@@ -13,6 +17,8 @@ import {
} from "./status-report";
import { getEnv, getErrorMessage, wrapError } from "./util";
export type { Logger } from "./logging";
/** Base state that is available to an Action on startup. */
export interface BaseState {
/** The name of the Action. */
@@ -23,6 +29,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. */
@@ -51,6 +59,10 @@ export interface FeatureState {
/** Information about enabled feature flags. */
features: FeatureEnablement;
};
FS: {
/** The file system operations to use. */
fs: FileSystem;
};
}
/** Identifies a type of state an Action may have. */
@@ -74,7 +86,7 @@ export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
* Each Action can then augment the `state` further if additional features are required.
*/
export type ActionMain = (
state: ActionState<["Base", "Logger", "Env", "Actions"]>,
state: ActionState<["Base", "FS", "Logger", "Env", "Actions"]>,
) => Promise<void>;
/** A specification for a CodeQL Action step. */
@@ -104,6 +116,8 @@ export async function runInActions(action: Action) {
startedAt,
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
fs,
logger,
env,
actions: actionsEnv,

View File

@@ -30,6 +30,7 @@ declare const __CODEQL_ACTION_VERSION__: string;
export interface ActionsEnv {
getRequiredInput: (name: string) => string;
getOptionalInput: (name: string) => string | undefined;
exportVariable: (name: string, value: string) => void;
}
/**
@@ -39,6 +40,7 @@ export function getActionsEnv(): ActionsEnv {
return {
getRequiredInput,
getOptionalInput,
exportVariable: core.exportVariable,
};
}
@@ -72,7 +74,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

@@ -214,10 +214,9 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
async function run({
startedAt,
env,
logger,
actions,
}: ActionState<["Base", "Env", "Logger", "Actions"]>) {
}: ActionState<["Base", "Logger", "Actions"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -288,7 +287,7 @@ async function run({
const apiDetails = getApiDetails();
const outputDir = actionsUtil.getRequiredInput("output");
env.export(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
core.exportVariable(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
const threads = util.getThreadsFlag(
actionsUtil.getOptionalInput("threads") || process.env["CODEQL_THREADS"],
logger,
@@ -457,7 +456,7 @@ async function run({
`expect-error input was set to true but no error was thrown.`,
);
}
env.export(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY, "true");
core.exportVariable(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY, "true");
} catch (unwrappedError) {
const error = util.wrapError(unwrappedError);
if (

View File

@@ -17,7 +17,6 @@ import {
ReadOnlyEnv,
RegistryProxyVars,
getEnv,
exportEnvVar,
} from "./environment";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
@@ -317,7 +316,7 @@ export async function getAnalysisKey(): Promise<string> {
const jobName = getRequiredEnvParam("GITHUB_JOB");
analysisKey = `${workflowPath}:${jobName}`;
exportEnvVar(EnvVar.ANALYSIS_KEY, analysisKey);
core.exportVariable(EnvVar.ANALYSIS_KEY, analysisKey);
return analysisKey;
}

View File

@@ -68,11 +68,7 @@ async function sendCompletedStatusReport(
}
}
async function run({
startedAt,
env,
logger,
}: ActionState<["Base", "Env", "Logger"]>) {
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -139,7 +135,7 @@ async function run({
return;
}
env.export(EnvVar.AUTOBUILD_DID_COMPLETE_SUCCESSFULLY, "true");
core.exportVariable(EnvVar.AUTOBUILD_DID_COMPLETE_SUCCESSFULLY, "true");
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
}

View File

@@ -1,9 +1,11 @@
import * as core from "@actions/core";
import { getTemporaryDirectory, getWorkflowEventName } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { DocUrl } from "./doc-url";
import { ActionsEnvVars, EnvVar, exportEnvVar } from "./environment";
import { ActionsEnvVars, EnvVar } from "./environment";
import { Feature, featureConfig, initFeatures } from "./feature-flags";
import { BuiltInLanguage, Language } from "./languages";
import { Logger } from "./logging";
@@ -134,16 +136,16 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
: ""
}`,
);
exportEnvVar(envVar, "false");
core.exportVariable(envVar, "false");
} else {
logger.info(
`Enabling ${featureName}. This can be disabled by setting the ${envVar} environment variable to 'false'. See ${DocUrl.DEFINE_ENV_VARIABLES} for more information.`,
);
exportEnvVar(envVar, "true");
core.exportVariable(envVar, "true");
}
} else {
logger.info(`Disabling ${featureName}.`);
exportEnvVar(envVar, "false");
core.exportVariable(envVar, "false");
}
}
@@ -163,7 +165,7 @@ export async function runAutobuild(
await codeQL.runAutobuild(config, language);
}
if (language === BuiltInLanguage.go) {
exportEnvVar(EnvVar.DID_AUTOBUILD_GOLANG, "true");
core.exportVariable(EnvVar.DID_AUTOBUILD_GOLANG, "true");
}
logger.endGroup();
}

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

@@ -17,7 +17,7 @@ import type { VersionInfo } from "./cli/types";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
import { Env, EnvVar, getEnv, ReadOnlyEnv } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -493,8 +493,9 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
export async function getCodeQLForTesting(
cmd = "codeql-for-testing",
logger: Logger = getRunnerLogger(true),
env: Env = getEnv(),
): Promise<CodeQL> {
return getCodeQLForCmd(logger, cmd, false);
return getCodeQLForCmd(logger, cmd, false, env);
}
/**
@@ -509,13 +510,14 @@ 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,
@@ -641,7 +643,7 @@ async function getCodeQLForCmd(
}
},
async runAutobuild(config: Config, language: Language) {
applyAutobuildAzurePipelinesTimeoutFix();
applyAutobuildAzurePipelinesTimeoutFix(env);
const autobuildCmd = path.join(
await this.resolveExtractor(language),
@@ -651,8 +653,11 @@ 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
@@ -684,7 +689,7 @@ async function getCodeQLForCmd(
},
async extractUsingBuildMode(config: Config, language: Language) {
if (config.buildMode === BuildMode.Autobuild) {
applyAutobuildAzurePipelinesTimeoutFix();
applyAutobuildAzurePipelinesTimeoutFix(env);
}
try {
await runCli(cmd, [
@@ -816,7 +821,7 @@ 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) {
@@ -1036,7 +1041,7 @@ 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();
@@ -1054,7 +1059,7 @@ async function getCodeQLForCmd(
}' by 'github/codeql-action/*@v${getActionVersion()}' in your code scanning workflow to ` +
"continue using this version of the CodeQL Action.",
);
exportEnvVar(EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING, "true");
core.exportVariable(EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING, "true");
}
return codeql;
}
@@ -1256,17 +1261,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

@@ -2,6 +2,7 @@ import * as fs from "fs";
import * as path from "path";
import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as yaml from "js-yaml";
import { ActionState } from "./action-common";
@@ -45,7 +46,7 @@ import {
makeTelemetryDiagnostic,
} from "./diagnostics";
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { EnvVar, exportEnvVar } from "./environment";
import { EnvVar } from "./environment";
import * as errorMessages from "./error-messages";
import { Feature, FeatureEnablement, FeatureWithoutCLI } from "./feature-flags";
import {
@@ -964,10 +965,10 @@ async function setCppTrapCachingEnvironmentVariables(
);
} else if (config.trapCaches[BuiltInLanguage.cpp]) {
logger.info("Enabling TRAP caching for C/C++.");
exportEnvVar(envVar, "true");
core.exportVariable(envVar, "true");
} else {
logger.debug(`Disabling TRAP caching for C/C++.`);
exportEnvVar(envVar, "false");
core.exportVariable(envVar, "false");
}
}
}

View File

@@ -11,7 +11,7 @@ import { dbIsFinalized } from "./analyze";
import { scanArtifactsForTokens } from "./artifact-scanner";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
import { EnvVar, exportEnvVar } from "./environment";
import { EnvVar } from "./environment";
import * as json from "./json";
import { Language } from "./languages";
import { Logger, withGroup } from "./logging";
@@ -330,7 +330,7 @@ export async function uploadArtifacts(
// some issues early.
if (isInTestMode()) {
await scanArtifactsForTokens(toUpload, logger);
exportEnvVar("CODEQL_ACTION_ARTIFACT_SCAN_FINISHED", "true");
core.exportVariable("CODEQL_ACTION_ARTIFACT_SCAN_FINISHED", "true");
}
const suffix = getArtifactSuffix(getOptionalInput("matrix"));

View File

@@ -1,12 +0,0 @@
import test from "ava";
import { Env, ReadOnlyEnv } from "./environment";
import { getTestEnv, setupTests } from "./testing-utils";
setupTests(test);
test("isTestingEnv() is true", (t) => {
t.true(new ReadOnlyEnv(process.env).isTestingEnv());
t.true(new Env(process.env).isTestingEnv());
t.true(getTestEnv().isTestingEnv());
});

View File

@@ -1,10 +1,3 @@
import * as core from "@actions/core";
/**
* This constant is set in `ava.setup.mjs` for tests.
*/
declare const __CODEQL_ACTION_TEST_ENV__: string | undefined;
/**
* Environment variables used by Default Setup to communicate the private registry proxy configuration.
*/
@@ -218,20 +211,8 @@ export enum ActionsEnvVars {
RUNNER_TOOL_CACHE = "RUNNER_TOOL_CACHE",
}
/** Environment variables which are not specific to CodeQL. */
export enum SystemEnvVar {
/**
* Used by Node and related tools to indicate what kind of environment we are running in.
*/
NODE_ENV = "NODE_ENV",
}
/** A type representing all known environment variables. */
export type KnownEnvVar =
| EnvVar
| ActionsEnvVars
| RegistryProxyVars
| SystemEnvVar;
export type KnownEnvVar = EnvVar | ActionsEnvVars | RegistryProxyVars;
/**
* Gets an environment variable, but throws an error if it is not set.
@@ -308,26 +289,6 @@ export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
public entries(): Array<[string, T]> {
return Object.entries(this.vars);
}
/**
* Gets a value indicating whether we should skip uploads of
* all kinds (SARIF results, status reports, DBs, ...).
*
* This is not guaranteed to be set in all test environments.
*/
public isSkippingUploadsInTests(): boolean {
return this.getOptional(EnvVar.TEST_MODE) === "true";
}
/**
* Gets a value indicative of whether we are in a testing environment
* by testing whether the value of the `NODE_ENV` variable is "test".
* This is expected to be the case if e.g. `ava` is running the tests
* or if this instance was constructed by `getTestEnv`.
*/
public isTestingEnv(): boolean {
return __CODEQL_ACTION_TEST_ENV__ === "unit-test";
}
}
/**
@@ -346,26 +307,6 @@ export class Env<
this.changed = true;
}
/**
* Wrapper around `core.exportVariable` which does not call `core.exportVariable`
* when running unit tests. This is important, because otherwise `core.exportVariable`
* sets environment variables for other steps in a workflow when we run unit tests in CI.
*
* @param name The name of the environment variable to set and export.
* @param val The value to set and export for the environment variable.
*/
public export(name: string, val: T): void {
// Setting the environment variable for this instance is always OK, including
// in tests, since we use fresh `Env` instances whenever needed. This allows
// tests to pass that rely on that part of the `core.exportVariable` behaviour.
this.set(name, val);
// Call `core.exportVariable` whenever we are not in a test environment.
if (!this.isTestingEnv()) {
core.exportVariable(name, val);
}
}
/** Gets a value indicating whether `set` was called at least once. */
public hasChanged(): boolean {
return this.changed;
@@ -376,34 +317,3 @@ export class Env<
export function getEnv(env: NodeJS.ProcessEnv = process.env): Env {
return new Env(env);
}
/**
* Returns whether we are in test mode. This is used by CodeQL Action PR checks.
*
* In test mode, we skip several uploads (SARIF results, status reports, DBs, ...).
*
* @deprecated
* The purpose of this function is ambiguous. Use `isSkippingUploadsInTests` on
* a `ReadOnlyEnv` instance instead for equivalent behaviour. Use `isTestingEnv`
* to determine if we are running in a unit test.
*/
export function isInTestMode(): boolean {
return getEnv().isSkippingUploadsInTests();
}
/**
* Wrapper around `core.exportVariable` which does not call `core.exportVariable`
* when running unit tests. This is important, because otherwise `core.exportVariable`
* sets environment variables for other steps in a workflow when we run unit tests in CI.
*
* @deprecated Use `export` on an `Env` instance instead.
*/
export function exportEnvVar(name: string, val: any): void {
const env = getEnv();
if (typeof val === "string") {
env.export(name, val);
} else {
env.export(name, JSON.stringify(val));
}
}

View File

@@ -170,6 +170,8 @@ export enum Feature {
*/
PerLanguageBundles = "per_language_bundles_v2",
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Whether we should fail early if we detect that traced Swift analysis is unsupported. */
SwiftSkipUnsupportedTracedAnalysis = "swift_skip_unsupported_traced_analysis",
/** Routes (some) API requests through the registry proxy. */
ProxyApiRequests = "proxy_api_requests",
/** Note that this currently only disables baseline file coverage information. */
@@ -466,6 +468,11 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_START_PROXY_USE_FEATURES_RELEASE",
minimumVersion: undefined,
},
[Feature.SwiftSkipUnsupportedTracedAnalysis]: {
defaultValue: false,
envVar: "CODEQL_ACTION_SWIFT_SKIP_UNSUPPORTED_TRACED_ANALYSIS",
minimumVersion: undefined,
},
[Feature.ToolsRepositoryProperty]: {
defaultValue: false,
envVar: "CODEQL_ACTION_TOOLS_REPOSITORY_PROPERTY",

35
src/fs.ts Normal file
View File

@@ -0,0 +1,35 @@
/**
* This module exports a `FileSystem` type which corresponds to the interface of the "fs" module.
*
* Functions which are parameterised over this type can then be passed a different implementation in tests:
*
* ```typescript
* import * as nodefs from "fs";
*
* function foo(fs: FileSystem = nodefs) {
* // Uses the real "fs" module by default, but can be given a different implementation.
* }
* ```
*
* The type can also be constrained to a subset of available operations. For example, in the following
* case we have a function that only needs `statSync`:
*
* ```
* function bar(fs: FileSystem<"statSync"> = nodefs) {
* // This function can only use `statSync`.
* }
* ```
*
* This is useful to define a clearer interface for what the function does and also only requires stubbing
* of the relevant functions.
*/
import * as fs from "fs";
/** Represents the names of operations exported from "fs". */
export type FileOperation = keyof typeof fs;
/** Represents the type of "fs", optionally filtered down to just `Ops`. */
export type FileSystem<Ops extends FileOperation = keyof typeof fs> = {
[Key in Ops]: (typeof fs)[Key];
};

View File

@@ -21,7 +21,7 @@ import {
DependencyCachingUsageReport,
getDependencyCacheUsage,
} from "./dependency-caching";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
import { EnvVar, getEnv } from "./environment";
import { initFeatures } from "./feature-flags";
import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper";
@@ -166,7 +166,7 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
let jobStatus: JobStatus;
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
exportEnvVar(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
core.exportVariable(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
jobStatus = JobStatus.SuccessStatus;
} else if (config !== undefined) {
// - We have computed a CodeQL config
@@ -191,7 +191,7 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
// This shouldn't be necessary, but in the odd case that we run more than one
// `init` post step, ensure the job status is consistent between them.
exportEnvVar(EnvVar.JOB_STATUS, jobStatus);
core.exportVariable(EnvVar.JOB_STATUS, jobStatus);
return jobStatus;
}

View File

@@ -56,6 +56,7 @@ import {
runDatabaseInitCluster,
} from "./init";
import { JavaEnvVars, BuiltInLanguage } from "./languages";
import { isSwiftCompatible } from "./languages/swift";
import { Logger, withGroupAsync } from "./logging";
import {
downloadOverlayBaseDatabaseFromCache,
@@ -93,6 +94,7 @@ import {
checkActionVersion,
getErrorMessage,
BuildMode,
getOptionalEnvVar,
} from "./util";
import { checkWorkflow } from "./workflow";
@@ -199,7 +201,7 @@ async function sendCompletedStatusReport(
}
async function run(
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
actionState: ActionState<["Base", "Logger", "Env", "Actions", "FS"]>,
) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -252,7 +254,7 @@ async function run(
);
const repositoryProperties = repositoryPropertiesResult.orElse({});
actionState.env.export(EnvVar.INIT_ACTION_HAS_RUN, "true");
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
// path.resolve() respects the intended semantics of source-root. If
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
@@ -368,7 +370,7 @@ async function run(
);
}
if (semver.lt(actualVer, publicPreview)) {
actionState.env.export(EnvVar.EXPERIMENTAL_FEATURES, "true");
core.exportVariable(EnvVar.EXPERIMENTAL_FEATURES, "true");
logger.info("Experimental Rust analysis enabled");
}
}
@@ -417,14 +419,7 @@ async function run(
logger,
});
if (
config.languages.includes(BuiltInLanguage.swift) &&
process.platform !== "darwin"
) {
throw new ConfigurationError(
`Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`,
);
}
await isSwiftCompatible(actionStateWithFeatures, config, codeql);
if (repositoryPropertiesResult.isFailure()) {
addNoLanguageDiagnostic(
@@ -516,9 +511,9 @@ async function run(
}
// Forward Go flags
const goFlags = actionState.env.getOptional("GOFLAGS");
const goFlags = process.env["GOFLAGS"];
if (goFlags) {
actionState.env.export("GOFLAGS", goFlags);
core.exportVariable("GOFLAGS", goFlags);
core.warning(
"Passing the GOFLAGS env parameter to the init action is deprecated. Please move this to the analyze action.",
);
@@ -564,7 +559,7 @@ async function run(
// Store the original location of our wrapper script somewhere where we can
// later retrieve it from and cross-check that it hasn't been changed.
actionState.env.export(EnvVar.GO_BINARY_LOCATION, goWrapperPath);
core.exportVariable(EnvVar.GO_BINARY_LOCATION, goWrapperPath);
} catch (e) {
logger.warning(
`Analyzing Go on Linux, but failed to install wrapper script. Tracing custom builds may fail: ${e}`,
@@ -573,7 +568,7 @@ async function run(
} else {
// Store the location of the original Go binary, so we can check that no setup tasks were performed after the
// `init` Action ran.
actionState.env.export(EnvVar.GO_BINARY_LOCATION, goBinaryPath);
core.exportVariable(EnvVar.GO_BINARY_LOCATION, goBinaryPath);
}
} catch (e) {
logger.warning(
@@ -608,12 +603,12 @@ async function run(
// threads it would ask extractors to use. See help text for the "--ram" and "--threads"
// options at https://codeql.github.com/docs/codeql-cli/manual/database-trace-command/
// for details.
actionState.env.export(
core.exportVariable(
"CODEQL_RAM",
process.env["CODEQL_RAM"] ||
getCodeQLMemoryLimit(getOptionalInput("ram"), logger).toString(),
);
actionState.env.export(
core.exportVariable(
"CODEQL_THREADS",
process.env["CODEQL_THREADS"] ||
getThreadsFlagValue(getOptionalInput("threads"), logger).toString(),
@@ -621,15 +616,12 @@ async function run(
// Disable Kotlin extractor if feature flag set
if (await features.getValue(Feature.DisableKotlinAnalysisEnabled)) {
actionState.env.export(
"CODEQL_EXTRACTOR_JAVA_AGENT_DISABLE_KOTLIN",
"true",
);
core.exportVariable("CODEQL_EXTRACTOR_JAVA_AGENT_DISABLE_KOTLIN", "true");
}
// Emergency override to force the CodeQL CLI back to the JGit-based Git backend.
if (await features.getValue(Feature.ForceJGit)) {
actionState.env.export("CODEQL_GIT_BACKEND", "jgit");
core.exportVariable("CODEQL_GIT_BACKEND", "jgit");
}
const kotlinLimitVar =
@@ -638,7 +630,7 @@ async function run(
(await codeQlVersionAtLeast(codeql, "2.20.3")) &&
!(await codeQlVersionAtLeast(codeql, "2.20.4"))
) {
actionState.env.export(kotlinLimitVar, "2.1.20");
core.exportVariable(kotlinLimitVar, "2.1.20");
}
// Restore dependency cache(s), if they exist.
@@ -687,7 +679,7 @@ async function run(
config.buildMode === BuildMode.None &&
config.languages.includes(BuiltInLanguage.java)
) {
actionState.env.export(
core.exportVariable(
EnvVar.JAVA_EXTRACTOR_MINIMIZE_DEPENDENCY_JARS,
"true",
);
@@ -745,7 +737,7 @@ async function run(
const tracerConfig = await getCombinedTracerConfig(logger, codeql, config);
if (tracerConfig !== undefined) {
for (const [key, value] of Object.entries(tracerConfig.env)) {
actionState.env.export(key, value);
core.exportVariable(key, value);
}
}
@@ -753,10 +745,10 @@ async function run(
if (await features.getValue(Feature.JavaNetworkDebugging)) {
// Get the existing value of `JAVA_TOOL_OPTIONS`, if any.
const existingJavaToolOptions =
actionState.env.getOptional(JavaEnvVars.JAVA_TOOL_OPTIONS) ?? "";
getOptionalEnvVar(JavaEnvVars.JAVA_TOOL_OPTIONS) || "";
// Add the network debugging options.
actionState.env.export(
core.exportVariable(
JavaEnvVars.JAVA_TOOL_OPTIONS,
`${existingJavaToolOptions} -Djavax.net.debug=all`,
);

View File

@@ -1,13 +1,14 @@
import * as fs from "fs";
import path from "path";
import * as core from "@actions/core";
import * as github from "@actions/github";
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { createStubCodeQL } from "./codeql";
import * as environment from "./environment";
import { ActionsEnvVars } from "./environment";
import { Feature } from "./feature-flags";
import {
checkPacksForOverlayCompatibility,
@@ -84,7 +85,7 @@ for (const { runnerEnv, ErrorConstructor, message } of [
`cleanupDatabaseClusterDirectory throws a ${ErrorConstructor.name} when cleanup fails on ${runnerEnv} runner`,
async (t) => {
await withTmpDir(async (tmpDir: string) => {
process.env[environment.ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnv;
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnv;
const dbLocation = path.resolve(tmpDir, "dbs");
fs.mkdirSync(dbLocation, { recursive: true });
@@ -545,7 +546,7 @@ test.serial(
test.serial(
"file coverage deprecation warning for org-owned repo with default setup recommends repo property",
(t) => {
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(true);
github.context.payload = {
repository: {
@@ -565,14 +566,14 @@ test.serial(
'with the name `github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to ' +
"`true` in the repository's settings.",
);
t.true(exportEnvVarStub.calledOnce);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for org-owned repo with advanced setup recommends env var and repo property",
(t) => {
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(false);
github.context.payload = {
repository: {
@@ -593,14 +594,14 @@ test.serial(
'with the name `github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to ' +
"`true` in the repository's settings.",
);
t.true(exportEnvVarStub.calledOnce);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for user-owned repo with default setup recommends advanced setup",
(t) => {
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(true);
github.context.payload = {
repository: {
@@ -619,14 +620,14 @@ test.serial(
"To opt out of this change, switch to an advanced setup workflow and " +
"set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportEnvVarStub.calledOnce);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for user-owned repo with advanced setup recommends env var",
(t) => {
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(false);
github.context.payload = {
repository: {
@@ -644,14 +645,14 @@ test.serial(
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportEnvVarStub.calledOnce);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for unknown owner type with default setup recommends advanced setup",
(t) => {
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(true);
github.context.payload = { repository: undefined };
const messages: LoggedMessage[] = [];
@@ -665,14 +666,14 @@ test.serial(
"To opt out of this change, switch to an advanced setup workflow and " +
"set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportEnvVarStub.calledOnce);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for unknown owner type with advanced setup recommends env var",
(t) => {
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(false);
github.context.payload = { repository: undefined };
const messages: LoggedMessage[] = [];
@@ -685,7 +686,7 @@ test.serial(
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportEnvVarStub.calledOnce);
t.true(exportVariableStub.calledOnce);
},
);
@@ -694,10 +695,10 @@ test.serial(
(t) => {
process.env["CODEQL_ACTION_DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION"] =
"true";
const exportEnvVarStub = sinon.stub(environment, "exportEnvVar");
const exportVariableStub = sinon.stub(core, "exportVariable");
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 0);
t.true(exportEnvVarStub.notCalled);
t.true(exportVariableStub.notCalled);
},
);

View File

@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
@@ -16,7 +17,7 @@ import {
import { GitHubApiDetails } from "./api-client";
import { CodeQL, setupCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { EnvVar, exportEnvVar } from "./environment";
import { EnvVar } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -410,5 +411,5 @@ export function logFileCoverageOnPrsDeprecationWarning(logger: Logger): void {
}
logger.warning(message);
exportEnvVar(EnvVar.DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION, "true");
core.exportVariable(EnvVar.DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION, "true");
}

589
src/languages/swift.test.ts Normal file
View File

@@ -0,0 +1,589 @@
import * as fs from "fs";
import test from "ava";
import * as sinon from "sinon";
import { getCodeQLForTesting } from "../codeql";
import * as diagnostics from "../diagnostics";
import { ActionsEnvVars } from "../environment";
import { Feature } from "../feature-flags";
import { FileSystem } from "../fs";
import {
checkExpectedLogMessages,
checkUnexpectedLogMessages,
createFeatures,
createTestConfig,
getTestEnv,
initAllState,
makeVersionInfo,
RecordingLogger,
setupTests,
} from "../testing-utils";
import { ToolsFeature } from "../tools-features";
import { withTmpDir } from "../util";
import {
isSwiftCompatible,
XCODE_SELECT_LINK_PATH,
xcodeVersion,
} from "./swift";
import { BuiltInLanguage } from ".";
setupTests(test);
type RequiredFS = FileSystem<"statSync" | "readlinkSync">;
/**
* Sets up a suitable mock `FileSystem` for use with `xcodeVersion`.
*
* @param statSyncResult The result of `statSync`.
* @param readlinkSyncResult The result of `readlinkSync`.
*
* @returns The mocked `FileSystem` and stubs.
*/
function mockFs(
statSyncResult: boolean | Error,
readlinkSyncResult: string = "",
) {
const stubbedFs: RequiredFS = {
statSync: fs.statSync,
readlinkSync: fs.readlinkSync,
};
const statSync = sinon.stub(stubbedFs, "statSync");
if (typeof statSyncResult === "boolean") {
statSync.returns({ isSymbolicLink: () => statSyncResult } as fs.Stats);
} else {
statSync.throws(new Error("ENOENT"));
}
const readlinkSync = sinon
.stub(stubbedFs, "readlinkSync")
.returns(readlinkSyncResult);
return { stubbedFs, statSync, readlinkSync };
}
test("xcodeVersion returns undefined if symlink doesn't exist", (t) => {
const logger = new RecordingLogger();
const { stubbedFs, statSync } = mockFs(new Error("ENOENT"));
t.is(xcodeVersion(logger, stubbedFs), undefined);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: ENOENT",
]);
});
test("xcodeVersion returns undefined if file is not a symlink", (t) => {
const logger = new RecordingLogger();
const { stubbedFs, statSync } = mockFs(false);
t.is(xcodeVersion(logger, stubbedFs), undefined);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"exists, but is not a symbolic link",
]);
});
test("xcodeVersion returns undefined if resolving the symlink returns nothing", (t) => {
const logger = new RecordingLogger();
const { stubbedFs, statSync, readlinkSync } = mockFs(true);
t.is(xcodeVersion(logger, stubbedFs), undefined);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"unexpectedly returned nothing",
]);
});
test("xcodeVersion returns undefined if resolved path doesn't include pattern", (t) => {
const logger = new RecordingLogger();
const { stubbedFs, statSync, readlinkSync } = mockFs(
true,
"/Applications/Xcode.app/Contents/Developer",
);
t.is(xcodeVersion(logger, stubbedFs), undefined);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"does not contain expected pattern",
]);
});
test("xcodeVersion returns undefined if match can't be parsed", (t) => {
const logger = new RecordingLogger();
const { stubbedFs, statSync, readlinkSync } = mockFs(
true,
"/Applications/Xcode_00.0.app/Contents/Developer",
);
t.is(xcodeVersion(logger, stubbedFs), undefined);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"Couldn't parse '00.0' as a semantic version.",
]);
});
test("xcodeVersion returns version from resolved path", (t) => {
const logger = new RecordingLogger();
const { stubbedFs, statSync, readlinkSync } = mockFs(
true,
"/Applications/Xcode_16.4.app/Contents/Developer",
);
const result = xcodeVersion(logger, stubbedFs);
if (t.truthy(result)) {
t.is(result.major, 16);
t.is(result.minor, 4);
}
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
});
test("isSwiftCompatible doesn't throw for non-Swift languages", async (t) => {
for (const language of Object.values(BuiltInLanguage)) {
if (language === BuiltInLanguage.swift) {
continue;
}
const codeql = await getCodeQLForTesting();
await t.notThrowsAsync(
isSwiftCompatible(
initAllState(),
createTestConfig({ languages: [language] }),
codeql,
),
);
}
});
test("isSwiftCompatible doesn't throw for Swift if CLI supports swiftSupportsAllPlatforms", async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env);
const supportsFeature = sinon
.stub(codeql, "supportsFeature")
.withArgs(ToolsFeature.SwiftSupportsAllPlatforms)
.resolves(true);
await t.notThrowsAsync(
isSwiftCompatible(
initAllState({ platform: "darwin", env, logger }),
createTestConfig({ languages: [BuiltInLanguage.swift] }),
codeql,
),
);
t.is(supportsFeature.callCount, 1);
t.deepEqual(supportsFeature.args[0], [
ToolsFeature.SwiftSupportsAllPlatforms,
]);
}));
test("isSwiftCompatible doesn't throw for Swift on darwin", async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
await t.notThrowsAsync(
isSwiftCompatible(
initAllState({ platform: "darwin", env, logger }),
createTestConfig({ languages: [BuiltInLanguage.swift] }),
codeql,
),
);
}));
const nonDarwinPlatforms: NodeJS.Platform[] = ["linux", "win32"];
for (const nonDarwinPlatform of nonDarwinPlatforms) {
test(`isSwiftCompatible throws for Swift on ${nonDarwinPlatform}`, async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLForTesting(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
await t.throwsAsync(
isSwiftCompatible(
initAllState({ platform: nonDarwinPlatform, env, logger }),
createTestConfig({ languages: [BuiltInLanguage.swift] }),
codeql,
),
);
}));
}
test("isSwiftCompatible warns if version string is not a semver", async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
await isSwiftCompatible(
initAllState({
logger,
platform: "darwin",
osRelease: "unexpected",
env,
}),
createTestConfig({ languages: [BuiltInLanguage.swift] }),
codeql,
);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine version of macOS, got: unexpected",
]);
}));
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
test.serial(
"isSwiftCompatible logs and adds diagnostic if macOS version is unsupported",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const codeql = await getCodeQLForTesting(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
await isSwiftCompatible(
initAllState({
logger,
platform: "darwin",
osRelease: "27.0.0",
env,
}),
config,
codeql,
);
checkExpectedLogMessages(t, logger.messages, [
"Traced Swift analysis is not supported on macOS 27",
]);
t.is(addDiagnostic.callCount, 1);
t.like(addDiagnostic.args[0], [
config,
BuiltInLanguage.swift,
{
attributes: {
languages: [BuiltInLanguage.swift],
macOSVersion: "27.0.0",
},
severity: "warning",
source: {
id: "codeql-action/unsupported-traced-swift-analysis-macos",
name: "Traced Swift analysis is not supported on this version of macOS",
},
visibility: {
cliSummaryTable: true,
statusPage: true,
telemetry: true,
},
} satisfies Partial<diagnostics.DiagnosticMessage>,
]);
}),
);
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
test.serial(
"isSwiftCompatible throws if macOS version is unsupported and FF is enabled",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const features = createFeatures([
Feature.SwiftSkipUnsupportedTracedAnalysis,
]);
const codeql = await getCodeQLForTesting(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
await t.throwsAsync(
isSwiftCompatible(
initAllState({
logger,
platform: "darwin",
osRelease: "27.0.0",
env,
features,
}),
config,
codeql,
),
);
t.is(addDiagnostic.callCount, 1);
t.like(addDiagnostic.args[0], [
config,
BuiltInLanguage.swift,
{
attributes: {
languages: [BuiltInLanguage.swift],
macOSVersion: "27.0.0",
},
severity: "error",
source: {
id: "codeql-action/unsupported-traced-swift-analysis-macos",
name: "Traced Swift analysis is not supported on this version of macOS",
},
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(
"isSwiftCompatible doesn't add a diagnostic if Xcode version is supported",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const { stubbedFs, statSync, readlinkSync } = mockFs(
true,
"/Applications/Xcode_26.0.app/Contents/Developer",
);
const codeql = await getCodeQLForTesting(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
await isSwiftCompatible(
initAllState({
logger,
platform: "darwin",
osRelease: "26.0.0",
env,
fs: stubbedFs as FileSystem,
}),
config,
codeql,
);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkUnexpectedLogMessages(t, logger.messages, [
"Traced Swift analysis is not supported on Xcode 27",
]);
t.is(addDiagnostic.callCount, 0);
}),
);
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
test.serial(
"isSwiftCompatible logs and adds diagnostic if Xcode version is unsupported",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const { stubbedFs, statSync, readlinkSync } = mockFs(
true,
"/Applications/Xcode_27.0.app/Contents/Developer",
);
const codeql = await getCodeQLForTesting(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
await isSwiftCompatible(
initAllState({
logger,
platform: "darwin",
osRelease: "26.0.0",
env,
fs: stubbedFs as FileSystem,
}),
config,
codeql,
);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
checkExpectedLogMessages(t, logger.messages, [
"Traced Swift analysis is not supported on Xcode 27",
]);
t.is(addDiagnostic.callCount, 1);
t.like(addDiagnostic.args[0], [
config,
BuiltInLanguage.swift,
{
attributes: {
languages: [BuiltInLanguage.swift],
xcodeVersion: "27.0.0",
},
severity: "warning",
source: {
id: "codeql-action/unsupported-traced-swift-analysis-xcode",
name: "Traced Swift analysis is not supported on this version of Xcode",
},
visibility: {
cliSummaryTable: true,
statusPage: true,
telemetry: true,
},
} satisfies Partial<diagnostics.DiagnosticMessage>,
]);
}),
);
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
test.serial(
"isSwiftCompatible throws if Xcode version is unsupported and FF is enabled",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const env = getTestEnv();
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
const { stubbedFs, statSync, readlinkSync } = mockFs(
true,
"/Applications/Xcode_27.0.app/Contents/Developer",
);
const features = createFeatures([
Feature.SwiftSkipUnsupportedTracedAnalysis,
]);
const codeql = await getCodeQLForTesting(
"codeql-for-testing",
logger,
env,
);
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
await t.throwsAsync(
isSwiftCompatible(
initAllState({
logger,
platform: "darwin",
osRelease: "26.0.0",
env,
features,
fs: stubbedFs as FileSystem,
}),
config,
codeql,
),
);
t.is(statSync.callCount, 1);
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(addDiagnostic.callCount, 1);
t.like(addDiagnostic.args[0], [
config,
BuiltInLanguage.swift,
{
attributes: {
languages: [BuiltInLanguage.swift],
xcodeVersion: "27.0.0",
},
severity: "error",
source: {
id: "codeql-action/unsupported-traced-swift-analysis-xcode",
name: "Traced Swift analysis is not supported on this version of Xcode",
},
visibility: {
cliSummaryTable: true,
statusPage: true,
telemetry: true,
},
} satisfies Partial<diagnostics.DiagnosticMessage>,
]);
}),
);

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

@@ -0,0 +1,218 @@
import * as semver from "semver";
import { ActionState, Logger } from "../action-common";
import { CodeQL } from "../codeql";
import { Config } from "../config-utils";
import { addDiagnostic, makeDiagnostic } from "../diagnostics";
import { Feature } from "../feature-flags";
import { FileSystem } from "../fs";
import { macOSVersion } from "../platform";
import { ToolsFeature } from "../tools-features";
import { ConfigurationError, getErrorMessage } from "../util";
import { BuiltInLanguage } from ".";
/** The static path we check for a symbolic link to the (dynamic) Xcode location. */
export const XCODE_SELECT_LINK_PATH = "/private/var/db/xcode_select_link";
/** The pattern we expect to find in the Xcode path. */
export const XCODE_APP_FILENAME_PATTERN = new RegExp(
/(?<filename>Xcode_(?<majorMinor>\d+.\d+).app)/,
);
/** macOS 27 and above do not support traced extraction for Swift. */
export const SWIFT_TRACED_UNSUPPORTED_MACOS = 27;
/** Xcode 27 and above do not support traced extraction for Swift. */
export const SWIFT_TRACED_UNSUPPORTED_XCODE = 27;
/**
* Tries to determine the version of Xcode that is installed.
*
* @param logger The logger to use.
* @returns The Xcode version or `undefined` if it couldn't be determined.
*/
export function xcodeVersion(
logger: Logger,
fs: FileSystem<"statSync" | "readlinkSync">,
): semver.SemVer | undefined {
try {
// Stat the expected symbolic link to check that it exists and is a symbolic link.
// The `readlinkSync` call below returns an empty string in either case and so
// this check allows us to distinguish between the two cases.
const stats = fs.statSync(XCODE_SELECT_LINK_PATH);
if (!stats.isSymbolicLink()) {
logger.warning(
`${XCODE_SELECT_LINK_PATH} exists, but is not a symbolic link.`,
);
return undefined;
}
// Read what the symbolic link points to.
const xcodePath = fs.readlinkSync(XCODE_SELECT_LINK_PATH);
if (xcodePath === "") {
logger.warning(
`Resolving ${XCODE_SELECT_LINK_PATH} unexpectedly returned nothing.`,
);
return undefined;
}
// Try to extract the version from the path.
const matchResult = xcodePath.match(XCODE_APP_FILENAME_PATTERN);
if (matchResult?.groups === undefined) {
logger.warning(
`Xcode path '${xcodePath}' does not contain expected pattern.`,
);
return undefined;
}
const majorMinor = matchResult.groups["majorMinor"];
const version = semver.coerce(majorMinor);
if (version === null) {
logger.warning(`Couldn't parse '${majorMinor}' as a semantic version.`);
return undefined;
}
return version;
} catch (err) {
logger.warning(
`Unable to determine Xcode version: ${getErrorMessage(err)}`,
);
return undefined;
}
}
/**
* Creates a diagnostic indicating that `version` of `product` is unsupported for traced Swift analysis.
* Depending on `skipUnsupportedTracedAnalysis`, this function then either throws a {@link ConfigurationError}
* or logs the problem as a warning.
*
* @param logger The logger to use.
* @param config The CodeQL Action configuration.
* @param skipUnsupportedTracedAnalysis Whether this is a fatal error.
* @param product The product that the version is unsupported of.
* @param version The unsupported version.
*/
function handleUnsupportedVersion(
logger: Logger,
config: Config,
skipUnsupportedTracedAnalysis: boolean,
product: "macOS" | "Xcode",
version: semver.SemVer,
) {
const baseMessage = [
`Traced Swift analysis is not supported on ${product} ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`,
`Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`,
`and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.`,
].join(" ");
const attributeName = product === "macOS" ? "macOSVersion" : "xcodeVersion";
// Create a diagnostic that will show up on the TSP.
addDiagnostic(
config,
BuiltInLanguage.swift,
makeDiagnostic(
`codeql-action/unsupported-traced-swift-analysis-${product.toLowerCase()}`,
`Traced Swift analysis is not supported on this version of ${product}`,
{
attributes: {
languages: config.languages,
[attributeName]: version.toString(),
},
markdownMessage: baseMessage,
severity: skipUnsupportedTracedAnalysis ? "error" : "warning",
visibility: {
cliSummaryTable: true,
statusPage: true,
telemetry: true,
},
},
),
);
// Throw an error to abort the analysis if the FF is enabled or log the message.
if (skipUnsupportedTracedAnalysis) {
// ConfigurationErrors are converted to the "aborted" status by the exception handler
// in `init-action.ts` that guards the call to `isSwiftCompatible`.
throw new ConfigurationError(baseMessage);
} else {
// This will also show up as a workflow annotation.
logger.warning(baseMessage);
}
}
/**
* Determines whether we can run a Swift analysis on the current runner.
*
* @param action The Action state.
* @param config The Action configuration.
*
* @throws {ConfigurationError} If Swift analysis is not possible on the current runner.
* @returns True if we can run a Swift analysis.
*/
export async function isSwiftCompatible(
action: ActionState<["Base", "Logger", "FeatureFlags", "FS"]>,
config: Config,
codeql: CodeQL,
) {
// The checks are not relevant if we are not trying to analyse Swift.
if (!config.languages.includes(BuiltInLanguage.swift)) {
return;
}
// Skip the checks if the `swiftSupportsAllPlatforms` feature is supported by the CLI.
// This is a forward-looking measure that allows a future CLI update to disable these
// platform checks in the Action when they shouldn't be enforced anymore.
if (await codeql.supportsFeature(ToolsFeature.SwiftSupportsAllPlatforms)) {
return;
}
// Try to get the macOS version.
const version = macOSVersion(action);
// If `version` is undefined, then we are not on macOS.
if (version === undefined) {
throw new ConfigurationError(
`Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`,
);
}
const skipUnsupportedTracedAnalysis = await action.features.getValue(
Feature.SwiftSkipUnsupportedTracedAnalysis,
);
if (typeof version === "string") {
// If we got a string, we are on macOS but couldn't parse the version string.
action.logger.warning(
`Unable to determine version of macOS, got: ${version}`,
);
} else if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) {
handleUnsupportedVersion(
action.logger,
config,
skipUnsupportedTracedAnalysis,
"macOS",
version,
);
}
// Determining whether the Xcode version is supported only makes sense on macOS, so we only do it
// after determining that we are running on macOS.
const xcodeVer = xcodeVersion(action.logger, action.fs);
if (
xcodeVer !== undefined &&
xcodeVer.major >= SWIFT_TRACED_UNSUPPORTED_XCODE
) {
handleUnsupportedVersion(
action.logger,
config,
skipUnsupportedTracedAnalysis,
"Xcode",
xcodeVer,
);
}
}

View File

@@ -8,7 +8,6 @@ import * as sinon from "sinon";
import * as actionsUtil from "../actions-util";
import * as apiClient from "../api-client";
import type { ResolveDatabaseOutput } from "../codeql";
import * as environment from "../environment";
import * as gitUtils from "../git-utils";
import { BuiltInLanguage } from "../languages";
import { getRunnerLogger } from "../logging";
@@ -83,7 +82,7 @@ const testDownloadOverlayBaseDatabaseFromCache = makeMacro({
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(environment, "isInTestMode").returns(testCase.isInTestMode);
sinon.stub(utils, "isInTestMode").returns(testCase.isInTestMode);
if (testCase.restoreCacheResult instanceof Error) {
sinon

View File

@@ -1,6 +1,7 @@
import test from "ava";
import { BundlePlatform, getBundlePlatform } from "./platform";
import { BundlePlatform, getBundlePlatform, macOSVersion } from "./platform";
import { initAllState } from "./testing-utils";
for (const [platform, arch, expected] of [
["linux", "x64", BundlePlatform.Linux64],
@@ -16,3 +17,40 @@ for (const [platform, arch, expected] of [
t.is(getBundlePlatform(platform, arch), expected);
});
}
const platforms: NodeJS.Platform[] = ["linux", "win32", "freebsd"];
for (const platform of platforms) {
test(`macOSVersion returns undefined on ${platform}`, (t) => {
t.is(macOSVersion(initAllState({ platform })), undefined);
});
}
test("macOSVersion returns raw string if semver parsing fails", (t) => {
const invalidSemVer = "sealOS-2026";
t.is(
macOSVersion(
initAllState({ platform: "darwin", osRelease: invalidSemVer }),
),
invalidSemVer,
);
});
test("macOSVersion returns semver if parsing succeeds", (t) => {
const validSemVer = "27.0.1";
const version = macOSVersion(
initAllState({ platform: "darwin", osRelease: validSemVer }),
);
// Check that `version` is not undefined and narrow the type; throws if undefined.
if (t.truthy(version)) {
// Check that it's also not just a string.
t.not(typeof version, "string");
// Should be an object with the expected properties.
t.is(typeof version, "object");
t.is(version["major"], 27);
t.is(version["minor"], 0);
t.is(version["patch"], 1);
}
});

View File

@@ -1,3 +1,7 @@
import * as semver from "semver";
import type { ActionState } from "./action-common";
/** Platform identifiers used in CodeQL bundle asset names. */
export enum BundlePlatform {
Linux64 = "linux64",
@@ -24,3 +28,28 @@ export function getBundlePlatform(
return undefined;
}
}
/**
* Tries to determine the version of macOS.
*
* @returns
* The version as either a semantic version object, the raw version string
* if it is not a semantic version, or `undefined` if we are not on macOS.
*/
export function macOSVersion(
action: ActionState<["Base"]>,
): semver.SemVer | string | undefined {
// Skip if we are not running on macOS.
if (action.platform !== "darwin") {
return undefined;
}
// Try to parse the OS version string.
const version = semver.parse(action.osRelease);
if (version === null) {
return action.osRelease;
}
return version;
}

View File

@@ -184,7 +184,7 @@ async function run(
core.setOutput("codeql-path", codeql.getPath());
core.setOutput("codeql-version", (await codeql.getVersion()).version);
actionState.env.export(EnvVar.SETUP_CODEQL_ACTION_HAS_RUN, "true");
core.exportVariable(EnvVar.SETUP_CODEQL_ACTION_HAS_RUN, "true");
} catch (unwrappedError) {
const error = wrapError(unwrappedError);
core.setFailed(error.message);

View File

@@ -3,6 +3,7 @@ import { OutgoingHttpHeaders } from "http";
import * as path from "path";
import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as toolcache from "@actions/tool-cache";
import { default as deepEqual } from "fast-deep-equal";
import * as semver from "semver";
@@ -28,7 +29,7 @@ import {
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { EnvVar, exportEnvVar, getEnv } from "./environment";
import { EnvVar, getEnv } from "./environment";
import {
CODEQL_VERSION_ZSTD_BUNDLE,
CodeQLDefaultVersionInfo,
@@ -1070,7 +1071,7 @@ export async function setupCodeQLBundle(
// Record that this job now has a copy of the CodeQL tools, so that a later step doesn't delete
// the toolcache out from under the path we are about to return.
exportEnvVar(EnvVar.HAS_SET_UP_CODEQL, "true");
core.exportVariable(EnvVar.HAS_SET_UP_CODEQL, "true");
return {
codeqlFolder,

View File

@@ -23,13 +23,7 @@ import type { ComputedInput, InputName } from "./config/inputs";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
import { DocUrl } from "./doc-url";
import {
EnvVar,
getEnv,
ReadOnlyEnv,
RegistryProxyVars,
exportEnvVar,
} from "./environment";
import { EnvVar, getEnv, ReadOnlyEnv, RegistryProxyVars } from "./environment";
import { getRef } from "./git-utils";
import * as json from "./json";
import type { Logger } from "./logging";
@@ -77,7 +71,9 @@ export function getDisplayActionName(actionName: ActionName): string {
* environment and returns it.
* If a new UUID is generated, it is also exported as an environment variable.
*/
export function getJobUUID(action: ActionState<["Logger", "Env", "Actions"]>) {
export function getJobUUID(
action: ActionState<["Logger", "ReadOnlyEnv", "Actions"]>,
) {
// Check if we already have a UUID for the analysis and return it if so.
const existingJobRunUuid = action.env.getOptional(EnvVar.JOB_RUN_UUID);
@@ -90,7 +86,7 @@ export function getJobUUID(action: ActionState<["Logger", "Env", "Actions"]>) {
const jobRunUuid = uuid.v4();
action.logger.info(`Job run UUID is ${jobRunUuid}.`);
action.env.export(EnvVar.JOB_RUN_UUID, jobRunUuid);
action.actions.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
return jobRunUuid;
}
@@ -272,12 +268,12 @@ export function getJobStatusDisplayName(status: JobStatus): string {
*/
function setJobStatusIfUnsuccessful(actionStatus: ActionStatus) {
if (actionStatus === "user-error") {
exportEnvVar(
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigErrorStatus,
);
} else if (actionStatus === "failure" || actionStatus === "aborted") {
exportEnvVar(
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.FailureStatus,
);
@@ -380,7 +376,7 @@ export async function createStatusReportBase(
let workflowStartedAt = process.env[EnvVar.WORKFLOW_STARTED_AT];
if (workflowStartedAt === undefined) {
workflowStartedAt = actionStartedAt.toISOString();
exportEnvVar(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
}
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const codeQlCliVersion = getCachedCodeQlVersion(
@@ -392,7 +388,7 @@ export async function createStatusReportBase(
// re-export the testing environment variable so that it is available to subsequent steps,
// even if it was only set for this step
if (testingEnvironment) {
exportEnvVar(EnvVar.TESTING_ENVIRONMENT, testingEnvironment);
core.exportVariable(EnvVar.TESTING_ENVIRONMENT, testingEnvironment);
}
const isSteadyStateDefaultSetupRun =
process.env["CODE_SCANNING_IS_STEADY_STATE_DEFAULT_SETUP"] === "true";

View File

@@ -1,4 +1,6 @@
import * as fs from "fs";
import { TextDecoder } from "node:util";
import * as os from "os";
import path from "path";
import * as github from "@actions/github";
@@ -34,6 +36,7 @@ import { Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { getBundlePlatform } from "./platform";
import { ActionName } from "./status-report";
import { ToolsFeature } from "./tools-features";
import {
DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME,
@@ -202,6 +205,10 @@ class TestActionsEnv implements ActionsEnv {
public getOptionalInput(_name: string): string | undefined {
return undefined;
}
public exportVariable(name: string, value: string): void {
this.env.set(name, value);
}
}
/**
@@ -220,6 +227,7 @@ type AllState = [
"Actions",
"Api",
"FeatureFlags",
"FS",
];
/** Initialise a fresh `ActionState<AllState>` value. */
@@ -232,11 +240,13 @@ export function initAllState(
startedAt: new Date(),
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
logger: new RecordingLogger(),
env,
actions: getTestActionsEnv(env),
apiClient: github.getOctokit("123"),
features: createFeatures([]),
fs,
...overrides,
};
}
@@ -870,7 +880,7 @@ export function mockLanguagesInRepo(languages: string[]) {
*/
export const makeVersionInfo = (
version: string,
features?: { [name: string]: boolean },
features?: { [key in ToolsFeature]?: boolean },
overlayVersion?: number,
): VersionInfo => ({
version,

View File

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

View File

@@ -14,7 +14,7 @@ import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import { getConfig } from "./config-utils";
import { readDiffRangesJsonFile } from "./diff-informed-analysis-utils";
import { EnvVar, exportEnvVar } from "./environment";
import { EnvVar } from "./environment";
import { FeatureEnablement } from "./feature-flags";
import * as fingerprints from "./fingerprints";
import * as gitUtils from "./git-utils";
@@ -126,7 +126,7 @@ async function combineSarifFilesUsingCLI(
logger.warning(
`Uploading multiple SARIF runs with the same category is deprecated ${deprecationWarningMessage}. Please update your workflow to upload a single run per category. ${deprecationMoreInformationMessage}`,
);
exportEnvVar("CODEQL_MERGE_SARIF_DEPRECATION_WARNING", "true");
core.exportVariable("CODEQL_MERGE_SARIF_DEPRECATION_WARNING", "true");
}
// If not, use the naive method of combining the files.
@@ -1032,7 +1032,7 @@ export function validateUniqueCategory(
`Category: (${id ? id : "none"}) Tool: (${tool ? tool : "none"})`,
);
}
exportEnvVar(sentinelEnvVar, sentinelEnvVar);
core.exportVariable(sentinelEnvVar, sentinelEnvVar);
}
}

View File

@@ -14,23 +14,13 @@ import * as apiCompatibility from "./api-compatibility.json";
import type { CodeQL } from "./codeql";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import {
EnvVar,
getRequiredEnvParam,
isInTestMode,
exportEnvVar,
} from "./environment";
import { EnvVar, getRequiredEnvParam } from "./environment";
import * as json from "./json";
import { Language } from "./languages";
import { Logger } from "./logging";
// Re-export for backwards compatibility to avoid updating a lot of imports elsewhere.
export {
getRequiredEnvParam,
getOptionalEnvVar,
getEnv,
isInTestMode,
} from "./environment";
export { getRequiredEnvParam, getOptionalEnvVar, getEnv } from "./environment";
/**
* The name of the file containing the base database OIDs, as stored in the
@@ -552,7 +542,7 @@ export function checkGitHubVersionInRange(
);
}
hasBeenWarnedAboutVersion = true;
exportEnvVar(CODEQL_ACTION_WARNED_ABOUT_VERSION_ENV_VAR, true);
core.exportVariable(CODEQL_ACTION_WARNED_ABOUT_VERSION_ENV_VAR, true);
}
export enum DisallowedAPIVersionReason {
@@ -596,11 +586,11 @@ export function assertNever(value: never): never {
* knowing what version of CodeQL we're running.
*/
export function initializeEnvironment(version: string) {
exportEnvVar(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
exportEnvVar(EnvVar.FEATURE_SANDWICH, "false");
exportEnvVar(EnvVar.FEATURE_SARIF_COMBINE, "true");
exportEnvVar(EnvVar.FEATURE_WILL_UPLOAD, "true");
exportEnvVar(EnvVar.VERSION, version);
core.exportVariable(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
core.exportVariable(EnvVar.FEATURE_SANDWICH, "false");
core.exportVariable(EnvVar.FEATURE_SARIF_COMBINE, "true");
core.exportVariable(EnvVar.FEATURE_WILL_UPLOAD, "true");
core.exportVariable(EnvVar.VERSION, version);
}
export class HTTPError extends Error {
@@ -721,6 +711,15 @@ export function isGoodVersion(versionSpec: string) {
return !BROKEN_VERSIONS.includes(versionSpec);
}
/**
* Returns whether we are in test mode. This is used by CodeQL Action PR checks.
*
* In test mode, we skip several uploads (SARIF results, status reports, DBs, ...).
*/
export function isInTestMode(): boolean {
return process.env[EnvVar.TEST_MODE] === "true";
}
/**
* Returns whether we specifically want to skip uploading SARIF files.
*/
@@ -949,7 +948,7 @@ export async function checkDiskUsage(
} else {
logger.debug(message);
}
exportEnvVar(EnvVar.HAS_WARNED_ABOUT_DISK_SPACE, "true");
core.exportVariable(EnvVar.HAS_WARNED_ABOUT_DISK_SPACE, "true");
}
return {
numAvailableBytes: diskUsage.bavail * blockSizeInBytes,
@@ -998,7 +997,7 @@ export function checkActionVersion(
"https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/",
);
// set LOG_VERSION_DEPRECATION env var to prevent the warning from being logged multiple times
exportEnvVar(EnvVar.LOG_VERSION_DEPRECATION, "true");
core.exportVariable(EnvVar.LOG_VERSION_DEPRECATION, "true");
}
}
}