diff --git a/lib/entry-points.js b/lib/entry-points.js index a2b225051..cc20e39e2 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -152243,6 +152243,15 @@ function getDefaultBundleSources(apiDetails, logger) { return !self2.slice(0, index2).some((other) => (0, import_fast_deep_equal.default)(source, other)); }); } +function isCacheableRelease(reference, cliVersion2, apiDetails, logger) { + if (cliVersion2 === void 0 || !/^\d+\.\d+\.\d+$/.test(cliVersion2) || reference.tagName !== `codeql-bundle-v${cliVersion2}`) { + return false; + } + const repository = `${reference.owner}/${reference.repo}`.toLowerCase(); + return getDefaultBundleSources(apiDetails, logger).some( + ([serverURL, sourceRepository]) => new URL(serverURL).origin === new URL(reference.serverURL).origin && sourceRepository.toLowerCase() === repository + ); +} async function selectDefaultBundle(action, tagName, apiDetails, options) { const { logger } = action; for (const [serverURL, repository] of getDefaultBundleSources( @@ -152534,7 +152543,9 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO release2.assetNames ?? [], logger ); - customReleaseURL = release2.url; + if (!isCacheableRelease(requestedRelease, cliVersion2, apiDetails, logger)) { + customReleaseURL = release2.url; + } } else if (toolsInput !== void 0) { tagName = tryGetTagNameFromUrl(toolsInput, logger); url2 = toolsInput; @@ -152776,7 +152787,7 @@ var downloadCodeQL = async function(source, apiDetails, tarVersion, tempDir, log function getToolcacheDestination({ logger }, source) { if (source.customReleaseURL !== void 0) { return new Failure( - `Not caching the CodeQL tools from ${source.customReleaseURL}, since we don't cache releases requested by URL.` + `Not caching the CodeQL tools from ${source.customReleaseURL}, since we only cache stable releases in the CodeQL Action repositories.` ); } if (source.bundle.kind !== "combined") { diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index 98a75d98c..d8840694d 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -1519,15 +1519,88 @@ test.serial( }, ); +for (const [apiDetails, variant] of [ + [SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM], + [GHES_API_DETAILS, GitHubVariant.GHES], +] as const) { + test.serial( + `setupCodeQLBundle caches a stable release in the CodeQL Action repository on ${variant}`, + async (t) => { + const fixture = stubRequestedRelease({ + apiDetails, + repository: "github/codeql-action", + }); + const extract = stubDownloadAndExtract(); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const setup = () => + setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + apiDetails, + tmpDir, + variant, + PER_LANGUAGE_CLI_VERSION, + undefined, // rawLanguages + false, // useOverlayAwareDefaultCliVersion + createFeatures([]), + getRunnerLogger(true), + ); + + const downloaded = await setup(); + t.deepEqual(fixture.requests, [ + `${apiDetails.apiURL}/repos/github/codeql-action/releases/tags/codeql-bundle-v${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}`, + ]); + t.is(downloaded.toolsSource, setupCodeql.ToolsSource.Download); + t.is(extract.firstCall.args[0], fixture.assets[0].url); + t.is(extract.firstCall.args[3], `token ${apiDetails.auth}`); + t.is( + downloaded.codeqlFolder, + toolcache.find("CodeQL", MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION), + ); + + const cached = await setup(); + t.is(cached.toolsSource, setupCodeql.ToolsSource.Toolcache); + t.is(cached.codeqlFolder, downloaded.codeqlFolder); + t.true(extract.calledOnce); + }); + }, + ); +} + for (const { repository, tagName, markers } of [ + { + repository: "github/codeql-action", + tagName: "codeql-bundle-v2.27.1-rc.1", + markers: [], + }, + { + repository: "github/codeql-action", + tagName: "codeql-bundle-v2.27.1", + markers: ["2.27.1+202609241200"], + }, + { + repository: "github/codeql-action", + tagName: "codeql-bundle-20260924", + markers: ["2.27.1+202609241200"], + }, + { + repository: "github/codeql-action", + tagName: "codeql-bundle-acme-2.27.1", + markers: ["2.27.1"], + }, { repository: "octo/tools", tagName: "codeql-bundle-feature_branch", markers: [], }, ]) { + const description = + markers.length === 0 + ? tagName + : `${tagName} with marker ${markers.join(", ")}`; test.serial( - `setupCodeQLBundle doesn't share the toolcache with ${tagName} in ${repository}`, + `setupCodeQLBundle doesn't share the toolcache with ${description} in ${repository}`, async (t) => { const fixture = stubRequestedRelease({ repository, tagName, markers }); const extract = stubDownloadAndExtract(); @@ -1590,7 +1663,10 @@ for (const repository of ["github/codeql-action", "octo/tools"]) { `https://github.com/${repository}/releases/download/codeql-bundle-v${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}/codeql-bundle-linux64.tar.zst`, ); t.is(extract.firstCall.args[3], undefined); - t.false(fs.existsSync(`${result.codeqlFolder}.complete`)); + t.is( + fs.existsSync(`${result.codeqlFolder}.complete`), + repository === "github/codeql-action", + ); }); }, ); diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index b6117343d..47cd26686 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -26,6 +26,7 @@ import { BundleSelection, BundleSelectionOptions, CodeQLRelease, + CodeQLReleaseReference, getPublicRelease, getRelease, getReleaseCliVersion, @@ -114,6 +115,32 @@ function getDefaultBundleSources( }); } +/** + * Whether a release requested by URL contains the same build as the toolcache entry for its CLI + * version. The toolcache is keyed by version alone, so we only assume this for stable releases + * tagged `codeql-bundle-v` in the repositories we download the default bundles from. + */ +function isCacheableRelease( + reference: CodeQLReleaseReference, + cliVersion: string | undefined, + apiDetails: api.GitHubApiDetails, + logger: Logger, +): boolean { + if ( + cliVersion === undefined || + !/^\d+\.\d+\.\d+$/.test(cliVersion) || + reference.tagName !== `codeql-bundle-v${cliVersion}` + ) { + return false; + } + const repository = `${reference.owner}/${reference.repo}`.toLowerCase(); + return getDefaultBundleSources(apiDetails, logger).some( + ([serverURL, sourceRepository]) => + new URL(serverURL).origin === new URL(reference.serverURL).origin && + sourceRepository.toLowerCase() === repository, + ); +} + /** * Selects a bundle from the first release tagged `tagName` that has a compatible bundle, trying the * Action repositories on this GitHub instance before the canonical Action on GitHub.com. If we @@ -405,9 +432,10 @@ async function resolveDefaultCliVersion( * * - A local path is extracted without using the toolcache. * - A release URL selects a bundle from that release, and takes precedence over the `force_nightly` - * feature flag. We don't use the toolcache, since a release may contain a different build than - * the cached bundle for its version. Bundle URLs keep using the toolcache for the version in - * their tag, for compatibility. + * feature flag. Only stable releases in the repositories we download the default bundles from use + * the toolcache, since other releases may contain a different build than the cached bundle for + * their version. Bundle URLs keep using the toolcache for the version in their tag, for + * compatibility. * - `nightly` or `nightly-latest`, or the `force_nightly` feature flag in a dynamic workflow, * selects a bundle from the latest nightly release. We then continue with that bundle's URL. * - `linked`, or its old name `latest`, selects the version shipped with the Action. @@ -625,7 +653,9 @@ export async function getCodeQLSource( release.assetNames ?? [], logger, ); - customReleaseURL = release.url; + if (!isCacheableRelease(requestedRelease, cliVersion, apiDetails, logger)) { + customReleaseURL = release.url; + } } else if (toolsInput !== undefined) { // Any other value is a bundle URL, including one we selected from the latest nightly above. // We use the version in its tag, if any, for the toolcache, so we assume that bundles with the @@ -971,8 +1001,8 @@ function getToolcacheDestination( ): util.Result { if (source.customReleaseURL !== undefined) { return new util.Failure( - `Not caching the CodeQL tools from ${source.customReleaseURL}, since we don't cache ` + - "releases requested by URL.", + `Not caching the CodeQL tools from ${source.customReleaseURL}, since we only cache stable ` + + "releases in the CodeQL Action repositories.", ); } if (source.bundle.kind !== "combined") {