/
feature-flags.js
207 lines (207 loc) · 9.09 KB
/
feature-flags.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Features = exports.FEATURE_FLAGS_FILE_NAME = exports.featureConfig = exports.Feature = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const api_client_1 = require("./api-client");
const util = __importStar(require("./util"));
var Feature;
(function (Feature) {
Feature["BypassToolcacheEnabled"] = "bypass_toolcache_enabled";
Feature["CliConfigFileEnabled"] = "cli_config_file_enabled";
Feature["DisableKotlinAnalysisEnabled"] = "disable_kotlin_analysis_enabled";
Feature["FileBaselineInformationEnabled"] = "file_baseline_information_enabled";
Feature["MlPoweredQueriesEnabled"] = "ml_powered_queries_enabled";
Feature["TrapCachingEnabled"] = "trap_caching_enabled";
})(Feature = exports.Feature || (exports.Feature = {}));
exports.featureConfig = {
[Feature.BypassToolcacheEnabled]: {
envVar: "CODEQL_BYPASS_TOOLCACHE",
minimumVersion: undefined,
},
[Feature.DisableKotlinAnalysisEnabled]: {
envVar: "CODEQL_DISABLE_KOTLIN_ANALYSIS",
minimumVersion: undefined,
},
[Feature.CliConfigFileEnabled]: {
envVar: "CODEQL_PASS_CONFIG_TO_CLI",
minimumVersion: "2.11.1",
},
[Feature.FileBaselineInformationEnabled]: {
envVar: "CODEQL_FILE_BASELINE_INFORMATION",
minimumVersion: "2.11.3",
},
[Feature.MlPoweredQueriesEnabled]: {
envVar: "CODEQL_ML_POWERED_QUERIES",
minimumVersion: "2.7.5",
},
[Feature.TrapCachingEnabled]: {
envVar: "CODEQL_TRAP_CACHING",
minimumVersion: undefined,
},
};
exports.FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json";
/**
* 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.
*/
class Features {
constructor(gitHubVersion, repositoryNwo, tempDir, logger) {
this.gitHubFeatureFlags = new GitHubFeatureFlags(gitHubVersion, repositoryNwo, path.join(tempDir, exports.FEATURE_FLAGS_FILE_NAME), logger);
}
/**
*
* @param feature The feature to check.
* @param codeql An optional CodeQL object. If provided, and a `minimumVersion` is specified for the
* feature, the version of the CodeQL CLI will be checked against the minimum version.
* If the version is less than the minimum version, the feature will be considered
* disabled. If not provided, and a `minimumVersion` is specified for the feature, the
* this function will throw.
* @returns true if the feature is enabled, false otherwise.
*
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
if (!codeql && exports.featureConfig[feature].minimumVersion) {
throw new Error(`Internal error: A minimum version is specified for feature ${feature}, but no instance of CodeQL was provided.`);
}
// Bypassing the toolcache is disabled in test mode.
if (feature === Feature.BypassToolcacheEnabled && util.isInTestMode()) {
return false;
}
const envVar = (process.env[exports.featureConfig[feature].envVar] || "").toLocaleLowerCase();
// Do not use this feature if user explicitly disables it via an environment variable.
if (envVar === "false") {
return false;
}
// Never use this feature if the CLI version explicitly can't support it.
const minimumVersion = exports.featureConfig[feature].minimumVersion;
if (codeql && minimumVersion) {
if (!(await util.codeQlVersionAbove(codeql, minimumVersion))) {
return false;
}
}
// Use this feature if user explicitly enables it via an environment variable.
if (envVar === "true") {
return true;
}
// Ask the GitHub API if the feature is enabled.
return await this.gitHubFeatureFlags.getValue(feature);
}
}
exports.Features = Features;
class GitHubFeatureFlags {
constructor(gitHubVersion, repositoryNwo, featureFlagsFile, logger) {
this.gitHubVersion = gitHubVersion;
this.repositoryNwo = repositoryNwo;
this.featureFlagsFile = featureFlagsFile;
this.logger = logger;
/**/
}
async getValue(feature) {
const response = await this.getAllFeatures();
if (response === undefined) {
this.logger.debug(`No feature flags API response for ${feature}, considering it disabled.`);
return false;
}
const featureEnablement = response[feature];
if (featureEnablement === undefined) {
this.logger.debug(`Feature '${feature}' undefined in API response, considering it disabled.`);
return false;
}
return !!featureEnablement;
}
async getAllFeatures() {
// if we have an in memory cache, use that
if (this.cachedApiResponse !== undefined) {
return this.cachedApiResponse;
}
// if a previous step has written a feature flags file to disk, use that
const fileFlags = await this.readLocalFlags();
if (fileFlags !== undefined) {
this.cachedApiResponse = fileFlags;
return fileFlags;
}
// if not, request flags from the server
let remoteFlags = await this.loadApiResponse();
if (remoteFlags === undefined) {
remoteFlags = {};
}
// cache the response in memory
this.cachedApiResponse = remoteFlags;
// and cache them to disk so future workflow steps can use them
await this.writeLocalFlags(remoteFlags);
return remoteFlags;
}
async readLocalFlags() {
try {
if (fs.existsSync(this.featureFlagsFile)) {
this.logger.debug(`Loading feature flags from ${this.featureFlagsFile}`);
return JSON.parse(fs.readFileSync(this.featureFlagsFile, "utf8"));
}
}
catch (e) {
this.logger.warning(`Error reading cached feature flags file ${this.featureFlagsFile}: ${e}. Requesting from GitHub instead.`);
}
return undefined;
}
async writeLocalFlags(flags) {
try {
this.logger.debug(`Writing feature flags to ${this.featureFlagsFile}`);
fs.writeFileSync(this.featureFlagsFile, JSON.stringify(flags));
}
catch (e) {
this.logger.warning(`Error writing cached feature flags file ${this.featureFlagsFile}: ${e}.`);
}
}
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 toggleable features.");
return {};
}
try {
const response = await (0, api_client_1.getApiClient)().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 features, such as `ml_powered_queries_enabled` affect the produced alerts.
// Considering these features 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.
throw new Error(`Encountered an error while trying to determine feature enablement: ${e}`);
}
}
}
}
//# sourceMappingURL=feature-flags.js.map