Add action logic

pull/11/head
Webber 2020-01-29 22:22:26 +01:00 committed by Webber Takken
parent d105f8c891
commit d6c937fe37
14 changed files with 346 additions and 0 deletions

1
action/index.js 100644

File diff suppressed because one or more lines are too long

23
src/index.js 100644
View File

@ -0,0 +1,23 @@
import * as core from '@actions/core';
import { Action, Docker, Input, ImageTag, Output } from './model';
async function action() {
Action.checkCompatibility();
const { dockerfile, workspace, actionFolder } = Action;
const { unityVersion, projectPath, artifactsPath } = Input.getFromUser();
const baseImage = ImageTag.createForBase(unityVersion);
// Build docker image
const actionImage = await Docker.build({ path: actionFolder, dockerfile, baseImage });
// Run docker image
await Docker.run(actionImage, { workspace, unityVersion, projectPath, artifactsPath });
// Set output
await Output.setArtifactsPath(artifactsPath);
}
action().catch(error => {
core.setFailed(error.message);
});

View File

@ -0,0 +1,48 @@
import path from 'path';
class Action {
static get supportedPlatforms() {
return ['linux'];
}
static get isRunningLocally() {
return process.env.RUNNER_WORKSPACE === undefined;
}
static get isRunningFromSource() {
return path.basename(__dirname) === 'model';
}
static get name() {
return 'unity-test-runner';
}
static get rootFolder() {
if (Action.isRunningFromSource) {
return path.dirname(path.dirname(path.dirname(__filename)));
}
return path.dirname(path.dirname(__filename));
}
static get actionFolder() {
return `${Action.rootFolder}/action`;
}
static get dockerfile() {
return `${Action.actionFolder}/Dockerfile`;
}
static get workspace() {
return process.env.GITHUB_WORKSPACE;
}
static checkCompatibility() {
const currentPlatform = process.platform;
if (!Action.supportedPlatforms.includes(currentPlatform)) {
throw new Error(`Currently ${currentPlatform}-platform is not supported`);
}
}
}
export default Action;

View File

@ -0,0 +1,36 @@
import path from 'path';
import fs from 'fs';
import Action from './action';
describe('Action', () => {
describe('compatibility check', () => {
it('throws for anything other than linux', () => {
if (process.platform !== 'linux') {
expect(() => Action.checkCompatibility()).toThrow();
} else {
expect(() => Action.checkCompatibility()).not.toThrow();
}
});
});
it('returns the root folder of the action', () => {
const { rootFolder, name } = Action;
expect(path.basename(rootFolder)).toStrictEqual(name);
expect(fs.existsSync(rootFolder)).toStrictEqual(true);
});
it('returns the action folder', () => {
const { actionFolder } = Action;
expect(path.basename(actionFolder)).toStrictEqual('action');
expect(fs.existsSync(actionFolder)).toStrictEqual(true);
});
it('returns the docker file', () => {
const { dockerfile } = Action;
expect(path.basename(dockerfile)).toStrictEqual('Dockerfile');
expect(fs.existsSync(dockerfile)).toStrictEqual(true);
});
});

View File

@ -0,0 +1,59 @@
import { exec } from '@actions/exec';
import ImageTag from './image-tag';
class Docker {
static async build(buildParameters, silent = false) {
const { path, dockerfile, baseImage } = buildParameters;
const { version } = baseImage;
const tag = ImageTag.createForAction(version);
const command = `docker build ${path} \
--file ${dockerfile} \
--build-arg IMAGE=${baseImage} \
--tag ${tag}`;
await exec(command, null, { silent });
return tag;
}
static async run(image, parameters, silent = false) {
const { unityVersion, workspace, projectPath, artifactsPath } = parameters;
const command = `docker run \
--workdir /github/workspace \
--rm \
--env UNITY_LICENSE \
--env UNITY_EMAIL \
--env UNITY_PASSWORD \
--env UNITY_SERIAL \
--env UNITY_VERSION=${unityVersion} \
--env PROJECT_PATH=${projectPath} \
--env ARTIFACTS_PATH=${artifactsPath}
--env HOME=/github/home \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \
--env GITHUB_ACTOR \
--env GITHUB_WORKFLOW \
--env GITHUB_HEAD_REF \
--env GITHUB_BASE_REF \
--env GITHUB_EVENT_NAME \
--env GITHUB_WORKSPACE=/github/workspace \
--env GITHUB_ACTION \
--env GITHUB_EVENT_PATH \
--env RUNNER_OS \
--env RUNNER_TOOL_CACHE \
--env RUNNER_TEMP \
--env RUNNER_WORKSPACE \
--volume "/var/run/docker.sock":"/var/run/docker.sock" \
--volume "/home/runner/work/_temp/_github_home":"/github/home" \
--volume "/home/runner/work/_temp/_github_workflow":"/github/workflow" \
--volume "${workspace}":"/github/workspace" \
${image}`;
await exec(command, null, { silent });
}
}
export default Docker;

View File

@ -0,0 +1,20 @@
import Action from './action';
import Docker from './docker';
import ImageTag from './image-tag';
describe('Docker', () => {
it('builds', async () => {
const path = Action.actionFolder;
const dockerfile = `${path}/Dockerfile`;
const baseImage = new ImageTag({
repository: '',
name: 'alpine',
version: '3',
});
const tag = await Docker.build({ path, dockerfile, baseImage }, true);
expect(tag).toBeInstanceOf(ImageTag);
expect(tag.toString()).toStrictEqual('unity-action:3');
}, 240000);
});

View File

@ -0,0 +1,41 @@
import { trimStart } from 'lodash-es';
class ImageTag {
static createForBase(version) {
const repository = 'gableroux';
const name = 'unity3d';
return new this({ repository, name, version });
}
static createForAction(version) {
const repository = '';
const name = 'unity-action';
return new this({ repository, name, version });
}
constructor({ repository = '', name, version }) {
if (!ImageTag.versionPattern.test(version)) {
throw new Error(`Invalid version "${version}".`);
}
Object.assign(this, { repository, name, version });
}
static get versionPattern() {
return /^20\d{2}\.\d\.\w{3,4}|3$/;
}
get tag() {
return this.version;
}
get image() {
return trimStart(`${this.repository}/${this.name}`, '/');
}
toString() {
return `${this.image}:${this.tag}`;
}
}
export default ImageTag;

View File

@ -0,0 +1,38 @@
import ImageTag from './image-tag';
describe('UnityImageVersion', () => {
describe('constructor', () => {
const some = {
name: 'someName',
version: '2020.0.00f0',
};
it('can be called', () => {
expect(() => new ImageTag(some)).not.toThrow();
});
it('accepts parameters and sets the right properties', () => {
const image = new ImageTag(some);
expect(image.repository).toStrictEqual('');
expect(image.name).toStrictEqual(some.name);
expect(image.version).toStrictEqual(some.version);
});
test.each(['2000.0.0f0', '2011.1.11f1'])('accepts %p version format', version => {
expect(() => new ImageTag({ version })).not.toThrow();
});
test.each(['some version', '', 1, null])('throws for incorrect versions %p', version => {
expect(() => new ImageTag({ version })).toThrow();
});
});
describe('toString', () => {
it('returns the correct version', () => {
const image = ImageTag.createForBase('2099.1.1111');
expect(image.toString()).toStrictEqual(`gableroux/unity3d:2099.1.1111`);
});
});
});

View File

@ -0,0 +1,7 @@
import Action from './action';
import Docker from './docker';
import Input from './input';
import ImageTag from './image-tag';
import Output from './output';
export { Action, Docker, Input, ImageTag, Output };

View File

@ -0,0 +1,7 @@
import * as Index from '.';
describe('Index', () => {
test.each(['Action', 'Docker', 'ImageTag', 'Input', 'Output'])('exports %s', exportedModule => {
expect(typeof Index[exportedModule]).toStrictEqual('function');
});
});

35
src/model/input.js 100644
View File

@ -0,0 +1,35 @@
import { getInput } from '@actions/core';
import { includes } from 'lodash-es';
class Input {
static get testModes() {
return ['all', 'playmode', 'editmode'];
}
static getFromUser() {
// Input variables specified in workflow using "with" prop.
const unityVersion = getInput('unityVersion') || '2019.2.11f1';
const testMode = getInput('testMode') || 'all';
const rawProjectPath = getInput('testMode') || '.';
const rawArtifactsPath = getInput('testMode') || 'artifacts';
// Validate input
if (!includes(this.testModes, testMode)) {
throw new Error(`Invalid testMode ${testMode}`);
}
// Sanitise input
const projectPath = rawProjectPath.replace(/\/$/, '');
const artifactsPath = rawArtifactsPath.replace(/\/$/, '');
// Return sanitised input
return {
unityVersion,
projectPath,
testMode,
artifactsPath,
};
}
}
export default Input;

View File

@ -0,0 +1,13 @@
import Input from './input';
describe('Input', () => {
describe('getFromUser', () => {
it('does not throw', () => {
expect(() => Input.getFromUser()).not.toThrow();
});
it('returns an object', () => {
expect(typeof Input.getFromUser()).toStrictEqual('object');
});
});
});

View File

@ -0,0 +1,9 @@
const core = require('@actions/core');
class Output {
static async setArtifactsPath(artifactsPath) {
await core.setOutput('artifactsPath', artifactsPath);
}
}
export default Output;

View File

@ -0,0 +1,9 @@
import Output from './output';
describe('Output', () => {
describe('setArtifactsPath', () => {
it('does not throw', async () => {
await expect(Output.setArtifactsPath()).resolves.not.toThrow();
});
});
});