typescript aws

pull/235/head
mdugdale 2021-03-27 22:34:09 +00:00
parent 224f973562
commit c14c2fe14b
6 changed files with 537 additions and 9 deletions

59
.github/workflows/aws-tests.yml vendored 100644
View File

@ -0,0 +1,59 @@
name: AWS
on:
push: { branches: [aws, aws-ts] }
env:
AWS_REGION: "eu-west-1"
jobs:
buildForAllPlatforms:
name: AWS Fargate Build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
# - 2019.2.11f1
- 2019.3.15f1
targetPlatform:
#- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
#- StandaloneWindows64 # Build a Windows 64-bit standalone.
- StandaloneLinux64 # Build a Linux 64-bit standalone.
#- iOS # Build an iOS player.
#- Android # Build an Android .apk.
#- WebGL # WebGL.
# - StandaloneWindows # Build a Windows standalone.
# - WSAPlayer # Build an Windows Store Apps player.
# - PS4 # Build a PS4 Standalone.
# - XboxOne # Build a Xbox One Standalone.
# - tvOS # Build to Apple's tvOS platform.
# - Switch # Build a Nintendo Switch player
# steps
steps:
- name: Checkout (default)
uses: actions/checkout@v2
if: github.event.event_type != 'pull_request_target'
with:
lfs: true
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-2
- uses: ./
id: aws-fargate-unity-build
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-2
with:
remoteBuildCluster: aws
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
githubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@ -30,6 +30,10 @@ inputs:
required: false
default: ''
description: 'Path to a Namespace.Class.StaticMethod to run to perform the build.'
remoteBuildCluster:
default: ''
required: false
description: 'To use a remote build cluster specify either, aws or k8s as well as the required parameters.'
kubeConfig:
default: ''
required: false

View File

@ -1,5 +1,5 @@
import * as core from '@actions/core';
import { Action, BuildParameters, Cache, Docker, ImageTag, Kubernetes, Output } from './model';
import { Action, BuildParameters, Cache, Docker, ImageTag, Kubernetes, Output, AWS } from './model';
async function run() {
try {
@ -10,14 +10,27 @@ async function run() {
const buildParameters = await BuildParameters.create();
const baseImage = new ImageTag(buildParameters);
if (buildParameters.kubeConfig) {
core.info('Building with Kubernetes');
await Kubernetes.runBuildJob(buildParameters, baseImage);
} else {
// Build docker image
// TODO: No image required (instead use a version published to dockerhub for the action, supply credentials for github cloning)
const builtImage = await Docker.build({ path: actionFolder, dockerfile, baseImage });
await Docker.run(builtImage, { workspace, ...buildParameters });
let builtImage;
switch (buildParameters.remoteBuildCluster) {
case 'k8s':
core.info('Building with Kubernetes');
await Kubernetes.runBuildJob(buildParameters, baseImage);
break;
case 'aws':
core.info('Building with AWS');
await AWS.runBuildJob(buildParameters, baseImage);
break;
// default and local case
default:
core.info('Building locally');
// Build docker image
// TODO: No image required (instead use a version published to dockerhub for the action, supply credentials for github cloning)
builtImage = await Docker.build({ path: actionFolder, dockerfile, baseImage });
await Docker.run(builtImage, { workspace, ...buildParameters });
break;
}
// Set output

447
src/model/aws.ts 100644
View File

@ -0,0 +1,447 @@
/* eslint-disable no-plusplus */
/* eslint-disable no-await-in-loop */
import * as SDK from 'aws-sdk';
import { nanoid } from 'nanoid';
const fs = require('fs');
const core = require('@actions/core');
const zlib = require('zlib');
class AWS {
static async runBuildJob(buildParameters, baseImage) {
try{
let buildUid = nanoid();
await this.run(buildUid,
buildParameters.awsStackName,
'alpine/git',
['/bin/sh'],
[
'-c',
`apk update;
apk add git-lfs;
apk add jq;
ls;
git clone https://$GITHUB_TOKEN@github.com/${process.env.GITHUB_REPOSITORY}.git ${buildUid}/repo;
git clone https://$GITHUB_TOKEN@github.com/webbertakken/unity-builder.git ${buildUid}/builder;
cd ${buildUid}/repo;
git checkout $GITHUB_SHA;
`],
'/data',
'/data/',
[
{
name: 'GITHUB_SHA',
value: process.env.GITHUB_SHA,
},
],
[
{
ParameterKey: 'GithubToken',
ParameterValue: buildParameters.githubToken,
},
],
);
await this.run(buildUid,
buildParameters.awsStackName,
baseImage.toString(),
['/bin/sh'],
['-c', `
cp -r /data/${buildUid}/builder/action/default-build-script /UnityBuilderAction;
cp -r /data/${buildUid}/builder/action/entrypoint.sh /entrypoint.sh;
cp -r /data/${buildUid}/builder/action/steps /steps;
ls;
chmod -R +x /entrypoint.sh;
chmod -R +x /steps;
/entrypoint.sh;
ls
`],
'/data',
`/data/${buildUid}/repo/`,
[
{
name: 'GITHUB_WORKSPACE',
value: `/data/${buildUid}/repo/`,
},
{
name: 'PROJECT_PATH',
value: buildParameters.projectPath,
},
{
name: 'BUILD_PATH',
value: buildParameters.buildPath,
},
{
name: 'BUILD_FILE',
value: buildParameters.buildFile,
},
{
name: 'BUILD_NAME',
value: buildParameters.buildName,
},
{
name: 'BUILD_METHOD',
value: buildParameters.buildMethod,
},
{
name: 'CUSTOM_PARAMETERS',
value: buildParameters.customParameters,
},
{
name: 'BUILD_TARGET',
value: buildParameters.platform,
},
{
name: 'ANDROID_VERSION_CODE',
value: buildParameters.androidVersionCode.toString(),
},
{
name: 'ANDROID_KEYSTORE_NAME',
value: buildParameters.androidKeystoreName,
},
{
name: 'ANDROID_KEYALIAS_NAME',
value: buildParameters.androidKeyaliasName,
},
],
[
{
ParameterKey: 'GithubToken',
ParameterValue: buildParameters.githubToken,
},
{
ParameterKey: 'UnityLicense',
ParameterValue: process.env.UNITY_LICENSE?process.env.UNITY_LICENSE:'0'
},
{
ParameterKey: 'UnityEmail',
ParameterValue: process.env.UNITY_EMAIL?process.env.UNITY_EMAIL:'0'
},
{
ParameterKey: 'UnityPassword',
ParameterValue: process.env.UNITY_PASSWORD?process.env.UNITY_PASSWORD:'0'
},
{
ParameterKey: 'UnitySerial',
ParameterValue: process.env.UNITY_SERIAL?process.env.UNITY_SERIAL:'0'
},
{
ParameterKey: 'AndroidKeystoreBase64',
ParameterValue: buildParameters.androidKeystoreBase64?buildParameters.androidKeystoreBase64:'0'
},
{
ParameterKey: 'AndroidKeystorePass',
ParameterValue: buildParameters.androidKeystorePass?buildParameters.androidKeystorePass:'0'
},
{
ParameterKey: 'AndroidKeyAliasPass',
ParameterValue: buildParameters.androidKeyaliasPass?buildParameters.androidKeyaliasPass:'0'
},
]
);
// Cleanup
await this.run(buildUid,
buildParameters.awsStackName,
'alpine',
['/bin/sh'],
[
'-c',
`
apk update;
apk add zip
zip -r ./${buildUid}/output.zip ./${buildUid}/repo/build
ls
`],
'/data',
'/data/',
[
{
name: 'GITHUB_SHA',
value: process.env.GITHUB_SHA,
},
],
[
{
ParameterKey: 'GithubToken',
ParameterValue: buildParameters.githubToken,
},
],
);
await this.run(buildUid,
buildParameters.awsStackName,
'amazon/aws-cli',
['/bin/sh'],
[
'-c',
`
aws s3 cp ./${buildUid}/output.zip s3://game-ci-storage/${buildUid}
rm -r ${buildUid}
ls
`],
'/data',
'/data/',
[
{
name: 'GITHUB_SHA',
value: process.env.GITHUB_SHA,
},
{
name: 'AWS_DEFAULT_REGION',
value: process.env.AWS_DEFAULT_REGION,
},
],
[
{
ParameterKey: 'GithubToken',
ParameterValue: buildParameters.githubToken,
},
{
ParameterKey: 'AWSAccessKeyID',
ParameterValue: process.env.AWS_ACCESS_KEY_ID,
},
{
ParameterKey: 'AWSSecretAccessKey',
ParameterValue: process.env.AWS_SECRET_ACCESS_KEY,
},
],
);
}
catch(error){
core.setFailed(error);
core.error(error);
}
}
static async run(buildUid, stackName, image, entrypoint, commands, mountdir, workingdir, environment, secrets) {
const ECS = new SDK.ECS();
const CF = new SDK.CloudFormation();
const taskDefStackName = `${stackName}-taskDef-${image}-${buildUid}`
.toString()
.replace(/[^\da-z]/gi, '');
core.info('Creating build job resources');
const taskDefCloudFormation = fs.readFileSync(`${__dirname}/task-def-formation.yml`, 'utf8');
await CF.createStack({
StackName: taskDefStackName,
TemplateBody: taskDefCloudFormation,
Parameters: [
{
ParameterKey: 'ImageUrl',
ParameterValue: image,
},
{
ParameterKey: 'ServiceName',
ParameterValue: taskDefStackName,
},
{
ParameterKey: 'Command',
ParameterValue: commands.join(','),
},
{
ParameterKey: 'EntryPoint',
ParameterValue: entrypoint.join(','),
},
{
ParameterKey: 'WorkingDirectory',
ParameterValue: workingdir,
},
{
ParameterKey: 'EFSMountDirectory',
ParameterValue: mountdir,
},
{
ParameterKey: 'BUILDID',
ParameterValue: buildUid,
}
].concat(secrets),
}).promise();
const ttlCloudFormation = fs.readFileSync(`${__dirname}/cloudformation-stack-ttl.yml`, 'utf8');
await CF.createStack({
StackName: taskDefStackName+"-ttl",
TemplateBody: ttlCloudFormation,
Capabilities: [ "CAPABILITY_IAM" ],
Parameters: [
{
ParameterKey: 'StackName',
ParameterValue: taskDefStackName,
},
{
ParameterKey: 'TTL',
ParameterValue: "100",
},
],
}).promise();
try{
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise();
}catch(error){
core.error(error);
}
const taskDefResources = await CF.describeStackResources({
StackName: taskDefStackName,
}).promise();
const baseResources = await CF.describeStackResources({ StackName: stackName }).promise();
const clusterName = baseResources.StackResources?.find(
(x) => x.LogicalResourceId === 'ECSCluster',
)?.PhysicalResourceId || "";
const task = await ECS.runTask({
cluster: clusterName,
taskDefinition: taskDefResources.StackResources?.find(
(x) => x.LogicalResourceId === 'TaskDefinition',
)?.PhysicalResourceId || "",
platformVersion: '1.4.0',
overrides: {
containerOverrides: [
{
name: taskDefStackName,
environment: environment.concat([
{name:'BUILDID', value: buildUid}
]),
},
],
},
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
subnets: [
baseResources.StackResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || "",
baseResources.StackResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')?.PhysicalResourceId || "",
],
assignPublicIp: 'ENABLED',
securityGroups: [
baseResources.StackResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')?.PhysicalResourceId || "",
],
},
},
}, undefined).promise();
core.info('Build job is starting');
try {
await ECS.waitFor('tasksRunning', {tasks: [task.tasks?.[0].taskArn||""],
cluster: clusterName,
}).promise();
} catch (error) {
await new Promise((resolve) => setTimeout(resolve, 3000));
let describeTasks = await ECS.describeTasks({
tasks: [task.tasks?.[0].taskArn||""],
cluster: clusterName,
}).promise();
core.info(
`Build job has ended ${describeTasks.tasks?.[0].containers?.[0].lastStatus}`
);
core.setFailed(error);
core.error(error);
}
core.info(`Build job is running`);
// watching logs
const kinesis = new SDK.Kinesis();
const getTaskStatus = async () => {
const tasks = await ECS.describeTasks({
cluster: clusterName,
tasks: [task.tasks?.[0].taskArn||""],
}).promise();
return tasks.tasks?.[0].lastStatus;
};
const stream = await kinesis.describeStream({
StreamName: taskDefResources.StackResources?.find(
(x) => x.LogicalResourceId === 'KinesisStream',
)?.PhysicalResourceId||"",
}, undefined).promise();
let iterator = (
await kinesis
.getShardIterator({
ShardIteratorType: 'TRIM_HORIZON',
StreamName: stream.StreamDescription.StreamName,
ShardId: stream.StreamDescription.Shards[0].ShardId,
})
.promise()
).ShardIterator||"";
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName+"-ttl" }).promise();
core.info(`Task status is ${await getTaskStatus()}`);
const logBaseUrl = `https://console.aws.amazon.com/cloudwatch/home?region=${SDK.config.region}#logsV2:log-groups/${taskDefStackName}`;
core.info(`You can also watch the logs at AWS Cloud Watch: ${logBaseUrl}`);
let readingLogs = true;
while (readingLogs) {
await new Promise((resolve) => setTimeout(resolve, 1500));
if ((await getTaskStatus()) !== 'RUNNING') {
readingLogs = false;
await new Promise((resolve) => setTimeout(resolve, 35000));
}
const records = await kinesis
.getRecords({
ShardIterator: iterator,
})
.promise();
iterator = records.NextShardIterator||"";
if (records.Records.length > 0) {
for (let index = 0; index < records.Records.length; index++) {
const json = JSON.parse(
zlib.gunzipSync(Buffer.from(records.Records[index].Data.toString(), 'base64')).toString('utf8'),
);
if (json.messageType === 'DATA_MESSAGE') {
for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) {
core.info(json.logEvents[logEventsIndex].message);
}
}
}
}
}
await ECS.waitFor('tasksStopped', { cluster: clusterName, tasks: [task.tasks?.[0].taskArn||""]}).promise();
const exitCode = (await ECS.describeTasks({
tasks: [task.tasks?.[0].taskArn||""],
cluster: clusterName,
}).promise()
).tasks?.[0].containers?.[0].exitCode;
if(exitCode!=0){
core.error(`job finished with exit code ${exitCode}`)
}
else{
core.info(`Build job has finished with exit code 0`);
}
await CF.deleteStack({
StackName: taskDefStackName,
}).promise();
await CF.deleteStack({
StackName: taskDefStackName+"-ttl",
}).promise();
await CF.waitFor('stackDeleteComplete', {
StackName: taskDefStackName
}).promise();
await CF.waitFor('stackDeleteComplete', {
StackName: taskDefStackName+"-ttl"
}).promise();
core.info('Cleanup complete');
}
static onlog(batch) {
batch.forEach((log) => {
core.info(`log: ${log}`);
});
}
}
export default AWS;

View File

@ -33,6 +33,7 @@ class BuildParameters {
androidKeyaliasName: Input.androidKeyaliasName,
androidKeyaliasPass: Input.androidKeyaliasPass,
customParameters: Input.customParameters,
remoteBuildCluster: Input.remoteBuildCluster,
kubeConfig: Input.kubeConfig,
githubToken: Input.githubToken,
kubeContainerMemory: Input.kubeContainerMemory,

View File

@ -85,6 +85,10 @@ class Input {
return core.getInput('customParameters') || '';
}
static get remoteBuildCluster(){
return core.getInput('remoteBuildCluster') || '';
}
static get kubeConfig() {
return core.getInput('kubeConfig') || '';
}