refactoring remote builder
parent
610e21cfed
commit
0777907008
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
673
src/model/aws.ts
673
src/model/aws.ts
|
|
@ -1,673 +0,0 @@
|
|||
import * as SDK from 'aws-sdk';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import * as fs from 'fs';
|
||||
import * as core from '@actions/core';
|
||||
import * as zlib from 'zlib';
|
||||
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
const repositoryDirectoryName = 'repo';
|
||||
const efsDirectoryName = 'data';
|
||||
const cacheDirectoryName = 'cache';
|
||||
|
||||
class AWS {
|
||||
static async runBuildJob(buildParameters, baseImage) {
|
||||
try {
|
||||
const nanoid = customAlphabet(alphabet, 4);
|
||||
const buildUid = `${process.env.GITHUB_RUN_NUMBER}-${buildParameters.platform
|
||||
.replace('Standalone', '')
|
||||
.replace('standalone', '')}-${nanoid()}`;
|
||||
const branchName = process.env.GITHUB_REF?.split('/').reverse()[0];
|
||||
const token: string = buildParameters.githubToken;
|
||||
const defaultSecretsArray = [
|
||||
{
|
||||
ParameterKey: 'GithubToken',
|
||||
EnvironmentVariable: 'GITHUB_TOKEN',
|
||||
ParameterValue: token,
|
||||
},
|
||||
];
|
||||
|
||||
core.info('Starting part 1/4 (clone from github and restore cache)');
|
||||
await this.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
'alpine/git',
|
||||
['/bin/sh'],
|
||||
[
|
||||
'-c',
|
||||
`apk update;
|
||||
apk add unzip;
|
||||
apk add git-lfs;
|
||||
apk add jq;
|
||||
# Get source repo for project to be built and game-ci repo for utilties
|
||||
git clone https://${buildParameters.githubToken}@github.com/${process.env.GITHUB_REPOSITORY}.git ${buildUid}/${repositoryDirectoryName} -q
|
||||
git clone https://${buildParameters.githubToken}@github.com/game-ci/unity-builder.git ${buildUid}/builder -q
|
||||
cd /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/
|
||||
git checkout $GITHUB_SHA
|
||||
cd /${efsDirectoryName}/
|
||||
# Look for usable cache
|
||||
if [ ! -d ${cacheDirectoryName} ]; then
|
||||
mkdir ${cacheDirectoryName}
|
||||
fi
|
||||
cd ${cacheDirectoryName}
|
||||
if [ ! -d "${branchName}" ]; then
|
||||
mkdir "${branchName}"
|
||||
fi
|
||||
cd "${branchName}"
|
||||
echo " "
|
||||
echo "Cached Libraries for ${branchName} from previous builds:"
|
||||
ls
|
||||
echo " "
|
||||
libDir="/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library"
|
||||
if [ -d "$libDir" ]; then
|
||||
rm -r "$libDir"
|
||||
echo "Setup .gitignore to ignore Library folder and remove it from builds"
|
||||
fi
|
||||
echo 'Checking cache'
|
||||
# Restore cache
|
||||
latest=$(ls -t | head -1)
|
||||
if [ ! -z "$latest" ]; then
|
||||
echo "Library cache exists from build $latest from ${branchName}"
|
||||
echo 'Creating empty Library folder for cache'
|
||||
mkdir "$libDir"
|
||||
unzip -q $latest -d '/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library/.'
|
||||
else
|
||||
echo 'Cache does not exist'
|
||||
fi
|
||||
# Print out important directories
|
||||
echo ' '
|
||||
echo 'Repo:'
|
||||
ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/
|
||||
echo ' '
|
||||
echo 'Project:'
|
||||
ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}
|
||||
echo ' '
|
||||
echo 'Library:'
|
||||
ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library/
|
||||
echo ' '
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/`,
|
||||
[
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: process.env.GITHUB_SHA,
|
||||
},
|
||||
],
|
||||
defaultSecretsArray,
|
||||
);
|
||||
|
||||
core.info('Starting part 2/4 (build unity project)');
|
||||
|
||||
const buildSecrets = new Array();
|
||||
|
||||
buildSecrets.push(defaultSecretsArray);
|
||||
|
||||
if (process.env.UNITY_LICENSE)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnityLicense',
|
||||
EnvironmentVariable: 'UNITY_LICENSE',
|
||||
ParameterValue: process.env.UNITY_LICENSE,
|
||||
});
|
||||
|
||||
if (process.env.UNITY_EMAIL)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnityEmail',
|
||||
EnvironmentVariable: 'UNITY_EMAIL',
|
||||
ParameterValue: process.env.UNITY_EMAIL,
|
||||
});
|
||||
|
||||
if (process.env.UNITY_PASSWORD)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnityPassword',
|
||||
EnvironmentVariable: 'UNITY_PASSWORD',
|
||||
ParameterValue: process.env.UNITY_PASSWORD,
|
||||
});
|
||||
|
||||
if (process.env.UNITY_SERIAL)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnitySerial',
|
||||
EnvironmentVariable: 'UNITY_SERIAL',
|
||||
ParameterValue: process.env.UNITY_SERIAL,
|
||||
});
|
||||
|
||||
if (buildParameters.androidKeystoreBase64)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'AndroidKeystoreBase64',
|
||||
EnvironmentVariable: 'ANDROID_KEYSTORE_BASE64',
|
||||
ParameterValue: buildParameters.androidKeystoreBase64,
|
||||
});
|
||||
|
||||
if (buildParameters.androidKeystorePass)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'AndroidKeystorePass',
|
||||
EnvironmentVariable: 'ANDROID_KEYSTORE_PASS',
|
||||
ParameterValue: buildParameters.androidKeystorePass,
|
||||
});
|
||||
|
||||
if (buildParameters.androidKeyaliasPass)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'AndroidKeyAliasPass',
|
||||
EnvironmentVariable: 'AWS_ACCESS_KEY_ALIAS_PASS',
|
||||
ParameterValue: buildParameters.androidKeyaliasPass,
|
||||
});
|
||||
|
||||
await this.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
baseImage.toString(),
|
||||
['/bin/sh'],
|
||||
[
|
||||
'-c',
|
||||
`
|
||||
cp -r /${efsDirectoryName}/${buildUid}/builder/dist/default-build-script/ /UnityBuilderAction;
|
||||
cp -r /${efsDirectoryName}/${buildUid}/builder/dist/entrypoint.sh /entrypoint.sh;
|
||||
cp -r /${efsDirectoryName}/${buildUid}/builder/dist/steps/ /steps;
|
||||
chmod -R +x /entrypoint.sh;
|
||||
chmod -R +x /steps;
|
||||
/entrypoint.sh;
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/`,
|
||||
[
|
||||
{
|
||||
name: 'ContainerMemory',
|
||||
value: buildParameters.remoteBuildMemory,
|
||||
},
|
||||
{
|
||||
name: 'ContainerCpu',
|
||||
value: buildParameters.remoteBuildCpu,
|
||||
},
|
||||
{
|
||||
name: 'GITHUB_WORKSPACE',
|
||||
value: `/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/`,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
buildSecrets,
|
||||
);
|
||||
core.info('Starting part 3/4 (zip unity build and Library for caching)');
|
||||
// Cleanup
|
||||
await this.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
'alpine',
|
||||
['/bin/sh'],
|
||||
[
|
||||
'-c',
|
||||
`
|
||||
apk update
|
||||
apk add zip
|
||||
cd Library
|
||||
zip -q -r lib-${buildUid}.zip .*
|
||||
mv lib-${buildUid}.zip /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip
|
||||
cd ../../
|
||||
zip -q -r build-${buildUid}.zip ${buildParameters.buildPath}/*
|
||||
mv build-${buildUid}.zip /${efsDirectoryName}/${buildUid}/build-${buildUid}.zip
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}`,
|
||||
[
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: process.env.GITHUB_SHA,
|
||||
},
|
||||
],
|
||||
defaultSecretsArray,
|
||||
);
|
||||
|
||||
core.info('Starting part 4/4 (upload build to s3)');
|
||||
await this.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
'amazon/aws-cli',
|
||||
['/bin/sh'],
|
||||
[
|
||||
'-c',
|
||||
`
|
||||
aws s3 cp ${buildUid}/build-${buildUid}.zip s3://game-ci-storage/
|
||||
# no need to upload Library cache for now
|
||||
# aws s3 cp /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip s3://game-ci-storage/
|
||||
rm -r ${buildUid}
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/`,
|
||||
[
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: process.env.GITHUB_SHA,
|
||||
},
|
||||
{
|
||||
name: 'AWS_DEFAULT_REGION',
|
||||
value: process.env.AWS_DEFAULT_REGION,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
ParameterKey: 'AWSAccessKeyID',
|
||||
EnvironmentVariable: 'AWS_ACCESS_KEY_ID',
|
||||
ParameterValue: process.env.AWS_ACCESS_KEY_ID,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'AWSSecretAccessKey',
|
||||
EnvironmentVariable: 'AWS_SECRET_ACCESS_KEY',
|
||||
ParameterValue: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
...defaultSecretsArray,
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
core.setFailed(error);
|
||||
core.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async run(
|
||||
buildUid: string,
|
||||
stackName: string,
|
||||
image: string,
|
||||
entrypoint: string[],
|
||||
commands,
|
||||
mountdir,
|
||||
workingdir,
|
||||
environment,
|
||||
secrets,
|
||||
) {
|
||||
const ECS = new SDK.ECS();
|
||||
const CF = new SDK.CloudFormation();
|
||||
|
||||
const taskDef = await this.setupCloudFormations(
|
||||
CF,
|
||||
buildUid,
|
||||
stackName,
|
||||
image,
|
||||
entrypoint,
|
||||
commands,
|
||||
mountdir,
|
||||
workingdir,
|
||||
secrets,
|
||||
);
|
||||
|
||||
await this.runTask(taskDef, ECS, CF, environment, buildUid);
|
||||
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
}
|
||||
|
||||
static async setupCloudFormations(
|
||||
CF,
|
||||
buildUid: string,
|
||||
stackName: string,
|
||||
image: string,
|
||||
entrypoint: string[],
|
||||
commands,
|
||||
mountdir,
|
||||
workingdir,
|
||||
secrets,
|
||||
) {
|
||||
const logid = customAlphabet(alphabet, 9)();
|
||||
commands[1] += `
|
||||
echo "${logid}"
|
||||
`;
|
||||
const taskDefStackName = `${stackName}-${buildUid}`;
|
||||
let taskDefCloudFormation = fs.readFileSync(`${__dirname}/cloud-formations/task-def-formation.yml`, 'utf8');
|
||||
for (const secret of secrets) {
|
||||
const insertionStringParameters = 'p1 - input';
|
||||
const insertionStringSecrets = 'p2 - secret';
|
||||
const insertionStringContainerSecrets = 'p3 - container def';
|
||||
const indexp1 =
|
||||
taskDefCloudFormation.search(insertionStringParameters) + insertionStringParameters.length + '\n'.length;
|
||||
const parameterTemplate = `
|
||||
${secret.ParameterKey}:
|
||||
Type: String
|
||||
Default: ''
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, indexp1),
|
||||
parameterTemplate,
|
||||
taskDefCloudFormation.slice(indexp1),
|
||||
].join('');
|
||||
const indexp2 =
|
||||
taskDefCloudFormation.search(insertionStringSecrets) + insertionStringSecrets.length + '\n'.length;
|
||||
const secretTemplate = `
|
||||
${secret.ParameterKey}Secret:
|
||||
Type: AWS::SecretsManager::Secret
|
||||
Properties:
|
||||
Name: !Join [ "", [ '${secret.ParameterKey}', !Ref BUILDID ] ]
|
||||
SecretString: !Ref ${secret.ParameterKey}
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, indexp2),
|
||||
secretTemplate,
|
||||
taskDefCloudFormation.slice(indexp2),
|
||||
].join('');
|
||||
const indexp3 =
|
||||
taskDefCloudFormation.search(insertionStringContainerSecrets) +
|
||||
insertionStringContainerSecrets.length +
|
||||
'\n'.length;
|
||||
const containerDefinitionSecretTemplate = `
|
||||
- Name: '${secret.EnvironmentVariable ? secret.EnvironmentVariable : secret.ParameterKey}'
|
||||
ValueFrom: !Ref ${secret.ParameterKey}Secret
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, indexp3),
|
||||
containerDefinitionSecretTemplate,
|
||||
taskDefCloudFormation.slice(indexp3),
|
||||
].join('');
|
||||
delete secret.EnvironmentVariable;
|
||||
}
|
||||
|
||||
core.info(taskDefCloudFormation);
|
||||
|
||||
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,
|
||||
},
|
||||
...secrets,
|
||||
],
|
||||
}).promise();
|
||||
core.info('Creating worker cluster...');
|
||||
|
||||
const cleanupTaskDefStackName = `${taskDefStackName}-cleanup`;
|
||||
const cleanupCloudFormation = fs.readFileSync(`${__dirname}/cloud-formations/cloudformation-stack-ttl.yml`, 'utf8');
|
||||
await CF.createStack({
|
||||
StackName: cleanupTaskDefStackName,
|
||||
TemplateBody: cleanupCloudFormation,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
Parameters: [
|
||||
{
|
||||
ParameterKey: 'StackName',
|
||||
ParameterValue: taskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'DeleteStackName',
|
||||
ParameterValue: cleanupTaskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'TTL',
|
||||
ParameterValue: '100',
|
||||
},
|
||||
{
|
||||
ParameterKey: 'BUILDID',
|
||||
ParameterValue: buildUid,
|
||||
},
|
||||
],
|
||||
}).promise();
|
||||
core.info('Creating cleanup cluster...');
|
||||
|
||||
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();
|
||||
|
||||
// in the future we should offer a parameter to choose if you want the guarnteed shutdown.
|
||||
core.info('Worker cluster created successfully (skipping wait for cleanup cluster to be ready)');
|
||||
|
||||
return {
|
||||
taskDefStackName,
|
||||
taskDefCloudFormation,
|
||||
taskDefStackNameTTL: cleanupTaskDefStackName,
|
||||
ttlCloudFormation: cleanupCloudFormation,
|
||||
taskDefResources,
|
||||
baseResources,
|
||||
logid,
|
||||
};
|
||||
}
|
||||
|
||||
static async runTask(taskDef, ECS, CF, environment, buildUid) {
|
||||
const cluster =
|
||||
taskDef.baseResources.StackResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
|
||||
const taskDefinition =
|
||||
taskDef.taskDefResources.StackResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')
|
||||
?.PhysicalResourceId || '';
|
||||
const SubnetOne =
|
||||
taskDef.baseResources.StackResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')
|
||||
?.PhysicalResourceId || '';
|
||||
const SubnetTwo =
|
||||
taskDef.baseResources.StackResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')
|
||||
?.PhysicalResourceId || '';
|
||||
const ContainerSecurityGroup =
|
||||
taskDef.baseResources.StackResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')
|
||||
?.PhysicalResourceId || '';
|
||||
const streamName =
|
||||
taskDef.taskDefResources.StackResources?.find((x) => x.LogicalResourceId === 'KinesisStream')
|
||||
?.PhysicalResourceId || '';
|
||||
|
||||
const task = await ECS.runTask({
|
||||
cluster,
|
||||
taskDefinition,
|
||||
platformVersion: '1.4.0',
|
||||
overrides: {
|
||||
containerOverrides: [
|
||||
{
|
||||
name: taskDef.taskDefStackName,
|
||||
environment: [...environment, { name: 'BUILDID', value: buildUid }],
|
||||
},
|
||||
],
|
||||
},
|
||||
launchType: 'FARGATE',
|
||||
networkConfiguration: {
|
||||
awsvpcConfiguration: {
|
||||
subnets: [SubnetOne, SubnetTwo],
|
||||
assignPublicIp: 'ENABLED',
|
||||
securityGroups: [ContainerSecurityGroup],
|
||||
},
|
||||
},
|
||||
}).promise();
|
||||
|
||||
core.info('Task is starting on worker cluster');
|
||||
const taskArn = task.tasks?.[0].taskArn || '';
|
||||
|
||||
try {
|
||||
await ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
|
||||
} catch (error) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
const describeTasks = await ECS.describeTasks({
|
||||
tasks: [taskArn],
|
||||
cluster,
|
||||
}).promise();
|
||||
core.info(`Task has ended ${describeTasks.tasks?.[0].containers?.[0].lastStatus}`);
|
||||
core.setFailed(error);
|
||||
core.error(error);
|
||||
}
|
||||
core.info(`Task is running on worker cluster`);
|
||||
await this.streamLogsUntilTaskStops(ECS, CF, taskDef, cluster, taskArn, streamName);
|
||||
await ECS.waitFor('tasksStopped', { cluster, tasks: [taskArn] }).promise();
|
||||
const exitCode = (
|
||||
await ECS.describeTasks({
|
||||
tasks: [taskArn],
|
||||
cluster,
|
||||
}).promise()
|
||||
).tasks?.[0].containers?.[0].exitCode;
|
||||
if (exitCode !== 0) {
|
||||
try {
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
} catch (error) {
|
||||
core.warning(`failed to cleanup ${error}`);
|
||||
}
|
||||
core.error(`job failed with exit code ${exitCode}`);
|
||||
throw new Error(`job failed with exit code ${exitCode}`);
|
||||
} else {
|
||||
core.info(`Task has finished successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
static async streamLogsUntilTaskStops(ECS: AWS.ECS, CF, taskDef, clusterName, taskArn, kinesisStreamName) {
|
||||
// watching logs
|
||||
const kinesis = new SDK.Kinesis();
|
||||
|
||||
const getTaskData = async () => {
|
||||
const tasks = await ECS.describeTasks({
|
||||
cluster: clusterName,
|
||||
tasks: [taskArn],
|
||||
}).promise();
|
||||
return tasks.tasks?.[0];
|
||||
};
|
||||
|
||||
const stream = await kinesis
|
||||
.describeStream({
|
||||
StreamName: kinesisStreamName,
|
||||
})
|
||||
.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: taskDef.taskDefStackNameTTL }).promise();
|
||||
|
||||
core.info(`Task status is ${(await getTaskData())?.lastStatus}`);
|
||||
|
||||
const logBaseUrl = `https://${SDK.config.region}.console.aws.amazon.com/cloudwatch/home?region=${SDK.config.region}#logsV2:log-groups/log-group/${taskDef.taskDefStackName}`;
|
||||
core.info(`You can also see the logs at AWS Cloud Watch: ${logBaseUrl}`);
|
||||
|
||||
let readingLogs = true;
|
||||
let timestamp: number = 0;
|
||||
while (readingLogs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const taskData = await getTaskData();
|
||||
if (taskData?.lastStatus !== 'RUNNING') {
|
||||
if (timestamp === 0) {
|
||||
core.info('Task stopped, streaming end of logs');
|
||||
timestamp = Date.now();
|
||||
}
|
||||
if (timestamp !== 0 && Date.now() - timestamp < 30000) {
|
||||
core.info('Task status is not RUNNING for 30 seconds, last query for logs');
|
||||
readingLogs = false;
|
||||
}
|
||||
}
|
||||
const records = await kinesis
|
||||
.getRecords({
|
||||
ShardIterator: iterator,
|
||||
})
|
||||
.promise();
|
||||
iterator = records.NextShardIterator || '';
|
||||
if (records.Records.length > 0 && iterator) {
|
||||
for (let index = 0; index < records.Records.length; index++) {
|
||||
const json = JSON.parse(
|
||||
zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'),
|
||||
);
|
||||
if (json.messageType === 'DATA_MESSAGE') {
|
||||
for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) {
|
||||
if (json.logEvents[logEventsIndex].message.includes(taskDef.logid)) {
|
||||
core.info('End of task logs');
|
||||
readingLogs = false;
|
||||
} else {
|
||||
core.info(json.logEvents[logEventsIndex].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async cleanupResources(CF, taskDef) {
|
||||
await CF.deleteStack({
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
|
||||
await CF.deleteStack({
|
||||
StackName: taskDef.taskDefStackNameTTL,
|
||||
}).promise();
|
||||
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
|
||||
// Currently too slow and causes too much waiting
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: taskDef.taskDefStackNameTTL,
|
||||
}).promise();
|
||||
|
||||
core.info('Cleanup complete');
|
||||
}
|
||||
|
||||
static onlog(batch) {
|
||||
for (const log of batch) {
|
||||
core.info(`log: ${log}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default AWS;
|
||||
|
|
@ -5,7 +5,33 @@ import UnityVersioning from './unity-versioning';
|
|||
import Versioning from './versioning';
|
||||
|
||||
class BuildParameters {
|
||||
static async create() {
|
||||
public version!: string;
|
||||
public customImage!: string;
|
||||
public runnerTempPath: string | undefined;
|
||||
public platform!: string;
|
||||
public projectPath!: string;
|
||||
public buildName!: string;
|
||||
public buildPath!: string;
|
||||
public buildFile!: string;
|
||||
public buildMethod!: string;
|
||||
public buildVersion!: string;
|
||||
public androidVersionCode!: string;
|
||||
public androidKeystoreName!: string;
|
||||
public androidKeystoreBase64!: string;
|
||||
public androidKeystorePass!: string;
|
||||
public androidKeyaliasName!: string;
|
||||
public androidKeyaliasPass!: string;
|
||||
public customParameters!: string;
|
||||
public remoteBuildCluster!: string;
|
||||
public awsStackName!: string;
|
||||
public kubeConfig!: string;
|
||||
public githubToken!: string;
|
||||
public remoteBuildMemory!: string;
|
||||
public remoteBuildCpu!: string;
|
||||
public kubeVolumeSize!: string;
|
||||
public kubeVolume!: string;
|
||||
|
||||
static async create(): Promise<BuildParameters> {
|
||||
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle);
|
||||
|
||||
const unityVersion = UnityVersioning.determineUnityVersion(Input.projectPath, Input.unityVersion);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import Project from './project';
|
|||
import Unity from './unity';
|
||||
import Versioning from './versioning';
|
||||
import Kubernetes from './kubernetes';
|
||||
import AWS from './aws';
|
||||
import RemoteBuilder from './remote-builder/remote-builder';
|
||||
|
||||
export {
|
||||
Action,
|
||||
|
|
@ -25,5 +25,5 @@ export {
|
|||
Unity,
|
||||
Versioning,
|
||||
Kubernetes,
|
||||
AWS,
|
||||
RemoteBuilder as AWS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// @ts-ignore
|
||||
import { Client, KubeConfig } from 'kubernetes-client';
|
||||
import Request from 'kubernetes-client/backends/request';
|
||||
import BuildParameters from './build-parameters';
|
||||
|
||||
const core = require('@actions/core');
|
||||
const base64 = require('base-64');
|
||||
|
|
@ -17,7 +18,7 @@ class Kubernetes {
|
|||
private static jobName: string;
|
||||
private static namespace: string;
|
||||
|
||||
static async runBuildJob(buildParameters, baseImage) {
|
||||
static async runBuildJob(buildParameters: BuildParameters, baseImage) {
|
||||
const kubeconfig = new KubeConfig();
|
||||
kubeconfig.loadFromString(base64.decode(buildParameters.kubeConfig));
|
||||
const backend = new Request({ kubeconfig });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,402 @@
|
|||
import * as SDK from 'aws-sdk';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import RemoteBuilderSecret from './remote-builder-secret';
|
||||
import RemoteBuilderEnvironmentVariable from './remote-builder-environment-variable';
|
||||
import * as fs from 'fs';
|
||||
import * as core from '@actions/core';
|
||||
import * as zlib from 'zlib';
|
||||
import RemoteBuilderTaskDef from './remote-builder-task-def';
|
||||
import RemoteBuilderAlphabet from './remote-builder-alphabet';
|
||||
|
||||
class AWS {
|
||||
static async run(
|
||||
buildId: string,
|
||||
stackName: string,
|
||||
image: string,
|
||||
commands: string[],
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: RemoteBuilderEnvironmentVariable[],
|
||||
secrets: RemoteBuilderSecret[],
|
||||
) {
|
||||
const ECS = new SDK.ECS();
|
||||
const CF = new SDK.CloudFormation();
|
||||
const entrypoint = ['/bin/sh'];
|
||||
|
||||
const taskDef = await this.setupCloudFormations(
|
||||
CF,
|
||||
buildId,
|
||||
stackName,
|
||||
image,
|
||||
entrypoint,
|
||||
commands,
|
||||
mountdir,
|
||||
workingdir,
|
||||
environment,
|
||||
secrets,
|
||||
);
|
||||
|
||||
await this.runTask(taskDef, ECS, CF, environment, buildId);
|
||||
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
}
|
||||
|
||||
static async setupCloudFormations(
|
||||
CF: SDK.CloudFormation,
|
||||
buildUid: string,
|
||||
stackName: string,
|
||||
image: string,
|
||||
entrypoint: string[],
|
||||
commands: string[],
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: RemoteBuilderEnvironmentVariable[],
|
||||
secrets: RemoteBuilderSecret[],
|
||||
): Promise<RemoteBuilderTaskDef> {
|
||||
const logid = customAlphabet(RemoteBuilderAlphabet.alphabet, 9)();
|
||||
commands[1] += `
|
||||
echo "${logid}"
|
||||
`;
|
||||
const taskDefStackName = `${stackName}-${buildUid}`;
|
||||
let taskDefCloudFormation = this.readTaskCloudFormationTemplate();
|
||||
for (const secret of secrets) {
|
||||
const insertionStringParameters = 'p1 - input';
|
||||
const insertionStringSecrets = 'p2 - secret';
|
||||
const insertionStringContainerSecrets = 'p3 - container def';
|
||||
const indexp1 =
|
||||
taskDefCloudFormation.search(insertionStringParameters) + insertionStringParameters.length + '\n'.length;
|
||||
const parameterTemplate = `
|
||||
${secret.ParameterKey}:
|
||||
Type: String
|
||||
Default: ''
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, indexp1),
|
||||
parameterTemplate,
|
||||
taskDefCloudFormation.slice(indexp1),
|
||||
].join('');
|
||||
const indexp2 =
|
||||
taskDefCloudFormation.search(insertionStringSecrets) + insertionStringSecrets.length + '\n'.length;
|
||||
const secretTemplate = `
|
||||
${secret.ParameterKey}Secret:
|
||||
Type: AWS::SecretsManager::Secret
|
||||
Properties:
|
||||
Name: !Join [ "", [ '${secret.ParameterKey}', !Ref BUILDID ] ]
|
||||
SecretString: !Ref ${secret.ParameterKey}
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, indexp2),
|
||||
secretTemplate,
|
||||
taskDefCloudFormation.slice(indexp2),
|
||||
].join('');
|
||||
const indexp3 =
|
||||
taskDefCloudFormation.search(insertionStringContainerSecrets) +
|
||||
insertionStringContainerSecrets.length +
|
||||
'\n'.length;
|
||||
const containerDefinitionSecretTemplate = `
|
||||
- Name: '${secret.EnvironmentVariable ? secret.EnvironmentVariable : secret.ParameterKey}'
|
||||
ValueFrom: !Ref ${secret.ParameterKey}Secret
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, indexp3),
|
||||
containerDefinitionSecretTemplate,
|
||||
taskDefCloudFormation.slice(indexp3),
|
||||
].join('');
|
||||
}
|
||||
for (const environmentVariable of environment) {
|
||||
const insertionStringKey = 'p1 - input';
|
||||
const index = taskDefCloudFormation.search(insertionStringKey) + insertionStringKey.length + '\n'.length;
|
||||
const parameterTemplate = `
|
||||
${environmentVariable.name}:
|
||||
Type: String
|
||||
Default: ''
|
||||
`;
|
||||
taskDefCloudFormation = [
|
||||
taskDefCloudFormation.slice(0, index),
|
||||
parameterTemplate,
|
||||
taskDefCloudFormation.slice(index),
|
||||
].join('');
|
||||
}
|
||||
|
||||
core.info(taskDefCloudFormation);
|
||||
|
||||
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,
|
||||
},
|
||||
...secrets,
|
||||
],
|
||||
}).promise();
|
||||
core.info('Creating worker cluster...');
|
||||
|
||||
const cleanupTaskDefStackName = `${taskDefStackName}-cleanup`;
|
||||
const cleanupCloudFormation = fs.readFileSync(`${__dirname}/cloud-formations/cloudformation-stack-ttl.yml`, 'utf8');
|
||||
await CF.createStack({
|
||||
StackName: cleanupTaskDefStackName,
|
||||
TemplateBody: cleanupCloudFormation,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
Parameters: [
|
||||
{
|
||||
ParameterKey: 'StackName',
|
||||
ParameterValue: taskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'DeleteStackName',
|
||||
ParameterValue: cleanupTaskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'TTL',
|
||||
ParameterValue: '100',
|
||||
},
|
||||
{
|
||||
ParameterKey: 'BUILDID',
|
||||
ParameterValue: buildUid,
|
||||
},
|
||||
],
|
||||
}).promise();
|
||||
core.info('Creating cleanup cluster...');
|
||||
|
||||
try {
|
||||
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise();
|
||||
} catch (error) {
|
||||
core.error(error);
|
||||
}
|
||||
|
||||
const taskDefResources = (
|
||||
await CF.describeStackResources({
|
||||
StackName: taskDefStackName,
|
||||
}).promise()
|
||||
).StackResources;
|
||||
|
||||
const baseResources = (await CF.describeStackResources({ StackName: stackName }).promise()).StackResources;
|
||||
|
||||
// in the future we should offer a parameter to choose if you want the guarnteed shutdown.
|
||||
core.info('Worker cluster created successfully (skipping wait for cleanup cluster to be ready)');
|
||||
|
||||
return {
|
||||
taskDefStackName,
|
||||
taskDefCloudFormation,
|
||||
taskDefStackNameTTL: cleanupTaskDefStackName,
|
||||
ttlCloudFormation: cleanupCloudFormation,
|
||||
taskDefResources,
|
||||
baseResources,
|
||||
logid,
|
||||
};
|
||||
}
|
||||
|
||||
static readTaskCloudFormationTemplate(): string {
|
||||
return fs.readFileSync(`${__dirname}/cloud-formations/task-def-formation.yml`, 'utf8');
|
||||
}
|
||||
|
||||
static async runTask(
|
||||
taskDef: RemoteBuilderTaskDef,
|
||||
ECS: AWS.ECS,
|
||||
CF: AWS.CloudFormation,
|
||||
environment: RemoteBuilderEnvironmentVariable[],
|
||||
buildUid: string,
|
||||
) {
|
||||
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
|
||||
const taskDefinition =
|
||||
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || '';
|
||||
const SubnetOne =
|
||||
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || '';
|
||||
const SubnetTwo =
|
||||
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')?.PhysicalResourceId || '';
|
||||
const ContainerSecurityGroup =
|
||||
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')?.PhysicalResourceId || '';
|
||||
const streamName =
|
||||
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || '';
|
||||
|
||||
const task = await ECS.runTask({
|
||||
cluster,
|
||||
taskDefinition,
|
||||
platformVersion: '1.4.0',
|
||||
overrides: {
|
||||
containerOverrides: [
|
||||
{
|
||||
name: taskDef.taskDefStackName,
|
||||
environment: [...environment, { name: 'BUILDID', value: buildUid }],
|
||||
},
|
||||
],
|
||||
},
|
||||
launchType: 'FARGATE',
|
||||
networkConfiguration: {
|
||||
awsvpcConfiguration: {
|
||||
subnets: [SubnetOne, SubnetTwo],
|
||||
assignPublicIp: 'ENABLED',
|
||||
securityGroups: [ContainerSecurityGroup],
|
||||
},
|
||||
},
|
||||
}).promise();
|
||||
|
||||
core.info('Task is starting on worker cluster');
|
||||
const taskArn = task.tasks?.[0].taskArn || '';
|
||||
|
||||
try {
|
||||
await ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
|
||||
} catch (error) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
const describeTasks = await ECS.describeTasks({
|
||||
tasks: [taskArn],
|
||||
cluster,
|
||||
}).promise();
|
||||
core.info(`Task has ended ${describeTasks.tasks?.[0].containers?.[0].lastStatus}`);
|
||||
core.setFailed(error);
|
||||
core.error(error);
|
||||
}
|
||||
core.info(`Task is running on worker cluster`);
|
||||
await this.streamLogsUntilTaskStops(ECS, CF, taskDef, cluster, taskArn, streamName);
|
||||
await ECS.waitFor('tasksStopped', { cluster, tasks: [taskArn] }).promise();
|
||||
const exitCode = (
|
||||
await ECS.describeTasks({
|
||||
tasks: [taskArn],
|
||||
cluster,
|
||||
}).promise()
|
||||
).tasks?.[0].containers?.[0].exitCode;
|
||||
if (exitCode !== 0) {
|
||||
try {
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
} catch (error) {
|
||||
core.warning(`failed to cleanup ${error}`);
|
||||
}
|
||||
core.error(`job failed with exit code ${exitCode}`);
|
||||
throw new Error(`job failed with exit code ${exitCode}`);
|
||||
} else {
|
||||
core.info(`Task has finished successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
static async streamLogsUntilTaskStops(
|
||||
ECS: AWS.ECS,
|
||||
CF: AWS.CloudFormation,
|
||||
taskDef: RemoteBuilderTaskDef,
|
||||
clusterName: string,
|
||||
taskArn: string,
|
||||
kinesisStreamName: string,
|
||||
) {
|
||||
// watching logs
|
||||
const kinesis = new SDK.Kinesis();
|
||||
|
||||
const getTaskData = async () => {
|
||||
const tasks = await ECS.describeTasks({
|
||||
cluster: clusterName,
|
||||
tasks: [taskArn],
|
||||
}).promise();
|
||||
return tasks.tasks?.[0];
|
||||
};
|
||||
|
||||
const stream = await kinesis
|
||||
.describeStream({
|
||||
StreamName: kinesisStreamName,
|
||||
})
|
||||
.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: taskDef.taskDefStackNameTTL }).promise();
|
||||
|
||||
core.info(`Task status is ${(await getTaskData())?.lastStatus}`);
|
||||
|
||||
const logBaseUrl = `https://${SDK.config.region}.console.aws.amazon.com/cloudwatch/home?region=${SDK.config.region}#logsV2:log-groups/log-group/${taskDef.taskDefStackName}`;
|
||||
core.info(`You can also see the logs at AWS Cloud Watch: ${logBaseUrl}`);
|
||||
|
||||
let readingLogs = true;
|
||||
let timestamp: number = 0;
|
||||
while (readingLogs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const taskData = await getTaskData();
|
||||
if (taskData?.lastStatus !== 'RUNNING') {
|
||||
if (timestamp === 0) {
|
||||
core.info('Task stopped, streaming end of logs');
|
||||
timestamp = Date.now();
|
||||
}
|
||||
if (timestamp !== 0 && Date.now() - timestamp < 30000) {
|
||||
core.info('Task status is not RUNNING for 30 seconds, last query for logs');
|
||||
readingLogs = false;
|
||||
}
|
||||
}
|
||||
const records = await kinesis
|
||||
.getRecords({
|
||||
ShardIterator: iterator,
|
||||
})
|
||||
.promise();
|
||||
iterator = records.NextShardIterator || '';
|
||||
if (records.Records.length > 0 && iterator) {
|
||||
for (let index = 0; index < records.Records.length; index++) {
|
||||
const json = JSON.parse(
|
||||
zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'),
|
||||
);
|
||||
if (json.messageType === 'DATA_MESSAGE') {
|
||||
for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) {
|
||||
if (json.logEvents[logEventsIndex].message.includes(taskDef.logid)) {
|
||||
core.info('End of task logs');
|
||||
readingLogs = false;
|
||||
} else {
|
||||
core.info(json.logEvents[logEventsIndex].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async cleanupResources(CF: AWS.CloudFormation, taskDef: RemoteBuilderTaskDef) {
|
||||
await CF.deleteStack({
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
|
||||
await CF.deleteStack({
|
||||
StackName: taskDef.taskDefStackNameTTL,
|
||||
}).promise();
|
||||
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
|
||||
// Currently too slow and causes too much waiting
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: taskDef.taskDefStackNameTTL,
|
||||
}).promise();
|
||||
|
||||
core.info('Cleanup complete');
|
||||
}
|
||||
}
|
||||
export default AWS;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
class RemoteBuilderAlphabet {
|
||||
static alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
export default RemoteBuilderAlphabet;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class RemoteBuilderEnvironmentVariable {
|
||||
public name!: string;
|
||||
public value!: string;
|
||||
}
|
||||
export default RemoteBuilderEnvironmentVariable;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
class RemoteBuilderSecret {
|
||||
public ParameterKey!: string;
|
||||
public EnvironmentVariable!: string;
|
||||
public ParameterValue!: string;
|
||||
}
|
||||
export default RemoteBuilderSecret;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import * as AWS from 'aws-sdk';
|
||||
|
||||
class RemoteBuilderTaskDef {
|
||||
public taskDefStackName!: string;
|
||||
public taskDefCloudFormation!: string;
|
||||
public taskDefStackNameTTL!: string;
|
||||
public ttlCloudFormation!: string;
|
||||
public taskDefResources: AWS.CloudFormation.StackResources | undefined;
|
||||
public baseResources: AWS.CloudFormation.StackResources | undefined;
|
||||
public logid!: string;
|
||||
}
|
||||
export default RemoteBuilderTaskDef;
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
import { customAlphabet } from 'nanoid';
|
||||
import AWS from './aws';
|
||||
import * as core from '@actions/core';
|
||||
import RemoteBuilderAlphabet from './remote-builder-alphabet';
|
||||
import { BuildParameters } from '..';
|
||||
const repositoryDirectoryName = 'repo';
|
||||
const efsDirectoryName = 'data';
|
||||
const cacheDirectoryName = 'cache';
|
||||
|
||||
class RemoteBuilder {
|
||||
static async runBuildJob(buildParameters: BuildParameters, baseImage) {
|
||||
try {
|
||||
const nanoid = customAlphabet(RemoteBuilderAlphabet.alphabet, 4);
|
||||
const buildUid = `${process.env.GITHUB_RUN_NUMBER}-${buildParameters.platform
|
||||
.replace('Standalone', '')
|
||||
.replace('standalone', '')}-${nanoid()}`;
|
||||
const branchName = process.env.GITHUB_REF?.split('/').reverse()[0];
|
||||
const token: string = buildParameters.githubToken;
|
||||
const defaultSecretsArray = [
|
||||
{
|
||||
ParameterKey: 'GithubToken',
|
||||
EnvironmentVariable: 'GITHUB_TOKEN',
|
||||
ParameterValue: token,
|
||||
},
|
||||
];
|
||||
|
||||
core.info('Starting part 1/4 (clone from github and restore cache)');
|
||||
await AWS.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
'alpine/git',
|
||||
[
|
||||
'-c',
|
||||
`apk update;
|
||||
apk add unzip;
|
||||
apk add git-lfs;
|
||||
apk add jq;
|
||||
# Get source repo for project to be built and game-ci repo for utilties
|
||||
git clone https://${buildParameters.githubToken}@github.com/${process.env.GITHUB_REPOSITORY}.git ${buildUid}/${repositoryDirectoryName} -q
|
||||
git clone https://${buildParameters.githubToken}@github.com/game-ci/unity-builder.git ${buildUid}/builder -q
|
||||
cd /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/
|
||||
git checkout $GITHUB_SHA
|
||||
cd /${efsDirectoryName}/
|
||||
# Look for usable cache
|
||||
if [ ! -d ${cacheDirectoryName} ]; then
|
||||
mkdir ${cacheDirectoryName}
|
||||
fi
|
||||
cd ${cacheDirectoryName}
|
||||
if [ ! -d "${branchName}" ]; then
|
||||
mkdir "${branchName}"
|
||||
fi
|
||||
cd "${branchName}"
|
||||
echo " "
|
||||
echo "Cached Libraries for ${branchName} from previous builds:"
|
||||
ls
|
||||
echo " "
|
||||
libDir="/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library"
|
||||
if [ -d "$libDir" ]; then
|
||||
rm -r "$libDir"
|
||||
echo "Setup .gitignore to ignore Library folder and remove it from builds"
|
||||
fi
|
||||
echo 'Checking cache'
|
||||
# Restore cache
|
||||
latest=$(ls -t | head -1)
|
||||
if [ ! -z "$latest" ]; then
|
||||
echo "Library cache exists from build $latest from ${branchName}"
|
||||
echo 'Creating empty Library folder for cache'
|
||||
mkdir "$libDir"
|
||||
unzip -q $latest -d '/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library/.'
|
||||
else
|
||||
echo 'Cache does not exist'
|
||||
fi
|
||||
# Print out important directories
|
||||
echo ' '
|
||||
echo 'Repo:'
|
||||
ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/
|
||||
echo ' '
|
||||
echo 'Project:'
|
||||
ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}
|
||||
echo ' '
|
||||
echo 'Library:'
|
||||
ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library/
|
||||
echo ' '
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/`,
|
||||
[
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: process.env.GITHUB_SHA || '',
|
||||
},
|
||||
],
|
||||
defaultSecretsArray,
|
||||
);
|
||||
|
||||
core.info('Starting part 2/4 (build unity project)');
|
||||
|
||||
const buildSecrets = new Array();
|
||||
|
||||
buildSecrets.push(defaultSecretsArray);
|
||||
|
||||
if (process.env.UNITY_LICENSE)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnityLicense',
|
||||
EnvironmentVariable: 'UNITY_LICENSE',
|
||||
ParameterValue: process.env.UNITY_LICENSE,
|
||||
});
|
||||
|
||||
if (process.env.UNITY_EMAIL)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnityEmail',
|
||||
EnvironmentVariable: 'UNITY_EMAIL',
|
||||
ParameterValue: process.env.UNITY_EMAIL,
|
||||
});
|
||||
|
||||
if (process.env.UNITY_PASSWORD)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnityPassword',
|
||||
EnvironmentVariable: 'UNITY_PASSWORD',
|
||||
ParameterValue: process.env.UNITY_PASSWORD,
|
||||
});
|
||||
|
||||
if (process.env.UNITY_SERIAL)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'UnitySerial',
|
||||
EnvironmentVariable: 'UNITY_SERIAL',
|
||||
ParameterValue: process.env.UNITY_SERIAL,
|
||||
});
|
||||
|
||||
if (buildParameters.androidKeystoreBase64)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'AndroidKeystoreBase64',
|
||||
EnvironmentVariable: 'ANDROID_KEYSTORE_BASE64',
|
||||
ParameterValue: buildParameters.androidKeystoreBase64,
|
||||
});
|
||||
|
||||
if (buildParameters.androidKeystorePass)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'AndroidKeystorePass',
|
||||
EnvironmentVariable: 'ANDROID_KEYSTORE_PASS',
|
||||
ParameterValue: buildParameters.androidKeystorePass,
|
||||
});
|
||||
|
||||
if (buildParameters.androidKeyaliasPass)
|
||||
buildSecrets.push({
|
||||
ParameterKey: 'AndroidKeyAliasPass',
|
||||
EnvironmentVariable: 'AWS_ACCESS_KEY_ALIAS_PASS',
|
||||
ParameterValue: buildParameters.androidKeyaliasPass,
|
||||
});
|
||||
|
||||
await AWS.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
baseImage.toString(),
|
||||
[
|
||||
'-c',
|
||||
`
|
||||
cp -r /${efsDirectoryName}/${buildUid}/builder/dist/default-build-script/ /UnityBuilderAction;
|
||||
cp -r /${efsDirectoryName}/${buildUid}/builder/dist/entrypoint.sh /entrypoint.sh;
|
||||
cp -r /${efsDirectoryName}/${buildUid}/builder/dist/steps/ /steps;
|
||||
chmod -R +x /entrypoint.sh;
|
||||
chmod -R +x /steps;
|
||||
/entrypoint.sh;
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/`,
|
||||
[
|
||||
{
|
||||
name: 'ContainerMemory',
|
||||
value: buildParameters.remoteBuildMemory,
|
||||
},
|
||||
{
|
||||
name: 'ContainerCpu',
|
||||
value: buildParameters.remoteBuildCpu,
|
||||
},
|
||||
{
|
||||
name: 'GITHUB_WORKSPACE',
|
||||
value: `/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/`,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
buildSecrets,
|
||||
);
|
||||
core.info('Starting part 3/4 (zip unity build and Library for caching)');
|
||||
// Cleanup
|
||||
await AWS.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
'alpine',
|
||||
[
|
||||
'-c',
|
||||
`
|
||||
apk update
|
||||
apk add zip
|
||||
cd Library
|
||||
zip -q -r lib-${buildUid}.zip .*
|
||||
mv lib-${buildUid}.zip /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip
|
||||
cd ../../
|
||||
zip -q -r build-${buildUid}.zip ${buildParameters.buildPath}/*
|
||||
mv build-${buildUid}.zip /${efsDirectoryName}/${buildUid}/build-${buildUid}.zip
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}`,
|
||||
[
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: process.env.GITHUB_SHA || '',
|
||||
},
|
||||
],
|
||||
defaultSecretsArray,
|
||||
);
|
||||
|
||||
core.info('Starting part 4/4 (upload build to s3)');
|
||||
await AWS.run(
|
||||
buildUid,
|
||||
buildParameters.awsStackName,
|
||||
'amazon/aws-cli',
|
||||
[
|
||||
'-c',
|
||||
`
|
||||
aws s3 cp ${buildUid}/build-${buildUid}.zip s3://game-ci-storage/
|
||||
# no need to upload Library cache for now
|
||||
# aws s3 cp /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip s3://game-ci-storage/
|
||||
rm -r ${buildUid}
|
||||
`,
|
||||
],
|
||||
`/${efsDirectoryName}`,
|
||||
`/${efsDirectoryName}/`,
|
||||
[
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: process.env.GITHUB_SHA || '',
|
||||
},
|
||||
{
|
||||
name: 'AWS_DEFAULT_REGION',
|
||||
value: process.env.AWS_DEFAULT_REGION || '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
ParameterKey: 'AWSAccessKeyID',
|
||||
EnvironmentVariable: 'AWS_ACCESS_KEY_ID',
|
||||
ParameterValue: process.env.AWS_ACCESS_KEY_ID || '',
|
||||
},
|
||||
{
|
||||
ParameterKey: 'AWSSecretAccessKey',
|
||||
EnvironmentVariable: 'AWS_SECRET_ACCESS_KEY',
|
||||
ParameterValue: process.env.AWS_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
...defaultSecretsArray,
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
core.setFailed(error);
|
||||
core.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default RemoteBuilder;
|
||||
Loading…
Reference in New Issue