From c96b8cf443ba50fdfd86d06d5c27d98e023d5364 Mon Sep 17 00:00:00 2001 From: Frostebite Date: Sun, 23 May 2021 05:08:40 +0100 Subject: [PATCH] Remote builder refactoring (#264) Remote builder refactoring --- .github/workflows/aws-tests.yml | 6 +- dist/cloud-formations/task-def-formation.yml | 111 +--- dist/index.js | Bin 14715098 -> 14728935 bytes dist/index.js.map | Bin 8931240 -> 8948749 bytes src/index.ts | 4 +- src/model/aws.ts | 611 ------------------ src/model/build-parameters.ts | 29 +- src/model/index.ts | 4 +- .../remote-builder/aws-build-platform.ts | 253 ++++++++ src/model/remote-builder/aws-build-runner.ts | 165 +++++ .../remote-builder-constants.ts | 4 + .../remote-builder-environment-variable.ts | 5 + .../remote-builder/remote-builder-secret.ts | 6 + .../remote-builder/remote-builder-task-def.ts | 12 + src/model/remote-builder/remote-builder.ts | 419 ++++++++++++ 15 files changed, 904 insertions(+), 725 deletions(-) delete mode 100644 src/model/aws.ts create mode 100644 src/model/remote-builder/aws-build-platform.ts create mode 100644 src/model/remote-builder/aws-build-runner.ts create mode 100644 src/model/remote-builder/remote-builder-constants.ts create mode 100644 src/model/remote-builder/remote-builder-environment-variable.ts create mode 100644 src/model/remote-builder/remote-builder-secret.ts create mode 100644 src/model/remote-builder/remote-builder-task-def.ts create mode 100644 src/model/remote-builder/remote-builder.ts diff --git a/.github/workflows/aws-tests.yml b/.github/workflows/aws-tests.yml index 0cfb1f2c..0c08dd1f 100644 --- a/.github/workflows/aws-tests.yml +++ b/.github/workflows/aws-tests.yml @@ -1,10 +1,10 @@ name: AWS on: - push: { branches: [aws, aws-ts-clean] } + push: { branches: [aws, remote-builder/refactor] } env: - AWS_REGION: "eu-west-1" + AWS_REGION: 'eu-west-1' jobs: buildForAllPlatforms: @@ -17,7 +17,7 @@ jobs: projectPath: - test-project unityVersion: - # - 2019.2.11f1 + # - 2019.2.11f1 - 2019.3.15f1 targetPlatform: #- StandaloneOSX # Build a macOS standalone (Intel 64-bit). diff --git a/dist/cloud-formations/task-def-formation.yml b/dist/cloud-formations/task-def-formation.yml index c2d3a4e7..9d482978 100644 --- a/dist/cloud-formations/task-def-formation.yml +++ b/dist/cloud-formations/task-def-formation.yml @@ -51,36 +51,7 @@ Parameters: EFSMountDirectory: Type: String Default: '/efsdata' - GithubToken: - Type: String - Default: '0' - UnityLicense: - Type: String - Default: '0' - UnityEmail: - Type: String - Default: '0' - UnityPassword: - Type: String - Default: '0' - UnitySerial: - Type: String - Default: '0' - AndroidKeystoreBase64: - Type: String - Default: '0' - AndroidKeystorePass: - Type: String - Default: '0' - AndroidKeyAliasPass: - Type: String - Default: '0' - AWSAccessKeyID: - Type: String - Default: '0' - AWSSecretAccessKey: - Type: String - Default: '0' + # template secrets p1 - input Mappings: SubnetConfig: VPC: @@ -128,64 +99,8 @@ Resources: 'AWS::CloudFormation::Designer': id: c6f18447-b879-4696-8873-f981b2cedd2b - GithubTokenSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'GithubToken', !Ref BUILDID ] ] - SecretString: !Ref GithubToken - - UnityLicenseSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'UnityLicense', !Ref BUILDID ] ] - SecretString: !Ref UnityLicense - - UnityEmailSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'UnityEmail', !Ref BUILDID ] ] - SecretString: !Ref UnityEmail - - UnityPasswordSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'UnityPassword', !Ref BUILDID ] ] - SecretString: !Ref UnityPassword - - UnitySerialSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'UnitySerial', !Ref BUILDID ] ] - SecretString: !Ref UnitySerial - - AndroidKeystoreBase64Secret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'AndroidKeystoreBase64', !Ref BUILDID ] ] - SecretString: !Ref AndroidKeystoreBase64 - - AndroidKeystorePassSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'AndroidKeystorePass', !Ref BUILDID ] ] - SecretString: !Ref AndroidKeystorePass - - AndroidKeyAliasPassSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'AndroidKeyAliasPass', !Ref BUILDID ] ] - SecretString: !Ref AndroidKeyAliasPass - AWSAccessKeyIDSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'AWSAccessKeyID', !Ref BUILDID ] ] - SecretString: !Ref AWSAccessKeyID - AWSSecretAccessKeySecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ 'AWSSecretAccessKey', !Ref BUILDID ] ] - SecretString: !Ref AWSSecretAccessKey - + # template secrets p2 - secret + TaskDefinition: Type: 'AWS::ECS::TaskDefinition' Properties: @@ -225,29 +140,13 @@ Resources: Environment: - Name: ALLOW_EMPTY_PASSWORD Value: 'yes' + # template - env vars MountPoints: - SourceVolume: efs-data ContainerPath: !Ref EFSMountDirectory ReadOnly: false Secrets: - - Name: 'GITHUB_TOKEN' - ValueFrom: !Ref GithubTokenSecret - - Name: 'UNITY_LICENSE' - ValueFrom: !Ref UnityLicenseSecret - - Name: 'UNITY_EMAIL' - ValueFrom: !Ref UnityEmailSecret - - Name: 'UNITY_PASSWORD' - ValueFrom: !Ref UnityPasswordSecret - - Name: 'UNITY_SERIAL' - ValueFrom: !Ref UnitySerialSecret - - Name: 'ANDROID_KEYSTORE_BASE64' - ValueFrom: !Ref AndroidKeystoreBase64Secret - - Name: 'ANDROID_KEYSTORE_PASS' - ValueFrom: !Ref AndroidKeystorePassSecret - - Name: 'AWS_ACCESS_KEY_ID' - ValueFrom: !Ref AWSAccessKeyIDSecret - - Name: 'AWS_SECRET_ACCESS_KEY' - ValueFrom: !Ref AWSSecretAccessKeySecret + # template secrets p3 - container def LogConfiguration: LogDriver: awslogs Options: diff --git a/dist/index.js b/dist/index.js index ec2fc773caf14b35b12f76f1ced75bb9a4e7736f..38c11e762ce93a9496995431c39c3287345a39d7 100644 GIT binary patch delta 28526 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 delta 21041 zcmdUX2~<>9w)VQGiYkUGCSs0a!o3PzE!fC5XP3ZYPp^N?sX8aq+8 z8Pzz%B*rN*W>jMm?F>eY?MtVPL!0i->o$Ywr1M^9Fz-9()~yQ!P5NK|-*5e{wejA2 z=6&|r^QrY)p3v^Of4g?$^#>nt#ywb|@jZC`aklcL_Q*-CR1!&BZ!kTz4VFsjz;M=| z(myGPr0c_32BnW+eZ{vhhZ+BzCH3iL=F#THlKQeyrH;i338N?=U#gZ{+@CL{(TRL1 zlqyHEa1DxSs-!tgrj@LQwvS?Q^u~X)aN2RZRC46|8?2boe5T1EpA42pl0~xkJ8D}U z)h@f!VQy}9#M8zdC|%TeXCS?6k$h>pMT(~57Acm-7E2-YgGCxnyEjesr)`@g9c>%S zw3Ht&4W%ciYmBsYljKbg$74d4Q@KX^L%d|7gL_#BNeNOMT^O8Wq{<~4EiH?8dC`~z zNl(uuxI*ZscO@^{IFIQlb&%vka}%T>TD8*{La7gGbks2joj#Qy4WYNDYC_0#Wm+(q z7L|L^^@ujDm&H22#X<)YT%p~%@tJRT%{FJwD74V>oyIV=FDI#;l#-*T>p9GaqCPYj>8C{IMactL0Qn@L@<+2ZCYt<`##<{k;L57vFxqwl z*m!Z7G>*Ppr}3isGo;bf%rui}NV1gVbJ2EQQe#mSduQ$>^^- zE+>fYNRf@}DE5+sHgp znn?8{B`2*|Jvf-IkHpR%-Vhp1v7@Bd>G`4l!SvA!=*s7IDN%CJhW)HJ<=>7&eeQ$M zAc0Vte>=A7lU2nb|4wxNoL5y`lLpedYdEkKo7f;?8EiOx+MA{59o0@pi_1(e4P%q( z(F~SAr!rU!UCUsx2IY**C^Rpmg%3)@OoEwmhC0vMY_715YTpB1duE5mhhENPpwKo= zAT1k@b2nrx>*r@}Xqs)Uvbhq?LpTEo=0$qW@Up4u07yG^9NKt~V}UgIkKh>B6C^GF zHjsv#$2U|C@`|spnnzRSZWc;A$FZS4&9-^A<`!Fg0-3*U>Fwb!0Vz$^W|uiFWr#Vx zy20tNnQNP!jpjP=uhuHF)lp+^wzasN%{FtjwR*NKK`^vnnW3frR`Sw-ZO~U0EFsE* zN!uN^6g0IYnoBGxRdz>8%WO*-UdVa?1aQ3`>)W9(4>@vu5<4j!88n%J`Ig3*OJnH2 z^I3xik4Mf6(gOzO&h$)C?GoSI&T zU^p-xS$sCM;a^HN{Ln;#_nTPX_Q*U5_??Q zhALo4EUlmx2t4C{4G~L|DMwwEKn7a)YR5ITS~veM-SGM()N>Ac zTw~wkXx94}Pb=0|F#*Jwpc~wLP*`dGnp~QJjUGA?5wu6 zv?SXc^OC3MPb?~~$eTDWE59(yJko4Q74+a{47cjpOPAGXwKurQM;SmzV~Uv1yEews8qV{&u1xm{oxVG=U?l!ch^}) za0>Ks_hZs2C}z+yL(ve|7)K0pDj=m)g#;J z!mDTS?sHaI-G?qkA_-1Z(~4J5GpUTG_SW>Jts6ilioF>qE1Ix3d(oUIC_6B>{iu8Z z}20fYUGE#<;2`^o?ngzq)o5z11#e3IQ#r3>V{2#n)0pkG=Q% zMluJeD)@+mcsM;;zmYPiC=1r@wrKbr-v0(BsrnlC9Sqht{voAPyaBoaOz__H%0D4_ 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 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; diff --git a/src/model/build-parameters.ts b/src/model/build-parameters.ts index 3fd58b21..e4e119e9 100644 --- a/src/model/build-parameters.ts +++ b/src/model/build-parameters.ts @@ -5,7 +5,34 @@ 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; + public chownFilesTo!: string; + + static async create(): Promise { const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle); const unityVersion = UnityVersioning.determineUnityVersion(Input.projectPath, Input.unityVersion); diff --git a/src/model/index.ts b/src/model/index.ts index 460a2a28..f277b85d 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -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, }; diff --git a/src/model/remote-builder/aws-build-platform.ts b/src/model/remote-builder/aws-build-platform.ts new file mode 100644 index 00000000..b4e1262b --- /dev/null +++ b/src/model/remote-builder/aws-build-platform.ts @@ -0,0 +1,253 @@ +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 RemoteBuilderTaskDef from './remote-builder-task-def'; +import RemoteBuilderConstants from './remote-builder-constants'; +import AWSBuildRunner from './aws-build-runner'; + +class AWSBuildEnvironment { + static async runBuild( + 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, + secrets, + ); + try { + await AWSBuildRunner.runTask(taskDef, ECS, CF, environment, buildId); + } finally { + await this.cleanupResources(CF, taskDef); + } + } + + // static async setupPlatformResources() { + // throw new Error('Method not implemented.'); + // } + + static getParameterTemplate(p1) { + return ` + ${p1}: + Type: String + Default: '' +`; + } + + static getSecretTemplate(p1) { + return ` + ${p1}Secret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ '${p1}', !Ref BUILDID ] ] + SecretString: !Ref ${p1} +`; + } + + static getSecretDefinitionTemplate(p1, p2) { + return ` + - Name: '${p1}' + ValueFrom: !Ref ${p2}Secret +`; + } + + static insertAtTemplate(template, insertionKey, insertion) { + const index = template.search(insertionKey) + insertionKey.length + '\n'.length; + template = [template.slice(0, index), insertion, template.slice(index)].join(''); + return template; + } + + static async setupCloudFormations( + CF: SDK.CloudFormation, + buildUid: string, + stackName: string, + image: string, + entrypoint: string[], + commands: string[], + mountdir: string, + workingdir: string, + secrets: RemoteBuilderSecret[], + ): Promise { + const logid = customAlphabet(RemoteBuilderConstants.alphabet, 9)(); + commands[1] += ` + echo "${logid}" + `; + const taskDefStackName = `${stackName}-${buildUid}`; + let taskDefCloudFormation = this.readTaskCloudFormationTemplate(); + const cleanupTaskDefStackName = `${taskDefStackName}-cleanup`; + const cleanupCloudFormation = fs.readFileSync(`${__dirname}/cloud-formations/cloudformation-stack-ttl.yml`, 'utf8'); + + try { + for (const secret of secrets) { + taskDefCloudFormation = this.insertAtTemplate( + taskDefCloudFormation, + 'p1 - input', + this.getParameterTemplate(secret.ParameterKey.replace(/[^\dA-Za-z]/g, '')), + ); + taskDefCloudFormation = this.insertAtTemplate( + taskDefCloudFormation, + 'p2 - secret', + this.getSecretTemplate(secret.ParameterKey.replace(/[^\dA-Za-z]/g, '')), + ); + taskDefCloudFormation = this.insertAtTemplate( + taskDefCloudFormation, + 'p3 - container def', + this.getSecretDefinitionTemplate(secret.EnvironmentVariable, secret.ParameterKey.replace(/[^\dA-Za-z]/g, '')), + ); + } + const mappedSecrets = secrets.map((x) => { + return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue }; + }); + + 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, + }, + ...mappedSecrets, + ], + }).promise(); + core.info('Creating worker cluster...'); + 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...'); + + await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise(); + } catch (error) { + await AWSBuildEnvironment.handleStackCreationFailure(error, CF, taskDefStackName, taskDefCloudFormation, secrets); + + throw 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, + }; + } + + private static async handleStackCreationFailure( + error: any, + CF: SDK.CloudFormation, + taskDefStackName: string, + taskDefCloudFormation: string, + secrets: RemoteBuilderSecret[], + ) { + core.info(JSON.stringify(secrets, undefined, 4)); + core.info(taskDefCloudFormation); + const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents; + const resources = (await CF.describeStackResources({ StackName: taskDefStackName }).promise()).StackResources; + core.info(JSON.stringify(events, undefined, 4)); + core.info(JSON.stringify(resources, undefined, 4)); + core.error(error); + } + + static readTaskCloudFormationTemplate(): string { + return fs.readFileSync(`${__dirname}/cloud-formations/task-def-formation.yml`, 'utf8'); + } + + static async cleanupResources(CF: SDK.CloudFormation, taskDef: RemoteBuilderTaskDef) { + core.info('Cleanup starting'); + 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 AWSBuildEnvironment; diff --git a/src/model/remote-builder/aws-build-runner.ts b/src/model/remote-builder/aws-build-runner.ts new file mode 100644 index 00000000..f52fd1a6 --- /dev/null +++ b/src/model/remote-builder/aws-build-runner.ts @@ -0,0 +1,165 @@ +import * as AWS from 'aws-sdk'; +import RemoteBuilderEnvironmentVariable from './remote-builder-environment-variable'; +import * as core from '@actions/core'; +import RemoteBuilderTaskDef from './remote-builder-task-def'; +import * as zlib from 'zlib'; + +class AWSBuildRunner { + 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) { + 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 AWS.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://${AWS.config.region}.console.aws.amazon.com/cloudwatch/home?region=${AWS.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); + } + } + } + } + } + } + } +} +export default AWSBuildRunner; diff --git a/src/model/remote-builder/remote-builder-constants.ts b/src/model/remote-builder/remote-builder-constants.ts new file mode 100644 index 00000000..f28d02d6 --- /dev/null +++ b/src/model/remote-builder/remote-builder-constants.ts @@ -0,0 +1,4 @@ +class RemoteBuilderConstants { + static alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; +} +export default RemoteBuilderConstants; diff --git a/src/model/remote-builder/remote-builder-environment-variable.ts b/src/model/remote-builder/remote-builder-environment-variable.ts new file mode 100644 index 00000000..212007a1 --- /dev/null +++ b/src/model/remote-builder/remote-builder-environment-variable.ts @@ -0,0 +1,5 @@ +class RemoteBuilderEnvironmentVariable { + public name!: string; + public value!: string; +} +export default RemoteBuilderEnvironmentVariable; diff --git a/src/model/remote-builder/remote-builder-secret.ts b/src/model/remote-builder/remote-builder-secret.ts new file mode 100644 index 00000000..bc102834 --- /dev/null +++ b/src/model/remote-builder/remote-builder-secret.ts @@ -0,0 +1,6 @@ +class RemoteBuilderSecret { + public ParameterKey!: string; + public EnvironmentVariable!: string; + public ParameterValue!: string; +} +export default RemoteBuilderSecret; diff --git a/src/model/remote-builder/remote-builder-task-def.ts b/src/model/remote-builder/remote-builder-task-def.ts new file mode 100644 index 00000000..e5566128 --- /dev/null +++ b/src/model/remote-builder/remote-builder-task-def.ts @@ -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; diff --git a/src/model/remote-builder/remote-builder.ts b/src/model/remote-builder/remote-builder.ts new file mode 100644 index 00000000..88b0c50a --- /dev/null +++ b/src/model/remote-builder/remote-builder.ts @@ -0,0 +1,419 @@ +import { customAlphabet } from 'nanoid'; +import AWSBuildPlatform from './aws-build-platform'; +import * as core from '@actions/core'; +import RemoteBuilderConstants from './remote-builder-constants'; +import { BuildParameters } from '..'; +const repositoryDirectoryName = 'repo'; +const efsDirectoryName = 'data'; +const cacheDirectoryName = 'cache'; + +class RemoteBuilder { + static SteamDeploy: boolean = false; + static async build(buildParameters: BuildParameters, baseImage) { + try { + this.SteamDeploy = process.env.STEAM_DEPLOY !== undefined || false; + const nanoid = customAlphabet(RemoteBuilderConstants.alphabet, 4); + const buildUid = `${process.env.GITHUB_RUN_NUMBER}-${buildParameters.platform + .replace('Standalone', '') + .replace('standalone', '')}-${nanoid()}`; + const defaultBranchName = + process.env.GITHUB_REF?.split('/') + .filter((x) => { + x = x[0].toUpperCase() + x.slice(1); + return x; + }) + .join('') || ''; + const branchName = + process.env.REMOTE_BUILDER_CACHE !== undefined ? process.env.REMOTE_BUILDER_CACHE : defaultBranchName; + const token: string = buildParameters.githubToken; + const defaultSecretsArray = [ + { + ParameterKey: 'GithubToken', + EnvironmentVariable: 'GITHUB_TOKEN', + ParameterValue: token, + }, + ]; + await RemoteBuilder.SetupStep(buildUid, buildParameters, branchName, defaultSecretsArray); + await RemoteBuilder.BuildStep(buildUid, buildParameters, baseImage, defaultSecretsArray); + await RemoteBuilder.CompressionStep(buildUid, buildParameters, branchName, defaultSecretsArray); + await RemoteBuilder.UploadArtifacts(buildUid, buildParameters, branchName, defaultSecretsArray); + if (this.SteamDeploy) { + await RemoteBuilder.DeployToSteam(buildUid, buildParameters, defaultSecretsArray); + } + } catch (error) { + core.setFailed(error); + core.error(error); + } + } + + private static async SetupStep( + buildUid: string, + buildParameters: BuildParameters, + branchName: string | undefined, + defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], + ) { + core.info('Starting step 1/4 clone and restore cache)'); + await AWSBuildPlatform.runBuild( + 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 + git clone https://${buildParameters.githubToken}@github.com/game-ci/steam-deploy.git ${buildUid}/steam -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 '' + ls "/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}" + 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 $libDir + # purge cache + ${process.env.PURGE_REMOTE_BUILDER_CACHE === undefined ? '#' : ''} rm -r $libDir + 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, + ); + } + + private static async BuildStep( + buildUid: string, + buildParameters: BuildParameters, + baseImage: any, + defaultSecretsArray: any[], + ) { + 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, + }); + core.info('Starting part 2/4 (build unity project)'); + await AWSBuildPlatform.runBuild( + 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, + ); + } + + private static async CompressionStep( + buildUid: string, + buildParameters: BuildParameters, + branchName: string | undefined, + defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], + ) { + core.info('Starting step 3/4 build compression'); + // Cleanup + await AWSBuildPlatform.runBuild( + buildUid, + buildParameters.awsStackName, + 'alpine', + [ + '-c', + ` + apk update + apk add zip + cd Library + zip -r lib-${buildUid}.zip .* + mv lib-${buildUid}.zip /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip + cd ../../ + zip -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('compression step complete'); + } + + private static async UploadArtifacts( + buildUid: string, + buildParameters: BuildParameters, + branchName: string | undefined, + defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], + ) { + core.info('Starting step 4/4 upload build to s3'); + await AWSBuildPlatform.runBuild( + 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/ + ${this.SteamDeploy ? '#' : ''} 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, + ], + ); + } + + private static async DeployToSteam( + buildUid: string, + buildParameters: BuildParameters, + defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], + ) { + core.info('Starting steam deployment'); + await AWSBuildPlatform.runBuild( + buildUid, + buildParameters.awsStackName, + 'cm2network/steamcmd:root', + [ + '-c', + ` + ls + ls / + cp -r /${efsDirectoryName}/${buildUid}/steam/action/entrypoint.sh /entrypoint.sh; + cp -r /${efsDirectoryName}/${buildUid}/steam/action/steps/ /steps; + chmod -R +x /entrypoint.sh; + chmod -R +x /steps; + /entrypoint.sh; + rm -r /${efsDirectoryName}/${buildUid} + `, + ], + `/${efsDirectoryName}`, + `/${efsDirectoryName}/${buildUid}/steam/action/`, + [ + { + name: 'GITHUB_SHA', + value: process.env.GITHUB_SHA || '', + }, + ], + [ + { + EnvironmentVariable: 'INPUT_APPID', + ParameterKey: 'appId', + ParameterValue: process.env.APP_ID || '', + }, + { + EnvironmentVariable: 'INPUT_BUILDDESCRIPTION', + ParameterKey: 'buildDescription', + ParameterValue: process.env.BUILD_DESCRIPTION || '', + }, + { + EnvironmentVariable: 'INPUT_ROOTPATH', + ParameterKey: 'rootPath', + ParameterValue: process.env.ROOT_PATH || '', + }, + { + EnvironmentVariable: 'INPUT_RELEASEBRANCH', + ParameterKey: 'releaseBranch', + ParameterValue: process.env.RELEASE_BRANCH || '', + }, + { + EnvironmentVariable: 'INPUT_LOCALCONTENTSERVER', + ParameterKey: 'localContentServer', + ParameterValue: process.env.LOCAL_CONTENT_SERVER || '', + }, + { + EnvironmentVariable: 'INPUT_PREVIEWENABLED', + ParameterKey: 'previewEnabled', + ParameterValue: process.env.PREVIEW_ENABLED || '', + }, + ...defaultSecretsArray, + ], + ); + } +} +export default RemoteBuilder;