From c05a5b71ad32145af66247be1af075b8cf321ecd Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 16 Jun 2026 13:00:13 +0200 Subject: [PATCH 01/16] Add `ActionsEnvVars` enum --- lib/entry-points.js | 26 ++++++++++++++------------ src/actions-util.ts | 44 ++++++++++++++++++++++++++++++++++---------- src/api-client.ts | 10 +++++++--- src/testing-utils.ts | 4 ++-- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 11a8c4c291..1ac3da8524 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149025,7 +149025,7 @@ var getOptionalInput = function(name) { }; function getTemporaryDirectory() { const value = process.env["CODEQL_ACTION_TEMP"]; - return value !== void 0 && value !== "" ? value : getRequiredEnvParam("RUNNER_TEMP"); + return value !== void 0 && value !== "" ? value : getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); } var PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; function getDiffRangesJsonFilePath() { @@ -149035,19 +149035,19 @@ function getActionVersion() { return "4.36.3"; } function getWorkflowEventName() { - return getRequiredEnvParam("GITHUB_EVENT_NAME"); + return getRequiredEnvParam("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); } function isRunningLocalAction() { const relativeScriptPath = getRelativeScriptPath(); return relativeScriptPath.startsWith("..") || path2.isAbsolute(relativeScriptPath); } function getRelativeScriptPath() { - const runnerTemp = getRequiredEnvParam("RUNNER_TEMP"); + const runnerTemp = getRequiredEnvParam("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"); + const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); try { return JSON.parse(fs2.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -149104,31 +149104,33 @@ function getUploadValue(input) { } } function getWorkflowRunID() { - const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID"); + const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( - `GITHUB_RUN_ID must define a non NaN workflow run ID. Current value is ${workflowRunIdString}` + `${"GITHUB_RUN_ID" /* GITHUB_RUN_ID */} must define a non NaN workflow run ID. Current value is ${workflowRunIdString}` ); } if (workflowRunID < 0) { throw new Error( - `GITHUB_RUN_ID must be a non-negative integer. Current value is ${workflowRunIdString}` + `${"GITHUB_RUN_ID" /* GITHUB_RUN_ID */} must be a non-negative integer. Current value is ${workflowRunIdString}` ); } return workflowRunID; } function getWorkflowRunAttempt() { - const workflowRunAttemptString = getRequiredEnvParam("GITHUB_RUN_ATTEMPT"); + const workflowRunAttemptString = getRequiredEnvParam( + "GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */ + ); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); if (Number.isNaN(workflowRunAttempt)) { throw new Error( - `GITHUB_RUN_ATTEMPT must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}` + `${"GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */} must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}` ); } if (workflowRunAttempt <= 0) { throw new Error( - `GITHUB_RUN_ATTEMPT must be a positive integer. Current value is ${workflowRunAttemptString}` + `${"GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */} must be a positive integer. Current value is ${workflowRunAttemptString}` ); } return workflowRunAttempt; @@ -149439,8 +149441,8 @@ function createApiClientWithDetails(apiDetails, { allowExternal = false } = {}) function getApiDetails() { return { auth: getRequiredInput("token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL") + url: getRequiredEnvParam("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), + apiURL: getRequiredEnvParam("GITHUB_API_URL" /* GITHUB_API_URL */) }; } function getApiClient() { diff --git a/src/actions-util.ts b/src/actions-util.ts index dea22d5c57..d7fbacbf3e 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -21,6 +21,28 @@ 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. @@ -65,7 +87,7 @@ export function getTemporaryDirectory(): string { const value = process.env["CODEQL_ACTION_TEMP"]; return value !== undefined && value !== "" ? value - : getRequiredEnvParam("RUNNER_TEMP"); + : getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); } const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; @@ -84,7 +106,7 @@ export function getActionVersion(): string { * This will be "dynamic" for default setup workflow runs. */ export function getWorkflowEventName() { - return getRequiredEnvParam("GITHUB_EVENT_NAME"); + return getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_NAME); } /** @@ -104,14 +126,14 @@ 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("RUNNER_TEMP"); + const runnerTemp = getRequiredEnvParam(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("GITHUB_EVENT_PATH"); + const eventJsonFile = getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_PATH); try { return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -181,16 +203,16 @@ export function getUploadValue(input: string | undefined): UploadKind { * Get the workflow run ID. */ export function getWorkflowRunID(): number { - const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID"); + const workflowRunIdString = getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ID); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( - `GITHUB_RUN_ID must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`, + `${ActionsEnvVars.GITHUB_RUN_ID} must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`, ); } if (workflowRunID < 0) { throw new Error( - `GITHUB_RUN_ID must be a non-negative integer. Current value is ${workflowRunIdString}`, + `${ActionsEnvVars.GITHUB_RUN_ID} must be a non-negative integer. Current value is ${workflowRunIdString}`, ); } return workflowRunID; @@ -200,16 +222,18 @@ export function getWorkflowRunID(): number { * Get the workflow run attempt number. */ export function getWorkflowRunAttempt(): number { - const workflowRunAttemptString = getRequiredEnvParam("GITHUB_RUN_ATTEMPT"); + const workflowRunAttemptString = getRequiredEnvParam( + ActionsEnvVars.GITHUB_RUN_ATTEMPT, + ); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); if (Number.isNaN(workflowRunAttempt)) { throw new Error( - `GITHUB_RUN_ATTEMPT must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`, + `${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`, ); } if (workflowRunAttempt <= 0) { throw new Error( - `GITHUB_RUN_ATTEMPT must be a positive integer. Current value is ${workflowRunAttemptString}`, + `${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must be a positive integer. Current value is ${workflowRunAttemptString}`, ); } return workflowRunAttempt; diff --git a/src/api-client.ts b/src/api-client.ts index 4a061d4828..16714b4804 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -2,7 +2,11 @@ import * as core from "@actions/core"; import * as githubUtils from "@actions/github/lib/utils"; import * as retry from "@octokit/plugin-retry"; -import { getActionVersion, getRequiredInput } from "./actions-util"; +import { + ActionsEnvVars, + getActionVersion, + getRequiredInput, +} from "./actions-util"; import { EnvVar } from "./environment"; import { Logger } from "./logging"; import { getRepositoryNwo, RepositoryNwo } from "./repository"; @@ -70,8 +74,8 @@ function createApiClientWithDetails( export function getApiDetails(): GitHubApiDetails { return { auth: getRequiredInput("token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL"), + url: getRequiredEnvParam(ActionsEnvVars.GITHUB_SERVER_URL), + apiURL: getRequiredEnvParam(ActionsEnvVars.GITHUB_API_URL), }; } diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 2660c21a69..411cb87319 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -10,7 +10,7 @@ import test, { import nock from "nock"; import * as sinon from "sinon"; -import { ActionsEnv, getActionVersion } from "./actions-util"; +import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util"; import { AnalysisKind } from "./analyses"; import * as apiClient from "./api-client"; import { GitHubApiDetails } from "./api-client"; @@ -200,7 +200,7 @@ export const DEFAULT_ACTIONS_VARS = { GITHUB_WORKFLOW: "test-workflow", RUNNER_NAME: "my-runner", RUNNER_OS: "Linux", -} as const satisfies Record; +} as const satisfies Partial>; /** Partial mappings from GitHub Actions environment variables to values. */ export type ActionVarOverrides = Partial< From 652296eb9e76be692cbe3c9faa47dec498736254 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 16 Jun 2026 13:22:35 +0200 Subject: [PATCH 02/16] Allow abstracting over `process.env` --- lib/entry-points.js | 14 ++++++++++---- src/environment.ts | 8 ++++++++ src/testing-utils.ts | 7 +++++++ src/util.ts | 42 +++++++++++++++++++++++++++++++++++------- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 1ac3da8524..fc5fa66dd6 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148660,20 +148660,26 @@ function initializeEnvironment(version) { core2.exportVariable("CODEQL_ACTION_FEATURE_WILL_UPLOAD" /* FEATURE_WILL_UPLOAD */, "true"); core2.exportVariable("CODEQL_ACTION_VERSION" /* VERSION */, version); } -function getRequiredEnvParam(paramName) { - const value = process.env[paramName]; +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 getOptionalEnvVar(paramName) { - const value = process.env[paramName]; +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) { diff --git a/src/environment.ts b/src/environment.ts index c3f54ebd27..c0ca050b03 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -160,3 +160,11 @@ export enum EnvVar { /** Used by Code Scanning Risk Assessment to communicate the assessment ID to the CodeQL Action. */ 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; +} diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 411cb87319..22ed1e21a5 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -18,6 +18,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 { CodeQLDefaultVersionInfo, Feature, @@ -29,6 +30,7 @@ import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { DEFAULT_DEBUG_ARTIFACT_NAME, DEFAULT_DEBUG_DATABASE_NAME, + getEnv, GitHubVariant, GitHubVersion, HTTPError, @@ -172,6 +174,11 @@ export function makeMacro( return wrapper; } +export function getTestEnv(): Env { + const testEnv: NodeJS.ProcessEnv = {}; + return getEnv(testEnv); +} + /** * Gets an `ActionsEnv` instance for use in tests. */ diff --git a/src/util.ts b/src/util.ts index 200d68d2c2..e2632bc957 100644 --- a/src/util.ts +++ b/src/util.ts @@ -13,7 +13,7 @@ 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 { EnvVar } from "./environment"; +import { Env, EnvVar } from "./environment"; import * as json from "./json"; import { Language } from "./languages"; import { Logger } from "./logging"; @@ -566,11 +566,22 @@ 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) => getOptionalEnvVar(name), + }; +} + /** - * Get an environment parameter, but throw an error if it is not set. + * Gets an environment variable, but throws an error if it is not set. */ -export function getRequiredEnvParam(paramName: string): string { - const value = process.env[paramName]; +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`); } @@ -578,16 +589,33 @@ export function getRequiredEnvParam(paramName: string): string { } /** - * Get an environment variable, but return `undefined` if it is not set or empty. + * Get an environment parameter, but throw an error if it is not set. */ -export function getOptionalEnvVar(paramName: string): string | undefined { - const value = process.env[paramName]; +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; From 9c4ee01a28d40524447d2ec6975079340d81e70c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 16:39:06 +0100 Subject: [PATCH 03/16] Add `RemoteFileAddress` type --- src/config/remote-file.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/config/remote-file.ts diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts new file mode 100644 index 0000000000..cb90b68208 --- /dev/null +++ b/src/config/remote-file.ts @@ -0,0 +1,11 @@ +/** Represents remote file addresses. */ +export interface RemoteFileAddress { + /** The owner of the repository. */ + owner: string; + /** The repository name. */ + repo: string; + /** The path of the file. */ + path: string; + /** The ref of the repository. */ + ref: string; +} From 07b6b1eac5245f536d915a7628b4244d38e7bed4 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 16:29:22 +0100 Subject: [PATCH 04/16] Move `getRemoteConfig` to `config/file.ts` --- lib/entry-points.js | 108 ++++++++++++++++++++++---------------------- src/config-utils.ts | 49 +------------------- src/config/file.ts | 53 ++++++++++++++++++++++ 3 files changed, 108 insertions(+), 102 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index fc5fa66dd6..93b8254ac3 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151428,6 +151428,58 @@ function parseUserConfig(logger, pathInput, contents, validateConfig) { } } +// src/config/file.ts +function getConfigFileInput(logger, actions, repositoryProperties) { + const input = actions.getOptionalInput("config-file"); + if (input !== void 0) { + logger.info(`Using configuration file input from workflow: ${input}`); + return input; + } + const propertyValue = repositoryProperties["github-codeql-config-file" /* CONFIG_FILE */]; + if (propertyValue !== void 0 && propertyValue.trim().length > 0) { + logger.info( + `Using configuration file input from repository property: ${propertyValue}` + ); + return propertyValue; + } + return void 0; +} +async function getRemoteConfig(logger, configFile, apiDetails, validateConfig) { + const format = new RegExp( + "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)" + ); + const pieces = format.exec(configFile); + if (pieces?.groups === void 0 || pieces.length < 5) { + throw new ConfigurationError( + getConfigFileRepoFormatInvalidMessage(configFile) + ); + } + const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({ + owner: pieces.groups.owner, + repo: pieces.groups.repo, + path: pieces.groups.path, + ref: pieces.groups.ref + }); + let fileContents; + if ("content" in response.data && response.data.content !== void 0) { + fileContents = response.data.content; + } else if (Array.isArray(response.data)) { + throw new ConfigurationError( + getConfigFileDirectoryGivenMessage(configFile) + ); + } else { + throw new ConfigurationError( + getConfigFileFormatInvalidMessage(configFile) + ); + } + return parseUserConfig( + logger, + configFile, + Buffer.from(fileContents, "base64").toString("binary"), + validateConfig + ); +} + // src/diagnostics.ts var import_fs = require("fs"); var import_path = __toESM(require("path")); @@ -152871,41 +152923,6 @@ function getLocalConfig(logger, configFile, validateConfig) { validateConfig ); } -async function getRemoteConfig(logger, configFile, apiDetails, validateConfig) { - const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)" - ); - const pieces = format.exec(configFile); - if (pieces?.groups === void 0 || pieces.length < 5) { - throw new ConfigurationError( - getConfigFileRepoFormatInvalidMessage(configFile) - ); - } - const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({ - owner: pieces.groups.owner, - repo: pieces.groups.repo, - path: pieces.groups.path, - ref: pieces.groups.ref - }); - let fileContents; - if ("content" in response.data && response.data.content !== void 0) { - fileContents = response.data.content; - } else if (Array.isArray(response.data)) { - throw new ConfigurationError( - getConfigFileDirectoryGivenMessage(configFile) - ); - } else { - throw new ConfigurationError( - getConfigFileFormatInvalidMessage(configFile) - ); - } - return parseUserConfig( - logger, - configFile, - Buffer.from(fileContents, "base64").toString("binary"), - validateConfig - ); -} function getPathToParsedConfigFile(tempDir) { return path10.join(tempDir, "config"); } @@ -160721,7 +160738,7 @@ var import_async = __toESM(require_async(), 1); var import_path6 = require("path"); // node_modules/archiver/lib/error.js -var import_util28 = __toESM(require("util"), 1); +var import_util29 = __toESM(require("util"), 1); var ERROR_CODES = { ABORTED: "archive was aborted", DIRECTORYDIRPATHREQUIRED: "diretory dirpath argument must be a non-empty string value", @@ -160746,7 +160763,7 @@ function ArchiverError(code, data) { this.code = code; this.data = data; } -import_util28.default.inherits(ArchiverError, Error); +import_util29.default.inherits(ArchiverError, Error); // node_modules/archiver/lib/core.js var import_readable_stream2 = __toESM(require_ours(), 1); @@ -163690,23 +163707,6 @@ var github3 = __toESM(require_github()); var io7 = __toESM(require_io()); var semver10 = __toESM(require_semver2()); -// src/config/file.ts -function getConfigFileInput(logger, actions, repositoryProperties) { - const input = actions.getOptionalInput("config-file"); - if (input !== void 0) { - logger.info(`Using configuration file input from workflow: ${input}`); - return input; - } - const propertyValue = repositoryProperties["github-codeql-config-file" /* CONFIG_FILE */]; - if (propertyValue !== void 0 && propertyValue.trim().length > 0) { - logger.info( - `Using configuration file input from repository property: ${propertyValue}` - ); - return propertyValue; - } - return void 0; -} - // src/workflow.ts var fs27 = __toESM(require("fs")); var path23 = __toESM(require("path")); diff --git a/src/config-utils.ts b/src/config-utils.ts index 972734877a..39e612674a 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -27,6 +27,7 @@ import { parseUserConfig, UserConfig, } from "./config/db-config"; +import { getRemoteConfig } from "./config/file"; import { addNoLanguageDiagnostic, makeTelemetryDiagnostic, @@ -1369,54 +1370,6 @@ function getLocalConfig( ); } -async function getRemoteConfig( - logger: Logger, - configFile: string, - apiDetails: api.GitHubApiCombinedDetails, - validateConfig: boolean, -): Promise { - // retrieve the various parts of the config location, and ensure they're present - const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", - ); - const pieces = format.exec(configFile); - // 5 = 4 groups + the whole expression - if (pieces?.groups === undefined || pieces.length < 5) { - throw new ConfigurationError( - errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), - ); - } - - const response = await api - .getApiClientWithExternalAuth(apiDetails) - .rest.repos.getContent({ - owner: pieces.groups.owner, - repo: pieces.groups.repo, - path: pieces.groups.path, - ref: pieces.groups.ref, - }); - - let fileContents: string; - if ("content" in response.data && response.data.content !== undefined) { - fileContents = response.data.content; - } else if (Array.isArray(response.data)) { - throw new ConfigurationError( - errorMessages.getConfigFileDirectoryGivenMessage(configFile), - ); - } else { - throw new ConfigurationError( - errorMessages.getConfigFileFormatInvalidMessage(configFile), - ); - } - - return parseUserConfig( - logger, - configFile, - Buffer.from(fileContents, "base64").toString("binary"), - validateConfig, - ); -} - /** * Get the file path where the parsed config will be stored. */ diff --git a/src/config/file.ts b/src/config/file.ts index 24613dc557..537d42f651 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -1,9 +1,14 @@ import { ActionsEnv } from "../actions-util"; +import * as api from "../api-client"; +import * as errorMessages from "../error-messages"; import { RepositoryProperties, RepositoryPropertyName, } from "../feature-flags/properties"; import { Logger } from "../logging"; +import { ConfigurationError } from "../util"; + +import { parseUserConfig, UserConfig } from "./db-config"; /** * Gets the value that is configured for the configuration file, if any. @@ -32,3 +37,51 @@ export function getConfigFileInput( return undefined; } + +export async function getRemoteConfig( + logger: Logger, + configFile: string, + apiDetails: api.GitHubApiCombinedDetails, + validateConfig: boolean, +): Promise { + // retrieve the various parts of the config location, and ensure they're present + const format = new RegExp( + "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", + ); + const pieces = format.exec(configFile); + // 5 = 4 groups + the whole expression + if (pieces?.groups === undefined || pieces.length < 5) { + throw new ConfigurationError( + errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), + ); + } + + const response = await api + .getApiClientWithExternalAuth(apiDetails) + .rest.repos.getContent({ + owner: pieces.groups.owner, + repo: pieces.groups.repo, + path: pieces.groups.path, + ref: pieces.groups.ref, + }); + + let fileContents: string; + if ("content" in response.data && response.data.content !== undefined) { + fileContents = response.data.content; + } else if (Array.isArray(response.data)) { + throw new ConfigurationError( + errorMessages.getConfigFileDirectoryGivenMessage(configFile), + ); + } else { + throw new ConfigurationError( + errorMessages.getConfigFileFormatInvalidMessage(configFile), + ); + } + + return parseUserConfig( + logger, + configFile, + Buffer.from(fileContents, "base64").toString("binary"), + validateConfig, + ); +} From 9ef8be7176ff5afdc4c6ac2856a68855b885505b Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 16:44:31 +0100 Subject: [PATCH 05/16] Refactor `parseRemoteFileAddress` out of `getRemoteConfig` --- lib/entry-points.js | 36 +++++++++++++++++++++--------------- src/config/file.ts | 21 ++++++--------------- src/config/remote-file.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 93b8254ac3..792a8cbd8e 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151428,6 +151428,20 @@ function parseUserConfig(logger, pathInput, contents, validateConfig) { } } +// src/config/remote-file.ts +function parseRemoteFileAddress(configFile) { + const format = new RegExp( + "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)" + ); + const pieces = format.exec(configFile); + if (pieces?.groups === void 0 || pieces.length < 5) { + throw new ConfigurationError( + getConfigFileRepoFormatInvalidMessage(configFile) + ); + } + return pieces.groups; +} + // src/config/file.ts function getConfigFileInput(logger, actions, repositoryProperties) { const input = actions.getOptionalInput("config-file"); @@ -151445,20 +151459,12 @@ function getConfigFileInput(logger, actions, repositoryProperties) { return void 0; } async function getRemoteConfig(logger, configFile, apiDetails, validateConfig) { - const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)" - ); - const pieces = format.exec(configFile); - if (pieces?.groups === void 0 || pieces.length < 5) { - throw new ConfigurationError( - getConfigFileRepoFormatInvalidMessage(configFile) - ); - } + const groups = parseRemoteFileAddress(configFile); const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({ - owner: pieces.groups.owner, - repo: pieces.groups.repo, - path: pieces.groups.path, - ref: pieces.groups.ref + owner: groups.owner, + repo: groups.repo, + path: groups.path, + ref: groups.ref }); let fileContents; if ("content" in response.data && response.data.content !== void 0) { @@ -160738,7 +160744,7 @@ var import_async = __toESM(require_async(), 1); var import_path6 = require("path"); // node_modules/archiver/lib/error.js -var import_util29 = __toESM(require("util"), 1); +var import_util30 = __toESM(require("util"), 1); var ERROR_CODES = { ABORTED: "archive was aborted", DIRECTORYDIRPATHREQUIRED: "diretory dirpath argument must be a non-empty string value", @@ -160763,7 +160769,7 @@ function ArchiverError(code, data) { this.code = code; this.data = data; } -import_util29.default.inherits(ArchiverError, Error); +import_util30.default.inherits(ArchiverError, Error); // node_modules/archiver/lib/core.js var import_readable_stream2 = __toESM(require_ours(), 1); diff --git a/src/config/file.ts b/src/config/file.ts index 537d42f651..b48a27c5c4 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -9,6 +9,7 @@ import { Logger } from "../logging"; import { ConfigurationError } from "../util"; import { parseUserConfig, UserConfig } from "./db-config"; +import { parseRemoteFileAddress } from "./remote-file"; /** * Gets the value that is configured for the configuration file, if any. @@ -44,25 +45,15 @@ export async function getRemoteConfig( apiDetails: api.GitHubApiCombinedDetails, validateConfig: boolean, ): Promise { - // retrieve the various parts of the config location, and ensure they're present - const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", - ); - const pieces = format.exec(configFile); - // 5 = 4 groups + the whole expression - if (pieces?.groups === undefined || pieces.length < 5) { - throw new ConfigurationError( - errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), - ); - } + const groups = parseRemoteFileAddress(configFile); const response = await api .getApiClientWithExternalAuth(apiDetails) .rest.repos.getContent({ - owner: pieces.groups.owner, - repo: pieces.groups.repo, - path: pieces.groups.path, - ref: pieces.groups.ref, + owner: groups.owner, + repo: groups.repo, + path: groups.path, + ref: groups.ref, }); let fileContents: string; diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index cb90b68208..a50e197eb8 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -1,3 +1,6 @@ +import * as errorMessages from "../error-messages"; +import { ConfigurationError } from "../util"; + /** Represents remote file addresses. */ export interface RemoteFileAddress { /** The owner of the repository. */ @@ -9,3 +12,27 @@ export interface RemoteFileAddress { /** The ref of the repository. */ ref: string; } + +/** + * Attempts to parse `configFile` into an array of `RemoteFileAddress` components. + * + * @param configFile The string to try and parse. + * @returns The successful result of executing the regex. + * @throws `ConfigurationError` if the format of `configFile` is not valid. + */ +export function parseRemoteFileAddress(configFile: string) { + // retrieve the various parts of the config location, and ensure they're present + const format = new RegExp( + "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", + ); + const pieces = format.exec(configFile); + + // 5 = 4 groups + the whole expression + if (pieces?.groups === undefined || pieces.length < 5) { + throw new ConfigurationError( + errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), + ); + } + + return pieces.groups; +} From 82e5ca6c55fb2fe8ccb303cb2c9a82798b6ed5fa Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 16:48:34 +0100 Subject: [PATCH 06/16] Return `RemoteFileAddress` from `parseRemoteFileAddress` --- lib/entry-points.js | 17 +++++++++++------ src/config/file.ts | 10 +++++----- src/config/remote-file.ts | 9 +++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 792a8cbd8e..40188eb2d7 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151439,7 +151439,12 @@ function parseRemoteFileAddress(configFile) { getConfigFileRepoFormatInvalidMessage(configFile) ); } - return pieces.groups; + return { + owner: pieces.groups.owner, + repo: pieces.groups.repo, + path: pieces.groups.path, + ref: pieces.groups.ref + }; } // src/config/file.ts @@ -151459,12 +151464,12 @@ function getConfigFileInput(logger, actions, repositoryProperties) { return void 0; } async function getRemoteConfig(logger, configFile, apiDetails, validateConfig) { - const groups = parseRemoteFileAddress(configFile); + const address = parseRemoteFileAddress(configFile); const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({ - owner: groups.owner, - repo: groups.repo, - path: groups.path, - ref: groups.ref + owner: address.owner, + repo: address.repo, + path: address.path, + ref: address.ref }); let fileContents; if ("content" in response.data && response.data.content !== void 0) { diff --git a/src/config/file.ts b/src/config/file.ts index b48a27c5c4..693a353061 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -45,15 +45,15 @@ export async function getRemoteConfig( apiDetails: api.GitHubApiCombinedDetails, validateConfig: boolean, ): Promise { - const groups = parseRemoteFileAddress(configFile); + const address = parseRemoteFileAddress(configFile); const response = await api .getApiClientWithExternalAuth(apiDetails) .rest.repos.getContent({ - owner: groups.owner, - repo: groups.repo, - path: groups.path, - ref: groups.ref, + owner: address.owner, + repo: address.repo, + path: address.path, + ref: address.ref, }); let fileContents: string; diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index a50e197eb8..66328586f3 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -20,7 +20,7 @@ export interface RemoteFileAddress { * @returns The successful result of executing the regex. * @throws `ConfigurationError` if the format of `configFile` is not valid. */ -export function parseRemoteFileAddress(configFile: string) { +export function parseRemoteFileAddress(configFile: string): RemoteFileAddress { // retrieve the various parts of the config location, and ensure they're present const format = new RegExp( "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", @@ -34,5 +34,10 @@ export function parseRemoteFileAddress(configFile: string) { ); } - return pieces.groups; + return { + owner: pieces.groups.owner, + repo: pieces.groups.repo, + path: pieces.groups.path, + ref: pieces.groups.ref, + }; } From c7a94c979d8510f500182a32005bbbce141a5487 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 16:55:23 +0100 Subject: [PATCH 07/16] Add `getRemoteConfig` JSDoc --- src/config/file.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/config/file.ts b/src/config/file.ts index 693a353061..420e418e04 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -39,6 +39,16 @@ export function getConfigFileInput( return undefined; } +/** + * Attempts to fetch a `UserConfig` from a remote `address`. + * + * @param logger The logger to use. + * @param configFile The remote address of the configuration file. + * @param apiDetails Information about how to connect to the API. + * @param validateConfig Whether to validate the configuration. + * + * @returns The `UserConfig`, if it could be fetched and parsed successfully. + */ export async function getRemoteConfig( logger: Logger, configFile: string, From 85c8a8cebeaacc948166643ed1d796e7d1baa3cd Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 17:14:06 +0100 Subject: [PATCH 08/16] Add tests for `parseRemoteFileAddress` --- src/config/remote-file.test.ts | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/config/remote-file.test.ts diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts new file mode 100644 index 0000000000..fe8589d945 --- /dev/null +++ b/src/config/remote-file.test.ts @@ -0,0 +1,36 @@ +import test from "ava"; + +import { ConfigurationError } from "../util"; + +import { parseRemoteFileAddress, RemoteFileAddress } from "./remote-file"; + +test("expandConfigFileInput accepts full remote addresses", async (t) => { + t.deepEqual(parseRemoteFileAddress("owner/repo/path@ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual( + parseRemoteFileAddress("owner/repo/path/to/codeql.yml@ref/feature"), + { + owner: "owner", + repo: "repo", + path: "path/to/codeql.yml", + ref: "ref/feature", + } satisfies RemoteFileAddress, + ); +}); + +test("expandConfigFileInput rejects invalid values", async (t) => { + t.throws(() => parseRemoteFileAddress(" "), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress("repo:/absolute"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress("repo:file.yml:unexpected"), { + instanceOf: ConfigurationError, + }); +}); From 598d00854a4bd82afc63bb4f0a7705e978b478dc Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 17:28:20 +0100 Subject: [PATCH 09/16] Make `ref` optional in `parseRemoteFileAddress` --- lib/entry-points.js | 7 ++++--- src/config/remote-file.test.ts | 22 +++++++++++++++++++++- src/config/remote-file.ts | 15 +++++++++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 40188eb2d7..48a157aa64 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151429,12 +151429,13 @@ function parseUserConfig(logger, pathInput, contents, validateConfig) { } // src/config/remote-file.ts +var DEFAULT_CONFIG_FILE_REF = "main"; function parseRemoteFileAddress(configFile) { const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)" + "(?[^/]+)/(?[^/]+)/(?[^@]+)(@(?.*))?" ); const pieces = format.exec(configFile); - if (pieces?.groups === void 0 || pieces.length < 5) { + if (!pieces?.groups?.owner || !pieces?.groups?.repo || !pieces?.groups?.path) { throw new ConfigurationError( getConfigFileRepoFormatInvalidMessage(configFile) ); @@ -151443,7 +151444,7 @@ function parseRemoteFileAddress(configFile) { owner: pieces.groups.owner, repo: pieces.groups.repo, path: pieces.groups.path, - ref: pieces.groups.ref + ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF }; } diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index fe8589d945..d45c2ec6dd 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -2,7 +2,11 @@ import test from "ava"; import { ConfigurationError } from "../util"; -import { parseRemoteFileAddress, RemoteFileAddress } from "./remote-file"; +import { + DEFAULT_CONFIG_FILE_REF, + parseRemoteFileAddress, + RemoteFileAddress, +} from "./remote-file"; test("expandConfigFileInput accepts full remote addresses", async (t) => { t.deepEqual(parseRemoteFileAddress("owner/repo/path@ref"), { @@ -23,6 +27,22 @@ test("expandConfigFileInput accepts full remote addresses", async (t) => { ); }); +test("expandConfigFileInput accepts remote address without a ref", async (t) => { + t.deepEqual(parseRemoteFileAddress("owner/repo/path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: DEFAULT_CONFIG_FILE_REF, + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress("owner/repo/path@"), { + owner: "owner", + repo: "repo", + path: "path", + ref: DEFAULT_CONFIG_FILE_REF, + } satisfies RemoteFileAddress); +}); + test("expandConfigFileInput rejects invalid values", async (t) => { t.throws(() => parseRemoteFileAddress(" "), { instanceOf: ConfigurationError, diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index 66328586f3..9ab89ad4a2 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -13,6 +13,9 @@ export interface RemoteFileAddress { ref: string; } +/** The default ref to use in configuration file shorthands. */ +export const DEFAULT_CONFIG_FILE_REF = "main"; + /** * Attempts to parse `configFile` into an array of `RemoteFileAddress` components. * @@ -23,12 +26,16 @@ export interface RemoteFileAddress { export function parseRemoteFileAddress(configFile: string): RemoteFileAddress { // retrieve the various parts of the config location, and ensure they're present const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", + "(?[^/]+)/(?[^/]+)/(?[^@]+)(@(?.*))?", ); const pieces = format.exec(configFile); - // 5 = 4 groups + the whole expression - if (pieces?.groups === undefined || pieces.length < 5) { + // Check that the regular expression matched and that we have at least the required components. + if ( + !pieces?.groups?.owner || + !pieces?.groups?.repo || + !pieces?.groups?.path + ) { throw new ConfigurationError( errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), ); @@ -38,6 +45,6 @@ export function parseRemoteFileAddress(configFile: string): RemoteFileAddress { owner: pieces.groups.owner, repo: pieces.groups.repo, path: pieces.groups.path, - ref: pieces.groups.ref, + ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF, }; } From e537ff20a45eb030d9b475d95f9a39313bd474c2 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 17:32:44 +0100 Subject: [PATCH 10/16] Make `path` optional in `parseRemoteFileAddress` --- lib/entry-points.js | 7 ++++--- src/config/remote-file.test.ts | 19 ++++++++++++++++++- src/config/remote-file.ts | 13 ++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 48a157aa64..7ad70e03d8 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151429,13 +151429,14 @@ function parseUserConfig(logger, pathInput, contents, validateConfig) { } // src/config/remote-file.ts +var DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml"; var DEFAULT_CONFIG_FILE_REF = "main"; function parseRemoteFileAddress(configFile) { const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)(@(?.*))?" + "(?[^/]+)/(?[^/@]+)(/(?[^@]+))?(@(?.*))?" ); const pieces = format.exec(configFile); - if (!pieces?.groups?.owner || !pieces?.groups?.repo || !pieces?.groups?.path) { + if (!pieces?.groups?.owner || !pieces?.groups?.repo) { throw new ConfigurationError( getConfigFileRepoFormatInvalidMessage(configFile) ); @@ -151443,7 +151444,7 @@ function parseRemoteFileAddress(configFile) { return { owner: pieces.groups.owner, repo: pieces.groups.repo, - path: pieces.groups.path, + path: pieces.groups.path || DEFAULT_CONFIG_FILE_NAME, ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF }; } diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index d45c2ec6dd..457c24120c 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -3,6 +3,7 @@ import test from "ava"; import { ConfigurationError } from "../util"; import { + DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_REF, parseRemoteFileAddress, RemoteFileAddress, @@ -27,6 +28,22 @@ test("expandConfigFileInput accepts full remote addresses", async (t) => { ); }); +test("expandConfigFileInput accepts remote address without a path", async (t) => { + t.deepEqual(parseRemoteFileAddress("owner/repo@ref"), { + owner: "owner", + repo: "repo", + path: DEFAULT_CONFIG_FILE_NAME, + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress("owner/repo"), { + owner: "owner", + repo: "repo", + path: DEFAULT_CONFIG_FILE_NAME, + ref: DEFAULT_CONFIG_FILE_REF, + } satisfies RemoteFileAddress); +}); + test("expandConfigFileInput accepts remote address without a ref", async (t) => { t.deepEqual(parseRemoteFileAddress("owner/repo/path"), { owner: "owner", @@ -47,7 +64,7 @@ test("expandConfigFileInput rejects invalid values", async (t) => { t.throws(() => parseRemoteFileAddress(" "), { instanceOf: ConfigurationError, }); - t.throws(() => parseRemoteFileAddress("repo:/absolute"), { + t.throws(() => parseRemoteFileAddress("repo//absolute"), { instanceOf: ConfigurationError, }); t.throws(() => parseRemoteFileAddress("repo:file.yml:unexpected"), { diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index 9ab89ad4a2..ad34864875 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -13,6 +13,9 @@ export interface RemoteFileAddress { ref: string; } +/** The default file path to use in configuration file shorthands. */ +export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml"; + /** The default ref to use in configuration file shorthands. */ export const DEFAULT_CONFIG_FILE_REF = "main"; @@ -26,16 +29,12 @@ export const DEFAULT_CONFIG_FILE_REF = "main"; export function parseRemoteFileAddress(configFile: string): RemoteFileAddress { // retrieve the various parts of the config location, and ensure they're present const format = new RegExp( - "(?[^/]+)/(?[^/]+)/(?[^@]+)(@(?.*))?", + "(?[^/]+)/(?[^/@]+)(/(?[^@]+))?(@(?.*))?", ); const pieces = format.exec(configFile); // Check that the regular expression matched and that we have at least the required components. - if ( - !pieces?.groups?.owner || - !pieces?.groups?.repo || - !pieces?.groups?.path - ) { + if (!pieces?.groups?.owner || !pieces?.groups?.repo) { throw new ConfigurationError( errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), ); @@ -44,7 +43,7 @@ export function parseRemoteFileAddress(configFile: string): RemoteFileAddress { return { owner: pieces.groups.owner, repo: pieces.groups.repo, - path: pieces.groups.path, + path: pieces.groups.path || DEFAULT_CONFIG_FILE_NAME, ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF, }; } From 12821cff0c52b3d5e5c6601a0903cdecd6a8a7ce Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 17:49:21 +0100 Subject: [PATCH 11/16] Make `owner` optional in `parseRemoteFileAddress` --- lib/entry-points.js | 33 ++++++++++++++--- src/config-utils.test.ts | 28 -------------- src/config/file.ts | 4 +- src/config/remote-file.test.ts | 68 ++++++++++++++++++++++++++++------ src/config/remote-file.ts | 41 +++++++++++++++++--- 5 files changed, 121 insertions(+), 53 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 7ad70e03d8..d1ce66f802 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148660,6 +148660,12 @@ 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) => getOptionalEnvVar(name) + }; +} function getRequiredEnvVar(env, paramName) { const value = env[paramName]; if (value === void 0 || value.length === 0) { @@ -151431,19 +151437,34 @@ function parseUserConfig(logger, pathInput, contents, validateConfig) { // src/config/remote-file.ts var DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml"; var DEFAULT_CONFIG_FILE_REF = "main"; -function parseRemoteFileAddress(configFile) { +function getDefaultOwner(env) { + const currentRepoNwo = env.getRequired("GITHUB_REPOSITORY" /* GITHUB_REPOSITORY */); + const nwoParts = currentRepoNwo.split("/"); + if (nwoParts.length !== 2 || nwoParts[0].trim().length === 0) { + throw new Error( + `Expected ${"GITHUB_REPOSITORY" /* GITHUB_REPOSITORY */} to contain a name with owner, but got '${currentRepoNwo}'.` + ); + } + return nwoParts[0].trim(); +} +function parseRemoteFileAddress(env, configFile) { const format = new RegExp( - "(?[^/]+)/(?[^/@]+)(/(?[^@]+))?(@(?.*))?" + "((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?" ); const pieces = format.exec(configFile); - if (!pieces?.groups?.owner || !pieces?.groups?.repo) { + if (!pieces?.groups?.repo || pieces.groups.repo.trim().length === 0) { throw new ConfigurationError( getConfigFileRepoFormatInvalidMessage(configFile) ); } + if (pieces.groups.path?.startsWith("/")) { + throw new ConfigurationError( + `The path component of '${configFile}' cannot be an absolute path.` + ); + } return { - owner: pieces.groups.owner, - repo: pieces.groups.repo, + owner: pieces.groups.owner || getDefaultOwner(env), + repo: pieces.groups.repo.trim(), path: pieces.groups.path || DEFAULT_CONFIG_FILE_NAME, ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF }; @@ -151466,7 +151487,7 @@ function getConfigFileInput(logger, actions, repositoryProperties) { return void 0; } async function getRemoteConfig(logger, configFile, apiDetails, validateConfig) { - const address = parseRemoteFileAddress(configFile); + const address = parseRemoteFileAddress(getEnv(), configFile); const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({ owner: address.owner, repo: address.repo, diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 27de780ad5..830defa712 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -424,34 +424,6 @@ test.serial("load input outside of workspace", async (t) => { }); }); -test.serial("load non-local input with invalid repo syntax", async (t) => { - return await withTmpDir(async (tempDir) => { - // no filename given, just a repo - const configFile = "octo-org/codeql-config@main"; - - try { - await configUtils.initConfig( - createFeatures([]), - createTestInitConfigInputs({ - configFile, - tempDir, - workspacePath: tempDir, - }), - ); - throw new Error("initConfig did not throw error"); - } catch (err) { - t.deepEqual( - err, - new ConfigurationError( - errorMessages.getConfigFileRepoFormatInvalidMessage( - "octo-org/codeql-config@main", - ), - ), - ); - } - }); -}); - test.serial("load non-existent input", async (t) => { return await withTmpDir(async (tempDir) => { const languagesInput = "javascript"; diff --git a/src/config/file.ts b/src/config/file.ts index 420e418e04..f178386512 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -6,7 +6,7 @@ import { RepositoryPropertyName, } from "../feature-flags/properties"; import { Logger } from "../logging"; -import { ConfigurationError } from "../util"; +import { ConfigurationError, getEnv } from "../util"; import { parseUserConfig, UserConfig } from "./db-config"; import { parseRemoteFileAddress } from "./remote-file"; @@ -55,7 +55,7 @@ export async function getRemoteConfig( apiDetails: api.GitHubApiCombinedDetails, validateConfig: boolean, ): Promise { - const address = parseRemoteFileAddress(configFile); + const address = parseRemoteFileAddress(getEnv(), configFile); const response = await api .getApiClientWithExternalAuth(apiDetails) diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index 457c24120c..456213a1a3 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -1,5 +1,8 @@ import test from "ava"; +import sinon from "sinon"; +import { ActionsEnvVars } from "../actions-util"; +import { getTestEnv } from "../testing-utils"; import { ConfigurationError } from "../util"; import { @@ -10,7 +13,9 @@ import { } from "./remote-file"; test("expandConfigFileInput accepts full remote addresses", async (t) => { - t.deepEqual(parseRemoteFileAddress("owner/repo/path@ref"), { + const env = getTestEnv(); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@ref"), { owner: "owner", repo: "repo", path: "path", @@ -18,7 +23,7 @@ test("expandConfigFileInput accepts full remote addresses", async (t) => { } satisfies RemoteFileAddress); t.deepEqual( - parseRemoteFileAddress("owner/repo/path/to/codeql.yml@ref/feature"), + parseRemoteFileAddress(env, "owner/repo/path/to/codeql.yml@ref/feature"), { owner: "owner", repo: "repo", @@ -28,15 +33,50 @@ test("expandConfigFileInput accepts full remote addresses", async (t) => { ); }); +test("expandConfigFileInput accepts remote address without an owner", async (t) => { + const env = getTestEnv(); + const owner = "test-owner"; + const getRequired = sinon.stub(env, "getRequired"); + getRequired + .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) + .returns(`${owner}/current-repo`); + + t.deepEqual(parseRemoteFileAddress(env, "repo@ref"), { + owner, + repo: "repo", + path: DEFAULT_CONFIG_FILE_NAME, + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "repo"), { + owner, + repo: "repo", + path: DEFAULT_CONFIG_FILE_NAME, + ref: DEFAULT_CONFIG_FILE_REF, + } satisfies RemoteFileAddress); +}); + +test("expandConfigFileInput throws for invalid `GITHUB_REPOSITORY`", async (t) => { + const env = getTestEnv(); + const getRequired = sinon.stub(env, "getRequired"); + getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`); + + t.throws(() => parseRemoteFileAddress(env, "repo@ref"), { + instanceOf: Error, + }); +}); + test("expandConfigFileInput accepts remote address without a path", async (t) => { - t.deepEqual(parseRemoteFileAddress("owner/repo@ref"), { + const env = getTestEnv(); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref"), { owner: "owner", repo: "repo", path: DEFAULT_CONFIG_FILE_NAME, ref: "ref", } satisfies RemoteFileAddress); - t.deepEqual(parseRemoteFileAddress("owner/repo"), { + t.deepEqual(parseRemoteFileAddress(env, "owner/repo"), { owner: "owner", repo: "repo", path: DEFAULT_CONFIG_FILE_NAME, @@ -45,14 +85,16 @@ test("expandConfigFileInput accepts remote address without a path", async (t) => }); test("expandConfigFileInput accepts remote address without a ref", async (t) => { - t.deepEqual(parseRemoteFileAddress("owner/repo/path"), { + const env = getTestEnv(); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path"), { owner: "owner", repo: "repo", path: "path", ref: DEFAULT_CONFIG_FILE_REF, } satisfies RemoteFileAddress); - t.deepEqual(parseRemoteFileAddress("owner/repo/path@"), { + t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@"), { owner: "owner", repo: "repo", path: "path", @@ -61,13 +103,17 @@ test("expandConfigFileInput accepts remote address without a ref", async (t) => }); test("expandConfigFileInput rejects invalid values", async (t) => { - t.throws(() => parseRemoteFileAddress(" "), { - instanceOf: ConfigurationError, - }); - t.throws(() => parseRemoteFileAddress("repo//absolute"), { + const env = getTestEnv(); + const owner = "owner"; + const getRequired = sinon.stub(env, "getRequired"); + getRequired + .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) + .returns(`${owner}/current-repo`); + + t.throws(() => parseRemoteFileAddress(env, " "), { instanceOf: ConfigurationError, }); - t.throws(() => parseRemoteFileAddress("repo:file.yml:unexpected"), { + t.throws(() => parseRemoteFileAddress(env, "repo//absolute"), { instanceOf: ConfigurationError, }); }); diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index ad34864875..9b8e370560 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -1,3 +1,5 @@ +import { ActionsEnvVars } from "../actions-util"; +import { Env } from "../environment"; import * as errorMessages from "../error-messages"; import { ConfigurationError } from "../util"; @@ -19,30 +21,57 @@ export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml"; /** The default ref to use in configuration file shorthands. */ export const DEFAULT_CONFIG_FILE_REF = "main"; +/** Extracts the owner from the `GITHUB_REPOSITORY` environment variable. */ +function getDefaultOwner(env: Env): string { + const currentRepoNwo = env.getRequired(ActionsEnvVars.GITHUB_REPOSITORY); + const nwoParts = currentRepoNwo.split("/"); + + if (nwoParts.length !== 2 || nwoParts[0].trim().length === 0) { + // This shouldn't happen, so we should throw if `GITHUB_REPOSITORY` doesn't match + // our expectations. + throw new Error( + `Expected ${ActionsEnvVars.GITHUB_REPOSITORY} to contain a name with owner, but got '${currentRepoNwo}'.`, + ); + } + + return nwoParts[0].trim(); +} + /** * Attempts to parse `configFile` into an array of `RemoteFileAddress` components. * + * @param env The current environment variables. * @param configFile The string to try and parse. * @returns The successful result of executing the regex. * @throws `ConfigurationError` if the format of `configFile` is not valid. */ -export function parseRemoteFileAddress(configFile: string): RemoteFileAddress { +export function parseRemoteFileAddress( + env: Env, + configFile: string, +): RemoteFileAddress { // retrieve the various parts of the config location, and ensure they're present const format = new RegExp( - "(?[^/]+)/(?[^/@]+)(/(?[^@]+))?(@(?.*))?", + "((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?", ); const pieces = format.exec(configFile); - // Check that the regular expression matched and that we have at least the required components. - if (!pieces?.groups?.owner || !pieces?.groups?.repo) { + // Check that the regular expression matched and that we have at least the repo name. + if (!pieces?.groups?.repo || pieces.groups.repo.trim().length === 0) { throw new ConfigurationError( errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), ); } + // Ensure that the path is a relative path. + if (pieces.groups.path?.startsWith("/")) { + throw new ConfigurationError( + `The path component of '${configFile}' cannot be an absolute path.`, + ); + } + return { - owner: pieces.groups.owner, - repo: pieces.groups.repo, + owner: pieces.groups.owner || getDefaultOwner(env), + repo: pieces.groups.repo.trim(), path: pieces.groups.path || DEFAULT_CONFIG_FILE_NAME, ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF, }; From 81ad479e13cf3898f40b8117f84811b3bba8b8c2 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 18:31:16 +0100 Subject: [PATCH 12/16] Fix test names --- src/config/remote-file.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index 456213a1a3..a914fde0c8 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -12,7 +12,7 @@ import { RemoteFileAddress, } from "./remote-file"; -test("expandConfigFileInput accepts full remote addresses", async (t) => { +test("parseRemoteFileAddress accepts full remote addresses", async (t) => { const env = getTestEnv(); t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@ref"), { @@ -33,7 +33,7 @@ test("expandConfigFileInput accepts full remote addresses", async (t) => { ); }); -test("expandConfigFileInput accepts remote address without an owner", async (t) => { +test("parseRemoteFileAddress accepts remote address without an owner", async (t) => { const env = getTestEnv(); const owner = "test-owner"; const getRequired = sinon.stub(env, "getRequired"); @@ -56,7 +56,7 @@ test("expandConfigFileInput accepts remote address without an owner", async (t) } satisfies RemoteFileAddress); }); -test("expandConfigFileInput throws for invalid `GITHUB_REPOSITORY`", async (t) => { +test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => { const env = getTestEnv(); const getRequired = sinon.stub(env, "getRequired"); getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`); @@ -66,7 +66,7 @@ test("expandConfigFileInput throws for invalid `GITHUB_REPOSITORY`", async (t) = }); }); -test("expandConfigFileInput accepts remote address without a path", async (t) => { +test("parseRemoteFileAddress accepts remote address without a path", async (t) => { const env = getTestEnv(); t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref"), { @@ -84,7 +84,7 @@ test("expandConfigFileInput accepts remote address without a path", async (t) => } satisfies RemoteFileAddress); }); -test("expandConfigFileInput accepts remote address without a ref", async (t) => { +test("parseRemoteFileAddress accepts remote address without a ref", async (t) => { const env = getTestEnv(); t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path"), { @@ -102,7 +102,7 @@ test("expandConfigFileInput accepts remote address without a ref", async (t) => } satisfies RemoteFileAddress); }); -test("expandConfigFileInput rejects invalid values", async (t) => { +test("parseRemoteFileAddress rejects invalid values", async (t) => { const env = getTestEnv(); const owner = "owner"; const getRequired = sinon.stub(env, "getRequired"); From 8102fa6675c005e2d4bf5079b09bb98dc5ed1f54 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 18:32:55 +0100 Subject: [PATCH 13/16] Anchor regex and trim input --- lib/entry-points.js | 4 ++-- src/config/remote-file.test.ts | 13 +++++++++++++ src/config/remote-file.ts | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index d1ce66f802..76c0e27d15 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151449,9 +151449,9 @@ function getDefaultOwner(env) { } function parseRemoteFileAddress(env, configFile) { const format = new RegExp( - "((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?" + "^((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?$" ); - const pieces = format.exec(configFile); + const pieces = format.exec(configFile.trim()); if (!pieces?.groups?.repo || pieces.groups.repo.trim().length === 0) { throw new ConfigurationError( getConfigFileRepoFormatInvalidMessage(configFile) diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index a914fde0c8..1d4ed1f724 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -31,6 +31,19 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => { ref: "ref/feature", } satisfies RemoteFileAddress, ); + + t.deepEqual( + parseRemoteFileAddress( + env, + " owner/repo/path/to/codeql.yml@ref/feature ", + ), + { + owner: "owner", + repo: "repo", + path: "path/to/codeql.yml", + ref: "ref/feature", + } satisfies RemoteFileAddress, + ); }); test("parseRemoteFileAddress accepts remote address without an owner", async (t) => { diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index 9b8e370560..c2303417e8 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -51,9 +51,9 @@ export function parseRemoteFileAddress( ): RemoteFileAddress { // retrieve the various parts of the config location, and ensure they're present const format = new RegExp( - "((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?", + "^((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?$", ); - const pieces = format.exec(configFile); + const pieces = format.exec(configFile.trim()); // Check that the regular expression matched and that we have at least the repo name. if (!pieces?.groups?.repo || pieces.groups.repo.trim().length === 0) { From 00e5a58139c86f1c0da29580717054c1caccd56a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 18:34:25 +0100 Subject: [PATCH 14/16] Update format in error message --- lib/entry-points.js | 2 +- src/error-messages.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 76c0e27d15..05a52df4a0 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151053,7 +151053,7 @@ function getInvalidConfigFileMessage(configFile, messages) { } function getConfigFileRepoFormatInvalidMessage(configFile) { let error3 = `The configuration file "${configFile}" is not a supported remote file reference.`; - error3 += " Expected format //@"; + error3 += " Expected format [/][/][@]"; return error3; } function getConfigFileFormatInvalidMessage(configFile) { diff --git a/src/error-messages.ts b/src/error-messages.ts index 578ec69733..b45d9562b1 100644 --- a/src/error-messages.ts +++ b/src/error-messages.ts @@ -34,7 +34,7 @@ export function getConfigFileRepoFormatInvalidMessage( configFile: string, ): string { let error = `The configuration file "${configFile}" is not a supported remote file reference.`; - error += " Expected format //@"; + error += " Expected format [/][/][@]"; return error; } From 8d69da902e3da6c8d555cb2cb9a96efca8ab2b76 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 18:42:43 +0100 Subject: [PATCH 15/16] Improve whitespace handling --- lib/entry-points.js | 16 +++++++----- src/config/remote-file.test.ts | 48 ++++++++++++++++++++++++++++++++++ src/config/remote-file.ts | 18 ++++++++----- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 05a52df4a0..dff19f1be8 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151452,21 +151452,25 @@ function parseRemoteFileAddress(env, configFile) { "^((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?$" ); const pieces = format.exec(configFile.trim()); - if (!pieces?.groups?.repo || pieces.groups.repo.trim().length === 0) { + const repo = pieces?.groups?.repo?.trim(); + if (!pieces?.groups || !repo || repo.length === 0) { throw new ConfigurationError( getConfigFileRepoFormatInvalidMessage(configFile) ); } - if (pieces.groups.path?.startsWith("/")) { + const owner = pieces.groups.owner?.trim(); + const path29 = pieces.groups.path?.trim(); + const ref = pieces.groups.ref?.trim(); + if (path29?.startsWith("/")) { throw new ConfigurationError( `The path component of '${configFile}' cannot be an absolute path.` ); } return { - owner: pieces.groups.owner || getDefaultOwner(env), - repo: pieces.groups.repo.trim(), - path: pieces.groups.path || DEFAULT_CONFIG_FILE_NAME, - ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF + owner: owner || getDefaultOwner(env), + repo, + path: path29 || DEFAULT_CONFIG_FILE_NAME, + ref: ref || DEFAULT_CONFIG_FILE_REF }; } diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index 1d4ed1f724..36b68fd552 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -22,6 +22,48 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => { ref: "ref", } satisfies RemoteFileAddress); + t.deepEqual(parseRemoteFileAddress(env, "owner /repo/path@ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/ repo/path@ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo /path@ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo/ path@ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path @ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@ ref"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + t.deepEqual( parseRemoteFileAddress(env, "owner/repo/path/to/codeql.yml@ref/feature"), { @@ -129,4 +171,10 @@ test("parseRemoteFileAddress rejects invalid values", async (t) => { t.throws(() => parseRemoteFileAddress(env, "repo//absolute"), { instanceOf: ConfigurationError, }); + t.throws(() => parseRemoteFileAddress(env, "/repo@ref"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, " /repo@ref"), { + instanceOf: ConfigurationError, + }); }); diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index c2303417e8..a4f8ea890a 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -55,24 +55,30 @@ export function parseRemoteFileAddress( ); const pieces = format.exec(configFile.trim()); + const repo: string | undefined = pieces?.groups?.repo?.trim(); + // Check that the regular expression matched and that we have at least the repo name. - if (!pieces?.groups?.repo || pieces.groups.repo.trim().length === 0) { + if (!pieces?.groups || !repo || repo.length === 0) { throw new ConfigurationError( errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), ); } + const owner: string | undefined = pieces.groups.owner?.trim(); + const path: string | undefined = pieces.groups.path?.trim(); + const ref: string | undefined = pieces.groups.ref?.trim(); + // Ensure that the path is a relative path. - if (pieces.groups.path?.startsWith("/")) { + if (path?.startsWith("/")) { throw new ConfigurationError( `The path component of '${configFile}' cannot be an absolute path.`, ); } return { - owner: pieces.groups.owner || getDefaultOwner(env), - repo: pieces.groups.repo.trim(), - path: pieces.groups.path || DEFAULT_CONFIG_FILE_NAME, - ref: pieces.groups.ref || DEFAULT_CONFIG_FILE_REF, + owner: owner || getDefaultOwner(env), + repo, + path: path || DEFAULT_CONFIG_FILE_NAME, + ref: ref || DEFAULT_CONFIG_FILE_REF, }; } From 3fe7ef97d30a60efd078de62d1c03699744655c0 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 23 Jun 2026 18:43:56 +0100 Subject: [PATCH 16/16] Fix `getEnv` not using `getOptionalEnvVarFrom` --- lib/entry-points.js | 2 +- src/util.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index dff19f1be8..c91850e7a7 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148663,7 +148663,7 @@ function initializeEnvironment(version) { function getEnv(env = process.env) { return { getRequired: (name) => getRequiredEnvVar(env, name), - getOptional: (name) => getOptionalEnvVar(name) + getOptional: (name) => getOptionalEnvVarFrom(env, name) }; } function getRequiredEnvVar(env, paramName) { diff --git a/src/util.ts b/src/util.ts index e2632bc957..ed8daaa08d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -570,7 +570,7 @@ export function initializeEnvironment(version: string) { export function getEnv(env: NodeJS.ProcessEnv = process.env): Env { return { getRequired: (name) => getRequiredEnvVar(env, name), - getOptional: (name) => getOptionalEnvVar(name), + getOptional: (name) => getOptionalEnvVarFrom(env, name), }; }