Compare commits

..

15 Commits

Author SHA1 Message Date
Michael B. Gale
c1641847bb Remove explicit authorization header from uploadBundledDatabase 2026-10-14 20:03:19 +01:00
Michael B. Gale
047c751d92 Add a test for uploadBundledDatabase 2026-10-14 20:02:27 +01:00
Michael B. Gale
f6e8dce502 Pass apiDetails to getApiClient in uploadBundledDatabase 2026-10-14 18:28:21 +01:00
Michael B. Gale
a08b587b61 Allow getApiClient to accept GitHubApiDetails 2026-10-14 18:27:15 +01:00
Michael B. Gale
bfe264b559 Add very basic getApiClientWithExternalAuth test 2026-10-14 18:19:52 +01:00
Michael B. Gale
f9b4185b99 Check token value in uploadBundledDatabase 2026-10-14 18:19:52 +01:00
Michael B. Gale
66c96fdc35 Check auth value in getAuthorizationHeaderFor
Also add some tests for it
2026-10-14 18:19:52 +01:00
Michael B. Gale
1d30a842cd Guard against blank auth value
This shouldn't be possible, but was previously handled by the library
2026-10-14 18:19:51 +01:00
Michael B. Gale
1b0a617641 Make externalRepoAuth check more consistent 2026-10-14 18:19:51 +01:00
Michael B. Gale
96501a33ac Use getCurrentInstance() 2026-10-14 18:19:51 +01:00
Michael B. Gale
a2438139ce Make auth property on GitHubApiDetails optional 2026-10-14 18:19:50 +01:00
Michael B. Gale
e0625309c3 Move GitHubApiDetails to github.ts 2026-10-14 18:19:50 +01:00
Michael B. Gale
61bfcffab9 Add functions to get the current and dotcom instance 2026-10-14 18:19:50 +01:00
Michael B. Gale
102e46e9d1 Move some definitions out of util.ts into a new github.ts module 2026-10-14 18:19:46 +01:00
Michael B. Gale
0735d7910f Improve GitHubApiDetails types and documentation 2026-10-14 18:14:48 +01:00
15 changed files with 555 additions and 229 deletions

134
lib/entry-points.js generated
View File

@@ -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);

View File

@@ -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,
);
});

View File

@@ -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.`);

View File

@@ -126,7 +126,7 @@ function mockReleaseApi({
});
}
function mockApiDetails(apiDetails: GitHubApiDetails) {
function mockApiDetails(apiDetails: Required<GitHubApiDetails>) {
// 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

View File

@@ -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);
}),
);

View File

@@ -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<number> {
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,
},

111
src/github.test.ts Normal file
View File

@@ -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);
});

110
src/github.ts Normal file
View File

@@ -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,
};
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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";

View File

@@ -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,

View File

@@ -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<Partial<sarif.Log>> {
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 =

View File

@@ -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);

View File

@@ -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";