Merge pull request #3477 from github/mbg/features/offline-features

This commit is contained in:
Michael B. Gale
2026-02-20 15:36:07 +00:00
committed by GitHub
21 changed files with 945 additions and 516 deletions
+3 -3
View File
@@ -12,7 +12,7 @@ import {
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import { setupTests } from "./testing-utils";
import { mockCCR, setupTests } from "./testing-utils";
import { initializeEnvironment } from "./util";
setupTests(test);
@@ -258,8 +258,8 @@ test("isDynamicWorkflow() returns true if event name is `dynamic`", (t) => {
});
test("isCCR() returns true when expected", (t) => {
process.env.GITHUB_EVENT_NAME = "dynamic";
process.env[EnvVar.ANALYSIS_KEY] = "dynamic/copilot-pull-request-reviewer";
mockCCR();
t.assert(isCCR());
t.false(isDefaultSetup());
});
+2 -2
View File
@@ -30,7 +30,7 @@ import {
} from "./dependency-caching";
import { getDiffInformedAnalysisBranches } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { Features } from "./feature-flags";
import { initFeatures } from "./feature-flags";
import { KnownLanguage } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { cleanupAndUploadOverlayBaseDatabaseToCache } from "./overlay-database-utils";
@@ -293,7 +293,7 @@ async function run(startedAt: Date) {
util.checkActionVersion(actionsUtil.getActionVersion(), gitHubVersion);
const features = new Features(
const features = initFeatures(
gitHubVersion,
repositoryNwo,
actionsUtil.getTemporaryDirectory(),
+2 -2
View File
@@ -6,7 +6,7 @@ import { CodeQL, getCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar } from "./environment";
import { Feature, featureConfig, Features } from "./feature-flags";
import { Feature, featureConfig, initFeatures } from "./feature-flags";
import { KnownLanguage, Language } from "./languages";
import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
@@ -117,7 +117,7 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
const featureName = "C++ automatic installation of dependencies";
const gitHubVersion = await getGitHubVersion();
const repositoryNwo = getRepositoryNwo();
const features = new Features(
const features = initFeatures(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),
+2 -2
View File
@@ -8,7 +8,7 @@ import {
shouldPerformDiffInformedAnalysis,
exportedForTesting,
} from "./diff-informed-analysis-utils";
import { Feature, Features } from "./feature-flags";
import { Feature, initFeatures } from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
@@ -63,7 +63,7 @@ const testShouldPerformDiffInformedAnalysis = test.macro({
delete process.env.CODEQL_ACTION_DIFF_INFORMED_QUERIES;
}
const features = new Features(
const features = initFeatures(
testCase.gitHubVersion,
parseRepositoryNwo("github/example"),
tmpDir,
+25 -110
View File
@@ -1,33 +1,32 @@
import * as fs from "fs";
import * as path from "path";
import test, { ExecutionContext } from "ava";
import test from "ava";
import * as defaults from "./defaults.json";
import { EnvVar } from "./environment";
import {
Feature,
featureConfig,
FeatureEnablement,
Features,
FEATURE_FLAGS_FILE_NAME,
FeatureConfig,
FeatureWithoutCLI,
} from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
setUpFeatureFlagTests,
getFeatureIncludingCodeQlIfRequired,
assertAllFeaturesUndefinedInApi,
assertAllFeaturesHaveDefaultValues,
} from "./feature-flags/testing-util";
import {
checkExpectedLogMessages,
getRecordingLogger,
initializeFeatures,
LoggedMessage,
mockCCR,
mockCodeQLVersion,
mockFeatureFlagApiEndpoint,
setupActionsVars,
setupTests,
stubFeatureFlagApiEndpoint,
} from "./testing-utils";
import { ToolsFeature } from "./tools-features";
import * as util from "./util";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";
setupTests(test);
@@ -36,11 +35,9 @@ test.beforeEach(() => {
initializeEnvironment("1.2.3");
});
const testRepositoryNwo = parseRepositoryNwo("github/example");
test(`All features use default values if running against GHES`, async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages: LoggedMessage[] = [];
const loggedMessages = [];
const features = setUpFeatureFlagTests(
tmpDir,
getRecordingLogger(loggedMessages),
@@ -48,30 +45,9 @@ test(`All features use default values if running against GHES`, async (t) => {
);
await assertAllFeaturesHaveDefaultValues(t, features);
assertLoggedMessage(
t,
loggedMessages,
checkExpectedLogMessages(t, loggedMessages, [
"Not running against github.com. Using default values for all features.",
);
});
});
test(`All features use default values if running in CCR`, async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages: LoggedMessage[] = [];
const features = setUpFeatureFlagTests(
tmpDir,
getRecordingLogger(loggedMessages),
);
process.env[EnvVar.ANALYSIS_KEY] = "dynamic/copilot-pull-request-reviewer";
await assertAllFeaturesHaveDefaultValues(t, features);
assertLoggedMessage(
t,
loggedMessages,
"Feature flags are not supported in Copilot Code Review. Using default values for all features.",
);
]);
});
});
@@ -553,79 +529,18 @@ test("non-legacy feature flags should not start with codeql_action_", async (t)
}
});
async function assertAllFeaturesHaveDefaultValues(
t: ExecutionContext<unknown>,
features: FeatureEnablement,
) {
for (const feature of Object.values(Feature)) {
t.deepEqual(
await getFeatureIncludingCodeQlIfRequired(features, feature),
featureConfig[feature].defaultValue,
);
}
}
test("initFeatures returns a `Features` instance by default", async (t) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
t.is("Features", features.constructor.name);
});
});
function assertLoggedMessage(
t: ExecutionContext<unknown>,
loggedMessages: LoggedMessage[],
expectedMessage: string,
) {
t.assert(
loggedMessages.find(
(v: LoggedMessage) => v.type === "debug" && v.message === expectedMessage,
) !== undefined,
);
}
test("initFeatures returns an `OfflineFeatures` instance in CCR", async (t) => {
await withTmpDir(async (tmpDir) => {
mockCCR();
function assertAllFeaturesUndefinedInApi(
t: ExecutionContext<unknown>,
loggedMessages: LoggedMessage[],
) {
for (const feature of Object.keys(featureConfig)) {
t.assert(
loggedMessages.find(
(v) =>
v.type === "debug" &&
(v.message as string).includes(feature) &&
(v.message as string).includes("undefined in API response"),
) !== undefined,
);
}
}
function setUpFeatureFlagTests(
tmpDir: string,
logger = getRunnerLogger(true),
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion,
): FeatureEnablement {
setupActionsVars(tmpDir, tmpDir);
return new Features(gitHubVersion, testRepositoryNwo, tmpDir, logger);
}
/**
* Returns an argument to pass to `getValue` that if required includes a CodeQL object meeting the
* minimum version or tool feature requirements specified by the feature.
*/
function getFeatureIncludingCodeQlIfRequired(
features: FeatureEnablement,
feature: Feature,
) {
const config = featureConfig[
feature
] satisfies FeatureConfig as FeatureConfig;
if (
config.minimumVersion === undefined &&
config.toolsFeature === undefined
) {
return features.getValue(feature as FeatureWithoutCLI);
}
return features.getValue(
feature,
mockCodeQLVersion(
"9.9.9",
Object.fromEntries(Object.values(ToolsFeature).map((v) => [v, true])),
),
);
}
const features = setUpFeatureFlagTests(tmpDir);
t.is("OfflineFeatures", features.constructor.name);
});
});
+125 -62
View File
@@ -373,51 +373,60 @@ type GitHubFeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;
export const 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.
* Determines the enablement status of a number of features locally without
* consulting the GitHub API.
*/
export class Features implements FeatureEnablement {
private gitHubFeatureFlags: GitHubFeatureFlags;
constructor(
gitHubVersion: util.GitHubVersion,
repositoryNwo: RepositoryNwo,
tempDir: string,
private readonly logger: Logger,
) {
this.gitHubFeatureFlags = new GitHubFeatureFlags(
gitHubVersion,
repositoryNwo,
path.join(tempDir, FEATURE_FLAGS_FILE_NAME),
logger,
);
}
class OfflineFeatures implements FeatureEnablement {
constructor(protected readonly logger: Logger) {}
async getDefaultCliVersion(
variant: util.GitHubVariant,
_variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
};
}
/**
* Gets the `FeatureConfig` for `feature`.
*/
getFeatureConfig(feature: Feature): FeatureConfig {
// Narrow the type to FeatureConfig to avoid type errors. To avoid unsafe use of `as`, we
// check that the required properties exist using `satisfies`.
return featureConfig[feature] satisfies FeatureConfig as FeatureConfig;
}
/**
* Determines whether `feature` is enabled without consulting the GitHub API.
*
* @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
* disabled. If not provided, and a `minimumVersion` is specified for the feature, then
* 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: Feature, codeql?: CodeQL): Promise<boolean> {
// Narrow the type to FeatureConfig to avoid type errors. To avoid unsafe use of `as`, we
// check that the required properties exist using `satisfies`.
const config = featureConfig[
feature
] satisfies FeatureConfig as FeatureConfig;
const offlineValue = await this.getOfflineValue(feature, codeql);
if (offlineValue !== undefined) {
return offlineValue;
}
return this.getDefaultValue(feature);
}
/**
* Determines whether `feature` is enabled using the CLI and environment variables.
*/
protected async getOfflineValue(
feature: Feature,
codeql?: CodeQL,
): Promise<boolean | undefined> {
const config = this.getFeatureConfig(feature);
if (!codeql && config.minimumVersion) {
throw new Error(
@@ -483,6 +492,68 @@ export class Features implements FeatureEnablement {
return true;
}
return undefined;
}
/** Gets the default value of `feature`. */
protected async getDefaultValue(feature: Feature): Promise<boolean> {
const config = this.getFeatureConfig(feature);
const defaultValue = config.defaultValue;
this.logger.debug(
`Feature ${feature} is ${
defaultValue ? "enabled" : "disabled"
} due to its default value.`,
);
return defaultValue;
}
}
/**
* 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 extends OfflineFeatures {
private gitHubFeatureFlags: GitHubFeatureFlags;
constructor(repositoryNwo: RepositoryNwo, tempDir: string, logger: Logger) {
super(logger);
this.gitHubFeatureFlags = new GitHubFeatureFlags(
repositoryNwo,
path.join(tempDir, FEATURE_FLAGS_FILE_NAME),
logger,
);
}
async getDefaultCliVersion(
variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
if (supportsFeatureFlags(variant)) {
return await this.gitHubFeatureFlags.getDefaultCliVersionFromFlags();
}
return super.getDefaultCliVersion(variant);
}
/**
*
* @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, then
* 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: Feature, codeql?: CodeQL): Promise<boolean> {
// Check whether the feature is enabled locally.
const offlineValue = await this.getOfflineValue(feature, codeql);
if (offlineValue !== undefined) {
return offlineValue;
}
// Ask the GitHub API if the feature is enabled.
const apiValue = await this.gitHubFeatureFlags.getValue(feature);
if (apiValue !== undefined) {
@@ -494,13 +565,8 @@ export class Features implements FeatureEnablement {
return apiValue;
}
const defaultValue = config.defaultValue;
this.logger.debug(
`Feature ${feature} is ${
defaultValue ? "enabled" : "disabled"
} due to its default value.`,
);
return defaultValue;
// Return the default value.
return this.getDefaultValue(feature);
}
}
@@ -512,7 +578,6 @@ class GitHubFeatureFlags {
private hasAccessedRemoteFeatureFlags: boolean;
constructor(
private readonly gitHubVersion: util.GitHubVersion,
private readonly repositoryNwo: RepositoryNwo,
private readonly featureFlagsFile: string,
private readonly logger: Logger,
@@ -543,18 +608,6 @@ class GitHubFeatureFlags {
return version;
}
async getDefaultCliVersion(
variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
if (supportsFeatureFlags(variant)) {
return await this.getDefaultCliVersionFromFlags();
}
return {
cliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
};
}
async getDefaultCliVersionFromFlags(): Promise<CodeQLDefaultVersionInfo> {
const response = await this.getAllFeatures();
@@ -680,21 +733,6 @@ class GitHubFeatureFlags {
}
private async loadApiResponse(): Promise<GitHubFeatureFlagsApiResponse> {
// Do nothing when not running against github.com
if (!supportsFeatureFlags(this.gitHubVersion.type)) {
this.logger.debug(
"Not running against github.com. Using default values for all features.",
);
this.hasAccessedRemoteFeatureFlags = false;
return {};
}
if (isCCR()) {
this.logger.debug(
"Feature flags are not supported in Copilot Code Review. Using default values for all features.",
);
this.hasAccessedRemoteFeatureFlags = false;
return {};
}
try {
const featuresToRequest = Object.entries(featureConfig)
.filter(
@@ -764,3 +802,28 @@ function supportsFeatureFlags(githubVariant: util.GitHubVariant): boolean {
githubVariant === util.GitHubVariant.GHEC_DR
);
}
/**
* Initialises an instance of a `FeatureEnablement` implementation. The implementation used
* is determined by the environment we are running in.
*/
export function initFeatures(
gitHubVersion: util.GitHubVersion,
repositoryNwo: RepositoryNwo,
tempDir: string,
logger: Logger,
): FeatureEnablement {
if (!supportsFeatureFlags(gitHubVersion.type)) {
logger.debug(
"Not running against github.com. Using default values for all features.",
);
return new OfflineFeatures(logger);
} else if (isCCR()) {
logger.debug(
"Querying feature flags is not currently supported in Copilot Code Review. Using offline data for all features.",
);
return new OfflineFeatures(logger);
} else {
return new Features(repositoryNwo, tempDir, logger);
}
}
@@ -0,0 +1,42 @@
import test from "ava";
import * as sinon from "sinon";
import * as apiClient from "../api-client";
import {
checkExpectedLogMessages,
getRecordingLogger,
LoggedMessage,
mockCCR,
setupTests,
} from "../testing-utils";
import { initializeEnvironment, withTmpDir } from "../util";
import {
assertAllFeaturesHaveDefaultValues,
setUpFeatureFlagTests,
} from "./testing-util";
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
mockCCR();
});
test("OfflineFeatures makes no API requests", async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const features = setUpFeatureFlagTests(tmpDir, logger);
t.is("OfflineFeatures", features.constructor.name);
sinon
.stub(apiClient, "getApiClient")
.throws(new Error("Should not have called getApiClient"));
await assertAllFeaturesHaveDefaultValues(t, features);
checkExpectedLogMessages(t, loggedMessages, [
"Querying feature flags is not currently supported in Copilot Code Review. Using offline data for all features.",
]);
});
});
+87
View File
@@ -0,0 +1,87 @@
import { type ExecutionContext } from "ava";
import {
Feature,
featureConfig,
FeatureConfig,
FeatureEnablement,
FeatureWithoutCLI,
initFeatures,
} from "../feature-flags";
import { getRunnerLogger } from "../logging";
import { parseRepositoryNwo } from "../repository";
import {
LoggedMessage,
mockCodeQLVersion,
setupActionsVars,
} from "../testing-utils";
import { ToolsFeature } from "../tools-features";
import { GitHubVariant } from "../util";
import * as util from "../util";
const testRepositoryNwo = parseRepositoryNwo("github/example");
export async function assertAllFeaturesHaveDefaultValues(
t: ExecutionContext<unknown>,
features: FeatureEnablement,
) {
for (const feature of Object.values(Feature)) {
t.deepEqual(
await getFeatureIncludingCodeQlIfRequired(features, feature),
featureConfig[feature].defaultValue,
);
}
}
export function assertAllFeaturesUndefinedInApi(
t: ExecutionContext<unknown>,
loggedMessages: LoggedMessage[],
) {
for (const feature of Object.keys(featureConfig)) {
t.assert(
loggedMessages.find(
(v) =>
v.type === "debug" &&
(v.message as string).includes(feature) &&
(v.message as string).includes("undefined in API response"),
) !== undefined,
);
}
}
export function setUpFeatureFlagTests(
tmpDir: string,
logger = getRunnerLogger(true),
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion,
): FeatureEnablement {
setupActionsVars(tmpDir, tmpDir);
return initFeatures(gitHubVersion, testRepositoryNwo, tmpDir, logger);
}
/**
* Returns an argument to pass to `getValue` that if required includes a CodeQL object meeting the
* minimum version or tool feature requirements specified by the feature.
*/
export function getFeatureIncludingCodeQlIfRequired(
features: FeatureEnablement,
feature: Feature,
) {
const config = featureConfig[
feature
] satisfies FeatureConfig as FeatureConfig;
if (
config.minimumVersion === undefined &&
config.toolsFeature === undefined
) {
return features.getValue(feature as FeatureWithoutCLI);
}
return features.getValue(
feature,
mockCodeQLVersion(
"9.9.9",
Object.fromEntries(Object.values(ToolsFeature).map((v) => [v, true])),
),
);
}
+2 -2
View File
@@ -21,7 +21,7 @@ import {
getDependencyCacheUsage,
} from "./dependency-caching";
import { EnvVar } from "./environment";
import { Features } from "./feature-flags";
import { initFeatures } from "./feature-flags";
import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper";
import { getActionsLogger } from "./logging";
@@ -62,7 +62,7 @@ async function run(startedAt: Date) {
checkGitHubVersionInRange(gitHubVersion, logger);
const repositoryNwo = getRepositoryNwo();
const features = new Features(
const features = initFeatures(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),
+3 -3
View File
@@ -38,7 +38,7 @@ import {
makeTelemetryDiagnostic,
} from "./diagnostics";
import { EnvVar } from "./environment";
import { Feature, FeatureEnablement, Features } from "./feature-flags";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import {
loadPropertiesFromApi,
RepositoryProperties,
@@ -211,7 +211,7 @@ async function run(startedAt: Date) {
let config: configUtils.Config | undefined;
let configFile: string | undefined;
let codeql: CodeQL;
let features: Features;
let features: FeatureEnablement;
let sourceRoot: string;
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
let toolsFeatureFlagsValid: boolean | undefined;
@@ -238,7 +238,7 @@ async function run(startedAt: Date) {
const repositoryNwo = getRepositoryNwo();
features = new Features(
features = initFeatures(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),
+2 -2
View File
@@ -10,7 +10,7 @@ import {
import { getGitHubVersion } from "./api-client";
import { CodeQL } from "./codeql";
import { EnvVar } from "./environment";
import { Features } from "./feature-flags";
import { initFeatures } from "./feature-flags";
import { initCodeQL } from "./init";
import { getActionsLogger, Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
@@ -114,7 +114,7 @@ async function run(startedAt: Date): Promise<void> {
const repositoryNwo = getRepositoryNwo();
const features = new Features(
const features = initFeatures(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),
+3 -3
View File
@@ -5,7 +5,7 @@ import * as core from "@actions/core";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { Feature, Features } from "./feature-flags";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import { KnownLanguage } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
@@ -32,7 +32,7 @@ async function run(startedAt: Date) {
// possible, and only use safe functions outside.
const logger = getActionsLogger();
let features: Features | undefined;
let features: FeatureEnablement | undefined;
let language: KnownLanguage | undefined;
try {
@@ -47,7 +47,7 @@ async function run(startedAt: Date) {
// Initialise FFs.
const repositoryNwo = getRepositoryNwo();
const gitHubVersion = await getGitHubVersion();
features = new Features(
features = initFeatures(
gitHubVersion,
repositoryNwo,
actionsUtil.getTemporaryDirectory(),
+7
View File
@@ -14,6 +14,7 @@ import { CachingKind } from "./caching-utils";
import * as codeql from "./codeql";
import { Config } from "./config-utils";
import * as defaults from "./defaults.json";
import { EnvVar } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -504,3 +505,9 @@ export function makeTestToken(length: number = 36) {
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return chars.repeat(Math.ceil(length / chars.length)).slice(0, length);
}
/** Sets the environment variables needed for isCCR() to be `true`. */
export function mockCCR() {
process.env.GITHUB_EVENT_NAME = "dynamic";
process.env[EnvVar.ANALYSIS_KEY] = "dynamic/copilot-pull-request-reviewer";
}
+2 -2
View File
@@ -4,7 +4,7 @@ import * as actionsUtil from "./actions-util";
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses";
import { getGitHubVersion } from "./api-client";
import { Features } from "./feature-flags";
import { initFeatures } from "./feature-flags";
import { Logger, getActionsLogger } from "./logging";
import { getRepositoryNwo } from "./repository";
import {
@@ -70,7 +70,7 @@ async function run(startedAt: Date) {
actionsUtil.persistInputs();
const repositoryNwo = getRepositoryNwo();
const features = new Features(
const features = initFeatures(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),