Compare commits

..

18 Commits
v0.13 ... v1.0

Author SHA1 Message Date
Webber
bfe6be7ce2 Update readme for v1.0 🎉 2020-05-23 00:25:26 +02:00
Webber
f15f40d265 Use head for tags 2020-05-22 23:01:58 +02:00
Webber
866f364f64 Use ref instead of tag vs branch 2020-05-22 23:01:58 +02:00
Webber
a245f08e75 rename to throwContextualError 2020-05-22 23:01:58 +02:00
Webber
21c211bbdd rebase on master 2020-05-22 23:01:58 +02:00
Webber
3718e05961 Describe errors in System.run 2020-05-22 23:01:58 +02:00
Webber
0159028bb1 Fix missing await 2020-05-22 23:01:58 +02:00
Webber
054c6bfab3 Catch command for in-shell errors 2020-05-22 23:01:58 +02:00
Webber
8c9ff3249e More info if command gives no output, just the exit code. 2020-05-22 23:01:58 +02:00
Webber
7386c669ad Fix no output from errors 2020-05-22 23:01:58 +02:00
Webber
ce865270c4 Use commit-ish for git description 2020-05-22 23:01:58 +02:00
Webber
7e17091251 Split responsibilities between Input and BuildParameters models 2020-05-22 00:55:26 +02:00
Webber
02ff5bbef2 Add documentation and tests for allowDirtyBuild 2020-05-22 00:55:26 +02:00
Webber
8c177b1bad Add flag for allowing dirty branch 2020-05-22 00:55:26 +02:00
Webber
699621ed21 Run versioning commands in projectPath instead 2020-05-21 14:26:37 +02:00
Webber
44bde7feb9 Base number of commits off of the branch on origin 2020-05-02 16:37:24 +02:00
Webber
5328bda08e Base number of commits off of the branch 2020-05-02 16:37:24 +02:00
Webber
34e4b86924 Fix case where no tags does not trigger false 2020-05-01 20:32:41 +02:00
15 changed files with 467 additions and 209 deletions

View File

@@ -51,7 +51,7 @@ your license file and add it as a secret.
Then, define the build step as follows: Then, define the build step as follows:
```yaml ```yaml
- uses: webbertakken/unity-builder@v0.11 - uses: webbertakken/unity-builder@<version>
env: env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with: with:
@@ -73,7 +73,7 @@ Instead, three variables will need to be set.
Define the build step as follows: Define the build step as follows:
```yaml ```yaml
- uses: webbertakken/unity-builder@v0.11 - uses: webbertakken/unity-builder@<version>
env: env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
@@ -177,7 +177,7 @@ jobs:
restore-keys: | restore-keys: |
Library-${{ matrix.projectPath }}- Library-${{ matrix.projectPath }}-
Library- Library-
- uses: webbertakken/unity-builder@v0.11 - uses: webbertakken/unity-builder@<version>
with: with:
projectPath: ${{ matrix.projectPath }} projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }} unityVersion: ${{ matrix.unityVersion }}
@@ -255,33 +255,7 @@ _**default:** Built-in script that will run a build out of the box._
#### versioning #### versioning
The versioning strategy to use. Configure a specific versioning strategy
Strategies only work when no custom buildMethod is specified.
_**required:** `false`_
_**default:** `Auto`_
#### _These are the available strategies:_
##### None
No version will be set by Builder.
```yaml
- uses: webbertakken/unity-builder@<version>
with:
versioning: None
```
Note that the version set in the project will be used instead.
##### Semantic (default)
Builder automatically generates a version based on [semantic versioning](https://semver.org/) out of the box.
The version works as follows: `<major>.<minor>.<patch>` for example `0.1.2`.
The latest tag dictates `<major>.<minor>` and the number of commits since that tag is used in `<patch>`.
```yaml ```yaml
- uses: webbertakken/unity-builder@<version> - uses: webbertakken/unity-builder@<version>
@@ -289,40 +263,52 @@ The latest tag dictates `<major>.<minor>` and the number of commits since that t
versioning: Semantic versioning: Semantic
``` ```
This strategy works well for the following reasons: Find the available strategies below:
- All builds have their unique version ##### Semantic
- No version related commits are created
- No knowledge of git or versioning is required
- Developer keeps control over `major` and `minor` versions using tags.
- Zero configuration; It works out of the box
##### Tag Versioning out of the box! **(recommended)**
Uses the tag that points at `HEAD` as the version. > Compatible with **all platforms**.
> Does **not** modify your repository.
> Requires **zero configuration**.
```yaml How it works:
- uses: webbertakken/unity-builder@<version>
with:
versioning: Tag
```
This strategy works well when using a pipeline that specifically runs for tags. > Generates a version based on [semantic versioning](https://semver.org/).
> Follows `<major>.<minor>.<patch>` for example `0.17.2`.
> The latest tag dictates `<major>.<minor>` (defaults to 0.0 for no tag).
> The number of commits (since the last tag, if any) is used for `<patch>`.
The tag must be a version tag. No configuration required.
##### Custom ##### Custom
Allows specifying a custom version in the `version` field. Allows specifying a custom version in the `version` field. **(advanced users)**
> This strategy is useful when your project or pipeline has some kind of orchestration
> that determines the versions.
##### None
No version will be set by Builder. **(not recommended)**
> Not recommended unless you generate a new version in a pre-commit hook. Manually
> setting versions is error-prone.
#### allowDirtyBuild
Allows the branch of the build to be dirty, and still generate the build.
```yaml ```yaml
- uses: webbertakken/unity-builder@<version> - uses: webbertakken/unity-builder@<version>
with: with:
versioning: Custom allowDirtyBuild: true
version: <some_version>
``` ```
If there is a use case missing from Builder, feel free to create a feature request. Note that it is generally bad practice to modify your branch
in a CI Pipeline. However there are exceptions where this might
be needed. (use with care).
#### customParameters #### customParameters

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import { Action, BuildParameters, Cache, Docker, Input, ImageTag } from './model'; import { Action, BuildParameters, Cache, Docker, ImageTag } from './model';
const core = require('@actions/core'); const core = require('@actions/core');
@@ -7,7 +7,8 @@ async function action() {
Cache.verify(); Cache.verify();
const { dockerfile, workspace, actionFolder } = Action; const { dockerfile, workspace, actionFolder } = Action;
const buildParameters = BuildParameters.create(await Input.getFromUser());
const buildParameters = await BuildParameters.create();
const baseImage = new ImageTag(buildParameters); const baseImage = new ImageTag(buildParameters);
// Build docker image // Build docker image

View File

@@ -26,4 +26,24 @@ expect.extend({
pass, pass,
}; };
}, },
toBeParsableToANumber(received) {
let pass = false;
let errorMessage = '';
try {
Number.parseInt(received, 10);
pass = true;
} catch (error) {
errorMessage = error;
}
const message = () => `Expected ${this.utils.printExpected(received)} to be parsable as a number
, but received error: ${this.utils.printReceived(errorMessage)}.`;
return {
message,
pass,
};
},
}); });

View File

@@ -0,0 +1,38 @@
/* eslint-disable unicorn/prevent-abbreviations */
// Import these named export into your test file:
export const mockProjectPath = jest.fn().mockResolvedValue('mockProjectPath');
export const mockIsDirtyAllowed = jest.fn().mockResolvedValue(false);
export const mockBranch = jest.fn().mockResolvedValue('mockBranch');
export const mockHeadRef = jest.fn().mockResolvedValue('mockHeadRef');
export const mockRef = jest.fn().mockResolvedValue('mockRef');
export const mockDetermineVersion = jest.fn().mockResolvedValue('1.2.3');
export const mockGenerateSemanticVersion = jest.fn().mockResolvedValue('2.3.4');
export const mockGenerateTagVersion = jest.fn().mockResolvedValue('1.0');
export const mockParseSemanticVersion = jest.fn().mockResolvedValue({});
export const mockFetch = jest.fn().mockImplementation(() => {});
export const mockGetVersionDescription = jest.fn().mockResolvedValue('1.2-3-g12345678-dirty');
export const mockIsDirty = jest.fn().mockResolvedValue(false);
export const mockGetTag = jest.fn().mockResolvedValue('v1.0');
export const mockHasAnyVersionTags = jest.fn().mockResolvedValue(true);
export const mockGetTotalNumberOfCommits = jest.fn().mockResolvedValue(3);
export const mockGit = jest.fn().mockImplementation(() => {});
export default {
projectPath: mockProjectPath,
isDirtyAllowed: mockIsDirtyAllowed,
branch: mockBranch,
headRef: mockHeadRef,
ref: mockRef,
determineVersion: mockDetermineVersion,
generateSemanticVersion: mockGenerateSemanticVersion,
generateTagVersion: mockGenerateTagVersion,
parseSemanticVersion: mockParseSemanticVersion,
fetch: mockFetch,
getVersionDescription: mockGetVersionDescription,
isDirty: mockIsDirty,
getTag: mockGetTag,
hasAnyVersionTags: mockHasAnyVersionTags,
getTotalNumberOfCommits: mockGetTotalNumberOfCommits,
git: mockGit,
};

View File

@@ -1,28 +1,25 @@
import Input from './input';
import Platform from './platform'; import Platform from './platform';
import Versioning from './versioning';
class BuildParameters { class BuildParameters {
static create(parameters) { static async create() {
const { const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform);
version, const buildVersion = await Versioning.determineVersion(
targetPlatform, Input.versioningStrategy,
projectPath, Input.specifiedVersion,
buildName, );
buildsPath,
buildMethod,
buildVersion,
customParameters,
} = parameters;
return { return {
version, version: Input.unityVersion,
platform: targetPlatform, platform: Input.targetPlatform,
projectPath, projectPath: Input.projectPath,
buildName, buildName: Input.buildName,
buildPath: `${buildsPath}/${targetPlatform}`, buildPath: `${Input.buildsPath}/${Input.targetPlatform}`,
buildFile: this.parseBuildFile(buildName, targetPlatform), buildFile,
buildMethod, buildMethod: Input.buildMethod,
buildVersion, buildVersion,
customParameters, customParameters: Input.customParameters,
}; };
} }

View File

@@ -1,82 +1,110 @@
import Versioning from './versioning';
import BuildParameters from './build-parameters'; import BuildParameters from './build-parameters';
import Input from './input';
import Platform from './platform'; import Platform from './platform';
const determineVersion = jest
.spyOn(Versioning, 'determineVersion')
.mockImplementation(() => '1.3.37');
afterEach(() => {
jest.clearAllMocks();
});
describe('BuildParameters', () => { describe('BuildParameters', () => {
describe('create', () => { describe('create', () => {
const someParameters = { it('does not throw', async () => {
version: 'someVersion', await expect(BuildParameters.create()).resolves.not.toThrow();
targetPlatform: 'somePlatform',
projectPath: 'path/to/project',
buildName: 'someBuildName',
buildsPath: 'someBuildsPath',
buildMethod: 'Namespace.Class.Method',
customParameters: '-someParam someValue',
};
it('does not throw', () => {
expect(() => BuildParameters.create(someParameters)).not.toThrow();
}); });
it('returns the version', () => { it('determines the version only once', async () => {
expect(BuildParameters.create(someParameters).version).toStrictEqual(someParameters.version); await BuildParameters.create();
expect(determineVersion).toHaveBeenCalledTimes(1);
}); });
it('returns the platform', () => { it('returns the version', async () => {
expect(BuildParameters.create(someParameters).platform).toStrictEqual( const mockValue = 'someVersion';
someParameters.targetPlatform, jest.spyOn(Input, 'unityVersion', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ version: mockValue }),
); );
}); });
it('returns the project path', () => { it('returns the platform', async () => {
expect(BuildParameters.create(someParameters).projectPath).toStrictEqual( const mockValue = 'somePlatform';
someParameters.projectPath, jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ platform: mockValue }),
); );
}); });
it('returns the build name', () => { it('returns the project path', async () => {
expect(BuildParameters.create(someParameters).buildName).toStrictEqual( const mockValue = 'path/to/project';
someParameters.buildName, jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ projectPath: mockValue }),
); );
}); });
it('returns the build path', () => { it('returns the build name', async () => {
expect(BuildParameters.create(someParameters).buildPath).toStrictEqual( const mockValue = 'someBuildName';
`${someParameters.buildsPath}/${someParameters.targetPlatform}`, jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildName: mockValue }),
); );
}); });
describe('build file', () => { it('returns the build path', async () => {
it('returns the build file', () => { const mockPath = 'somePath';
expect(BuildParameters.create(someParameters).buildFile).toStrictEqual( const mockPlatform = 'somePlatform';
someParameters.buildName, const expectedBuildPath = `${mockPath}/${mockPlatform}`;
jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildPath: expectedBuildPath }),
);
});
it('returns the build file', async () => {
const mockValue = 'someBuildName';
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: mockValue }),
);
});
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])(
'appends exe for %s',
async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}.exe` }),
); );
}); },
);
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])( test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => {
'appends exe for %s', jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
(targetPlatform) => { jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
expect( await expect(BuildParameters.create()).resolves.toEqual(
BuildParameters.create({ ...someParameters, targetPlatform }).buildFile, expect.objectContaining({ buildFile: `${targetPlatform}.apk` }),
).toStrictEqual(`${someParameters.buildName}.exe`);
},
);
test.each([Platform.types.Android])('appends apk for %s', (targetPlatform) => {
expect(
BuildParameters.create({ ...someParameters, targetPlatform }).buildFile,
).toStrictEqual(`${someParameters.buildName}.apk`);
});
});
it('returns the build method', () => {
expect(BuildParameters.create(someParameters).buildMethod).toStrictEqual(
someParameters.buildMethod,
); );
}); });
it('returns the custom parameters', () => { it('returns the build method', async () => {
expect(BuildParameters.create(someParameters).customParameters).toStrictEqual( const mockValue = 'Namespace.ClassName.BuildMethod';
someParameters.customParameters, jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildMethod: mockValue }),
);
});
it('returns the custom parameters', async () => {
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ customParameters: mockValue }),
); );
}); });
}); });

View File

@@ -1,3 +1,4 @@
import * as core from '@actions/core';
import fs from 'fs'; import fs from 'fs';
import Action from './action'; import Action from './action';
import Project from './project'; import Project from './project';
@@ -14,11 +15,11 @@ class Cache {
return; return;
} }
// eslint-disable-next-line no-console core.warning(`
console.log(`
Library folder does not exist. Library folder does not exist.
Consider setting up caching to speed up your workflow Consider setting up caching to speed up your workflow,
If this is not your first build.`); if this is not your first build.
`);
} }
} }

View File

@@ -1,38 +1,54 @@
import Platform from './platform'; import Platform from './platform';
import Versioning from './versioning';
const core = require('@actions/core'); const core = require('@actions/core');
/**
* Input variables specified in workflows using "with" prop.
*
* Note that input is always passed as a string, even booleans.
*/
class Input { class Input {
static async getFromUser() { static get unityVersion() {
// Input variables specified in workflows using "with" prop. return core.getInput('unityVersion');
const version = core.getInput('unityVersion'); }
const targetPlatform = core.getInput('targetPlatform') || Platform.default;
static get targetPlatform() {
return core.getInput('targetPlatform') || Platform.default;
}
static get projectPath() {
const rawProjectPath = core.getInput('projectPath') || '.'; const rawProjectPath = core.getInput('projectPath') || '.';
const buildName = core.getInput('buildName') || targetPlatform; return rawProjectPath.replace(/\/$/, '');
const buildsPath = core.getInput('buildsPath') || 'build'; }
const buildMethod = core.getInput('buildMethod'); // processed in docker file
const versioningStrategy = core.getInput('versioning') || 'Semantic';
const specifiedVersion = core.getInput('version') || '';
const customParameters = core.getInput('customParameters') || '';
// Sanitise input static get buildName() {
const projectPath = rawProjectPath.replace(/\/$/, ''); return core.getInput('buildName') || this.targetPlatform;
}
// Parse input static get buildsPath() {
const buildVersion = await Versioning.determineVersion(versioningStrategy, specifiedVersion); return core.getInput('buildsPath') || 'build';
}
// Return validated input static get buildMethod() {
return { return core.getInput('buildMethod'); // processed in docker file
version, }
targetPlatform,
projectPath, static get versioningStrategy() {
buildName, return core.getInput('versioning') || 'Semantic';
buildsPath, }
buildMethod,
buildVersion, static get specifiedVersion() {
customParameters, return core.getInput('version') || '';
}; }
static get allowDirtyBuild() {
const input = core.getInput('allowDirtyBuild') || 'false';
return input === 'true' ? 'true' : 'false';
}
static get customParameters() {
return core.getInput('customParameters') || '';
} }
} }

View File

@@ -1,27 +1,151 @@
import Input from './input'; import * as core from '@actions/core';
import Versioning from './versioning';
const determineVersion = jest import Input from './input';
.spyOn(Versioning, 'determineVersion') import Platform from './platform';
.mockImplementation(() => '1.3.37');
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.restoreAllMocks();
}); });
describe('Input', () => { describe('Input', () => {
describe('getFromUser', () => { describe('unityVersion', () => {
it('does not throw', async () => { it('returns the default value', () => {
await expect(Input.getFromUser()).resolves.not.toBeNull(); expect(Input.unityVersion).toStrictEqual('');
}); });
it('returns an object', async () => { it('takes input from the users workflow', () => {
await expect(typeof (await Input.getFromUser())).toStrictEqual('object'); const mockValue = '2020.4.99f9';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.unityVersion).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('targetPlatform', () => {
it('returns the default value', () => {
expect(Input.targetPlatform).toStrictEqual(Platform.default);
}); });
it('calls version generator once', async () => { it('takes input from the users workflow', () => {
await Input.getFromUser(); const mockValue = 'Android';
expect(determineVersion).toHaveBeenCalledTimes(1); const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.targetPlatform).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('projectPath', () => {
it('returns the default value', () => {
expect(Input.projectPath).toStrictEqual('.');
});
it('takes input from the users workflow', () => {
const mockValue = 'customProjectPath';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.projectPath).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('buildName', () => {
it('returns the default value', () => {
expect(Input.buildName).toStrictEqual(Input.targetPlatform);
});
it('takes input from the users workflow', () => {
const mockValue = 'Build';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildName).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
it('takes special characters as input', () => {
const mockValue = '1ßúëld2';
jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildName).toStrictEqual(mockValue);
});
});
describe('buildsPath', () => {
it('returns the default value', () => {
expect(Input.buildsPath).toStrictEqual('build');
});
it('takes input from the users workflow', () => {
const mockValue = 'customBuildsPath';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildsPath).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('buildMethod', () => {
it('returns the default value', () => {
expect(Input.buildMethod).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'Namespace.ClassName.Method';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildMethod).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('versioningStrategy', () => {
it('returns the default value', () => {
expect(Input.versioningStrategy).toStrictEqual('Semantic');
});
it('takes input from the users workflow', () => {
const mockValue = 'Anything';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.versioningStrategy).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('specifiedVersion', () => {
it('returns the default value', () => {
expect(Input.specifiedVersion).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = '1.33.7';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.specifiedVersion).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('allowDirtyBuild', () => {
it('returns the default value', () => {
expect(Input.allowDirtyBuild).toStrictEqual('false');
});
it('returns true when string true is passed', () => {
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
expect(Input.allowDirtyBuild).toStrictEqual('true');
expect(spy).toHaveBeenCalledTimes(1);
});
it('returns false when string false is passed', () => {
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
expect(Input.allowDirtyBuild).toStrictEqual('false');
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('customParameters', () => {
it('returns the default value', () => {
expect(Input.customParameters).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = '-imAFlag';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.customParameters).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View File

@@ -1,11 +1,10 @@
import * as core from '@actions/core'; import Input from './input';
import Unity from './unity'; import Unity from './unity';
import Action from './action'; import Action from './action';
class Project { class Project {
static get relativePath() { static get relativePath() {
// Todo - properly use Input for this. const { projectPath } = Input;
const projectPath = core.getInput('projectPath') || '.';
return `${projectPath}`; return `${projectPath}`;
} }

View File

@@ -19,22 +19,40 @@ class System {
}, },
}; };
const exitCode = await exec(command, arguments_, { silent: true, listeners, ...options }); const showOutput = () => {
if (debug !== '') {
core.debug(debug);
}
if (debug !== '') { if (result !== '') {
core.debug(debug); core.info(result);
} }
if (result !== '') { if (error !== '') {
core.info(result); core.warning(error);
} }
};
if (exitCode !== 0) { const throwContextualError = (message) => {
throw new Error(error); let commandAsString = command;
} if (Array.isArray(arguments_)) {
commandAsString += ` ${arguments_.join(' ')}`;
} else if (typeof arguments_ === 'string') {
commandAsString += ` ${arguments_}`;
}
if (error !== '') { throw new Error(`Failed to run "${commandAsString}".\n ${message}`);
core.warning(error); };
try {
const exitCode = await exec(command, arguments_, { silent: true, listeners, ...options });
showOutput();
if (exitCode !== 0) {
throwContextualError(`Command returned non-zero exit code.\nError: ${error}`);
}
} catch (inCommandError) {
showOutput();
throwContextualError(`In-command error caught: ${inCommandError}`);
} }
return result; return result;

View File

@@ -44,5 +44,13 @@ describe('System', () => {
expect(info).toHaveBeenCalledTimes(2); expect(info).toHaveBeenCalledTimes(2);
expect(info).toHaveBeenLastCalledWith('3\n'); expect(info).toHaveBeenLastCalledWith('3\n');
}); });
it('allows pipes using buffer', async () => {
await expect(
System.run('sh', undefined, {
input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
}),
).resolves.toBeParsableToANumber();
});
}); });
}); });

View File

@@ -1,9 +1,18 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import NotImplementedException from './error/not-implemented-exception'; import NotImplementedException from './error/not-implemented-exception';
import ValidationError from './error/validation-error'; import ValidationError from './error/validation-error';
import Input from './input';
import System from './system'; import System from './system';
export default class Versioning { export default class Versioning {
static get projectPath() {
return Input.projectPath;
}
static get isDirtyAllowed() {
return Input.allowDirtyBuild === 'true';
}
static get strategies() { static get strategies() {
return { None: 'None', Semantic: 'Semantic', Tag: 'Tag', Custom: 'Custom' }; return { None: 'None', Semantic: 'Semantic', Tag: 'Tag', Custom: 'Custom' };
} }
@@ -79,7 +88,7 @@ export default class Versioning {
static async generateSemanticVersion() { static async generateSemanticVersion() {
await this.fetch(); await this.fetch();
if (await this.isDirty()) { if ((await this.isDirty()) && !this.isDirtyAllowed) {
throw new Error('Branch is dirty. Refusing to base semantic version on uncommitted changes'); throw new Error('Branch is dirty. Refusing to base semantic version on uncommitted changes');
} }
@@ -137,10 +146,10 @@ export default class Versioning {
*/ */
static async fetch() { static async fetch() {
try { try {
await System.run('git', ['fetch', '--unshallow']); await this.git(['fetch', '--unshallow']);
} catch (error) { } catch (error) {
core.warning(error); core.warning(`Fetch --unshallow caught: ${error}`);
await System.run('git', ['fetch']); await this.git(['fetch']);
} }
} }
@@ -153,21 +162,15 @@ export default class Versioning {
* identifies the current commit. * identifies the current commit.
*/ */
static async getVersionDescription() { static async getVersionDescription() {
return System.run('git', [ const commitIsh = (await this.getTag()) ? 'HEAD' : `origin/${this.branch}`;
'describe', return this.git(['describe', '--long', '--tags', '--always', '--debug', commitIsh]);
'--long',
'--tags',
'--always',
'--debug',
`origin/${this.branch}`,
]);
} }
/** /**
* Returns whether there are uncommitted changes that are not ignored. * Returns whether there are uncommitted changes that are not ignored.
*/ */
static async isDirty() { static async isDirty() {
const output = await System.run('git', ['status', '--porcelain']); const output = await this.git(['status', '--porcelain']);
return output !== ''; return output !== '';
} }
@@ -176,33 +179,42 @@ export default class Versioning {
* Get the tag if there is one pointing at HEAD * Get the tag if there is one pointing at HEAD
*/ */
static async getTag() { static async getTag() {
return System.run('git', ['tag', '--points-at', 'HEAD']); return this.git(['tag', '--points-at', 'HEAD']);
} }
/** /**
* Whether or not the repository has any version tags yet. * Whether or not the repository has any version tags yet.
*/ */
static async hasAnyVersionTags() { static async hasAnyVersionTags() {
const numberOfVersionCommits = await System.run('git', [ const numberOfCommitsAsString = await System.run('sh', undefined, {
'tag', input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
'--list', silent: false,
'--merged', });
'HEAD',
'|',
'grep v[0-9]*',
'|',
'wc -l',
]);
return numberOfVersionCommits !== '0'; const numberOfCommits = Number.parseInt(numberOfCommitsAsString, 10);
return numberOfCommits !== 0;
} }
/** /**
* Get the total number of commits on head. * Get the total number of commits on head.
*
* Note: HEAD should not be used, as it may be detached, resulting in an additional count.
*/ */
static async getTotalNumberOfCommits() { static async getTotalNumberOfCommits() {
const numberOfCommitsAsString = await System.run('git', ['rev-list', '--count', 'HEAD']); const numberOfCommitsAsString = await this.git([
'rev-list',
'--count',
`origin/${this.branch}`,
]);
return Number.parseInt(numberOfCommitsAsString, 10); return Number.parseInt(numberOfCommitsAsString, 10);
} }
/**
* Run git in the specified project path
*/
static async git(arguments_, options = {}) {
return System.run('git', arguments_, { cwd: this.projectPath, ...options });
}
} }

View File

@@ -90,6 +90,16 @@ describe('Versioning', () => {
}); });
}); });
describe('isDirtyAllowed', () => {
it('does not throw', () => {
expect(() => Versioning.isDirtyAllowed).not.toThrow();
});
it('returns false by default', () => {
expect(Versioning.isDirtyAllowed).toStrictEqual(false);
});
});
describe('descriptionRegex', () => { describe('descriptionRegex', () => {
it('is a valid regex', () => { it('is a valid regex', () => {
expect(Versioning.descriptionRegex).toBeInstanceOf(RegExp); expect(Versioning.descriptionRegex).toBeInstanceOf(RegExp);