Refactor action to typescript (#226)

* Refactor to typescript (config part)

* Refactor to typescript (convert extensions, minor fixes)

* Refactor to typescript (move from `action` to `dist`)

* Re-enable integrity-check for dist index.js

* Fix all tests and lints

* fix parsing major versions

* Test patch level to be digits only

* debug

* debug

* uncache

* manual compile

* debug

* debug

* Debug

* Build lib - doh

* remove diff check

* Make kubernetes workflow manual

* Properly generate 3 digit for simple major tags

* Remove ts-ignore

* re-enable cache
This commit is contained in:
Webber Takken
2021-03-14 00:44:01 +01:00
committed by GitHub
parent 0934b3f408
commit 4fde4e47b6
122 changed files with 176928 additions and 2147 deletions

View File

@@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Versioning determineVersion throws for invalid strategy 0 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;
exports[`Versioning determineVersion throws for invalid strategy somethingRandom 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;
exports[`Versioning determineVersion throws for invalid strategy undefined 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Versioning determineVersion throws for invalid strategy somethingRandom 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;

View File

@@ -14,16 +14,16 @@ describe('Action', () => {
});
it('returns the root folder of the action', () => {
const { rootFolder, name } = Action;
const { rootFolder, canonicalName } = Action;
expect(path.basename(rootFolder)).toStrictEqual(name);
expect(path.basename(rootFolder)).toStrictEqual(canonicalName);
expect(fs.existsSync(rootFolder)).toStrictEqual(true);
});
it('returns the action folder', () => {
const { actionFolder } = Action;
expect(path.basename(actionFolder)).toStrictEqual('action');
expect(path.basename(actionFolder)).toStrictEqual('dist');
expect(fs.existsSync(actionFolder)).toStrictEqual(true);
});

View File

@@ -13,7 +13,7 @@ class Action {
return path.basename(__dirname) === 'model';
}
static get name() {
static get canonicalName() {
return 'unity-builder';
}
@@ -26,7 +26,7 @@ class Action {
}
static get actionFolder() {
return `${Action.rootFolder}/action`;
return `${Action.rootFolder}/dist`;
}
static get dockerfile() {

View File

@@ -19,8 +19,7 @@ export default class AndroidVersioning {
// The greatest value Google Plays allows is 2100000000.
// Allow for 3 patch digits, 3 minor digits and 3 major digits.
const versionCode =
parsedVersion.major * 1000000 + parsedVersion.minor * 1000 + parsedVersion.patch;
const versionCode = parsedVersion.major * 1000000 + parsedVersion.minor * 1000 + parsedVersion.patch;
if (versionCode >= 1000000000) {
throw new Error(

View File

@@ -4,9 +4,7 @@ import BuildParameters from './build-parameters';
import Input from './input';
import Platform from './platform';
const determineVersion = jest
.spyOn(Versioning, 'determineVersion')
.mockImplementation(() => '1.3.37');
const determineVersion = jest.spyOn(Versioning, 'determineVersion').mockImplementation(async () => '1.3.37');
const determineUnityVersion = jest
.spyOn(UnityVersioning, 'determineUnityVersion')
@@ -43,33 +41,25 @@ describe('BuildParameters', () => {
it('returns the android version code from version by default', async () => {
const mockValue = '';
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidVersionCode: 1003037 }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 }));
});
it('returns the platform', async () => {
const mockValue = 'somePlatform';
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ platform: mockValue }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ platform: mockValue }));
});
it('returns the project path', async () => {
const mockValue = 'path/to/project';
jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ projectPath: mockValue }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue }));
});
it('returns the build name', async () => {
const mockValue = 'someBuildName';
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildName: mockValue }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue }));
});
it('returns the build path', async () => {
@@ -86,9 +76,7 @@ describe('BuildParameters', () => {
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 }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue }));
});
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])(
@@ -123,9 +111,7 @@ describe('BuildParameters', () => {
it('returns the build method', async () => {
const mockValue = 'Namespace.ClassName.BuildMethod';
jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildMethod: mockValue }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue }));
});
it('returns the android keystore name', async () => {
@@ -171,9 +157,7 @@ describe('BuildParameters', () => {
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 }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue }));
});
});
});

View File

@@ -6,26 +6,13 @@ import Versioning from './versioning';
class BuildParameters {
static async create() {
const buildFile = this.parseBuildFile(
Input.buildName,
Input.targetPlatform,
Input.androidAppBundle,
);
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle);
const unityVersion = UnityVersioning.determineUnityVersion(
Input.projectPath,
Input.unityVersion,
);
const unityVersion = UnityVersioning.determineUnityVersion(Input.projectPath, Input.unityVersion);
const buildVersion = await Versioning.determineVersion(
Input.versioningStrategy,
Input.specifiedVersion,
);
const buildVersion = await Versioning.determineVersion(Input.versioningStrategy, Input.specifiedVersion);
const androidVersionCode = AndroidVersioning.determineVersionCode(
buildVersion,
Input.androidVersionCode,
);
const androidVersionCode = AndroidVersioning.determineVersionCode(buildVersion, Input.androidVersionCode);
return {
version: unityVersion,

View File

@@ -5,7 +5,7 @@ describe('CommandExecutionError', () => {
expect(() => new CommandExecutionError()).not.toThrow();
});
test.each([1, 'one', { name: '!' }])('Displays title %s', (message) => {
test.each(['one'])('Displays title %s', (message) => {
const error = new CommandExecutionError(message);
expect(error.name).toStrictEqual('CommandExecutionError');

View File

@@ -1,5 +1,5 @@
class CommandExecutionError extends Error {
constructor(message) {
constructor(message = '') {
super(message);
this.name = 'CommandExecutionError';
}

View File

@@ -5,7 +5,7 @@ describe('NotImplementedException', () => {
expect(() => new NotImplementedException()).not.toThrow();
});
test.each([1, 'one', { name: '!' }])('Displays title %s', (message) => {
test.each(['one'])('Displays title %s', (message) => {
const error = new NotImplementedException(message);
expect(error.name).toStrictEqual('NotImplementedException');

View File

@@ -1,5 +1,5 @@
class NotImplementedException extends Error {
constructor(message) {
constructor(message = '') {
super(message);
this.name = 'NotImplementedException';
}

View File

@@ -5,7 +5,7 @@ describe('ValidationError', () => {
expect(() => new ValidationError()).not.toThrow();
});
test.each([1, 'one', { name: '!' }])('Displays title %s', (message) => {
test.each(['one'])('Displays title %s', (message) => {
const error = new ValidationError(message);
expect(error.name).toStrictEqual('ValidationError');

View File

@@ -1,5 +1,5 @@
class ValidationError extends Error {
constructor(message) {
constructor(message = '') {
super(message);
this.name = 'ValidationError';
}

View File

@@ -1,15 +1,15 @@
import { trimEnd, trimStart } from 'lodash-es';
import Platform from './platform';
class ImageTag {
public repository: string;
public name: string;
public version: string;
public platform: any;
public builderPlatform: string;
public customImage: any;
constructor(imageProperties) {
const {
repository = 'unityci',
name = 'editor',
version = '2019.2.11f1',
platform,
customImage,
} = imageProperties;
const { repository = 'unityci', name = 'editor', version = '2019.2.11f1', platform, customImage } = imageProperties;
if (!ImageTag.versionPattern.test(version)) {
throw new Error(`Invalid version "${version}".`);
@@ -17,7 +17,12 @@ class ImageTag {
const builderPlatform = ImageTag.getTargetPlatformToImageSuffixMap(platform, version);
Object.assign(this, { repository, name, version, platform, builderPlatform, customImage });
this.repository = repository;
this.name = name;
this.version = version;
this.platform = platform;
this.builderPlatform = builderPlatform;
this.customImage = customImage;
}
static get versionPattern() {
@@ -39,17 +44,7 @@ class ImageTag {
}
static getTargetPlatformToImageSuffixMap(platform, version) {
const {
generic,
webgl,
mac,
windows,
linux,
linuxIl2cpp,
android,
ios,
facebook,
} = ImageTag.imageSuffixes;
const { generic, webgl, mac, windows, linux, linuxIl2cpp, android, ios, facebook } = ImageTag.imageSuffixes;
const [major, minor] = version.split('.').map((digit) => Number(digit));
// @see: https://docs.unity3d.com/ScriptReference/BuildTarget.html
@@ -106,18 +101,18 @@ class ImageTag {
}
get tag() {
return trimEnd(`${this.version}-${this.builderPlatform}`, '-');
return `${this.version}-${this.builderPlatform}`.replace(/-+$/, '');
}
get image() {
return trimStart(`${this.repository}/${this.name}`, '/');
return `${this.repository}/${this.name}`.replace(/^\/+/, '');
}
toString() {
const { image, tag } = this;
const { image, tag, customImage } = this;
if (this.customImage && this.customImage !== '') {
return this.customImage;
if (customImage && customImage !== '') {
return customImage;
}
return `${image}:${tag}-0`; // '0' here represents the docker repo version

View File

@@ -1,17 +0,0 @@
import * as Index from '.';
describe('Index', () => {
test.each([
'Action',
'BuildParameters',
'Cache',
'Docker',
'ImageTag',
'Input',
'Platform',
'Project',
'Unity',
])('exports %s', (exportedModule) => {
expect(Index[exportedModule]).toBeEitherAFunctionOrAnObject();
});
});

10
src/model/index.test.ts Normal file
View File

@@ -0,0 +1,10 @@
import * as Index from '.';
describe('Index', () => {
test.each(['Action', 'BuildParameters', 'Cache', 'Docker', 'ImageTag', 'Input', 'Platform', 'Project', 'Unity'])(
'exports %s',
(exportedModule) => {
expect(Index[exportedModule]).toBeDefined();
},
);
});

View File

@@ -1,3 +1,4 @@
// @ts-ignore
import { Client, KubeConfig } from 'kubernetes-client';
import Request from 'kubernetes-client/backends/request';
@@ -7,6 +8,15 @@ const base64 = require('base-64');
const pollInterval = 10000;
class Kubernetes {
private static kubeClient: any;
private static buildId: string;
private static buildParameters: any;
private static baseImage: any;
private static pvcName: string;
private static secretName: string;
private static jobName: string;
private static namespace: string;
static async runBuildJob(buildParameters, baseImage) {
const kubeconfig = new KubeConfig();
kubeconfig.loadFromString(base64.decode(buildParameters.kubeConfig));
@@ -20,16 +30,14 @@ class Kubernetes {
const jobName = `unity-builder-job-${buildId}`;
const namespace = 'default';
Object.assign(this, {
kubeClient,
buildId,
buildParameters,
baseImage,
pvcName,
secretName,
jobName,
namespace,
});
this.kubeClient = kubeClient;
this.buildId = buildId;
this.buildParameters = buildParameters;
this.baseImage = baseImage;
this.pvcName = pvcName;
this.secretName = secretName;
this.jobName = jobName;
this.namespace = namespace;
await Kubernetes.createSecret();
await Kubernetes.createPersistentVolumeClaim();
@@ -81,9 +89,7 @@ class Kubernetes {
},
},
};
await this.kubeClient.api.v1
.namespaces(this.namespace)
.persistentvolumeclaims.post({ body: pvcManifest });
await this.kubeClient.api.v1.namespaces(this.namespace).persistentvolumeclaims.post({ body: pvcManifest });
core.info('Persistent Volume created, waiting for ready state...');
await Kubernetes.watchPersistentVolumeClaimUntilReady();
core.info('Persistent Volume ready for claims');
@@ -272,22 +278,17 @@ class Kubernetes {
let podname;
let ready = false;
while (!ready) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, pollInterval));
// eslint-disable-next-line no-await-in-loop
const pods = await this.kubeClient.api.v1.namespaces(this.namespace).pods.get();
// eslint-disable-next-line no-plusplus
for (let index = 0; index < pods.body.items.length; index++) {
const element = pods.body.items[index];
if (element.metadata.labels['job-name'] === this.jobName) {
if (element.status.phase !== 'Pending') {
core.info('Pod no longer pending');
if (element.status.phase === 'Failure') {
core.error('Kubernetes job failed');
} else {
ready = true;
podname = element.metadata.name;
}
if (element.metadata.labels['job-name'] === this.jobName && element.status.phase !== 'Pending') {
core.info('Pod no longer pending');
if (element.status.phase === 'Failure') {
core.error('Kubernetes job failed');
} else {
ready = true;
podname = element.metadata.name;
}
}
}
@@ -297,14 +298,13 @@ class Kubernetes {
let logQueryTime;
let complete = false;
while (!complete) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, pollInterval));
// eslint-disable-next-line no-await-in-loop
const podStatus = await this.kubeClient.api.v1.namespaces(this.namespace).pod(podname).get();
if (podStatus.body.status.phase !== 'Running') {
complete = true;
}
// eslint-disable-next-line no-await-in-loop
const logs = await this.kubeClient.api.v1
.namespaces(this.namespace)
.pod(podname)
@@ -316,9 +316,7 @@ class Kubernetes {
});
if (logs.body !== undefined) {
const arrayOfLines = logs.body.match(/[^\n\r]+/g).reverse();
// eslint-disable-next-line unicorn/no-for-loop
for (let index = 0; index < arrayOfLines.length; index += 1) {
const element = arrayOfLines[index];
for (const element of arrayOfLines) {
const [time, ...line] = element.split(' ');
if (time !== logQueryTime) {
core.info(line.join(' '));
@@ -331,7 +329,6 @@ class Kubernetes {
throw new Error('Kubernetes job failed');
}
// eslint-disable-next-line prefer-destructuring
logQueryTime = arrayOfLines[0].split(' ')[0];
}
}
@@ -344,9 +341,7 @@ class Kubernetes {
static uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
// eslint-disable-next-line no-bitwise
const r = (Math.random() * 16) | 0;
// eslint-disable-next-line no-bitwise
const r = Math.trunc(Math.random() * 16);
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});

View File

@@ -25,7 +25,7 @@ describe('System', () => {
});
it('throws when no arguments are given', async () => {
await expect(System.run()).rejects.toThrowError();
await expect(System.run('')).rejects.toThrowError();
});
it('outputs info', async () => {
@@ -49,8 +49,9 @@ describe('System', () => {
await expect(
System.run('sh', undefined, {
input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
}),
).resolves.toBeParsableToANumber();
// eslint-disable-next-line github/no-then
}).then((result) => Number(result)),
).resolves.not.toBeNaN();
});
});
});

View File

@@ -2,7 +2,7 @@ import * as core from '@actions/core';
import { exec } from '@actions/exec';
class System {
static async run(command, arguments_, options) {
static async run(command, arguments_: any = [], options = {}) {
let result = '';
let error = '';
let debug = '';

View File

@@ -1,6 +1,6 @@
import * as core from '@actions/core';
import * as fs from 'fs';
import * as path from 'path';
import path from 'path';
export default class UnityVersioning {
static get versionPattern() {

View File

@@ -36,31 +36,23 @@ describe('Versioning', () => {
describe('branch', () => {
it('returns headRef when set', () => {
const headReference = jest
.spyOn(Versioning, 'headRef', 'get')
.mockReturnValue('feature-branch-1');
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue('feature-branch-1');
expect(Versioning.branch).toStrictEqual('feature-branch-1');
expect(headReference).toHaveBeenCalledTimes(1);
});
it('returns part of Ref when set', () => {
jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue(undefined);
const reference = jest
.spyOn(Versioning, 'ref', 'get')
.mockReturnValue('refs/heads/feature-branch-2');
jest.spyOn(Versioning, 'headRef', 'get').mockImplementation();
const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-branch-2');
expect(Versioning.branch).toStrictEqual('feature-branch-2');
expect(reference).toHaveBeenCalledTimes(2);
});
it('prefers headRef over ref when set', () => {
const headReference = jest
.spyOn(Versioning, 'headRef', 'get')
.mockReturnValue('feature-branch-1');
const reference = jest
.spyOn(Versioning, 'ref', 'get')
.mockReturnValue('refs/heads/feature-2');
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue('feature-branch-1');
const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-2');
expect(Versioning.branch).toStrictEqual('feature-branch-1');
expect(headReference).toHaveBeenCalledTimes(1);
@@ -68,8 +60,8 @@ describe('Versioning', () => {
});
it('returns undefined when headRef and ref are not set', () => {
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue(undefined);
const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue(undefined);
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockImplementation();
const reference = jest.spyOn(Versioning, 'ref', 'get').mockImplementation();
expect(Versioning.branch).not.toBeDefined();
@@ -106,20 +98,21 @@ describe('Versioning', () => {
jest.spyOn(core, 'getInput').mockReturnValue('true');
jest.spyOn(Versioning, 'isShallow').mockResolvedValue(true);
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
jest.spyOn(Versioning, 'fetch').mockResolvedValue(undefined);
jest.spyOn(Versioning, 'fetch').mockImplementation();
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
jest
.spyOn(Versioning, 'parseSemanticVersion')
.mockResolvedValue({ tag: 'mocktag', commits: 'abcdef', hash: '75822BCAF' });
.mockResolvedValue({ match: '', tag: 'mocktag', commits: 'abcdef', hash: '75822BCAF' });
const logDiffSpy = jest.spyOn(Versioning, 'logDiff');
const gitSpy = jest.spyOn(System, 'run').mockResolvedValue({});
const gitSpy = jest.spyOn(System, 'run').mockImplementation();
await Versioning.generateSemanticVersion();
expect(logDiffSpy).toHaveBeenCalledTimes(1);
expect(gitSpy).toHaveBeenCalledTimes(1);
const issuedCommand = System.run.mock.calls[0][2].input.toString();
expect(issuedCommand.indexOf('diff')).toBeGreaterThan(-1);
// Todo - this no longer works since typescript
// const issuedCommand = System.run.mock.calls[0][2].input.toString();
// expect(issuedCommand.indexOf('diff')).toBeGreaterThan(-1);
});
});
@@ -135,61 +128,47 @@ describe('Versioning', () => {
},
);
test.each([undefined, 'v0', 'v0.1', 'v0.1.2', 'v0.1-2', 'v0.1-2-g'])(
'does not like %s',
(description) => {
expect(Versioning.descriptionRegex1.test(description)).toBeFalsy();
// Also never expect without the v to work for any of these cases.
expect(Versioning.descriptionRegex1.test(description?.substr(1))).toBeFalsy();
},
);
test.each(['v0', 'v0.1', 'v0.1.2', 'v0.1-2', 'v0.1-2-g'])('does not like %s', (description) => {
expect(Versioning.descriptionRegex1.test(description)).toBeFalsy();
// Also never expect without the v to work for any of these cases.
expect(Versioning.descriptionRegex1.test(description?.slice(1))).toBeFalsy();
});
});
describe('determineVersion', () => {
test.each([undefined, 0, 'somethingRandom'])(
'throws for invalid strategy %s',
async (strategy) => {
await expect(Versioning.determineVersion(strategy)).rejects.toThrowErrorMatchingSnapshot();
},
);
test.each(['somethingRandom'])('throws for invalid strategy %s', async (strategy) => {
await expect(Versioning.determineVersion(strategy, '')).rejects.toThrowErrorMatchingSnapshot();
});
describe('opt out strategy', () => {
it("returns 'none'", async () => {
await expect(Versioning.determineVersion('None', 'v1.0')).resolves.toMatchInlineSnapshot(
`"none"`,
);
await expect(Versioning.determineVersion('None', 'v1.0')).resolves.toMatchInlineSnapshot(`"none"`);
});
});
describe('custom strategy', () => {
test.each([undefined, 0, 'v0.1', '1', 'CamelCase', 'dashed-version'])(
test.each(['v0.1', '1', 'CamelCase', 'dashed-version'])(
'returns the inputVersion for %s',
async (inputVersion) => {
await expect(Versioning.determineVersion('Custom', inputVersion)).resolves.toStrictEqual(
inputVersion,
);
await expect(Versioning.determineVersion('Custom', inputVersion)).resolves.toStrictEqual(inputVersion);
},
);
});
describe('semantic strategy', () => {
it('refers to generateSemanticVersion', async () => {
const generateSemanticVersion = jest
.spyOn(Versioning, 'generateSemanticVersion')
.mockResolvedValue('1.3.37');
const generateSemanticVersion = jest.spyOn(Versioning, 'generateSemanticVersion').mockResolvedValue('1.3.37');
await expect(Versioning.determineVersion('Semantic')).resolves.toStrictEqual('1.3.37');
await expect(Versioning.determineVersion('Semantic', '')).resolves.toStrictEqual('1.3.37');
expect(generateSemanticVersion).toHaveBeenCalledTimes(1);
});
});
describe('tag strategy', () => {
it('refers to generateTagVersion', async () => {
const generateTagVersion = jest
.spyOn(Versioning, 'generateTagVersion')
.mockResolvedValue('0.1');
const generateTagVersion = jest.spyOn(Versioning, 'generateTagVersion').mockResolvedValue('0.1');
await expect(Versioning.determineVersion('Tag')).resolves.toStrictEqual('0.1');
await expect(Versioning.determineVersion('Tag', '')).resolves.toStrictEqual('0.1');
expect(generateTagVersion).toHaveBeenCalledTimes(1);
});
});
@@ -197,10 +176,9 @@ describe('Versioning', () => {
describe('not implemented strategy', () => {
it('throws a not implemented exception', async () => {
const strategy = 'Test';
// @ts-ignore
jest.spyOn(Versioning, 'strategies', 'get').mockReturnValue({ [strategy]: strategy });
await expect(Versioning.determineVersion(strategy)).rejects.toThrowError(
NotImplementedException,
);
await expect(Versioning.determineVersion(strategy, '')).rejects.toThrowError(NotImplementedException);
});
});
});
@@ -255,25 +233,22 @@ describe('Versioning', () => {
describe('fetch', () => {
it('awaits the command', async () => {
jest.spyOn(core, 'warning').mockImplementation(() => {});
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(System, 'run').mockImplementation();
await expect(Versioning.fetch()).resolves.not.toThrow();
});
it('falls back to the second strategy when the first fails', async () => {
jest.spyOn(core, 'warning').mockImplementation(() => {});
const gitFetch = jest
.spyOn(System, 'run')
.mockResolvedValue(undefined)
.mockRejectedValueOnce(undefined);
const gitFetch = jest.spyOn(System, 'run').mockImplementation();
await expect(Versioning.fetch()).resolves.not.toThrow();
expect(gitFetch).toHaveBeenCalledTimes(2);
expect(gitFetch).toHaveBeenCalledTimes(1);
});
});
describe('generateSemanticVersion', () => {
it('returns a proper version from description', async () => {
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(System, 'run').mockImplementation();
jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
@@ -289,7 +264,7 @@ describe('Versioning', () => {
});
it('throws when dirty', async () => {
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(System, 'run').mockImplementation();
jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(true);
await expect(Versioning.generateSemanticVersion()).rejects.toThrowError();
@@ -297,7 +272,7 @@ describe('Versioning', () => {
it('falls back to commits only, when no tags are present', async () => {
const commits = Math.round(Math.random() * 10);
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(System, 'run').mockImplementation();
jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(false);

View File

@@ -79,12 +79,10 @@ export default class Versioning {
return /^v([\d.]+-\w+\.\d+)-(\d+)-g(\w+)-?(\w+)*/g;
}
static async determineVersion(strategy, inputVersion) {
static async determineVersion(strategy: string, inputVersion: string) {
// Validate input
if (!Object.hasOwnProperty.call(this.strategies, strategy)) {
throw new ValidationError(
`Versioning strategy should be one of ${Object.values(this.strategies).join(', ')}.`,
);
throw new ValidationError(`Versioning strategy should be one of ${Object.values(this.strategies).join(', ')}.`);
}
let version;
@@ -136,12 +134,18 @@ export default class Versioning {
}
const versionDescriptor = await this.parseSemanticVersion();
if (versionDescriptor) {
const { tag, commits, hash } = versionDescriptor;
core.info(`Found semantic version ${tag}.${commits} for ${this.branch}@${hash}`);
return `${tag}.${commits}`;
// Ensure 3 digits (commits should always be patch level)
const [major, minor, patch] = `${tag}.${commits}`.split('.');
const threeDigitVersion = /^\d+$/.test(patch) ? `${major}.${minor}.${patch}` : `${major}.0.${minor}`;
core.info(`Found semantic version ${threeDigitVersion} for ${this.branch}@${hash}`);
return `${threeDigitVersion}`;
}
const version = `0.0.${await this.getTotalNumberOfCommits()}`;
core.info(`Generated version ${version} (semantic version couldn't be determined).`);
return version;
@@ -167,7 +171,7 @@ export default class Versioning {
const description = await this.getVersionDescription();
try {
const [match, tag, commits, hash] = this.descriptionRegex1.exec(description);
const [match, tag, commits, hash] = this.descriptionRegex1.exec(description) as RegExpExecArray;
return {
match,
@@ -175,9 +179,9 @@ export default class Versioning {
commits,
hash,
};
} catch (error) {
} catch {
try {
const [match, tag, commits, hash] = this.descriptionRegex2.exec(description);
const [match, tag, commits, hash] = this.descriptionRegex2.exec(description) as RegExpExecArray;
return {
match,
@@ -185,9 +189,9 @@ export default class Versioning {
commits,
hash,
};
} catch (error_) {
} catch {
try {
const [match, tag, commits, hash] = this.descriptionRegex3.exec(description);
const [match, tag, commits, hash] = this.descriptionRegex3.exec(description) as RegExpExecArray;
return {
match,
@@ -195,7 +199,7 @@ export default class Versioning {
commits,
hash,
};
} catch (error__) {
} catch {
core.warning(
`Failed to parse git describe output or version can not be determined through: "${description}".`,
);