From 0735d7910f37690a85af436f06afded391ee600a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 14:24:12 +0100 Subject: [PATCH 01/15] Improve `GitHubApiDetails` types and documentation --- src/api-client.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/api-client.ts b/src/api-client.ts index 6a5e99468c..80872c31eb 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -43,20 +43,38 @@ const GITHUB_ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version"; */ export const DO_NOT_RETRY_STATUSES = [400, 410, 422, 451]; +/** + * Represents information about a GitHub instance, how to authenticate to it, + * and how to authenticate to other repos on it. + */ export type GitHubApiCombinedDetails = GitHubApiDetails & GitHubApiExternalRepoDetails; -export interface GitHubApiDetails { - auth: string; +/** Represents information about a GitHub instance. */ +export type GitHubInstance = { + /** The base URL of the GitHub instance, such as `https://github.com`. */ url: string; + /** + * The API base URL of the GitHub instance, such as `https://api.github.com`. + * Typically this is the `api.` subdomain of the `url` domain. + */ apiURL: string | undefined; -} +}; + +/** Represents a {@link GitHubInstance} with a token for accessing the API. */ +export type GitHubApiDetails = GitHubInstance & { + /** The token to use for accessing the API of the GitHub instance. */ + auth: string; +}; -export interface GitHubApiExternalRepoDetails { +/** + * Represents information for accessing a repository on the same GitHub instance as the Action + * is currently running on. + */ +export type GitHubApiExternalRepoDetails = { + /** An optional token for accessing repo(s) on the same GitHub instance. */ externalRepoAuth?: string; - url: string; - apiURL: string | undefined; -} +}; /** * Gets the configuration for the private registry authentication proxy, From 102e46e9d1abdb4871aac04e4ed3fec64c04e7aa Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 14:42:12 +0100 Subject: [PATCH 02/15] Move some definitions out of `util.ts` into a new `github.ts` module --- lib/entry-points.js | 60 ++++++++++++++++++----------------- src/api-client.ts | 3 +- src/database-upload.ts | 3 +- src/github.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++ src/github.ts | 62 ++++++++++++++++++++++++++++++++++++ src/setup/repository.ts | 3 +- src/status-report.ts | 2 +- src/util.test.ts | 60 ----------------------------------- src/util.ts | 48 ---------------------------- 9 files changed, 168 insertions(+), 142 deletions(-) create mode 100644 src/github.test.ts create mode 100644 src/github.ts diff --git a/lib/entry-points.js b/lib/entry-points.js index d5e64717a0..891d8b74fd 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -169558,7 +169558,6 @@ function checkSchema(schema, obj, options = {}, path30 = "") { // src/util.ts var BASE_DATABASE_OIDS_FILE_NAME = "base-database-oids.json"; var BROKEN_VERSIONS = ["0.0.0-20211207"]; -var GITHUB_DOTCOM_URL = "https://github.com"; var DEFAULT_DEBUG_ARTIFACT_NAME = "debug-artifacts"; var DEFAULT_DEBUG_DATABASE_NAME = "db"; var DEFAULT_RESERVED_RAM_SCALING_FACTOR = 0.05; @@ -169774,34 +169773,6 @@ function getGeneratedSuitePath(config, language) { "config-queries.qls" ); } -function parseGitHubUrl(inputUrl) { - const originalUrl = inputUrl; - if (inputUrl.indexOf("://") === -1) { - inputUrl = `https://${inputUrl}`; - } - if (!inputUrl.startsWith("http://") && !inputUrl.startsWith("https://")) { - throw new ConfigurationError(`"${originalUrl}" is not a http or https URL`); - } - let url2; - try { - url2 = new URL(inputUrl); - } catch { - throw new ConfigurationError(`"${originalUrl}" is not a valid URL`); - } - if (url2.hostname === "github.com" || url2.hostname === "api.github.com") { - return GITHUB_DOTCOM_URL; - } - if (url2.pathname.indexOf("/api/v3") !== -1) { - url2.pathname = url2.pathname.substring(0, url2.pathname.indexOf("/api/v3")); - } - if (url2.hostname.startsWith("api.")) { - url2.hostname = url2.hostname.substring(4); - } - if (!url2.pathname.endsWith("/")) { - url2.pathname = `${url2.pathname}/`; - } - return url2.toString(); -} var CODEQL_ACTION_WARNED_ABOUT_VERSION_ENV_VAR = "CODEQL_ACTION_WARNED_ABOUT_VERSION"; var hasBeenWarnedAboutVersion = false; function checkGitHubVersionInRange(version, logger2) { @@ -170659,6 +170630,37 @@ retry.VERSION = VERSION7; // src/api-client.ts var import_undici = __toESM(require_undici()); +// src/github.ts +var GITHUB_DOTCOM_URL = "https://github.com"; +function parseGitHubUrl(inputUrl) { + const originalUrl = inputUrl; + if (inputUrl.indexOf("://") === -1) { + inputUrl = `https://${inputUrl}`; + } + if (!inputUrl.startsWith("http://") && !inputUrl.startsWith("https://")) { + throw new ConfigurationError(`"${originalUrl}" is not a http or https URL`); + } + let url2; + try { + url2 = new URL(inputUrl); + } catch { + throw new ConfigurationError(`"${originalUrl}" is not a valid URL`); + } + if (url2.hostname === "github.com" || url2.hostname === "api.github.com") { + return GITHUB_DOTCOM_URL; + } + if (url2.pathname.indexOf("/api/v3") !== -1) { + url2.pathname = url2.pathname.substring(0, url2.pathname.indexOf("/api/v3")); + } + if (url2.hostname.startsWith("api.")) { + url2.hostname = url2.hostname.substring(4); + } + if (!url2.pathname.endsWith("/")) { + url2.pathname = `${url2.pathname}/`; + } + return url2.toString(); +} + // src/repository.ts function isRepositoryNwoStr(val) { return val.includes("/"); diff --git a/src/api-client.ts b/src/api-client.ts index 80872c31eb..8f35d77718 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -19,16 +19,15 @@ import { getEnv, exportEnvVar, } from "./environment"; +import { GITHUB_DOTCOM_URL, parseGitHubUrl } from "./github"; import { Logger } from "./logging"; import { getRepositoryNwo, RepositoryNwo } from "./repository"; import { asHTTPError, ConfigurationError, getRequiredEnvParam, - GITHUB_DOTCOM_URL, GitHubVariant, GitHubVersion, - parseGitHubUrl, parseMatrixInput, } from "./util"; diff --git a/src/database-upload.ts b/src/database-upload.ts index 35dfd90785..d6f8dd1ff0 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -12,11 +12,12 @@ import { type CodeQL } from "./codeql"; import { Config } from "./config-utils"; import { Feature } from "./feature-flags"; import * as gitUtils from "./git-utils"; +import { parseGitHubUrl } from "./github"; import { Logger, withGroupAsync } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { RepositoryNwo } from "./repository"; import * as util from "./util"; -import { asHTTPError, bundleDb, CleanupLevel, parseGitHubUrl } from "./util"; +import { asHTTPError, bundleDb, CleanupLevel } from "./util"; /** Information about a database upload. */ export interface DatabaseUploadResult { diff --git a/src/github.test.ts b/src/github.test.ts new file mode 100644 index 0000000000..8645f493bb --- /dev/null +++ b/src/github.test.ts @@ -0,0 +1,69 @@ +import test from "ava"; + +import * as github from "./github"; +import { setupTests } from "./testing-utils"; + +setupTests(test); + +test("parseGitHubUrl", (t) => { + t.deepEqual(github.parseGitHubUrl("github.com"), "https://github.com"); + t.deepEqual( + github.parseGitHubUrl("https://github.com"), + "https://github.com", + ); + t.deepEqual( + github.parseGitHubUrl("https://api.github.com"), + "https://github.com", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.com/foo/bar"), + "https://github.com", + ); + + t.deepEqual( + github.parseGitHubUrl("github.example.com"), + "https://github.example.com/", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.example.com"), + "https://github.example.com/", + ); + t.deepEqual( + github.parseGitHubUrl("https://api.github.example.com"), + "https://github.example.com/", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.example.com/api/v3"), + "https://github.example.com/", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.example.com:1234"), + "https://github.example.com:1234/", + ); + t.deepEqual( + github.parseGitHubUrl("https://api.github.example.com:1234"), + "https://github.example.com:1234/", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.example.com:1234/api/v3"), + "https://github.example.com:1234/", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.example.com/base/path"), + "https://github.example.com/base/path/", + ); + t.deepEqual( + github.parseGitHubUrl("https://github.example.com/base/path/api/v3"), + "https://github.example.com/base/path/", + ); + + t.throws(() => github.parseGitHubUrl(""), { + message: '"" is not a valid URL', + }); + t.throws(() => github.parseGitHubUrl("ssh://github.com"), { + message: '"ssh://github.com" is not a http or https URL', + }); + t.throws(() => github.parseGitHubUrl("http:///::::433"), { + message: '"http:///::::433" is not a valid URL', + }); +}); diff --git a/src/github.ts b/src/github.ts new file mode 100644 index 0000000000..00d2f34351 --- /dev/null +++ b/src/github.ts @@ -0,0 +1,62 @@ +import type { GitHubInstance } from "./api-client"; +import { ActionsEnvVars, type ReadOnlyEnv } from "./environment"; +import { ConfigurationError } from "./util"; + +/** A string representing a GitHub instance address. */ +export type InstanceURL = string; + +/** + * The URL for github.com. + */ +export const GITHUB_DOTCOM_URL: InstanceURL = "https://github.com"; + +/** A string representing a GitHub API instance address. */ +export type InstanceApiURL = string; + +/** + * The URL for api.github.com. + */ +export const GITHUB_DOTCOM_API_URL: InstanceApiURL = "https://api.github.com"; + +/** + * Parses user input of a github.com or GHES URL to a canonical form. + * Removes any API prefix or suffix if one is present. + */ +export function parseGitHubUrl(inputUrl: string): string { + const originalUrl = inputUrl; + if (inputUrl.indexOf("://") === -1) { + inputUrl = `https://${inputUrl}`; + } + if (!inputUrl.startsWith("http://") && !inputUrl.startsWith("https://")) { + throw new ConfigurationError(`"${originalUrl}" is not a http or https URL`); + } + + let url: URL; + try { + url = new URL(inputUrl); + } catch { + throw new ConfigurationError(`"${originalUrl}" is not a valid URL`); + } + + // If we detect this is trying to be to github.com + // then return with a fixed canonical URL. + if (url.hostname === "github.com" || url.hostname === "api.github.com") { + return GITHUB_DOTCOM_URL; + } + + // Remove the API prefix if it's present + if (url.pathname.indexOf("/api/v3") !== -1) { + url.pathname = url.pathname.substring(0, url.pathname.indexOf("/api/v3")); + } + // Also consider subdomain isolation on GHES + if (url.hostname.startsWith("api.")) { + url.hostname = url.hostname.substring(4); + } + + // Normalise path to having a trailing slash for consistency + if (!url.pathname.endsWith("/")) { + url.pathname = `${url.pathname}/`; + } + + return url.toString(); +} diff --git a/src/setup/repository.ts b/src/setup/repository.ts index 6da0ea3e9a..341a278755 100644 --- a/src/setup/repository.ts +++ b/src/setup/repository.ts @@ -2,12 +2,13 @@ import { ActionState } from "../action-common"; import { isRunningLocalAction } from "../actions-util"; import * as api from "../api-client"; import { ActionsEnvVars } from "../environment"; +import { GITHUB_DOTCOM_URL } from "../github"; import { isRepositoryNwoStr, parseRepositoryNwo, type RepositoryNwoStr, } from "../repository"; -import { GITHUB_DOTCOM_URL, type InstanceURL } from "../util"; +import { type InstanceURL } from "../util"; /** The NWO of the standard CodeQL Action repository. */ export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; diff --git a/src/status-report.ts b/src/status-report.ts index 0731acbe61..9420aa2a10 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -31,6 +31,7 @@ import { exportEnvVar, } from "./environment"; import { getRef } from "./git-utils"; +import { GITHUB_DOTCOM_URL } from "./github"; import * as json from "./json"; import type { Logger } from "./logging"; import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching"; @@ -41,7 +42,6 @@ import { ConfigurationError, getRequiredEnvParam, isInTestMode, - GITHUB_DOTCOM_URL, DiskUsage, assertNever, BuildMode, diff --git a/src/util.test.ts b/src/util.test.ts index 074310279f..bca2b452e7 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -164,66 +164,6 @@ test.serial("getExtraOptionsEnvParam() fails on invalid JSON", (t) => { process.env.CODEQL_ACTION_EXTRA_OPTIONS = origExtraOptions; }); -test("parseGitHubUrl", (t) => { - t.deepEqual(util.parseGitHubUrl("github.com"), "https://github.com"); - t.deepEqual(util.parseGitHubUrl("https://github.com"), "https://github.com"); - t.deepEqual( - util.parseGitHubUrl("https://api.github.com"), - "https://github.com", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.com/foo/bar"), - "https://github.com", - ); - - t.deepEqual( - util.parseGitHubUrl("github.example.com"), - "https://github.example.com/", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.example.com"), - "https://github.example.com/", - ); - t.deepEqual( - util.parseGitHubUrl("https://api.github.example.com"), - "https://github.example.com/", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.example.com/api/v3"), - "https://github.example.com/", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.example.com:1234"), - "https://github.example.com:1234/", - ); - t.deepEqual( - util.parseGitHubUrl("https://api.github.example.com:1234"), - "https://github.example.com:1234/", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.example.com:1234/api/v3"), - "https://github.example.com:1234/", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.example.com/base/path"), - "https://github.example.com/base/path/", - ); - t.deepEqual( - util.parseGitHubUrl("https://github.example.com/base/path/api/v3"), - "https://github.example.com/base/path/", - ); - - t.throws(() => util.parseGitHubUrl(""), { - message: '"" is not a valid URL', - }); - t.throws(() => util.parseGitHubUrl("ssh://github.com"), { - message: '"ssh://github.com" is not a http or https URL', - }); - t.throws(() => util.parseGitHubUrl("http:///::::433"), { - message: '"http:///::::433" is not a valid URL', - }); -}); - test("allowed API versions", async (t) => { t.is(util.apiVersionInRange("1.33.0", "1.33", "2.0"), undefined); t.is(util.apiVersionInRange("1.33.1", "1.33", "2.0"), undefined); diff --git a/src/util.ts b/src/util.ts index c7bd9e8d3a..02e105d577 100644 --- a/src/util.ts +++ b/src/util.ts @@ -50,11 +50,6 @@ const BROKEN_VERSIONS = ["0.0.0-20211207"]; /** A string representing a GitHub instance. */ export type InstanceURL = string; -/** - * The URL for github.com. - */ -export const GITHUB_DOTCOM_URL: InstanceURL = "https://github.com"; - /** * Default name of the debugging artifact. */ @@ -470,49 +465,6 @@ export function getGeneratedSuitePath(config: Config, language: Language) { ); } -/** - * Parses user input of a github.com or GHES URL to a canonical form. - * Removes any API prefix or suffix if one is present. - */ -export function parseGitHubUrl(inputUrl: string): string { - const originalUrl = inputUrl; - if (inputUrl.indexOf("://") === -1) { - inputUrl = `https://${inputUrl}`; - } - if (!inputUrl.startsWith("http://") && !inputUrl.startsWith("https://")) { - throw new ConfigurationError(`"${originalUrl}" is not a http or https URL`); - } - - let url: URL; - try { - url = new URL(inputUrl); - } catch { - throw new ConfigurationError(`"${originalUrl}" is not a valid URL`); - } - - // If we detect this is trying to be to github.com - // then return with a fixed canonical URL. - if (url.hostname === "github.com" || url.hostname === "api.github.com") { - return GITHUB_DOTCOM_URL; - } - - // Remove the API prefix if it's present - if (url.pathname.indexOf("/api/v3") !== -1) { - url.pathname = url.pathname.substring(0, url.pathname.indexOf("/api/v3")); - } - // Also consider subdomain isolation on GHES - if (url.hostname.startsWith("api.")) { - url.hostname = url.hostname.substring(4); - } - - // Normalise path to having a trailing slash for consistency - if (!url.pathname.endsWith("/")) { - url.pathname = `${url.pathname}/`; - } - - return url.toString(); -} - const CODEQL_ACTION_WARNED_ABOUT_VERSION_ENV_VAR = "CODEQL_ACTION_WARNED_ABOUT_VERSION"; From 61bfcffab940433010be7ea4a590ab7b79af33fc Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 14:48:24 +0100 Subject: [PATCH 03/15] Add functions to get the current and dotcom instance --- src/github.test.ts | 44 +++++++++++++++++++++++++++++++++++++++++++- src/github.ts | 16 ++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/github.test.ts b/src/github.test.ts index 8645f493bb..79dbb23311 100644 --- a/src/github.test.ts +++ b/src/github.test.ts @@ -1,7 +1,8 @@ import test from "ava"; +import { ActionsEnvVars } from "./environment"; import * as github from "./github"; -import { setupTests } from "./testing-utils"; +import { getTestEnv, setupTests } from "./testing-utils"; setupTests(test); @@ -67,3 +68,44 @@ test("parseGitHubUrl", (t) => { message: '"http:///::::433" is not a valid URL', }); }); + +test("getCurrentInstance throws if required env vars aren't set", (t) => { + t.throws(() => github.getCurrentInstance(getTestEnv())); + t.throws(() => { + const env = getTestEnv({ + [ActionsEnvVars.GITHUB_SERVER_URL]: "https://localhost", + }); + github.getCurrentInstance(env); + }); + t.throws(() => { + const env = getTestEnv({ + [ActionsEnvVars.GITHUB_API_URL]: "https://api.localhost", + }); + github.getCurrentInstance(env); + }); +}); + +test("getCurrentInstance constructs instance from environment", (t) => { + const url = "https://localhost"; + const apiUrl = "https://api.localhost"; + + const env = getTestEnv({ + [ActionsEnvVars.GITHUB_SERVER_URL]: url, + [ActionsEnvVars.GITHUB_API_URL]: apiUrl, + }); + const instance = github.getCurrentInstance(env); + + t.is(instance.url, url); + t.is(instance.apiURL, apiUrl); +}); + +test("getCurrentInstance constructs dotcom instance with dotcom env vars", (t) => { + const env = getTestEnv({ + [ActionsEnvVars.GITHUB_SERVER_URL]: github.GITHUB_DOTCOM_URL, + [ActionsEnvVars.GITHUB_API_URL]: github.GITHUB_DOTCOM_API_URL, + }); + const instance = github.getCurrentInstance(env); + const dotcomInstance = github.getDotcomInstance(); + + t.deepEqual(dotcomInstance, instance); +}); diff --git a/src/github.ts b/src/github.ts index 00d2f34351..8a5a2c389c 100644 --- a/src/github.ts +++ b/src/github.ts @@ -60,3 +60,19 @@ export function parseGitHubUrl(inputUrl: string): string { return url.toString(); } + +/** Retrieves a {@link GitHubInstance} describing the instance we are running on. */ +export function getCurrentInstance(env: ReadOnlyEnv): GitHubInstance { + return { + url: env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), + apiURL: env.getRequired(ActionsEnvVars.GITHUB_API_URL), + }; +} + +/** Retrieves a {@link GitHubInstance} describing the GitHub.com instance. */ +export function getDotcomInstance(): GitHubInstance { + return { + url: GITHUB_DOTCOM_URL, + apiURL: GITHUB_DOTCOM_API_URL, + }; +} From e0625309c3719bb247f65c530f610b0070b0b4c9 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 17:16:32 +0100 Subject: [PATCH 04/15] Move `GitHubApiDetails` to `github.ts` --- src/api-client.ts | 42 ++++++++---------------------------------- src/github.ts | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/api-client.ts b/src/api-client.ts index 8f35d77718..c734261bf6 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -19,7 +19,12 @@ import { getEnv, exportEnvVar, } from "./environment"; -import { GITHUB_DOTCOM_URL, parseGitHubUrl } from "./github"; +import { + GITHUB_DOTCOM_URL, + parseGitHubUrl, + type GitHubApiCombinedDetails, + type GitHubApiDetails, +} from "./github"; import { Logger } from "./logging"; import { getRepositoryNwo, RepositoryNwo } from "./repository"; import { @@ -31,6 +36,8 @@ import { parseMatrixInput, } from "./util"; +export type { GitHubApiDetails, GitHubApiCombinedDetails } from "./github"; + const GITHUB_ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version"; /** @@ -42,39 +49,6 @@ const GITHUB_ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version"; */ export const DO_NOT_RETRY_STATUSES = [400, 410, 422, 451]; -/** - * Represents information about a GitHub instance, how to authenticate to it, - * and how to authenticate to other repos on it. - */ -export type GitHubApiCombinedDetails = GitHubApiDetails & - GitHubApiExternalRepoDetails; - -/** Represents information about a GitHub instance. */ -export type GitHubInstance = { - /** The base URL of the GitHub instance, such as `https://github.com`. */ - url: string; - /** - * The API base URL of the GitHub instance, such as `https://api.github.com`. - * Typically this is the `api.` subdomain of the `url` domain. - */ - apiURL: string | undefined; -}; - -/** Represents a {@link GitHubInstance} with a token for accessing the API. */ -export type GitHubApiDetails = GitHubInstance & { - /** The token to use for accessing the API of the GitHub instance. */ - auth: string; -}; - -/** - * Represents information for accessing a repository on the same GitHub instance as the Action - * is currently running on. - */ -export type GitHubApiExternalRepoDetails = { - /** An optional token for accessing repo(s) on the same GitHub instance. */ - externalRepoAuth?: string; -}; - /** * Gets the configuration for the private registry authentication proxy, * if it is available in the environment. diff --git a/src/github.ts b/src/github.ts index 8a5a2c389c..4eef113de8 100644 --- a/src/github.ts +++ b/src/github.ts @@ -1,4 +1,3 @@ -import type { GitHubInstance } from "./api-client"; import { ActionsEnvVars, type ReadOnlyEnv } from "./environment"; import { ConfigurationError } from "./util"; @@ -18,6 +17,39 @@ export type InstanceApiURL = string; */ export const GITHUB_DOTCOM_API_URL: InstanceApiURL = "https://api.github.com"; +/** + * Represents information about a GitHub instance, how to authenticate to it, + * and how to authenticate to other repos on it. + */ +export type GitHubApiCombinedDetails = GitHubApiDetails & + GitHubApiExternalRepoDetails; + +/** Represents information about a GitHub instance. */ +export type GitHubInstance = { + /** The base URL of the GitHub instance, such as `https://github.com`. */ + url: string; + /** + * The API base URL of the GitHub instance, such as `https://api.github.com`. + * Typically this is the `api.` subdomain of the `url` domain. + */ + apiURL: string | undefined; +}; + +/** Represents a {@link GitHubInstance} with a token for accessing the API. */ +export type GitHubApiDetails = GitHubInstance & { + /** The token to use for accessing the API of the GitHub instance. */ + auth: string; +}; + +/** + * Represents information for accessing a repository on the same GitHub instance as the Action + * is currently running on. + */ +export type GitHubApiExternalRepoDetails = { + /** An optional token for accessing repo(s) on the same GitHub instance. */ + externalRepoAuth?: string; +}; + /** * Parses user input of a github.com or GHES URL to a canonical form. * Removes any API prefix or suffix if one is present. From a2438139ceded157f810d8253ce354b7ead36f56 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 15:32:43 +0100 Subject: [PATCH 05/15] Make `auth` property on `GitHubApiDetails` optional --- lib/entry-points.js | 40 ++++++++++++++++++++-------------------- src/api-client.ts | 35 +++++++++++++++++++---------------- src/codeql.test.ts | 2 +- src/github.ts | 2 +- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 891d8b74fd..0a0e56eec8 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -26401,7 +26401,7 @@ var require_utils4 = __commonJS({ })(); Object.defineProperty(exports2, "__esModule", { value: true }); exports2.GitHub = exports2.defaults = exports2.context = void 0; - exports2.getOctokitOptions = getOctokitOptions2; + exports2.getOctokitOptions = getOctokitOptions; var Context = __importStar2(require_context()); var Utils = __importStar2(require_utils3()); var core_1 = (init_dist_src2(), __toCommonJS(dist_src_exports)); @@ -26417,7 +26417,7 @@ var require_utils4 = __commonJS({ } }; exports2.GitHub = core_1.Octokit.plugin(plugin_rest_endpoint_methods_1.restEndpointMethods, plugin_paginate_rest_1.paginateRest).defaults(exports2.defaults); - function getOctokitOptions2(token, options) { + function getOctokitOptions(token, options) { const opts = Object.assign({}, options || {}); const auth2 = Utils.getAuthString(token, opts); if (auth2) { @@ -140010,7 +140010,7 @@ var require_utils10 = __commonJS({ } }; exports2.GitHub = core_1.Octokit.plugin(plugin_rest_endpoint_methods_1.restEndpointMethods, plugin_paginate_rest_1.paginateRest).defaults(exports2.defaults); - function getOctokitOptions2(token, options) { + function getOctokitOptions(token, options) { const opts = Object.assign({}, options || {}); const auth2 = Utils.getAuthString(token, opts); if (auth2) { @@ -140018,7 +140018,7 @@ var require_utils10 = __commonJS({ } return opts; } - exports2.getOctokitOptions = getOctokitOptions2; + exports2.getOctokitOptions = getOctokitOptions; } }); @@ -170729,22 +170729,22 @@ function createApiClientWithDetails(apiDetails, { allowExternal = false, proxy = const auth2 = allowExternal && apiDetails.externalRepoAuth || apiDetails.auth; const retryingOctokit = githubUtils.GitHub.plugin(retry); const requestOptions = makeProxyRequestOptions(proxy); - return new retryingOctokit( - githubUtils.getOctokitOptions(auth2, { - baseUrl: apiDetails.apiURL, - userAgent: `CodeQL-Action/${getActionVersion()}`, - log: { - debug: core6.debug, - info: core6.info, - warn: core6.warning, - error: core6.error - }, - request: requestOptions, - retry: { - doNotRetry: DO_NOT_RETRY_STATUSES - } - }) - ); + const opts = { + auth: auth2 !== void 0 ? `token ${auth2}` : void 0, + baseUrl: apiDetails.apiURL, + userAgent: `CodeQL-Action/${getActionVersion()}`, + log: { + debug: core6.debug, + info: core6.info, + warn: core6.warning, + error: core6.error + }, + request: requestOptions, + retry: { + doNotRetry: DO_NOT_RETRY_STATUSES + } + }; + return new retryingOctokit(opts); } function getApiDetails(env = getEnv()) { return { diff --git a/src/api-client.ts b/src/api-client.ts index c734261bf6..e1e5dcc0a4 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -1,5 +1,6 @@ import * as core from "@actions/core"; import * as githubUtils from "@actions/github/lib/utils"; +import type { OctokitOptions } from "@octokit/core"; import * as retry from "@octokit/plugin-retry"; import { RequestRequestOptions } from "@octokit/types"; import { @@ -133,22 +134,24 @@ function createApiClientWithDetails( (allowExternal && apiDetails.externalRepoAuth) || apiDetails.auth; const retryingOctokit = githubUtils.GitHub.plugin(retry.retry); const requestOptions = makeProxyRequestOptions(proxy); - return new retryingOctokit( - githubUtils.getOctokitOptions(auth, { - baseUrl: apiDetails.apiURL, - userAgent: `CodeQL-Action/${getActionVersion()}`, - log: { - debug: core.debug, - info: core.info, - warn: core.warning, - error: core.error, - }, - request: requestOptions, - retry: { - doNotRetry: DO_NOT_RETRY_STATUSES, - }, - }), - ); + + const opts: OctokitOptions = { + auth: auth !== undefined ? `token ${auth}` : undefined, + baseUrl: apiDetails.apiURL, + userAgent: `CodeQL-Action/${getActionVersion()}`, + log: { + debug: core.debug, + info: core.info, + warn: core.warning, + error: core.error, + }, + request: requestOptions, + retry: { + doNotRetry: DO_NOT_RETRY_STATUSES, + }, + }; + + return new retryingOctokit(opts); } export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails { diff --git a/src/codeql.test.ts b/src/codeql.test.ts index 3237d57723..e70dcd99f9 100644 --- a/src/codeql.test.ts +++ b/src/codeql.test.ts @@ -126,7 +126,7 @@ function mockReleaseApi({ }); } -function mockApiDetails(apiDetails: GitHubApiDetails) { +function mockApiDetails(apiDetails: Required) { // This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to // mock this directly. The difficulty is that `getApiDetails()` is called locally in // `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to diff --git a/src/github.ts b/src/github.ts index 4eef113de8..6350fb3f30 100644 --- a/src/github.ts +++ b/src/github.ts @@ -38,7 +38,7 @@ export type GitHubInstance = { /** Represents a {@link GitHubInstance} with a token for accessing the API. */ export type GitHubApiDetails = GitHubInstance & { /** The token to use for accessing the API of the GitHub instance. */ - auth: string; + auth?: string; }; /** From 96501a33aca65626acc8abecc8828308ea02e5d0 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 17:28:17 +0100 Subject: [PATCH 06/15] Use `getCurrentInstance()` --- lib/entry-points.js | 19 +++++++++++-------- src/api-client.ts | 10 +++++++--- src/init-action.ts | 4 ++-- src/setup-codeql-action.ts | 6 +++--- src/upload-lib.ts | 7 ++++--- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 0a0e56eec8..47dc7c001c 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -170660,6 +170660,12 @@ function parseGitHubUrl(inputUrl) { } return url2.toString(); } +function getCurrentInstance(env) { + return { + url: env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), + apiURL: env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) + }; +} // src/repository.ts function isRepositoryNwoStr(val) { @@ -170749,8 +170755,7 @@ function createApiClientWithDetails(apiDetails, { allowExternal = false, proxy = function getApiDetails(env = getEnv()) { return { auth: getRequiredInput("token"), - url: env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), - apiURL: env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) + ...getCurrentInstance(env) }; } function getApiClient(env = getEnv()) { @@ -180013,6 +180018,7 @@ async function shouldDisableCombineSarifFiles(sarifObjects, githubVersion) { return true; } async function combineSarifFilesUsingCLI(sarifFiles, gitHubVersion, features, logger2) { + const env = getEnv(); logger2.info("Combining SARIF files using the CodeQL CLI"); const sarifObjects = sarifFiles.map(readSarifFile); const deprecationWarningMessage = gitHubVersion.type === "GitHub Enterprise Server" /* GHES */ ? "and will be removed in GitHub Enterprise Server 3.18" : "and will be removed in July 2025"; @@ -180045,8 +180051,7 @@ async function combineSarifFilesUsingCLI(sarifFiles, gitHubVersion, features, lo externalRepoAuth: getOptionalInput( "external-repository-token" ), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL") + ...getCurrentInstance(env) }; const codeQLDefaultVersionInfo = await features.getEnabledDefaultCliVersions(gitHubVersion.type); const initCodeQLResult = await initCodeQL( @@ -186452,8 +186457,7 @@ async function run3(actionState) { apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: actionState.env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), - apiURL: actionState.env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) + ...getCurrentInstance(actionState.env) }; const gitHubVersion = await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger2); @@ -187495,8 +187499,7 @@ async function run6(actionState) { const apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: actionState.env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), - apiURL: actionState.env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) + ...getCurrentInstance(actionState.env) }; const gitHubVersion = await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger2); diff --git a/src/api-client.ts b/src/api-client.ts index e1e5dcc0a4..48313dbba9 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -13,7 +13,6 @@ import { import type { ActionState } from "./action-common"; import { getActionVersion, getRequiredInput } from "./actions-util"; import { - ActionsEnvVars, EnvVar, ReadOnlyEnv, RegistryProxyVars, @@ -21,6 +20,7 @@ import { exportEnvVar, } from "./environment"; import { + getCurrentInstance, GITHUB_DOTCOM_URL, parseGitHubUrl, type GitHubApiCombinedDetails, @@ -154,11 +154,15 @@ function createApiClientWithDetails( return new retryingOctokit(opts); } +/** + * Gets a `GitHubApiDetails` object describing the GitHub instance we are currently running on. + * + * @param env The environment variables to read. + */ export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails { return { auth: getRequiredInput("token"), - url: env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), - apiURL: env.getRequired(ActionsEnvVars.GITHUB_API_URL), + ...getCurrentInstance(env), }; } diff --git a/src/init-action.ts b/src/init-action.ts index 5376763b52..f58d4ccca7 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -50,6 +50,7 @@ import { loadRepositoryProperties, RepositoryPropertyName, } from "./feature-flags/properties"; +import * as github from "./github"; import { checkInstallPython311, checkPacksForOverlayCompatibility, @@ -233,8 +234,7 @@ async function run( apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), - apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL), + ...github.getCurrentInstance(actionState.env), }; const gitHubVersion = await getGitHubVersion(); diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 1344b66b70..757b26fac5 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -17,9 +17,10 @@ import { getGitHubVersion } from "./api-client"; import { CodeQL } from "./codeql"; import { ComputedInput, getToolsInput } from "./config/inputs"; import { getRawLanguagesNoAutodetect } from "./config-utils"; -import { ActionsEnvVars, EnvVar } from "./environment"; +import { EnvVar } from "./environment"; import { initFeatures } from "./feature-flags"; import { loadRepositoryProperties } from "./feature-flags/properties"; +import * as github from "./github"; import { initCodeQL } from "./init"; import { Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; @@ -112,8 +113,7 @@ async function run( const apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), - apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL), + ...github.getCurrentInstance(actionState.env), }; const gitHubVersion = await getGitHubVersion(); diff --git a/src/upload-lib.ts b/src/upload-lib.ts index 21f3417ff2..3c46c15198 100644 --- a/src/upload-lib.ts +++ b/src/upload-lib.ts @@ -18,6 +18,7 @@ import { ActionsEnvVars, EnvVar, exportEnvVar } from "./environment"; import { FeatureEnablement } from "./feature-flags"; import * as fingerprints from "./fingerprints"; import * as gitUtils from "./git-utils"; +import * as github from "./github"; import { initCodeQL } from "./init"; import { Logger } from "./logging"; import { getRepositoryNwo, RepositoryNwo } from "./repository"; @@ -33,7 +34,6 @@ import * as util from "./util"; import { ConfigurationError, getErrorMessage, - getRequiredEnvParam, GitHubVariant, GitHubVersion, satisfiesGHESVersion, @@ -104,6 +104,8 @@ async function combineSarifFilesUsingCLI( features: FeatureEnablement, logger: Logger, ): Promise> { + const env = util.getEnv(); + logger.info("Combining SARIF files using the CodeQL CLI"); const sarifObjects = sarifFiles.map(sarif.readSarifFile); @@ -152,8 +154,7 @@ async function combineSarifFilesUsingCLI( externalRepoAuth: actionsUtil.getOptionalInput( "external-repository-token", ), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL"), + ...github.getCurrentInstance(env), }; const codeQLDefaultVersionInfo = From 1b0a6176418fa9e5a76c322d979238da25860f29 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 18:06:52 +0100 Subject: [PATCH 07/15] Make `externalRepoAuth` check more consistent --- lib/entry-points.js | 2 +- src/api-client.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 47dc7c001c..bd5da1bf62 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -170732,7 +170732,7 @@ function makeProxyRequestOptions(dispatcher) { }; } function createApiClientWithDetails(apiDetails, { allowExternal = false, proxy = void 0 } = {}) { - const auth2 = allowExternal && apiDetails.externalRepoAuth || apiDetails.auth; + const auth2 = allowExternal && apiDetails.externalRepoAuth !== void 0 ? apiDetails.externalRepoAuth : apiDetails.auth; const retryingOctokit = githubUtils.GitHub.plugin(retry); const requestOptions = makeProxyRequestOptions(proxy); const opts = { diff --git a/src/api-client.ts b/src/api-client.ts index 48313dbba9..31169480af 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -131,7 +131,9 @@ function createApiClientWithDetails( { allowExternal = false, proxy = undefined }: CreateApiClientOptions = {}, ): ApiClient { const auth = - (allowExternal && apiDetails.externalRepoAuth) || apiDetails.auth; + allowExternal && apiDetails.externalRepoAuth !== undefined + ? apiDetails.externalRepoAuth + : apiDetails.auth; const retryingOctokit = githubUtils.GitHub.plugin(retry.retry); const requestOptions = makeProxyRequestOptions(proxy); From 1d30a842cdeeeefc0329cf66b8c22f08f1d1a99c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 13 Oct 2026 18:05:49 +0100 Subject: [PATCH 08/15] Guard against blank `auth` value This shouldn't be possible, but was previously handled by the library --- lib/entry-points.js | 2 +- src/api-client.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index bd5da1bf62..19f324e0f1 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -170736,7 +170736,7 @@ function createApiClientWithDetails(apiDetails, { allowExternal = false, proxy = const retryingOctokit = githubUtils.GitHub.plugin(retry); const requestOptions = makeProxyRequestOptions(proxy); const opts = { - auth: auth2 !== void 0 ? `token ${auth2}` : void 0, + auth: auth2 !== void 0 && auth2.trim() !== "" ? `token ${auth2}` : void 0, baseUrl: apiDetails.apiURL, userAgent: `CodeQL-Action/${getActionVersion()}`, log: { diff --git a/src/api-client.ts b/src/api-client.ts index 31169480af..4d0a999206 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -138,7 +138,8 @@ function createApiClientWithDetails( const requestOptions = makeProxyRequestOptions(proxy); const opts: OctokitOptions = { - auth: auth !== undefined ? `token ${auth}` : undefined, + auth: + auth !== undefined && auth.trim() !== "" ? `token ${auth}` : undefined, baseUrl: apiDetails.apiURL, userAgent: `CodeQL-Action/${getActionVersion()}`, log: { From 66c96fdc352a0a789e220cc8bf958e8a5fcb516c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 11:13:41 +0100 Subject: [PATCH 09/15] Check `auth` value in `getAuthorizationHeaderFor` Also add some tests for it --- lib/entry-points.js | 6 +++- src/api-client.test.ts | 80 +++++++++++++++++++++++++++++++++++++++++- src/api-client.ts | 8 ++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 19f324e0f1..681f56b89c 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -170765,9 +170765,13 @@ function getApiClientWithExternalAuth(apiDetails, proxy) { return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy }); } function getAuthorizationHeaderFor(logger2, apiDetails, url2) { + const token = apiDetails.auth?.trim(); + if (token === void 0 || token === "") { + return void 0; + } if (url2.startsWith(`${apiDetails.url}/`) || apiDetails.apiURL && url2.startsWith(`${apiDetails.apiURL}/`)) { logger2.debug(`Providing an authorization token.`); - return `token ${apiDetails.auth}`; + return `token ${token}`; } logger2.debug(`Not using an authorization token.`); return void 0; diff --git a/src/api-client.test.ts b/src/api-client.test.ts index 12fe819856..1a37425e4c 100644 --- a/src/api-client.test.ts +++ b/src/api-client.test.ts @@ -8,7 +8,14 @@ import * as actionsUtil from "./actions-util"; import * as api from "./api-client"; import { DO_NOT_RETRY_STATUSES } from "./api-client"; import { ActionsEnvVars, RegistryProxyVars } from "./environment"; -import { callee, getTestEnv, setupTests } from "./testing-utils"; +import { GITHUB_DOTCOM_API_URL, GITHUB_DOTCOM_URL } from "./github"; +import { + callee, + getTestEnv, + RecordingLogger, + SAMPLE_DOTCOM_API_DETAILS, + setupTests, +} from "./testing-utils"; import * as util from "./util"; setupTests(test); @@ -284,3 +291,74 @@ test("makeProxyRequestOptions - returns fetch with custom proxy", async (t) => { githubUtils.defaults.request, ); }); + +test("getAuthorizationHeaderFor - returns undefined if there's no token", (t) => { + const logger = new RecordingLogger(); + + t.is( + api.getAuthorizationHeaderFor( + logger, + { ...SAMPLE_DOTCOM_API_DETAILS, auth: undefined }, + `${GITHUB_DOTCOM_URL}/github`, + ), + undefined, + ); + t.is( + api.getAuthorizationHeaderFor( + logger, + { ...SAMPLE_DOTCOM_API_DETAILS, auth: "" }, + `${GITHUB_DOTCOM_URL}/github`, + ), + undefined, + ); + t.is( + api.getAuthorizationHeaderFor( + logger, + { ...SAMPLE_DOTCOM_API_DETAILS, auth: " " }, + `${GITHUB_DOTCOM_URL}/github`, + ), + undefined, + ); +}); + +test("getAuthorizationHeaderFor - returns header value with token if url is allowed", (t) => { + const logger = new RecordingLogger(); + + t.is( + api.getAuthorizationHeaderFor( + logger, + SAMPLE_DOTCOM_API_DETAILS, + `${GITHUB_DOTCOM_URL}/github`, + ), + `token ${SAMPLE_DOTCOM_API_DETAILS.auth}`, + ); + t.is( + api.getAuthorizationHeaderFor( + logger, + SAMPLE_DOTCOM_API_DETAILS, + `${GITHUB_DOTCOM_API_URL}/github`, + ), + `token ${SAMPLE_DOTCOM_API_DETAILS.auth}`, + ); +}); + +test("getAuthorizationHeaderFor - returns undefined if url is not allowed", (t) => { + const logger = new RecordingLogger(); + + t.is( + api.getAuthorizationHeaderFor( + logger, + SAMPLE_DOTCOM_API_DETAILS, + `https://localhost/github`, + ), + undefined, + ); + t.is( + api.getAuthorizationHeaderFor( + logger, + SAMPLE_DOTCOM_API_DETAILS, + `https://api.localhost/github`, + ), + undefined, + ); +}); diff --git a/src/api-client.ts b/src/api-client.ts index 4d0a999206..b75aacbb79 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -195,6 +195,12 @@ export function getAuthorizationHeaderFor( apiDetails: GitHubApiDetails, url: string, ): string | undefined { + // We don't have to check the `url` if we don't have a token to begin with. + const token = apiDetails.auth?.trim(); + if (token === undefined || token === "") { + return undefined; + } + // We only want to provide an authorization header if we are downloading // from the same GitHub instance the Action is running on. // This avoids leaking Enterprise tokens to dotcom. @@ -203,7 +209,7 @@ export function getAuthorizationHeaderFor( (apiDetails.apiURL && url.startsWith(`${apiDetails.apiURL}/`)) ) { logger.debug(`Providing an authorization token.`); - return `token ${apiDetails.auth}`; + return `token ${token}`; } logger.debug(`Not using an authorization token.`); From f9b4185b99e69102ca688508e7446a19f2c937be Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 11:21:44 +0100 Subject: [PATCH 10/15] Check token value in `uploadBundledDatabase` --- lib/entry-points.js | 5 ++++- src/database-upload.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 681f56b89c..f35cad21cf 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -178509,6 +178509,9 @@ async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundled if (uploadsBaseUrl.endsWith("/")) { uploadsBaseUrl = uploadsBaseUrl.slice(0, -1); } + const token = apiDetails.auth?.trim(); + const hasToken2 = token !== void 0 && token !== ""; + const authorization = hasToken2 ? `token ${token}` : void 0; const bundledDbReadStream = fs18.createReadStream(bundledDb); try { const startTime = performance.now(); @@ -178523,7 +178526,7 @@ async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundled commit_oid: commitOid, data: bundledDbReadStream, headers: { - authorization: `token ${apiDetails.auth}`, + authorization, "Content-Type": "application/zip", "Content-Length": bundledDbSize }, diff --git a/src/database-upload.ts b/src/database-upload.ts index d6f8dd1ff0..07b1626b49 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -279,6 +279,10 @@ async function uploadBundledDatabase( uploadsBaseUrl = uploadsBaseUrl.slice(0, -1); } + const token = apiDetails.auth?.trim(); + const hasToken = token !== undefined && token !== ""; + const authorization = hasToken ? `token ${token}` : undefined; + const bundledDbReadStream = fs.createReadStream(bundledDb); try { const startTime = performance.now(); @@ -293,7 +297,7 @@ async function uploadBundledDatabase( commit_oid: commitOid, data: bundledDbReadStream, headers: { - authorization: `token ${apiDetails.auth}`, + authorization, "Content-Type": "application/zip", "Content-Length": bundledDbSize, }, From bfe264b55993bd05f49bd7a65119f716425a1ffd Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 13:41:12 +0100 Subject: [PATCH 11/15] Add very basic `getApiClientWithExternalAuth` test --- src/api-client.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/api-client.test.ts b/src/api-client.test.ts index 1a37425e4c..2bb878f3b7 100644 --- a/src/api-client.test.ts +++ b/src/api-client.test.ts @@ -292,6 +292,19 @@ test("makeProxyRequestOptions - returns fetch with custom proxy", async (t) => { ); }); +test.serial( + "getApiClientWithExternalAuth constructs clients with or without tokens", + (t) => { + t.notThrows(() => { + api.getApiClientWithExternalAuth(SAMPLE_DOTCOM_API_DETAILS); + api.getApiClientWithExternalAuth({ + ...SAMPLE_DOTCOM_API_DETAILS, + auth: undefined, + }); + }); + }, +); + test("getAuthorizationHeaderFor - returns undefined if there's no token", (t) => { const logger = new RecordingLogger(); From a08b587b612ed1666540f2a8e91b768e58e452ec Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 18:27:15 +0100 Subject: [PATCH 12/15] Allow `getApiClient` to accept `GitHubApiDetails` --- lib/entry-points.js | 4 ++-- src/api-client.ts | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index f35cad21cf..ede4701fdf 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -170758,8 +170758,8 @@ function getApiDetails(env = getEnv()) { ...getCurrentInstance(env) }; } -function getApiClient(env = getEnv()) { - return createApiClientWithDetails(getApiDetails(env)); +function getApiClient(env = getEnv(), apiDetails) { + return createApiClientWithDetails(apiDetails ?? getApiDetails(env)); } function getApiClientWithExternalAuth(apiDetails, proxy) { return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy }); diff --git a/src/api-client.ts b/src/api-client.ts index b75aacbb79..7aa4d61d75 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -169,8 +169,22 @@ export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails { }; } -export function getApiClient(env: ReadOnlyEnv = getEnv()) { - return createApiClientWithDetails(getApiDetails(env)); +/** + * Constructs an {@link ApiClient} using {@link apiDetails}. + * + * If {@link apiDetails} is not specified, then the GitHub instance URLs and auth token + * are taken from {@link env}. + * + * If {@link env} is not specified, then the process environment is used. + * + * @param env The environment to obtain the GitHub instance URLs from. + * @param apiDetails The {@link GitHubApiDetails} to construct the {@link ApiClient} for. + */ +export function getApiClient( + env: ReadOnlyEnv = getEnv(), + apiDetails?: GitHubApiDetails, +) { + return createApiClientWithDetails(apiDetails ?? getApiDetails(env)); } export function getApiClientWithExternalAuth( From f6e8dce502d1ae775fbdbfce10c37cf05ae3074c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 11:19:56 +0100 Subject: [PATCH 13/15] Pass `apiDetails` to `getApiClient` in `uploadBundledDatabase` --- lib/entry-points.js | 2 +- src/database-upload.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ede4701fdf..aeb16a4d28 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -178502,7 +178502,7 @@ async function recordClearCleanupSizes(codeql, config, reports, logger2) { } } async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundledDb, bundledDbSize, apiDetails) { - const client = getApiClient(); + const client = getApiClient(getEnv(), apiDetails); const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url)); uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`; let uploadsBaseUrl = uploadsUrl.toString(); diff --git a/src/database-upload.ts b/src/database-upload.ts index 07b1626b49..75caa46ca5 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -267,7 +267,10 @@ async function uploadBundledDatabase( bundledDbSize: number, apiDetails: GitHubApiDetails, ): Promise { - const client = getApiClient(); + // Ensure that the `apiDetails` that `getApiClient` uses match the ones we were given here, + // because we use `apiDetails.auth` below which must be used with the same instance that + // `apiDetails` refers to. + const client = getApiClient(util.getEnv(), apiDetails); const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url)); uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`; From 047c751d925e1a08464af8184eb81d1a3ac034ac Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 19:13:22 +0100 Subject: [PATCH 14/15] Add a test for `uploadBundledDatabase` --- src/database-upload.test.ts | 92 ++++++++++++++++++++++++++++++++++++- src/database-upload.ts | 2 +- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/database-upload.test.ts b/src/database-upload.test.ts index b6ac5c8115..908994112e 100644 --- a/src/database-upload.test.ts +++ b/src/database-upload.test.ts @@ -1,4 +1,5 @@ -import * as fs from "fs"; +import fs from "fs"; +import path from "path"; import * as github from "@actions/github"; import test from "ava"; @@ -10,7 +11,10 @@ import { GitHubApiDetails } from "./api-client"; import * as apiClient from "./api-client"; import { createStubCodeQL } from "./codeql"; import { Config } from "./config-utils"; -import { cleanupAndUploadDatabases } from "./database-upload"; +import { + cleanupAndUploadDatabases, + uploadBundledDatabase, +} from "./database-upload"; import { Feature } from "./feature-flags"; import * as gitUtils from "./git-utils"; import { BuiltInLanguage } from "./languages"; @@ -23,6 +27,7 @@ import { getTestEnv, initAllState, RecordingLogger, + SAMPLE_DOTCOM_API_DETAILS, setupActionsVars, setupTests, } from "./testing-utils"; @@ -550,3 +555,86 @@ test.serial( }); }, ); + +test.serial( + "uploadBundledDatabase provides expected request arguments", + async (t) => + withTmpDir(async (tmpDir) => { + // Write some file that `uploadBundledDatabase` can use for its `fs.createReadStream(bundledDb)` call + // so we don't have to stub that or create a fake `ReadStream`. + const dbPath = path.join(tmpDir, "db"); + const content = JSON.stringify({}); + fs.writeFileSync(dbPath, content); + + // A simple stub for `fetch` that satisfies Octokit when it inspects the response. + const fetchStub = sinon.stub().resolves({ + status: 200, + headers: new Headers(), + text: async () => "", + }); + + // Construct a real `ApiClient`. + const client = apiClient.getApiClient( + getTestEnv(), + SAMPLE_DOTCOM_API_DETAILS, + ); + + // Then wrap the real `request` method in a stub that calls it with our stubbed `fetch`. + // This ensures that we have a real `ApiClient` with a mostly real `request`, but which + // does not perform any actual requests. + const request = client.request; + sinon.stub(client, "request").callsFake(async (route, options) => + request(route, { + ...options, + request: { ...options?.request, fetch: fetchStub }, + }), + ); + + // Stub `getApiClient` to return our modified `ApiClient`. + const getApiClient = sinon + .stub(apiClient, "getApiClient") + .returns(client); + + // Calling `uploadBundledDatabase` shouldn't throw. + const nwo: RepositoryNwo = { owner: "test", repo: "foo" }; + const language: BuiltInLanguage = BuiltInLanguage.java; + + await t.notThrowsAsync(async () => { + await uploadBundledDatabase( + nwo, + language, + "123", + dbPath, + content.length, + SAMPLE_DOTCOM_API_DETAILS, + ); + }); + + // Check that our stubs were called and that they received the expected arguments. + t.is(getApiClient.callCount, 1); + t.is(fetchStub.callCount, 1); + + const args = fetchStub.args[0]; + t.is(args.length, 2); + + const url = args[0]; + t.is( + url, + `https://uploads.github.com/repos/${nwo.owner}/${nwo.repo}/code-scanning/codeql/databases/${language}?name=${language}-database&commit_oid=123`, + ); + + const options = args[1]; + t.like(options, { + // Going via `new Headers` and `Object.fromEntries` ensures we get the same cases + // for the header keys as we are given. + headers: Object.fromEntries( + new Headers({ + authorization: `token ${SAMPLE_DOTCOM_API_DETAILS.auth}`, + "Content-Type": "application/zip", + "Content-Length": content.length.toString(), + }).entries(), + ), + method: "POST", + } satisfies RequestInit); + }), +); diff --git a/src/database-upload.ts b/src/database-upload.ts index 75caa46ca5..829d744886 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -259,7 +259,7 @@ async function recordClearCleanupSizes( * * @returns the duration of the upload in milliseconds */ -async function uploadBundledDatabase( +export async function uploadBundledDatabase( repositoryNwo: RepositoryNwo, language: string, commitOid: string, From c1641847bb01dd388b3d0ef6a5dd7b4af7ea222c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 14 Oct 2026 20:03:19 +0100 Subject: [PATCH 15/15] Remove explicit `authorization` header from `uploadBundledDatabase` --- lib/entry-points.js | 4 ---- src/database-upload.ts | 5 ----- 2 files changed, 9 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index aeb16a4d28..a0e269a8e1 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -178509,9 +178509,6 @@ async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundled if (uploadsBaseUrl.endsWith("/")) { uploadsBaseUrl = uploadsBaseUrl.slice(0, -1); } - const token = apiDetails.auth?.trim(); - const hasToken2 = token !== void 0 && token !== ""; - const authorization = hasToken2 ? `token ${token}` : void 0; const bundledDbReadStream = fs18.createReadStream(bundledDb); try { const startTime = performance.now(); @@ -178526,7 +178523,6 @@ async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundled commit_oid: commitOid, data: bundledDbReadStream, headers: { - authorization, "Content-Type": "application/zip", "Content-Length": bundledDbSize }, diff --git a/src/database-upload.ts b/src/database-upload.ts index 829d744886..2a24a2e1d4 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -282,10 +282,6 @@ export async function uploadBundledDatabase( uploadsBaseUrl = uploadsBaseUrl.slice(0, -1); } - const token = apiDetails.auth?.trim(); - const hasToken = token !== undefined && token !== ""; - const authorization = hasToken ? `token ${token}` : undefined; - const bundledDbReadStream = fs.createReadStream(bundledDb); try { const startTime = performance.now(); @@ -300,7 +296,6 @@ export async function uploadBundledDatabase( commit_oid: commitOid, data: bundledDbReadStream, headers: { - authorization, "Content-Type": "application/zip", "Content-Length": bundledDbSize, },