Support determining Dotcom CLI version from feature flags

This commit is contained in:
Henry Mercer 2023-01-05 13:11:53 +00:00
parent 6ba0a36550
commit cdb90196f2
12 changed files with 307 additions and 7 deletions

46
lib/feature-flags.js generated
View file

@ -23,7 +23,11 @@ exports.Features = exports.FEATURE_FLAGS_FILE_NAME = exports.featureConfig = exp
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const api_client_1 = require("./api-client");
const defaults = __importStar(require("./defaults.json")); // Referenced from codeql-action-sync-tool!
const util = __importStar(require("./util"));
const DEFAULT_VERSION_FEATURE_FLAG_PREFIX = "default_codeql_version_";
const DEFAULT_VERSION_FEATURE_FLAG_SUFFIX = "_enabled";
const MINIMUM_ENABLED_CODEQL_VERSION = "2.11.6";
var Feature;
(function (Feature) {
Feature["BypassToolcacheEnabled"] = "bypass_toolcache_enabled";
@ -78,6 +82,9 @@ class Features {
constructor(gitHubVersion, repositoryNwo, tempDir, logger) {
this.gitHubFeatureFlags = new GitHubFeatureFlags(gitHubVersion, repositoryNwo, path.join(tempDir, exports.FEATURE_FLAGS_FILE_NAME), logger);
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
/**
*
* @param feature The feature to check.
@ -127,6 +134,45 @@ class GitHubFeatureFlags {
this.logger = logger;
/**/
}
static getCliVersionFromFeatureFlag(f) {
if (!f.startsWith(DEFAULT_VERSION_FEATURE_FLAG_PREFIX) ||
!f.endsWith(DEFAULT_VERSION_FEATURE_FLAG_SUFFIX)) {
return undefined;
}
return f
.substring(DEFAULT_VERSION_FEATURE_FLAG_PREFIX.length, f.length - DEFAULT_VERSION_FEATURE_FLAG_SUFFIX.length)
.replace(/_/g, ".");
}
async getDefaultCliVersion(variant) {
if (variant === util.GitHubVariant.DOTCOM) {
return {
cliVersion: await this.getDefaultDotcomCliVersion(),
variant,
};
}
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
variant,
};
}
async getDefaultDotcomCliVersion() {
const response = await this.getAllFeatures();
const enabledFeatureFlagCliVersions = Object.entries(response)
.map(([f, isEnabled]) => isEnabled
? GitHubFeatureFlags.getCliVersionFromFeatureFlag(f)
: undefined)
.filter((f) => f !== undefined)
.map((f) => f);
if (enabledFeatureFlagCliVersions.length === 0) {
this.logger.debug("Feature flags do not specify a default CLI version. Falling back to CLI version " +
`${MINIMUM_ENABLED_CODEQL_VERSION}.`);
return MINIMUM_ENABLED_CODEQL_VERSION;
}
const maxCliVersion = enabledFeatureFlagCliVersions.reduce((maxVersion, currentVersion) => currentVersion > maxVersion ? currentVersion : maxVersion, enabledFeatureFlagCliVersions[0]);
this.logger.debug(`Derived default CLI version of ${maxCliVersion} from feature flags.`);
return maxCliVersion;
}
async getValue(feature) {
const response = await this.getAllFeatures();
if (response === undefined) {

File diff suppressed because one or more lines are too long

View file

@ -25,6 +25,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const ava_1 = __importDefault(require("ava"));
const defaults = __importStar(require("./defaults.json")); // Referenced from codeql-action-sync-tool!
const feature_flags_1 = require("./feature-flags");
const logging_1 = require("./logging");
const repository_1 = require("./repository");
@ -208,6 +209,46 @@ for (const feature of Object.keys(feature_flags_1.featureConfig)) {
t.false(await featureEnablement.getValue(feature_flags_1.Feature.CliConfigFileEnabled, includeCodeQlIfRequired(feature_flags_1.Feature.CliConfigFileEnabled)), "Feature flag should be disabled after setting env var");
});
});
for (const variant of [util_1.GitHubVariant.GHAE, util_1.GitHubVariant.GHES]) {
(0, ava_1.default)(`selects CLI from defaults.json on ${util_1.GitHubVariant[variant]}`, async (t) => {
await (0, util_1.withTmpDir)(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
t.deepEqual(await features.getDefaultCliVersion(variant), {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
variant,
});
});
});
}
(0, ava_1.default)("selects CLI v2.12.1 on Dotcom when feature flags enable v2.12.0 and v2.12.1", async (t) => {
await (0, util_1.withTmpDir)(async (tmpDir) => {
const featureEnablement = setUpFeatureFlagTests(tmpDir);
const expectedFeatureEnablement = initializeFeatures(true);
expectedFeatureEnablement["default_codeql_version_2_12_0_enabled"] = true;
expectedFeatureEnablement["default_codeql_version_2_12_1_enabled"] = true;
expectedFeatureEnablement["default_codeql_version_2_12_2_enabled"] = false;
expectedFeatureEnablement["default_codeql_version_2_12_3_enabled"] = false;
expectedFeatureEnablement["default_codeql_version_2_12_4_enabled"] = false;
expectedFeatureEnablement["default_codeql_version_2_12_5_enabled"] = false;
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
t.deepEqual(await featureEnablement.getDefaultCliVersion(util_1.GitHubVariant.DOTCOM), {
cliVersion: "2.12.1",
variant: util_1.GitHubVariant.DOTCOM,
});
});
});
(0, ava_1.default)(`selects CLI v2.11.6 on Dotcom when no default version feature flags are enabled`, async (t) => {
await (0, util_1.withTmpDir)(async (tmpDir) => {
const featureEnablement = setUpFeatureFlagTests(tmpDir);
const expectedFeatureEnablement = initializeFeatures(true);
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
t.deepEqual(await featureEnablement.getDefaultCliVersion(util_1.GitHubVariant.DOTCOM), {
cliVersion: "2.11.6",
variant: util_1.GitHubVariant.DOTCOM,
});
});
});
function assertAllFeaturesUndefinedInApi(t, loggedMessages) {
for (const feature of Object.keys(feature_flags_1.featureConfig)) {
t.assert(loggedMessages.find((v) => v.type === "debug" &&

File diff suppressed because one or more lines are too long

3
lib/testing-utils.js generated
View file

@ -181,6 +181,9 @@ exports.mockCodeQLVersion = mockCodeQLVersion;
*/
function createFeatures(enabledFeatures) {
return {
getDefaultCliVersion: async () => {
throw new Error("not implemented");
},
getValue: async (feature) => {
return enabledFeatures.includes(feature);
},

File diff suppressed because one or more lines are too long

24
lib/util.js generated
View file

@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMatrixInput = exports.shouldBypassToolcache = exports.isHostedRunner = exports.checkForTimeout = exports.withTimeout = exports.tryGetFolderBytes = exports.listFolder = exports.doesDirectoryExist = exports.logCodeScanningConfigInCli = exports.useCodeScanningConfigInCli = exports.isInTestMode = exports.checkActionVersion = exports.getMlPoweredJsQueriesStatus = exports.getMlPoweredJsQueriesPack = exports.ML_POWERED_JS_QUERIES_PACK_NAME = exports.isGoodVersion = exports.delay = exports.bundleDb = exports.codeQlVersionAbove = exports.getCachedCodeQlVersion = exports.cacheCodeQlVersion = exports.isHTTPError = exports.UserError = exports.HTTPError = exports.getRequiredEnvParam = exports.enrichEnvironment = exports.initializeEnvironment = exports.EnvVar = exports.assertNever = exports.apiVersionInRange = exports.DisallowedAPIVersionReason = exports.checkGitHubVersionInRange = exports.getGitHubVersion = exports.GitHubVariant = exports.parseGitHubUrl = exports.getCodeQLDatabasePath = exports.getThreadsFlag = exports.getThreadsFlagValue = exports.getAddSnippetsFlag = exports.getMemoryFlag = exports.getMemoryFlagValue = exports.withTmpDir = exports.getToolNames = exports.getExtraOptionsEnvParam = exports.DID_AUTOBUILD_GO_ENV_VAR_NAME = exports.DEFAULT_DEBUG_DATABASE_NAME = exports.DEFAULT_DEBUG_ARTIFACT_NAME = exports.GITHUB_DOTCOM_URL = void 0;
exports.getPinnedCodeqlVersion = exports.parseMatrixInput = exports.shouldBypassToolcache = exports.isHostedRunner = exports.checkForTimeout = exports.withTimeout = exports.tryGetFolderBytes = exports.listFolder = exports.doesDirectoryExist = exports.logCodeScanningConfigInCli = exports.useCodeScanningConfigInCli = exports.isInTestMode = exports.checkActionVersion = exports.getMlPoweredJsQueriesStatus = exports.getMlPoweredJsQueriesPack = exports.ML_POWERED_JS_QUERIES_PACK_NAME = exports.isGoodVersion = exports.delay = exports.bundleDb = exports.codeQlVersionAbove = exports.getCachedCodeQlVersion = exports.cacheCodeQlVersion = exports.isHTTPError = exports.UserError = exports.HTTPError = exports.getRequiredEnvParam = exports.enrichEnvironment = exports.initializeEnvironment = exports.EnvVar = exports.assertNever = exports.apiVersionInRange = exports.DisallowedAPIVersionReason = exports.checkGitHubVersionInRange = exports.getGitHubVersion = exports.GitHubVariant = exports.parseGitHubUrl = exports.getCodeQLDatabasePath = exports.getThreadsFlag = exports.getThreadsFlagValue = exports.getAddSnippetsFlag = exports.getMemoryFlag = exports.getMemoryFlagValue = exports.withTmpDir = exports.getToolNames = exports.getExtraOptionsEnvParam = exports.DID_AUTOBUILD_GO_ENV_VAR_NAME = exports.DEFAULT_DEBUG_DATABASE_NAME = exports.DEFAULT_DEBUG_ARTIFACT_NAME = exports.GITHUB_DOTCOM_URL = void 0;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
@ -36,6 +36,7 @@ const api_client_1 = require("./api-client");
const apiCompatibility = __importStar(require("./api-compatibility.json"));
const codeql_1 = require("./codeql");
const config_utils_1 = require("./config-utils");
const defaults = __importStar(require("./defaults.json")); // Referenced from codeql-action-sync-tool!
const feature_flags_1 = require("./feature-flags");
const languages_1 = require("./languages");
const shared_environment_1 = require("./shared-environment");
@ -757,4 +758,25 @@ function parseMatrixInput(matrixInput) {
return JSON.parse(matrixInput);
}
exports.parseMatrixInput = parseMatrixInput;
function computeToolcacheVersion(cliVersion, tagName) {
return `${cliVersion}-${tagName}`;
}
/**
* Gets version information about the CodeQL bundle which was current as of the release of this
* Action.
*
* This should be used in the following circumstances:
*
* - When running the Action on Enterprise instances.
* - When a user requests `tools: latest`.
* - When the Action is running on Dotcom and no default CodeQL version feature flags are enabled.
*/
function getPinnedCodeqlVersion() {
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
toolcacheVersion: computeToolcacheVersion(defaults.cliVersion, defaults.bundleVersion),
};
}
exports.getPinnedCodeqlVersion = getPinnedCodeqlVersion;
//# sourceMappingURL=util.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,9 @@
import * as fs from "fs";
import * as path from "path";
import test from "ava";
import test, { ExecutionContext } from "ava";
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
import {
Feature,
featureConfig,
@ -371,7 +372,61 @@ test("Environment variable can override feature flag cache", async (t) => {
});
});
function assertAllFeaturesUndefinedInApi(t, loggedMessages: LoggedMessage[]) {
for (const variant of [GitHubVariant.GHAE, GitHubVariant.GHES]) {
test(`selects CLI from defaults.json on ${GitHubVariant[variant]}`, async (t) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
t.deepEqual(await features.getDefaultCliVersion(variant), {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
variant,
});
});
});
}
test("selects CLI v2.12.1 on Dotcom when feature flags enable v2.12.0 and v2.12.1", async (t) => {
await withTmpDir(async (tmpDir) => {
const featureEnablement = setUpFeatureFlagTests(tmpDir);
const expectedFeatureEnablement = initializeFeatures(true);
expectedFeatureEnablement["default_codeql_version_2_12_0_enabled"] = true;
expectedFeatureEnablement["default_codeql_version_2_12_1_enabled"] = true;
expectedFeatureEnablement["default_codeql_version_2_12_2_enabled"] = false;
expectedFeatureEnablement["default_codeql_version_2_12_3_enabled"] = false;
expectedFeatureEnablement["default_codeql_version_2_12_4_enabled"] = false;
expectedFeatureEnablement["default_codeql_version_2_12_5_enabled"] = false;
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
t.deepEqual(
await featureEnablement.getDefaultCliVersion(GitHubVariant.DOTCOM),
{
cliVersion: "2.12.1",
variant: GitHubVariant.DOTCOM,
}
);
});
});
test(`selects CLI v2.11.6 on Dotcom when no default version feature flags are enabled`, async (t) => {
await withTmpDir(async (tmpDir) => {
const featureEnablement = setUpFeatureFlagTests(tmpDir);
const expectedFeatureEnablement = initializeFeatures(true);
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
t.deepEqual(
await featureEnablement.getDefaultCliVersion(GitHubVariant.DOTCOM),
{
cliVersion: "2.11.6",
variant: GitHubVariant.DOTCOM,
}
);
});
});
function assertAllFeaturesUndefinedInApi(
t: ExecutionContext<unknown>,
loggedMessages: LoggedMessage[]
) {
for (const feature of Object.keys(featureConfig)) {
t.assert(
loggedMessages.find(

View file

@ -3,11 +3,31 @@ import * as path from "path";
import { getApiClient } from "./api-client";
import { CodeQL } from "./codeql";
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
import { Logger } from "./logging";
import { RepositoryNwo } from "./repository";
import * as util from "./util";
const DEFAULT_VERSION_FEATURE_FLAG_PREFIX = "default_codeql_version_";
const DEFAULT_VERSION_FEATURE_FLAG_SUFFIX = "_enabled";
const MINIMUM_ENABLED_CODEQL_VERSION = "2.11.6";
export type CodeQLDefaultVersionInfo =
| {
cliVersion: string;
variant: util.GitHubVariant.DOTCOM;
}
| {
cliVersion: string;
tagName: string;
variant: util.GitHubVariant.GHAE | util.GitHubVariant.GHES;
};
export interface FeatureEnablement {
/** Gets the default version of the CodeQL tools. */
getDefaultCliVersion(
variant: util.GitHubVariant
): Promise<CodeQLDefaultVersionInfo>;
getValue(feature: Feature, codeql?: CodeQL): Promise<boolean>;
}
@ -91,6 +111,12 @@ export class Features implements FeatureEnablement {
);
}
async getDefaultCliVersion(
variant: util.GitHubVariant
): Promise<CodeQLDefaultVersionInfo> {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
/**
*
* @param feature The feature to check.
@ -153,6 +179,68 @@ class GitHubFeatureFlags implements FeatureEnablement {
/**/
}
private static getCliVersionFromFeatureFlag(f: string): string | undefined {
if (
!f.startsWith(DEFAULT_VERSION_FEATURE_FLAG_PREFIX) ||
!f.endsWith(DEFAULT_VERSION_FEATURE_FLAG_SUFFIX)
) {
return undefined;
}
return f
.substring(
DEFAULT_VERSION_FEATURE_FLAG_PREFIX.length,
f.length - DEFAULT_VERSION_FEATURE_FLAG_SUFFIX.length
)
.replace(/_/g, ".");
}
async getDefaultCliVersion(
variant: util.GitHubVariant
): Promise<CodeQLDefaultVersionInfo> {
if (variant === util.GitHubVariant.DOTCOM) {
return {
cliVersion: await this.getDefaultDotcomCliVersion(),
variant,
};
}
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
variant,
};
}
async getDefaultDotcomCliVersion(): Promise<string> {
const response = await this.getAllFeatures();
const enabledFeatureFlagCliVersions = Object.entries(response)
.map(([f, isEnabled]) =>
isEnabled
? GitHubFeatureFlags.getCliVersionFromFeatureFlag(f)
: undefined
)
.filter((f) => f !== undefined)
.map((f) => f as string);
if (enabledFeatureFlagCliVersions.length === 0) {
this.logger.debug(
"Feature flags do not specify a default CLI version. Falling back to CLI version " +
`${MINIMUM_ENABLED_CODEQL_VERSION}.`
);
return MINIMUM_ENABLED_CODEQL_VERSION;
}
const maxCliVersion = enabledFeatureFlagCliVersions.reduce(
(maxVersion, currentVersion) =>
currentVersion > maxVersion ? currentVersion : maxVersion,
enabledFeatureFlagCliVersions[0]
);
this.logger.debug(
`Derived default CLI version of ${maxCliVersion} from feature flags.`
);
return maxCliVersion;
}
async getValue(feature: Feature): Promise<boolean> {
const response = await this.getAllFeatures();
if (response === undefined) {

View file

@ -201,6 +201,9 @@ export function mockCodeQLVersion(version) {
*/
export function createFeatures(enabledFeatures: Feature[]): FeatureEnablement {
return {
getDefaultCliVersion: async () => {
throw new Error("not implemented");
},
getValue: async (feature) => {
return enabledFeatures.includes(feature);
},

View file

@ -19,6 +19,7 @@ import {
parsePacksSpecification,
prettyPrintPack,
} from "./config-utils";
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
import { Feature, FeatureEnablement } from "./feature-flags";
import { KOTLIN_SWIFT_BYPASS, Language } from "./languages";
import { Logger } from "./logging";
@ -901,3 +902,44 @@ export function parseMatrixInput(
}
return JSON.parse(matrixInput);
}
/** Version information about a CodeQL bundle. */
export interface CodeqlBundleVersionInfo {
/** Version number of the CLI contained within this bundle. */
cliVersion: string;
/** The name of the tag of the GitHub Release containing this bundle. */
tagName: string;
/**
* Version number of this bundle within the toolcache.
*
* For compatibility with previous runner images and toolcaches, consumers should fallback to
* looking up the `0.0.0-pre` version number within the toolcache if this version number is not
* found, where `pre` is the prerelease component of this version number.
*/
toolcacheVersion: string;
}
function computeToolcacheVersion(cliVersion: string, tagName: string): string {
return `${cliVersion}-${tagName}`;
}
/**
* Gets version information about the CodeQL bundle which was current as of the release of this
* Action.
*
* This should be used in the following circumstances:
*
* - When running the Action on Enterprise instances.
* - When a user requests `tools: latest`.
* - When the Action is running on Dotcom and no default CodeQL version feature flags are enabled.
*/
export function getPinnedCodeqlVersion(): CodeqlBundleVersionInfo {
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
toolcacheVersion: computeToolcacheVersion(
defaults.cliVersion,
defaults.bundleVersion
),
};
}