Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor handling of feature flags #1284

Merged
merged 11 commits into from
Oct 12, 2022
Merged

Refactor handling of feature flags #1284

merged 11 commits into from
Oct 12, 2022

Conversation

aeisenberg
Copy link
Contributor

@aeisenberg aeisenberg commented Oct 5, 2022

This commit centralizes how feature flags are handled. All feature flags must now add an entry in the featureFlagConfig dictionary. This dictionary associates the flag with an environment variable name and optionally a minimum version for CodeQL.

The new logic is:

  • if the environment variable is set to false: disabled
  • if the minimum version requirement specified and not met: disabled
  • if a minimum version requirement is specified, but a codeql instance is not passed in: throw
  • if the environment variable is set to true: enable
  • Otherwise check feature flag enablement from the server

Merge / deployment checklist

  • Confirm this change is backwards compatible with existing workflows.
  • Confirm the readme has been updated if necessary.
  • Confirm the changelog has been updated if necessary.

@aeisenberg aeisenberg requested a review from a team as a code owner October 5, 2022 22:56
src/util.test.ts Fixed Show fixed Hide fixed
@aeisenberg aeisenberg force-pushed the aeisenberg/ff-refactoring branch from bf52457 to 4de6737 Compare October 5, 2022 23:10
This commit centralizes how feature flags are handled. All feature flags
must now add an entry in the `featureFlagConfig` dictionary. This
dictionary associates the flag with an environment variable name and
optionally a minimum version for CodeQL.

The new logic is:

- if the environment variable is set to false: disabled
- if the minimum version requirement specified and met: disabled
- if the environment variable is set to true: enable
- Otherwise check feature flag enablement from the server
@aeisenberg aeisenberg force-pushed the aeisenberg/ff-refactoring branch from 4de6737 to e5c3375 Compare October 5, 2022 23:41
Copy link
Contributor

@henrymercer henrymercer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this. As a general piece of feedback, I wonder whether it would be appropriate to rename FeatureFlags to Features. This would reflect a distinction between "feature flags", which are flippers on the monolith, and "features", which are functions in the CodeQL Action whose enablement depends on environment variables, CLI versions, and feature flags.

src/feature-flags.test.ts Outdated Show resolved Hide resolved
src/feature-flags.test.ts Outdated Show resolved Hide resolved
src/feature-flags.test.ts Outdated Show resolved Hide resolved
src/feature-flags.test.ts Outdated Show resolved Hide resolved
src/feature-flags.test.ts Outdated Show resolved Hide resolved
Comment on lines 222 to 249
// feature flag should be disabled when an old CLI version is set
let codeql = mockCodeQLVersion("2.0.0");
t.assert(
!(await featureFlags.getValue(featureFlag as FeatureFlag, codeql))
);

// even setting the env var to true should not enable the feature flag if
// the minimum CLI version is not met
process.env[featureFlagConfig[featureFlag].envVar] = "true";
t.assert(
!(await featureFlags.getValue(featureFlag as FeatureFlag, codeql))
);

// feature flag should be enabled when a new CLI version is set
// and env var is not set
process.env[featureFlagConfig[featureFlag].envVar] = "";
codeql = mockCodeQLVersion(
featureFlagConfig[featureFlag].minimumVersion
);
t.assert(
await featureFlags.getValue(featureFlag as FeatureFlag, codeql)
);

// set env var to false and check that the feature flag is now disabled
process.env[featureFlagConfig[featureFlag].envVar] = "false";
t.assert(
!(await featureFlags.getValue(featureFlag as FeatureFlag, codeql))
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should each of these be separate tests?

src/feature-flags.ts Outdated Show resolved Hide resolved
src/feature-flags.ts Outdated Show resolved Hide resolved
src/feature-flags.ts Outdated Show resolved Hide resolved
src/feature-flags.ts Outdated Show resolved Hide resolved
@aeisenberg
Copy link
Contributor Author

I wonder whether it would be appropriate to rename FeatureFlags to Features.

Sure, that makes sense. Maybe FeatureEnablement is better?

@henrymercer
Copy link
Contributor

Sure, that makes sense. Maybe FeatureEnablement is better?

Sure, that works too.

@aeisenberg
Copy link
Contributor Author

Actually, I like Features better since the names are used for more than just enablement.

- Change env var name for `MlPoweredQueriesEnabled`
- Throw error if minimumVersion is specified, but CodeQL argument is not
  supplied.
- Fix failing tests. Note that I removed a config-utils test because it
  is no longer relevant since we handle codeql minimum versions in the
  `getValue` function.
@aeisenberg
Copy link
Contributor Author

Addressed all comments. Please look at the two commits individually since the second commit adds a lot of noise through a renaming.

src/feature-flags.test.ts Outdated Show resolved Hide resolved
src/feature-flags.test.ts Outdated Show resolved Hide resolved
src/feature-flags.ts Outdated Show resolved Hide resolved
Comment on lines 7 to 9
export interface FeatureFlags {
getValue(flag: FeatureFlag): Promise<boolean>;
getValue(flag: Feature, codeql?: CodeQL): Promise<boolean>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably ought to rename this too to Features.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little bit of bike shedding, but I thought this should remain FeatureFlags since it really is an interface that checks whether a Feature is turned on or off. Also, the concrete instantiation of this is GitHubFeatureFlags.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that FeatureFlags.getValue(x) tells us whether the feature x should be enabled, which as of this PR is no longer the same as whether the feature flag on the monolith is enabled for x.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree we're in bikeshedding territory here, but this interface is also public facing and is really about feature enablement, not feature flags. Couple of suggestions:

  • Rename this interface to Features, and the class that implements it to GithubFeatures
  • Rename this interface to FeatureEnablement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right. A slightly more complex structure now, but I think it is better:

  • Interface is FeatureEnablement
  • The public facing class is Features
  • This delegates to another internal class GitHubFeatureFlags for the portion of the enablement checking that needs to get information from github.

@aeisenberg aeisenberg force-pushed the aeisenberg/ff-refactoring branch from 0a4f3d3 to 6de05e4 Compare October 6, 2022 20:06
@aeisenberg aeisenberg force-pushed the aeisenberg/ff-refactoring branch from aa1634e to 730228d Compare October 6, 2022 23:17
Internal refactoring so that `GitHubFeatureFlags` is
private only. The public facing class is `Features`.
@aeisenberg aeisenberg force-pushed the aeisenberg/ff-refactoring branch from 730228d to b27aed7 Compare October 7, 2022 01:00
Copy link
Contributor

@henrymercer henrymercer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking generally in good shape to me. Some follow up suggestions, some of which are optional.

Comment on lines 7 to 9
export interface FeatureFlags {
getValue(flag: FeatureFlag): Promise<boolean>;
getValue(flag: Feature, codeql?: CodeQL): Promise<boolean>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree we're in bikeshedding territory here, but this interface is also public facing and is really about feature enablement, not feature flags. Couple of suggestions:

  • Rename this interface to Features, and the class that implements it to GithubFeatures
  • Rename this interface to FeatureEnablement

src/feature-flags.ts Outdated Show resolved Hide resolved
src/feature-flags.ts Outdated Show resolved Hide resolved
Comment on lines 94 to 97
// Bypassing the toolcache is disabled in test mode.
if (flag === Feature.BypassToolcacheEnabled && util.isInTestMode()) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Make this a property of the featureConfig record.

@@ -53,51 +150,52 @@ export class GitHubFeatureFlags implements FeatureFlags {
);
return false;
}
return flagValue;
return flagValue || false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding: When would we need the || here? We already tackled the undefined case above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are getting this information from an API. I just wanted to ensure the value was coerced into a boolean. I should have put:

    return !!flagValue;

and I will change.

// 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."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Not running against github.com. Disabling all feature flags."
"Not running against github.com. Feature flags will only be enabled if they are enabled explicitly, for example through an environment variable."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to use toggleable features instead of feature flags since that seems more appropriate. "Feature flags" is only when they are coming from github. "Features" is the more general concept.

However, here you are suggesting we mention environment variables, but we don't have any place where we actually document them, so it might be confusing. So, I think it would be better if we just do:

Not running against github.com. Disabling all toggleable features.

@@ -106,7 +204,7 @@ export class GitHubFeatureFlags implements FeatureFlags {
*
* This should be only used within tests.
*/
export function createFeatureFlags(enabledFlags: FeatureFlag[]): FeatureFlags {
export function createFeatureFlags(enabledFlags: Feature[]): FeatureFlags {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename this if you go with my suggestion to rename the FeatureFlags interface

@@ -157,7 +157,7 @@ async function run() {
getRequiredEnvParam("GITHUB_REPOSITORY")
);

const featureFlags = new GitHubFeatureFlags(
const featureFlags = new Features(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variable assignments ought to be renamed too. It's a shame it's quite a lot of noise.

Comment on lines 324 to 327
if (trapCaching !== undefined) {
return trapCaching === "true";
}
return await featureFlags.getValue(Feature.TrapCachingEnabled);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional

Suggested change
if (trapCaching !== undefined) {
return trapCaching === "true";
}
return await featureFlags.getValue(Feature.TrapCachingEnabled);
return trapCaching === "true" || (await featureFlags.getValue(Feature.TrapCachingEnabled));

Avoid usage of "Feature Flag" unless we are talking specifically about
the response from github features api. Otherwise, use terms like
"Toggleable features".

Note both "toggleable" and "togglable" appear to be valid spellings of
the word. I chose the first for no good reason.
@aeisenberg aeisenberg force-pushed the aeisenberg/ff-refactoring branch from 9a002a0 to 919e4ca Compare October 7, 2022 21:14
@angelapwen
Copy link
Contributor

From PR body — 

The new logic is:
...
if the minimum version requirement specified and met: disabled

Should the above be if the minimum version requirement specified is not met?

Copy link
Contributor

@angelapwen angelapwen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just small comment typo nits 😸

Thanks for the code comments + clear variable naming!

export interface FeatureFlags {
getValue(flag: FeatureFlag): Promise<boolean>;
export interface FeatureEnablement {
getValue(feaature: Feature, codeql?: CodeQL): Promise<boolean>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
getValue(feaature: Feature, codeql?: CodeQL): Promise<boolean>;
getValue(feature: Feature, codeql?: CodeQL): Promise<boolean>;

(+autogenerated code)

`please ensure the Action has the 'security-events: write' permission. Details: ${e}`
);
} else {
// Some feature, such as `ml_powered_queries_enabled` affect the produced alerts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Some feature, such as `ml_powered_queries_enabled` affect the produced alerts.
// Some features, such as `ml_powered_queries_enabled` affect the produced alerts.

);
} else {
// Some feature, such as `ml_powered_queries_enabled` affect the produced alerts.
// Considering these feature disabled in the event of a transient error could
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Considering these feature disabled in the event of a transient error could
// Considering these features disabled in the event of a transient error could

src/feature-flags.test.ts Show resolved Hide resolved
// specify a minimum version, then we will have a bunch of code no longer being
// tested. This is unlikely, and this test will fail if that happens.
// If we do end up in that situation, then we should consider adding a synthetic
// feature flag with a minium version that is only used for tests.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// feature flag with a minium version that is only used for tests.
// feature flag with a minimum version that is only used for tests.

@aeisenberg
Copy link
Contributor Author

Done!

Copy link
Contributor

@henrymercer henrymercer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a bunch of uses of featureFlags across the codebase that probably ought to become features or featureEnablement. I would be open to merging this and cleaning those up as follow up though.

@@ -76,7 +80,7 @@ async function mockApiAndSetupCodeQL({
version,
}: {
apiDetails?: GitHubApiDetails;
featureFlags?: FeatureFlags;
featureFlags?: FeatureEnablement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be features or featureEnablement

"",
undefined,
undefined,
createFeatureFlags([Feature.CliConfigFileEnabled]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be createFeatures or createFeatureEnablement


// Alls flags should be false except the one we're testing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Alls flags should be false except the one we're testing
// All flags should be false except the one we're testing

testApiDetails,
testRepositoryNwo,
getRecordingLogger(loggedMessages)
const featureFlags = setUpTests(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be features / featureEnablement throughout.


/**
*
* @param feature The feature flag to check.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param feature The feature flag to check.
* @param feature The feature to check.

}
}

class GitHubFeatureFlags implements FeatureEnablement {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a doc comment here saying something like "Feature enablement based solely on the GitHub API. Most of the time you'll want to use Features instead."

@@ -1,115 +1,200 @@
import { getApiClient, GitHubApiDetails } from "./api-client";
import { CodeQL } from "./codeql";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming this file to features or feature-enablement.

@aeisenberg
Copy link
Contributor Author

Let's just get it all done now.

@henrymercer
Copy link
Contributor

Let's just get it all done now.

Sounds good to me! I've pushed a commit to another branch with some more renaming suggestions to avoid needing to make them all in the UI :) https://github.com/github/codeql-action/compare/aeisenberg/ff-refactoring...henrymercer/more-ff-renaming?expand=1.

henrymercer
henrymercer previously approved these changes Oct 12, 2022
Copy link
Contributor

@henrymercer henrymercer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review up to the penultimate commit, since I authored the last one.

@aeisenberg
Copy link
Contributor Author

I reviewed the last commit. 🤠

@aeisenberg aeisenberg enabled auto-merge October 12, 2022 14:06
@aeisenberg aeisenberg added the Update dependencies Trigger PR workflow to update dependencies label Oct 12, 2022
@github-actions github-actions bot removed the Update dependencies Trigger PR workflow to update dependencies label Oct 12, 2022
@aeisenberg
Copy link
Contributor Author

@henrymercer, can you give another approval? Not sure why your previous one was dismissed.

@aeisenberg aeisenberg merged commit 160e3fe into main Oct 12, 2022
@aeisenberg aeisenberg deleted the aeisenberg/ff-refactoring branch October 12, 2022 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants