Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Add support for feature flagging via the GitHub API
- v3.22.12
- v3.22.11
- v3
- v2.22.12
- v2.22.11
- v2.22.10
- v2.22.9
- v2.22.8
- v2.22.7
- v2.22.6
- v2.22.5
- v2.22.4
- v2.22.3
- v2.22.2
- v2.22.1
- v2.22.0
- v2.21.9
- v2.21.8
- v2.21.7
- v2.21.6
- v2.21.5
- v2.21.4
- v2.21.3
- v2.21.2
- v2.21.1
- v2.21.0
- v2.20.4
- v2.20.3
- v2.20.2
- v2.20.1
- v2.20.0
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.12
- v2.2.11
- v2.2.10
- v2.2.9
- v2.2.8
- v2.2.7
- v2.2.6
- v2.2.5
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.39
- v2.1.38
- v2.1.37
- v2.1.36
- v2.1.35
- v2.1.34
- v2.1.33
- v2.1.32
- v2.1.31
- v2.1.30
- v2.1.29
- v2.1.28
- v2.1.27
- v2.1.26
- v2.1.25
- v2.1.24
- v2.1.23
- v2.1.22
- v2.1.21
- v2.1.20
- v2.1.19
- v2.1.18
- v2.1.17
- v2.1.16
- v2.1.15
- v2.1.14
- v2.1.13
- v2.1.12
- v2.1.11
- v2.1.10
- v2.1.9
- v2.1.8
- v2.1.7
- v2.1.6
- v2
- v1.1.39
- v1.1.38
- v1.1.37
- v1.1.36
- v1.1.35
- v1.1.34
- v1.1.33
- v1.1.32
- v1.1.31
- v1.1.30
- v1.1.29
- v1.1.28
- v1.1.27
- v1.1.26
- v1.1.25
- v1.1.24
- v1.1.23
- v1.1.22
- v1.1.21
- v1.1.20
- v1.1.19
- v1.1.18
- v1.1.17
- v1.1.16
- v1.1.15
- v1.1.14
- v1.1.13
- v1.1.12
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.32
- v1.0.31
- v1.0.30
- v1.0.29
- v1.0.28
- v1.0.27
- v1
Henry Mercer
committed
Dec 15, 2021
1 parent
e1f0590
commit 04671ef
Showing
12 changed files
with
590 additions
and
66 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import * as github from "@actions/github"; | ||
import test from "ava"; | ||
import * as sinon from "sinon"; | ||
|
||
import * as apiClient from "./api-client"; | ||
import { GitHubApiDetails } from "./api-client"; | ||
import { GitHubFeatureFlags } from "./feature-flags"; | ||
import { | ||
getRecordingLogger, | ||
LoggedMessage, | ||
setupActionsVars, | ||
setupTests, | ||
} from "./testing-utils"; | ||
import * as util from "./util"; | ||
import { | ||
GitHubVariant, | ||
HTTPError, | ||
initializeEnvironment, | ||
Mode, | ||
withTmpDir, | ||
} from "./util"; | ||
|
||
setupTests(test); | ||
|
||
test.beforeEach(() => { | ||
initializeEnvironment(Mode.actions, "1.2.3"); | ||
|
||
sinon | ||
.stub(util, "getRequiredEnvParam") | ||
.withArgs("GITHUB_REPOSITORY") | ||
.returns("github/example"); | ||
}); | ||
|
||
const testApiDetails: GitHubApiDetails = { | ||
auth: "1234", | ||
url: "https://github.com", | ||
}; | ||
|
||
function mockHttpRequests( | ||
responseStatusCode: number, | ||
flags: { [flagName: string]: boolean } | ||
) { | ||
// Passing an auth token is required, so we just use a dummy value | ||
const client = github.getOctokit("123"); | ||
|
||
const requestSpy = sinon.stub(client, "request"); | ||
|
||
const optInSpy = requestSpy.withArgs( | ||
"GET /repos/:owner/:repo/code-scanning/codeql-action/features" | ||
); | ||
if (responseStatusCode < 300) { | ||
optInSpy.resolves({ | ||
status: responseStatusCode, | ||
data: flags, | ||
headers: {}, | ||
url: "GET /repos/:owner/:repo/code-scanning/codeql-action/features", | ||
}); | ||
} else { | ||
optInSpy.throws(new HTTPError("some error message", responseStatusCode)); | ||
} | ||
|
||
sinon.stub(apiClient, "getApiClient").value(() => client); | ||
} | ||
|
||
const ALL_FEATURE_FLAGS_DISABLED_VARIANTS: Array<{ | ||
description: string; | ||
gitHubVersion: util.GitHubVersion; | ||
}> = [ | ||
{ | ||
description: "GHES", | ||
gitHubVersion: { type: GitHubVariant.GHES, version: "3.0.0" }, | ||
}, | ||
{ description: "GHAE", gitHubVersion: { type: GitHubVariant.GHAE } }, | ||
]; | ||
|
||
for (const variant of ALL_FEATURE_FLAGS_DISABLED_VARIANTS) { | ||
test(`All feature flags are disabled if running against ${variant.description}`, async (t) => { | ||
await withTmpDir(async (tmpDir) => { | ||
setupActionsVars(tmpDir, tmpDir); | ||
|
||
const loggedMessages = []; | ||
const featureFlags = new GitHubFeatureFlags( | ||
variant.gitHubVersion, | ||
testApiDetails, | ||
getRecordingLogger(loggedMessages) | ||
); | ||
|
||
t.assert((await featureFlags.getDatabaseUploadsEnabled()) === false); | ||
t.assert((await featureFlags.getMlPoweredQueriesEnabled()) === false); | ||
t.assert((await featureFlags.getUploadsDomainEnabled()) === false); | ||
|
||
t.assert( | ||
loggedMessages.find( | ||
(v: LoggedMessage) => | ||
v.type === "debug" && | ||
v.message === | ||
"Not running against github.com. Disabling all feature flags." | ||
) !== undefined | ||
); | ||
}); | ||
}); | ||
} | ||
|
||
test("Feature flags are disabled if they're not returned in API response", async (t) => { | ||
await withTmpDir(async (tmpDir) => { | ||
setupActionsVars(tmpDir, tmpDir); | ||
|
||
const loggedMessages = []; | ||
const featureFlags = new GitHubFeatureFlags( | ||
{ type: GitHubVariant.DOTCOM }, | ||
testApiDetails, | ||
getRecordingLogger(loggedMessages) | ||
); | ||
|
||
mockHttpRequests(200, {}); | ||
|
||
t.assert((await featureFlags.getDatabaseUploadsEnabled()) === false); | ||
t.assert((await featureFlags.getMlPoweredQueriesEnabled()) === false); | ||
t.assert((await featureFlags.getUploadsDomainEnabled()) === false); | ||
|
||
for (const featureFlag of [ | ||
"database_uploads_enabled", | ||
"ml_powered_queries_enabled", | ||
"uploads_domain_enabled", | ||
]) { | ||
t.assert( | ||
loggedMessages.find( | ||
(v: LoggedMessage) => | ||
v.type === "debug" && | ||
v.message === | ||
`Feature flag '${featureFlag}' undefined in API response, considering it disabled.` | ||
) !== undefined | ||
); | ||
} | ||
}); | ||
}); | ||
|
||
test("All feature flags are disabled if the API request errors", async (t) => { | ||
await withTmpDir(async (tmpDir) => { | ||
setupActionsVars(tmpDir, tmpDir); | ||
|
||
const loggedMessages = []; | ||
const featureFlags = new GitHubFeatureFlags( | ||
{ type: GitHubVariant.DOTCOM }, | ||
testApiDetails, | ||
getRecordingLogger(loggedMessages) | ||
); | ||
|
||
mockHttpRequests(500, {}); | ||
|
||
t.assert((await featureFlags.getDatabaseUploadsEnabled()) === false); | ||
t.assert((await featureFlags.getMlPoweredQueriesEnabled()) === false); | ||
t.assert((await featureFlags.getUploadsDomainEnabled()) === false); | ||
|
||
t.assert( | ||
loggedMessages.find( | ||
(v: LoggedMessage) => | ||
v.type === "info" && | ||
v.message === | ||
"Disabling all feature flags due to unknown error: Error: some error message" | ||
) !== undefined | ||
); | ||
}); | ||
}); | ||
|
||
const FEATURE_FLAGS = [ | ||
"database_uploads_enabled", | ||
"ml_powered_queries_enabled", | ||
"uploads_domain_enabled", | ||
]; | ||
|
||
for (const featureFlag of FEATURE_FLAGS) { | ||
test(`Feature flag '${featureFlag}' is enabled if enabled in the API response`, async (t) => { | ||
await withTmpDir(async (tmpDir) => { | ||
setupActionsVars(tmpDir, tmpDir); | ||
|
||
const loggedMessages = []; | ||
const featureFlags = new GitHubFeatureFlags( | ||
{ type: GitHubVariant.DOTCOM }, | ||
testApiDetails, | ||
getRecordingLogger(loggedMessages) | ||
); | ||
|
||
const expectedFeatureFlags = {}; | ||
for (const f of FEATURE_FLAGS) { | ||
expectedFeatureFlags[f] = false; | ||
} | ||
expectedFeatureFlags[featureFlag] = true; | ||
mockHttpRequests(200, expectedFeatureFlags); | ||
|
||
const actualFeatureFlags = { | ||
database_uploads_enabled: | ||
await featureFlags.getDatabaseUploadsEnabled(), | ||
ml_powered_queries_enabled: | ||
await featureFlags.getMlPoweredQueriesEnabled(), | ||
uploads_domain_enabled: await featureFlags.getUploadsDomainEnabled(), | ||
}; | ||
|
||
t.deepEqual(actualFeatureFlags, expectedFeatureFlags); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { getApiClient, GitHubApiDetails } from "./api-client"; | ||
import { Logger } from "./logging"; | ||
import { parseRepositoryNwo } from "./repository"; | ||
import * as util from "./util"; | ||
|
||
interface FeatureFlags { | ||
getDatabaseUploadsEnabled(): Promise<boolean>; | ||
getMlPoweredQueriesEnabled(): Promise<boolean>; | ||
getUploadsDomainEnabled(): Promise<boolean>; | ||
} | ||
|
||
/** | ||
* A response from the GitHub API that contains feature flag enablement information for the CodeQL | ||
* Action. | ||
* | ||
* It maps feature flags to whether they are enabled or not. | ||
*/ | ||
type FeatureFlagsApiResponse = { [flagName: string]: boolean }; | ||
|
||
export class GitHubFeatureFlags implements FeatureFlags { | ||
private cachedApiResponse: FeatureFlagsApiResponse | undefined; | ||
|
||
constructor( | ||
private gitHubVersion: util.GitHubVersion, | ||
private apiDetails: GitHubApiDetails, | ||
private logger: Logger | ||
) {} | ||
|
||
getDatabaseUploadsEnabled(): Promise<boolean> { | ||
return this.getFeatureFlag("database_uploads_enabled"); | ||
} | ||
|
||
getMlPoweredQueriesEnabled(): Promise<boolean> { | ||
return this.getFeatureFlag("ml_powered_queries_enabled"); | ||
} | ||
|
||
getUploadsDomainEnabled(): Promise<boolean> { | ||
return this.getFeatureFlag("uploads_domain_enabled"); | ||
} | ||
|
||
async preloadFeatureFlags(): Promise<void> { | ||
await this.getApiResponse(); | ||
} | ||
|
||
private async getFeatureFlag(name: string): Promise<boolean> { | ||
const response = (await this.getApiResponse())[name]; | ||
if (response === undefined) { | ||
this.logger.debug( | ||
`Feature flag '${name}' undefined in API response, considering it disabled.` | ||
); | ||
} | ||
return response || 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); | ||
const repositoryNwo = parseRepositoryNwo( | ||
util.getRequiredEnvParam("GITHUB_REPOSITORY") | ||
); | ||
try { | ||
const response = await client.request( | ||
"GET /repos/:owner/:repo/code-scanning/codeql-action/features", | ||
{ | ||
owner: repositoryNwo.owner, | ||
repo: repositoryNwo.repo, | ||
} | ||
); | ||
return response.data; | ||
} catch (e) { | ||
console.log(e); | ||
this.logger.info( | ||
`Disabling all feature flags due to unknown error: ${e}` | ||
); | ||
return {}; | ||
} | ||
}; | ||
|
||
const apiResponse = this.cachedApiResponse || (await loadApiResponse()); | ||
this.cachedApiResponse = apiResponse; | ||
return apiResponse; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters