Merge branch 'main' into henrymercer/overlay-repo-property

This commit is contained in:
Henry Mercer
2026-02-26 15:27:25 +00:00
16 changed files with 300 additions and 57 deletions
+6
View File
@@ -77,6 +77,7 @@ export enum Feature {
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Note that this currently only disables baseline file coverage information. */
SkipFileCoverageOnPrs = "skip_file_coverage_on_prs",
StartProxyUseFeaturesRelease = "start_proxy_use_features_release",
UploadOverlayDbToApi = "upload_overlay_db_to_api",
UseRepositoryProperties = "use_repository_properties_v2",
ValidateDbConfig = "validate_db_config",
@@ -327,6 +328,11 @@ export const featureConfig = {
// cannot be found when interpreting results.
minimumVersion: undefined,
},
[Feature.StartProxyUseFeaturesRelease]: {
defaultValue: false,
envVar: "CODEQL_ACTION_START_PROXY_USE_FEATURES_RELEASE",
minimumVersion: undefined,
},
[Feature.UploadOverlayDbToApi]: {
defaultValue: false,
envVar: "CODEQL_ACTION_UPLOAD_OVERLAY_DB_TO_API",
+2 -2
View File
@@ -5,7 +5,7 @@ import * as core from "@actions/core";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import { FeatureEnablement, initFeatures } from "./feature-flags";
import { KnownLanguage } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
@@ -98,7 +98,7 @@ async function run(startedAt: Date) {
};
// Start the Proxy
const proxyBin = await getProxyBinaryPath(logger);
const proxyBin = await getProxyBinaryPath(logger, features);
const proxyInfo = await startProxy(
proxyBin,
proxyConfig,
+171 -37
View File
@@ -7,6 +7,7 @@ import sinon from "sinon";
import * as apiClient from "./api-client";
import * as defaults from "./defaults.json";
import { setUpFeatureFlagTests } from "./feature-flags/testing-util";
import { KnownLanguage } from "./languages";
import { getRunnerLogger, Logger } from "./logging";
import * as startProxyExports from "./start-proxy";
@@ -14,12 +15,19 @@ import { parseLanguage } from "./start-proxy";
import * as statusReport from "./status-report";
import {
checkExpectedLogMessages,
createFeatures,
getRecordingLogger,
makeTestToken,
RecordingLogger,
setupTests,
withRecordingLoggerAsync,
} from "./testing-utils";
import { ConfigurationError } from "./util";
import {
ConfigurationError,
GitHubVariant,
GitHubVersion,
withTmpDir,
} from "./util";
setupTests(test);
@@ -347,8 +355,18 @@ test("parseLanguage", async (t) => {
t.deepEqual(parseLanguage(""), undefined);
});
function mockGetReleaseByTag(assets?: Array<{ name: string; url?: string }>) {
const mockClient = sinon.stub(apiClient, "getApiClient");
function mockGetApiClient(endpoints: any) {
return (
sinon
.stub(apiClient, "getApiClient")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.returns({ rest: endpoints } as any)
);
}
type ReleaseAssets = Array<{ name: string; url?: string }>;
function mockGetReleaseByTag(assets?: ReleaseAssets) {
const getReleaseByTag =
assets === undefined
? sinon.stub().rejects()
@@ -359,57 +377,82 @@ function mockGetReleaseByTag(assets?: Array<{ name: string; url?: string }>) {
url: "GET /repos/:owner/:repo/releases/tags/:tag",
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockClient.returns({
rest: {
repos: {
getReleaseByTag,
},
},
} as any);
return mockClient;
return mockGetApiClient({ repos: { getReleaseByTag } });
}
test("getDownloadUrl returns fallback when `getLinkedRelease` rejects", async (t) => {
function mockOfflineFeatures(tempDir: string, logger: Logger) {
// Using GHES ensures that we are using `OfflineFeatures`.
const gitHubVersion = {
type: GitHubVariant.GHES,
version: "3.0.0",
};
sinon.stub(apiClient, "getGitHubVersion").resolves(gitHubVersion);
return setUpFeatureFlagTests(tempDir, logger, gitHubVersion);
}
test("getDownloadUrl returns fallback when `getReleaseByVersion` rejects", async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
const info = await startProxyExports.getDownloadUrl(getRunnerLogger(true));
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
t.is(
info.url,
startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
);
});
test("getDownloadUrl returns fallback when there's no matching release asset", async (t) => {
const testAssets = [[], [{ name: "foo" }]];
for (const assets of testAssets) {
const stub = mockGetReleaseByTag(assets);
const info = await startProxyExports.getDownloadUrl(getRunnerLogger(true));
await withTmpDir(async (tempDir) => {
const features = mockOfflineFeatures(tempDir, logger);
const info = await startProxyExports.getDownloadUrl(
getRunnerLogger(true),
features,
);
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
t.is(
info.url,
startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
);
});
});
stub.restore();
}
test("getDownloadUrl returns fallback when there's no matching release asset", async (t) => {
const logger = new RecordingLogger();
const testAssets = [[], [{ name: "foo" }]];
await withTmpDir(async (tempDir) => {
const features = mockOfflineFeatures(tempDir, logger);
for (const assets of testAssets) {
const stub = mockGetReleaseByTag(assets);
const info = await startProxyExports.getDownloadUrl(
getRunnerLogger(true),
features,
);
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
t.is(
info.url,
startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
);
stub.restore();
}
});
});
test("getDownloadUrl returns matching release asset", async (t) => {
const logger = new RecordingLogger();
const assets = [
{ name: "foo", url: "other-url" },
{ name: startProxyExports.getProxyPackage(), url: "url-we-want" },
];
mockGetReleaseByTag(assets);
const info = await startProxyExports.getDownloadUrl(getRunnerLogger(true));
await withTmpDir(async (tempDir) => {
const features = mockOfflineFeatures(tempDir, logger);
const info = await startProxyExports.getDownloadUrl(
getRunnerLogger(true),
features,
);
t.is(info.version, defaults.cliVersion);
t.is(info.url, "url-we-want");
t.is(info.version, defaults.cliVersion);
t.is(info.url, "url-we-want");
});
});
test("credentialToStr - hides passwords", (t) => {
@@ -560,13 +603,15 @@ test(
);
test("getProxyBinaryPath - returns path from tool cache if available", async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
await withRecordingLoggerAsync(async (logger) => {
await withTmpDir(async (tempDir) => {
const toolcachePath = "/path/to/proxy/dir";
sinon.stub(toolcache, "find").returns(toolcachePath);
const path = await startProxyExports.getProxyBinaryPath(logger);
const features = mockOfflineFeatures(tempDir, logger);
const path = await startProxyExports.getProxyBinaryPath(logger, features);
t.assert(path);
t.is(
@@ -577,12 +622,80 @@ test("getProxyBinaryPath - returns path from tool cache if available", async (t)
});
test("getProxyBinaryPath - downloads proxy if not in cache", async (t) => {
const logger = new RecordingLogger();
const downloadUrl = "url-we-want";
mockGetReleaseByTag([
{ name: startProxyExports.getProxyPackage(), url: downloadUrl },
]);
await withRecordingLoggerAsync(async (logger) => {
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);
const archivePath = "/path/to/archive";
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.resolves(archivePath);
const extractedPath = "/path/to/extracted";
const extractTar = sinon
.stub(toolcache, "extractTar")
.resolves(extractedPath);
const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath);
const path = await startProxyExports.getProxyBinaryPath(
logger,
createFeatures([]),
);
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));
t.assert(path);
t.is(
path,
filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
);
checkExpectedLogMessages(t, logger.messages, [
`Found '${startProxyExports.getProxyPackage()}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`,
]);
});
test("getProxyBinaryPath - downloads proxy based on features if not in cache", async (t) => {
const logger = new RecordingLogger();
const expectedTag = "codeql-bundle-v2.20.1";
const expectedParams = {
owner: "github",
repo: "codeql-action",
tag: expectedTag,
};
const downloadUrl = "url-we-want";
const assets = [
{
name: startProxyExports.getProxyPackage(),
url: downloadUrl,
},
];
const getReleaseByTag = sinon.stub();
getReleaseByTag.withArgs(sinon.match(expectedParams)).resolves({
status: 200,
data: { assets },
headers: {},
url: "GET /repos/:owner/:repo/releases/tags/:tag",
});
mockGetApiClient({ repos: { getReleaseByTag } });
await withTmpDir(async (tempDir) => {
const toolcachePath = "/path/to/proxy/dir";
const find = sinon.stub(toolcache, "find").returns("");
const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
@@ -603,8 +716,25 @@ test("getProxyBinaryPath - downloads proxy if not in cache", async (t) => {
.resolves(extractedPath);
const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath);
const path = await startProxyExports.getProxyBinaryPath(logger);
const gitHubVersion: GitHubVersion = {
type: GitHubVariant.DOTCOM,
};
sinon.stub(apiClient, "getGitHubVersion").resolves(gitHubVersion);
const features = setUpFeatureFlagTests(tempDir, logger, gitHubVersion);
sinon.stub(features, "getValue").callsFake(async (_feature, _codeql) => {
return true;
});
const getDefaultCliVersion = sinon
.stub(features, "getDefaultCliVersion")
.resolves({ cliVersion: "2.20.1", tagName: expectedTag });
const path = await startProxyExports.getProxyBinaryPath(logger, features);
t.assert(getDefaultCliVersion.calledOnce);
sinon.assert.calledOnceWithMatch(
getReleaseByTag,
sinon.match(expectedParams),
);
t.assert(find.calledOnce);
t.assert(getApiDetails.calledOnce);
t.assert(getAuthorizationHeaderFor.calledOnce);
@@ -618,4 +748,8 @@ test("getProxyBinaryPath - downloads proxy if not in cache", async (t) => {
filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
);
});
checkExpectedLogMessages(t, logger.messages, [
`Found '${startProxyExports.getProxyPackage()}' in release '${expectedTag}' at '${downloadUrl}'`,
]);
});
+41 -9
View File
@@ -7,10 +7,16 @@ import {
getApiClient,
getApiDetails,
getAuthorizationHeaderFor,
getGitHubVersion,
} from "./api-client";
import * as artifactScanner from "./artifact-scanner";
import { Config } from "./config-utils";
import * as defaults from "./defaults.json";
import {
CodeQLDefaultVersionInfo,
Feature,
FeatureEnablement,
} from "./feature-flags";
import { KnownLanguage } from "./languages";
import { Logger } from "./logging";
import {
@@ -391,46 +397,69 @@ export function getFallbackUrl(proxyPackage: string): string {
/**
* Uses the GitHub API to obtain information about the CodeQL CLI bundle release
* that is pointed at by `defaults.json`.
* that is tagged by `version`.
*
* @returns The response from the GitHub API.
*/
async function getLinkedRelease() {
async function getReleaseByVersion(version: string) {
return getApiClient().rest.repos.getReleaseByTag({
owner: "github",
repo: "codeql-action",
tag: defaults.bundleVersion,
tag: version,
});
}
/** Uses `features` to determine the default CLI version. */
async function getCliVersionFromFeatures(
features: FeatureEnablement,
): Promise<CodeQLDefaultVersionInfo> {
const gitHubVersion = await getGitHubVersion();
return await features.getDefaultCliVersion(gitHubVersion.type);
}
/**
* Determines the URL of the proxy release asset that we should download if its not
* already in the toolcache, and its version.
*
* @param logger The logger to use.
* @param features Information about enabled features.
* @returns Returns the download URL and version of the proxy package we plan to use.
*/
export async function getDownloadUrl(
logger: Logger,
features: FeatureEnablement,
): Promise<{ url: string; version: string }> {
const proxyPackage = getProxyPackage();
try {
// Try to retrieve information about the CLI bundle release pointed at by `defaults.json`.
const cliRelease = await getLinkedRelease();
const useFeaturesToDetermineCLI = await features.getValue(
Feature.StartProxyUseFeaturesRelease,
);
// Retrieve information about the CLI version we should use. This will be either the linked
// version, or the one enabled by FFs.
const versionInfo = useFeaturesToDetermineCLI
? await getCliVersionFromFeatures(features)
: {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
};
// Try to retrieve information about the CLI bundle release identified by `versionInfo`.
const cliRelease = await getReleaseByVersion(versionInfo.tagName);
// Search the release's assets to find the one we are looking for.
for (const asset of cliRelease.data.assets) {
if (asset.name === proxyPackage) {
logger.info(
`Found '${proxyPackage}' in release '${defaults.bundleVersion}' at '${asset.url}'`,
`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: defaults.cliVersion,
version: versionInfo.cliVersion,
};
}
}
@@ -548,9 +577,12 @@ export function getProxyFilename() {
* @param logger The logger to use.
* @returns The path to the proxy binary.
*/
export async function getProxyBinaryPath(logger: Logger): Promise<string> {
export async function getProxyBinaryPath(
logger: Logger,
features: FeatureEnablement,
): Promise<string> {
const proxyFileName = getProxyFilename();
const proxyInfo = await getDownloadUrl(logger);
const proxyInfo = await getDownloadUrl(logger, features);
let proxyBin = toolcache.find(proxyFileName, proxyInfo.version);
if (!proxyBin) {