Merge pull request #4198 from github/mbg/start-proxy/deprecate-fallback

Use `setup-codeql` logic for finding release assets in `start-proxy`
This commit is contained in:
Michael B. Gale
2026-10-14 14:39:32 +00:00
committed by GitHub
14 changed files with 612 additions and 419 deletions

326
lib/entry-points.js generated
View File

@@ -42967,11 +42967,11 @@ var require_tracingPolicy = __commonJS({
var import_log = require_log3();
var import_core_util = require_commonjs4();
var import_restError = require_restError3();
var import_util58 = require_internal3();
var import_util59 = require_internal3();
var tracingPolicyName2 = "tracingPolicy";
function tracingPolicy2(options = {}) {
const userAgentPromise = (0, import_userAgent.getUserAgentValue)(options.userAgentPrefix);
const sanitizer = new import_util58.Sanitizer({
const sanitizer = new import_util59.Sanitizer({
additionalAllowedQueryParameters: options.additionalAllowedQueryParameters
});
const tracingClient = tryCreateTracingClient();
@@ -46597,11 +46597,11 @@ var require_response2 = __commonJS({
});
module2.exports = __toCommonJS2(response_exports);
var import_core_rest_pipeline = require_commonjs6();
var import_util58 = require_util9();
var import_util59 = require_util9();
var originalResponse = /* @__PURE__ */ Symbol("Original FullOperationResponse");
function toCompatResponse2(response, options) {
let request3 = (0, import_util58.toWebResourceLike)(response.request);
let headers = (0, import_util58.toHttpHeadersLike)(response.headers);
let request3 = (0, import_util59.toWebResourceLike)(response.request);
let headers = (0, import_util59.toHttpHeadersLike)(response.headers);
if (options?.createProxy) {
return new Proxy(response, {
get(target, prop, receiver) {
@@ -46642,7 +46642,7 @@ var require_response2 = __commonJS({
return {
...compatResponse,
headers,
request: (0, import_util58.toPipelineRequest)(compatResponse.request)
request: (0, import_util59.toPipelineRequest)(compatResponse.request)
};
}
}
@@ -46749,7 +46749,7 @@ var require_requestPolicyFactoryPolicy = __commonJS({
requestPolicyFactoryPolicyName: () => requestPolicyFactoryPolicyName2
});
module2.exports = __toCommonJS2(requestPolicyFactoryPolicy_exports);
var import_util58 = require_util9();
var import_util59 = require_util9();
var import_response = require_response2();
var HttpPipelineLogLevel2 = /* @__PURE__ */ ((HttpPipelineLogLevel22) => {
HttpPipelineLogLevel22[HttpPipelineLogLevel22["ERROR"] = 1] = "ERROR";
@@ -46773,14 +46773,14 @@ var require_requestPolicyFactoryPolicy = __commonJS({
async sendRequest(request3, next) {
let httpPipeline = {
async sendRequest(httpRequest) {
const response2 = await next((0, import_util58.toPipelineRequest)(httpRequest));
const response2 = await next((0, import_util59.toPipelineRequest)(httpRequest));
return (0, import_response.toCompatResponse)(response2, { createProxy: true });
}
};
for (const factory of orderedFactories) {
httpPipeline = factory.create(httpPipeline, mockRequestPolicyOptions);
}
const webResourceLike = (0, import_util58.toWebResourceLike)(request3, { createProxy: true });
const webResourceLike = (0, import_util59.toWebResourceLike)(request3, { createProxy: true });
const response = await httpPipeline.sendRequest(webResourceLike);
return (0, import_response.toPipelineResponse)(response);
}
@@ -46815,12 +46815,12 @@ var require_httpClientAdapter = __commonJS({
});
module2.exports = __toCommonJS2(httpClientAdapter_exports);
var import_response = require_response2();
var import_util58 = require_util9();
var import_util59 = require_util9();
function convertHttpClient2(requestPolicyClient) {
return {
sendRequest: async (request3) => {
const response = await requestPolicyClient.sendRequest(
(0, import_util58.toWebResourceLike)(request3, { createProxy: true })
(0, import_util59.toWebResourceLike)(request3, { createProxy: true })
);
return (0, import_response.toPipelineResponse)(response);
}
@@ -46858,7 +46858,7 @@ var require_commonjs9 = __commonJS({
disableKeepAlivePolicyName: () => import_disableKeepAlivePolicy.disableKeepAlivePolicyName,
requestPolicyFactoryPolicyName: () => import_requestPolicyFactoryPolicy.requestPolicyFactoryPolicyName,
toCompatResponse: () => import_response.toCompatResponse,
toHttpHeadersLike: () => import_util58.toHttpHeadersLike
toHttpHeadersLike: () => import_util59.toHttpHeadersLike
});
module2.exports = __toCommonJS2(src_exports);
var import_extendedClient = require_extendedClient();
@@ -46866,7 +46866,7 @@ var require_commonjs9 = __commonJS({
var import_requestPolicyFactoryPolicy = require_requestPolicyFactoryPolicy();
var import_disableKeepAlivePolicy = require_disableKeepAlivePolicy();
var import_httpClientAdapter = require_httpClientAdapter();
var import_util58 = require_util9();
var import_util59 = require_util9();
}
});
@@ -107111,41 +107111,6 @@ var require_tool_cache = __commonJS({
}
});
// node_modules/fast-deep-equal/index.js
var require_fast_deep_equal = __commonJS({
"node_modules/fast-deep-equal/index.js"(exports2, module2) {
"use strict";
module2.exports = function equal(a, b) {
if (a === b) return true;
if (a && b && typeof a == "object" && typeof b == "object") {
if (a.constructor !== b.constructor) return false;
var length, i, keys;
if (Array.isArray(a)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0; )
if (!equal(a[i], b[i])) return false;
return true;
}
if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) return false;
for (i = length; i-- !== 0; )
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
for (i = length; i-- !== 0; ) {
var key = keys[i];
if (!equal(a[key], b[key])) return false;
}
return true;
}
return a !== a && b !== b;
};
}
});
// node_modules/follow-redirects/debug.js
var require_debug3 = __commonJS({
"node_modules/follow-redirects/debug.js"(exports2, module2) {
@@ -169874,12 +169839,12 @@ var ExhaustivityCheckingError = class extends Error {
function assertNever(value) {
throw new ExhaustivityCheckingError(value);
}
function initializeEnvironment(version) {
exportEnvVar("CODEQL_ACTION_FEATURE_MULTI_LANGUAGE" /* FEATURE_MULTI_LANGUAGE */, "false");
exportEnvVar("CODEQL_ACTION_FEATURE_SANDWICH" /* FEATURE_SANDWICH */, "false");
exportEnvVar("CODEQL_ACTION_FEATURE_SARIF_COMBINE" /* FEATURE_SARIF_COMBINE */, "true");
exportEnvVar("CODEQL_ACTION_FEATURE_WILL_UPLOAD" /* FEATURE_WILL_UPLOAD */, "true");
exportEnvVar("CODEQL_ACTION_VERSION" /* VERSION */, version);
function initializeEnvironment(version, env = getEnv()) {
env.export("CODEQL_ACTION_FEATURE_MULTI_LANGUAGE" /* FEATURE_MULTI_LANGUAGE */, "false");
env.export("CODEQL_ACTION_FEATURE_SANDWICH" /* FEATURE_SANDWICH */, "false");
env.export("CODEQL_ACTION_FEATURE_SARIF_COMBINE" /* FEATURE_SARIF_COMBINE */, "true");
env.export("CODEQL_ACTION_FEATURE_WILL_UPLOAD" /* FEATURE_WILL_UPLOAD */, "true");
env.export("CODEQL_ACTION_VERSION" /* VERSION */, version);
}
var HTTPError = class extends Error {
status;
@@ -170689,6 +170654,9 @@ retry.VERSION = VERSION7;
var import_undici = __toESM(require_undici());
// src/repository.ts
function isRepositoryNwoStr(val) {
return val.includes("/");
}
function getRepositoryNwo() {
return getRepositoryNwoFromEnv("GITHUB_REPOSITORY");
}
@@ -175307,7 +175275,6 @@ var fs14 = __toESM(require("fs"));
var path13 = __toESM(require("path"));
var import_perf_hooks4 = require("perf_hooks");
var toolcache3 = __toESM(require_tool_cache());
var import_fast_deep_equal = __toESM(require_fast_deep_equal());
var semver9 = __toESM(require_semver2());
// src/codeql-bundle.ts
@@ -175763,6 +175730,78 @@ function logPerLanguageBundleFallback({ logger: logger2 }, language, location) {
);
}
// src/setup/repository.ts
var CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
function getCodeQLActionRepository(action) {
if (isRunningLocalAction(action.env)) {
action.logger.info(
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository."
);
return CODEQL_DEFAULT_ACTION_REPOSITORY;
}
const actionRepository = action.env.getRequired(
"GITHUB_ACTION_REPOSITORY" /* GITHUB_ACTION_REPOSITORY */
);
if (isRepositoryNwoStr(actionRepository)) {
return actionRepository;
}
throw new Error(
`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;
};
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 potentialDownloadSources = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
DEFAULT_DOWNLOAD_SOURCE
];
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])) {
break;
}
const { owner: repositoryOwner, repo: repositoryName } = parseRepositoryNwo(repository);
try {
const release2 = await getApiClient().rest.repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName
});
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}.`
);
return asset.url;
}
}
} catch (e) {
action.logger.info(
`Looked for ${assetKind} ${assetName} in ${repository} on ${instanceURL} but got error ${e}.`
);
}
}
return getDefaultDotcomDownloadURL(tagName, assetName);
}
// src/tar.ts
var import_child_process = require("child_process");
var fs12 = __toESM(require("fs"));
@@ -176154,64 +176193,11 @@ function writeToolcacheMarkerFile(extractedPath, logger2) {
}
// src/setup-codeql.ts
var CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
var CODEQL_NIGHTLIES_REPOSITORY_OWNER = "dsp-testing";
var CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies";
var CODEQL_BUNDLE_VERSION_ALIAS = ["linked", "latest"];
var CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"];
var CODEQL_TOOLCACHE_INPUT = "toolcache";
function getCodeQLActionRepository(logger2) {
if (isRunningLocalAction()) {
logger2.info(
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository."
);
return CODEQL_DEFAULT_ACTION_REPOSITORY;
}
return getRequiredEnvParam("GITHUB_ACTION_REPOSITORY");
}
async function getCodeQLBundleDownloadURL(tagName, apiDetails, codeQLBundleName, logger2) {
const codeQLActionRepository = getCodeQLActionRepository(logger2);
const potentialDownloadSources = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
[GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY]
];
const uniqueDownloadSources = potentialDownloadSources.filter(
(source, index2, self2) => {
return !self2.slice(0, index2).some((other) => (0, import_fast_deep_equal.default)(source, other));
}
);
for (const downloadSource of uniqueDownloadSources) {
const [apiURL, repository] = downloadSource;
if (apiURL === GITHUB_DOTCOM_URL && repository === CODEQL_DEFAULT_ACTION_REPOSITORY) {
break;
}
const [repositoryOwner, repositoryName] = repository.split("/");
try {
const release2 = await getApiClient().rest.repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName
});
for (const asset of release2.data.assets) {
if (asset.name === codeQLBundleName) {
logger2.info(
`Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.`
);
return asset.url;
}
}
} catch (e) {
logger2.info(
`Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.`
);
}
}
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
}
function tryGetBundleVersionFromTagName(tagName, logger2) {
const match2 = tagName.match(/^codeql-bundle-(.+)$/);
if (match2 === null || match2.length < 2) {
@@ -176569,9 +176555,10 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, othe
);
}
compressionMethod = cliVersion2 !== void 0 && await useZstdBundle(cliVersion2, tarSupportsZstd) ? "zstd" : "gzip";
const action = { env: getEnv(), logger: logger2 };
const platform2 = getBundlePlatform();
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
{ env: getEnv(), features, logger: logger2 },
{ ...action, features },
{
rawLanguages,
otherLanguagePacksReason,
@@ -176581,11 +176568,11 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, othe
variant
}
);
const resolveBundleURL = (language) => getCodeQLBundleDownloadURL(
bundleTagName,
const resolveBundleURL = (language) => getCodeQLAssetDownloadURL(
action,
apiDetails,
getCodeQLBundleName(compressionMethod, platform2, language),
logger2
bundleTagName,
getCodeQLBundleName(compressionMethod, platform2, language)
);
const combinedBundleURL = await resolveBundleURL();
if (perLanguageBundleLanguage !== void 0) {
@@ -183046,7 +183033,7 @@ var import_async = __toESM(require_async(), 1);
var import_path7 = require("path");
// node_modules/archiver/lib/error.js
var import_util36 = __toESM(require("util"), 1);
var import_util37 = __toESM(require("util"), 1);
var ERROR_CODES = {
ABORTED: "archive was aborted",
DIRECTORYDIRPATHREQUIRED: "diretory dirpath argument must be a non-empty string value",
@@ -183071,7 +183058,7 @@ function ArchiverError(code, data) {
this.code = code;
this.data = data;
}
import_util36.default.inherits(ArchiverError, Error);
import_util37.default.inherits(ArchiverError, Error);
// node_modules/archiver/lib/core.js
var import_readable_stream2 = __toESM(require_ours(), 1);
@@ -187652,8 +187639,6 @@ async function sendFailedStatusReport(logger2, startedAt, language, unwrappedErr
}
}
var UPDATEJOB_PROXY = "update-job-proxy";
var UPDATEJOB_PROXY_VERSION = "v2.0.20250624110901";
var UPDATEJOB_PROXY_URL_PREFIX = "https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.22.0/";
function isPAT(value) {
return isAuthToken(value, [
GITHUB_PAT_CLASSIC_PATTERN,
@@ -187771,21 +187756,11 @@ function getCredentials(logger2, registrySecrets, registriesCredentials, languag
function getProxyPackage(platform2) {
return `${UPDATEJOB_PROXY}-${platform2}.tar.gz`;
}
function getFallbackUrl(proxyPackage) {
return `${UPDATEJOB_PROXY_URL_PREFIX}${proxyPackage}`;
}
async function getReleaseByVersion(version) {
return getApiClient().rest.repos.getReleaseByTag({
owner: "github",
repo: "codeql-action",
tag: version
});
}
async function getCliVersionFromFeatures(features) {
const gitHubVersion = await getGitHubVersion();
return await features.getEnabledDefaultCliVersions(gitHubVersion.type);
}
async function getDownloadUrl(action) {
async function getDownloadUrl(action, apiDetails) {
let platform2 = getBundlePlatform(action.platform, action.arch);
if (platform2 === void 0) {
action.logger.warning(
@@ -187802,37 +187777,29 @@ async function getDownloadUrl(action) {
cliVersion,
tagName: bundleVersion
};
const cliRelease = await getReleaseByVersion(versionInfo.tagName);
for (const asset of cliRelease.data.assets) {
if (asset.name === proxyPackage) {
action.logger.info(
`Found '${proxyPackage}' in release '${versionInfo.tagName}' at '${asset.url}'`
);
return {
url: asset.url,
// The `update-job-proxy` doesn't have a version as such. Since we now bundle it
// with CodeQL CLI bundle releases, we use the corresponding CLI version to
// differentiate between (potentially) different versions of `update-job-proxy`.
version: versionInfo.cliVersion
};
}
}
const proxyAssetUrl = await getCodeQLAssetDownloadURL(
action,
apiDetails,
versionInfo.tagName,
proxyPackage,
"private registry proxy"
);
return {
url: proxyAssetUrl,
// The `update-job-proxy` doesn't have a version as such. Since we now bundle it
// with CodeQL CLI bundle releases, we use the corresponding CLI version to
// differentiate between (potentially) different versions of `update-job-proxy`.
version: versionInfo.cliVersion
};
} catch (ex) {
action.logger.warning(
throw new Error(
`Failed to retrieve information about the linked release: ${getErrorMessage(ex)}`
);
}
action.logger.info(
`Did not find '${proxyPackage}' in the linked release, falling back to hard-coded version.`
);
return {
url: getFallbackUrl(proxyPackage),
version: UPDATEJOB_PROXY_VERSION
};
}
async function downloadProxy(logger2, url2, authorization) {
try {
return toolcache4.downloadTool(
return await toolcache4.downloadTool(
url2,
/* dest: */
void 0,
@@ -187871,28 +187838,40 @@ async function cacheProxy(logger2, source, filename, version) {
function getProxyFilename() {
return process.platform === "win32" ? `${UPDATEJOB_PROXY}.exe` : UPDATEJOB_PROXY;
}
async function getProxyBinaryPath(action) {
async function getProxyBinaryPath(action, apiDetails) {
const logger2 = action.logger;
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(action);
let proxyBin = toolcache4.find(proxyFileName, proxyInfo.version);
if (!proxyBin) {
const apiDetails = getApiDetails();
const authorization = getAuthorizationHeaderFor(
logger2,
apiDetails,
proxyInfo.url
);
const temp = await downloadProxy(logger2, proxyInfo.url, authorization);
const extracted = await extractProxy(logger2, temp);
proxyBin = await cacheProxy(
logger2,
extracted,
proxyFileName,
proxyInfo.version
logger2.startGroup("Setting up authentication proxy");
try {
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(action, apiDetails);
logger2.info(
`Using authentication proxy from CodeQL release ${proxyInfo.version}.`
);
let proxyDir = toolcache4.find(proxyFileName, proxyInfo.version);
if (!proxyDir) {
logger2.info(
`Authentication proxy is not in the toolcache. Will try to download it from '${proxyInfo.url}'.`
);
const authorization = getAuthorizationHeaderFor(
logger2,
apiDetails,
proxyInfo.url
);
const temp = await downloadProxy(logger2, proxyInfo.url, authorization);
logger2.info(`Downloaded the authentication proxy asset to '${temp}'.`);
const extracted = await extractProxy(logger2, temp);
proxyDir = await cacheProxy(
logger2,
extracted,
proxyFileName,
proxyInfo.version
);
logger2.info(`Extracted the authentication proxy to '${proxyDir}'.`);
}
return path27.join(proxyDir, proxyFileName);
} finally {
logger2.endGroup();
}
return path27.join(proxyBin, proxyFileName);
}
// src/start-proxy/ca.ts
@@ -188255,7 +188234,10 @@ async function run7(action) {
ca
};
const actionWithFeatures = extendActionState(action, { features });
const proxyBin = await getProxyBinaryPath(actionWithFeatures);
const proxyBin = await getProxyBinaryPath(
actionWithFeatures,
getApiDetails(action.env)
);
const proxyInfo = await startProxy(
proxyBin,
proxyConfig,

2
package-lock.json generated
View File

@@ -27,7 +27,6 @@
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"@octokit/plugin-retry": "^8.1.1",
"archiver": "^8.0.0",
"fast-deep-equal": "^3.1.3",
"follow-redirects": "^1.16.1",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
@@ -5802,6 +5801,7 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {

View File

@@ -35,7 +35,6 @@
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"@octokit/plugin-retry": "^8.1.1",
"archiver": "^8.0.0",
"fast-deep-equal": "^3.1.3",
"follow-redirects": "^1.16.1",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",

View File

@@ -101,7 +101,7 @@ export function getWorkflowEventName(env: Env = getEnv()) {
* Returns whether the current workflow is executing a local copy of the Action, e.g. we're running
* a workflow on the codeql-action repo itself.
*/
export function isRunningLocalAction(env: Env = getEnv()): boolean {
export function isRunningLocalAction(env: ReadOnlyEnv = getEnv()): boolean {
const relativeScriptPath = getRelativeScriptPath(env);
return (
relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath)
@@ -113,7 +113,7 @@ export function isRunningLocalAction(env: Env = getEnv()): boolean {
*
* This can be used to get the Action's name or tell if we're running a local Action.
*/
function getRelativeScriptPath(env: Env): string {
function getRelativeScriptPath(env: ReadOnlyEnv): string {
const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP);
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
return path.relative(actionsDirectory, __filename);

View File

@@ -6,6 +6,14 @@ export interface RepositoryNwo {
repo: string;
}
/** An approximation of what a repository NWO string looks like. */
export type RepositoryNwoStr = `${string}/${string}`;
/** A basic check that `val` looks like a `RepositoryNwoStr`. */
export function isRepositoryNwoStr(val: string): val is RepositoryNwoStr {
return val.includes("/");
}
/**
* Get the repository name with owner from the environment variable
* `GITHUB_REPOSITORY`.

View File

@@ -126,24 +126,6 @@ test.serial("convert to semver", (t) => {
}
});
test.serial("getCodeQLActionRepository", (t) => {
const logger = getRunnerLogger(true);
initializeEnvironment("1.2.3");
// isRunningLocalAction() === true
delete process.env["GITHUB_ACTION_REPOSITORY"];
process.env["RUNNER_TEMP"] = path.dirname(__dirname);
const repoLocalRunner = setupCodeql.getCodeQLActionRepository(logger);
t.deepEqual(repoLocalRunner, "github/codeql-action");
// isRunningLocalAction() === false
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
process.env["GITHUB_ACTION_REPOSITORY"] = "xxx/yyy";
const repoEnv = setupCodeql.getCodeQLActionRepository(logger);
t.deepEqual(repoEnv, "xxx/yyy");
});
test.serial(
"getCodeQLSource sets CLI version for a semver tagged bundle",
async (t) => {

View File

@@ -4,7 +4,6 @@ import * as path from "path";
import { performance } from "perf_hooks";
import * as toolcache from "@actions/tool-cache";
import { default as deepEqual } from "fast-deep-equal";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
@@ -13,7 +12,6 @@ import {
isAnalyzingPullRequest,
isDynamicWorkflow,
isGitHubHostedRunner,
isRunningLocalAction,
} from "./actions-util";
import * as api from "./api-client";
import {
@@ -44,6 +42,7 @@ import {
logPerLanguageBundleFallback,
} from "./per-language-bundles";
import { getBundlePlatform } from "./platform";
import { getCodeQLAssetDownloadURL } from "./setup/repository";
import * as tar from "./tar";
import {
deleteToolcacheBundles,
@@ -65,7 +64,6 @@ export enum ToolsSource {
Download = "DOWNLOAD",
}
const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
const CODEQL_NIGHTLIES_REPOSITORY_OWNER = "dsp-testing";
const CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies";
@@ -73,75 +71,6 @@ const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"];
const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"];
const CODEQL_TOOLCACHE_INPUT = "toolcache";
export function getCodeQLActionRepository(logger: Logger): string {
if (isRunningLocalAction()) {
// This handles the case where the Action does not come from an Action repository,
// e.g. our integration tests which use the Action code from the current checkout.
// In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set.
logger.info(
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository.",
);
return CODEQL_DEFAULT_ACTION_REPOSITORY;
}
return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY");
}
async function getCodeQLBundleDownloadURL(
tagName: string,
apiDetails: api.GitHubApiDetails,
codeQLBundleName: string,
logger: Logger,
): Promise<string> {
const codeQLActionRepository = getCodeQLActionRepository(logger);
const potentialDownloadSources = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
[util.GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY],
];
// We now filter out any duplicates.
// Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork.
const uniqueDownloadSources = potentialDownloadSources.filter(
(source, index, self) => {
return !self.slice(0, index).some((other) => deepEqual(source, other));
},
);
for (const downloadSource of uniqueDownloadSources) {
const [apiURL, repository] = downloadSource;
// If we've reached the final case, short-circuit the API check since we know the bundle exists and is public.
if (
apiURL === util.GITHUB_DOTCOM_URL &&
repository === CODEQL_DEFAULT_ACTION_REPOSITORY
) {
break;
}
const [repositoryOwner, repositoryName] = repository.split("/");
try {
const release = await api.getApiClient().rest.repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName,
});
for (const asset of release.data.assets) {
if (asset.name === codeQLBundleName) {
logger.info(
`Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.`,
);
return asset.url;
}
}
} catch (e) {
logger.info(
`Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.`,
);
}
}
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
}
function tryGetBundleVersionFromTagName(
tagName: string,
logger: Logger,
@@ -720,9 +649,10 @@ export async function getCodeQLSource(
? "zstd"
: "gzip";
const action = { env: getEnv(), logger };
const platform = getBundlePlatform();
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
{ env: getEnv(), features, logger },
{ ...action, features },
{
rawLanguages,
otherLanguagePacksReason,
@@ -735,11 +665,11 @@ export async function getCodeQLSource(
// Resolves the combined or per-language bundle URL for the requested release.
const resolveBundleURL = (language?: BuiltInLanguage) =>
getCodeQLBundleDownloadURL(
bundleTagName,
getCodeQLAssetDownloadURL(
action,
apiDetails,
bundleTagName,
getCodeQLBundleName(compressionMethod, platform, language),
logger,
);
const combinedBundleURL = await resolveBundleURL();

View File

@@ -0,0 +1,35 @@
import * as path from "path";
import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "../actions-util";
import { ActionsEnvVars } from "../environment";
import { callee, getTestEnv, setupTests } from "../testing-utils";
import { initializeEnvironment } from "../util";
import { getCodeQLActionRepository } from "./repository";
setupTests(test);
test.serial("getCodeQLActionRepository", async (t) => {
const initialEnv = getTestEnv({
[ActionsEnvVars.RUNNER_TEMP]: path.dirname(__dirname),
});
initializeEnvironment("1.2.3", initialEnv);
const target = callee(getCodeQLActionRepository)
.withArgs()
.withEnv(initialEnv);
// isRunningLocalAction() === true
await target.passes(t.deepEqual, "github/codeql-action");
// isRunningLocalAction() === false
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
await target
.withEnv((env) => {
env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "xxx/yyy");
})
.passes(t.deepEqual, "xxx/yyy");
});

158
src/setup/repository.ts Normal file
View File

@@ -0,0 +1,158 @@
import { ActionState } from "../action-common";
import { isRunningLocalAction } from "../actions-util";
import * as api from "../api-client";
import { ActionsEnvVars } from "../environment";
import {
isRepositoryNwoStr,
parseRepositoryNwo,
type RepositoryNwoStr,
} from "../repository";
import { GITHUB_DOTCOM_URL, type InstanceURL } from "../util";
/** The NWO of the standard CodeQL Action repository. */
export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
/**
* Retrieves the NWO of the repository that the CodeQL Action is loaded from.
* Typically, this will be `github/codeql-action`, but it may be something else for
* forks of the Action or in environments other than GitHub.com.
*
* If this function detects that we are running the Action locally (e.g. in tests),
* then we default to `CODEQL_DEFAULT_ACTION_REPOSITORY`.
*/
export function getCodeQLActionRepository(
action: ActionState<["ReadOnlyEnv", "Logger"]>,
): RepositoryNwoStr {
if (isRunningLocalAction(action.env)) {
// This handles the case where the Action does not come from an Action repository,
// e.g. our integration tests which use the Action code from the current checkout.
// In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set.
action.logger.info(
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository.",
);
return CODEQL_DEFAULT_ACTION_REPOSITORY;
}
const actionRepository = action.env.getRequired(
ActionsEnvVars.GITHUB_ACTION_REPOSITORY,
);
if (isRepositoryNwoStr(actionRepository)) {
return actionRepository;
}
throw new Error(
`The value of '${ActionsEnvVars.GITHUB_ACTION_REPOSITORY}' is '${actionRepository}', which does not match the expected 'owner/repo' format.`,
);
}
/**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,
];
/** Decides if the two provided download sources are the same. */
const isSameDownloadSource = (
[srcUrl, srcRepo]: DownloadSource,
[otherUrl, otherRepo]: DownloadSource,
) => {
return srcUrl === otherUrl && srcRepo === otherRepo;
};
/** A download URL is represented as a string. */
export type DownloadURL = string;
/** Gets the default GitHub.com download URL for `assetName` in a release tagged `tagName`. */
export function getDefaultDotcomDownloadURL(
tagName: string,
assetName: string,
): DownloadURL {
return `${GITHUB_DOTCOM_URL}/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`;
}
/** Enumerates log-friendly descriptions of different kinds of assets we might look for in releases. */
export type ReleaseAssetKind = "CodeQL bundle" | "private registry proxy";
/**
* Tries to find a download URL for `assetName` in a release tagged with `tagName`.
*
* Depending on where and how the CodeQL Action is running, we may be using different `apiDetails` and there may be
* different options for where to source CodeQL releases from.
*
* This function either returns the download URL for the asset for the first release we find, or
* defaults to the assumed download URL for the asset on the default CodeQL Action repository on GitHub.com.
* In the latter case, this function does not guarantee that the asset actually exists.
*
* @param action The Action state.
* @param apiDetails The details of the GitHub API in use.
* @param tagName The name of the release tag we want to obtain the asset from.
* @param assetName The name of the asset we should look for in the release.
* @param [assetKind="CodeQL bundle"] The kind of asset we are looking for to show in log messages.
* @returns A URL that we can use to download the asset.
*/
export async function getCodeQLAssetDownloadURL(
action: ActionState<["ReadOnlyEnv", "Logger"]>,
apiDetails: api.GitHubApiDetails,
tagName: string,
assetName: string,
assetKind: ReleaseAssetKind = "CodeQL bundle",
): Promise<DownloadURL> {
const codeQLActionRepository = getCodeQLActionRepository(action);
const potentialDownloadSources: DownloadSource[] = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
DEFAULT_DOWNLOAD_SOURCE,
];
// We now filter out any duplicates.
// Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork.
const uniqueDownloadSources = potentialDownloadSources.filter(
(source, index, self) => {
return !self
.slice(0, index)
.some((other) => isSameDownloadSource(source, other));
},
);
for (const [instanceURL, 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])
) {
break;
}
const { owner: repositoryOwner, repo: repositoryName } =
parseRepositoryNwo(repository);
try {
const release = await api.getApiClient().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}.`,
);
return asset.url;
}
}
} catch (e) {
action.logger.info(
`Looked for ${assetKind} ${assetName} in ${repository} on ${instanceURL} but got error ${e}.`,
);
}
}
return getDefaultDotcomDownloadURL(tagName, assetName);
}

View File

@@ -10,7 +10,7 @@ import {
runInActions,
} from "./action-common";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { getApiDetails, getGitHubVersion } from "./api-client";
import { FeatureEnablement, initFeatures } from "./feature-flags";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { Logger } from "./logging";
@@ -104,7 +104,10 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
// Start the Proxy
const actionWithFeatures = extendActionState(action, { features });
const proxyBin = await getProxyBinaryPath(actionWithFeatures);
const proxyBin = await getProxyBinaryPath(
actionWithFeatures,
getApiDetails(action.env),
);
const proxyInfo = await startProxy(
proxyBin,
proxyConfig,

View File

@@ -6,17 +6,21 @@ import test, { ExecutionContext } from "ava";
import sinon from "sinon";
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 { setUpFeatureFlagTests } from "./feature-flags/testing-util";
import { UnvalidatedObject, validateSchema } from "./json";
import { makeFromSchema } from "./json/testing-util";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger, Logger } from "./logging";
import { BundlePlatform, getBundlePlatform } from "./platform";
import * as repository from "./setup/repository";
import * as startProxyExports from "./start-proxy";
import * as statusReport from "./status-report";
import {
addSinonAssertions,
assertNotLogged,
checkExpectedLogMessages,
createFeatures,
@@ -24,6 +28,7 @@ import {
makeMacro,
makeTestToken,
RecordingLogger,
SAMPLE_DOTCOM_API_DETAILS,
setupTests,
withRecordingLoggerAsync,
} from "./testing-utils";
@@ -705,18 +710,29 @@ function mockGetApiClient(endpoints: any) {
type ReleaseAssets = Array<{ name: string; url?: string }>;
function mockGetReleaseByTag(assets?: ReleaseAssets) {
function mockGetReleaseByTag(expectedTag?: string, assets?: ReleaseAssets) {
const getReleaseByTag =
assets === undefined
? sinon.stub().rejects()
: sinon.stub().resolves({
status: 200,
data: { assets },
headers: {},
url: "GET /repos/:owner/:repo/releases/tags/:tag",
: sinon.stub().callsFake(({ tag }) => {
if (tag === expectedTag) {
return {
status: 200,
data: { assets },
headers: {},
url: "GET /repos/:owner/:repo/releases/tags/:tag",
};
} else {
return {
status: 404,
};
}
});
return mockGetApiClient({ repos: { getReleaseByTag } });
return {
getReleaseByTag,
getApiClient: mockGetApiClient({ repos: { getReleaseByTag } }),
};
}
function mockOfflineFeatures(tempDir: string, logger: Logger) {
@@ -751,25 +767,33 @@ test.serial(
logger,
features,
});
const info = await startProxyExports.getDownloadUrl(state);
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
t.is(
info.url,
startProxyExports.getFallbackUrl(
startProxyExports.getProxyPackage(BundlePlatform.Linux64),
),
// Stub this to always return `true` so we get the default repo for the tests.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
const info = await startProxyExports.getDownloadUrl(
state,
SAMPLE_DOTCOM_API_DETAILS,
);
t.true(
logger.hasMessage(`Unsupported platform android on architecture ppc`),
);
t.is(info.version, defaults.cliVersion);
t.is(
info.url,
repository.getDefaultDotcomDownloadURL(
defaults.bundleVersion,
startProxyExports.getProxyPackage(BundlePlatform.Linux64),
),
);
});
},
);
test.serial(
"getDownloadUrl returns fallback when `getReleaseByVersion` rejects",
"getDownloadUrl returns default bundle URL for linked version",
async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
@@ -777,12 +801,20 @@ test.serial(
await withTmpDir(async (tempDir) => {
const features = mockOfflineFeatures(tempDir, logger);
const state = initAllState({ logger, features });
const info = await startProxyExports.getDownloadUrl(state);
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
// Stub this to always return `true` so we get the default repo for the tests.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
const info = await startProxyExports.getDownloadUrl(
state,
SAMPLE_DOTCOM_API_DETAILS,
);
t.is(info.version, defaults.cliVersion);
t.is(
info.url,
startProxyExports.getFallbackUrl(
repository.getDefaultDotcomDownloadURL(
defaults.bundleVersion,
startProxyExports.getProxyPackage(getTestPlatform(state)),
),
);
@@ -791,7 +823,7 @@ test.serial(
);
test.serial(
"getDownloadUrl returns fallback when there's no matching release asset",
"getDownloadUrl doesn't check that the default asset exists",
async (t) => {
const logger = new RecordingLogger();
const testAssets = [[], [{ name: "foo" }]];
@@ -800,19 +832,28 @@ test.serial(
const features = mockOfflineFeatures(tempDir, logger);
const state = initAllState({ logger, features });
for (const assets of testAssets) {
const stub = mockGetReleaseByTag(assets);
const info = await startProxyExports.getDownloadUrl(state);
// Stub this to always return `true` so we get the default repo for the tests.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
for (const assets of testAssets) {
const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets);
const info = await startProxyExports.getDownloadUrl(
state,
SAMPLE_DOTCOM_API_DETAILS,
);
t.is(info.version, defaults.cliVersion);
t.is(
info.url,
startProxyExports.getFallbackUrl(
repository.getDefaultDotcomDownloadURL(
defaults.bundleVersion,
startProxyExports.getProxyPackage(getTestPlatform(state)),
),
);
stub.restore();
// mockGetReleaseByTag stubs the global getApiClient; since we are calling
// that in a loop, we need to manually restore it here
stubs.getApiClient.restore();
}
});
},
@@ -828,11 +869,22 @@ test.serial("getDownloadUrl returns matching release asset", async (t) => {
url: "url-we-want",
},
];
mockGetReleaseByTag(assets);
const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets);
await withTmpDir(async (tempDir) => {
state.env.set(ActionsEnvVars.RUNNER_TEMP, tempDir);
const features = mockOfflineFeatures(tempDir, logger);
const info = await startProxyExports.getDownloadUrl({ ...state, features });
// Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo");
const info = await startProxyExports.getDownloadUrl(
{ ...state, features },
SAMPLE_DOTCOM_API_DETAILS,
);
t.is(stubs.getReleaseByTag.callCount, 1);
t.is(info.version, defaults.cliVersion);
t.is(info.url, "url-we-want");
@@ -965,9 +1017,13 @@ test.serial(
const toolcachePath = "/path/to/proxy/dir";
sinon.stub(toolcache, "find").returns(toolcachePath);
// Stub this to always return `true` so we get the default repo for the tests.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
const features = mockOfflineFeatures(tempDir, logger);
const path = await startProxyExports.getProxyBinaryPath(
initAllState({ logger, features }),
SAMPLE_DOTCOM_API_DETAILS,
);
t.assert(path);
@@ -985,7 +1041,7 @@ test.serial(
const logger = new RecordingLogger();
const state = initAllState({ logger });
const downloadUrl = "url-we-want";
mockGetReleaseByTag([
const stubs = mockGetReleaseByTag(defaults.bundleVersion, [
{
name: startProxyExports.getProxyPackage(getTestPlatform(state)),
url: downloadUrl,
@@ -994,11 +1050,6 @@ test.serial(
const toolcachePath = "/path/to/proxy/dir";
const find = sinon.stub(toolcache, "find").returns("");
const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
auth: "",
url: "",
apiURL: "",
});
const getAuthorizationHeaderFor = sinon
.stub(apiClient, "getAuthorizationHeaderFor")
.returns(undefined);
@@ -1012,17 +1063,27 @@ test.serial(
.resolves(extractedPath);
const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath);
const path = await startProxyExports.getProxyBinaryPath({
...state,
features: createFeatures([]),
});
// Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo");
const path = await startProxyExports.getProxyBinaryPath(
{
...state,
features: createFeatures([]),
},
SAMPLE_DOTCOM_API_DETAILS,
);
t.is(stubs.getReleaseByTag.callCount, 1);
t.assert(find.calledOnce);
t.assert(getApiDetails.calledOnce);
t.assert(getAuthorizationHeaderFor.calledOnce);
t.assert(downloadTool.calledOnceWith(downloadUrl));
t.assert(extractTar.calledOnceWith(archivePath));
t.assert(cacheDir.calledOnceWith(extractedPath));
const st = addSinonAssertions(t);
st.onceWith(downloadTool, downloadUrl);
st.onceWith(extractTar, archivePath);
st.onceWith(cacheDir, extractedPath);
t.assert(path);
t.is(
path,
@@ -1030,7 +1091,7 @@ test.serial(
);
checkExpectedLogMessages(t, logger.messages, [
`Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`,
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL ${downloadUrl}`,
]);
},
);
@@ -1042,8 +1103,8 @@ test.serial(
const state = initAllState({ logger });
const expectedTag = "codeql-bundle-v2.20.1";
const expectedParams = {
owner: "github",
repo: "codeql-action",
owner: "test",
repo: "repo",
tag: expectedTag,
};
const downloadUrl = "url-we-want";
@@ -1066,11 +1127,6 @@ test.serial(
await withTmpDir(async (tempDir) => {
const toolcachePath = "/path/to/proxy/dir";
const find = sinon.stub(toolcache, "find").returns("");
const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
auth: "",
url: "",
apiURL: "",
});
const getAuthorizationHeaderFor = sinon
.stub(apiClient, "getAuthorizationHeaderFor")
.returns(undefined);
@@ -1100,22 +1156,28 @@ test.serial(
.resolves({
enabledVersions: [{ cliVersion: "2.20.1", tagName: expectedTag }],
});
const path = await startProxyExports.getProxyBinaryPath({
...state,
features,
});
// Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API.
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo");
const path = await startProxyExports.getProxyBinaryPath(
{
...state,
features,
},
SAMPLE_DOTCOM_API_DETAILS,
);
t.assert(getDefaultCliVersion.calledOnce);
sinon.assert.calledOnceWithMatch(
getReleaseByTag,
sinon.match(expectedParams),
);
const st = addSinonAssertions(t);
st.onceWith(getReleaseByTag, expectedParams);
t.assert(find.calledOnce);
t.assert(getApiDetails.calledOnce);
t.assert(getAuthorizationHeaderFor.calledOnce);
t.assert(downloadTool.calledOnceWith(downloadUrl));
t.assert(extractTar.calledOnceWith(archivePath));
t.assert(cacheDir.calledOnceWith(extractedPath));
st.onceWith(downloadTool, downloadUrl);
st.onceWith(extractTar, archivePath);
st.onceWith(cacheDir, extractedPath);
t.assert(path);
t.is(
@@ -1125,7 +1187,7 @@ test.serial(
});
checkExpectedLogMessages(t, logger.messages, [
`Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${expectedTag}' at '${downloadUrl}'`,
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL url-we-want.`,
]);
},
);

View File

@@ -5,10 +5,9 @@ import * as toolcache from "@actions/tool-cache";
import { ActionState } from "./action-common";
import {
getApiClient,
getApiDetails,
getAuthorizationHeaderFor,
getGitHubVersion,
GitHubApiDetails,
} from "./api-client";
import * as artifactScanner from "./artifact-scanner";
import { Config } from "./config-utils";
@@ -22,6 +21,7 @@ import * as json from "./json";
import { BuiltInLanguage } from "./languages";
import { Logger } from "./logging";
import { BundlePlatform, getBundlePlatform } from "./platform";
import { getCodeQLAssetDownloadURL } from "./setup/repository";
import {
Address,
Registry,
@@ -171,10 +171,8 @@ export async function sendFailedStatusReport(
}
}
/** The basename of the proxy artifacts. */
export const UPDATEJOB_PROXY = "update-job-proxy";
export const UPDATEJOB_PROXY_VERSION = "v2.0.20250624110901";
const UPDATEJOB_PROXY_URL_PREFIX =
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.22.0/";
function isPAT(value: string) {
return artifactScanner.isAuthToken(value, [
@@ -379,30 +377,6 @@ export function getProxyPackage(platform: BundlePlatform): string {
return `${UPDATEJOB_PROXY}-${platform}.tar.gz`;
}
/**
* Gets the fallback URL for downloading the proxy release asset.
*
* @param proxyPackage The asset name.
* @returns The full URL to download the specified asset from the fallback release.
*/
export function getFallbackUrl(proxyPackage: string): string {
return `${UPDATEJOB_PROXY_URL_PREFIX}${proxyPackage}`;
}
/**
* Uses the GitHub API to obtain information about the CodeQL CLI bundle release
* that is tagged by `version`.
*
* @returns The response from the GitHub API.
*/
async function getReleaseByVersion(version: string) {
return getApiClient().rest.repos.getReleaseByTag({
owner: "github",
repo: "codeql-action",
tag: version,
});
}
/** Uses `features` to determine the default CLI version. */
async function getCliVersionFromFeatures(
features: FeatureEnablement,
@@ -419,7 +393,8 @@ async function getCliVersionFromFeatures(
* @returns Returns the download URL and version of the proxy package we plan to use.
*/
export async function getDownloadUrl(
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
action: ActionState<["Base", "ReadOnlyEnv", "Logger", "FeatureFlags"]>,
apiDetails: GitHubApiDetails,
): Promise<{ url: string; version: string }> {
// Default to linux64 if we don't recognise the platform+arch pair.
// This maintains the behaviour we had before switching to `getBundlePlatform` here.
@@ -448,37 +423,26 @@ export async function getDownloadUrl(
};
// Try to retrieve information about the CLI bundle release identified by `versionInfo`.
const cliRelease = await getReleaseByVersion(versionInfo.tagName);
const proxyAssetUrl = await getCodeQLAssetDownloadURL(
action,
apiDetails,
versionInfo.tagName,
proxyPackage,
"private registry proxy",
);
// Search the release's assets to find the one we are looking for.
for (const asset of cliRelease.data.assets) {
if (asset.name === proxyPackage) {
action.logger.info(
`Found '${proxyPackage}' in release '${versionInfo.tagName}' at '${asset.url}'`,
);
return {
url: asset.url,
// The `update-job-proxy` doesn't have a version as such. Since we now bundle it
// with CodeQL CLI bundle releases, we use the corresponding CLI version to
// differentiate between (potentially) different versions of `update-job-proxy`.
version: versionInfo.cliVersion,
};
}
}
return {
url: proxyAssetUrl,
// The `update-job-proxy` doesn't have a version as such. Since we now bundle it
// with CodeQL CLI bundle releases, we use the corresponding CLI version to
// differentiate between (potentially) different versions of `update-job-proxy`.
version: versionInfo.cliVersion,
};
} catch (ex) {
action.logger.warning(
throw new Error(
`Failed to retrieve information about the linked release: ${getErrorMessage(ex)}`,
);
}
// Fallback to the hard-coded URL.
action.logger.info(
`Did not find '${proxyPackage}' in the linked release, falling back to hard-coded version.`,
);
return {
url: getFallbackUrl(proxyPackage),
version: UPDATEJOB_PROXY_VERSION,
};
}
/**
@@ -497,9 +461,14 @@ export async function downloadProxy(
try {
// Download the proxy archive from `url`. We let `downloadTool` choose where
// to store it. The path to the downloaded file will be returned if successful.
return toolcache.downloadTool(url, /* dest: */ undefined, authorization, {
accept: "application/octet-stream",
});
return await toolcache.downloadTool(
url,
/* dest: */ undefined,
authorization,
{
accept: "application/octet-stream",
},
);
} catch (error) {
logger.error(
`Failed to download proxy archive from ${url}: ${getErrorMessage(error)}`,
@@ -569,28 +538,48 @@ export function getProxyFilename() {
* @returns The path to the proxy binary.
*/
export async function getProxyBinaryPath(
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
action: ActionState<["Base", "ReadOnlyEnv", "Logger", "FeatureFlags"]>,
apiDetails: GitHubApiDetails,
): Promise<string> {
const logger = action.logger;
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(action);
logger.startGroup("Setting up authentication proxy");
let proxyBin = toolcache.find(proxyFileName, proxyInfo.version);
if (!proxyBin) {
const apiDetails = getApiDetails();
const authorization = getAuthorizationHeaderFor(
logger,
apiDetails,
proxyInfo.url,
);
const temp = await downloadProxy(logger, proxyInfo.url, authorization);
const extracted = await extractProxy(logger, temp);
proxyBin = await cacheProxy(
logger,
extracted,
proxyFileName,
proxyInfo.version,
try {
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(action, apiDetails);
logger.info(
`Using authentication proxy from CodeQL release ${proxyInfo.version}.`,
);
let proxyDir = toolcache.find(proxyFileName, proxyInfo.version);
if (!proxyDir) {
logger.info(
`Authentication proxy is not in the toolcache. Will try to download it from '${proxyInfo.url}'.`,
);
const authorization = getAuthorizationHeaderFor(
logger,
apiDetails,
proxyInfo.url,
);
const temp = await downloadProxy(logger, proxyInfo.url, authorization);
logger.info(`Downloaded the authentication proxy asset to '${temp}'.`);
const extracted = await extractProxy(logger, temp);
proxyDir = await cacheProxy(
logger,
extracted,
proxyFileName,
proxyInfo.version,
);
logger.info(`Extracted the authentication proxy to '${proxyDir}'.`);
}
return path.join(proxyDir, proxyFileName);
} finally {
logger.endGroup();
}
return path.join(proxyBin, proxyFileName);
}

View File

@@ -163,6 +163,46 @@ export function setupTests(testFn: TestFn<any>) {
});
}
export interface AvaSinonAssertions {
/**
* Asserts that `stub` must have been called once with at least the `expected` arguments.
*/
onceWith: <TArgs extends readonly any[]>(
stub: sinon.SinonStub<TArgs>,
...expected: sinon.MatchPartialArguments<TArgs>
) => boolean;
}
/** Adds additional assertions for use with `sinon` to `t`. */
export function addSinonAssertions(
t: ExecutionContext<unknown>,
): ExecutionContext<unknown> & AvaSinonAssertions {
return {
...t,
onceWith: <TArgs extends readonly any[]>(
stub: sinon.SinonStub<TArgs>,
...expected: sinon.MatchPartialArguments<TArgs>
) => {
// Fail if the stub hasn't been called at all so that we can safely compare
// the arguments of the first call in the else branch.
if (stub.callCount === 0) {
t.fail("The stub wasn't called.");
} else {
// Reduce the arguments of each to the same number that was provided,
// so that extra arguments don't lead to a failure.
const actual = stub.args.map<sinon.MatchPartialArguments<TArgs>>(
(args: TArgs) =>
args.slice(
0,
expected.length,
) as sinon.MatchPartialArguments<TArgs>,
);
return t.deepEqual(actual, [expected]);
}
},
};
}
/**
* Declare a reusable test implementation, with better type safety than `test.macro`.
*/

View File

@@ -20,6 +20,8 @@ import {
getRequiredEnvParam,
isInTestMode,
exportEnvVar,
getEnv,
type Env,
} from "./environment";
import * as json from "./json";
import { Language } from "./languages";
@@ -45,10 +47,13 @@ const BASE_DATABASE_OIDS_FILE_NAME = "base-database-oids.json";
*/
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 = "https://github.com";
export const GITHUB_DOTCOM_URL: InstanceURL = "https://github.com";
/**
* Default name of the debugging artifact.
@@ -599,12 +604,12 @@ export function assertNever(value: never): never {
* Set some initial environment variables that we can set even without
* knowing what version of CodeQL we're running.
*/
export function initializeEnvironment(version: string) {
exportEnvVar(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
exportEnvVar(EnvVar.FEATURE_SANDWICH, "false");
exportEnvVar(EnvVar.FEATURE_SARIF_COMBINE, "true");
exportEnvVar(EnvVar.FEATURE_WILL_UPLOAD, "true");
exportEnvVar(EnvVar.VERSION, version);
export function initializeEnvironment(version: string, env: Env = getEnv()) {
env.export(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
env.export(EnvVar.FEATURE_SANDWICH, "false");
env.export(EnvVar.FEATURE_SARIF_COMBINE, "true");
env.export(EnvVar.FEATURE_WILL_UPLOAD, "true");
env.export(EnvVar.VERSION, version);
}
export class HTTPError extends Error {