diff --git a/CHANGELOG.md b/CHANGELOG.md index e158d0af0..52f0ec6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th ## [UNRELEASED] -No user facing changes. +- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. [#3956](https://github.com/github/codeql-action/pull/3956) ## 4.37.0 - 08 Jul 2026 diff --git a/lib/entry-points.js b/lib/entry-points.js index 664f590f7..92cbe4dae 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -141462,6 +141462,61 @@ var toolrunner = __toESM(require_toolrunner()); var github = __toESM(require_github()); var io2 = __toESM(require_io()); +// src/environment.ts +function getRequiredEnvVar(env, paramName) { + const value = env[paramName]; + if (value === void 0 || value.length === 0) { + throw new Error(`${paramName} environment variable must be set`); + } + return value; +} +function getRequiredEnvParam(paramName) { + return getRequiredEnvVar(process.env, paramName); +} +function getOptionalEnvVarFrom(env, paramName) { + const value = env[paramName]; + if (value?.trim().length === 0) { + return void 0; + } + return value; +} +function getOptionalEnvVar(paramName) { + return getOptionalEnvVarFrom(process.env, paramName); +} +var ReadOnlyEnv = class { + constructor(vars) { + this.vars = vars; + } + vars; + /** Tries to get the value for `name` and throws if there isn't one. */ + getRequired(name) { + return getRequiredEnvVar(this.vars, name); + } + /** Gets the value for `name`, or `undefined` if it isn't set or empty. */ + getOptional(name) { + return getOptionalEnvVarFrom(this.vars, name); + } + /** Gets the entries of the underlying `ProcessEnv`. */ + entries() { + return Object.entries(this.vars); + } +}; +var Env = class extends ReadOnlyEnv { + changed = false; + /** Sets an environment variable. */ + set(name, value) { + this.vars[name] = value; + this.changed = true; + } + /** Gets a value indicating whether `set` was called at least once. */ + hasChanged() { + return this.changed; + } +}; +function getEnv(env = process.env) { + return new Env(env); +} + // src/util.ts var fs = __toESM(require("fs")); var fsPromises = __toESM(require("fs/promises")); @@ -144432,7 +144487,7 @@ var semver = __toESM(require_semver2()); // src/api-compatibility.json var maximumVersion = "3.22"; -var minimumVersion = "3.16"; +var minimumVersion = "3.17"; // src/json/index.ts function parseString(data) { @@ -144775,32 +144830,6 @@ function initializeEnvironment(version) { core2.exportVariable("CODEQL_ACTION_FEATURE_WILL_UPLOAD" /* FEATURE_WILL_UPLOAD */, "true"); core2.exportVariable("CODEQL_ACTION_VERSION" /* VERSION */, version); } -function getEnv(env = process.env) { - return { - getRequired: (name) => getRequiredEnvVar(env, name), - getOptional: (name) => getOptionalEnvVarFrom(env, name) - }; -} -function getRequiredEnvVar(env, paramName) { - const value = env[paramName]; - if (value === void 0 || value.length === 0) { - throw new Error(`${paramName} environment variable must be set`); - } - return value; -} -function getRequiredEnvParam(paramName) { - return getRequiredEnvVar(process.env, paramName); -} -function getOptionalEnvVarFrom(env, paramName) { - const value = env[paramName]; - if (value?.trim().length === 0) { - return void 0; - } - return value; -} -function getOptionalEnvVar(paramName) { - return getOptionalEnvVarFrom(process.env, paramName); -} var HTTPError = class extends Error { status; constructor(message, status) { @@ -145150,31 +145179,30 @@ var getOptionalInput = function(name) { const value = core3.getInput(name); return value.length > 0 ? value : void 0; }; -function getTemporaryDirectory() { - const value = process.env["CODEQL_ACTION_TEMP"]; - return value !== void 0 && value !== "" ? value : getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); +function getTemporaryDirectory(env = getEnv()) { + return env.getOptional("CODEQL_ACTION_TEMP" /* TEMP */) ?? env.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */); } var PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; -function getDiffRangesJsonFilePath() { - return path2.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME); +function getDiffRangesJsonFilePath(env = getEnv()) { + return path2.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } function getActionVersion() { return "4.37.1"; } -function getWorkflowEventName() { - return getRequiredEnvParam("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); +function getWorkflowEventName(env = getEnv()) { + return env.getRequired("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); } -function isRunningLocalAction() { - const relativeScriptPath = getRelativeScriptPath(); +function isRunningLocalAction(env = getEnv()) { + const relativeScriptPath = getRelativeScriptPath(env); return relativeScriptPath.startsWith("..") || path2.isAbsolute(relativeScriptPath); } -function getRelativeScriptPath() { - const runnerTemp = getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); +function getRelativeScriptPath(env) { + const runnerTemp = env.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */); const actionsDirectory = path2.join(path2.dirname(runnerTemp), "_actions"); return path2.relative(actionsDirectory, __filename); } -function getWorkflowEvent() { - const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); +function getWorkflowEvent(env = getEnv()) { + const eventJsonFile = env.getRequired("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); try { return JSON.parse(fs2.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -145230,8 +145258,8 @@ function getUploadValue(input) { return "always"; } } -function getWorkflowRunID() { - const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); +function getWorkflowRunID(env = getEnv()) { + const workflowRunIdString = env.getRequired("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( @@ -145245,8 +145273,8 @@ function getWorkflowRunID() { } return workflowRunID; } -function getWorkflowRunAttempt() { - const workflowRunAttemptString = getRequiredEnvParam( +function getWorkflowRunAttempt(env = getEnv()) { + const workflowRunAttemptString = env.getRequired( "GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */ ); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); @@ -145299,14 +145327,14 @@ var getFileType = async (filePath) => { throw e; } }; -function isSelfHostedRunner() { - return process.env.RUNNER_ENVIRONMENT === "self-hosted"; +function isSelfHostedRunner(env = getEnv()) { + return env.getOptional("RUNNER_ENVIRONMENT" /* RUNNER_ENVIRONMENT */) === "self-hosted"; } -function isDynamicWorkflow() { - return getWorkflowEventName() === "dynamic"; +function isDynamicWorkflow(env = getEnv()) { + return getWorkflowEventName(env) === "dynamic"; } -function isDefaultSetup() { - return isDynamicWorkflow(); +function isDefaultSetup(env = getEnv()) { + return isDynamicWorkflow(env); } function prettyPrintInvocation(cmd, args) { return [cmd, ...args].map((x) => x.includes(" ") ? `'${x}'` : x).join(" "); @@ -145370,8 +145398,9 @@ async function runTool(cmd, args = [], opts = {}) { return stdout; } var persistedInputsKey = "persisted_inputs"; -var persistInputs = function() { - const inputEnvironmentVariables = Object.entries(process.env).filter( +var persistInputs = function(env = getEnv()) { + const entries = env.entries(); + const inputEnvironmentVariables = entries.filter( ([name]) => name.startsWith("INPUT_") ); core3.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables)); @@ -145384,7 +145413,7 @@ var restoreInputs = function() { } } }; -function getPullRequestBranches() { +function getPullRequestBranches(env = getEnv()) { const pullRequest = github.context.payload.pull_request; if (pullRequest) { return { @@ -145395,8 +145424,10 @@ function getPullRequestBranches() { head: pullRequest.head.label }; } - const codeScanningRef = process.env.CODE_SCANNING_REF; - const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH; + const codeScanningRef = env.getOptional("CODE_SCANNING_REF" /* CODE_SCANNING_REF */); + const codeScanningBaseBranch = env.getOptional( + "CODE_SCANNING_BASE_BRANCH" /* CODE_SCANNING_BASE_BRANCH */ + ); if (codeScanningRef && codeScanningBaseBranch) { return { base: codeScanningBaseBranch, @@ -145407,8 +145438,8 @@ function getPullRequestBranches() { } return void 0; } -function isAnalyzingPullRequest() { - return getPullRequestBranches() !== void 0; +function isAnalyzingPullRequest(env = getEnv()) { + return getPullRequestBranches(env) !== void 0; } var qualityCategoryMapping = { "c#": "csharp", @@ -145420,8 +145451,8 @@ var qualityCategoryMapping = { typescript: "javascript-typescript", kotlin: "java-kotlin" }; -function fixCodeQualityCategory(logger, category) { - if (category !== void 0 && isDefaultSetup() && category.startsWith("/language:")) { +function fixCodeQualityCategory(logger, category, env = getEnv()) { + if (category !== void 0 && isDefaultSetup(env) && category.startsWith("/language:")) { const language = category.substring("/language:".length); const mappedLanguage = qualityCategoryMapping[language]; if (mappedLanguage) { @@ -145605,15 +145636,15 @@ function createApiClientWithDetails(apiDetails, { allowExternal = false } = {}) }) ); } -function getApiDetails() { +function getApiDetails(env = getEnv()) { return { auth: getRequiredInput("token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), - apiURL: getRequiredEnvParam("GITHUB_API_URL" /* GITHUB_API_URL */) + url: env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), + apiURL: env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) }; } -function getApiClient() { - return createApiClientWithDetails(getApiDetails()); +function getApiClient(env = getEnv()) { + return createApiClientWithDetails(getApiDetails(env)); } function getApiClientWithExternalAuth(apiDetails) { return createApiClientWithDetails(apiDetails, { allowExternal: true }); @@ -151013,9 +151044,9 @@ async function getCombinedTracerConfig(codeql, config) { // src/codeql.ts var cachedCodeQL = void 0; var CODEQL_MINIMUM_VERSION = "2.19.4"; -var CODEQL_NEXT_MINIMUM_VERSION = "2.19.4"; -var GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.15"; -var GHES_MOST_RECENT_DEPRECATION_DATE = "2026-04-09"; +var CODEQL_NEXT_MINIMUM_VERSION = "2.20.7"; +var GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16"; +var GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01"; var EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++"; async function setupCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, useOverlayAwareDefaultCliVersion, features, logger, checkVersion) { try { @@ -151648,7 +151679,7 @@ async function setupCppAutobuild(codeql, logger) { logger ); if (await features.getValue("cpp_dependency_installation_enabled" /* CppDependencyInstallation */, codeql)) { - if (process.env["RUNNER_ENVIRONMENT"] === "self-hosted" && process.env[envVar] !== "true") { + if (process.env["RUNNER_ENVIRONMENT" /* RUNNER_ENVIRONMENT */] === "self-hosted" && process.env[envVar] !== "true") { logger.info( `Disabling ${featureName} as we are on a self-hosted runner.${getWorkflowEventName() !== "dynamic" ? ` To override this, set the ${envVar} environment variable to 'true' in your workflow. See ${"https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow" /* DEFINE_ENV_VARIABLES */} for more information.` : ""}` ); diff --git a/src/actions-util.ts b/src/actions-util.ts index d7fbacbf3..5fd1ebc4f 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -7,12 +7,13 @@ import * as github from "@actions/github"; import * as io from "@actions/io"; import type { Config } from "./config-utils"; +import { Env, EnvVar, ActionsEnvVars } from "./environment"; import { Logger } from "./logging"; import { doesDirectoryExist, getCodeQLDatabasePath, - getRequiredEnvParam, ConfigurationError, + getEnv, } from "./util"; /** @@ -21,28 +22,6 @@ import { */ declare const __CODEQL_ACTION_VERSION__: string; -/** - * Enumerates known GitHub Actions environment variables that we expect - * to be set in a GitHub Actions environment. - */ -export enum ActionsEnvVars { - GITHUB_ACTION_REPOSITORY = "GITHUB_ACTION_REPOSITORY", - GITHUB_API_URL = "GITHUB_API_URL", - GITHUB_EVENT_NAME = "GITHUB_EVENT_NAME", - GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH", - GITHUB_JOB = "GITHUB_JOB", - GITHUB_REF = "GITHUB_REF", - GITHUB_REPOSITORY = "GITHUB_REPOSITORY", - GITHUB_RUN_ATTEMPT = "GITHUB_RUN_ATTEMPT", - GITHUB_RUN_ID = "GITHUB_RUN_ID", - GITHUB_SERVER_URL = "GITHUB_SERVER_URL", - GITHUB_SHA = "GITHUB_SHA", - GITHUB_WORKFLOW = "GITHUB_WORKFLOW", - RUNNER_NAME = "RUNNER_NAME", - RUNNER_OS = "RUNNER_OS", - RUNNER_TEMP = "RUNNER_TEMP", -} - /** * Abstracts over GitHub Actions functions so that we do not have to stub * global functions in tests. @@ -83,17 +62,21 @@ export const getOptionalInput = function (name: string): string | undefined { return value.length > 0 ? value : undefined; }; -export function getTemporaryDirectory(): string { - const value = process.env["CODEQL_ACTION_TEMP"]; - return value !== undefined && value !== "" - ? value - : getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); +/** + * Gets the temporary directory used by the CodeQL Action. This will either be the temporary + * 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 { + return ( + env.getOptional(EnvVar.TEMP) ?? env.getRequired(ActionsEnvVars.RUNNER_TEMP) + ); } const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; -export function getDiffRangesJsonFilePath(): string { - return path.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME); +export function getDiffRangesJsonFilePath(env: Env = getEnv()): string { + return path.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } export function getActionVersion(): string { @@ -105,16 +88,16 @@ export function getActionVersion(): string { * * This will be "dynamic" for default setup workflow runs. */ -export function getWorkflowEventName() { - return getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_NAME); +export function getWorkflowEventName(env: Env = getEnv()) { + return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME); } /** * Returns whether the current workflow is executing a local copy of the Action, e.g. we're running * a workflow on the codeql-action repo itself. */ -export function isRunningLocalAction(): boolean { - const relativeScriptPath = getRelativeScriptPath(); +export function isRunningLocalAction(env: Env = getEnv()): boolean { + const relativeScriptPath = getRelativeScriptPath(env); return ( relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath) ); @@ -125,15 +108,15 @@ export function isRunningLocalAction(): boolean { * * This can be used to get the Action's name or tell if we're running a local Action. */ -function getRelativeScriptPath(): string { - const runnerTemp = getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); +function getRelativeScriptPath(env: Env): string { + const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP); const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions"); return path.relative(actionsDirectory, __filename); } /** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */ -export function getWorkflowEvent(): any { - const eventJsonFile = getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_PATH); +export function getWorkflowEvent(env: Env = getEnv()): any { + const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH); try { return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -202,8 +185,8 @@ export function getUploadValue(input: string | undefined): UploadKind { /** * Get the workflow run ID. */ -export function getWorkflowRunID(): number { - const workflowRunIdString = getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ID); +export function getWorkflowRunID(env: Env = getEnv()): number { + const workflowRunIdString = env.getRequired(ActionsEnvVars.GITHUB_RUN_ID); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( @@ -221,8 +204,8 @@ export function getWorkflowRunID(): number { /** * Get the workflow run attempt number. */ -export function getWorkflowRunAttempt(): number { - const workflowRunAttemptString = getRequiredEnvParam( +export function getWorkflowRunAttempt(env: Env = getEnv()): number { + const workflowRunAttemptString = env.getRequired( ActionsEnvVars.GITHUB_RUN_ATTEMPT, ); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); @@ -290,18 +273,18 @@ export const getFileType = async (filePath: string): Promise => { } }; -export function isSelfHostedRunner() { - return process.env.RUNNER_ENVIRONMENT === "self-hosted"; +export function isSelfHostedRunner(env: Env = getEnv()) { + return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted"; } /** Determines whether the workflow trigger is `dynamic`. */ -export function isDynamicWorkflow(): boolean { - return getWorkflowEventName() === "dynamic"; +export function isDynamicWorkflow(env: Env = getEnv()): boolean { + return getWorkflowEventName(env) === "dynamic"; } /** Determines whether we are running in default setup. */ -export function isDefaultSetup(): boolean { - return isDynamicWorkflow(); +export function isDefaultSetup(env: Env = getEnv()): boolean { + return isDynamicWorkflow(env); } export function prettyPrintInvocation(cmd: string, args: string[]): string { @@ -399,9 +382,10 @@ const persistedInputsKey = "persisted_inputs"; * This would be simplified if actions/runner#3514 is addressed. * https://github.com/actions/runner/issues/3514 */ -export const persistInputs = function () { - const inputEnvironmentVariables = Object.entries(process.env).filter( - ([name]) => name.startsWith("INPUT_"), +export const persistInputs = function (env: Env = getEnv()) { + const entries = env.entries(); + const inputEnvironmentVariables = entries.filter(([name]) => + name.startsWith("INPUT_"), ); core.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables)); }; @@ -429,7 +413,9 @@ export interface PullRequestBranches { * @returns the base and head branches of the pull request, or undefined if * we are not analyzing a pull request. */ -export function getPullRequestBranches(): PullRequestBranches | undefined { +export function getPullRequestBranches( + env: Env = getEnv(), +): PullRequestBranches | undefined { const pullRequest = github.context.payload.pull_request; if (pullRequest) { return { @@ -443,8 +429,10 @@ export function getPullRequestBranches(): PullRequestBranches | undefined { // PR analysis under Default Setup does not have the pull_request context, // but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH. - const codeScanningRef = process.env.CODE_SCANNING_REF; - const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH; + const codeScanningRef = env.getOptional(EnvVar.CODE_SCANNING_REF); + const codeScanningBaseBranch = env.getOptional( + EnvVar.CODE_SCANNING_BASE_BRANCH, + ); if (codeScanningRef && codeScanningBaseBranch) { return { base: codeScanningBaseBranch, @@ -459,8 +447,8 @@ export function getPullRequestBranches(): PullRequestBranches | undefined { /** * Returns whether we are analyzing a pull request. */ -export function isAnalyzingPullRequest(): boolean { - return getPullRequestBranches() !== undefined; +export function isAnalyzingPullRequest(env: Env = getEnv()): boolean { + return getPullRequestBranches(env) !== undefined; } /** @@ -484,13 +472,14 @@ const qualityCategoryMapping: Record = { export function fixCodeQualityCategory( logger: Logger, category?: string, + env: Env = getEnv(), ): string | undefined { // The `category` should always be set by Default Setup. We perform this check // to avoid potential issues if Code Quality supports Advanced Setup in the future // and before this workaround is removed. if ( category !== undefined && - isDefaultSetup() && + isDefaultSetup(env) && category.startsWith("/language:") ) { const language = category.substring("/language:".length); diff --git a/src/api-client.test.ts b/src/api-client.test.ts index 29cad338f..34a72f14e 100644 --- a/src/api-client.test.ts +++ b/src/api-client.test.ts @@ -6,7 +6,8 @@ import * as sinon from "sinon"; import * as actionsUtil from "./actions-util"; import * as api from "./api-client"; import { DO_NOT_RETRY_STATUSES } from "./api-client"; -import { setupTests } from "./testing-utils"; +import { ActionsEnvVars } from "./environment"; +import { getTestEnv, setupTests } from "./testing-utils"; import * as util from "./util"; setupTests(test); @@ -20,16 +21,13 @@ test.serial("getApiClient", async (t) => { const githubStub: sinon.SinonStub = sinon.stub(); pluginStub.returns(githubStub); - sinon.stub(actionsUtil, "getRequiredInput").withArgs("token").returns("xyz"); - const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam"); - requiredEnvParamStub - .withArgs("GITHUB_SERVER_URL") - .returns("http://github.localhost"); - requiredEnvParamStub - .withArgs("GITHUB_API_URL") - .returns("http://api.github.localhost"); + const env = getTestEnv(); + env.set(ActionsEnvVars.GITHUB_SERVER_URL, "http://github.localhost"); + env.set(ActionsEnvVars.GITHUB_API_URL, "http://api.github.localhost"); - api.getApiClient(); + sinon.stub(actionsUtil, "getRequiredInput").withArgs("token").returns("xyz"); + + api.getApiClient(env); t.assert( githubStub.calledOnceWithExactly({ diff --git a/src/api-client.ts b/src/api-client.ts index 16e4082e9..eafac7ec7 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -2,12 +2,8 @@ import * as core from "@actions/core"; import * as githubUtils from "@actions/github/lib/utils"; import * as retry from "@octokit/plugin-retry"; -import { - ActionsEnvVars, - getActionVersion, - getRequiredInput, -} from "./actions-util"; -import { EnvVar } from "./environment"; +import { getActionVersion, getRequiredInput } from "./actions-util"; +import { EnvVar, ReadOnlyEnv, ActionsEnvVars, getEnv } from "./environment"; import { Logger } from "./logging"; import { getRepositoryNwo, RepositoryNwo } from "./repository"; import { @@ -71,16 +67,16 @@ function createApiClientWithDetails( ); } -export function getApiDetails(): GitHubApiDetails { +export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails { return { auth: getRequiredInput("token"), - url: getRequiredEnvParam(ActionsEnvVars.GITHUB_SERVER_URL), - apiURL: getRequiredEnvParam(ActionsEnvVars.GITHUB_API_URL), + url: env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), + apiURL: env.getRequired(ActionsEnvVars.GITHUB_API_URL), }; } -export function getApiClient() { - return createApiClientWithDetails(getApiDetails()); +export function getApiClient(env: ReadOnlyEnv = getEnv()) { + return createApiClientWithDetails(getApiDetails(env)); } export function getApiClientWithExternalAuth( diff --git a/src/api-compatibility.json b/src/api-compatibility.json index 435f8f1d6..756944019 100644 --- a/src/api-compatibility.json +++ b/src/api-compatibility.json @@ -1 +1 @@ -{"maximumVersion": "3.22", "minimumVersion": "3.16"} +{"maximumVersion": "3.22", "minimumVersion": "3.17"} diff --git a/src/autobuild.ts b/src/autobuild.ts index fc4983f4e..7ec6ba987 100644 --- a/src/autobuild.ts +++ b/src/autobuild.ts @@ -5,7 +5,7 @@ import { getGitHubVersion } from "./api-client"; import { CodeQL, getCodeQL } from "./codeql"; import * as configUtils from "./config-utils"; import { DocUrl } from "./doc-url"; -import { EnvVar } from "./environment"; +import { ActionsEnvVars, EnvVar } from "./environment"; import { Feature, featureConfig, initFeatures } from "./feature-flags"; import { BuiltInLanguage, Language } from "./languages"; import { Logger } from "./logging"; @@ -126,7 +126,7 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) { if (await features.getValue(Feature.CppDependencyInstallation, codeql)) { // disable autoinstall on self-hosted runners unless explicitly requested if ( - process.env["RUNNER_ENVIRONMENT"] === "self-hosted" && + process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] === "self-hosted" && process.env[envVar] !== "true" ) { logger.info( diff --git a/src/codeql.ts b/src/codeql.ts index bb6c0fb55..f98130f11 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -272,17 +272,17 @@ const CODEQL_MINIMUM_VERSION = "2.19.4"; /** * This version will shortly become the oldest version of CodeQL that the Action will run with. */ -const CODEQL_NEXT_MINIMUM_VERSION = "2.19.4"; +const CODEQL_NEXT_MINIMUM_VERSION = "2.20.7"; /** * This is the version of GHES that was most recently deprecated. */ -const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.15"; +const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16"; /** * This is the deprecation date for the version of GHES that was most recently deprecated. */ -const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-04-09"; +const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01"; /** The CLI verbosity level to use for extraction in debug mode. */ const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++"; diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index c36637029..ac1492d90 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -1,7 +1,7 @@ import test from "ava"; import sinon from "sinon"; -import { ActionsEnvVars } from "../actions-util"; +import { ActionsEnvVars } from "../environment"; import * as errors from "../error-messages"; import { Feature } from "../feature-flags"; import { callee, getTestEnv } from "../testing-utils"; diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index 15990ed23..15b34af2b 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -1,6 +1,5 @@ import { ActionState } from "../action-common"; -import { ActionsEnvVars } from "../actions-util"; -import { Env } from "../environment"; +import { Env, ActionsEnvVars } from "../environment"; import * as errorMessages from "../error-messages"; import { Feature } from "../feature-flags"; import { ConfigurationError, Failure, Result, Success } from "../util"; diff --git a/src/environment.ts b/src/environment.ts index c0ca050b0..6ea0e87e4 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -17,6 +17,18 @@ export enum EnvVar { */ CLI_VERBOSITY = "CODEQL_VERBOSITY", + /** + * Set by Default Setup to the base branch of the PR being analysed, if analysing a PR. + * This is needed because the `pull_request` context is not available for `dynamic` events. + */ + CODE_SCANNING_BASE_BRANCH = "CODE_SCANNING_BASE_BRANCH", + + /** + * Set by Default Setup to the full ref being analysed, if analysing a PR. + * This is needed because the `pull_request` context is not available for `dynamic` events. + */ + CODE_SCANNING_REF = "CODE_SCANNING_REF", + /** * `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of * invoking `codeql version` again. @@ -83,6 +95,9 @@ export enum EnvVar { /** Whether to suppress the warning if the current CLI will soon be unsupported. */ SUPPRESS_DEPRECATED_SOON_WARNING = "CODEQL_ACTION_SUPPRESS_DEPRECATED_SOON_WARNING", + /** Used to dictate or persist the temporary directory used by the CodeQL Action. */ + TEMP = "CODEQL_ACTION_TEMP", + /** Whether to disable uploading SARIF results or status reports to the GitHub API */ TEST_MODE = "CODEQL_ACTION_TEST_MODE", @@ -161,10 +176,122 @@ export enum EnvVar { RISK_ASSESSMENT_ID = "CODEQL_ACTION_RISK_ASSESSMENT_ID", } -/** A wrapper around an environment, to allow abstracting away from `process.env` in tests. */ -export interface Env { - /** Tries to get the value for `name` and throws if there isn't one. */ - getRequired(name: string): string; - /** Gets the value for `name`, or `undefined` if it isn't set or empty. */ - getOptional(name: string): string | undefined; +/** + * Enumerates known GitHub Actions environment variables that we expect + * to be set in a GitHub Actions environment. + */ +export enum ActionsEnvVars { + GITHUB_ACTION_REPOSITORY = "GITHUB_ACTION_REPOSITORY", + GITHUB_API_URL = "GITHUB_API_URL", + GITHUB_EVENT_NAME = "GITHUB_EVENT_NAME", + GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH", + GITHUB_JOB = "GITHUB_JOB", + GITHUB_REF = "GITHUB_REF", + GITHUB_REPOSITORY = "GITHUB_REPOSITORY", + GITHUB_RUN_ATTEMPT = "GITHUB_RUN_ATTEMPT", + GITHUB_RUN_ID = "GITHUB_RUN_ID", + GITHUB_SERVER_URL = "GITHUB_SERVER_URL", + GITHUB_SHA = "GITHUB_SHA", + GITHUB_WORKFLOW = "GITHUB_WORKFLOW", + RUNNER_ENVIRONMENT = "RUNNER_ENVIRONMENT", + RUNNER_NAME = "RUNNER_NAME", + RUNNER_OS = "RUNNER_OS", + RUNNER_TEMP = "RUNNER_TEMP", +} + +/** A type representing all known environment variables. */ +export type KnownEnvVar = EnvVar | ActionsEnvVars; + +/** + * Gets an environment variable, but throws an error if it is not set. + */ +function getRequiredEnvVar(env: NodeJS.ProcessEnv, paramName: string): string { + const value = env[paramName]; + if (value === undefined || value.length === 0) { + throw new Error(`${paramName} environment variable must be set`); + } + return value; +} + +/** + * Get an environment parameter, but throw an error if it is not set. + * + * @deprecated Use `getRequired` of a `ReadOnlyEnv` or `Env` instance instead. + */ +export function getRequiredEnvParam(paramName: string): string { + return getRequiredEnvVar(process.env, paramName); +} + +/** + * Gets an environment variable, but returns `undefined` if it is not set or empty. + */ +function getOptionalEnvVarFrom( + env: NodeJS.ProcessEnv, + paramName: string, +): string | undefined { + const value = env[paramName]; + if (value?.trim().length === 0) { + return undefined; + } + return value; +} + +/** + * Get an environment variable, but return `undefined` if it is not set or empty. + * + * @deprecated Use `getOptional` of a `ReadOnlyEnv` or `Env` instance instead. + */ +export function getOptionalEnvVar(paramName: string): string | undefined { + return getOptionalEnvVarFrom(process.env, paramName); +} + +/** + * An abstraction around read-only environment variables, to allow abstracting away from `process.env` + * in tests, while clearly signalling in regular code that the consumer of the `ReadOnlyEnv` instance + * will only read from it. + */ +export class ReadOnlyEnv { + constructor(protected readonly vars: Record) {} + + /** Tries to get the value for `name` and throws if there isn't one. */ + public getRequired(name: string): string { + return getRequiredEnvVar(this.vars, name); + } + + /** Gets the value for `name`, or `undefined` if it isn't set or empty. */ + public getOptional(name: string): string | undefined { + return getOptionalEnvVarFrom(this.vars, name); + } + + /** Gets the entries of the underlying `ProcessEnv`. */ + public entries(): Array<[string, T]> { + return Object.entries(this.vars); + } +} + +/** + * A wrapper around an environment, to allow abstracting away from `process.env` in tests. + * Use `ReadOnlyEnv` instead if you only plan to read from the environment. + * This type allows writing to the environment. + */ +export class Env< + T extends string | undefined = string | undefined, +> extends ReadOnlyEnv { + private changed: boolean = false; + + /** Sets an environment variable. */ + public set(name: string, value: T): void { + this.vars[name] = value; + this.changed = true; + } + + /** Gets a value indicating whether `set` was called at least once. */ + public hasChanged(): boolean { + return this.changed; + } +} + +/** Gets an `Env` instance for `env`, which is `process.env` by default. */ +export function getEnv(env: NodeJS.ProcessEnv = process.env): Env { + return new Env(env); } diff --git a/src/init.test.ts b/src/init.test.ts index 88ad0c9b1..1f0d2c701 100644 --- a/src/init.test.ts +++ b/src/init.test.ts @@ -8,6 +8,7 @@ import * as sinon from "sinon"; import * as actionsUtil from "./actions-util"; import { createStubCodeQL } from "./codeql"; +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["RUNNER_ENVIRONMENT"] = runnerEnv; + process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnv; const dbLocation = path.resolve(tmpDir, "dbs"); fs.mkdirSync(dbLocation, { recursive: true }); diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index 49d4d66aa..f2ba43c10 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -7,6 +7,7 @@ import * as sinon from "sinon"; import * as actionsUtil from "./actions-util"; import * as api from "./api-client"; +import { EnvVar } from "./environment"; import { Feature } from "./feature-flags"; import { getRunnerLogger } from "./logging"; import { getCacheRestoreKeyPrefix } from "./overlay/caching"; @@ -637,8 +638,8 @@ test.serial( async (t) => { await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); - process.env["CODE_SCANNING_REF"] = "refs/heads/feature-branch"; - process.env["CODE_SCANNING_BASE_BRANCH"] = "main"; + process.env[EnvVar.CODE_SCANNING_REF] = "refs/heads/feature-branch"; + process.env[EnvVar.CODE_SCANNING_BASE_BRANCH] = "main"; sinon.stub(api, "getAutomationID").resolves("test/"); const listStub = sinon.stub(api, "listActionsCaches").resolves([ diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 748ce40ea..ec518853f 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -11,7 +11,7 @@ import nock from "nock"; import * as sinon from "sinon"; import { ActionState, StateFeature } from "./action-common"; -import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util"; +import { ActionsEnv, getActionVersion } from "./actions-util"; import { AnalysisKind } from "./analyses"; import * as apiClient from "./api-client"; import { GitHubApiDetails } from "./api-client"; @@ -19,7 +19,7 @@ import { CachingKind } from "./caching-utils"; import * as codeql from "./codeql"; import { Config } from "./config-utils"; import * as defaults from "./defaults.json"; -import { Env } from "./environment"; +import { Env, ActionsEnvVars } from "./environment"; import { CodeQLDefaultVersionInfo, Feature, @@ -331,11 +331,15 @@ export type ActionVarOverrides = Partial< * excluding some that are expected to be set to paths. See `setupActionsVars`. * * @param overrides Overrides for the defaults. + * @param env The environment to set the variables for. */ -export function setupBaseActionsVars(overrides?: ActionVarOverrides) { +export function setupBaseActionsVars( + overrides?: ActionVarOverrides, + env: Env = getEnv(), +) { const vars = { ...DEFAULT_ACTIONS_VARS, ...overrides }; for (const [key, value] of Object.entries(vars)) { - process.env[key] = value; + env.set(key, value); } } diff --git a/src/util.ts b/src/util.ts index 7f5245660..b7d27afae 100644 --- a/src/util.ts +++ b/src/util.ts @@ -13,11 +13,14 @@ import * as apiCompatibility from "./api-compatibility.json"; import type { CodeQL, VersionInfo } from "./codeql"; import type { Pack } from "./config/db-config"; import type { Config } from "./config-utils"; -import { Env, EnvVar } 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 } from "./environment"; + /** * The name of the file containing the base database OIDs, as stored in the * root of the database location. @@ -566,56 +569,6 @@ export function initializeEnvironment(version: string) { core.exportVariable(EnvVar.VERSION, version); } -/** Gets an `Env` instance for `env`, which is `process.env` by default. */ -export function getEnv(env: NodeJS.ProcessEnv = process.env): Env { - return { - getRequired: (name) => getRequiredEnvVar(env, name), - getOptional: (name) => getOptionalEnvVarFrom(env, name), - }; -} - -/** - * Gets an environment variable, but throws an error if it is not set. - */ -export function getRequiredEnvVar( - env: NodeJS.ProcessEnv, - paramName: string, -): string { - const value = env[paramName]; - if (value === undefined || value.length === 0) { - throw new Error(`${paramName} environment variable must be set`); - } - return value; -} - -/** - * Get an environment parameter, but throw an error if it is not set. - */ -export function getRequiredEnvParam(paramName: string): string { - return getRequiredEnvVar(process.env, paramName); -} - -/** - * Gets an environment variable, but returns `undefined` if it is not set or empty. - */ -export function getOptionalEnvVarFrom( - env: NodeJS.ProcessEnv, - paramName: string, -): string | undefined { - const value = env[paramName]; - if (value?.trim().length === 0) { - return undefined; - } - return value; -} - -/** - * Get an environment variable, but return `undefined` if it is not set or empty. - */ -export function getOptionalEnvVar(paramName: string): string | undefined { - return getOptionalEnvVarFrom(process.env, paramName); -} - export class HTTPError extends Error { public status: number;