Extract GitHubFeatureFlags to a separate class

Internal refactoring so that `GitHubFeatureFlags` is
private only. The public facing class is `Features`.
This commit is contained in:
Andrew Eisenberg 2022-10-06 14:42:57 -07:00
parent 5915e70486
commit b27aed78f5
15 changed files with 149 additions and 110 deletions

View file

@ -19,7 +19,7 @@ import { runAutobuild } from "./autobuild";
import { getCodeQL } from "./codeql";
import { Config, getConfig } from "./config-utils";
import { uploadDatabases } from "./database-upload";
import { FeatureFlags, GitHubFeatureFlags } from "./feature-flags";
import { FeatureFlags, Features } from "./feature-flags";
import { Language } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { parseRepositoryNwo } from "./repository";
@ -228,7 +228,7 @@ async function run() {
const gitHubVersion = await getGitHubVersionActionsOnly();
const featureFlags = new GitHubFeatureFlags(
const featureFlags = new Features(
gitHubVersion,
apiDetails,
repositoryNwo,

View file

@ -11,7 +11,7 @@ import {
import { getApiDetails, getGitHubVersionActionsOnly } from "./api-client";
import { determineAutobuildLanguages, runAutobuild } from "./autobuild";
import * as configUtils from "./config-utils";
import { GitHubFeatureFlags } from "./feature-flags";
import { Features } from "./feature-flags";
import { Language } from "./languages";
import { getActionsLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
@ -76,7 +76,7 @@ async function run() {
const gitHubVersion = await getGitHubVersionActionsOnly();
checkGitHubVersionInRange(gitHubVersion, logger, Mode.actions);
const featureFlags = new GitHubFeatureFlags(
const featureFlags = new Features(
gitHubVersion,
getApiDetails(),
parseRepositoryNwo(getRequiredEnvParam("GITHUB_REPOSITORY")),

View file

@ -5,7 +5,7 @@ import {
Feature,
featureConfig,
FeatureFlags,
GitHubFeatureFlags,
Features,
} from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
@ -217,7 +217,7 @@ for (const featureFlag of Object.keys(featureConfig)) {
await t.throwsAsync(
async () => featureFlags.getValue(featureFlag as Feature),
{
message: `A minimum version is specified for feature flag ${featureFlag}, but no instance of CodeQL was provided.`,
message: `Internal error: A minimum version is specified for feature flag ${featureFlag}, but no instance of CodeQL was provided.`,
}
);
});
@ -288,12 +288,7 @@ function setUpTests(
): FeatureFlags {
setupActionsVars(tmpDir, tmpDir);
return new GitHubFeatureFlags(
gitHubVersion,
testApiDetails,
testRepositoryNwo,
logger
);
return new Features(gitHubVersion, testApiDetails, testRepositoryNwo, logger);
}
function includeCodeQlIfRequired(featureFlag: string) {

View file

@ -50,15 +50,27 @@ export const featureConfig: Record<
*/
type FeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;
export class GitHubFeatureFlags implements FeatureFlags {
private cachedApiResponse: FeatureFlagsApiResponse | undefined;
/**
* Determines the enablement status of a number of features.
* If feature enablement is not able to be determined locally, a request to the
* github API is made to determine the enablement status.
*/
export class Features implements FeatureFlags {
private gitHubFeatureFlags: GitHubFeatureFlags;
constructor(
private gitHubVersion: util.GitHubVersion,
private apiDetails: GitHubApiDetails,
private repositoryNwo: RepositoryNwo,
private logger: Logger
) {}
gitHubVersion: util.GitHubVersion,
apiDetails: GitHubApiDetails,
repositoryNwo: RepositoryNwo,
logger: Logger
) {
this.gitHubFeatureFlags = new GitHubFeatureFlags(
gitHubVersion,
apiDetails,
repositoryNwo,
logger
);
}
/**
*
@ -107,6 +119,23 @@ export class GitHubFeatureFlags implements FeatureFlags {
}
// Ask the GitHub API if the feature is enabled.
return await this.gitHubFeatureFlags.getValue(flag);
}
}
class GitHubFeatureFlags implements FeatureFlags {
private cachedApiResponse: FeatureFlagsApiResponse | undefined;
constructor(
private gitHubVersion: util.GitHubVersion,
private apiDetails: GitHubApiDetails,
private repositoryNwo: RepositoryNwo,
private logger: Logger
) {
/**/
}
async getValue(flag: Feature): Promise<boolean> {
const response = await this.getApiResponse();
if (response === undefined) {
this.logger.debug(
@ -121,52 +150,53 @@ export class GitHubFeatureFlags implements FeatureFlags {
);
return false;
}
return flagValue;
return flagValue || false;
}
private async getApiResponse(): Promise<FeatureFlagsApiResponse> {
const loadApiResponse = async () => {
// Do nothing when not running against github.com
if (this.gitHubVersion.type !== util.GitHubVariant.DOTCOM) {
this.logger.debug(
"Not running against github.com. Disabling all feature flags."
);
return {};
}
const client = getApiClient(this.apiDetails);
try {
const response = await client.request(
"GET /repos/:owner/:repo/code-scanning/codeql-action/features",
{
owner: this.repositoryNwo.owner,
repo: this.repositoryNwo.repo,
}
);
return response.data;
} catch (e) {
if (util.isHTTPError(e) && e.status === 403) {
this.logger.warning(
"This run of the CodeQL Action does not have permission to access Code Scanning API endpoints. " +
"As a result, it will not be opted into any experimental features. " +
"This could be because the Action is running on a pull request from a fork. If not, " +
`please ensure the Action has the 'security-events: write' permission. Details: ${e}`
);
} else {
// Some feature flags, such as `ml_powered_queries_enabled` affect the produced alerts.
// Considering these feature flags disabled in the event of a transient error could
// therefore lead to alert churn. As a result, we crash if we cannot determine the value of
// the feature flags.
throw new Error(
`Encountered an error while trying to load feature flags: ${e}`
);
}
}
};
const apiResponse = this.cachedApiResponse || (await loadApiResponse());
const apiResponse =
this.cachedApiResponse || (await this.loadApiResponse());
this.cachedApiResponse = apiResponse;
return apiResponse;
}
private async loadApiResponse() {
// Do nothing when not running against github.com
if (this.gitHubVersion.type !== util.GitHubVariant.DOTCOM) {
this.logger.debug(
"Not running against github.com. Disabling all feature flags."
);
return {};
}
const client = getApiClient(this.apiDetails);
try {
const response = await client.request(
"GET /repos/:owner/:repo/code-scanning/codeql-action/features",
{
owner: this.repositoryNwo.owner,
repo: this.repositoryNwo.repo,
}
);
return response.data;
} catch (e) {
if (util.isHTTPError(e) && e.status === 403) {
this.logger.warning(
"This run of the CodeQL Action does not have permission to access Code Scanning API endpoints. " +
"As a result, it will not be opted into any experimental features. " +
"This could be because the Action is running on a pull request from a fork. If not, " +
`please ensure the Action has the 'security-events: write' permission. Details: ${e}`
);
} else {
// Some feature flags, such as `ml_powered_queries_enabled` affect the produced alerts.
// Considering these feature flags disabled in the event of a transient error could
// therefore lead to alert churn. As a result, we crash if we cannot determine the value of
// the feature flags.
throw new Error(
`Encountered an error while trying to load feature flags: ${e}`
);
}
}
}
}
/**

View file

@ -15,7 +15,7 @@ import {
import { getGitHubVersionActionsOnly } from "./api-client";
import { CodeQL, CODEQL_VERSION_NEW_TRACING } from "./codeql";
import * as configUtils from "./config-utils";
import { Feature, FeatureFlags, GitHubFeatureFlags } from "./feature-flags";
import { Feature, FeatureFlags, Features } from "./feature-flags";
import {
initCodeQL,
initConfig,
@ -157,7 +157,7 @@ async function run() {
getRequiredEnvParam("GITHUB_REPOSITORY")
);
const featureFlags = new GitHubFeatureFlags(
const featureFlags = new Features(
gitHubVersion,
apiDetails,
repositoryNwo,