diff --git a/lib/entry-points.js b/lib/entry-points.js index 406916cf6e..e89711f226 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")); @@ -144776,32 +144831,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) { @@ -145151,31 +145180,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) { @@ -145231,8 +145259,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( @@ -145246,8 +145274,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); @@ -145300,14 +145328,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(" "); @@ -145371,8 +145399,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)); @@ -145385,7 +145414,7 @@ var restoreInputs = function() { } } }; -function getPullRequestBranches() { +function getPullRequestBranches(env = getEnv()) { const pullRequest = github.context.payload.pull_request; if (pullRequest) { return { @@ -145396,8 +145425,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, @@ -145408,8 +145439,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", @@ -145421,8 +145452,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) { @@ -145606,15 +145637,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 }); @@ -151649,7 +151680,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 d7fbacbf3e..5fd1ebc4fe 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 29cad338f7..34a72f14e5 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); + const env = getTestEnv(); + env.set(ActionsEnvVars.GITHUB_SERVER_URL, "http://github.localhost"); + env.set(ActionsEnvVars.GITHUB_API_URL, "http://api.github.localhost"); + 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"); - - api.getApiClient(); + + api.getApiClient(env); t.assert( githubStub.calledOnceWithExactly({ diff --git a/src/api-client.ts b/src/api-client.ts index 16e4082e90..eafac7ec76 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/autobuild.ts b/src/autobuild.ts index fc4983f4ef..7ec6ba9873 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/config/remote-file.test.ts b/src/config/remote-file.test.ts index c366370293..ac1492d906 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 15990ed23f..15b34af2b6 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 c0ca050b03..6ea0e87e4f 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 { +/** + * 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. */ - getRequired(name: string): string; + public getRequired(name: string): string { + return getRequiredEnvVar(this.vars, name); + } + /** Gets the value for `name`, or `undefined` if it isn't set or empty. */ - getOptional(name: string): string | undefined; + 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 88ad0c9b18..1f0d2c701c 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 49d4d66aad..f2ba43c101 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 748ce40ea3..ec518853f8 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 7f52456608..b7d27afae3 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;