Add OfflineFeatures class

This commit is contained in:
Michael B. Gale
2026-02-12 17:42:40 +00:00
parent 5283c3ba5a
commit 368f322a09
8 changed files with 577 additions and 168 deletions
+101 -35
View File
@@ -354,33 +354,15 @@ type GitHubFeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;
export const FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json";
/**
* Determines the enablement status of a number of features.
* If feature enablement is not able to be determined locally, a request to the
* GitHub API is made to determine the enablement status.
* Determines the enablement status of a number of features locally without
* consulting the GitHub API.
*/
export class Features implements FeatureEnablement {
private gitHubFeatureFlags: GitHubFeatureFlags;
constructor(
gitHubVersion: util.GitHubVersion,
repositoryNwo: RepositoryNwo,
tempDir: string,
private readonly logger: Logger,
) {
this.gitHubFeatureFlags = new GitHubFeatureFlags(
gitHubVersion,
repositoryNwo,
path.join(tempDir, FEATURE_FLAGS_FILE_NAME),
logger,
);
}
export class OfflineFeatures implements FeatureEnablement {
constructor(protected readonly logger: Logger) {}
async getDefaultCliVersion(
variant: util.GitHubVariant,
_variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
if (supportsFeatureFlags(variant)) {
return await this.gitHubFeatureFlags.getDefaultCliVersionFromFlags();
}
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
@@ -388,6 +370,16 @@ export class Features implements FeatureEnablement {
}
/**
* Gets the `FeatureConfig` for `feature`.
*/
getFeatureConfig(feature: Feature): FeatureConfig {
// Narrow the type to FeatureConfig to avoid type errors. To avoid unsafe use of `as`, we
// check that the required properties exist using `satisfies`.
return featureConfig[feature] satisfies FeatureConfig as FeatureConfig;
}
/**
* Determines whether `feature` is enabled without consulting the GitHub API.
*
* @param feature The feature to check.
* @param codeql An optional CodeQL object. If provided, and a `minimumVersion` is specified for the
@@ -400,11 +392,22 @@ export class Features implements FeatureEnablement {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature: Feature, codeql?: CodeQL): Promise<boolean> {
// Narrow the type to FeatureConfig to avoid type errors. To avoid unsafe use of `as`, we
// check that the required properties exist using `satisfies`.
const config = featureConfig[
feature
] satisfies FeatureConfig as FeatureConfig;
const offlineValue = await this.getOfflineValue(feature, codeql);
if (offlineValue !== undefined) {
return offlineValue;
}
return this.getDefaultValue(feature);
}
/**
* Determines whether `feature` is enabled using the CLI and environment variables.
*/
protected async getOfflineValue(
feature: Feature,
codeql?: CodeQL,
): Promise<boolean | undefined> {
const config = this.getFeatureConfig(feature);
if (!codeql && config.minimumVersion) {
throw new Error(
@@ -470,6 +473,74 @@ export class Features implements FeatureEnablement {
return true;
}
return undefined;
}
/** Gets the default value of `feature`. */
protected async getDefaultValue(feature: Feature): Promise<boolean> {
const config = this.getFeatureConfig(feature);
const defaultValue = config.defaultValue;
this.logger.debug(
`Feature ${feature} is ${
defaultValue ? "enabled" : "disabled"
} due to its default value.`,
);
return defaultValue;
}
}
/**
* Determines the enablement status of a number of features.
* If feature enablement is not able to be determined locally, a request to the
* GitHub API is made to determine the enablement status.
*/
export class Features extends OfflineFeatures {
private gitHubFeatureFlags: GitHubFeatureFlags;
constructor(
gitHubVersion: util.GitHubVersion,
repositoryNwo: RepositoryNwo,
tempDir: string,
logger: Logger,
) {
super(logger);
this.gitHubFeatureFlags = new GitHubFeatureFlags(
gitHubVersion,
repositoryNwo,
path.join(tempDir, FEATURE_FLAGS_FILE_NAME),
logger,
);
}
async getDefaultCliVersion(
variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
if (supportsFeatureFlags(variant)) {
return await this.gitHubFeatureFlags.getDefaultCliVersionFromFlags();
}
return super.getDefaultCliVersion(variant);
}
/**
*
* @param feature The feature to check.
* @param codeql An optional CodeQL object. If provided, and a `minimumVersion` is specified for the
* feature, the version of the CodeQL CLI will be checked against the minimum version.
* If the version is less than the minimum version, the feature will be considered
* disabled. If not provided, and a `minimumVersion` is specified for the feature, the
* this function will throw.
* @returns true if the feature is enabled, false otherwise.
*
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature: Feature, codeql?: CodeQL): Promise<boolean> {
// Check whether the feature is enabled locally.
const offlineValue = await this.getOfflineValue(feature, codeql);
if (offlineValue !== undefined) {
return offlineValue;
}
// Ask the GitHub API if the feature is enabled.
const apiValue = await this.gitHubFeatureFlags.getValue(feature);
if (apiValue !== undefined) {
@@ -481,13 +552,8 @@ export class Features implements FeatureEnablement {
return apiValue;
}
const defaultValue = config.defaultValue;
this.logger.debug(
`Feature ${feature} is ${
defaultValue ? "enabled" : "disabled"
} due to its default value.`,
);
return defaultValue;
// Return the default value.
return this.getDefaultValue(feature);
}
}