Skip to content

Commit

Permalink
Added extra warnings and options to VariableSubstitution custom githu…
Browse files Browse the repository at this point in the history
…b action
  • Loading branch information
jezzsantos committed Jan 11, 2025
1 parent 1d5d752 commit f72999e
Show file tree
Hide file tree
Showing 17 changed files with 450 additions and 116 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deploy-azure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
files: 'src/WebsiteHost/**/.env.deploy'
variables: ${{ toJSON(vars)}}
secrets: ${{ toJSON(secrets)}}
warnOnAdditionalVars: false
- name: Build WebsiteHost (FrontEnd) for Deploy
run: |
cd src/WebsiteHost/ClientApp
Expand All @@ -56,6 +57,8 @@ jobs:
files: '**/appsettings.json,**/appsettings.Azure.json'
variables: ${{ toJSON(vars)}}
secrets: ${{ toJSON(secrets)}}
warnOnAdditionalVars: true
ignoreAdditionalVars: '^DEPLOY_'
- name: Package (ApiHost1) for Deploy
run: dotnet publish --configuration ${{env.DEPLOY_BUILD_CONFIGURATION}} "src/ApiHost1/ApiHost1.csproj" --output ".\publish\ApiHost1" /p:HostingPlatform=${{env.HOSTED_ON}}
- name: Package (WebsiteHost) for Deploy
Expand Down
2 changes: 2 additions & 0 deletions src/SaaStack.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,7 @@ public void When$condition$_Then$outcome$()
<s:Boolean x:Key="/Default/UserDictionary/Words/=ascope/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=asecret/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=asecretsigningkey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=asecretvalue/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=asender/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=asenderemailaddress/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=aserviceclass/@EntryIndexedValue">True</s:Boolean>
Expand Down Expand Up @@ -1795,6 +1796,7 @@ public void When$condition$_Then$outcome$()
<s:Boolean x:Key="/Default/UserDictionary/Words/=avalue/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=avalueobject/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=avariable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=avariablevalue/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=avatared/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=averificationtoken/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=awholekey/@EntryIndexedValue">True</s:Boolean>
Expand Down
23 changes: 20 additions & 3 deletions src/Tools.GitHubActions/VariableSubstitution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,28 @@ In the GitHub project, you would define the following variables or secrets:

**Required**. This must have this specific value in the YML file: `${{ toJSON(vars)}}`.

## Outputs

None.
### `warnOnAdditionalVars`

**Optional**. If set to `true`, the action will create a build warning if there are additional variables or secrets in the GitHub repository that are not substituted into any `appsettings.json`/`.env` files. Default `false`.

### `ignoreAdditionalVars`

**Optional**. A regular expression that matches any GitHub variables/secrets that should be ignored if `warnOnAdditionalVars` is `true`.

## Outputs

All variables, in all files found by the `files` input, will be substituted with the values of the variables/secrets found in the GitHub repository, for the specified environment.

Additional build warnings will be raised in these cases:
* In `appsettings.json` files, when a declared `Deploy -> Required -> Keys` key in the `appsettings.json` file is present in specific `appsettings.json` file.
* In all files, when a variable is substituted and the variable is both defined as a secret and a variable in the GitHub repository. In this case, the secret value will be preferred.

> These warnings cannot be suppressed.
## Example usage

For C# host projects:
For .NET host projects:

```yaml
jobs:
Expand All @@ -164,6 +177,8 @@ jobs:
files: '**/appsettings.json'
variables: ${{ toJSON(vars)}}
secrets: ${{ toJSON(secrets)}}
warnOnAdditionalVars: true
ignoreAdditionalVars: '^DEPLOY_'
```
For JavaScript/NodeJS projects:
Expand All @@ -180,6 +195,8 @@ jobs:
files: '**/WebsiteHost/**/.env.deploy'
variables: ${{ toJSON(vars)}}
secrets: ${{ toJSON(secrets)}}
warnOnAdditionalVars: true
ignoreAdditionalVars: '^DEPLOY_'
```
> Note: That you should specify the name of the deployment environment that you have set up in your GitHub project, on the job itself. When you do, all secrets and variables from that environment, plus those form the GitHub repository (plus those from your GitHub organization).
7 changes: 7 additions & 0 deletions src/Tools.GitHubActions/VariableSubstitution/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ inputs:
variables:
description: 'The variables of the GitHub repository (and environment). Must be the value: \$\{\{ toJSON(vars)\}\}'
required: true
warnOnAdditionalVars:
description: 'Whether to output warnings if there are additional variables or secrets in the GitHub repository that are not substituted into any files'
required: false
default: false
ignoreAdditionalVars:
description: 'A regular expression that matches any GitHub variables/secrets that should be ignored if `warnOnAdditionalVars` is enabled'
required: false
runs:
using: 'node20'
main: './built/index.js'
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {ILogger} from "./logger";
import {AppSettingRequiredVariable, SettingsFileProcessorMessages} from "./settingsFileProcessor";
import {IFileReaderWriter} from "./fileReaderWriter";
import {AppSettingsJsonFileProcessor} from "./appSettingsJsonFileProcessor";
import {WarningOptions} from "./main";

describe('getVariables', () => {
const logger: jest.Mocked<ILogger> = {
Expand Down Expand Up @@ -246,13 +247,13 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({}, {});
const result = await processor.substitute(new WarningOptions(), {}, {});

expect(result).toEqual(true);
expect(readerWriter.readSettingsFile).not.toHaveBeenCalled();
expect(logger.info).not.toHaveBeenCalled();
});

it('should not substitute, when read file throws', async () => {

const readerWriter: jest.Mocked<IFileReaderWriter> = {
Expand All @@ -261,7 +262,7 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({"aname": "avalue"}, {});
const result = await processor.substitute(new WarningOptions(), {"aname": "avalue"}, {});

expect(result).toEqual(false);
expect(readerWriter.readSettingsFile).toHaveBeenCalledWith("apath");
Expand All @@ -278,7 +279,7 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({"aname": "avalue"}, {});
const result = await processor.substitute(new WarningOptions(), {"aname": "avalue"}, {});

expect(result).toEqual(false);
expect(readerWriter.readSettingsFile).toHaveBeenCalledWith("apath");
Expand All @@ -295,7 +296,7 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({"ANAME": "avalue2"}, {});
const result = await processor.substitute(new WarningOptions(), {"ANAME": "avalue2"}, {});

expect(result).toEqual(true);
expect(readerWriter.writeSettingsFile).toHaveBeenCalledWith("apath", {"aname": "avalue2"});
Expand All @@ -311,7 +312,7 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({}, {"ANAME": "avalue2"});
const result = await processor.substitute(new WarningOptions(), {}, {"ANAME": "avalue2"});

expect(result).toEqual(true);
expect(readerWriter.writeSettingsFile).toHaveBeenCalledWith("apath", {"aname": "avalue2"});
Expand All @@ -333,7 +334,7 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({}, {"ALEVEL1_ALEVEL2_ALEVEL3": "avalue2"});
const result = await processor.substitute(new WarningOptions(), {}, {"ALEVEL1_ALEVEL2_ALEVEL3": "avalue2"});

expect(result).toEqual(true);
expect(readerWriter.writeSettingsFile).toHaveBeenCalledWith("apath", {
Expand Down Expand Up @@ -371,7 +372,7 @@ describe('substitute', () => {
};
const processor = new AppSettingsJsonFileProcessor(logger, readerWriter, "apath");

const result = await processor.substitute({}, {"ALEVEL1_ALEVEL2_ALEVEL3": "avalue2"});
const result = await processor.substitute(new WarningOptions(), {}, {"ALEVEL1_ALEVEL2_ALEVEL3": "avalue2"});

expect(result).toEqual(true);
expect(readerWriter.writeSettingsFile).toHaveBeenCalledWith("apath", expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ISettingsFileProcessor,
SettingsFileProcessorMessages
} from "./settingsFileProcessor";
import {WarningOptions} from "./main";

export class AppSettingsJsonFileProcessor implements ISettingsFileProcessor {
_logger: ILogger;
Expand Down Expand Up @@ -41,7 +42,7 @@ export class AppSettingsJsonFileProcessor implements ISettingsFileProcessor {
}
}

private static substituteVariablesRecursively(logger: ILogger, gitHubVariables: any, gitHubSecrets: any, json: any, prefix: string = "") {
private static substituteVariablesRecursively(logger: ILogger, warningOptions: WarningOptions, gitHubVariables: any, gitHubSecrets: any, json: any, prefix: string = "") {
for (const key in json) {
if (json.hasOwnProperty(key)) {
const element = json[key];
Expand All @@ -51,12 +52,12 @@ export class AppSettingsJsonFileProcessor implements ISettingsFileProcessor {
if (deployKey) {
json[key] = SettingsFileProcessorMessages.redactedDeployMessage();
} else {
AppSettingsJsonFileProcessor.substituteVariablesRecursively(logger, gitHubVariables, gitHubSecrets, element, fullyQualifiedVariableName);
AppSettingsJsonFileProcessor.substituteVariablesRecursively(logger, warningOptions, gitHubVariables, gitHubSecrets, element, fullyQualifiedVariableName);
}
} else {
if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
const githubVariableName = GitHubVariables.calculateVariableOrSecretName(fullyQualifiedVariableName);
const gitHubSecretOrVariableValue = GitHubVariables.getVariableOrSecretValue(gitHubVariables, gitHubSecrets, githubVariableName);
const gitHubSecretOrVariableValue = GitHubVariables.getVariableOrSecretValue(logger, warningOptions, gitHubVariables, gitHubSecrets, githubVariableName);
if (gitHubSecretOrVariableValue) {
logger.info(SettingsFileProcessorMessages.substitutingVariable(fullyQualifiedVariableName));
json[key] = gitHubSecretOrVariableValue;
Expand Down Expand Up @@ -153,15 +154,15 @@ export class AppSettingsJsonFileProcessor implements ISettingsFileProcessor {
return `${prefix}:${key}`;
}

async substitute(gitHubVariables: any, gitHubSecrets: any): Promise<boolean> {
async substitute(warningOptions: WarningOptions, gitHubVariables: any, gitHubSecrets: any): Promise<boolean> {

if (Object.keys(gitHubVariables).length === 0 && Object.keys(gitHubSecrets).length === 0) {
return true;
}

try {
const json = await this._readerWriter.readSettingsFile(this._path);
AppSettingsJsonFileProcessor.substituteVariablesRecursively(this._logger, gitHubVariables, gitHubSecrets, json);
AppSettingsJsonFileProcessor.substituteVariablesRecursively(this._logger, warningOptions, gitHubVariables, gitHubSecrets, json);
await this._readerWriter.writeSettingsFile(this._path, json);
this._logger.info(SettingsFileProcessorMessages.substitutingSucceeded(this._path));
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ISettingsFile, SettingsFile} from "./settingsFile";
import {ILogger} from "./logger";
import {AppSettingRequiredVariable, GitHubVariables} from "./settingsFileProcessor";
import {WarningOptions} from "./main";

export interface IConfigurationSet {
readonly hostProjectPath: string;
Expand All @@ -12,7 +13,7 @@ export interface IConfigurationSet {

verify(logger: ILogger, gitHubVariables: any, gitHubSecrets: any): boolean;

substitute(logger: ILogger, gitHubVariables: any, gitHubSecrets: any): Promise<boolean>;
substitute(logger: ILogger, warningOptions: WarningOptions, gitHubVariables: any, gitHubSecrets: any): Promise<boolean>;
}

export class ConfigurationSet implements IConfigurationSet {
Expand Down Expand Up @@ -47,11 +48,11 @@ export class ConfigurationSet implements IConfigurationSet {
return this._definedVariables;
}

async substitute(logger: ILogger, gitHubVariables: any, gitHubSecrets: any): Promise<boolean> {
async substitute(logger: ILogger, warningOptions: WarningOptions, gitHubVariables: any, gitHubSecrets: any): Promise<boolean> {
logger.info(ConfigurationSetMessages.startSubstitution(this.hostProjectPath));
let isSetSubstituted = true;
for (const file of this.settingFiles) {
const isSubstituted = await file.substitute(logger, gitHubVariables, gitHubSecrets);
const isSubstituted = await file.substitute(logger, warningOptions, gitHubVariables, gitHubSecrets);
if (!isSubstituted) {
isSetSubstituted = false;
}
Expand Down
Loading

0 comments on commit f72999e

Please sign in to comment.