diff --git a/lib/entry-points.js b/lib/entry-points.js index 21c0ccd15f..6e080eaa5d 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -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(); } }); @@ -107096,41 +107096,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) { @@ -169845,12 +169810,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; @@ -170475,12 +170440,24 @@ function fixCodeQualityCategory(logger2, category, env = getEnv()) { // src/logging.ts var core5 = __toESM(require_core()); +var joinMessageStrings = (message) => { + if (Array.isArray(message)) { + return message.join(" "); + } else { + return message; + } +}; +var autoJoinMessageStrings = (fn) => { + return (message) => { + return fn(joinMessageStrings(message)); + }; +}; function getActionsLogger() { return { - debug: core5.debug, - info: core5.info, - warning: core5.warning, - error: core5.error, + debug: autoJoinMessageStrings(core5.debug), + info: autoJoinMessageStrings(core5.info), + warning: autoJoinMessageStrings(core5.warning), + error: autoJoinMessageStrings(core5.error), isDebug: core5.isDebug, startGroup: core5.startGroup, endGroup: core5.endGroup @@ -170654,6 +170631,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"); } @@ -175228,7 +175208,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 @@ -175684,6 +175663,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 [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 === 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")); @@ -176075,64 +176126,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) { @@ -176490,9 +176488,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, @@ -176502,11 +176501,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) { @@ -182967,7 +182966,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", @@ -182992,7 +182991,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); @@ -187559,8 +187558,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, @@ -187678,21 +187675,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( @@ -187709,37 +187696,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, @@ -187778,28 +187757,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 proxyBin = toolcache4.find(proxyFileName, proxyInfo.version); + if (!proxyBin) { + 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 assset to '${temp}'.`); + const extracted = await extractProxy(logger2, temp); + proxyBin = await cacheProxy( + logger2, + extracted, + proxyFileName, + proxyInfo.version + ); + logger2.info(`Extracted the authentication proxy to '${proxyBin}'.`); + } + return path27.join(proxyBin, proxyFileName); + } finally { + logger2.endGroup(); } - return path27.join(proxyBin, proxyFileName); } // src/start-proxy/ca.ts @@ -188162,7 +188153,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, diff --git a/package-lock.json b/package-lock.json index 050ff1e484..d5b4a5c4d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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.0", "get-folder-size": "^5.0.0", "https-proxy-agent": "^7.0.6", @@ -5817,6 +5816,7 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "dev": true, "license": "MIT" }, "node_modules/fast-diff": { diff --git a/package.json b/package.json index 51fd4a4505..513f4b528e 100644 --- a/package.json +++ b/package.json @@ -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.0", "get-folder-size": "^5.0.0", "https-proxy-agent": "^7.0.6", diff --git a/src/actions-util.ts b/src/actions-util.ts index ea9a119ed4..04e78eec39 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -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); diff --git a/src/logging.test.ts b/src/logging.test.ts new file mode 100644 index 0000000000..33f6f8c162 --- /dev/null +++ b/src/logging.test.ts @@ -0,0 +1,17 @@ +import test from "ava"; + +import { joinMessageStrings } from "./logging"; +import { setupTests } from "./testing-utils"; + +setupTests(test); + +test("joinMessageStrings", async (t) => { + // For strings and errors, it is the identity function. + t.deepEqual(joinMessageStrings("Hello"), "Hello"); + + const error = new Error("Some error"); + t.deepEqual(joinMessageStrings(error), error); + + // For arrays of strings, we get a join-ed string. + t.deepEqual(joinMessageStrings(["foo", "bar"]), "foo bar"); +}); diff --git a/src/logging.ts b/src/logging.ts index 2c34cb54d4..177aca6c7b 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,10 +1,16 @@ import * as core from "@actions/core"; +/** Types that all of our logging functions accept. */ +export type Loggable = string | string[]; + +/** Some logging functions also accept errors directly. */ +export type LoggableError = Loggable | Error; + export interface Logger { - debug: (message: string) => void; - info: (message: string) => void; - warning: (message: string | Error) => void; - error: (message: string | Error) => void; + debug: (message: Loggable) => void; + info: (message: Loggable) => void; + warning: (message: LoggableError) => void; + error: (message: LoggableError) => void; isDebug: () => boolean; @@ -12,12 +18,33 @@ export interface Logger { endGroup: () => void; } +/** If `message` is an array of strings, the array is `join`-ed into one string separated by spaces. */ +export const joinMessageStrings = ( + message: string[] | string | T, +): string | T => { + if (Array.isArray(message)) { + return message.join(" "); + } else { + return message; + } +}; + +/** + * Wraps a logging function so that `joinMessageStrings` is applied to the input before + * calling the wrapped logging function. + */ +const autoJoinMessageStrings = (fn: (msg: string | T) => void) => { + return (message: string[] | string | T) => { + return fn(joinMessageStrings(message)); + }; +}; + export function getActionsLogger(): Logger { return { - debug: core.debug, - info: core.info, - warning: core.warning, - error: core.error, + debug: autoJoinMessageStrings(core.debug), + info: autoJoinMessageStrings(core.info), + warning: autoJoinMessageStrings(core.warning), + error: autoJoinMessageStrings(core.error), isDebug: core.isDebug, startGroup: core.startGroup, endGroup: core.endGroup, diff --git a/src/repository.ts b/src/repository.ts index f1e7f369bc..652521fe33 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -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`. diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index 3fef11e63e..6bdb29f462 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -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) => { diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index daaba174c4..07666d6302 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -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 { - 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(); diff --git a/src/setup/repository.test.ts b/src/setup/repository.test.ts new file mode 100644 index 0000000000..d1c6d034c0 --- /dev/null +++ b/src/setup/repository.test.ts @@ -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"); +}); diff --git a/src/setup/repository.ts b/src/setup/repository.ts new file mode 100644 index 0000000000..959ca4b8d7 --- /dev/null +++ b/src/setup/repository.ts @@ -0,0 +1,153 @@ +import { ActionState } from "../action-common"; +import { isRunningLocalAction } from "../actions-util"; +import * as api from "../api-client"; +import { ActionsEnvVars } from "../environment"; +import { isRepositoryNwoStr, 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 { + 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 [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 === 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); +} diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index bc3351da53..2f6f769884 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -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, diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index bd24897136..d01543e709 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -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 }); + // Stub this to always return `true` so we get the default repo for the tests. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + for (const assets of testAssets) { - const stub = mockGetReleaseByTag(assets); - const info = await startProxyExports.getDownloadUrl(state); + const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets); + const info = await startProxyExports.getDownloadUrl( + state, + SAMPLE_DOTCOM_API_DETAILS, + ); - t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION); + 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, - }); - t.assert(getDefaultCliVersion.calledOnce); - sinon.assert.calledOnceWithMatch( - getReleaseByTag, - sinon.match(expectedParams), + // 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); + + 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.`, ]); }, ); diff --git a/src/start-proxy.ts b/src/start-proxy.ts index 6221c9940f..21a5012f12 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -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 { const logger = action.logger; - const proxyFileName = getProxyFilename(); - const proxyInfo = await getDownloadUrl(action); - - 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, + logger.startGroup("Setting up authentication proxy"); + + try { + const proxyFileName = getProxyFilename(); + const proxyInfo = await getDownloadUrl(action, apiDetails); + + logger.info( + `Using authentication proxy from CodeQL release ${proxyInfo.version}.`, ); + + let proxyBin = toolcache.find(proxyFileName, proxyInfo.version); + + if (!proxyBin) { + 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 assset to '${temp}'.`); + + const extracted = await extractProxy(logger, temp); + proxyBin = await cacheProxy( + logger, + extracted, + proxyFileName, + proxyInfo.version, + ); + + logger.info(`Extracted the authentication proxy to '${proxyBin}'.`); + } + return path.join(proxyBin, proxyFileName); + } finally { + logger.endGroup(); } - return path.join(proxyBin, proxyFileName); } diff --git a/src/testing-utils.ts b/src/testing-utils.ts index cd60318944..4ff4d49343 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -30,7 +30,7 @@ import { featureConfig, FeatureEnablement, } from "./feature-flags"; -import { Logger } from "./logging"; +import { joinMessageStrings, Loggable, LoggableError, Logger } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { getBundlePlatform } from "./platform"; import { ActionName } from "./status-report"; @@ -163,6 +163,46 @@ export function setupTests(testFn: TestFn) { }); } +export interface AvaSinonAssertions { + /** + * Asserts that `stub` must have been called once with at least the `expected` arguments. + */ + onceWith: ( + stub: sinon.SinonStub, + ...expected: sinon.MatchPartialArguments + ) => boolean; +} + +/** Adds additional assertions for use with `sinon` to `t`. */ +export function addSinonAssertions( + t: ExecutionContext, +): ExecutionContext & AvaSinonAssertions { + return { + ...t, + onceWith: ( + stub: sinon.SinonStub, + ...expected: sinon.MatchPartialArguments + ) => { + // 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>( + (args: TArgs) => + args.slice( + 0, + expected.length, + ) as sinon.MatchPartialArguments, + ); + return t.deepEqual(actual, [expected]); + } + }, + }; +} + /** * Declare a reusable test implementation, with better type safety than `test.macro`. */ @@ -618,8 +658,8 @@ export class RecordingLogger implements Logger { constructor(private readonly logToConsole: boolean = true) {} - private addMessage(level: LogLevel, message: string | Error): void { - this.messages.push({ type: level, message }); + private addMessage(level: LogLevel, message: LoggableError): void { + this.messages.push({ type: level, message: joinMessageStrings(message) }); if (this.logToConsole) { // eslint-disable-next-line no-console @@ -644,19 +684,19 @@ export class RecordingLogger implements Logger { return true; } - debug(message: string) { + debug(message: Loggable) { this.addMessage("debug", message); } - info(message: string) { + info(message: Loggable) { this.addMessage("info", message); } - warning(message: string | Error) { + warning(message: LoggableError) { this.addMessage("warning", message); } - error(message: string | Error) { + error(message: LoggableError) { this.addMessage("error", message); } diff --git a/src/util.ts b/src/util.ts index 674e2a2eef..0b10e0f043 100644 --- a/src/util.ts +++ b/src/util.ts @@ -19,6 +19,8 @@ import { getRequiredEnvParam, isInTestMode, exportEnvVar, + getEnv, + type Env, } from "./environment"; import * as json from "./json"; import { Language } from "./languages"; @@ -44,10 +46,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. @@ -595,12 +600,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 {