diff --git a/lib/entry-points.js b/lib/entry-points.js index d5e64717a0..a0e269a8e1 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; } }); @@ -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,43 @@ 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(); +} +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) { return val.includes("/"); @@ -170724,43 +170732,46 @@ 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); - 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 && auth2.trim() !== "" ? `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 { 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()) { - return createApiClientWithDetails(getApiDetails(env)); +function getApiClient(env = getEnv(), apiDetails) { + return createApiClientWithDetails(apiDetails ?? getApiDetails(env)); } 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; @@ -178491,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(); @@ -178512,7 +178523,6 @@ async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundled commit_oid: commitOid, data: bundledDbReadStream, headers: { - authorization: `token ${apiDetails.auth}`, "Content-Type": "application/zip", "Content-Length": bundledDbSize }, @@ -180011,6 +180021,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"; @@ -180043,8 +180054,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( @@ -186450,8 +186460,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); @@ -187493,8 +187502,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.test.ts b/src/api-client.test.ts index 12fe819856..2bb878f3b7 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,87 @@ test("makeProxyRequestOptions - returns fetch with custom proxy", async (t) => { githubUtils.defaults.request, ); }); + +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(); + + 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 6a5e99468c..7aa4d61d75 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 { @@ -12,26 +13,32 @@ import { import type { ActionState } from "./action-common"; import { getActionVersion, getRequiredInput } from "./actions-util"; import { - ActionsEnvVars, EnvVar, ReadOnlyEnv, RegistryProxyVars, getEnv, exportEnvVar, } from "./environment"; +import { + getCurrentInstance, + GITHUB_DOTCOM_URL, + parseGitHubUrl, + type GitHubApiCombinedDetails, + type GitHubApiDetails, +} from "./github"; import { Logger } from "./logging"; import { getRepositoryNwo, RepositoryNwo } from "./repository"; import { asHTTPError, ConfigurationError, getRequiredEnvParam, - GITHUB_DOTCOM_URL, GitHubVariant, GitHubVersion, - parseGitHubUrl, parseMatrixInput, } from "./util"; +export type { GitHubApiDetails, GitHubApiCombinedDetails } from "./github"; + const GITHUB_ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version"; /** @@ -43,21 +50,6 @@ const GITHUB_ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version"; */ export const DO_NOT_RETRY_STATUSES = [400, 410, 422, 451]; -export type GitHubApiCombinedDetails = GitHubApiDetails & - GitHubApiExternalRepoDetails; - -export interface GitHubApiDetails { - auth: string; - url: string; - apiURL: string | undefined; -} - -export interface GitHubApiExternalRepoDetails { - externalRepoAuth?: string; - url: string; - apiURL: string | undefined; -} - /** * Gets the configuration for the private registry authentication proxy, * if it is available in the environment. @@ -139,37 +131,60 @@ 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); - 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 && auth.trim() !== "" ? `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); } +/** + * 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), }; } -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( @@ -194,6 +209,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. @@ -202,7 +223,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.`); 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/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 35dfd90785..2a24a2e1d4 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 { @@ -258,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, @@ -266,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}`; @@ -292,7 +296,6 @@ async function uploadBundledDatabase( commit_oid: commitOid, data: bundledDbReadStream, headers: { - authorization: `token ${apiDetails.auth}`, "Content-Type": "application/zip", "Content-Length": bundledDbSize, }, diff --git a/src/github.test.ts b/src/github.test.ts new file mode 100644 index 0000000000..79dbb23311 --- /dev/null +++ b/src/github.test.ts @@ -0,0 +1,111 @@ +import test from "ava"; + +import { ActionsEnvVars } from "./environment"; +import * as github from "./github"; +import { getTestEnv, 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', + }); +}); + +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 new file mode 100644 index 0000000000..6350fb3f30 --- /dev/null +++ b/src/github.ts @@ -0,0 +1,110 @@ +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"; + +/** + * 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. + */ +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(); +} + +/** 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, + }; +} 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/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/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 = 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";