-
Notifications
You must be signed in to change notification settings - Fork 334
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
Conversation
bf52457
to
4de6737
Compare
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
4de6737
to
e5c3375
Compare
There was a problem hiding this 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
// 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)) | ||
); |
There was a problem hiding this comment.
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?
Sure, that makes sense. Maybe |
Sure, that works too. |
Actually, I like |
- 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.
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.ts
Outdated
export interface FeatureFlags { | ||
getValue(flag: FeatureFlag): Promise<boolean>; | ||
getValue(flag: Feature, codeql?: CodeQL): Promise<boolean>; | ||
} |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 toGithubFeatures
- Rename this interface to
FeatureEnablement
There was a problem hiding this comment.
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.
0a4f3d3
to
6de05e4
Compare
aa1634e
to
730228d
Compare
Internal refactoring so that `GitHubFeatureFlags` is private only. The public facing class is `Features`.
730228d
to
b27aed7
Compare
There was a problem hiding this 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.
src/feature-flags.ts
Outdated
export interface FeatureFlags { | ||
getValue(flag: FeatureFlag): Promise<boolean>; | ||
getValue(flag: Feature, codeql?: CodeQL): Promise<boolean>; | ||
} |
There was a problem hiding this comment.
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 toGithubFeatures
- Rename this interface to
FeatureEnablement
src/feature-flags.ts
Outdated
// Bypassing the toolcache is disabled in test mode. | ||
if (flag === Feature.BypassToolcacheEnabled && util.isInTestMode()) { | ||
return false; | ||
} |
There was a problem hiding this comment.
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.
src/feature-flags.ts
Outdated
@@ -53,51 +150,52 @@ export class GitHubFeatureFlags implements FeatureFlags { | |||
); | |||
return false; | |||
} | |||
return flagValue; | |||
return flagValue || false; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/feature-flags.ts
Outdated
// 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." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"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." |
There was a problem hiding this comment.
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.
src/feature-flags.ts
Outdated
@@ -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 { |
There was a problem hiding this comment.
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
src/init-action.ts
Outdated
@@ -157,7 +157,7 @@ async function run() { | |||
getRequiredEnvParam("GITHUB_REPOSITORY") | |||
); | |||
|
|||
const featureFlags = new GitHubFeatureFlags( | |||
const featureFlags = new Features( |
There was a problem hiding this comment.
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.
src/init-action.ts
Outdated
if (trapCaching !== undefined) { | ||
return trapCaching === "true"; | ||
} | ||
return await featureFlags.getValue(Feature.TrapCachingEnabled); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional
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.
9a002a0
to
919e4ca
Compare
From PR body —
Should the above be |
There was a problem hiding this 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!
src/feature-flags.ts
Outdated
export interface FeatureFlags { | ||
getValue(flag: FeatureFlag): Promise<boolean>; | ||
export interface FeatureEnablement { | ||
getValue(feaature: Feature, codeql?: CodeQL): Promise<boolean>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getValue(feaature: Feature, codeql?: CodeQL): Promise<boolean>; | |
getValue(feature: Feature, codeql?: CodeQL): Promise<boolean>; |
(+autogenerated code)
src/feature-flags.ts
Outdated
`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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Some feature, such as `ml_powered_queries_enabled` affect the produced alerts. | |
// Some features, such as `ml_powered_queries_enabled` affect the produced alerts. |
src/feature-flags.ts
Outdated
); | ||
} 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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
Outdated
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// feature flag with a minium version that is only used for tests. | |
// feature flag with a minimum version that is only used for tests. |
Done! |
There was a problem hiding this 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.
src/codeql.test.ts
Outdated
@@ -76,7 +80,7 @@ async function mockApiAndSetupCodeQL({ | |||
version, | |||
}: { | |||
apiDetails?: GitHubApiDetails; | |||
featureFlags?: FeatureFlags; | |||
featureFlags?: FeatureEnablement; |
There was a problem hiding this comment.
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
src/codeql.test.ts
Outdated
"", | ||
undefined, | ||
undefined, | ||
createFeatureFlags([Feature.CliConfigFileEnabled]), |
There was a problem hiding this comment.
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
src/feature-flags.test.ts
Outdated
|
||
// Alls flags should be false except the one we're testing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Alls flags should be false except the one we're testing | |
// All flags should be false except the one we're testing |
src/feature-flags.test.ts
Outdated
testApiDetails, | ||
testRepositoryNwo, | ||
getRecordingLogger(loggedMessages) | ||
const featureFlags = setUpTests( |
There was a problem hiding this comment.
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.
src/feature-flags.ts
Outdated
|
||
/** | ||
* | ||
* @param feature The feature flag to check. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param feature The feature flag to check. | |
* @param feature The feature to check. |
} | ||
} | ||
|
||
class GitHubFeatureFlags implements FeatureEnablement { |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
.
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. |
There was a problem hiding this 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.
I reviewed the last commit. 🤠 |
@henrymercer, can you give another approval? Not sure why your previous one was dismissed. |
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:
Merge / deployment checklist