Compare commits

...

7 Commits

Author SHA1 Message Date
Michael B. Gale
d9e3972d38 Explain when to use getAuthorizationHeaderFor 2026-10-16 12:40:43 +01:00
Michael B. Gale
1c350b1564 Address feedback from previous PR 2026-10-16 12:36:22 +01:00
Michael B. Gale
1772d44790 Merge remote-tracking branch 'origin/main' into mbg/use-github-instance-for-getApiClient 2026-10-16 12:30:11 +01:00
Michael B. Gale
8884b5b423 Add InstanceApiClient and incorporate getAuthorizationHeaderFor into it 2026-10-14 20:49:06 +01:00
Michael B. Gale
b2bf75026d Include apiDetails in CodeQLDownloadSource 2026-10-14 20:48:44 +01:00
Michael B. Gale
f2547e0e83 Use instance in DownloadSource and pass it on to getApiClient 2026-10-14 20:48:40 +01:00
Michael B. Gale
f728bb6678 Add getDotcomApiDetails 2026-10-14 20:31:06 +01:00
13 changed files with 339 additions and 229 deletions

120
lib/entry-points.js generated
View File

@@ -170632,6 +170632,7 @@ var import_undici = __toESM(require_undici());
// src/github.ts
var GITHUB_DOTCOM_URL = "https://github.com";
var GITHUB_DOTCOM_API_URL = "https://api.github.com";
function parseGitHubUrl(inputUrl) {
const originalUrl = inputUrl;
if (inputUrl.indexOf("://") === -1) {
@@ -170666,6 +170667,12 @@ function getCurrentInstance(env) {
apiURL: env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */)
};
}
function getDotcomInstance() {
return {
url: GITHUB_DOTCOM_URL,
apiURL: GITHUB_DOTCOM_API_URL
};
}
// src/repository.ts
function isRepositoryNwoStr(val) {
@@ -170731,6 +170738,37 @@ function makeProxyRequestOptions(dispatcher) {
}
};
}
var InstanceApiClient = class {
client;
apiDetails;
authorizationHeader;
constructor(apiDetails, clientOptions = {}) {
this.apiDetails = apiDetails;
this.client = createApiClientWithDetails(apiDetails, clientOptions);
const token = this.apiDetails.auth?.trim();
if (token !== void 0 && token !== "") {
this.authorizationHeader = `token ${token}`;
}
}
/**
* Gets a value for the `Authorization` header for a request to `url`; or `undefined` if the
* `Authorization` header should not be set for `url`.
*
* @param logger The logger to use for debugging messages.
* @param url The URL for which we want to add an `Authorization` header.
*/
getAuthorizationHeaderFor(logger2, url2) {
if (this.authorizationHeader === void 0) {
return void 0;
}
if (url2.startsWith(`${this.apiDetails.url}/`) || this.apiDetails.apiURL && url2.startsWith(`${this.apiDetails.apiURL}/`)) {
logger2.debug(`Providing an authorization token.`);
return this.authorizationHeader;
}
logger2.debug(`Not using an authorization token.`);
return void 0;
}
};
function createApiClientWithDetails(apiDetails, { allowExternal = false, proxy = void 0 } = {}) {
const auth2 = allowExternal && apiDetails.externalRepoAuth !== void 0 ? apiDetails.externalRepoAuth : apiDetails.auth;
const retryingOctokit = githubUtils.GitHub.plugin(retry);
@@ -170761,21 +170799,19 @@ function getApiDetails(env = getEnv()) {
function getApiClient(env = getEnv(), apiDetails) {
return createApiClientWithDetails(apiDetails ?? getApiDetails(env));
}
function getDotcomApiDetails(env) {
if (env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */) === GITHUB_DOTCOM_URL) {
return getApiDetails(env);
} else {
return {
auth: void 0,
...getDotcomInstance()
};
}
}
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 ${token}`;
}
logger2.debug(`Not using an authorization token.`);
return void 0;
}
var cachedGitHubVersion = void 0;
async function getGitHubVersionFromApi(apiClient, apiDetails) {
if (parseGitHubUrl(apiDetails.url) === GITHUB_DOTCOM_URL) {
@@ -175781,38 +175817,38 @@ function getCodeQLActionRepository(action) {
`The value of '${"GITHUB_ACTION_REPOSITORY" /* GITHUB_ACTION_REPOSITORY */}' is '${actionRepository}', which does not match the expected 'owner/repo' format.`
);
}
var DEFAULT_DOWNLOAD_SOURCE = [
GITHUB_DOTCOM_URL,
CODEQL_DEFAULT_ACTION_REPOSITORY
];
var isSameDownloadSource = ([srcUrl, srcRepo], [otherUrl, otherRepo]) => {
return srcUrl === otherUrl && srcRepo === otherRepo;
var isSameDownloadSource = ([srcInstance, srcRepo], [otherInstance, otherRepo]) => {
return srcInstance.url === otherInstance.url && srcRepo === otherRepo;
};
function getDefaultDotcomDownloadURL(tagName, assetName) {
return `${GITHUB_DOTCOM_URL}/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`;
}
async function getCodeQLAssetDownloadURL(action, apiDetails, tagName, assetName, assetKind = "CodeQL bundle") {
const codeQLActionRepository = getCodeQLActionRepository(action);
const defaultDownloadSource = [
getDotcomApiDetails(action.env),
CODEQL_DEFAULT_ACTION_REPOSITORY
];
const potentialDownloadSources = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
[apiDetails, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
[apiDetails, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
DEFAULT_DOWNLOAD_SOURCE
defaultDownloadSource
];
const uniqueDownloadSources = potentialDownloadSources.filter(
(source, index2, self2) => {
return !self2.slice(0, index2).some((other) => isSameDownloadSource(source, other));
}
);
for (const [instanceURL, repository] of uniqueDownloadSources) {
if (isSameDownloadSource(DEFAULT_DOWNLOAD_SOURCE, [instanceURL, repository])) {
for (const [instance8, repository] of uniqueDownloadSources) {
if (isSameDownloadSource(defaultDownloadSource, [instance8, repository])) {
break;
}
const { owner: repositoryOwner, repo: repositoryName } = parseRepositoryNwo(repository);
try {
const release2 = await getApiClient().rest.repos.getReleaseByTag({
const release2 = await getApiClient(action.env, instance8).rest.repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName
@@ -175820,14 +175856,14 @@ async function getCodeQLAssetDownloadURL(action, apiDetails, tagName, assetName,
for (const asset of release2.data.assets) {
if (asset.name === assetName) {
action.logger.info(
`Found ${assetKind} ${assetName} in ${repository} on ${instanceURL} with URL ${asset.url}.`
`Found ${assetKind} ${assetName} in ${repository} on ${instance8.apiURL} with URL ${asset.url}.`
);
return asset.url;
}
}
} catch (e) {
action.logger.info(
`Looked for ${assetKind} ${assetName} in ${repository} on ${instanceURL} but got error ${e}.`
`Looked for ${assetKind} ${assetName} in ${repository} on ${instance8.apiURL} but got error ${e}.`
);
}
}
@@ -176648,7 +176684,8 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, othe
cliVersion: cliVersion2,
compressionMethod,
sourceType: "download",
toolsVersion: resolvedVersion ?? "unknown"
toolsVersion: resolvedVersion ?? "unknown",
apiDetails
};
}
async function tryGetFallbackToolcacheVersion(cliVersion2, tagName, logger2) {
@@ -176662,7 +176699,7 @@ async function tryGetFallbackToolcacheVersion(cliVersion2, tagName, logger2) {
);
return fallbackVersion;
}
var downloadCodeQL = async function(source, apiDetails, tarVersion, tempDir, logger2) {
var downloadCodeQL = async function(source, tarVersion, tempDir, logger2) {
const { bundle, compressionMethod } = source;
const codeqlURL = bundle.url;
const parsedCodeQLURL = new URL(codeqlURL);
@@ -176674,11 +176711,8 @@ var downloadCodeQL = async function(source, apiDetails, tarVersion, tempDir, log
if (searchParams.has("token")) {
logger2.debug("CodeQL tools URL contains an authorization token.");
} else {
authorization = getAuthorizationHeaderFor(
logger2,
apiDetails,
codeqlURL
);
const client = new InstanceApiClient(source.apiDetails);
authorization = client.getAuthorizationHeaderFor(logger2, codeqlURL);
}
const toolcacheDestination = getToolcacheDestination({ logger: logger2 }, source);
const extractedBundlePath = toolcacheDestination.orElse(
@@ -176800,7 +176834,6 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defau
const result = await downloadCodeQLBundle(
{ env: getEnv(), features, logger: logger2 },
source,
apiDetails,
zstdAvailability.version,
tempDir
);
@@ -176820,19 +176853,13 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defau
toolsVersion: source.toolsVersion
};
}
async function downloadCodeQLBundle(action, source, apiDetails, tarVersion, tempDir) {
async function downloadCodeQLBundle(action, source, tarVersion, tempDir) {
const { bundle } = source;
const { logger: logger2 } = action;
await tryDeleteToolcacheBundles(action);
const startTime = import_perf_hooks4.performance.now();
try {
return await downloadCodeQL(
source,
apiDetails,
tarVersion,
tempDir,
logger2
);
return await downloadCodeQL(source, tarVersion, tempDir, logger2);
} catch (e) {
if (bundle.kind !== "per-language" || bundle.combinedBundleURL === void 0 || asHTTPError(e)?.status !== 404) {
throw e;
@@ -176843,7 +176870,6 @@ async function downloadCodeQLBundle(action, source, apiDetails, tarVersion, temp
...source,
bundle: { kind: "combined", url: bundle.combinedBundleURL }
},
apiDetails,
tarVersion,
tempDir,
logger2
@@ -178501,8 +178527,8 @@ async function recordClearCleanupSizes(codeql, config, reports, logger2) {
report.clear_cleanup_measurement_duration_ms = durationMs;
}
}
async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundledDb, bundledDbSize, apiDetails) {
const client = getApiClient(getEnv(), apiDetails);
async function uploadBundledDatabase(repositoryNwo, language, commitOid, bundledDb, bundledDbSize, apiDetails, env = getEnv()) {
const client = getApiClient(env, apiDetails);
const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url));
uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`;
let uploadsBaseUrl = uploadsUrl.toString();
@@ -187920,6 +187946,7 @@ async function getProxyBinaryPath(action, apiDetails) {
const logger2 = action.logger;
logger2.startGroup("Setting up authentication proxy");
try {
const client = new InstanceApiClient(apiDetails);
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(action, apiDetails);
logger2.info(
@@ -187930,9 +187957,8 @@ async function getProxyBinaryPath(action, apiDetails) {
logger2.info(
`Authentication proxy is not in the toolcache. Will try to download it from '${proxyInfo.url}'.`
);
const authorization = getAuthorizationHeaderFor(
const authorization = client.getAuthorizationHeaderFor(
logger2,
apiDetails,
proxyInfo.url
);
const temp = await downloadProxy(logger2, proxyInfo.url, authorization);

View File

@@ -308,70 +308,45 @@ test.serial(
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,
);
for (const authValue of [undefined, "", " "]) {
const client = new api.InstanceApiClient({
...SAMPLE_DOTCOM_API_DETAILS,
auth: authValue,
});
t.is(
client.getAuthorizationHeaderFor(logger, `${GITHUB_DOTCOM_URL}/github`),
undefined,
);
}
});
test("getAuthorizationHeaderFor - returns header value with token if url is allowed", (t) => {
const logger = new RecordingLogger();
const client = new api.InstanceApiClient({
...SAMPLE_DOTCOM_API_DETAILS,
});
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}`,
);
for (const url of [
`${GITHUB_DOTCOM_URL}/github`,
`${GITHUB_DOTCOM_API_URL}/github`,
]) {
t.is(
client.getAuthorizationHeaderFor(logger, url),
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
);
}
});
test("getAuthorizationHeaderFor - returns undefined if url is not allowed", (t) => {
const logger = new RecordingLogger();
const client = new api.InstanceApiClient({
...SAMPLE_DOTCOM_API_DETAILS,
});
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,
);
for (const url of [
`https://localhost/github`,
`https://api.localhost/github`,
]) {
t.is(client.getAuthorizationHeaderFor(logger, url), undefined);
}
});

View File

@@ -18,9 +18,11 @@ import {
RegistryProxyVars,
getEnv,
exportEnvVar,
ActionsEnvVars,
} from "./environment";
import {
getCurrentInstance,
getDotcomInstance,
GITHUB_DOTCOM_URL,
parseGitHubUrl,
type GitHubApiCombinedDetails,
@@ -120,6 +122,66 @@ export function makeProxyRequestOptions(
/** The type of GitHub API client we use. */
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
/**
* Encapsulates a {@link GitHubApiCombinedDetails} along with operations which ensure that any credentials
* are only used with the GitHub instance that is represented by the {@link GitHubApiCombinedDetails} value.
*/
export class InstanceApiClient {
private readonly client: ApiClient;
private readonly apiDetails: GitHubApiCombinedDetails;
private readonly authorizationHeader: string | undefined;
constructor(
apiDetails: GitHubApiCombinedDetails,
clientOptions: CreateApiClientOptions = {},
) {
this.apiDetails = apiDetails;
this.client = createApiClientWithDetails(apiDetails, clientOptions);
// Compute the Authorization header value if we have a token.
const token = this.apiDetails.auth?.trim();
if (token !== undefined && token !== "") {
this.authorizationHeader = `token ${token}`;
}
}
/**
* In most cases, we use an {@link ApiClient} which automatically handles auth for requests
* made with it and this method is not needed and should not be used in that setting.
*
* In some cases, we need to make requests that can't go through an {@link ApiClient}, but still
* require auth. For example, this is the case when using the `toolcache` module to download
* files. For these cases, this method gets a value for the `Authorization` header for a request
* to `url`; or `undefined` if the `Authorization` header should not be set for `url`.
*
* @param logger The logger to use for debugging messages.
* @param url The URL for which we want to add an `Authorization` header.
*/
public getAuthorizationHeaderFor(
logger: Logger,
url: string,
): string | undefined {
// We don't have to check the `url` if we don't have a token to begin with.
if (this.authorizationHeader === undefined) {
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.
if (
url.startsWith(`${this.apiDetails.url}/`) ||
(this.apiDetails.apiURL && url.startsWith(`${this.apiDetails.apiURL}/`))
) {
logger.debug(`Providing an authorization token.`);
return this.authorizationHeader;
}
logger.debug(`Not using an authorization token.`);
return undefined;
}
}
/** Options for `createApiClientWithDetails`. */
interface CreateApiClientOptions {
allowExternal?: boolean;
@@ -172,8 +234,8 @@ export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails {
/**
* 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 apiDetails} is not specified, then the GitHub instance URLs are taken from {@link env}
* and the auth token is taken from the `token` input.
*
* If {@link env} is not specified, then the process environment is used.
*
@@ -187,6 +249,28 @@ export function getApiClient(
return createApiClientWithDetails(apiDetails ?? getApiDetails(env));
}
/*
* Gets a `GitHubApiDetails` object describing the GitHub Dotcom instance.
*
* @param env The environment variables to read.
*/
export function getDotcomApiDetails(env: ReadOnlyEnv): GitHubApiDetails {
// If we are currently running on dotcom, then just call `getApiDetails` which will
// construct the appropriate `GitHubApiDetails` from the environment and include the
// value of the `token` input. The `token` input relates to the GitHub instance that
// we are running on only. Therefore, if we are not currently running on dotcom,
// then we construct `GitHubApiDetails` without the `token` value and using the
// hard-coded instance and API URLs.
if (env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL) === GITHUB_DOTCOM_URL) {
return getApiDetails(env);
} else {
return {
auth: undefined,
...getDotcomInstance(),
};
}
}
export function getApiClientWithExternalAuth(
apiDetails: GitHubApiCombinedDetails,
proxy?: ProxyAgent,
@@ -194,42 +278,6 @@ export function getApiClientWithExternalAuth(
return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy });
}
/**
* Gets a value for the `Authorization` header for a request to `url`; or `undefined` if the
* `Authorization` header should not be set for `url`.
*
* @param logger The logger to use for debugging messages.
* @param apiDetails Details of the GitHub API we are using.
* @param url The URL for which we want to add an `Authorization` header.
*
* @returns The value for the `Authorization` header or `undefined` if it shouldn't be populated.
*/
export function getAuthorizationHeaderFor(
logger: Logger,
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.
if (
url.startsWith(`${apiDetails.url}/`) ||
(apiDetails.apiURL && url.startsWith(`${apiDetails.apiURL}/`))
) {
logger.debug(`Providing an authorization token.`);
return `token ${token}`;
}
logger.debug(`Not using an authorization token.`);
return undefined;
}
let cachedGitHubVersion: GitHubVersion | undefined = undefined;
export async function getGitHubVersionFromApi(

View File

@@ -1,3 +1,4 @@
import type { GitHubApiDetails } from "./github";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { BundlePlatform } from "./platform";
import type { CompressionMethod } from "./tar";
@@ -17,6 +18,8 @@ export type CodeQLBundle =
export interface CodeQLDownloadSource {
/** Distinguishes downloads from local archives and cached installations. */
sourceType: "download";
/** Information about the GitHub instance that contains the asset. */
apiDetails: GitHubApiDetails;
/** The bundle to download. */
bundle: CodeQLBundle;
/** The compression format of the bundle archive. */

View File

@@ -34,6 +34,7 @@ import {
makeVersionInfo,
createTestConfig,
makeMacro,
mockApiDetails,
} from "./testing-utils";
import { ToolsDownloadStatusReport } from "./tools-download";
import * as util from "./util";
@@ -126,19 +127,6 @@ function mockReleaseApi({
});
}
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
// `getApiDetails()` via an imported `api` module.
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("token")
.returns(apiDetails.auth);
process.env["GITHUB_SERVER_URL"] = apiDetails.url;
process.env["GITHUB_API_URL"] = apiDetails.apiURL || "";
}
async function stubCodeql(): Promise<codeql.CodeQL> {
const codeqlObject = await codeql.getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6"));
@@ -290,6 +278,7 @@ for (const { cliVersion, tagName, expectedToolcacheVersion } of [
async (t) => {
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const result = await installIntoToolcache({
cliVersion,
@@ -470,6 +459,7 @@ test.serial(
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
await installIntoToolcache({
tagName: "codeql-bundle-20200601",
@@ -517,6 +507,7 @@ test.serial(
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
await installIntoToolcache({
tagName: "codeql-bundle-20200601",

View File

@@ -599,6 +599,7 @@ test.serial(
const nwo: RepositoryNwo = { owner: "test", repo: "foo" };
const language: BuiltInLanguage = BuiltInLanguage.java;
const env = getTestEnv();
await t.notThrowsAsync(async () => {
await uploadBundledDatabase(
nwo,
@@ -607,11 +608,14 @@ test.serial(
dbPath,
content.length,
SAMPLE_DOTCOM_API_DETAILS,
env,
);
});
// Check that our stubs were called and that they received the expected arguments.
t.is(getApiClient.callCount, 1);
t.deepEqual(getApiClient.args[0], [env, SAMPLE_DOTCOM_API_DETAILS]);
t.is(fetchStub.callCount, 1);
const args = fetchStub.args[0];

View File

@@ -10,6 +10,7 @@ import {
} from "./api-client";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
import type { ReadOnlyEnv } from "./environment";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { parseGitHubUrl } from "./github";
@@ -266,11 +267,12 @@ export async function uploadBundledDatabase(
bundledDb: string,
bundledDbSize: number,
apiDetails: GitHubApiDetails,
env: ReadOnlyEnv = util.getEnv(),
): Promise<number> {
// 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);
// It is important that the `apiDetails` that `getApiClient` uses match the ones we were given,
// because we use `apiDetails.url` below to construct the URL to upload the database to. That
// URL will be specific to the GitHub instance given by `apiDetails` and so the client must match it.
const client = getApiClient(env, apiDetails);
const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url));
uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`;

View File

@@ -32,6 +32,7 @@ import {
getTestEnv,
initAllState,
makeMacro,
mockApiDetails,
mockBundleDownloadApi,
setupActionsVars,
setupTests,
@@ -155,6 +156,7 @@ test.serial(
compressionMethod: "gzip",
sourceType: "download",
toolsVersion: "1.2.3",
apiDetails: SAMPLE_DOTCOM_API_DETAILS,
} satisfies setupCodeql.CodeQLDownloadSource);
});
},
@@ -221,6 +223,7 @@ for (const {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const source = await setupCodeql.getCodeQLSource(
"linked",
SAMPLE_DEFAULT_CLI_VERSION,
@@ -255,6 +258,7 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const source = await setupCodeql.getCodeQLSource(
"latest",
SAMPLE_DEFAULT_CLI_VERSION,
@@ -298,6 +302,7 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const result = await setupCodeql.setupCodeQLBundle(
"linked",
SAMPLE_DOTCOM_API_DETAILS,
@@ -431,6 +436,7 @@ test.serial(
compressionMethod: "zstd",
sourceType: "download",
toolsVersion: expectedVersion,
apiDetails: SAMPLE_DOTCOM_API_DETAILS,
} satisfies setupCodeql.CodeQLToolsSource);
const result = await setupCodeql.setupCodeQLBundle(
@@ -479,6 +485,7 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" });
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const source = await setupCodeql.getCodeQLSource(
undefined,
@@ -503,6 +510,7 @@ test.serial(
compressionMethod: "zstd",
sourceType: "download",
toolsVersion: expectedVersion,
apiDetails: SAMPLE_DOTCOM_API_DETAILS,
} satisfies setupCodeql.CodeQLToolsSource);
// Afterwards, ensure that we see the expected messages in the log.
@@ -624,6 +632,7 @@ for (const toolsInput of ["nightly", "nightly-latest"]) {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const source = await setupCodeql.getCodeQLSource(
toolsInput,
SAMPLE_DEFAULT_CLI_VERSION,
@@ -649,6 +658,7 @@ for (const toolsInput of ["nightly", "nightly-latest"]) {
cliVersion: undefined,
compressionMethod: "zstd",
toolsVersion: "0.0.0-30260213",
apiDetails: SAMPLE_DOTCOM_API_DETAILS,
} satisfies setupCodeql.CodeQLDownloadSource);
t.true(latestNightlyRequest.calledOnce);
});
@@ -801,6 +811,7 @@ for (const date of ["20200101", "30260213"]) {
cliVersion: undefined,
compressionMethod: "zstd",
toolsVersion: `0.0.0-${date}`,
apiDetails: SAMPLE_DOTCOM_API_DETAILS,
} satisfies setupCodeql.CodeQLDownloadSource);
await t.throwsAsync(
@@ -945,6 +956,7 @@ const toolcacheInputFallbackMacro = makeMacro({
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
for (const [k, v] of Object.entries(environment)) {
process.env[k] = v;
@@ -1165,6 +1177,7 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const source = await setupCodeql.getCodeQLSource(
undefined,
PER_LANGUAGE_CLI_VERSION,
@@ -1207,6 +1220,7 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
for (const { otherLanguagePacksReason, features } of [
// The per-language feature is disabled.
{ otherLanguagePacksReason: undefined, features: createFeatures([]) },
@@ -1276,7 +1290,14 @@ for (const fallback of [false, true]) {
request: { fetch: fetchRelease },
});
sinon.stub(api, "getApiClient").value(() => client);
const authorizationSpy = sinon.spy(api, "getAuthorizationHeaderFor");
const spyClient = new api.InstanceApiClient(SAMPLE_DOTCOM_API_DETAILS);
const authorizationSpy = sinon.spy(
spyClient,
"getAuthorizationHeaderFor",
);
sinon.stub(api, "InstanceApiClient").returns(spyClient);
const extractStub = stubDownloadAndExtract();
if (fallback) {
extractStub.onFirstCall().rejects(new HTTPError("Not Found", 404));
@@ -1286,6 +1307,7 @@ for (const fallback of [false, true]) {
setupActionsVars(tmpDir, tmpDir, {
GITHUB_ACTION_REPOSITORY: "codeql-testing/action-fork",
});
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
const result = await setupCodeql.setupCodeQLBundle(
undefined,
SAMPLE_DOTCOM_API_DETAILS,
@@ -1308,9 +1330,9 @@ for (const fallback of [false, true]) {
t.is(extractStub.firstCall.args[0], assetURL);
t.is(extractStub.lastCall.args[0], fallback ? combinedURL : assetURL);
t.is(authorizationSpy.callCount, extractStub.callCount);
t.is(authorizationSpy.firstCall.args[2], assetURL);
t.is(authorizationSpy.firstCall.args[1], assetURL);
t.is(
authorizationSpy.lastCall.args[2],
authorizationSpy.lastCall.args[1],
fallback ? combinedURL : assetURL,
);
t.is(extractStub.lastCall.args[3], "token token");
@@ -1553,6 +1575,7 @@ for (const error of [
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
await t.throwsAsync(
setupCodeql.setupCodeQLBundle(
undefined,
@@ -1853,8 +1876,8 @@ async function runDownloadCodeQL(
cliVersion: CLEANUP_CLI_VERSION,
sourceType: "download",
toolsVersion: CLEANUP_CLI_VERSION,
apiDetails: SAMPLE_DOTCOM_API_DETAILS,
},
SAMPLE_DOTCOM_API_DETAILS,
undefined, // tarVersion
toolcacheRoot, // tempDir
);
@@ -2259,6 +2282,7 @@ test.serial(
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
stubDownloadAndExtract();
@@ -2294,6 +2318,7 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
const staleDirectory = createToolcacheEntry(

View File

@@ -718,6 +718,7 @@ export async function getCodeQLSource(
compressionMethod,
sourceType: "download",
toolsVersion: resolvedVersion ?? "unknown",
apiDetails,
};
}
@@ -746,7 +747,6 @@ async function tryGetFallbackToolcacheVersion(
// be able to stub this function and have other functions in this file use that stub.
export const downloadCodeQL = async function (
source: CodeQLDownloadSource,
apiDetails: api.GitHubApiDetails,
tarVersion: tar.TarVersion | undefined,
tempDir: string,
logger: Logger,
@@ -767,11 +767,8 @@ export const downloadCodeQL = async function (
if (searchParams.has("token")) {
logger.debug("CodeQL tools URL contains an authorization token.");
} else {
authorization = api.getAuthorizationHeaderFor(
logger,
apiDetails,
codeqlURL,
);
const client = new api.InstanceApiClient(source.apiDetails);
authorization = client.getAuthorizationHeaderFor(logger, codeqlURL);
}
const toolcacheDestination = getToolcacheDestination({ logger }, source);
@@ -985,7 +982,6 @@ export async function setupCodeQLBundle(
const result = await downloadCodeQLBundle(
{ env: getEnv(), features, logger },
source,
apiDetails,
zstdAvailability.version,
tempDir,
);
@@ -1021,7 +1017,6 @@ export async function setupCodeQLBundle(
export async function downloadCodeQLBundle(
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
source: CodeQLDownloadSource,
apiDetails: api.GitHubApiDetails,
tarVersion: tar.TarVersion | undefined,
tempDir: string,
): Promise<{
@@ -1035,13 +1030,7 @@ export async function downloadCodeQLBundle(
const startTime = performance.now();
try {
return await downloadCodeQL(
source,
apiDetails,
tarVersion,
tempDir,
logger,
);
return await downloadCodeQL(source, tarVersion, tempDir, logger);
} catch (e) {
if (
bundle.kind !== "per-language" ||
@@ -1057,7 +1046,6 @@ export async function downloadCodeQLBundle(
...source,
bundle: { kind: "combined", url: bundle.combinedBundleURL },
},
apiDetails,
tarVersion,
tempDir,
logger,

View File

@@ -8,7 +8,6 @@ import {
parseRepositoryNwo,
type RepositoryNwoStr,
} from "../repository";
import { type InstanceURL } from "../util";
/** The NWO of the standard CodeQL Action repository. */
export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
@@ -48,20 +47,14 @@ export function getCodeQLActionRepository(
}
/**A download source is a pair of strings. */
export type DownloadSource = [InstanceURL, RepositoryNwoStr];
/** GitHub.com, and the canonical Action. */
export const DEFAULT_DOWNLOAD_SOURCE: DownloadSource = [
GITHUB_DOTCOM_URL,
CODEQL_DEFAULT_ACTION_REPOSITORY,
];
export type DownloadSource = [api.GitHubApiDetails, RepositoryNwoStr];
/** Decides if the two provided download sources are the same. */
const isSameDownloadSource = (
[srcUrl, srcRepo]: DownloadSource,
[otherUrl, otherRepo]: DownloadSource,
[srcInstance, srcRepo]: DownloadSource,
[otherInstance, otherRepo]: DownloadSource,
) => {
return srcUrl === otherUrl && srcRepo === otherRepo;
return srcInstance.url === otherInstance.url && srcRepo === otherRepo;
};
/** A download URL is represented as a string. */
@@ -104,13 +97,21 @@ export async function getCodeQLAssetDownloadURL(
): Promise<DownloadURL> {
const codeQLActionRepository = getCodeQLActionRepository(action);
// We construct the default GitHub.com source here, because `getDotcomApiDetails`
// may retrieve the `token` input if we are currently running on GitHub.com and
// we want to avoid doing that statically.
const defaultDownloadSource: DownloadSource = [
api.getDotcomApiDetails(action.env),
CODEQL_DEFAULT_ACTION_REPOSITORY,
];
const potentialDownloadSources: DownloadSource[] = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
[apiDetails, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
[apiDetails, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
DEFAULT_DOWNLOAD_SOURCE,
defaultDownloadSource,
];
// We now filter out any duplicates.
@@ -123,34 +124,34 @@ export async function getCodeQLAssetDownloadURL(
},
);
for (const [instanceURL, repository] of uniqueDownloadSources) {
for (const [instance, repository] of uniqueDownloadSources) {
// If we've reached the final case, short-circuit the API check since we know the asset exists and is public.
if (
isSameDownloadSource(DEFAULT_DOWNLOAD_SOURCE, [instanceURL, repository])
) {
if (isSameDownloadSource(defaultDownloadSource, [instance, repository])) {
break;
}
const { owner: repositoryOwner, repo: repositoryName } =
parseRepositoryNwo(repository);
try {
const release = await api.getApiClient().rest.repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName,
});
const release = await api
.getApiClient(action.env, instance)
.rest.repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName,
});
for (const asset of release.data.assets) {
if (asset.name === assetName) {
action.logger.info(
`Found ${assetKind} ${assetName} in ${repository} on ${instanceURL} with URL ${asset.url}.`,
`Found ${assetKind} ${assetName} in ${repository} on ${instance.apiURL} with URL ${asset.url}.`,
);
return asset.url;
}
}
} catch (e) {
action.logger.info(
`Looked for ${assetKind} ${assetName} in ${repository} on ${instanceURL} but got error ${e}.`,
`Looked for ${assetKind} ${assetName} in ${repository} on ${instance.apiURL} but got error ${e}.`,
);
}
}

View File

@@ -8,7 +8,7 @@ import { ActionState } from "./action-common";
import * as actionsUtil from "./actions-util";
import * as apiClient from "./api-client";
import * as defaults from "./defaults.json";
import { ActionsEnvVars } from "./environment";
import { ActionsEnvVars, type Env } from "./environment";
import { setUpFeatureFlagTests } from "./feature-flags/testing-util";
import { UnvalidatedObject, validateSchema } from "./json";
import { makeFromSchema } from "./json/testing-util";
@@ -25,6 +25,7 @@ import {
initAllState,
makeMacro,
makeTestToken,
mockApiDetails,
RecordingLogger,
SAMPLE_DOTCOM_API_DETAILS,
setupTests,
@@ -613,18 +614,23 @@ test("getProxyPackage - includes platform in name", (t) => {
}
});
function mockGetApiClient(endpoints: any) {
return (
sinon
.stub(apiClient, "getApiClient")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.returns({ rest: endpoints } as any)
);
function mockGetApiClient(env: Env, endpoints: any) {
const getRequiredInput = mockApiDetails(SAMPLE_DOTCOM_API_DETAILS, env);
const getApiClient = sinon
.stub(apiClient, "getApiClient")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.returns({ rest: endpoints } as any);
return { getRequiredInput, getApiClient };
}
type ReleaseAssets = Array<{ name: string; url?: string }>;
function mockGetReleaseByTag(expectedTag?: string, assets?: ReleaseAssets) {
function mockGetReleaseByTag(
env: Env,
expectedTag?: string,
assets?: ReleaseAssets,
) {
const getReleaseByTag =
assets === undefined
? sinon.stub().rejects()
@@ -645,7 +651,7 @@ function mockGetReleaseByTag(expectedTag?: string, assets?: ReleaseAssets) {
return {
getReleaseByTag,
getApiClient: mockGetApiClient({ repos: { getReleaseByTag } }),
...mockGetApiClient(env, { repos: { getReleaseByTag } }),
};
}
@@ -671,7 +677,6 @@ test.serial(
"getDownloadUrl logs unknown platforms/arch and defaults to linux64",
async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
await withTmpDir(async (tempDir) => {
const features = mockOfflineFeatures(tempDir, logger);
@@ -681,6 +686,7 @@ test.serial(
logger,
features,
});
mockGetReleaseByTag(state.env);
// Stub this to always return `true` so we get the default repo for the tests.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
@@ -710,11 +716,11 @@ test.serial(
"getDownloadUrl returns default bundle URL for linked version",
async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
await withTmpDir(async (tempDir) => {
const features = mockOfflineFeatures(tempDir, logger);
const state = initAllState({ logger, features });
mockGetReleaseByTag(state.env);
// Stub this to always return `true` so we get the default repo for the tests.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
@@ -750,7 +756,11 @@ test.serial(
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
for (const assets of testAssets) {
const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets);
const stubs = mockGetReleaseByTag(
state.env,
defaults.bundleVersion,
assets,
);
const info = await startProxyExports.getDownloadUrl(
state,
SAMPLE_DOTCOM_API_DETAILS,
@@ -768,6 +778,7 @@ test.serial(
// mockGetReleaseByTag stubs the global getApiClient; since we are calling
// that in a loop, we need to manually restore it here
stubs.getApiClient.restore();
stubs.getRequiredInput.restore();
}
});
},
@@ -783,7 +794,7 @@ test.serial("getDownloadUrl returns matching release asset", async (t) => {
url: "url-we-want",
},
];
const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets);
const stubs = mockGetReleaseByTag(state.env, defaults.bundleVersion, assets);
await withTmpDir(async (tempDir) => {
state.env.set(ActionsEnvVars.RUNNER_TEMP, tempDir);
@@ -925,7 +936,8 @@ test.serial(
"getProxyBinaryPath - returns path from tool cache if available",
async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
sinon.stub(apiClient, "getApiDetails").returns(SAMPLE_DOTCOM_API_DETAILS);
await withTmpDir(async (tempDir) => {
const toolcachePath = "/path/to/proxy/dir";
@@ -935,8 +947,11 @@ test.serial(
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
const features = mockOfflineFeatures(tempDir, logger);
const state = initAllState({ logger, features });
mockGetReleaseByTag(state.env);
const result = await startProxyExports.getProxyBinaryPath(
initAllState({ logger, features }),
state,
SAMPLE_DOTCOM_API_DETAILS,
);
@@ -956,7 +971,7 @@ test.serial(
const logger = new RecordingLogger();
const state = initAllState({ logger });
const downloadUrl = "url-we-want";
const stubs = mockGetReleaseByTag(defaults.bundleVersion, [
const stubs = mockGetReleaseByTag(state.env, defaults.bundleVersion, [
{
name: startProxyExports.getProxyPackage(getTestPlatform(state)),
url: downloadUrl,
@@ -965,9 +980,16 @@ test.serial(
const toolcachePath = "/path/to/proxy/dir";
const find = sinon.stub(toolcache, "find").returns("");
const apiDetails = {
auth: "",
url: "",
apiURL: "",
} satisfies apiClient.GitHubApiDetails;
const client = new apiClient.InstanceApiClient(apiDetails);
const getAuthorizationHeaderFor = sinon
.stub(apiClient, "getAuthorizationHeaderFor")
.stub(client, "getAuthorizationHeaderFor")
.returns(undefined);
sinon.stub(apiClient, "InstanceApiClient").returns(client);
const archivePath = "/path/to/archive";
const downloadTool = sinon
.stub(toolcache, "downloadTool")
@@ -1007,7 +1029,7 @@ test.serial(
t.is(result.version, defaults.cliVersion);
checkExpectedLogMessages(t, logger.messages, [
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL ${downloadUrl}`,
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://api.github.com with URL ${downloadUrl}`,
]);
},
);
@@ -1039,14 +1061,21 @@ test.serial(
headers: {},
url: "GET /repos/:owner/:repo/releases/tags/:tag",
});
mockGetApiClient({ repos: { getReleaseByTag } });
mockGetApiClient(state.env, { repos: { getReleaseByTag } });
await withTmpDir(async (tempDir) => {
const toolcachePath = "/path/to/proxy/dir";
const find = sinon.stub(toolcache, "find").returns("");
const apiDetails = {
auth: "",
url: "",
apiURL: "",
} satisfies apiClient.GitHubApiDetails;
const client = new apiClient.InstanceApiClient(apiDetails);
const getAuthorizationHeaderFor = sinon
.stub(apiClient, "getAuthorizationHeaderFor")
.stub(client, "getAuthorizationHeaderFor")
.returns(undefined);
sinon.stub(apiClient, "InstanceApiClient").returns(client);
const archivePath = "/path/to/archive";
const downloadTool = sinon
.stub(toolcache, "downloadTool")
@@ -1107,7 +1136,7 @@ test.serial(
});
checkExpectedLogMessages(t, logger.messages, [
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL url-we-want.`,
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://api.github.com with URL url-we-want.`,
]);
},
);

View File

@@ -4,9 +4,9 @@ import * as toolcache from "@actions/tool-cache";
import { ActionState } from "./action-common";
import {
getAuthorizationHeaderFor,
getGitHubVersion,
GitHubApiDetails,
InstanceApiClient,
type GitHubApiDetails,
} from "./api-client";
import * as artifactScanner from "./artifact-scanner";
import * as defaults from "./defaults.json";
@@ -423,6 +423,7 @@ export async function getProxyBinaryPath(
logger.startGroup("Setting up authentication proxy");
try {
const client = new InstanceApiClient(apiDetails);
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(action, apiDetails);
@@ -437,9 +438,8 @@ export async function getProxyBinaryPath(
`Authentication proxy is not in the toolcache. Will try to download it from '${proxyInfo.url}'.`,
);
const authorization = getAuthorizationHeaderFor(
const authorization = client.getAuthorizationHeaderFor(
logger,
apiDetails,
proxyInfo.url,
);
const temp = await downloadProxy(logger, proxyInfo.url, authorization);

View File

@@ -14,6 +14,7 @@ import * as sinon from "sinon";
import { ActionState, StateFeature } from "./action-common";
import { ActionsEnv, getActionVersion } from "./actions-util";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import * as apiClient from "./api-client";
import { GitHubApiDetails } from "./api-client";
@@ -52,6 +53,23 @@ export const SAMPLE_DOTCOM_API_DETAILS = {
apiURL: "https://api.github.com",
};
export function mockApiDetails(
apiDetails: Required<GitHubApiDetails>,
env: Env = getEnv(),
) {
// 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
// `getApiDetails()` via an imported `api` module.
const getRequiredInput = sinon.stub(actionsUtil, "getRequiredInput");
getRequiredInput.withArgs("token").returns(apiDetails.auth);
env.set("GITHUB_SERVER_URL", apiDetails.url);
env.set("GITHUB_API_URL", apiDetails.apiURL ?? "");
return getRequiredInput;
}
export const LINKED_CLI_VERSION = {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,