diff --git a/src/AWSLambdas.Api.WorkerHost/appsettings.Deploy.json b/src/AWSLambdas.Api.WorkerHost/appsettings.Deploy.json
index add412f3..5792d284 100644
--- a/src/AWSLambdas.Api.WorkerHost/appsettings.Deploy.json
+++ b/src/AWSLambdas.Api.WorkerHost/appsettings.Deploy.json
@@ -3,8 +3,8 @@
"Notes": "Lists the required configuration keys that must be overwritten (by GitHub action) when we deploy this host",
"Required": [
{
- "description": "AWS specific settings from appsettings.json",
- "keys": [
+ "Description": "AWS specific settings from appsettings.json",
+ "Keys": [
"Hosts:ApiHost1:BaseUrl",
"Hosts:ApiHost1:HMACAuthNSecret",
"Hosts:AncillaryApi:BaseUrl",
diff --git a/src/ApiHost1/appsettings.Deploy.json b/src/ApiHost1/appsettings.Deploy.json
index 309a842e..86c9fbc5 100644
--- a/src/ApiHost1/appsettings.Deploy.json
+++ b/src/ApiHost1/appsettings.Deploy.json
@@ -3,10 +3,24 @@
"Notes": "Lists the required configuration keys that must be overwritten (by the GitHub configuration action) when we deploy this host",
"Required": [
{
- "description": "General settings from appsettings.json",
- "keys": [
- "ApplicationServices:Persistence:Kurrent:ConnectionString",
+ "Description": "General settings from appsettings.json",
+ "Keys": [
"ApplicationServices:SSOProvidersService:SSOUserTokens:AesSecret",
+ "ApplicationServices:Gravatar:BaseUrl",
+ "Hosts:AncillaryApi:BaseUrl",
+ "Hosts:AncillaryApi:HMACAuthNSecret",
+ "Hosts:IdentityApi:BaseUrl",
+ "Hosts:IdentityApi:JWT:SigningSecret",
+ "Hosts:ImagesApi:BaseUrl",
+ "Hosts:EndUsersApi:Authorization:OperatorWhitelist",
+ "Hosts:WebsiteHost:BaseUrl"
+ ]
+ },
+ {
+ "Description": "General settings from appsettings.json for specific optional technology adapters",
+ "Disabled": true,
+ "Keys": [
+ "ApplicationServices:Persistence:Kurrent:ConnectionString",
"ApplicationServices:Chargebee:BaseUrl",
"ApplicationServices:Chargebee:ApiKey",
"ApplicationServices:Chargebee:SiteName",
@@ -19,7 +33,6 @@
"ApplicationServices:Chargebee:Webhook:Password",
"ApplicationServices:Flagsmith:BaseUrl",
"ApplicationServices:Flagsmith:EnvironmentKey",
- "ApplicationServices:Gravatar:BaseUrl",
"ApplicationServices:Mailgun:BaseUrl",
"ApplicationServices:Mailgun:DomainName",
"ApplicationServices:Mailgun:ApiKey",
@@ -30,26 +43,25 @@
"ApplicationServices:Twilio:SenderPhoneNumber",
"ApplicationServices:Twilio:WebhookCallbackUrl",
"ApplicationServices:UserPilot:BaseUrl",
- "ApplicationServices:UserPilot:ApiKey",
- "Hosts:AncillaryApi:BaseUrl",
- "Hosts:AncillaryApi:HMACAuthNSecret",
- "Hosts:IdentityApi:BaseUrl",
- "Hosts:IdentityApi:JWT:SigningSecret",
- "Hosts:ImagesApi:BaseUrl",
- "Hosts:EndUsersApi:Authorization:OperatorWhitelist",
- "Hosts:WebsiteHost:BaseUrl"
+ "ApplicationServices:UserPilot:ApiKey"
]
},
{
- "description": "Azure specific settings from appsettings.Azure.json",
- "keys": [
+ "Description": "Azure specific settings from appsettings.Azure.json",
+ "Keys": [
"ApplicationInsights:ConnectionString",
"ApplicationServices:Persistence:AzureStorageAccount:AccountName",
"ApplicationServices:Persistence:AzureStorageAccount:AccountKey",
"ApplicationServices:Persistence:AzureServiceBus:ConnectionString",
"ApplicationServices:Persistence:SqlServer:DbServerName",
"ApplicationServices:Persistence:SqlServer:DbCredentials",
- "ApplicationServices:Persistence:SqlServer:DbName",
+ "ApplicationServices:Persistence:SqlServer:DbName"
+ ]
+ },
+ {
+ "Description": "Azure specific settings from appsettings.Azure.json for specific optional technology adapters",
+ "Disabled": true,
+ "Keys": [
"ApplicationServices:MicrosoftIdentity:BaseUrl",
"ApplicationServices:MicrosoftIdentity:ClientId",
"ApplicationServices:MicrosoftIdentity:ClientSecret",
@@ -57,9 +69,9 @@
]
},
{
- "description": "AWS specific settings from appsettings.AWS.json",
- "disabled": true,
- "keys": [
+ "Description": "AWS specific settings from appsettings.AWS.json",
+ "Disabled": true,
+ "Keys": [
"ApplicationServices:Persistence:AWS:AccessKey",
"ApplicationServices:Persistence:AWS:SecretKey",
"ApplicationServices:Persistence:AWS:Region",
diff --git a/src/AzureFunctions.Api.WorkerHost/appsettings.Deploy.json b/src/AzureFunctions.Api.WorkerHost/appsettings.Deploy.json
index 85849ebd..8accab6f 100644
--- a/src/AzureFunctions.Api.WorkerHost/appsettings.Deploy.json
+++ b/src/AzureFunctions.Api.WorkerHost/appsettings.Deploy.json
@@ -3,8 +3,8 @@
"Notes": "Lists the required configuration keys that must be overwritten (by GitHub action) when we deploy this host",
"Required": [
{
- "description": "Azure specific settings from appsettings.json",
- "keys": [
+ "Description": "Azure specific settings from appsettings.json",
+ "Keys": [
"Hosts:ApiHost1:BaseUrl",
"Hosts:ApiHost1:HMACAuthNSecret",
"Hosts:AncillaryApi:BaseUrl",
diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings
index d5e1aa05..6f0d2928 100644
--- a/src/SaaStack.sln.DotSettings
+++ b/src/SaaStack.sln.DotSettings
@@ -1523,6 +1523,7 @@ public void When$condition$_Then$outcome$()
True
True
True
+ True
True
True
True
diff --git a/src/Tools.GitHubActions/VariableSubstitution/README.md b/src/Tools.GitHubActions/VariableSubstitution/README.md
index cc097e09..2adee731 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/README.md
+++ b/src/Tools.GitHubActions/VariableSubstitution/README.md
@@ -19,7 +19,7 @@ This action also relies on the variables/secrets defined in the GitHub project,
### Source Code
-This action depends on the definition of a set of "required" settings defined in one of your `appsettings.json` files.
+This action depends on the definition of a set of "Required" settings defined in one of your `appsettings.json` files.
For example,
@@ -29,15 +29,15 @@ For example,
"Notes": "Lists the required configuration keys that must be overwritten (by the GitHub configuration action) when we deploy this host",
"Required": [
{
- "description": "General settings from appsettings.json",
- "keys": [
+ "Description": "General settings from appsettings.json",
+ "Keys": [
"ApplicationServices:Persistence:Kurrent:ConnectionString",
"Hosts:WebsiteHost:BaseUrl"
]
},
{
- "description": "Azure specific settings from appsettings.Azure.json",
- "keys": [
+ "Description": "Azure specific settings from appsettings.Azure.json",
+ "Keys": [
"ApplicationInsights:ConnectionString",
"ApplicationServices:Persistence:SqlServer:DbServerName",
"ApplicationServices:Persistence:SqlServer:DbCredentials",
@@ -45,9 +45,9 @@ For example,
]
},
{
- "description": "AWS specific settings from appsettings.AWS.json",
- "disabled": true,
- "keys": [
+ "Description": "AWS specific settings from appsettings.AWS.json",
+ "Disabled": true,
+ "Keys": [
"ApplicationServices:Persistence:AWS:AccessKey",
"ApplicationServices:Persistence:AWS:SecretKey",
"ApplicationServices:Persistence:AWS:Region",
@@ -58,9 +58,10 @@ For example,
}
}
```
-> Note: These definitions are assumed exist in your `appsettings.json` files. However, they can also exist in separate files, just used for deployment use. e.g., `appsettings.Deploy.json`.
-> Note: without these definitions, this action just performs variable substitution, without any verification.
+> Note: These definitions are assumed exist within your main `appsettings.json` file. Or they can also exist in separate files, just used for deployment use. e.g., `appsettings.Deploy.json`.
+> Note: Keys where `Disabled` is set to `true` will be ignored.
+> Note: without any of these "Keys" definitions, this action will just perform variable substitution, without any verification.
### GitHub Project
diff --git a/src/Tools.GitHubActions/VariableSubstitution/src/appSettingsJsonFileReader.spec.ts b/src/Tools.GitHubActions/VariableSubstitution/src/appSettingsJsonFileReader.spec.ts
index e334f967..e81e4dcc 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/src/appSettingsJsonFileReader.spec.ts
+++ b/src/Tools.GitHubActions/VariableSubstitution/src/appSettingsJsonFileReader.spec.ts
@@ -21,7 +21,7 @@ describe('AppSettingsJsonFileReader', () => {
try {
await reader.readAppSettingsFile(path);
} catch (error) {
- expect(error.message).toMatch(`File '${path}' does not contain valid JSON: SyntaxError: Unexpected token 'i', \"invalid\" is not valid JSON`);
+ expect(error.message).toContain(`File '${path}' does not contain valid JSON: SyntaxError: Unexpected token`);
}
});
diff --git a/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.spec.ts b/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.spec.ts
index 332e0402..60857e34 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.spec.ts
+++ b/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.spec.ts
@@ -26,7 +26,7 @@ describe('ConfigurationSets', () => {
expect(sets.hasNone).toBe(true);
expect(globParser.parseFiles).toHaveBeenCalledWith([]);
expect(jsonFileReader.readAppSettingsFile).not.toHaveBeenCalled();
- expect(logger.warning).toHaveBeenCalledWith('No settings files found in this repository, using the glob patterns: ');
+ expect(logger.warning).toHaveBeenCalledWith('No settings files found in this repository, applying the glob patterns: ');
});
it('should create a single set, when has one file at the root', async () => {
@@ -123,11 +123,23 @@ describe('ConfigurationSets', () => {
jsonFileReader.readAppSettingsFile
.mockResolvedValueOnce({
"aname1": "avalue",
- "Required": ["arequired1", "arequired2"]
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": ["arequired1", "arequired2"]
+ }
+ ]
+ }
})
.mockResolvedValueOnce({
"aname2": "avalue",
- "Required": ["arequired2", "arequired3"]
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": ["arequired2", "arequired3"]
+ }
+ ]
+ }
});
const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
@@ -159,7 +171,7 @@ describe('ConfigurationSets', () => {
const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
- const result = sets.verifyConfiguration();
+ const result = sets.verifyConfiguration({}, {});
expect(result).toBe(true)
});
@@ -179,13 +191,13 @@ describe('ConfigurationSets', () => {
const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
- const result = sets.verifyConfiguration();
+ const result = sets.verifyConfiguration({}, {});
expect(result).toBe(true);
- expect(logger.info).toHaveBeenCalledWith(`Verification of host 'apath' completed successfully`);
+ expect(logger.info).toHaveBeenCalledWith(`Verifying settings files in host: 'apath' -> Successful!`);
});
- it('should return false, when the set contains required variable, and not exists', async () => {
+ it('should return true, but warn, when the set defines required variable, but no variable exists to substitute', async () => {
const globParser: jest.Mocked = {
parseFiles: jest.fn(_matches => Promise.resolve(["apath/afile1.json"])),
@@ -196,20 +208,57 @@ describe('ConfigurationSets', () => {
jsonFileReader.readAppSettingsFile
.mockResolvedValueOnce({
"aname": "avalue",
- "Required": ["arequired"]
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": ["arequired"]
+ }
+ ]
+ }
});
const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
- const result = sets.verifyConfiguration();
+ const result = sets.verifyConfiguration({}, {});
+
+ expect(result).toBe(true);
+ expect(logger.warning).toHaveBeenCalledWith(`Required variable 'arequired' is not yet defined in any of the settings files of this host! Consider either defining it in one of the settings files of this host, OR remove it from the 'Required' section of the settings files in this host`);
+ expect(logger.info).toHaveBeenCalledWith(`Verifying settings files in host: 'apath' -> Successful!`);
+ });
+
+ it('should return false, when the set defines required variable, but no variable/secret exists in GitHub', async () => {
+
+ const globParser: jest.Mocked = {
+ parseFiles: jest.fn(_matches => Promise.resolve(["apath/afile1.json"])),
+ };
+ const jsonFileReader: jest.Mocked = {
+ readAppSettingsFile: jest.fn()
+ };
+ jsonFileReader.readAppSettingsFile
+ .mockResolvedValueOnce({
+ "arequired-arequired": {
+ "aname": "avalue"
+ },
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": ["arequired-arequired:aname"]
+ }
+ ]
+ }
+ });
+
+ const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
+
+ const result = sets.verifyConfiguration({}, {});
expect(result).toBe(false);
expect(logger.info).not.toHaveBeenCalledWith(`Verification of host 'apath' completed successfully`);
- expect(logger.error).toHaveBeenCalledWith(`Required variable 'arequired' is not defined in any of the settings files of this host!`);
- expect(logger.error).toHaveBeenCalledWith(`Verification of host 'apath' failed, there is at least one missing required variable!`);
+ expect(logger.error).toHaveBeenCalledWith(`Required GitHub environment variable (or secret) 'AREQUIRED_AREQUIRED_ANAME' (alias: 'arequired-arequired:aname') has not been defined in the environment variables (or secrets) of this GitHub project!`);
+ expect(logger.error).toHaveBeenCalledWith(`Verification settings files in host: 'apath' -> Failed! there is at least one missing required environment variable or secret in this GitHub project`);
});
- it('should return true, when the set contains required variable, and exists', async () => {
+ it('should return true, when the set defines required variable, and variable exists in GitHub', async () => {
const globParser: jest.Mocked = {
parseFiles: jest.fn(_matches => Promise.resolve(["apath/afile1.json"])),
@@ -219,17 +268,54 @@ describe('ConfigurationSets', () => {
};
jsonFileReader.readAppSettingsFile
.mockResolvedValueOnce({
- "aname": "avalue",
- "Required": ["aname"]
+ "arequired-arequired": {
+ "aname": "avalue"
+ },
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": ["arequired-arequired:aname"]
+ }
+ ]
+ }
});
const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
- const result = sets.verifyConfiguration();
+ const result = sets.verifyConfiguration({"AREQUIRED_AREQUIRED_ANAME": "avalue"}, {});
expect(result).toBe(true);
- expect(logger.info).toHaveBeenCalledWith(`Verification of host 'apath' completed successfully`);
+ expect(logger.info).toHaveBeenCalledWith(`Verifying settings files in host: 'apath' -> Successful!`);
});
+ it('should return true, when the set defines required variable, and secret exists in GitHub', async () => {
+
+ const globParser: jest.Mocked = {
+ parseFiles: jest.fn(_matches => Promise.resolve(["apath/afile1.json"])),
+ };
+ const jsonFileReader: jest.Mocked = {
+ readAppSettingsFile: jest.fn()
+ };
+ jsonFileReader.readAppSettingsFile
+ .mockResolvedValueOnce({
+ "arequired-arequired": {
+ "aname": "avalue"
+ },
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": ["arequired-arequired:aname"]
+ }
+ ]
+ }
+ });
+
+ const sets = await ConfigurationSets.create(logger, globParser, jsonFileReader, '');
+
+ const result = sets.verifyConfiguration({}, {"AREQUIRED_AREQUIRED_ANAME": "avalue"});
+
+ expect(result).toBe(true);
+ expect(logger.info).toHaveBeenCalledWith(`Verifying settings files in host: 'apath' -> Successful!`);
+ });
});
});
\ No newline at end of file
diff --git a/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.ts b/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.ts
index ccc5f327..88a18b75 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.ts
+++ b/src/Tools.GitHubActions/VariableSubstitution/src/configurationSets.ts
@@ -87,7 +87,7 @@ export class ConfigurationSets {
const files = await globParser.parseFiles(matches);
if (files.length === 0) {
- logger.warning(`No settings files found in this repository, using the glob patterns: ${globPattern}`);
+ logger.warning(`No settings files found in this repository, applying the glob patterns: ${globPattern}`);
return new ConfigurationSets(logger, []);
}
@@ -133,35 +133,53 @@ export class ConfigurationSets {
}
}
- verifyConfiguration(): boolean {
+ verifyConfiguration(gitHubVariables: any, gitHubSecrets: any): boolean {
if (this.sets.length === 0) {
return true;
}
- let setsVerified = true;
+ let isAllSetsVerified = true;
for (const set of this.sets) {
this.logger.info(`Verifying settings files in host: '${set.hostProjectPath}'`);
- let setVerified = true;
+ let isSetVerified = true;
for (const requiredVariable of set.requiredVariables) {
if (!set.definedVariables.includes(requiredVariable)) {
- setVerified = false;
- this.logger.error(`Required variable '${requiredVariable}' is not defined in any of the settings files of this host!`);
+ this.logger.warning(`Required variable '${requiredVariable}' is not yet defined in any of the settings files of this host! Consider either defining it in one of the settings files of this host, OR remove it from the '${SettingsFile.RequiredProperty}' section of the settings files in this host`);
+ } else {
+ const gitHubVariableName = this.calculateGitHubVariableName(requiredVariable);
+ if (!this.isDefinedInGitHubVariables(gitHubVariables, gitHubSecrets, gitHubVariableName)) {
+ isSetVerified = false;
+ this.logger.error(`Required GitHub environment variable (or secret) '${gitHubVariableName}' (alias: '${requiredVariable}') has not been defined in the environment variables (or secrets) of this GitHub project!`);
+ }
}
}
- if (!setVerified) {
- this.logger.error(`Verification of host '${set.hostProjectPath}' failed, there is at least one missing required variable!`);
- setsVerified = false;
+ if (!isSetVerified) {
+ this.logger.error(`Verification settings files in host: '${set.hostProjectPath}' -> Failed! there is at least one missing required environment variable or secret in this GitHub project`);
+ isAllSetsVerified = false;
} else {
- this.logger.info(`Verification of host '${set.hostProjectPath}' completed successfully`);
+ this.logger.info(`Verifying settings files in host: '${set.hostProjectPath}' -> Successful!`);
}
}
- if (!setsVerified) {
- this.logger.error("Verification of the settings files failed! there are missing required variables in at least one of the hosts!");
+ if (!isAllSetsVerified) {
+ this.logger.error("Verification of all settings files, in all hosts: -> Failed! there are missing required variables in at least one of the hosts. See errors above");
+ } else {
+ this.logger.info("Verification of all settings files, in all hosts: -> Successful!");
}
- return setsVerified;
+ return isAllSetsVerified;
+ }
+
+ private isDefinedInGitHubVariables(gitHubVariables: any, gitHubSecrets: any, name: string): boolean {
+
+ return gitHubVariables.hasOwnProperty(name) || gitHubSecrets.hasOwnProperty(name);
+ }
+
+ private calculateGitHubVariableName(requiredVariable: string) {
+ return requiredVariable
+ .toUpperCase()
+ .replace(/[-:.]/g, '_');
}
}
\ No newline at end of file
diff --git a/src/Tools.GitHubActions/VariableSubstitution/src/index.ts b/src/Tools.GitHubActions/VariableSubstitution/src/index.ts
index 3e749b3d..1ee26261 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/src/index.ts
+++ b/src/Tools.GitHubActions/VariableSubstitution/src/index.ts
@@ -18,8 +18,8 @@ async function run() {
const projectName = github.context.repo.repo;
logger.info(`Scanning settings files:'${filesParam}', in GitHub project ${projectName}'`);
- const secrets = JSON.parse(secretsParam);
- const variables = JSON.parse(variablesParam);
+ const gitHubSecrets = (secretsParam !== null && secretsParam !== undefined ? JSON.parse(secretsParam) : {}) ?? {};
+ const gitHubEnvironmentVariables = (variablesParam !== null && variablesParam !== undefined ? JSON.parse(variablesParam) : {}) ?? {};
const globParser = new GlobPatternParser();
const jsonFileReader = new AppSettingsJsonFileReader();
@@ -28,7 +28,7 @@ async function run() {
logger.info('No settings files found, skipping variable substitution');
return;
} else {
- const verified = configurationSets.verifyConfiguration();
+ const verified = configurationSets.verifyConfiguration(gitHubEnvironmentVariables, gitHubSecrets);
if (!verified) {
return;
}
diff --git a/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.spec.ts b/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.spec.ts
index 99f968fc..eb0f7766 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.spec.ts
+++ b/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.spec.ts
@@ -41,14 +41,14 @@ describe('SettingsFile', () => {
expect(file.hasRequired).toEqual(false);
});
- it('should return file without Required, when file has incorrectly typed Required value', async () => {
+ it('should return file without Required, when file has incorrectly typed DeployRequired value', async () => {
const path = `${__dirname}/testing/__data/appsettings.json`;
const reader: jest.Mocked = {
readAppSettingsFile: jest.fn().mockResolvedValue(
{
"Level1": "avalue",
- "Required": "arequired"
+ "Deploy": "adeploy"
}),
};
@@ -57,21 +57,57 @@ describe('SettingsFile', () => {
expect(file.path).toEqual(path);
expect(file.variables.length).toEqual(2);
expect(file.variables[0]).toEqual("Level1");
- expect(file.variables[1]).toEqual("Required");
+ expect(file.variables[1]).toEqual("Deploy");
expect(file.hasRequired).toEqual(false);
});
- it('should return file without Required, when file has incorrectly nested Required value', async () => {
+ it('should return file without Required, when file has incorrectly nested DeployRequired value', async () => {
const path = `${__dirname}/testing/__data/appsettings.json`;
const reader: jest.Mocked = {
readAppSettingsFile: jest.fn().mockResolvedValue(
{
"Level1": {
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": [
+ "arequired1",
+ "arequired2",
+ "arequired3"]
+ }
+ ]
+ }
+ }
+ }),
+ };
+
+ const file = await SettingsFile.create(reader, path);
+
+ expect(file.path).toEqual(path);
+ expect(file.variables.length).toEqual(3);
+ expect(file.variables[0]).toEqual("Level1:Deploy:Required:0:Keys:0");
+ expect(file.variables[1]).toEqual("Level1:Deploy:Required:0:Keys:1");
+ expect(file.variables[2]).toEqual("Level1:Deploy:Required:0:Keys:2");
+ expect(file.hasRequired).toEqual(false);
+ });
+
+ it('should return file without Required, when file has correct DeployRequired values, but explicitly disabled Keys', async () => {
+
+ const path = `${__dirname}/testing/__data/appsettings.json`;
+ const reader: jest.Mocked = {
+ readAppSettingsFile: jest.fn().mockResolvedValue(
+ {
+ "Level1": "avalue",
+ "Deploy": {
"Required": [
- "arequired1",
- "arequired2",
- "arequired3"
+ {
+ "Disabled": true,
+ "Keys": [
+ "arequired1",
+ "arequired2",
+ "arequired3"]
+ }
]
}
}),
@@ -80,25 +116,29 @@ describe('SettingsFile', () => {
const file = await SettingsFile.create(reader, path);
expect(file.path).toEqual(path);
- expect(file.variables.length).toEqual(3);
- expect(file.variables[0]).toEqual("Level1:Required:0");
- expect(file.variables[1]).toEqual("Level1:Required:1");
- expect(file.variables[2]).toEqual("Level1:Required:2");
+ expect(file.variables.length).toEqual(1);
+ expect(file.variables[0]).toEqual("Level1");
expect(file.hasRequired).toEqual(false);
+ expect(file.requiredVariables.length).toEqual(0);
});
- it('should return file with Required, when file has correct Required values', async () => {
+ it('should return file with Required, when file has correct DeployRequired values, and not explicitly disabled Keys', async () => {
const path = `${__dirname}/testing/__data/appsettings.json`;
const reader: jest.Mocked = {
readAppSettingsFile: jest.fn().mockResolvedValue(
{
"Level1": "avalue",
- "Required": [
- "arequired1",
- "arequired2",
- "arequired3"
- ]
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": [
+ "arequired1",
+ "arequired2",
+ "arequired3"]
+ }
+ ]
+ }
}),
};
@@ -113,4 +153,79 @@ describe('SettingsFile', () => {
expect(file.requiredVariables[1]).toEqual("arequired2");
expect(file.requiredVariables[2]).toEqual("arequired3");
})
-});
\ No newline at end of file
+
+ it('should return file with Required, when file has correct DeployRequired values, and explicitly enabled Keys', async () => {
+
+ const path = `${__dirname}/testing/__data/appsettings.json`;
+ const reader: jest.Mocked = {
+ readAppSettingsFile: jest.fn().mockResolvedValue(
+ {
+ "Level1": "avalue",
+ "Deploy": {
+ "Required": [
+ {
+ "Disabled": false,
+ "Keys": [
+ "arequired1",
+ "arequired2",
+ "arequired3"]
+ }
+ ]
+ }
+ }),
+ };
+
+ const file = await SettingsFile.create(reader, path);
+
+ expect(file.path).toEqual(path);
+ expect(file.variables.length).toEqual(1);
+ expect(file.variables[0]).toEqual("Level1");
+ expect(file.hasRequired).toEqual(true);
+ expect(file.requiredVariables.length).toEqual(3);
+ expect(file.requiredVariables[0]).toEqual("arequired1");
+ expect(file.requiredVariables[1]).toEqual("arequired2");
+ expect(file.requiredVariables[2]).toEqual("arequired3");
+ });
+
+ it('should return file with Required, when file has correct DeployRequired values, and multiple Key sections', async () => {
+
+ const path = `${__dirname}/testing/__data/appsettings.json`;
+ const reader: jest.Mocked = {
+ readAppSettingsFile: jest.fn().mockResolvedValue(
+ {
+ "Level1": "avalue",
+ "Deploy": {
+ "Required": [
+ {
+ "Keys": [
+ "arequired1",
+ "arequired2",
+ "arequired3"]
+ },
+ {
+ "Keys": [
+ "arequired4",
+ "arequired5",
+ "arequired6"]
+ }
+ ]
+ }
+ }),
+ };
+
+ const file = await SettingsFile.create(reader, path);
+
+ expect(file.path).toEqual(path);
+ expect(file.variables.length).toEqual(1);
+ expect(file.variables[0]).toEqual("Level1");
+ expect(file.hasRequired).toEqual(true);
+ expect(file.requiredVariables.length).toEqual(6);
+ expect(file.requiredVariables[0]).toEqual("arequired1");
+ expect(file.requiredVariables[1]).toEqual("arequired2");
+ expect(file.requiredVariables[2]).toEqual("arequired3");
+ expect(file.requiredVariables[3]).toEqual("arequired4");
+ expect(file.requiredVariables[4]).toEqual("arequired5");
+ expect(file.requiredVariables[5]).toEqual("arequired6");
+ })
+});
+
\ No newline at end of file
diff --git a/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.ts b/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.ts
index 2e19cb22..222cc4bd 100644
--- a/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.ts
+++ b/src/Tools.GitHubActions/VariableSubstitution/src/settingsFile.ts
@@ -8,6 +8,12 @@ export interface ISettingsFile {
}
export class SettingsFile implements ISettingsFile {
+
+ public static DeployProperty: string = "Deploy";
+ public static RequiredProperty: string = "Required";
+ public static KeysProperty: string = "Keys";
+ public static DisabledProperty: string = "Disabled";
+
private constructor(path: string, variables: string[], requiredVariables: string[]) {
this._path = path;
this._variables = variables;
@@ -51,10 +57,10 @@ export class SettingsFile implements ISettingsFile {
const element = json[key];
const nextPrefix = SettingsFile.createVariablePath(prefix, key);
if (typeof element === "object") {
- if (SettingsFile.isTopLevelRequiredKey(element, key, prefix)) {
- for (let index = 0; index < element.length; index++) {
- const requiredKey = element[index];
- requiredVariables.push(requiredKey);
+ if (SettingsFile.isDeployRequiredKey(element, key, prefix)) {
+ const required = SettingsFile.getDeployRequiredVariables(element);
+ if (required.length > 0) {
+ requiredVariables.push(...required);
}
} else {
SettingsFile.scrapeVariablesRecursively(element, variables, requiredVariables, nextPrefix);
@@ -73,9 +79,56 @@ export class SettingsFile implements ISettingsFile {
return `${prefix}:${key}`;
}
- private static isTopLevelRequiredKey(element: any, key: string, prefix: string): boolean {
- return (key === "required" || key === "Required")
- && Array.isArray(element)
- && prefix === "";
+ private static isDeployRequiredKey(element: any, key: string, prefix: string): boolean {
+ if (prefix !== "") {
+ return false;
+ }
+
+ if (key.toUpperCase() !== SettingsFile.DeployProperty.toUpperCase()) {
+ return false;
+ }
+
+ if (!element.hasOwnProperty(SettingsFile.RequiredProperty)) {
+ return false;
+ }
+
+
+ const required = element[SettingsFile.RequiredProperty];
+ if (!required) {
+ return false;
+ }
+
+ return Array.isArray(required);
+ }
+
+ private static getDeployRequiredVariables(element: any): string[] {
+
+ const required = element[SettingsFile.RequiredProperty];
+ if (required) {
+ if (Array.isArray(required)) {
+ let requiredVariables: string[] = [];
+ for (let index = 0; index < required.length; index++) {
+ const requiredSection = required[index];
+
+ if (requiredSection.hasOwnProperty(SettingsFile.KeysProperty)) {
+
+ if (requiredSection.hasOwnProperty(SettingsFile.DisabledProperty)) {
+ const disabled = requiredSection[SettingsFile.DisabledProperty];
+ if (disabled) {
+ continue;
+ }
+ }
+
+ const keys = requiredSection[SettingsFile.KeysProperty];
+ if (keys) {
+ requiredVariables.push(...keys);
+ }
+ }
+ }
+ return requiredVariables;
+ }
+ }
+
+ return [];
}
}
\ No newline at end of file
diff --git a/src/WebsiteHost/appsettings.Deploy.json b/src/WebsiteHost/appsettings.Deploy.json
index 0bc8f3e3..8ac0d4a9 100644
--- a/src/WebsiteHost/appsettings.Deploy.json
+++ b/src/WebsiteHost/appsettings.Deploy.json
@@ -3,8 +3,8 @@
"Notes": "Lists the required configuration keys that must be overwritten (by GitHub action) when we deploy this host",
"Required": [
{
- "description": "General settings from appsettings.json",
- "keys": [
+ "Description": "General settings from appsettings.json",
+ "Keys": [
"Hosts:ApiHost1:BaseUrl",
"Hosts:AncillaryApi:BaseUrl",
"Hosts:AncillaryApi:HMACAuthNSecret",
@@ -15,17 +15,17 @@
]
},
{
- "description": "Azure specific settings from appsettings.Azure.json",
- "keys": [
+ "Description": "Azure specific settings from appsettings.Azure.json",
+ "Keys": [
"ApplicationInsights:ConnectionString",
"ApplicationServices:Persistence:AzureStorageAccount:AccountName",
"ApplicationServices:Persistence:AzureStorageAccount:AccountKey"
]
},
{
- "description": "AWS specific settings from appsettings.AWS.json",
- "disabled": true,
- "keys": [
+ "Description": "AWS specific settings from appsettings.AWS.json",
+ "Disabled": true,
+ "Keys": [
"ApplicationServices:Persistence:AWS:AccessKey",
"ApplicationServices:Persistence:AWS:SecretKey",
"ApplicationServices:Persistence:AWS:Region",