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:
Andrew Eisenberg
2022-11-21 11:14:38 -08:00
parent 4fddc51e4f
commit c29fca48a1
12 changed files with 255 additions and 49 deletions

60
lib/feature-flags.js generated
View File

@@ -19,7 +19,9 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
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 util = __importStar(require("./util"));
var Feature;
@@ -57,14 +59,15 @@ exports.featureConfig = {
minimumVersion: undefined,
},
};
exports.FEATURE_FLAGS_FILE_NAME = "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, logger) {
this.gitHubFeatureFlags = new GitHubFeatureFlags(gitHubVersion, repositoryNwo, logger);
constructor(gitHubVersion, repositoryNwo, tempDir, 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;
class GitHubFeatureFlags {
constructor(gitHubVersion, repositoryNwo, logger) {
constructor(gitHubVersion, repositoryNwo, featureFlagsFile, logger) {
this.gitHubVersion = gitHubVersion;
this.repositoryNwo = repositoryNwo;
this.featureFlagsFile = featureFlagsFile;
this.logger = logger;
/**/
}
async getValue(feature) {
const response = await this.getApiResponse();
const response = await this.getAllFeatures();
if (response === undefined) {
this.logger.debug(`No feature flags API response for ${feature}, considering it disabled.`);
return false;
@@ -127,10 +131,48 @@ class GitHubFeatureFlags {
}
return !!featureEnablement;
}
async getApiResponse() {
const apiResponse = this.cachedApiResponse || (await this.loadApiResponse());
this.cachedApiResponse = apiResponse;
return apiResponse;
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