Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Track outcomes of querying FFs with getValue
  • Loading branch information
mbg committed Jan 28, 2026
commit 9d2031faa42248f8b6e14d21fc4258b034193d9e
11 changes: 11 additions & 0 deletions lib/analyze-action.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions lib/autobuild-action.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions lib/init-action-post.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions lib/init-action.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions lib/setup-codeql-action.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions lib/upload-sarif-action.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 26 additions & 1 deletion src/feature-flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ test(`Feature flags are requested in GHEC-DR`, async (t) => {
});
});

test("Queried feature flags are recorded", async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages = [];
const features = setUpFeatureFlagTests(
tmpDir,
getRecordingLogger(loggedMessages),
{ type: GitHubVariant.DOTCOM },
);

mockFeatureFlagApiEndpoint(200, initializeFeatures(true));

// No features should have been queried initially.
t.is(Object.keys(features.getQueriedFeatures()).length, 0);

// Query all features.
const allFeatures = Object.values(Feature);
for (const feature of allFeatures) {
await getFeatureIncludingCodeQlIfRequired(features, feature);
}

// All features should have a been queried.
t.is(Object.keys(features.getQueriedFeatures()).length, allFeatures.length);
});
});

test("API response missing and features use default value", async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages: LoggedMessage[] = [];
Expand Down Expand Up @@ -562,7 +587,7 @@ function setUpFeatureFlagTests(
tmpDir: string,
logger = getRunnerLogger(true),
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion,
): FeatureEnablement {
): Features {
setupActionsVars(tmpDir, tmpDir);

return new Features(gitHubVersion, testRepositoryNwo, tmpDir, logger);
Expand Down
25 changes: 25 additions & 0 deletions src/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,14 @@ export interface FeatureEnablement {
*/
type GitHubFeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;

// Even though we are currently only tracking the value of queried features, we use an object
// here rather than just a boolean to keep open the possibility of also tracking the reason
// for why a particular value was resolved (i.e. because of an environment variable or API result)
// in the future.
export interface QueriedFeatureStatus {
value: boolean;
}

export const FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json";

/**
Expand All @@ -355,6 +363,9 @@ export const FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json";
export class Features implements FeatureEnablement {
private gitHubFeatureFlags: GitHubFeatureFlags;

// Tracks features that have been queried at some point and the outcome.
private queriedFeatures: Partial<Record<Feature, QueriedFeatureStatus>> = {};

constructor(
gitHubVersion: util.GitHubVersion,
repositoryNwo: RepositoryNwo,
Expand All @@ -369,6 +380,11 @@ export class Features implements FeatureEnablement {
);
}

/** Gets a record of features that were queried and the corresponding outcomes. */
public getQueriedFeatures() {
return this.queriedFeatures;
}

async getDefaultCliVersion(
variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
Expand All @@ -388,6 +404,15 @@ export class Features implements FeatureEnablement {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature: Feature, codeql?: CodeQL): Promise<boolean> {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}

private async getValueInternal(
feature: Feature,
codeql?: CodeQL,
): Promise<boolean> {
// Narrow the type to FeatureConfig to avoid type errors. To avoid unsafe use of `as`, we
// check that the required properties exist using `satisfies`.
const config = featureConfig[
Expand Down