diff --git a/lib/analyze-action.js b/lib/analyze-action.js index 6e0fd716cf..e74fd8e7ae 100644 --- a/lib/analyze-action.js +++ b/lib/analyze-action.js @@ -94707,24 +94707,25 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - if (!codeql && featureConfig[feature].minimumVersion) { + const config = featureConfig[feature]; + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - const envVar = (process.env[featureConfig[feature].envVar] || "").toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is disabled via the environment variable ${config.envVar}.` ); return false; } - const minimumVersion = featureConfig[feature].minimumVersion; + const minimumVersion = config.minimumVersion; if (codeql && minimumVersion) { if (!await codeQlVersionAtLeast(codeql, minimumVersion)) { this.logger.debug( @@ -94737,7 +94738,7 @@ var Features = class { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!await codeql.supportsFeature(toolsFeature)) { this.logger.debug( @@ -94752,7 +94753,7 @@ var Features = class { } if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is enabled via the environment variable ${config.envVar}.` ); return true; } @@ -94763,7 +94764,7 @@ var Features = class { ); return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` ); @@ -94900,7 +94901,9 @@ var GitHubFeatureFlags = class { return {}; } try { - const featuresToRequest = Object.entries(featureConfig).filter(([, config]) => !config.legacyApi).map(([f]) => f); + const featuresToRequest = Object.entries(featureConfig).filter( + ([, config]) => !config.legacyApi + ).map(([f]) => f); const FEATURES_PER_REQUEST = 25; const featureChunks = []; while (featuresToRequest.length > 0) { diff --git a/lib/autobuild-action.js b/lib/autobuild-action.js index 92df03c773..d9aed7e1f5 100644 --- a/lib/autobuild-action.js +++ b/lib/autobuild-action.js @@ -91058,24 +91058,25 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - if (!codeql && featureConfig[feature].minimumVersion) { + const config = featureConfig[feature]; + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - const envVar = (process.env[featureConfig[feature].envVar] || "").toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is disabled via the environment variable ${config.envVar}.` ); return false; } - const minimumVersion2 = featureConfig[feature].minimumVersion; + const minimumVersion2 = config.minimumVersion; if (codeql && minimumVersion2) { if (!await codeQlVersionAtLeast(codeql, minimumVersion2)) { this.logger.debug( @@ -91088,7 +91089,7 @@ var Features = class { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!await codeql.supportsFeature(toolsFeature)) { this.logger.debug( @@ -91103,7 +91104,7 @@ var Features = class { } if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is enabled via the environment variable ${config.envVar}.` ); return true; } @@ -91114,7 +91115,7 @@ var Features = class { ); return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` ); @@ -91251,7 +91252,9 @@ var GitHubFeatureFlags = class { return {}; } try { - const featuresToRequest = Object.entries(featureConfig).filter(([, config]) => !config.legacyApi).map(([f]) => f); + const featuresToRequest = Object.entries(featureConfig).filter( + ([, config]) => !config.legacyApi + ).map(([f]) => f); const FEATURES_PER_REQUEST = 25; const featureChunks = []; while (featuresToRequest.length > 0) { diff --git a/lib/init-action-post.js b/lib/init-action-post.js index c624b613d4..4652be55d2 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -130579,24 +130579,25 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - if (!codeql && featureConfig[feature].minimumVersion) { + const config = featureConfig[feature]; + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - const envVar = (process.env[featureConfig[feature].envVar] || "").toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is disabled via the environment variable ${config.envVar}.` ); return false; } - const minimumVersion2 = featureConfig[feature].minimumVersion; + const minimumVersion2 = config.minimumVersion; if (codeql && minimumVersion2) { if (!await codeQlVersionAtLeast(codeql, minimumVersion2)) { this.logger.debug( @@ -130609,7 +130610,7 @@ var Features = class { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!await codeql.supportsFeature(toolsFeature)) { this.logger.debug( @@ -130624,7 +130625,7 @@ var Features = class { } if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is enabled via the environment variable ${config.envVar}.` ); return true; } @@ -130635,7 +130636,7 @@ var Features = class { ); return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` ); @@ -130772,7 +130773,9 @@ var GitHubFeatureFlags = class { return {}; } try { - const featuresToRequest = Object.entries(featureConfig).filter(([, config]) => !config.legacyApi).map(([f]) => f); + const featuresToRequest = Object.entries(featureConfig).filter( + ([, config]) => !config.legacyApi + ).map(([f]) => f); const FEATURES_PER_REQUEST = 25; const featureChunks = []; while (featuresToRequest.length > 0) { diff --git a/lib/init-action.js b/lib/init-action.js index 98deff1fe0..50663e3ca9 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -92180,24 +92180,25 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - if (!codeql && featureConfig[feature].minimumVersion) { + const config = featureConfig[feature]; + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - const envVar = (process.env[featureConfig[feature].envVar] || "").toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is disabled via the environment variable ${config.envVar}.` ); return false; } - const minimumVersion2 = featureConfig[feature].minimumVersion; + const minimumVersion2 = config.minimumVersion; if (codeql && minimumVersion2) { if (!await codeQlVersionAtLeast(codeql, minimumVersion2)) { this.logger.debug( @@ -92210,7 +92211,7 @@ var Features = class { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!await codeql.supportsFeature(toolsFeature)) { this.logger.debug( @@ -92225,7 +92226,7 @@ var Features = class { } if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is enabled via the environment variable ${config.envVar}.` ); return true; } @@ -92236,7 +92237,7 @@ var Features = class { ); return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` ); @@ -92373,7 +92374,9 @@ var GitHubFeatureFlags = class { return {}; } try { - const featuresToRequest = Object.entries(featureConfig).filter(([, config]) => !config.legacyApi).map(([f]) => f); + const featuresToRequest = Object.entries(featureConfig).filter( + ([, config]) => !config.legacyApi + ).map(([f]) => f); const FEATURES_PER_REQUEST = 25; const featureChunks = []; while (featuresToRequest.length > 0) { diff --git a/lib/setup-codeql-action.js b/lib/setup-codeql-action.js index c097d4de82..825c716f26 100644 --- a/lib/setup-codeql-action.js +++ b/lib/setup-codeql-action.js @@ -90961,24 +90961,25 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - if (!codeql && featureConfig[feature].minimumVersion) { + const config = featureConfig[feature]; + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - const envVar = (process.env[featureConfig[feature].envVar] || "").toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is disabled via the environment variable ${config.envVar}.` ); return false; } - const minimumVersion2 = featureConfig[feature].minimumVersion; + const minimumVersion2 = config.minimumVersion; if (codeql && minimumVersion2) { if (!await codeQlVersionAtLeast(codeql, minimumVersion2)) { this.logger.debug( @@ -90991,7 +90992,7 @@ var Features = class { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!await codeql.supportsFeature(toolsFeature)) { this.logger.debug( @@ -91006,7 +91007,7 @@ var Features = class { } if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is enabled via the environment variable ${config.envVar}.` ); return true; } @@ -91017,7 +91018,7 @@ var Features = class { ); return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` ); @@ -91154,7 +91155,9 @@ var GitHubFeatureFlags = class { return {}; } try { - const featuresToRequest = Object.entries(featureConfig).filter(([, config]) => !config.legacyApi).map(([f]) => f); + const featuresToRequest = Object.entries(featureConfig).filter( + ([, config]) => !config.legacyApi + ).map(([f]) => f); const FEATURES_PER_REQUEST = 25; const featureChunks = []; while (featuresToRequest.length > 0) { diff --git a/lib/upload-sarif-action.js b/lib/upload-sarif-action.js index 4d30ee6eee..f19ce89bd3 100644 --- a/lib/upload-sarif-action.js +++ b/lib/upload-sarif-action.js @@ -93911,24 +93911,25 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - if (!codeql && featureConfig[feature].minimumVersion) { + const config = featureConfig[feature]; + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.` ); } - const envVar = (process.env[featureConfig[feature].envVar] || "").toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is disabled via the environment variable ${config.envVar}.` ); return false; } - const minimumVersion = featureConfig[feature].minimumVersion; + const minimumVersion = config.minimumVersion; if (codeql && minimumVersion) { if (!await codeQlVersionAtLeast(codeql, minimumVersion)) { this.logger.debug( @@ -93941,7 +93942,7 @@ var Features = class { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!await codeql.supportsFeature(toolsFeature)) { this.logger.debug( @@ -93956,7 +93957,7 @@ var Features = class { } if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.` + `Feature ${feature} is enabled via the environment variable ${config.envVar}.` ); return true; } @@ -93967,7 +93968,7 @@ var Features = class { ); return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` ); @@ -94104,7 +94105,9 @@ var GitHubFeatureFlags = class { return {}; } try { - const featuresToRequest = Object.entries(featureConfig).filter(([, config]) => !config.legacyApi).map(([f]) => f); + const featuresToRequest = Object.entries(featureConfig).filter( + ([, config]) => !config.legacyApi + ).map(([f]) => f); const FEATURES_PER_REQUEST = 25; const featureChunks = []; while (featuresToRequest.length > 0) { diff --git a/src/feature-flags.test.ts b/src/feature-flags.test.ts index 11e7ba538f..cdab85e279 100644 --- a/src/feature-flags.test.ts +++ b/src/feature-flags.test.ts @@ -10,6 +10,8 @@ import { FeatureEnablement, Features, FEATURE_FLAGS_FILE_NAME, + FeatureConfig, + FeatureWithoutCLI, } from "./feature-flags"; import { getRunnerLogger } from "./logging"; import { parseRepositoryNwo } from "./repository"; @@ -46,7 +48,7 @@ test(`All features are disabled if running against GHES`, async (t) => { for (const feature of Object.values(Feature)) { t.deepEqual( - await features.getValue(feature, includeCodeQlIfRequired(feature)), + await getFeatureIncludingCodeQlIfRequired(features, feature), featureConfig[feature].defaultValue, ); } @@ -75,9 +77,7 @@ test(`Feature flags are requested in GHEC-DR`, async (t) => { for (const feature of Object.values(Feature)) { // Ensure we have gotten a response value back from the Mock API - t.assert( - await features.getValue(feature, includeCodeQlIfRequired(feature)), - ); + t.assert(await getFeatureIncludingCodeQlIfRequired(features, feature)); } // And that we haven't bailed preemptively. @@ -104,7 +104,7 @@ test("API response missing and features use default value", async (t) => { for (const feature of Object.values(Feature)) { t.assert( - (await features.getValue(feature, includeCodeQlIfRequired(feature))) === + (await getFeatureIncludingCodeQlIfRequired(features, feature)) === featureConfig[feature].defaultValue, ); } @@ -124,7 +124,7 @@ test("Features use default value if they're not returned in API response", async for (const feature of Object.values(Feature)) { t.assert( - (await features.getValue(feature, includeCodeQlIfRequired(feature))) === + (await getFeatureIncludingCodeQlIfRequired(features, feature)) === featureConfig[feature].defaultValue, ); } @@ -151,7 +151,7 @@ test("Include no more than 25 features in each API request", async (t) => { // from the API. const feature = Object.values(Feature)[0]; await t.notThrowsAsync(async () => - features.getValue(feature, includeCodeQlIfRequired(feature)), + getFeatureIncludingCodeQlIfRequired(features, feature), ); }); }); @@ -165,8 +165,7 @@ test("Feature flags exception is propagated if the API request errors", async (t const someFeature = Object.values(Feature)[0]; await t.throwsAsync( - async () => - features.getValue(someFeature, includeCodeQlIfRequired(someFeature)), + async () => getFeatureIncludingCodeQlIfRequired(features, someFeature), { message: "Encountered an error while trying to determine feature enablement: Error: some error message", @@ -190,9 +189,9 @@ for (const feature of Object.keys(featureConfig)) { // retrieve the values of the actual features const actualFeatureEnablement: { [feature: string]: boolean } = {}; for (const f of Object.keys(featureConfig)) { - actualFeatureEnablement[f] = await features.getValue( + actualFeatureEnablement[f] = await getFeatureIncludingCodeQlIfRequired( + features, f as Feature, - includeCodeQlIfRequired(f), ); } @@ -210,19 +209,16 @@ for (const feature of Object.keys(featureConfig)) { // feature should be disabled initially t.assert( - !(await features.getValue( + !(await getFeatureIncludingCodeQlIfRequired( + features, feature as Feature, - includeCodeQlIfRequired(feature), )), ); // set env var to true and check that the feature is now enabled process.env[featureConfig[feature].envVar] = "true"; t.assert( - await features.getValue( - feature as Feature, - includeCodeQlIfRequired(feature), - ), + await getFeatureIncludingCodeQlIfRequired(features, feature as Feature), ); }); }); @@ -236,18 +232,15 @@ for (const feature of Object.keys(featureConfig)) { // feature should be enabled initially t.assert( - await features.getValue( - feature as Feature, - includeCodeQlIfRequired(feature), - ), + await getFeatureIncludingCodeQlIfRequired(features, feature as Feature), ); // set env var to false and check that the feature is now disabled process.env[featureConfig[feature].envVar] = "false"; t.assert( - !(await features.getValue( + !(await getFeatureIncludingCodeQlIfRequired( + features, feature as Feature, - includeCodeQlIfRequired(feature), )), ); }); @@ -264,13 +257,19 @@ for (const feature of Object.keys(featureConfig)) { const expectedFeatureEnablement = initializeFeatures(true); mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - await t.throwsAsync(async () => features.getValue(feature as Feature), { - message: `Internal error: A ${ - featureConfig[feature].minimumVersion !== undefined - ? "minimum version" - : "required tools feature" - } is specified for feature ${feature}, but no instance of CodeQL was provided.`, - }); + // The type system should prevent this happening, but test that if we + // bypass it we get the expected error. + await t.throwsAsync( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + async () => features.getValue(feature as any), + { + message: `Internal error: A ${ + featureConfig[feature].minimumVersion !== undefined + ? "minimum version" + : "required tools feature" + } is specified for feature ${feature}, but no instance of CodeQL was provided.`, + }, + ); }); }); } @@ -354,9 +353,9 @@ test("Feature flags are saved to disk", async (t) => { ); t.true( - await features.getValue( + await getFeatureIncludingCodeQlIfRequired( + features, Feature.QaTelemetryEnabled, - includeCodeQlIfRequired(Feature.QaTelemetryEnabled), ), "Feature flag should be enabled initially", ); @@ -382,9 +381,9 @@ test("Feature flags are saved to disk", async (t) => { (features as any).gitHubFeatureFlags.cachedApiResponse = undefined; t.false( - await features.getValue( + await getFeatureIncludingCodeQlIfRequired( + features, Feature.QaTelemetryEnabled, - includeCodeQlIfRequired(Feature.QaTelemetryEnabled), ), "Feature flag should be enabled after reading from cached file", ); @@ -399,9 +398,9 @@ test("Environment variable can override feature flag cache", async (t) => { const cachedFeatureFlags = path.join(tmpDir, FEATURE_FLAGS_FILE_NAME); t.true( - await features.getValue( + await getFeatureIncludingCodeQlIfRequired( + features, Feature.QaTelemetryEnabled, - includeCodeQlIfRequired(Feature.QaTelemetryEnabled), ), "Feature flag should be enabled initially", ); @@ -413,9 +412,9 @@ test("Environment variable can override feature flag cache", async (t) => { process.env.CODEQL_ACTION_QA_TELEMETRY = "false"; t.false( - await features.getValue( + await getFeatureIncludingCodeQlIfRequired( + features, Feature.QaTelemetryEnabled, - includeCodeQlIfRequired(Feature.QaTelemetryEnabled), ), "Feature flag should be disabled after setting env var", ); @@ -512,7 +511,7 @@ for (const variant of [GitHubVariant.DOTCOM, GitHubVariant.GHEC_DR]) { test("legacy feature flags should end with _enabled", async (t) => { for (const [feature, config] of Object.entries(featureConfig)) { - if (config.legacyApi) { + if ((config satisfies FeatureConfig as FeatureConfig).legacyApi) { t.assert( feature.endsWith("_enabled"), `legacy feature ${feature} should end with '_enabled'`, @@ -523,7 +522,7 @@ test("legacy feature flags should end with _enabled", async (t) => { test("non-legacy feature flags should not end with _enabled", async (t) => { for (const [feature, config] of Object.entries(featureConfig)) { - if (!config.legacyApi) { + if (!(config satisfies FeatureConfig as FeatureConfig).legacyApi) { t.false( feature.endsWith("_enabled"), `non-legacy feature ${feature} should not end with '_enabled'`, @@ -534,7 +533,7 @@ test("non-legacy feature flags should not end with _enabled", async (t) => { test("non-legacy feature flags should not start with codeql_action_", async (t) => { for (const [feature, config] of Object.entries(featureConfig)) { - if (!config.legacyApi) { + if (!(config satisfies FeatureConfig as FeatureConfig).legacyApi) { t.false( feature.startsWith("codeql_action_"), `non-legacy feature ${feature} should not start with 'codeql_action_'`, @@ -573,12 +572,25 @@ function setUpFeatureFlagTests( * Returns an argument to pass to `getValue` that if required includes a CodeQL object meeting the * minimum version or tool feature requirements specified by the feature. */ -function includeCodeQlIfRequired(feature: string) { - return featureConfig[feature].minimumVersion !== undefined || - featureConfig[feature].toolsFeature !== undefined - ? mockCodeQLVersion( - "9.9.9", - Object.fromEntries(Object.values(ToolsFeature).map((v) => [v, true])), - ) - : undefined; +function getFeatureIncludingCodeQlIfRequired( + features: FeatureEnablement, + feature: Feature, +) { + const config = featureConfig[ + feature + ] satisfies FeatureConfig as FeatureConfig; + if ( + config.minimumVersion === undefined && + config.toolsFeature === undefined + ) { + return features.getValue(feature as FeatureWithoutCLI); + } + + return features.getValue( + feature, + mockCodeQLVersion( + "9.9.9", + Object.fromEntries(Object.values(ToolsFeature).map((v) => [v, true])), + ), + ); } diff --git a/src/feature-flags.ts b/src/feature-flags.ts index 24e84f7c27..0695d62daa 100644 --- a/src/feature-flags.ts +++ b/src/feature-flags.ts @@ -26,16 +26,8 @@ export interface CodeQLDefaultVersionInfo { toolsFeatureFlagsValid?: boolean; } -export interface FeatureEnablement { - /** Gets the default version of the CodeQL tools. */ - getDefaultCliVersion( - variant: util.GitHubVariant, - ): Promise; - getValue(feature: Feature, codeql?: CodeQL): Promise; -} - /** - * Feature enablement as returned by the GitHub API endpoint. + * Features as named by the GitHub API endpoint. * * Do not include the `codeql_action_` prefix as this is stripped by the API * endpoint. @@ -82,37 +74,36 @@ export enum Feature { ValidateDbConfig = "validate_db_config", } -export const featureConfig: Record< - Feature, - { - /** - * Default value in environments where the feature flags API is not available, - * such as GitHub Enterprise Server. - */ - defaultValue: boolean; - /** - * Environment variable for explicitly enabling or disabling the feature. - * - * This overrides enablement status from the feature flags API. - */ - envVar: string; - /** - * Whether the feature flag is part of the legacy feature flags API (defaults to false). - * - * These feature flags are included by default in the API response and do not need to be - * explicitly requested. - */ - legacyApi?: boolean; - /** - * Minimum version of the CLI, if applicable. - * - * Prefer using `ToolsFeature`s for future flags. - */ - minimumVersion: string | undefined; - /** Required tools feature, if applicable. */ - toolsFeature?: ToolsFeature; - } -> = { +export type FeatureConfig = { + /** + * Default value in environments where the feature flags API is not available, + * such as GitHub Enterprise Server. + */ + defaultValue: boolean; + /** + * Environment variable for explicitly enabling or disabling the feature. + * + * This overrides enablement status from the feature flags API. + */ + envVar: string; + /** + * Whether the feature flag is part of the legacy feature flags API (defaults to false). + * + * These feature flags are included by default in the API response and do not need to be + * explicitly requested. + */ + legacyApi?: boolean; + /** + * Minimum version of the CLI, if applicable. + * + * Prefer using `ToolsFeature`s for future flags. + */ + minimumVersion: string | undefined; + /** Required tools feature, if applicable. */ + toolsFeature?: ToolsFeature; +}; + +export const featureConfig = { [Feature.AllowToolcacheInput]: { defaultValue: false, envVar: "CODEQL_ACTION_ALLOW_TOOLCACHE_INPUT", @@ -305,7 +296,29 @@ export const featureConfig: Record< envVar: "CODEQL_ACTION_VALIDATE_DB_CONFIG", minimumVersion: undefined, }, -}; +} satisfies Record; + +/** A feature whose enablement does not depend on the version of the CodeQL CLI. */ +export type FeatureWithoutCLI = { + [K in Feature]: (typeof featureConfig)[K] extends + | { + minimumVersion: string; + } + | { + toolsFeature: ToolsFeature; + } + ? never + : K; +}[keyof typeof featureConfig]; + +export interface FeatureEnablement { + /** Gets the default version of the CodeQL tools. */ + getDefaultCliVersion( + variant: util.GitHubVariant, + ): Promise; + getValue(feature: FeatureWithoutCLI): Promise; + getValue(feature: Feature, codeql: CodeQL): Promise; +} /** * A response from the GitHub API that contains feature flag enablement information for the CodeQL @@ -358,31 +371,35 @@ 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 { - if (!codeql && featureConfig[feature].minimumVersion) { + // 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; + + if (!codeql && config.minimumVersion) { throw new Error( `Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.`, ); } - if (!codeql && featureConfig[feature].toolsFeature) { + if (!codeql && config.toolsFeature) { throw new Error( `Internal error: A required tools feature is specified for feature ${feature}, but no instance of CodeQL was provided.`, ); } - const envVar = ( - process.env[featureConfig[feature].envVar] || "" - ).toLocaleLowerCase(); + const envVar = (process.env[config.envVar] || "").toLocaleLowerCase(); // Do not use this feature if user explicitly disables it via an environment variable. if (envVar === "false") { this.logger.debug( - `Feature ${feature} is disabled via the environment variable ${featureConfig[feature].envVar}.`, + `Feature ${feature} is disabled via the environment variable ${config.envVar}.`, ); return false; } // Never use this feature if the CLI version explicitly can't support it. - const minimumVersion = featureConfig[feature].minimumVersion; + const minimumVersion = config.minimumVersion; if (codeql && minimumVersion) { if (!(await util.codeQlVersionAtLeast(codeql, minimumVersion))) { this.logger.debug( @@ -399,7 +416,7 @@ export class Features implements FeatureEnablement { ); } } - const toolsFeature = featureConfig[feature].toolsFeature; + const toolsFeature = config.toolsFeature; if (codeql && toolsFeature) { if (!(await codeql.supportsFeature(toolsFeature))) { this.logger.debug( @@ -419,7 +436,7 @@ export class Features implements FeatureEnablement { // Use this feature if user explicitly enables it via an environment variable. if (envVar === "true") { this.logger.debug( - `Feature ${feature} is enabled via the environment variable ${featureConfig[feature].envVar}.`, + `Feature ${feature} is enabled via the environment variable ${config.envVar}.`, ); return true; } @@ -435,7 +452,7 @@ export class Features implements FeatureEnablement { return apiValue; } - const defaultValue = featureConfig[feature].defaultValue; + const defaultValue = config.defaultValue; this.logger.debug( `Feature ${feature} is ${ defaultValue ? "enabled" : "disabled" @@ -631,7 +648,10 @@ class GitHubFeatureFlags { } try { const featuresToRequest = Object.entries(featureConfig) - .filter(([, config]) => !config.legacyApi) + .filter( + ([, config]) => + !(config satisfies FeatureConfig as FeatureConfig).legacyApi, + ) .map(([f]) => f); const FEATURES_PER_REQUEST = 25; diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 897bafcc21..66a6c25fb7 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -316,7 +316,7 @@ export function createFeatures(enabledFeatures: Feature[]): FeatureEnablement { throw new Error("not implemented"); }, getValue: async (feature) => { - return enabledFeatures.includes(feature); + return enabledFeatures.includes(feature as Feature); }, }; }