Skip to content

Commit f1c154a

Browse files
authored
Attempt to streamline dev lifecycle (git hooks) (game-ci#357)
* fix: misalignments in dev lifecycle * fix: dist no longer added to staged * fix: misalignments in dev lifecycle * chore: multi-platform hooks and tests * chore: multi-platform hooks and tests * chore: add intention for colors * chore: update lint-staged to fix color * chore: update dist files * feat: move to lefthook (remove husky and lint-staged) * feat: move to lefthook (remove husky and lint-staged) * fix: test aftereach * fix: test aftereach * fix: early restore call * feat: jest fails if something gets logged to console * chore: add todos of misplaced code * chore: update dist files * chore: move jest file
1 parent 9440c54 commit f1c154a

17 files changed

+4790
-4878
lines changed

.husky/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

.husky/pre-commit

Lines changed: 0 additions & 7 deletions
This file was deleted.

dist/index.js

Lines changed: 4601 additions & 4595 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jest.config.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
module.exports = {
2+
// Automatically clear mock calls and instances between every test
23
clearMocks: true,
4+
5+
// An array of file extensions your modules use
36
moduleFileExtensions: ['js', 'ts'],
7+
8+
// The test environment that will be used for testing
49
testEnvironment: 'node',
10+
11+
// The glob patterns Jest uses to detect test files
512
testMatch: ['**/*.test.ts'],
13+
14+
// This option allows use of a custom test runner
615
testRunner: 'jest-circus/runner',
16+
17+
// A map with regular expressions for transformers to paths
718
transform: {
819
'^.+\\.ts$': 'ts-jest',
920
},
21+
22+
// Indicates whether each individual test should be reported during the run
1023
verbose: true,
24+
25+
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
26+
modulePathIgnorePatterns: ['<rootDir>/lib/', '<rootDir>/dist/'],
27+
28+
// A list of paths to modules that run some code to configure or set up the testing framework before each test
29+
setupFilesAfterEnv: ['<rootDir>/src/jest.setup.ts'],
1130
};

lefthook.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# EXAMPLE USAGE
2+
# Refer for explanation to following link:
3+
# https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md
4+
#
5+
6+
color: true
7+
extends: {}
8+
9+
pre-push:
10+
parallel: true
11+
commands:
12+
packages-audit:
13+
tags: security
14+
run: yarn audit
15+
16+
pre-commit:
17+
parallel: true
18+
commands:
19+
format documents:
20+
glob: '*.{md,mdx}'
21+
run: yarn prettier --write {staged_files}
22+
format configs:
23+
glob: '*.{json,yml,yaml}'
24+
run: yarn prettier --write {staged_files}
25+
format code:
26+
glob: '*.{js,jsx,ts,tsx}'
27+
exclude: 'dist/'
28+
run: yarn prettier --write {staged_files} && yarn eslint {staged_files} && git add {staged_files}
29+
run tests:
30+
glob: '*.{js,jsx,ts,tsx}'
31+
exclude: 'dist/'
32+
run: yarn jest --passWithNoTests --findRelatedTests {staged_files}
33+
build distributables:
34+
skip: ['merge', 'rebase']
35+
run: yarn build && git add dist
36+
make shell script executable:
37+
glob: '*.sh'
38+
run: git update-index --chmod=+x

package.json

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
"author": "Webber <[email protected]>",
88
"license": "MIT",
99
"scripts": {
10-
"prebuild": "yarn",
11-
"build": "tsc && ncc build lib --source-map --license licenses.txt",
10+
"prepare": "lefthook install",
11+
"build": "yarn && tsc && ncc build lib --source-map --license licenses.txt",
1212
"lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts",
1313
"format": "prettier --write \"src/**/*.{js,ts}\"",
14-
"prepare": "husky install",
1514
"cli": "yarn ts-node src/index.ts -m cli",
1615
"cli-aws": "cross-env cloudRunnerCluster=aws yarn run test-cli",
1716
"cli-k8s": "cross-env cloudRunnerCluster=k8s yarn run test-cli",
@@ -41,6 +40,7 @@
4140
"yaml": "^1.10.2"
4241
},
4342
"devDependencies": {
43+
"@arkweid/lefthook": "^0.7.7",
4444
"@types/jest": "^27.4.1",
4545
"@types/node": "^17.0.21",
4646
"@types/semver": "^7.3.9",
@@ -53,27 +53,13 @@
5353
"eslint-plugin-jest": "24.1.3",
5454
"eslint-plugin-prettier": "^3.3.1",
5555
"eslint-plugin-unicorn": "28.0.2",
56-
"husky": "^7.0.4",
5756
"jest": "^27.5.1",
5857
"jest-circus": "^27.5.1",
58+
"jest-fail-on-console": "^2.3.0",
5959
"js-yaml": "^4.1.0",
60-
"lint-staged": "^12.3.4",
6160
"prettier": "^2.5.1",
6261
"ts-jest": "^27.1.3",
6362
"ts-node": "10.4.0",
6463
"typescript": "4.1.3"
65-
},
66-
"lint-staged": {
67-
"*.{js,jsx,ts,tsx}": [
68-
"prettier --write",
69-
"eslint",
70-
"jest --findRelatedTests"
71-
],
72-
"*.{json,md,yaml,yml}": [
73-
"prettier --write"
74-
],
75-
"*.sh": [
76-
"git update-index --chmod=+x"
77-
]
7864
}
7965
}

src/jest.setup.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import failOnConsole from 'jest-fail-on-console';
2+
3+
// Fail when console logs something inside a test - use spyOn instead
4+
failOnConsole({
5+
shouldFailOnWarn: true,
6+
shouldFailOnError: true,
7+
shouldFailOnLog: true,
8+
shouldFailOnAssert: true,
9+
});

src/model/build-parameters.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import BuildParameters from './build-parameters';
55
import Input from './input';
66
import Platform from './platform';
77

8+
// Todo - Don't use process.env directly, that's what the input model class is for.
89
const testLicense =
910
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
1011
process.env.UNITY_LICENSE = testLicense;
1112

1213
const determineVersion = jest.spyOn(Versioning, 'determineVersion').mockImplementation(async () => '1.3.37');
13-
1414
const determineUnityVersion = jest
1515
.spyOn(UnityVersioning, 'determineUnityVersion')
1616
.mockImplementation(() => '2019.2.11f1');
17-
1817
const determineSdkManagerParameters = jest
1918
.spyOn(AndroidVersioning, 'determineSdkManagerParameters')
2019
.mockImplementation(() => 'platforms;android-30');

src/model/build-parameters.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,13 @@ class BuildParameters {
5555

5656
static async create(): Promise<BuildParameters> {
5757
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle);
58-
5958
const unityVersion = UnityVersioning.determineUnityVersion(Input.projectPath, Input.unityVersion);
60-
6159
const buildVersion = await Versioning.determineVersion(Input.versioningStrategy, Input.specifiedVersion);
62-
6360
const androidVersionCode = AndroidVersioning.determineVersionCode(buildVersion, Input.androidVersionCode);
64-
6561
const androidSdkManagerParameters = AndroidVersioning.determineSdkManagerParameters(Input.androidTargetSdkVersion);
6662

63+
// Todo - Don't use process.env directly, that's what the input model class is for.
64+
// ---
6765
let unitySerial = '';
6866
if (!process.env.UNITY_SERIAL) {
6967
//No serial was present so it is a personal license that we need to convert
@@ -78,6 +76,7 @@ class BuildParameters {
7876
unitySerial = process.env.UNITY_SERIAL!;
7977
}
8078
core.setSecret(unitySerial);
79+
// ---
8180

8281
return {
8382
version: unityVersion,

src/model/input-readers/git-repo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ export class GitRepoReader {
77
static GetSha() {
88
return '';
99
}
10+
1011
public static async GetRemote() {
1112
return (await CloudRunnerSystem.Run(`git remote -v`))
1213
.split(' ')[1]
1314
.split('https://github.com/')[1]
1415
.split('.git')[0];
1516
}
17+
1618
public static async GetBranch() {
1719
assert(fs.existsSync(`.git`));
1820
return (await System.run(`git branch`, [], {}, false)).split('*')[1].split(`\n`)[0].replace(/ /g, ``);

src/model/input-readers/github-cli.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { GithubCliReader } from './github-cli';
22
import * as core from '@actions/core';
33

44
describe(`github cli`, () => {
5-
it(`returns`, async () => {
5+
// Todo - We can not assume that everyone has the GitHub cli installed locally.
6+
it.skip(`returns`, async () => {
67
const token = await GithubCliReader.GetGitHubAuthToken();
8+
9+
// Todo - use expect(result).toStrictEqual(something)
710
core.info(token);
811
});
912
});

src/model/input.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,15 @@ class Input {
147147
}
148148

149149
static get androidTargetSdkVersion() {
150-
return core.getInput('androidTargetSdkVersion') || '';
150+
return Input.getInput('androidTargetSdkVersion') || '';
151151
}
152152

153153
static get sshAgent() {
154154
return Input.getInput('sshAgent') || '';
155155
}
156156

157157
static async gitPrivateToken() {
158-
return core.getInput('gitPrivateToken') || (await Input.githubToken());
158+
return Input.getInput('gitPrivateToken') || (await Input.githubToken());
159159
}
160160

161161
static get chownFilesTo() {

src/model/system.integration.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as core from '@actions/core';
2+
import System from './system';
3+
4+
jest.spyOn(core, 'debug').mockImplementation(() => {});
5+
jest.spyOn(core, 'info').mockImplementation(() => {});
6+
jest.spyOn(core, 'warning').mockImplementation(() => {});
7+
jest.spyOn(core, 'error').mockImplementation(() => {});
8+
9+
afterEach(() => jest.clearAllMocks());
10+
11+
describe('System', () => {
12+
describe('run', () => {
13+
/**
14+
* Not all shells (e.g. Powershell, sh) have a reference to `echo` binary (absent or alias).
15+
* To ensure our integration with '@actions/exec' works as expected we run some specific tests in CI only
16+
*/
17+
describe('integration', () => {
18+
if (!process.env.CI) {
19+
it("doesn't run locally", () => {
20+
expect(true).toBe(true);
21+
});
22+
} else {
23+
it('runs a command successfully', async () => {
24+
await expect(System.run('true')).resolves.not.toBeNull();
25+
});
26+
27+
it('outputs results', async () => {
28+
await expect(System.run('echo test')).resolves.toStrictEqual('test\n');
29+
});
30+
31+
it('throws on when error code is not 0', async () => {
32+
await expect(System.run('false')).rejects.toThrowError();
33+
});
34+
35+
it('allows pipes using buffer', async () => {
36+
await expect(
37+
System.run('sh', undefined, {
38+
input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
39+
// eslint-disable-next-line github/no-then
40+
}).then((result) => Number(result)),
41+
).resolves.not.toBeNaN();
42+
});
43+
}
44+
});
45+
});
46+
});

src/model/system.test.ts

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,46 @@
11
import * as core from '@actions/core';
2+
import * as exec from '@actions/exec';
23
import System from './system';
34

45
jest.spyOn(core, 'debug').mockImplementation(() => {});
56
const info = jest.spyOn(core, 'info').mockImplementation(() => {});
67
jest.spyOn(core, 'warning').mockImplementation(() => {});
78
jest.spyOn(core, 'error').mockImplementation(() => {});
9+
const execSpy = jest.spyOn(exec, 'exec').mockImplementation(async () => 0);
810

9-
afterEach(() => {
10-
jest.clearAllMocks();
11-
});
11+
afterEach(() => jest.clearAllMocks());
1212

1313
describe('System', () => {
1414
describe('run', () => {
15-
it('runs a command successfully', async () => {
16-
await expect(System.run('true')).resolves.not.toBeNull();
17-
});
18-
19-
it('outputs results', async () => {
20-
await expect(System.run('echo test')).resolves.toStrictEqual('test\n');
21-
});
22-
23-
it('throws on when error code is not 0', async () => {
24-
await expect(System.run('false')).rejects.toThrowError();
25-
});
26-
27-
it('throws when no arguments are given', async () => {
28-
await expect(System.run('')).rejects.toThrowError();
29-
});
30-
31-
it('outputs info', async () => {
32-
await expect(System.run('echo test')).resolves.not.toBeNull();
33-
expect(info).toHaveBeenLastCalledWith('test\n');
34-
});
35-
36-
it('outputs info only once', async () => {
37-
await expect(System.run('echo 1')).resolves.not.toBeNull();
38-
expect(info).toHaveBeenCalledTimes(1);
39-
expect(info).toHaveBeenLastCalledWith('1\n');
40-
41-
info.mockClear();
42-
await expect(System.run('echo 2')).resolves.not.toBeNull();
43-
await expect(System.run('echo 3')).resolves.not.toBeNull();
44-
expect(info).toHaveBeenCalledTimes(2);
45-
expect(info).toHaveBeenLastCalledWith('3\n');
46-
});
47-
48-
it('allows pipes using buffer', async () => {
49-
await expect(
50-
System.run('sh', undefined, {
51-
input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
52-
// eslint-disable-next-line github/no-then
53-
}).then((result) => Number(result)),
54-
).resolves.not.toBeNaN();
15+
describe('units', () => {
16+
it('passes the command to command line', async () => {
17+
await expect(System.run('echo test')).resolves.not.toBeNull();
18+
await expect(execSpy).toHaveBeenLastCalledWith('echo test', expect.anything(), expect.anything());
19+
});
20+
21+
it('throws on when error code is not 0', async () => {
22+
execSpy.mockImplementationOnce(async () => 1);
23+
await expect(System.run('false')).rejects.toThrowError();
24+
});
25+
26+
it('throws when no command is given', async () => {
27+
await expect(System.run('')).rejects.toThrowError();
28+
});
29+
30+
it('throws when command consists only of spaces', async () => {
31+
await expect(System.run(' \t\n')).rejects.toThrowError();
32+
});
33+
34+
it('outputs info', async () => {
35+
execSpy.mockImplementationOnce(async (input, _, options) => {
36+
options?.listeners?.stdout?.(Buffer.from(input, 'utf8'));
37+
return 0;
38+
});
39+
40+
await expect(System.run('foo-bar')).resolves.not.toBeNull();
41+
expect(info).toHaveBeenCalledTimes(1);
42+
expect(info).toHaveBeenLastCalledWith('foo-bar');
43+
});
5544
});
5645
});
5746
});

src/model/system.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class System {
4545
};
4646

4747
try {
48+
if (command.trim() === '') {
49+
throw new Error(`Failed to execute empty command`);
50+
}
51+
4852
const exitCode = await exec(command, arguments_, { silent: true, listeners, ...options });
4953
showOutput();
5054
if (exitCode !== 0) {

0 commit comments

Comments
 (0)