typescript aws
parent
224f973562
commit
c14c2fe14b
|
|
@ -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 }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
31
src/index.ts
31
src/index.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ class Input {
|
|||
return core.getInput('customParameters') || '';
|
||||
}
|
||||
|
||||
static get remoteBuildCluster(){
|
||||
return core.getInput('remoteBuildCluster') || '';
|
||||
}
|
||||
|
||||
static get kubeConfig() {
|
||||
return core.getInput('kubeConfig') || '';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue