Cache feature flags on disk
This will allow feature flags to be shared across steps in the same job, avoiding an error we saw earlier where the init action had the flag enabled, but the analyze step had it disabled. This uses the runner's temp folder to cache the flags file, which will stick around until the job completes.
This commit is contained in:
parent
4fddc51e4f
commit
c29fca48a1
12 changed files with 255 additions and 49 deletions
2
lib/analyze-action.js
generated
2
lib/analyze-action.js
generated
|
|
@ -155,7 +155,7 @@ async function run() {
|
||||||
const memory = util.getMemoryFlag(actionsUtil.getOptionalInput("ram") || process.env["CODEQL_RAM"]);
|
const memory = util.getMemoryFlag(actionsUtil.getOptionalInput("ram") || process.env["CODEQL_RAM"]);
|
||||||
const repositoryNwo = (0, repository_1.parseRepositoryNwo)(util.getRequiredEnvParam("GITHUB_REPOSITORY"));
|
const repositoryNwo = (0, repository_1.parseRepositoryNwo)(util.getRequiredEnvParam("GITHUB_REPOSITORY"));
|
||||||
const gitHubVersion = await (0, api_client_1.getGitHubVersion)();
|
const gitHubVersion = await (0, api_client_1.getGitHubVersion)();
|
||||||
const features = new feature_flags_1.Features(gitHubVersion, repositoryNwo, logger);
|
const features = new feature_flags_1.Features(gitHubVersion, repositoryNwo, actionsUtil.getTemporaryDirectory(), logger);
|
||||||
await runAutobuildIfLegacyGoWorkflow(config, logger);
|
await runAutobuildIfLegacyGoWorkflow(config, logger);
|
||||||
dbCreationTimings = await (0, analyze_1.runFinalize)(outputDir, threads, memory, config, logger);
|
dbCreationTimings = await (0, analyze_1.runFinalize)(outputDir, threads, memory, config, logger);
|
||||||
if (actionsUtil.getRequiredInput("skip-queries") !== "true") {
|
if (actionsUtil.getRequiredInput("skip-queries") !== "true") {
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
60
lib/feature-flags.js
generated
60
lib/feature-flags.js
generated
|
|
@ -19,7 +19,9 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.Features = exports.featureConfig = exports.Feature = void 0;
|
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 api_client_1 = require("./api-client");
|
||||||
const util = __importStar(require("./util"));
|
const util = __importStar(require("./util"));
|
||||||
var Feature;
|
var Feature;
|
||||||
|
|
@ -57,14 +59,15 @@ exports.featureConfig = {
|
||||||
minimumVersion: undefined,
|
minimumVersion: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
exports.FEATURE_FLAGS_FILE_NAME = "feature-flags.json";
|
||||||
/**
|
/**
|
||||||
* Determines the enablement status of a number of features.
|
* Determines the enablement status of a number of features.
|
||||||
* If feature enablement is not able to be determined locally, a request to the
|
* If feature enablement is not able to be determined locally, a request to the
|
||||||
* GitHub API is made to determine the enablement status.
|
* GitHub API is made to determine the enablement status.
|
||||||
*/
|
*/
|
||||||
class Features {
|
class Features {
|
||||||
constructor(gitHubVersion, repositoryNwo, logger) {
|
constructor(gitHubVersion, repositoryNwo, tempDir, logger) {
|
||||||
this.gitHubFeatureFlags = new GitHubFeatureFlags(gitHubVersion, repositoryNwo, logger);
|
this.gitHubFeatureFlags = new GitHubFeatureFlags(gitHubVersion, repositoryNwo, path.join(tempDir, exports.FEATURE_FLAGS_FILE_NAME), logger);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
@ -108,14 +111,15 @@ class Features {
|
||||||
}
|
}
|
||||||
exports.Features = Features;
|
exports.Features = Features;
|
||||||
class GitHubFeatureFlags {
|
class GitHubFeatureFlags {
|
||||||
constructor(gitHubVersion, repositoryNwo, logger) {
|
constructor(gitHubVersion, repositoryNwo, featureFlagsFile, logger) {
|
||||||
this.gitHubVersion = gitHubVersion;
|
this.gitHubVersion = gitHubVersion;
|
||||||
this.repositoryNwo = repositoryNwo;
|
this.repositoryNwo = repositoryNwo;
|
||||||
|
this.featureFlagsFile = featureFlagsFile;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
/**/
|
/**/
|
||||||
}
|
}
|
||||||
async getValue(feature) {
|
async getValue(feature) {
|
||||||
const response = await this.getApiResponse();
|
const response = await this.getAllFeatures();
|
||||||
if (response === undefined) {
|
if (response === undefined) {
|
||||||
this.logger.debug(`No feature flags API response for ${feature}, considering it disabled.`);
|
this.logger.debug(`No feature flags API response for ${feature}, considering it disabled.`);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -127,10 +131,48 @@ class GitHubFeatureFlags {
|
||||||
}
|
}
|
||||||
return !!featureEnablement;
|
return !!featureEnablement;
|
||||||
}
|
}
|
||||||
async getApiResponse() {
|
async getAllFeatures() {
|
||||||
const apiResponse = this.cachedApiResponse || (await this.loadApiResponse());
|
// if we have an in memory cache, use that
|
||||||
this.cachedApiResponse = apiResponse;
|
if (this.cachedApiResponse !== undefined) {
|
||||||
return apiResponse;
|
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() {
|
async loadApiResponse() {
|
||||||
// Do nothing when not running against github.com
|
// Do nothing when not running against github.com
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"feature-flags.js","sourceRoot":"","sources":["../src/feature-flags.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"feature-flags.js","sourceRoot":"","sources":["../src/feature-flags.ts"],"names":[],"mappings":"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"}
|
||||||
62
lib/feature-flags.test.js
generated
62
lib/feature-flags.test.js
generated
|
|
@ -1,8 +1,29 @@
|
||||||
"use strict";
|
"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;
|
||||||
|
};
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const path = __importStar(require("path"));
|
||||||
const ava_1 = __importDefault(require("ava"));
|
const ava_1 = __importDefault(require("ava"));
|
||||||
const feature_flags_1 = require("./feature-flags");
|
const feature_flags_1 = require("./feature-flags");
|
||||||
const logging_1 = require("./logging");
|
const logging_1 = require("./logging");
|
||||||
|
|
@ -25,7 +46,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
(0, ava_1.default)(`All features are disabled if running against ${variant.description}`, async (t) => {
|
(0, ava_1.default)(`All features are disabled if running against ${variant.description}`, async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
const featureEnablement = setUpTests(tmpDir, (0, testing_utils_1.getRecordingLogger)(loggedMessages), variant.gitHubVersion);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir, (0, testing_utils_1.getRecordingLogger)(loggedMessages), variant.gitHubVersion);
|
||||||
for (const feature of Object.values(feature_flags_1.Feature)) {
|
for (const feature of Object.values(feature_flags_1.Feature)) {
|
||||||
t.false(await featureEnablement.getValue(feature, includeCodeQlIfRequired(feature)));
|
t.false(await featureEnablement.getValue(feature, includeCodeQlIfRequired(feature)));
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +59,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
(0, ava_1.default)("API response missing", async (t) => {
|
(0, ava_1.default)("API response missing", async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
const featureEnablement = setUpTests(tmpDir, (0, testing_utils_1.getRecordingLogger)(loggedMessages));
|
const featureEnablement = setUpFeatureFlagTests(tmpDir, (0, testing_utils_1.getRecordingLogger)(loggedMessages));
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(403, {});
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(403, {});
|
||||||
for (const feature of Object.values(feature_flags_1.Feature)) {
|
for (const feature of Object.values(feature_flags_1.Feature)) {
|
||||||
t.assert((await featureEnablement.getValue(feature, includeCodeQlIfRequired(feature))) === false);
|
t.assert((await featureEnablement.getValue(feature, includeCodeQlIfRequired(feature))) === false);
|
||||||
|
|
@ -49,7 +70,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
(0, ava_1.default)("Features are disabled if they're not returned in API response", async (t) => {
|
(0, ava_1.default)("Features are disabled if they're not returned in API response", async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
const featureEnablement = setUpTests(tmpDir, (0, testing_utils_1.getRecordingLogger)(loggedMessages));
|
const featureEnablement = setUpFeatureFlagTests(tmpDir, (0, testing_utils_1.getRecordingLogger)(loggedMessages));
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, {});
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, {});
|
||||||
for (const feature of Object.values(feature_flags_1.Feature)) {
|
for (const feature of Object.values(feature_flags_1.Feature)) {
|
||||||
t.assert((await featureEnablement.getValue(feature, includeCodeQlIfRequired(feature))) === false);
|
t.assert((await featureEnablement.getValue(feature, includeCodeQlIfRequired(feature))) === false);
|
||||||
|
|
@ -59,7 +80,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
});
|
});
|
||||||
(0, ava_1.default)("Feature flags exception is propagated if the API request errors", async (t) => {
|
(0, ava_1.default)("Feature flags exception is propagated if the API request errors", async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(500, {});
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(500, {});
|
||||||
await t.throwsAsync(async () => featureEnablement.getValue(feature_flags_1.Feature.MlPoweredQueriesEnabled, includeCodeQlIfRequired(feature_flags_1.Feature.MlPoweredQueriesEnabled)), {
|
await t.throwsAsync(async () => featureEnablement.getValue(feature_flags_1.Feature.MlPoweredQueriesEnabled, includeCodeQlIfRequired(feature_flags_1.Feature.MlPoweredQueriesEnabled)), {
|
||||||
message: "Encountered an error while trying to determine feature enablement: Error: some error message",
|
message: "Encountered an error while trying to determine feature enablement: Error: some error message",
|
||||||
|
|
@ -69,7 +90,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
(0, ava_1.default)(`Only feature '${feature}' is enabled if enabled in the API response. Other features disabled`, async (t) => {
|
(0, ava_1.default)(`Only feature '${feature}' is enabled if enabled in the API response. Other features disabled`, async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
// set all features to false except the one we're testing
|
// set all features to false except the one we're testing
|
||||||
const expectedFeatureEnablement = {};
|
const expectedFeatureEnablement = {};
|
||||||
for (const f of Object.keys(feature_flags_1.featureConfig)) {
|
for (const f of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
|
|
@ -87,7 +108,7 @@ for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
});
|
});
|
||||||
(0, ava_1.default)(`Only feature '${feature}' is enabled if the associated environment variable is true. Others disabled.`, async (t) => {
|
(0, ava_1.default)(`Only feature '${feature}' is enabled if the associated environment variable is true. Others disabled.`, async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
const expectedFeatureEnablement = initializeFeatures(false);
|
const expectedFeatureEnablement = initializeFeatures(false);
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
||||||
// feature should be disabled initially
|
// feature should be disabled initially
|
||||||
|
|
@ -99,7 +120,7 @@ for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
});
|
});
|
||||||
(0, ava_1.default)(`Feature '${feature}' is disabled if the associated environment variable is false, even if enabled in API`, async (t) => {
|
(0, ava_1.default)(`Feature '${feature}' is disabled if the associated environment variable is false, even if enabled in API`, async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
const expectedFeatureEnablement = initializeFeatures(true);
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
||||||
// feature should be enabled initially
|
// feature should be enabled initially
|
||||||
|
|
@ -112,7 +133,7 @@ for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
if (feature_flags_1.featureConfig[feature].minimumVersion !== undefined) {
|
if (feature_flags_1.featureConfig[feature].minimumVersion !== undefined) {
|
||||||
(0, ava_1.default)(`Getting feature '${feature} should throw if no codeql is provided`, async (t) => {
|
(0, ava_1.default)(`Getting feature '${feature} should throw if no codeql is provided`, async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
const expectedFeatureEnablement = initializeFeatures(true);
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
||||||
await t.throwsAsync(async () => featureEnablement.getValue(feature), {
|
await t.throwsAsync(async () => featureEnablement.getValue(feature), {
|
||||||
|
|
@ -124,7 +145,7 @@ for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
if (feature_flags_1.featureConfig[feature].minimumVersion !== undefined) {
|
if (feature_flags_1.featureConfig[feature].minimumVersion !== undefined) {
|
||||||
(0, ava_1.default)(`Feature '${feature}' is disabled if the minimum CLI version is below ${feature_flags_1.featureConfig[feature].minimumVersion}`, async (t) => {
|
(0, ava_1.default)(`Feature '${feature}' is disabled if the minimum CLI version is below ${feature_flags_1.featureConfig[feature].minimumVersion}`, async (t) => {
|
||||||
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
const expectedFeatureEnablement = initializeFeatures(true);
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
||||||
// feature should be disabled when an old CLI version is set
|
// feature should be disabled when an old CLI version is set
|
||||||
|
|
@ -156,6 +177,25 @@ for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
// An even less likely scenario is that we no longer have any features.
|
// An even less likely scenario is that we no longer have any features.
|
||||||
t.assert(Object.values(feature_flags_1.featureConfig).length > 0, "There should be at least one feature");
|
t.assert(Object.values(feature_flags_1.featureConfig).length > 0, "There should be at least one feature");
|
||||||
});
|
});
|
||||||
|
(0, ava_1.default)("Feature flags are saved to disk", async (t) => {
|
||||||
|
await (0, util_1.withTmpDir)(async (tmpDir) => {
|
||||||
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
|
(0, testing_utils_1.mockFeatureFlagApiEndpoint)(200, expectedFeatureEnablement);
|
||||||
|
const cachedFeatureFlags = path.join(tmpDir, feature_flags_1.FEATURE_FLAGS_FILE_NAME);
|
||||||
|
t.false(fs.existsSync(cachedFeatureFlags), "Feature flag cached file should not exist before getting feature flags");
|
||||||
|
t.true(await featureEnablement.getValue(feature_flags_1.Feature.CliConfigFileEnabled, includeCodeQlIfRequired(feature_flags_1.Feature.CliConfigFileEnabled)), "Feature flag should be enabled initially");
|
||||||
|
t.true(fs.existsSync(cachedFeatureFlags), "Feature flag cached file should exist after getting feature flags");
|
||||||
|
const actualFeatureEnablement = JSON.parse(fs.readFileSync(cachedFeatureFlags, "utf8"));
|
||||||
|
t.deepEqual(actualFeatureEnablement, expectedFeatureEnablement);
|
||||||
|
// now test that we actually use the feature flag cache instead of the server
|
||||||
|
actualFeatureEnablement[feature_flags_1.Feature.CliConfigFileEnabled] = false;
|
||||||
|
fs.writeFileSync(cachedFeatureFlags, JSON.stringify(actualFeatureEnablement));
|
||||||
|
// delete the in memory cache so that we are forced to use the cached file
|
||||||
|
featureEnablement.gitHubFeatureFlags.cachedApiResponse = undefined;
|
||||||
|
t.false(await featureEnablement.getValue(feature_flags_1.Feature.CliConfigFileEnabled, includeCodeQlIfRequired(feature_flags_1.Feature.CliConfigFileEnabled)), "Feature flag should be enabled after reading from cached file");
|
||||||
|
});
|
||||||
|
});
|
||||||
function assertAllFeaturesUndefinedInApi(t, loggedMessages) {
|
function assertAllFeaturesUndefinedInApi(t, loggedMessages) {
|
||||||
for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
for (const feature of Object.keys(feature_flags_1.featureConfig)) {
|
||||||
t.assert(loggedMessages.find((v) => v.type === "debug" &&
|
t.assert(loggedMessages.find((v) => v.type === "debug" &&
|
||||||
|
|
@ -169,9 +209,9 @@ function initializeFeatures(initialValue) {
|
||||||
return features;
|
return features;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
function setUpTests(tmpDir, logger = (0, logging_1.getRunnerLogger)(true), gitHubVersion = { type: util_1.GitHubVariant.DOTCOM }) {
|
function setUpFeatureFlagTests(tmpDir, logger = (0, logging_1.getRunnerLogger)(true), gitHubVersion = { type: util_1.GitHubVariant.DOTCOM }) {
|
||||||
(0, testing_utils_1.setupActionsVars)(tmpDir, tmpDir);
|
(0, testing_utils_1.setupActionsVars)(tmpDir, tmpDir);
|
||||||
return new feature_flags_1.Features(gitHubVersion, testRepositoryNwo, logger);
|
return new feature_flags_1.Features(gitHubVersion, testRepositoryNwo, tmpDir, logger);
|
||||||
}
|
}
|
||||||
function includeCodeQlIfRequired(feature) {
|
function includeCodeQlIfRequired(feature) {
|
||||||
return feature_flags_1.featureConfig[feature].minimumVersion !== undefined
|
return feature_flags_1.featureConfig[feature].minimumVersion !== undefined
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
2
lib/init-action.js
generated
2
lib/init-action.js
generated
|
|
@ -88,7 +88,7 @@ async function run() {
|
||||||
const gitHubVersion = await (0, api_client_1.getGitHubVersion)();
|
const gitHubVersion = await (0, api_client_1.getGitHubVersion)();
|
||||||
(0, util_1.checkGitHubVersionInRange)(gitHubVersion, logger);
|
(0, util_1.checkGitHubVersionInRange)(gitHubVersion, logger);
|
||||||
const repositoryNwo = (0, repository_1.parseRepositoryNwo)((0, util_1.getRequiredEnvParam)("GITHUB_REPOSITORY"));
|
const repositoryNwo = (0, repository_1.parseRepositoryNwo)((0, util_1.getRequiredEnvParam)("GITHUB_REPOSITORY"));
|
||||||
const features = new feature_flags_1.Features(gitHubVersion, repositoryNwo, logger);
|
const features = new feature_flags_1.Features(gitHubVersion, repositoryNwo, (0, actions_util_1.getTemporaryDirectory)(), logger);
|
||||||
try {
|
try {
|
||||||
const workflowErrors = await (0, actions_util_1.validateWorkflow)();
|
const workflowErrors = await (0, actions_util_1.validateWorkflow)();
|
||||||
if (!(await (0, actions_util_1.sendStatusReport)(await (0, actions_util_1.createStatusReportBase)("init", "starting", startedAt, workflowErrors)))) {
|
if (!(await (0, actions_util_1.sendStatusReport)(await (0, actions_util_1.createStatusReportBase)("init", "starting", startedAt, workflowErrors)))) {
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -217,7 +217,12 @@ async function run() {
|
||||||
|
|
||||||
const gitHubVersion = await getGitHubVersion();
|
const gitHubVersion = await getGitHubVersion();
|
||||||
|
|
||||||
const features = new Features(gitHubVersion, repositoryNwo, logger);
|
const features = new Features(
|
||||||
|
gitHubVersion,
|
||||||
|
repositoryNwo,
|
||||||
|
actionsUtil.getTemporaryDirectory(),
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
|
||||||
await runAutobuildIfLegacyGoWorkflow(config, logger);
|
await runAutobuildIfLegacyGoWorkflow(config, logger);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -5,6 +8,7 @@ import {
|
||||||
featureConfig,
|
featureConfig,
|
||||||
FeatureEnablement,
|
FeatureEnablement,
|
||||||
Features,
|
Features,
|
||||||
|
FEATURE_FLAGS_FILE_NAME,
|
||||||
} from "./feature-flags";
|
} from "./feature-flags";
|
||||||
import { getRunnerLogger } from "./logging";
|
import { getRunnerLogger } from "./logging";
|
||||||
import { parseRepositoryNwo } from "./repository";
|
import { parseRepositoryNwo } from "./repository";
|
||||||
|
|
@ -42,7 +46,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
test(`All features are disabled if running against ${variant.description}`, async (t) => {
|
test(`All features are disabled if running against ${variant.description}`, async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
const featureEnablement = setUpTests(
|
const featureEnablement = setUpFeatureFlagTests(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
getRecordingLogger(loggedMessages),
|
getRecordingLogger(loggedMessages),
|
||||||
variant.gitHubVersion
|
variant.gitHubVersion
|
||||||
|
|
@ -72,7 +76,7 @@ for (const variant of ALL_FEATURES_DISABLED_VARIANTS) {
|
||||||
test("API response missing", async (t) => {
|
test("API response missing", async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const loggedMessages: LoggedMessage[] = [];
|
const loggedMessages: LoggedMessage[] = [];
|
||||||
const featureEnablement = setUpTests(
|
const featureEnablement = setUpFeatureFlagTests(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
getRecordingLogger(loggedMessages)
|
getRecordingLogger(loggedMessages)
|
||||||
);
|
);
|
||||||
|
|
@ -94,7 +98,7 @@ test("API response missing", async (t) => {
|
||||||
test("Features are disabled if they're not returned in API response", async (t) => {
|
test("Features are disabled if they're not returned in API response", async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const loggedMessages: LoggedMessage[] = [];
|
const loggedMessages: LoggedMessage[] = [];
|
||||||
const featureEnablement = setUpTests(
|
const featureEnablement = setUpFeatureFlagTests(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
getRecordingLogger(loggedMessages)
|
getRecordingLogger(loggedMessages)
|
||||||
);
|
);
|
||||||
|
|
@ -116,7 +120,7 @@ test("Features are disabled if they're not returned in API response", async (t)
|
||||||
|
|
||||||
test("Feature flags exception is propagated if the API request errors", async (t) => {
|
test("Feature flags exception is propagated if the API request errors", async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
|
||||||
mockFeatureFlagApiEndpoint(500, {});
|
mockFeatureFlagApiEndpoint(500, {});
|
||||||
|
|
||||||
|
|
@ -137,7 +141,7 @@ test("Feature flags exception is propagated if the API request errors", async (t
|
||||||
for (const feature of Object.keys(featureConfig)) {
|
for (const feature of Object.keys(featureConfig)) {
|
||||||
test(`Only feature '${feature}' is enabled if enabled in the API response. Other features disabled`, async (t) => {
|
test(`Only feature '${feature}' is enabled if enabled in the API response. Other features disabled`, async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
|
||||||
// set all features to false except the one we're testing
|
// set all features to false except the one we're testing
|
||||||
const expectedFeatureEnablement: { [feature: string]: boolean } = {};
|
const expectedFeatureEnablement: { [feature: string]: boolean } = {};
|
||||||
|
|
@ -162,7 +166,7 @@ for (const feature of Object.keys(featureConfig)) {
|
||||||
|
|
||||||
test(`Only feature '${feature}' is enabled if the associated environment variable is true. Others disabled.`, async (t) => {
|
test(`Only feature '${feature}' is enabled if the associated environment variable is true. Others disabled.`, async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
|
||||||
const expectedFeatureEnablement = initializeFeatures(false);
|
const expectedFeatureEnablement = initializeFeatures(false);
|
||||||
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
||||||
|
|
@ -188,7 +192,7 @@ for (const feature of Object.keys(featureConfig)) {
|
||||||
|
|
||||||
test(`Feature '${feature}' is disabled if the associated environment variable is false, even if enabled in API`, async (t) => {
|
test(`Feature '${feature}' is disabled if the associated environment variable is false, even if enabled in API`, async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
|
||||||
const expectedFeatureEnablement = initializeFeatures(true);
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
||||||
|
|
@ -215,7 +219,7 @@ for (const feature of Object.keys(featureConfig)) {
|
||||||
if (featureConfig[feature].minimumVersion !== undefined) {
|
if (featureConfig[feature].minimumVersion !== undefined) {
|
||||||
test(`Getting feature '${feature} should throw if no codeql is provided`, async (t) => {
|
test(`Getting feature '${feature} should throw if no codeql is provided`, async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
|
||||||
const expectedFeatureEnablement = initializeFeatures(true);
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
||||||
|
|
@ -233,7 +237,7 @@ for (const feature of Object.keys(featureConfig)) {
|
||||||
if (featureConfig[feature].minimumVersion !== undefined) {
|
if (featureConfig[feature].minimumVersion !== undefined) {
|
||||||
test(`Feature '${feature}' is disabled if the minimum CLI version is below ${featureConfig[feature].minimumVersion}`, async (t) => {
|
test(`Feature '${feature}' is disabled if the minimum CLI version is below ${featureConfig[feature].minimumVersion}`, async (t) => {
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
const featureEnablement = setUpTests(tmpDir);
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
|
||||||
const expectedFeatureEnablement = initializeFeatures(true);
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
||||||
|
|
@ -285,6 +289,57 @@ test("At least one feature has a minimum version specified", (t) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Feature flags are saved to disk", async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
const featureEnablement = setUpFeatureFlagTests(tmpDir);
|
||||||
|
const expectedFeatureEnablement = initializeFeatures(true);
|
||||||
|
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
|
||||||
|
|
||||||
|
const cachedFeatureFlags = path.join(tmpDir, FEATURE_FLAGS_FILE_NAME);
|
||||||
|
|
||||||
|
t.false(
|
||||||
|
fs.existsSync(cachedFeatureFlags),
|
||||||
|
"Feature flag cached file should not exist before getting feature flags"
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
await featureEnablement.getValue(
|
||||||
|
Feature.CliConfigFileEnabled,
|
||||||
|
includeCodeQlIfRequired(Feature.CliConfigFileEnabled)
|
||||||
|
),
|
||||||
|
"Feature flag should be enabled initially"
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
fs.existsSync(cachedFeatureFlags),
|
||||||
|
"Feature flag cached file should exist after getting feature flags"
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualFeatureEnablement = JSON.parse(
|
||||||
|
fs.readFileSync(cachedFeatureFlags, "utf8")
|
||||||
|
);
|
||||||
|
t.deepEqual(actualFeatureEnablement, expectedFeatureEnablement);
|
||||||
|
|
||||||
|
// now test that we actually use the feature flag cache instead of the server
|
||||||
|
actualFeatureEnablement[Feature.CliConfigFileEnabled] = false;
|
||||||
|
fs.writeFileSync(
|
||||||
|
cachedFeatureFlags,
|
||||||
|
JSON.stringify(actualFeatureEnablement)
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete the in memory cache so that we are forced to use the cached file
|
||||||
|
(featureEnablement as any).gitHubFeatureFlags.cachedApiResponse = undefined;
|
||||||
|
|
||||||
|
t.false(
|
||||||
|
await featureEnablement.getValue(
|
||||||
|
Feature.CliConfigFileEnabled,
|
||||||
|
includeCodeQlIfRequired(Feature.CliConfigFileEnabled)
|
||||||
|
),
|
||||||
|
"Feature flag should be enabled after reading from cached file"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function assertAllFeaturesUndefinedInApi(t, loggedMessages: LoggedMessage[]) {
|
function assertAllFeaturesUndefinedInApi(t, loggedMessages: LoggedMessage[]) {
|
||||||
for (const feature of Object.keys(featureConfig)) {
|
for (const feature of Object.keys(featureConfig)) {
|
||||||
t.assert(
|
t.assert(
|
||||||
|
|
@ -305,14 +360,14 @@ function initializeFeatures(initialValue: boolean) {
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpTests(
|
function setUpFeatureFlagTests(
|
||||||
tmpDir: string,
|
tmpDir: string,
|
||||||
logger = getRunnerLogger(true),
|
logger = getRunnerLogger(true),
|
||||||
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion
|
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion
|
||||||
): FeatureEnablement {
|
): FeatureEnablement {
|
||||||
setupActionsVars(tmpDir, tmpDir);
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
|
||||||
return new Features(gitHubVersion, testRepositoryNwo, logger);
|
return new Features(gitHubVersion, testRepositoryNwo, tmpDir, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
function includeCodeQlIfRequired(feature: string) {
|
function includeCodeQlIfRequired(feature: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import { getApiClient } from "./api-client";
|
import { getApiClient } from "./api-client";
|
||||||
import { CodeQL } from "./codeql";
|
import { CodeQL } from "./codeql";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
|
|
@ -55,6 +58,8 @@ export const featureConfig: Record<
|
||||||
*/
|
*/
|
||||||
type GitHubFeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;
|
type GitHubFeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;
|
||||||
|
|
||||||
|
export const FEATURE_FLAGS_FILE_NAME = "feature-flags.json";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the enablement status of a number of features.
|
* Determines the enablement status of a number of features.
|
||||||
* If feature enablement is not able to be determined locally, a request to the
|
* If feature enablement is not able to be determined locally, a request to the
|
||||||
|
|
@ -66,11 +71,13 @@ export class Features implements FeatureEnablement {
|
||||||
constructor(
|
constructor(
|
||||||
gitHubVersion: util.GitHubVersion,
|
gitHubVersion: util.GitHubVersion,
|
||||||
repositoryNwo: RepositoryNwo,
|
repositoryNwo: RepositoryNwo,
|
||||||
|
tempDir: string,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
) {
|
) {
|
||||||
this.gitHubFeatureFlags = new GitHubFeatureFlags(
|
this.gitHubFeatureFlags = new GitHubFeatureFlags(
|
||||||
gitHubVersion,
|
gitHubVersion,
|
||||||
repositoryNwo,
|
repositoryNwo,
|
||||||
|
path.join(tempDir, FEATURE_FLAGS_FILE_NAME),
|
||||||
logger
|
logger
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +127,6 @@ export class Features implements FeatureEnablement {
|
||||||
if (envVar === "true") {
|
if (envVar === "true") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the GitHub API if the feature is enabled.
|
// Ask the GitHub API if the feature is enabled.
|
||||||
return await this.gitHubFeatureFlags.getValue(feature);
|
return await this.gitHubFeatureFlags.getValue(feature);
|
||||||
}
|
}
|
||||||
|
|
@ -130,15 +136,16 @@ class GitHubFeatureFlags implements FeatureEnablement {
|
||||||
private cachedApiResponse: GitHubFeatureFlagsApiResponse | undefined;
|
private cachedApiResponse: GitHubFeatureFlagsApiResponse | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private gitHubVersion: util.GitHubVersion,
|
private readonly gitHubVersion: util.GitHubVersion,
|
||||||
private repositoryNwo: RepositoryNwo,
|
private readonly repositoryNwo: RepositoryNwo,
|
||||||
private logger: Logger
|
private readonly featureFlagsFile: string,
|
||||||
|
private readonly logger: Logger
|
||||||
) {
|
) {
|
||||||
/**/
|
/**/
|
||||||
}
|
}
|
||||||
|
|
||||||
async getValue(feature: Feature): Promise<boolean> {
|
async getValue(feature: Feature): Promise<boolean> {
|
||||||
const response = await this.getApiResponse();
|
const response = await this.getAllFeatures();
|
||||||
if (response === undefined) {
|
if (response === undefined) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`No feature flags API response for ${feature}, considering it disabled.`
|
`No feature flags API response for ${feature}, considering it disabled.`
|
||||||
|
|
@ -155,11 +162,63 @@ class GitHubFeatureFlags implements FeatureEnablement {
|
||||||
return !!featureEnablement;
|
return !!featureEnablement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getApiResponse(): Promise<GitHubFeatureFlagsApiResponse> {
|
private async getAllFeatures(): Promise<GitHubFeatureFlagsApiResponse> {
|
||||||
const apiResponse =
|
// if we have an in memory cache, use that
|
||||||
this.cachedApiResponse || (await this.loadApiResponse());
|
if (this.cachedApiResponse !== undefined) {
|
||||||
this.cachedApiResponse = apiResponse;
|
return this.cachedApiResponse;
|
||||||
return apiResponse;
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readLocalFlags(): Promise<
|
||||||
|
GitHubFeatureFlagsApiResponse | undefined
|
||||||
|
> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeLocalFlags(
|
||||||
|
flags: GitHubFeatureFlagsApiResponse
|
||||||
|
): Promise<void> {
|
||||||
|
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}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadApiResponse() {
|
private async loadApiResponse() {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,12 @@ async function run() {
|
||||||
getRequiredEnvParam("GITHUB_REPOSITORY")
|
getRequiredEnvParam("GITHUB_REPOSITORY")
|
||||||
);
|
);
|
||||||
|
|
||||||
const features = new Features(gitHubVersion, repositoryNwo, logger);
|
const features = new Features(
|
||||||
|
gitHubVersion,
|
||||||
|
repositoryNwo,
|
||||||
|
getTemporaryDirectory(),
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workflowErrors = await validateWorkflow();
|
const workflowErrors = await validateWorkflow();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue