diff --git a/lib/analyze-action.js b/lib/analyze-action.js index 7e4e89eeab..29e37a23ba 100644 --- a/lib/analyze-action.js +++ b/lib/analyze-action.js @@ -107793,21 +107793,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path5.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -107820,7 +107823,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -107870,6 +107883,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path5.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -107877,11 +107936,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -107912,15 +107967,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -108068,6 +108114,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/diff-informed-analysis-utils.ts async function getDiffInformedAnalysisBranches(codeql, features, logger) { @@ -110013,7 +110066,7 @@ async function setupCppAutobuild(codeql, logger) { const featureName = "C++ automatic installation of dependencies"; const gitHubVersion = await getGitHubVersion(); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), @@ -112912,7 +112965,7 @@ async function run(startedAt2) { const repositoryNwo = getRepositoryNwo(); const gitHubVersion = await getGitHubVersion(); checkActionVersion(getActionVersion(), gitHubVersion); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/lib/autobuild-action.js b/lib/autobuild-action.js index e495cffbee..3d947e3a2e 100644 --- a/lib/autobuild-action.js +++ b/lib/autobuild-action.js @@ -103130,6 +103130,10 @@ function getWorkflowRunAttempt() { function isSelfHostedRunner() { return process.env.RUNNER_ENVIRONMENT === "self-hosted"; } +var CCR_KEY_PREFIX = "dynamic/copilot-pull-request-reviewer"; +function isCCR() { + return process.env["CODEQL_ACTION_ANALYSIS_KEY" /* ANALYSIS_KEY */]?.startsWith(CCR_KEY_PREFIX) || false; +} function prettyPrintInvocation(cmd, args) { return [cmd, ...args].map((x) => x.includes(" ") ? `'${x}'` : x).join(" "); } @@ -104126,21 +104130,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path3.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -104153,7 +104160,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -104203,6 +104220,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path3.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -104210,11 +104273,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -104245,15 +104304,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -104401,6 +104451,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/trap-caching.ts var actionsCache2 = __toESM(require_cache5()); @@ -105152,7 +105209,7 @@ async function setupCppAutobuild(codeql, logger) { const featureName = "C++ automatic installation of dependencies"; const gitHubVersion = await getGitHubVersion(); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/lib/init-action-post.js b/lib/init-action-post.js index 1f1869f412..690a2718fb 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -165119,21 +165119,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path4.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -165146,7 +165149,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -165196,6 +165209,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path4.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -165203,11 +165262,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -165238,15 +165293,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -165394,6 +165440,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/diff-informed-analysis-utils.ts function getDiffRangesJsonFilePath() { @@ -169890,7 +169943,7 @@ async function run2(startedAt) { const gitHubVersion = await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/lib/init-action.js b/lib/init-action.js index 34c83bc4ae..8a38c6c541 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -105326,21 +105326,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path6.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -105353,7 +105356,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -105403,6 +105416,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path6.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -105410,11 +105469,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -105445,15 +105500,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -105601,6 +105647,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/diff-informed-analysis-utils.ts async function shouldPerformDiffInformedAnalysis(codeql, features, logger) { @@ -109093,7 +109146,7 @@ async function run(startedAt) { checkGitHubVersionInRange(gitHubVersion, logger); checkActionVersion(getActionVersion(), gitHubVersion); const repositoryNwo = getRepositoryNwo(); - features = new Features( + features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/lib/setup-codeql-action.js b/lib/setup-codeql-action.js index 445f3b5768..b4842f0e88 100644 --- a/lib/setup-codeql-action.js +++ b/lib/setup-codeql-action.js @@ -103247,6 +103247,10 @@ function isSelfHostedRunner() { function isDynamicWorkflow() { return getWorkflowEventName() === "dynamic"; } +var CCR_KEY_PREFIX = "dynamic/copilot-pull-request-reviewer"; +function isCCR() { + return process.env["CODEQL_ACTION_ANALYSIS_KEY" /* ANALYSIS_KEY */]?.startsWith(CCR_KEY_PREFIX) || false; +} function prettyPrintInvocation(cmd, args) { return [cmd, ...args].map((x) => x.includes(" ") ? `'${x}'` : x).join(" "); } @@ -104027,21 +104031,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path4.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -104054,7 +104061,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -104104,6 +104121,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path4.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -104111,11 +104174,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -104146,15 +104205,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -104302,6 +104352,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/init.ts var toolrunner4 = __toESM(require_toolrunner()); @@ -106396,7 +106453,7 @@ async function run(startedAt) { checkGitHubVersionInRange(gitHubVersion, logger); checkActionVersion(getActionVersion(), gitHubVersion); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/lib/start-proxy-action.js b/lib/start-proxy-action.js index 1aa9d0f634..1e0559af89 100644 --- a/lib/start-proxy-action.js +++ b/lib/start-proxy-action.js @@ -120267,6 +120267,10 @@ function getWorkflowRunAttempt() { function isSelfHostedRunner() { return process.env.RUNNER_ENVIRONMENT === "self-hosted"; } +var CCR_KEY_PREFIX = "dynamic/copilot-pull-request-reviewer"; +function isCCR() { + return process.env["CODEQL_ACTION_ANALYSIS_KEY" /* ANALYSIS_KEY */]?.startsWith(CCR_KEY_PREFIX) || false; +} var persistedInputsKey = "persisted_inputs"; var persistInputs = function() { const inputEnvironmentVariables = Object.entries(process.env).filter( @@ -120820,21 +120824,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -120847,7 +120854,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -120897,6 +120914,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -120904,11 +120967,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -120939,15 +120998,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -121095,6 +121145,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/start-proxy.ts var path2 = __toESM(require("path")); @@ -121863,7 +121920,7 @@ async function run(startedAt) { core11.saveState("proxy-log-file", proxyLogFilePath); const repositoryNwo = getRepositoryNwo(); const gitHubVersion = await getGitHubVersion(); - features = new Features( + features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/lib/upload-sarif-action.js b/lib/upload-sarif-action.js index e326893e67..59bb9ad3e4 100644 --- a/lib/upload-sarif-action.js +++ b/lib/upload-sarif-action.js @@ -106981,21 +106981,24 @@ var featureConfig = { } }; var FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json"; -var Features = class { - constructor(gitHubVersion, repositoryNwo, tempDir, logger) { +var OfflineFeatures = class { + constructor(logger) { this.logger = logger; - this.gitHubFeatureFlags = new GitHubFeatureFlags( - gitHubVersion, - repositoryNwo, - path4.join(tempDir, FEATURE_FLAGS_FILE_NAME), - logger - ); } - gitHubFeatureFlags; - async getDefaultCliVersion(variant) { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + async getDefaultCliVersion(_variant) { + return { + cliVersion, + tagName: bundleVersion + }; } /** + * Gets the `FeatureConfig` for `feature`. + */ + getFeatureConfig(feature) { + return featureConfig[feature]; + } + /** + * 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 @@ -107008,7 +107011,17 @@ var Features = class { * @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided. */ async getValue(feature, codeql) { - const config = featureConfig[feature]; + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } + return this.getDefaultValue(feature); + } + /** + * Determines whether `feature` is enabled using the CLI and environment variables. + */ + async getOfflineValue(feature, codeql) { + const config = this.getFeatureConfig(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.` @@ -107058,6 +107071,52 @@ var Features = class { ); return true; } + return void 0; + } + /** Gets the default value of `feature`. */ + async getDefaultValue(feature) { + 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; + } +}; +var Features = class extends OfflineFeatures { + gitHubFeatureFlags; + constructor(gitHubVersion, repositoryNwo, tempDir, logger) { + super(logger); + this.gitHubFeatureFlags = new GitHubFeatureFlags( + gitHubVersion, + repositoryNwo, + path4.join(tempDir, FEATURE_FLAGS_FILE_NAME), + logger + ); + } + async getDefaultCliVersion(variant) { + 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, codeql) { + const offlineValue = await this.getOfflineValue(feature, codeql); + if (offlineValue !== void 0) { + return offlineValue; + } const apiValue = await this.gitHubFeatureFlags.getValue(feature); if (apiValue !== void 0) { this.logger.debug( @@ -107065,11 +107124,7 @@ var Features = class { ); return apiValue; } - const defaultValue = config.defaultValue; - this.logger.debug( - `Feature ${feature} is ${defaultValue ? "enabled" : "disabled"} due to its default value.` - ); - return defaultValue; + return this.getDefaultValue(feature); } }; var GitHubFeatureFlags = class { @@ -107100,15 +107155,6 @@ var GitHubFeatureFlags = class { } return version; } - async getDefaultCliVersion(variant) { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion, - tagName: bundleVersion - }; - } async getDefaultCliVersionFromFlags() { const response = await this.getAllFeatures(); const enabledFeatureFlagCliVersions = Object.entries(response).map( @@ -107256,6 +107302,13 @@ var GitHubFeatureFlags = class { function supportsFeatureFlags(githubVariant) { return githubVariant === "GitHub.com" /* DOTCOM */ || githubVariant === "GitHub Enterprise Cloud with data residency" /* GHEC_DR */; } +function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} // src/status-report.ts var os = __toESM(require("os")); @@ -111226,7 +111279,7 @@ async function run(startedAt) { checkActionVersion(getActionVersion(), gitHubVersion); persistInputs(); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/src/actions-util.test.ts b/src/actions-util.test.ts index 71a409da6d..ba5591a41c 100644 --- a/src/actions-util.test.ts +++ b/src/actions-util.test.ts @@ -12,7 +12,7 @@ import { import { computeAutomationID } from "./api-client"; import { EnvVar } from "./environment"; import { getRunnerLogger } from "./logging"; -import { setupTests } from "./testing-utils"; +import { mockCCR, setupTests } from "./testing-utils"; import { initializeEnvironment } from "./util"; setupTests(test); @@ -258,8 +258,8 @@ test("isDynamicWorkflow() returns true if event name is `dynamic`", (t) => { }); test("isCCR() returns true when expected", (t) => { - process.env.GITHUB_EVENT_NAME = "dynamic"; - process.env[EnvVar.ANALYSIS_KEY] = "dynamic/copilot-pull-request-reviewer"; + mockCCR(); + t.assert(isCCR()); t.false(isDefaultSetup()); }); diff --git a/src/analyze-action.ts b/src/analyze-action.ts index 3cc1ad019a..4093e37869 100644 --- a/src/analyze-action.ts +++ b/src/analyze-action.ts @@ -30,7 +30,7 @@ import { } from "./dependency-caching"; import { getDiffInformedAnalysisBranches } from "./diff-informed-analysis-utils"; import { EnvVar } from "./environment"; -import { Features } from "./feature-flags"; +import { initFeatures } from "./feature-flags"; import { KnownLanguage } from "./languages"; import { getActionsLogger, Logger } from "./logging"; import { cleanupAndUploadOverlayBaseDatabaseToCache } from "./overlay-database-utils"; @@ -293,7 +293,7 @@ async function run(startedAt: Date) { util.checkActionVersion(actionsUtil.getActionVersion(), gitHubVersion); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, actionsUtil.getTemporaryDirectory(), diff --git a/src/autobuild.ts b/src/autobuild.ts index ce3d45cc4e..46935bba67 100644 --- a/src/autobuild.ts +++ b/src/autobuild.ts @@ -6,7 +6,7 @@ import { CodeQL, getCodeQL } from "./codeql"; import * as configUtils from "./config-utils"; import { DocUrl } from "./doc-url"; import { EnvVar } from "./environment"; -import { Feature, featureConfig, Features } from "./feature-flags"; +import { Feature, featureConfig, initFeatures } from "./feature-flags"; import { KnownLanguage, Language } from "./languages"; import { Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; @@ -117,7 +117,7 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) { const featureName = "C++ automatic installation of dependencies"; const gitHubVersion = await getGitHubVersion(); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/src/diff-informed-analysis-utils.test.ts b/src/diff-informed-analysis-utils.test.ts index eeb06cd1b6..2d98a5f639 100644 --- a/src/diff-informed-analysis-utils.test.ts +++ b/src/diff-informed-analysis-utils.test.ts @@ -8,7 +8,7 @@ import { shouldPerformDiffInformedAnalysis, exportedForTesting, } from "./diff-informed-analysis-utils"; -import { Feature, Features } from "./feature-flags"; +import { Feature, initFeatures } from "./feature-flags"; import { getRunnerLogger } from "./logging"; import { parseRepositoryNwo } from "./repository"; import { @@ -63,7 +63,7 @@ const testShouldPerformDiffInformedAnalysis = test.macro({ delete process.env.CODEQL_ACTION_DIFF_INFORMED_QUERIES; } - const features = new Features( + const features = initFeatures( testCase.gitHubVersion, parseRepositoryNwo("github/example"), tmpDir, diff --git a/src/feature-flags.test.ts b/src/feature-flags.test.ts index cdab85e279..da4b097195 100644 --- a/src/feature-flags.test.ts +++ b/src/feature-flags.test.ts @@ -1,32 +1,30 @@ import * as fs from "fs"; import * as path from "path"; -import test, { ExecutionContext } from "ava"; +import test from "ava"; import * as defaults from "./defaults.json"; import { Feature, featureConfig, - FeatureEnablement, - Features, FEATURE_FLAGS_FILE_NAME, FeatureConfig, - FeatureWithoutCLI, } from "./feature-flags"; -import { getRunnerLogger } from "./logging"; -import { parseRepositoryNwo } from "./repository"; +import { + setUpFeatureFlagTests, + getFeatureIncludingCodeQlIfRequired, + assertAllFeaturesUndefinedInApi, +} from "./feature-flags/testing-util"; import { getRecordingLogger, initializeFeatures, LoggedMessage, + mockCCR, mockCodeQLVersion, mockFeatureFlagApiEndpoint, - setupActionsVars, setupTests, stubFeatureFlagApiEndpoint, } from "./testing-utils"; -import { ToolsFeature } from "./tools-features"; -import * as util from "./util"; import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util"; setupTests(test); @@ -35,8 +33,6 @@ test.beforeEach(() => { initializeEnvironment("1.2.3"); }); -const testRepositoryNwo = parseRepositoryNwo("github/example"); - test(`All features are disabled if running against GHES`, async (t) => { await withTmpDir(async (tmpDir) => { const loggedMessages = []; @@ -542,55 +538,18 @@ test("non-legacy feature flags should not start with codeql_action_", async (t) } }); -function assertAllFeaturesUndefinedInApi( - t: ExecutionContext, - loggedMessages: LoggedMessage[], -) { - for (const feature of Object.keys(featureConfig)) { - t.assert( - loggedMessages.find( - (v) => - v.type === "debug" && - (v.message as string).includes(feature) && - (v.message as string).includes("undefined in API response"), - ) !== undefined, - ); - } -} - -function setUpFeatureFlagTests( - tmpDir: string, - logger = getRunnerLogger(true), - gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion, -): FeatureEnablement { - setupActionsVars(tmpDir, tmpDir); - - return new Features(gitHubVersion, testRepositoryNwo, tmpDir, logger); -} +test("initFeatures returns a `Features` instance by default", async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + t.is("Features", features.constructor.name); + }); +}); -/** - * 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 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); - } +test("initFeatures returns an `OfflineFeatures` instance in CCR", async (t) => { + await withTmpDir(async (tmpDir) => { + mockCCR(); - return features.getValue( - feature, - mockCodeQLVersion( - "9.9.9", - Object.fromEntries(Object.values(ToolsFeature).map((v) => [v, true])), - ), - ); -} + const features = setUpFeatureFlagTests(tmpDir); + t.is("OfflineFeatures", features.constructor.name); + }); +}); diff --git a/src/feature-flags.ts b/src/feature-flags.ts index ea8cc1767b..793fb0cbbf 100644 --- a/src/feature-flags.ts +++ b/src/feature-flags.ts @@ -3,6 +3,7 @@ import * as path from "path"; import * as semver from "semver"; +import { isCCR } from "./actions-util"; import { getApiClient } from "./api-client"; import type { CodeQL } from "./codeql"; import * as defaults from "./defaults.json"; @@ -354,34 +355,32 @@ type GitHubFeatureFlagsApiResponse = Partial>; 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, - ); - } +class OfflineFeatures implements FeatureEnablement { + constructor(protected readonly logger: Logger) {} async getDefaultCliVersion( - variant: util.GitHubVariant, + _variant: util.GitHubVariant, ): Promise { - return await this.gitHubFeatureFlags.getDefaultCliVersion(variant); + return { + cliVersion: defaults.cliVersion, + tagName: defaults.bundleVersion, + }; + } + + /** + * 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 @@ -394,11 +393,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 { - // 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 { + const config = this.getFeatureConfig(feature); if (!codeql && config.minimumVersion) { throw new Error( @@ -464,6 +474,74 @@ export class Features implements FeatureEnablement { return true; } + return undefined; + } + + /** Gets the default value of `feature`. */ + protected async getDefaultValue(feature: Feature): Promise { + 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. + */ +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 { + 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 { + // 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) { @@ -475,13 +553,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); } } @@ -524,18 +597,6 @@ class GitHubFeatureFlags { return version; } - async getDefaultCliVersion( - variant: util.GitHubVariant, - ): Promise { - if (supportsFeatureFlags(variant)) { - return await this.getDefaultCliVersionFromFlags(); - } - return { - cliVersion: defaults.cliVersion, - tagName: defaults.bundleVersion, - }; - } - async getDefaultCliVersionFromFlags(): Promise { const response = await this.getAllFeatures(); @@ -738,3 +799,20 @@ function supportsFeatureFlags(githubVariant: util.GitHubVariant): boolean { githubVariant === util.GitHubVariant.GHEC_DR ); } + +/** + * Initialises an instance of a `FeatureEnablement` implementation. The implementation used + * is determined by the environment we are running in. + */ +export function initFeatures( + gitHubVersion: util.GitHubVersion, + repositoryNwo: RepositoryNwo, + tempDir: string, + logger: Logger, +): FeatureEnablement { + if (isCCR()) { + return new OfflineFeatures(logger); + } else { + return new Features(gitHubVersion, repositoryNwo, tempDir, logger); + } +} diff --git a/src/feature-flags/offline-features.test.ts b/src/feature-flags/offline-features.test.ts new file mode 100644 index 0000000000..fac7686faf --- /dev/null +++ b/src/feature-flags/offline-features.test.ts @@ -0,0 +1,37 @@ +import test from "ava"; +import * as sinon from "sinon"; + +import * as apiClient from "../api-client"; +import { Feature, featureConfig } from "../feature-flags"; +import { mockCCR, setupTests } from "../testing-utils"; +import { initializeEnvironment, withTmpDir } from "../util"; + +import { + getFeatureIncludingCodeQlIfRequired, + setUpFeatureFlagTests, +} from "./testing-util"; + +setupTests(test); + +test.beforeEach(() => { + initializeEnvironment("1.2.3"); + mockCCR(); +}); + +test("OfflineFeatures makes no API requests", async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + t.is("OfflineFeatures", features.constructor.name); + + sinon + .stub(apiClient, "getApiClient") + .throws(new Error("Should not have called getApiClient")); + + for (const feature of Object.values(Feature)) { + t.deepEqual( + await getFeatureIncludingCodeQlIfRequired(features, feature), + featureConfig[feature].defaultValue, + ); + } + }); +}); diff --git a/src/feature-flags/testing-util.ts b/src/feature-flags/testing-util.ts new file mode 100644 index 0000000000..91596c416e --- /dev/null +++ b/src/feature-flags/testing-util.ts @@ -0,0 +1,75 @@ +import { type ExecutionContext } from "ava"; + +import { + Feature, + featureConfig, + FeatureConfig, + FeatureEnablement, + FeatureWithoutCLI, + initFeatures, +} from "../feature-flags"; +import { getRunnerLogger } from "../logging"; +import { parseRepositoryNwo } from "../repository"; +import { + LoggedMessage, + mockCodeQLVersion, + setupActionsVars, +} from "../testing-utils"; +import { ToolsFeature } from "../tools-features"; +import { GitHubVariant } from "../util"; +import * as util from "../util"; + +const testRepositoryNwo = parseRepositoryNwo("github/example"); + +export function assertAllFeaturesUndefinedInApi( + t: ExecutionContext, + loggedMessages: LoggedMessage[], +) { + for (const feature of Object.keys(featureConfig)) { + t.assert( + loggedMessages.find( + (v) => + v.type === "debug" && + (v.message as string).includes(feature) && + (v.message as string).includes("undefined in API response"), + ) !== undefined, + ); + } +} + +export function setUpFeatureFlagTests( + tmpDir: string, + logger = getRunnerLogger(true), + gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion, +): FeatureEnablement { + setupActionsVars(tmpDir, tmpDir); + + return initFeatures(gitHubVersion, testRepositoryNwo, tmpDir, logger); +} + +/** + * 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. + */ +export 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/init-action-post.ts b/src/init-action-post.ts index cfae096938..d5aab32f33 100644 --- a/src/init-action-post.ts +++ b/src/init-action-post.ts @@ -21,7 +21,7 @@ import { getDependencyCacheUsage, } from "./dependency-caching"; import { EnvVar } from "./environment"; -import { Features } from "./feature-flags"; +import { initFeatures } from "./feature-flags"; import * as gitUtils from "./git-utils"; import * as initActionPostHelper from "./init-action-post-helper"; import { getActionsLogger } from "./logging"; @@ -62,7 +62,7 @@ async function run(startedAt: Date) { checkGitHubVersionInRange(gitHubVersion, logger); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/src/init-action.ts b/src/init-action.ts index 5d459acaec..353485fc13 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -38,7 +38,7 @@ import { makeTelemetryDiagnostic, } from "./diagnostics"; import { EnvVar } from "./environment"; -import { Feature, FeatureEnablement, Features } from "./feature-flags"; +import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; import { loadPropertiesFromApi, RepositoryProperties, @@ -210,7 +210,7 @@ async function run(startedAt: Date) { let config: configUtils.Config | undefined; let configFile: string | undefined; let codeql: CodeQL; - let features: Features; + let features: FeatureEnablement; let sourceRoot: string; let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined; let toolsFeatureFlagsValid: boolean | undefined; @@ -237,7 +237,7 @@ async function run(startedAt: Date) { const repositoryNwo = getRepositoryNwo(); - features = new Features( + features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 31c8986679..bd504f3fd3 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -10,7 +10,7 @@ import { import { getGitHubVersion } from "./api-client"; import { CodeQL } from "./codeql"; import { EnvVar } from "./environment"; -import { Features } from "./feature-flags"; +import { initFeatures } from "./feature-flags"; import { initCodeQL } from "./init"; import { getActionsLogger, Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; @@ -114,7 +114,7 @@ async function run(startedAt: Date): Promise { const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(), diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index a9b355eaa5..4ca4e9ea51 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -6,7 +6,7 @@ import { pki } from "node-forge"; import * as actionsUtil from "./actions-util"; import { getGitHubVersion } from "./api-client"; -import { Feature, Features } from "./feature-flags"; +import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; import { KnownLanguage } from "./languages"; import { getActionsLogger, Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; @@ -99,7 +99,7 @@ async function run(startedAt: Date) { // possible, and only use safe functions outside. const logger = getActionsLogger(); - let features: Features | undefined; + let features: FeatureEnablement | undefined; let language: KnownLanguage | undefined; try { @@ -114,7 +114,7 @@ async function run(startedAt: Date) { // Initialise FFs. const repositoryNwo = getRepositoryNwo(); const gitHubVersion = await getGitHubVersion(); - features = new Features( + features = initFeatures( gitHubVersion, repositoryNwo, actionsUtil.getTemporaryDirectory(), diff --git a/src/testing-utils.ts b/src/testing-utils.ts index aff7780436..1f78e5acbf 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -14,6 +14,7 @@ import { CachingKind } from "./caching-utils"; import * as codeql from "./codeql"; import { Config } from "./config-utils"; import * as defaults from "./defaults.json"; +import { EnvVar } from "./environment"; import { CodeQLDefaultVersionInfo, Feature, @@ -436,3 +437,9 @@ export function makeTestToken(length: number = 36) { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return chars.repeat(Math.ceil(length / chars.length)).slice(0, length); } + +/** Sets the environment variables needed for isCCR() to be `true`. */ +export function mockCCR() { + process.env.GITHUB_EVENT_NAME = "dynamic"; + process.env[EnvVar.ANALYSIS_KEY] = "dynamic/copilot-pull-request-reviewer"; +} diff --git a/src/upload-sarif-action.ts b/src/upload-sarif-action.ts index 5273909bad..cec41b2766 100644 --- a/src/upload-sarif-action.ts +++ b/src/upload-sarif-action.ts @@ -4,7 +4,7 @@ import * as actionsUtil from "./actions-util"; import { getActionVersion, getTemporaryDirectory } from "./actions-util"; import * as analyses from "./analyses"; import { getGitHubVersion } from "./api-client"; -import { Features } from "./feature-flags"; +import { initFeatures } from "./feature-flags"; import { Logger, getActionsLogger } from "./logging"; import { getRepositoryNwo } from "./repository"; import { @@ -70,7 +70,7 @@ async function run(startedAt: Date) { actionsUtil.persistInputs(); const repositoryNwo = getRepositoryNwo(); - const features = new Features( + const features = initFeatures( gitHubVersion, repositoryNwo, getTemporaryDirectory(),