From e334dc785ad6e0bf96de76abc8a8c7511958dbab Mon Sep 17 00:00:00 2001 From: Frostebite Date: Fri, 20 Jan 2023 17:40:57 +0000 Subject: [PATCH] Cloud runner develop - better parameterization of s3 usage, improved async workflow and GC, github checks early integration (#479) * custom steps may leave value undefined, will be pulled from env vars * custom steps may leave value undefined, will be pulled from env vars * custom steps may leave value undefined, will be pulled from env vars * add 3 new premade steps, steam-deploy-client, steam-deploy-project, aws-s3-pull-build * fix * fix * fix * continue building async-workflow support * test checks * test checks * test checks * move github checks within build workflow * async workflow test * async workflow test * async workflow test * async workflow test * async workflow test * async workflow test * async workflow test * async workflow test for aws only * async workflow test for aws only * async workflow test for aws only * async workflow test for aws only * cleanup logging * disable lz4 compression by default * disable lz4 compression by default * AWS BASE STACK for tests * AWS BASE STACK for tests * AWS BASE STACK for tests * AWS BASE STACK for tests * AWS BASE STACK for tests * AWS BASE STACK for tests * disable lz4 compression by default * disable lz4 compression by default * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * Update github check with aws log * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * kinesis and subscription filter for logs creation skipped when watchToEnd false * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * cleanup local pipeline, log aws formation * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * async pipeline * workflow * workflow * workflow * workflow * workflow * workflow * workflow * workflow * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 * parameterize s3 --- .../workflows/cloud-runner-async-checks.yml | 3 +- .../workflows/cloud-runner-local-pipeline.yml | 95 ---------- .github/workflows/cloud-runner-pipeline.yml | 130 +++----------- action.yml | 4 + dist/index.js | Bin 21970566 -> 22035185 bytes dist/index.js.map | Bin 16365268 -> 16441712 bytes dist/licenses.txt | Bin 290396 -> 311152 bytes src/model/build-parameters.ts | 2 + src/model/cli/cli.ts | 19 ++ .../cloud-runner/cloud-runner-options.ts | 35 +++- src/model/cloud-runner/cloud-runner.ts | 12 ++ .../providers/aws/aws-job-stack.ts | 53 +++++- .../providers/aws/aws-task-runner.ts | 9 +- .../cloud-formations/base-stack-formation.ts | 5 + .../cleanup-cron-formation.ts | 13 +- .../task-definition-formation.ts | 57 +++--- src/model/cloud-runner/providers/aws/index.ts | 6 + .../providers/aws/services/task-service.ts | 3 +- .../cloud-runner/providers/docker/index.ts | 12 +- src/model/cloud-runner/providers/k8s/index.ts | 14 +- .../k8s/kubernetes-job-spec-factory.ts | 2 + .../providers/k8s/kubernetes-pods.ts | 5 +- .../cloud-runner/remote-client/caching.ts | 14 +- .../services/cloud-runner-custom-hooks.ts | 5 +- .../services/cloud-runner-custom-steps.ts | 109 +++++++++--- .../cloud-runner/services/custom-step.ts | 9 + .../services/follow-log-stream-service.ts | 4 + .../services/shared-workspace-locking.ts | 48 +++--- .../services/task-parameter-serializer.ts | 1 + .../tests/cloud-runner-async-workflow.test.ts | 33 ++++ ...cloud-runner-run-once-custom-hooks.test.ts | 8 + .../cloud-runner-run-twice-caching.test.ts | 6 + .../cloud-runner-run-twice-retaining.test.ts | 9 +- .../cloud-runner-s3-prebuilt-steps.test.ts | 2 +- .../tests/shared-workspace-locking.test.ts | 2 + .../cloud-runner/workflows/async-workflow.ts | 60 +++++++ .../cloud-runner/workflows/custom-workflow.ts | 10 +- .../workflows/workflow-composition-root.ts | 6 + src/model/github.ts | 163 ++++++++++++++++++ 39 files changed, 661 insertions(+), 307 deletions(-) delete mode 100644 .github/workflows/cloud-runner-local-pipeline.yml rename dist/cloud-formations/cloudformation-stack-ttl.yml => src/model/cloud-runner/providers/aws/cloud-formations/cleanup-cron-formation.ts (92%) create mode 100644 src/model/cloud-runner/services/custom-step.ts create mode 100644 src/model/cloud-runner/tests/cloud-runner-async-workflow.test.ts create mode 100644 src/model/cloud-runner/workflows/async-workflow.ts diff --git a/.github/workflows/cloud-runner-async-checks.yml b/.github/workflows/cloud-runner-async-checks.yml index 91e4d1a3..1cdf2a03 100644 --- a/.github/workflows/cloud-runner-async-checks.yml +++ b/.github/workflows/cloud-runner-async-checks.yml @@ -8,7 +8,7 @@ on: required: false default: '' -permissions: +permissions: checks: write env: @@ -44,7 +44,6 @@ jobs: with: lfs: false - run: yarn - - run: yarn run cli --help - run: yarn run cli -m checks-update timeout-minutes: 180 env: diff --git a/.github/workflows/cloud-runner-local-pipeline.yml b/.github/workflows/cloud-runner-local-pipeline.yml deleted file mode 100644 index f44198bf..00000000 --- a/.github/workflows/cloud-runner-local-pipeline.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Cloud Runner Local - -on: - push: { branches: ['!cloud-runner-develop', '!cloud-runner-preview', '!main'] } -# push: { branches: [main] } -# pull_request: -# paths-ignore: -# - '.github/**' - -jobs: - integrationTests: - name: Integration Tests - if: github.event.event_type != 'pull_request_target' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - cloudRunnerCluster: - - local-docker - targetPlatform: - - StandaloneWindows64 # Build a Windows 64-bit standalone. - # steps - steps: - - name: Checkout (default) - uses: actions/checkout@v2 - with: - lfs: true - - run: yarn - - run: yarn run cli --help - - run: yarn run test-i --detectOpenHandles --forceExit --runInBand - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - PROJECT_PATH: ${{ matrix.projectPath }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TARGET_PLATFORM: ${{ matrix.targetPlatform }} - cloudRunnerTests: true - versioning: None - CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} - buildTests: - name: Build Tests - if: github.event.event_type != 'pull_request_target' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - cloudRunnerCluster: - - local-docker - targetPlatform: - - StandaloneOSX # Build a macOS standalone (Intel 64-bit). - - StandaloneWindows64 # Build a Windows 64-bit standalone. - - StandaloneLinux64 # Build a Linux 64-bit standalone. - - WebGL # WebGL. - - iOS # Build an iOS player. - - Android # Build an Android .apk. - # - 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 - with: - lfs: true - - uses: ./ - id: unity-build - timeout-minutes: 25 - env: - CLOUD_RUNNER_BRANCH: ${{ github.ref }} - CLOUD_RUNNER_DEBUG: true - CLOUD_RUNNER_DEBUG_TREE: true - DEBUG: true - PROJECT_PATH: test-project - UNITY_VERSION: 2019.3.15f1 - USE_IL2CPP: false - with: - cloudRunnerTests: true - versioning: None - projectPath: ${{ matrix.projectPath }} - gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} - targetPlatform: ${{ matrix.targetPlatform }} - cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} - - run: | - mv ./cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 - ls - ########################### - # Upload # - ########################### - - uses: actions/upload-artifact@v2 - with: - name: Local Build (${{ matrix.targetPlatform }}) - path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 - retention-days: 14 diff --git a/.github/workflows/cloud-runner-pipeline.yml b/.github/workflows/cloud-runner-pipeline.yml index 4f3b6672..2e16227c 100644 --- a/.github/workflows/cloud-runner-pipeline.yml +++ b/.github/workflows/cloud-runner-pipeline.yml @@ -2,6 +2,12 @@ name: Cloud Runner CI Pipeline on: push: { branches: [cloud-runner-develop, cloud-runner-preview, main] } + workflow_dispatch: + +permissions: + checks: write + contents: read + actions: write env: GKE_ZONE: 'us-central1' @@ -15,7 +21,7 @@ env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: eu-west-2 - AWS_BASE_STACK_NAME: game-ci-github-pipelines + AWS_BASE_STACK_NAME: game-ci-team-pipelines CLOUD_RUNNER_BRANCH: ${{ github.ref }} CLOUD_RUNNER_DEBUG: true CLOUD_RUNNER_DEBUG_TREE: true @@ -24,6 +30,7 @@ env: PROJECT_PATH: test-project UNITY_VERSION: 2019.3.15f1 USE_IL2CPP: false + USE_GKE_GCLOUD_AUTH_PLUGIN: true jobs: integrationTests: @@ -41,14 +48,17 @@ jobs: - name: Checkout (default) uses: actions/checkout@v2 with: - lfs: true - - uses: google-github-actions/setup-gcloud@v0 + lfs: false + - uses: google-github-actions/auth@v1 with: - version: '288.0.0' - service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} - service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + - name: 'Set up Cloud SDK' + uses: 'google-github-actions/setup-gcloud@v1' - name: Get GKE cluster credentials - run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT + run: | + export USE_GKE_GCLOUD_AUTH_PLUGIN=True + gcloud components install gke-gcloud-auth-plugin + gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -56,9 +66,8 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-west-2 - run: yarn - - run: yarn run cli --help - - run: yarn run test "cloud-runner-run-twice-retaining" --detectOpenHandles --forceExit --runInBand - if: matrix.CloudRunnerCluster == 'aws' || matrix.CloudRunnerCluster == 'k8s' + - run: yarn run test "cloud-runner-async-workflow" --detectOpenHandles --forceExit --runInBand + if: matrix.CloudRunnerCluster != 'local-docker' timeout-minutes: 180 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} @@ -68,6 +77,7 @@ jobs: cloudRunnerTests: true versioning: None CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: yarn run test-i --detectOpenHandles --forceExit --runInBand if: matrix.CloudRunnerCluster == 'local-docker' timeout-minutes: 180 @@ -79,10 +89,8 @@ jobs: cloudRunnerTests: true versioning: None CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} - - buildTargetTests: - name: Build Tests - Targets - if: github.event.event_type != 'pull_request_target' + localBuildTests: + name: Local Build Target Tests runs-on: ubuntu-latest strategy: fail-fast: false @@ -93,7 +101,7 @@ jobs: #- k8s targetPlatform: - StandaloneOSX # Build a macOS standalone (Intel 64-bit). - # - StandaloneWindows64 # Build a Windows 64-bit standalone. + - StandaloneWindows64 # Build a Windows 64-bit standalone. - StandaloneLinux64 # Build a Linux 64-bit standalone. - WebGL # WebGL. - iOS # Build an iOS player. @@ -102,21 +110,7 @@ jobs: - name: Checkout (default) uses: actions/checkout@v2 with: - lfs: true - - - uses: google-github-actions/setup-gcloud@v0 - with: - version: '288.0.0' - service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} - service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} - - name: Get GKE cluster credentials - run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT - - 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 + lfs: false - run: yarn - uses: ./ id: unity-build @@ -130,82 +124,10 @@ jobs: gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} targetPlatform: ${{ matrix.targetPlatform }} cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} - customStepFiles: aws-s3-upload-build,aws-s3-pull-cache,aws-s3-upload-cache - run: | - aws s3 cp s3://game-ci-test-storage/cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 - ls - - run: yarn run cli -m list-resources - env: - cloudRunnerTests: true - CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + cp ./cloud-runner-cache/cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/${{ steps.unity-build.outputs.BUILD_ARTIFACT }} ${{ steps.unity-build.outputs.BUILD_ARTIFACT }} - uses: actions/upload-artifact@v2 with: name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }}) - path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 - retention-days: 14 - buildTests: - name: Build Tests - Providers - if: github.event.event_type != 'pull_request_target' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - cloudRunnerCluster: - - aws - - local-docker - - k8s - targetPlatform: - #- StandaloneOSX # Build a macOS standalone (Intel 64-bit). - - StandaloneWindows64 # Build a Windows 64-bit standalone. - #- StandaloneLinux64 # Build a Linux 64-bit standalone. - #- WebGL # WebGL. - #- iOS # Build an iOS player. - #- Android # Build an Android .apk. - # steps - steps: - - name: Checkout (default) - uses: actions/checkout@v2 - with: - lfs: true - - - uses: google-github-actions/setup-gcloud@v0 - with: - version: '288.0.0' - service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} - service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} - - name: Get GKE cluster credentials - run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT - - - 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 - - run: yarn - - uses: ./ - id: unity-build - timeout-minutes: 90 - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - with: - cloudRunnerTests: true - versioning: None - projectPath: test-project - gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} - targetPlatform: ${{ matrix.targetPlatform }} - cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} - customStepFiles: aws-s3-upload-build,aws-s3-pull-cache,aws-s3-upload-cache - - run: | - aws s3 cp s3://game-ci-test-storage/cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 - ls - - run: yarn run cli -m list-resources - if: always() - env: - cloudRunnerTests: true - CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} - - uses: actions/upload-artifact@v2 - with: - name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }}) - path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + path: ${{ steps.unity-build.outputs.BUILD_ARTIFACT }} retention-days: 14 diff --git a/action.yml b/action.yml index 68f93923..f7e111d7 100644 --- a/action.yml +++ b/action.yml @@ -83,6 +83,10 @@ inputs: required: false default: '' description: '[CloudRunner] Github private token to pull from github' + githubOwner: + required: false + default: '' + description: '[CloudRunner] GitHub owner name or organization/team name' chownFilesTo: required: false default: '' diff --git a/dist/index.js b/dist/index.js index fcb92b1b7cb6841601efb788568db148ab8ac351..7ae33ee08554f5ffc67c2ecdc911915710397a41 100644 GIT binary patch delta 43552 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%0A!5B?(<+*#|=T6!_;&}Oi<$Xlc6 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 delta 3380 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 Q*Yo!;$XhEHzvxf=1M~0hB>(^b delta 42 zcmV+_0M-BS{u12W5wN>Gv!YjTm$#W40T_I@y_x}Kw}*IE0=IZp1I6;UfJ_ANa@DL9 A&Hw-a diff --git a/src/model/build-parameters.ts b/src/model/build-parameters.ts index c3c622a8..581036d1 100644 --- a/src/model/build-parameters.ts +++ b/src/model/build-parameters.ts @@ -70,6 +70,7 @@ class BuildParameters { public useLz4Compression!: boolean; public garbageCollectionMaxAge!: number; public constantGarbageCollection!: boolean; + public githubChecks!: boolean; static async create(): Promise { const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle); @@ -153,6 +154,7 @@ class BuildParameters { maxRetainedWorkspaces: CloudRunnerOptions.maxRetainedWorkspaces, constantGarbageCollection: CloudRunnerOptions.constantGarbageCollection, garbageCollectionMaxAge: CloudRunnerOptions.garbageCollectionMaxAge, + githubChecks: CloudRunnerOptions.githubChecks, }; } diff --git a/src/model/cli/cli.ts b/src/model/cli/cli.ts index 392ed28f..687c5451 100644 --- a/src/model/cli/cli.ts +++ b/src/model/cli/cli.ts @@ -109,6 +109,25 @@ export class Cli { return await CloudRunner.run(buildParameter, baseImage.toString()); } + @CliFunction(`async-workflow`, `runs a cloud runner build`) + public static async asyncronousWorkflow(): Promise { + const buildParameter = await BuildParameters.create(); + const baseImage = new ImageTag(buildParameter); + + return await CloudRunner.run(buildParameter, baseImage.toString()); + } + + @CliFunction(`checks-update`, `runs a cloud runner build`) + public static async checksUpdate() { + const input = JSON.parse(process.env.CHECKS_UPDATE || ``); + core.info(`Checks Update ${process.env.CHECKS_UPDATE}`); + if (input.mode === `create`) { + throw new Error(`Not supported: only use update`); + } else if (input.mode === `update`) { + await GitHub.updateGitHubCheckRequest(input.data); + } + } + @CliFunction(`garbage-collect`, `runs garbage collection`) public static async GarbageCollect(): Promise { const buildParameter = await BuildParameters.create(); diff --git a/src/model/cloud-runner/cloud-runner-options.ts b/src/model/cloud-runner/cloud-runner-options.ts index 511c396c..023e4648 100644 --- a/src/model/cloud-runner/cloud-runner-options.ts +++ b/src/model/cloud-runner/cloud-runner-options.ts @@ -56,6 +56,21 @@ class CloudRunnerOptions { return CloudRunnerOptions.getInput('region') || 'eu-west-2'; } + // ### ### ### + // GitHub parameters + // ### ### ### + static get githubChecks(): boolean { + return CloudRunnerOptions.getInput('githubChecks') || false; + } + + static get githubOwner() { + return CloudRunnerOptions.getInput('githubOwner') || CloudRunnerOptions.githubRepo.split(`/`)[0] || false; + } + + static get githubRepoName() { + return CloudRunnerOptions.getInput('githubRepoName') || CloudRunnerOptions.githubRepo.split(`/`)[1] || false; + } + // ### ### ### // Git syncronization parameters // ### ### ### @@ -220,19 +235,31 @@ class CloudRunnerOptions { } static get watchCloudRunnerToEnd(): boolean { - return (CloudRunnerOptions.getInput(`watchToEnd`) || true) !== 'false'; + if (CloudRunnerOptions.asyncCloudRunner) { + return false; + } + + return CloudRunnerOptions.getInput(`watchToEnd`) || true; + } + + static get asyncCloudRunner(): boolean { + return (CloudRunnerOptions.getInput('asyncCloudRunner') || `false`) === `true` || false; } public static get useSharedLargePackages(): boolean { - return (CloudRunnerOptions.getInput(`useSharedLargePackages`) || 'false') !== 'false'; + return (CloudRunnerOptions.getInput(`useSharedLargePackages`) || 'false') === 'true'; } public static get useSharedBuilder(): boolean { - return (CloudRunnerOptions.getInput(`useSharedBuilder`) || true) !== 'false'; + return (CloudRunnerOptions.getInput(`useSharedBuilder`) || 'true') === 'true'; } public static get useLz4Compression(): boolean { - return (CloudRunnerOptions.getInput(`useLz4Compression`) || true) !== false; + return (CloudRunnerOptions.getInput(`useLz4Compression`) || 'false') === 'true'; + } + + public static get useCleanupCron(): boolean { + return (CloudRunnerOptions.getInput(`useCleanupCron`) || 'true') === 'true'; } // ### ### ### diff --git a/src/model/cloud-runner/cloud-runner.ts b/src/model/cloud-runner/cloud-runner.ts index d2916fc3..d1f716a3 100644 --- a/src/model/cloud-runner/cloud-runner.ts +++ b/src/model/cloud-runner/cloud-runner.ts @@ -23,6 +23,7 @@ class CloudRunner { private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[]; static lockedWorkspace: string | undefined; public static readonly retainedWorkspacePrefix: string = `retained-workspace`; + public static githubCheckId; public static setup(buildParameters: BuildParameters) { CloudRunnerLogger.setup(); CloudRunnerLogger.log(`Setting up cloud runner`); @@ -41,6 +42,12 @@ class CloudRunner { // CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`); core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]); } + core.setOutput( + Input.ToEnvVarFormat(`buildArtifact`), + `build-${CloudRunner.buildParameters.buildGuid}.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + }`, + ); } } @@ -68,6 +75,8 @@ class CloudRunner { static async run(buildParameters: BuildParameters, baseImage: string) { CloudRunner.setup(buildParameters); try { + CloudRunner.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid); + if (buildParameters.retainWorkspace) { CloudRunner.lockedWorkspace = `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`; @@ -97,6 +106,7 @@ class CloudRunner { CloudRunner.defaultSecrets, ); if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters.buildGuid); const output = await new WorkflowCompositionRoot().run( new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets), ); @@ -109,6 +119,7 @@ class CloudRunner { ); CloudRunnerLogger.log(`Cleanup complete`); if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, `success`, `success`, `completed`); if (CloudRunner.buildParameters.retainWorkspace) { await SharedWorkspaceLocking.ReleaseWorkspace( @@ -125,6 +136,7 @@ class CloudRunner { return output; } catch (error) { + await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, error, `failure`, `completed`); if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); await CloudRunnerError.handleException(error, CloudRunner.buildParameters, CloudRunner.defaultSecrets); throw error; diff --git a/src/model/cloud-runner/providers/aws/aws-job-stack.ts b/src/model/cloud-runner/providers/aws/aws-job-stack.ts index 9c7703f7..002b3c0b 100644 --- a/src/model/cloud-runner/providers/aws/aws-job-stack.ts +++ b/src/model/cloud-runner/providers/aws/aws-job-stack.ts @@ -5,6 +5,9 @@ import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates'; import CloudRunnerLogger from '../../services/cloud-runner-logger'; import { AWSError } from './aws-error'; import CloudRunner from '../../cloud-runner'; +import { CleanupCronFormation } from './cloud-formations/cleanup-cron-formation'; +import CloudRunnerOptions from '../../cloud-runner-options'; +import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation'; export class AWSJobStack { private baseStackName: string; @@ -38,6 +41,13 @@ export class AWSJobStack { `ContainerMemory: Default: ${Number.parseInt(memory)}`, ); + if (CloudRunnerOptions.watchCloudRunnerToEnd) { + taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate( + taskDefCloudFormation, + '# template resources logstream', + TaskDefinitionFormation.streamLogs, + ); + } for (const secret of secrets) { secret.ParameterKey = `${buildGuid.replace(/[^\dA-Za-z]/g, '')}${secret.ParameterKey.replace( /[^\dA-Za-z]/g, @@ -57,7 +67,7 @@ export class AWSJobStack { ); taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate( taskDefCloudFormation, - 'p2 - secret', + '# template resources secrets', AWSCloudFormationTemplates.getSecretTemplate(`${secret.ParameterKey}`), ); taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate( @@ -132,14 +142,53 @@ export class AWSJobStack { }; try { + CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`); await CF.createStack(createStackInput).promise(); - CloudRunnerLogger.log('Creating cloud runner job'); await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise(); } catch (error) { await AWSError.handleStackCreationFailure(error, CF, taskDefStackName); throw error; } + const createCleanupStackInput: SDK.CloudFormation.CreateStackInput = { + StackName: `${taskDefStackName}-cleanup`, + TemplateBody: CleanupCronFormation.formation, + Capabilities: ['CAPABILITY_IAM'], + Parameters: [ + { + ParameterKey: 'StackName', + ParameterValue: taskDefStackName, + }, + { + ParameterKey: 'DeleteStackName', + ParameterValue: `${taskDefStackName}-cleanup`, + }, + { + ParameterKey: 'TTL', + ParameterValue: `1080`, + }, + { + ParameterKey: 'BUILDGUID', + ParameterValue: CloudRunner.buildParameters.buildGuid, + }, + { + ParameterKey: 'EnvironmentName', + ParameterValue: this.baseStackName, + }, + ], + }; + if (CloudRunnerOptions.useCleanupCron) { + try { + CloudRunnerLogger.log(`Creating job cleanup formation`); + CF.createStack(createCleanupStackInput).promise(); + + // await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise(); + } catch (error) { + await AWSError.handleStackCreationFailure(error, CF, taskDefStackName); + throw error; + } + } + const taskDefResources = ( await CF.describeStackResources({ StackName: taskDefStackName, diff --git a/src/model/cloud-runner/providers/aws/aws-task-runner.ts b/src/model/cloud-runner/providers/aws/aws-task-runner.ts index 6822e382..6d5bd8e3 100644 --- a/src/model/cloud-runner/providers/aws/aws-task-runner.ts +++ b/src/model/cloud-runner/providers/aws/aws-task-runner.ts @@ -9,10 +9,12 @@ import CloudRunner from '../../cloud-runner'; import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks'; import { FollowLogStreamService } from '../../services/follow-log-stream-service'; import CloudRunnerOptions from '../../cloud-runner-options'; +import GitHub from '../../../github'; class AWSTaskRunner { public static ECS: AWS.ECS; public static Kinesis: AWS.Kinesis; + private static readonly encodedUnderscore = `$252F`; static async runTask( taskDef: CloudRunnerAWSTaskDef, environment: CloudRunnerEnvironmentVariable[], @@ -56,7 +58,9 @@ class AWSTaskRunner { CloudRunnerLogger.log('Cloud runner job is starting'); await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster); CloudRunnerLogger.log( - `Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus}`, + `Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus} Watch:${ + CloudRunnerOptions.watchCloudRunnerToEnd + } Async:${CloudRunnerOptions.asyncCloudRunner}`, ); if (!CloudRunnerOptions.watchCloudRunnerToEnd) { const shouldCleanup: boolean = false; @@ -125,8 +129,9 @@ class AWSTaskRunner { const stream = await AWSTaskRunner.getLogStream(kinesisStreamName); let iterator = await AWSTaskRunner.getLogIterator(stream); - const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${Input.region}#logsV2:log-groups/log-group/${CloudRunner.buildParameters.awsBaseStackName}-${CloudRunner.buildParameters.buildGuid}`; + const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${Input.region}#logsV2:log-groups/log-group/${CloudRunner.buildParameters.awsBaseStackName}${AWSTaskRunner.encodedUnderscore}${CloudRunner.buildParameters.awsBaseStackName}-${CloudRunner.buildParameters.buildGuid}`; CloudRunnerLogger.log(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`); + await GitHub.updateGitHubCheck(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`, ``); let shouldReadLogs = true; let shouldCleanup = true; let timestamp: number = 0; diff --git a/src/model/cloud-runner/providers/aws/cloud-formations/base-stack-formation.ts b/src/model/cloud-runner/providers/aws/cloud-formations/base-stack-formation.ts index 3cbe61d7..0913afee 100644 --- a/src/model/cloud-runner/providers/aws/cloud-formations/base-stack-formation.ts +++ b/src/model/cloud-runner/providers/aws/cloud-formations/base-stack-formation.ts @@ -47,6 +47,11 @@ Resources: EnableDnsHostnames: true CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] + MainBucket: + Type: "AWS::S3::Bucket" + Properties: + BucketName: !Ref EnvironmentName + EFSServerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: diff --git a/dist/cloud-formations/cloudformation-stack-ttl.yml b/src/model/cloud-runner/providers/aws/cloud-formations/cleanup-cron-formation.ts similarity index 92% rename from dist/cloud-formations/cloudformation-stack-ttl.yml rename to src/model/cloud-runner/providers/aws/cloud-formations/cleanup-cron-formation.ts index c93578e7..b1f59040 100644 --- a/dist/cloud-formations/cloudformation-stack-ttl.yml +++ b/src/model/cloud-runner/providers/aws/cloud-formations/cleanup-cron-formation.ts @@ -1,4 +1,5 @@ -AWSTemplateFormatVersion: '2010-09-09' +export class CleanupCronFormation { + public static readonly formation: string = `AWSTemplateFormatVersion: '2010-09-09' Description: Schedule automatic deletion of CloudFormation stacks Metadata: AWS::CloudFormation::Interface: @@ -64,10 +65,10 @@ Resources: stackName: !Ref 'StackName' deleteStackName: !Ref 'DeleteStackName' Handler: "index.handler" - Runtime: "python3.6" + Runtime: "python3.9" Timeout: "5" Role: - 'Fn::ImportValue': !Sub '${EnvironmentName}:DeleteCFNLambdaExecutionRole' + 'Fn::ImportValue': !Sub '\${EnvironmentName}:DeleteCFNLambdaExecutionRole' DeleteStackEventRule: DependsOn: - DeleteCFNLambda @@ -130,10 +131,10 @@ Resources: status = cfnresponse.FAILED cfnresponse.send(event, context, status, {}, None) Handler: "index.handler" - Runtime: "python3.6" + Runtime: "python3.9" Timeout: "5" Role: - 'Fn::ImportValue': !Sub '${EnvironmentName}:DeleteCFNLambdaExecutionRole' + 'Fn::ImportValue': !Sub '\${EnvironmentName}:DeleteCFNLambdaExecutionRole' GenerateCronExpression: Type: "Custom::GenerateCronExpression" Version: "1.0" @@ -141,3 +142,5 @@ Resources: Name: !Join [ "", [ 'GenerateCronExpression', !Ref BUILDGUID ] ] ServiceToken: !GetAtt GenerateCronExpLambda.Arn ttl: !Ref 'TTL' +`; +} diff --git a/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts b/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts index 44de060c..4d792b09 100644 --- a/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts +++ b/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts @@ -76,32 +76,10 @@ Resources: Metadata: 'AWS::CloudFormation::Designer': id: aece53ae-b82d-4267-bc16-ed964b05db27 - SubscriptionFilter: - Type: 'AWS::Logs::SubscriptionFilter' - Properties: - FilterPattern: '' - RoleArn: - 'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:CloudWatchIAMRole' - LogGroupName: !Ref LogGroupName - DestinationArn: - 'Fn::GetAtt': - - KinesisStream - - Arn - Metadata: - 'AWS::CloudFormation::Designer': - id: 7f809e91-9e5d-4678-98c1-c5085956c480 - DependsOn: - - LogGroup - - KinesisStream - KinesisStream: - Type: 'AWS::Kinesis::Stream' - Properties: - Name: !Ref ServiceName - ShardCount: 1 - Metadata: - 'AWS::CloudFormation::Designer': - id: c6f18447-b879-4696-8873-f981b2cedd2b - # template secrets p2 - secret + # template resources secrets + + # template resources logstream + TaskDefinition: Type: 'AWS::ECS::TaskDefinition' Properties: @@ -156,5 +134,32 @@ Resources: awslogs-stream-prefix: !Ref ServiceName DependsOn: - LogGroup +`; + public static streamLogs = ` + SubscriptionFilter: + Type: 'AWS::Logs::SubscriptionFilter' + Properties: + FilterPattern: '' + RoleArn: + 'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:CloudWatchIAMRole' + LogGroupName: !Ref LogGroupName + DestinationArn: + 'Fn::GetAtt': + - KinesisStream + - Arn + Metadata: + 'AWS::CloudFormation::Designer': + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + DependsOn: + - LogGroup + - KinesisStream + KinesisStream: + Type: 'AWS::Kinesis::Stream' + Properties: + Name: !Ref ServiceName + ShardCount: 1 + Metadata: + 'AWS::CloudFormation::Designer': + id: c6f18447-b879-4696-8873-f981b2cedd2b `; } diff --git a/src/model/cloud-runner/providers/aws/index.ts b/src/model/cloud-runner/providers/aws/index.ts index 47b0faee..dc8c2322 100644 --- a/src/model/cloud-runner/providers/aws/index.ts +++ b/src/model/cloud-runner/providers/aws/index.ts @@ -13,6 +13,7 @@ import { GarbageCollectionService } from './services/garbage-collection-service' import { ProviderResource } from '../provider-resource'; import { ProviderWorkflow } from '../provider-workflow'; import { TaskService } from './services/task-service'; +import CloudRunnerOptions from '../../cloud-runner-options'; class AWSBuildEnvironment implements ProviderInterface { private baseStackName: string; @@ -133,6 +134,11 @@ class AWSBuildEnvironment implements ProviderInterface { await CF.deleteStack({ StackName: taskDef.taskDefStackName, }).promise(); + if (CloudRunnerOptions.useCleanupCron) { + await CF.deleteStack({ + StackName: `${taskDef.taskDefStackName}-cleanup`, + }).promise(); + } await CF.waitFor('stackDeleteComplete', { StackName: taskDef.taskDefStackName, diff --git a/src/model/cloud-runner/providers/aws/services/task-service.ts b/src/model/cloud-runner/providers/aws/services/task-service.ts index e9972854..4e9ed6a2 100644 --- a/src/model/cloud-runner/providers/aws/services/task-service.ts +++ b/src/model/cloud-runner/providers/aws/services/task-service.ts @@ -4,6 +4,7 @@ import CloudRunnerLogger from '../../../services/cloud-runner-logger'; import { BaseStackFormation } from '../cloud-formations/base-stack-formation'; import AwsTaskRunner from '../aws-task-runner'; import { ListObjectsRequest } from 'aws-sdk/clients/s3'; +import CloudRunner from '../../../cloud-runner'; export class TaskService { static async watch() { @@ -158,7 +159,7 @@ export class TaskService { process.env.AWS_REGION = Input.region; const s3 = new AWS.S3(); const listRequest: ListObjectsRequest = { - Bucket: `game-ci-test-storage`, + Bucket: CloudRunner.buildParameters.awsBaseStackName, }; const results = await s3.listObjects(listRequest).promise(); diff --git a/src/model/cloud-runner/providers/docker/index.ts b/src/model/cloud-runner/providers/docker/index.ts index 8de6cce3..88f0fe16 100644 --- a/src/model/cloud-runner/providers/docker/index.ts +++ b/src/model/cloud-runner/providers/docker/index.ts @@ -47,10 +47,18 @@ class LocalDockerCloudRunner implements ProviderInterface { defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) { const { workspace } = Action; - if (fs.existsSync(`${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar.lz4`)) { + if ( + fs.existsSync( + `${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + }`, + ) + ) { await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache/cache/build/`); await CloudRunnerSystem.Run( - `rm -r ${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar.lz4`, + `rm -r ${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + }`, ); } } diff --git a/src/model/cloud-runner/providers/k8s/index.ts b/src/model/cloud-runner/providers/k8s/index.ts index bc427e20..c06d1682 100644 --- a/src/model/cloud-runner/providers/k8s/index.ts +++ b/src/model/cloud-runner/providers/k8s/index.ts @@ -154,6 +154,7 @@ class Kubernetes implements ProviderInterface { } else { CloudRunnerLogger.log('Pod still running, recovering stream...'); } + await this.cleanupTaskResources(); } catch (error: any) { let errorParsed; try { @@ -161,10 +162,16 @@ class Kubernetes implements ProviderInterface { } catch { errorParsed = error; } - const reason = errorParsed.reason || errorParsed.response?.body?.reason || ``; - const errorMessage = errorParsed.message || ``; - const continueStreaming = reason === `NotFound` || errorMessage.includes(`dial timeout, backstop`); + const reason = errorParsed.reason || errorParsed.response?.body?.reason || ``; + const errorMessage = errorParsed.message || reason; + + const continueStreaming = + errorMessage.includes(`dial timeout, backstop`) || + errorMessage.includes(`HttpError: HTTP request failed`) || + errorMessage.includes(`an error occurred when try to find container`) || + errorMessage.includes(`not found`) || + errorMessage.includes(`Not Found`); if (continueStreaming) { CloudRunnerLogger.log('Log Stream Container Not Found'); await new Promise((resolve) => resolve(5000)); @@ -175,7 +182,6 @@ class Kubernetes implements ProviderInterface { } } } - await this.cleanupTaskResources(); return output; } catch (error) { diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts b/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts index 8d39b7d1..d3e69618 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts @@ -158,6 +158,8 @@ class KubernetesJobSpecFactory { }, }; + job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '5Gi'; + return job; } } diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts b/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts index d9b24fc3..a496b88c 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts @@ -4,8 +4,11 @@ class KubernetesPods { public static async IsPodRunning(podName: string, namespace: string, kubeClient: CoreV1Api) { const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.filter((x) => podName === x.metadata?.name); const running = pods.length > 0 && (pods[0].status?.phase === `Running` || pods[0].status?.phase === `Pending`); - const phase = pods[0].status?.phase || 'undefined status'; + const phase = pods[0]?.status?.phase || 'undefined status'; CloudRunnerLogger.log(`Getting pod status: ${phase}`); + if (phase === `Failed`) { + throw new Error(`K8s pod failed`); + } return running; } diff --git a/src/model/cloud-runner/remote-client/caching.ts b/src/model/cloud-runner/remote-client/caching.ts index fa8314b6..0c266855 100644 --- a/src/model/cloud-runner/remote-client/caching.ts +++ b/src/model/cloud-runner/remote-client/caching.ts @@ -46,7 +46,11 @@ export class Caching { public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheArtifactName: string) { cacheArtifactName = cacheArtifactName.replace(' ', ''); const startPath = process.cwd(); - const compressionSuffix = CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''; + let compressionSuffix = ''; + if (CloudRunner.buildParameters.useLz4Compression === true) { + compressionSuffix = `.lz4`; + } + CloudRunnerLogger.log(`Compression: ${CloudRunner.buildParameters.useLz4Compression} ${compressionSuffix}`); try { if (!(await fileExists(cacheFolder))) { await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`); @@ -99,9 +103,12 @@ export class Caching { } public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheArtifactName: string = ``) { cacheArtifactName = cacheArtifactName.replace(' ', ''); - const compressionSuffix = CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''; + let compressionSuffix = ''; + if (CloudRunner.buildParameters.useLz4Compression === true) { + compressionSuffix = `.lz4`; + } const startPath = process.cwd(); - RemoteClientLogger.log(`Caching for ${path.basename(destinationFolder)}`); + RemoteClientLogger.log(`Caching for (lz4 ${compressionSuffix}) ${path.basename(destinationFolder)}`); try { if (!(await fileExists(cacheFolder))) { await fs.promises.mkdir(cacheFolder); @@ -153,6 +160,7 @@ export class Caching { RemoteClientLogger.logWarning( `cache item ${cacheArtifactName}.tar${compressionSuffix} doesn't exist ${destinationFolder}`, ); + await CloudRunnerSystem.Run(`tree ${cacheFolder}`); throw new Error(`Failed to get cache item, but cache hit was found: ${cacheSelection}`); } } diff --git a/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts b/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts index 0b415950..94e265ef 100644 --- a/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts +++ b/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts @@ -5,7 +5,8 @@ import { RemoteClientLogger } from '../remote-client/remote-client-logger'; import path from 'path'; import CloudRunnerOptions from '../cloud-runner-options'; import * as fs from 'fs'; -import CloudRunnerLogger from './cloud-runner-logger'; + +// import CloudRunnerLogger from './cloud-runner-logger'; export class CloudRunnerCustomHooks { // TODO also accept hooks as yaml files in the repo @@ -83,7 +84,7 @@ export class CloudRunnerCustomHooks { // if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) { - CloudRunnerLogger.log(`Parsing build hooks: ${steps}`); + // CloudRunnerLogger.log(`Parsing build hooks: ${steps}`); // } const isArray = steps.replace(/\s/g, ``)[0] === `-`; diff --git a/src/model/cloud-runner/services/cloud-runner-custom-steps.ts b/src/model/cloud-runner/services/cloud-runner-custom-steps.ts index dec8d010..d46335b0 100644 --- a/src/model/cloud-runner/services/cloud-runner-custom-steps.ts +++ b/src/model/cloud-runner/services/cloud-runner-custom-steps.ts @@ -1,5 +1,4 @@ import YAML from 'yaml'; -import CloudRunnerSecret from './cloud-runner-secret'; import CloudRunner from '../cloud-runner'; import * as core from '@actions/core'; import { CustomWorkflow } from '../workflows/custom-workflow'; @@ -9,6 +8,7 @@ import * as fs from 'fs'; import Input from '../../input'; import CloudRunnerOptions from '../cloud-runner-options'; import CloudRunnerLogger from './cloud-runner-logger'; +import { CustomStep } from './custom-step'; export class CloudRunnerCustomSteps { static GetCustomStepsFromFiles(hookLifecycle: string): CustomStep[] { @@ -43,8 +43,14 @@ export class CloudRunnerCustomSteps { aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default aws configure set region $AWS_DEFAULT_REGION --profile default - aws s3 cp /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 - rm /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 + aws s3 cp /data/cache/$CACHE_KEY/build/build-${CloudRunner.buildParameters.buildGuid}.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + } s3://${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + } + rm /data/cache/$CACHE_KEY/build/build-${CloudRunner.buildParameters.buildGuid}.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + } secrets: - name: awsAccessKeyId value: ${process.env.AWS_ACCESS_KEY_ID || ``} @@ -52,6 +58,62 @@ export class CloudRunnerCustomSteps { value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} - name: awsDefaultRegion value: ${process.env.AWS_REGION || ``} +- name: aws-s3-pull-build + image: amazon/aws-cli + commands: | + aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default + aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default + aws configure set region $AWS_DEFAULT_REGION --profile default + aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/ || true + aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/build || true + aws s3 cp s3://${ + CloudRunner.buildParameters.awsBaseStackName + }/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + } /data/cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${ + CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + } + secrets: + - name: awsAccessKeyId + - name: awsSecretAccessKey + - name: awsDefaultRegion + - name: BUILD_GUID_TARGET +- name: steam-deploy-client + image: steamcmd/steamcmd + commands: | + apt-get update + apt-get install -y curl tar coreutils git tree > /dev/null + curl -s https://gist.githubusercontent.com/frostebite/1d56f5505b36b403b64193b7a6e54cdc/raw/fa6639ed4ef750c4268ea319d63aa80f52712ffb/deploy-client-steam.sh | bash + secrets: + - name: STEAM_USERNAME + - name: STEAM_PASSWORD + - name: STEAM_APPID + - name: STEAM_SSFN_FILE_NAME + - name: STEAM_SSFN_FILE_CONTENTS + - name: STEAM_CONFIG_VDF_1 + - name: STEAM_CONFIG_VDF_2 + - name: STEAM_CONFIG_VDF_3 + - name: STEAM_CONFIG_VDF_4 + - name: BUILD_GUID_TARGET + - name: RELEASE_BRANCH +- name: steam-deploy-project + image: steamcmd/steamcmd + commands: | + apt-get update + apt-get install -y curl tar coreutils git tree > /dev/null + curl -s https://gist.githubusercontent.com/frostebite/969da6a41002a0e901174124b643709f/raw/02403e53fb292026cba81ddcf4ff35fc1eba111d/steam-deploy-project.sh | bash + secrets: + - name: STEAM_USERNAME + - name: STEAM_PASSWORD + - name: STEAM_APPID + - name: STEAM_SSFN_FILE_NAME + - name: STEAM_SSFN_FILE_CONTENTS + - name: STEAM_CONFIG_VDF_1 + - name: STEAM_CONFIG_VDF_2 + - name: STEAM_CONFIG_VDF_3 + - name: STEAM_CONFIG_VDF_4 + - name: BUILD_GUID_2 + - name: RELEASE_BRANCH - name: aws-s3-upload-cache image: amazon/aws-cli hook: after @@ -59,9 +121,13 @@ export class CloudRunnerCustomSteps { aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default aws configure set region $AWS_DEFAULT_REGION --profile default - aws s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/lfs + aws s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://${ + CloudRunner.buildParameters.awsBaseStackName + }/cloud-runner-cache/$CACHE_KEY/lfs rm -r /data/cache/$CACHE_KEY/lfs - aws s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/Library + aws s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://${ + CloudRunner.buildParameters.awsBaseStackName + }/cloud-runner-cache/$CACHE_KEY/Library rm -r /data/cache/$CACHE_KEY/Library secrets: - name: awsAccessKeyId @@ -77,13 +143,13 @@ export class CloudRunnerCustomSteps { aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default aws configure set region $AWS_DEFAULT_REGION --profile default - aws s3 ls game-ci-test-storage/cloud-runner-cache/ || true - aws s3 ls game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/ || true - BUCKET1="game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/Library/" + aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/ || true + aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/ || true + BUCKET1="${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/Library/" aws s3 ls $BUCKET1 || true OBJECT1="$(aws s3 ls $BUCKET1 | sort | tail -n 1 | awk '{print $4}' || '')" aws s3 cp s3://$BUCKET1$OBJECT1 /data/cache/$CACHE_KEY/Library/ || true - BUCKET2="game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/lfs/" + BUCKET2="${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/lfs/" aws s3 ls $BUCKET2 || true OBJECT2="$(aws s3 ls $BUCKET2 | sort | tail -n 1 | awk '{print $4}' || '')" aws s3 cp s3://$BUCKET2$OBJECT2 /data/cache/$CACHE_KEY/lfs/ || true @@ -135,21 +201,21 @@ export class CloudRunnerCustomSteps { if (steps === '') { return []; } - - // if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) { - - // CloudRunnerLogger.log(`Parsing build steps: ${steps}`); - - // } const isArray = steps.replace(/\s/g, ``)[0] === `-`; - if (CloudRunner.buildParameters?.cloudRunnerDebug) { - CloudRunnerLogger.log(`Parsing: ${steps}`); - } const object: CustomStep[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)]; for (const step of object) { CloudRunnerCustomSteps.ConvertYamlSecrets(step); if (step.secrets === undefined) { step.secrets = []; + } else { + for (const secret of step.secrets) { + if (secret.ParameterValue === undefined && process.env[secret.EnvironmentVariable] !== undefined) { + if (CloudRunner.buildParameters?.cloudRunnerDebug) { + CloudRunnerLogger.log(`Injecting custom step ${step.name} from env var ${secret.ParameterKey}`); + } + secret.ParameterValue = process.env[secret.ParameterKey] || ``; + } + } } if (step.image === undefined) { step.image = `ubuntu`; @@ -201,10 +267,3 @@ export class CloudRunnerCustomSteps { return output; } } -export class CustomStep { - public commands; - public secrets: CloudRunnerSecret[] = new Array(); - public name; - public image: string = `ubuntu`; - public hook!: string; -} diff --git a/src/model/cloud-runner/services/custom-step.ts b/src/model/cloud-runner/services/custom-step.ts new file mode 100644 index 00000000..6f660b25 --- /dev/null +++ b/src/model/cloud-runner/services/custom-step.ts @@ -0,0 +1,9 @@ +import CloudRunnerSecret from './cloud-runner-secret'; + +export class CustomStep { + public commands; + public secrets: CloudRunnerSecret[] = new Array(); + public name; + public image: string = `ubuntu`; + public hook!: string; +} diff --git a/src/model/cloud-runner/services/follow-log-stream-service.ts b/src/model/cloud-runner/services/follow-log-stream-service.ts index c25ef6d0..486fb356 100644 --- a/src/model/cloud-runner/services/follow-log-stream-service.ts +++ b/src/model/cloud-runner/services/follow-log-stream-service.ts @@ -2,6 +2,7 @@ import CloudRunnerLogger from './cloud-runner-logger'; import * as core from '@actions/core'; import CloudRunner from '../cloud-runner'; import { CloudRunnerStatics } from '../cloud-runner-statics'; +import GitHub from '../../github'; export class FollowLogStreamService { public static handleIteration(message, shouldReadLogs, shouldCleanup, output) { @@ -9,11 +10,14 @@ export class FollowLogStreamService { CloudRunnerLogger.log('End of log transmission received'); shouldReadLogs = false; } else if (message.includes('Rebuilding Library because the asset database could not be found!')) { + GitHub.updateGitHubCheck(`Library was not found, importing new Library`, ``); core.warning('LIBRARY NOT FOUND!'); core.setOutput('library-found', 'false'); } else if (message.includes('Build succeeded')) { + GitHub.updateGitHubCheck(`Build succeeded`, `Build succeeded`); core.setOutput('build-result', 'success'); } else if (message.includes('Build fail')) { + GitHub.updateGitHubCheck(`Build failed`, `Build failed`); core.setOutput('build-result', 'failed'); core.setFailed('unity build failed'); core.error('BUILD FAILED!'); diff --git a/src/model/cloud-runner/services/shared-workspace-locking.ts b/src/model/cloud-runner/services/shared-workspace-locking.ts index dd4b3935..45c1ae61 100644 --- a/src/model/cloud-runner/services/shared-workspace-locking.ts +++ b/src/model/cloud-runner/services/shared-workspace-locking.ts @@ -5,8 +5,12 @@ import CloudRunnerOptions from '../cloud-runner-options'; import BuildParameters from '../../build-parameters'; import CloudRunner from '../cloud-runner'; export class SharedWorkspaceLocking { - private static readonly workspaceBucketRoot = `s3://game-ci-test-storage/`; - private static readonly workspaceRoot = `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`; + private static get workspaceBucketRoot() { + return `s3://${CloudRunner.buildParameters.awsBaseStackName}/`; + } + private static get workspaceRoot() { + return `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`; + } public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise { if (!(await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext))) { return []; @@ -19,14 +23,11 @@ export class SharedWorkspaceLocking { ).map((x) => x.replace(`/`, ``)); } public static async DoesWorkspaceTopLevelExist(buildParametersContext: BuildParameters) { - return ( - (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`)) - .map((x) => x.replace(`/`, ``)) - .includes(`locks`) && - (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`)) - .map((x) => x.replace(`/`, ``)) - .includes(buildParametersContext.cacheKey) - ); + await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`); + + return (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`)) + .map((x) => x.replace(`/`, ``)) + .includes(buildParametersContext.cacheKey); } public static async GetAllLocks(workspace: string, buildParametersContext: BuildParameters): Promise { if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { @@ -49,20 +50,27 @@ export class SharedWorkspaceLocking { if (!CloudRunnerOptions.retainWorkspaces) { return; } - if (await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext)) { - const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext); - CloudRunnerLogger.log(`run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`); - for (const element of workspaces) { - await new Promise((promise) => setTimeout(promise, 1000)); - const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext); - CloudRunnerLogger.log(`run agent: ${runId} try lock workspace: ${element} result: ${lockResult}`); - if (lockResult) { - CloudRunner.lockedWorkspace = element; + try { + if (await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext)) { + const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext); + CloudRunnerLogger.log( + `run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`, + ); + for (const element of workspaces) { + await new Promise((promise) => setTimeout(promise, 1000)); + const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext); + CloudRunnerLogger.log(`run agent: ${runId} try lock workspace: ${element} result: ${lockResult}`); - return true; + if (lockResult) { + CloudRunner.lockedWorkspace = element; + + return true; + } } } + } catch { + return; } const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext, runId); diff --git a/src/model/cloud-runner/services/task-parameter-serializer.ts b/src/model/cloud-runner/services/task-parameter-serializer.ts index ddc7d289..5e330087 100644 --- a/src/model/cloud-runner/services/task-parameter-serializer.ts +++ b/src/model/cloud-runner/services/task-parameter-serializer.ts @@ -145,6 +145,7 @@ export class TaskParameterSerializer { array = TaskParameterSerializer.tryAddInput(array, 'UNITY_EMAIL'); array = TaskParameterSerializer.tryAddInput(array, 'UNITY_PASSWORD'); array = TaskParameterSerializer.tryAddInput(array, 'UNITY_LICENSE'); + array = TaskParameterSerializer.tryAddInput(array, 'GIT_PRIVATE_TOKEN'); return array; } diff --git a/src/model/cloud-runner/tests/cloud-runner-async-workflow.test.ts b/src/model/cloud-runner/tests/cloud-runner-async-workflow.test.ts new file mode 100644 index 00000000..a4cb1552 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-async-workflow.test.ts @@ -0,0 +1,33 @@ +import { BuildParameters, ImageTag } from '../..'; +import CloudRunner from '../cloud-runner'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerOptions from '../cloud-runner-options'; +import setups from './cloud-runner-suite.test'; + +async function CreateParameters(overrides) { + if (overrides) Cli.options = overrides; + + return BuildParameters.create(); +} +describe('Cloud Runner Async Workflows', () => { + setups(); + it('Responds', () => {}); + + if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `local-docker`) { + it('Async Workflows', async () => { + // Setup parameters + const buildParameter = await CreateParameters({ + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + asyncCloudRunner: `true`, + githubChecks: `true`, + }); + const baseImage = new ImageTag(buildParameter); + + // Run the job + await CloudRunner.run(buildParameter, baseImage.toString()); + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts b/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts index 4ee010ab..fc0107bd 100644 --- a/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts @@ -24,6 +24,14 @@ describe('Cloud Runner Custom Hooks And Steps', () => { commands: echo "test"`; const yamlString2 = `- hook: before commands: echo "test"`; + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + }; + CloudRunner.setup(await CreateParameters(overrides)); const stringObject = CloudRunnerCustomSteps.ParseSteps(yamlString); const stringObject2 = CloudRunnerCustomSteps.ParseSteps(yamlString2); diff --git a/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts b/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts index f5e730c9..d93551b6 100644 --- a/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts @@ -6,6 +6,7 @@ import CloudRunnerLogger from '../services/cloud-runner-logger'; import { v4 as uuidv4 } from 'uuid'; import CloudRunnerOptions from '../cloud-runner-options'; import setups from './cloud-runner-suite.test'; +import * as fs from 'fs'; async function CreateParameters(overrides) { if (overrides) { @@ -45,6 +46,11 @@ describe('Cloud Runner Caching', () => { expect(results).not.toContain(cachePushFail); CloudRunnerLogger.log(`run 1 succeeded`); + + if (CloudRunnerOptions.cloudRunnerCluster === `local-docker`) { + const cacheFolderExists = fs.existsSync(`cloud-runner-cache/cache/${overrides.cacheKey}`); + expect(cacheFolderExists).toBeTruthy(); + } const buildParameter2 = await CreateParameters(overrides); buildParameter2.cacheKey = buildParameter.cacheKey; diff --git a/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts b/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts index b357ac42..30b221c9 100644 --- a/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts @@ -6,7 +6,6 @@ import CloudRunnerLogger from '../services/cloud-runner-logger'; import { v4 as uuidv4 } from 'uuid'; import CloudRunnerOptions from '../cloud-runner-options'; import setups from './cloud-runner-suite.test'; -import { CloudRunnerSystem } from '../services/cloud-runner-system'; import * as fs from 'fs'; import path from 'path'; import { CloudRunnerFolders } from '../services/cloud-runner-folders'; @@ -46,6 +45,11 @@ describe('Cloud Runner Retain Workspace', () => { expect(results).toContain(buildSucceededString); expect(results).not.toContain(cachePushFail); + if (CloudRunnerOptions.cloudRunnerCluster === `local-docker`) { + const cacheFolderExists = fs.existsSync(`cloud-runner-cache/cache/${overrides.cacheKey}`); + expect(cacheFolderExists).toBeTruthy(); + } + CloudRunnerLogger.log(`run 1 succeeded`); const buildParameter2 = await CreateParameters(overrides); @@ -84,9 +88,6 @@ describe('Cloud Runner Retain Workspace', () => { CloudRunnerLogger.log( `Cleaning up ./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`, ); - await CloudRunnerSystem.Run( - `rm -r ./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`, - ); } }); } diff --git a/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts b/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts index 6694cb79..2cf4bf61 100644 --- a/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts @@ -38,7 +38,7 @@ describe('Cloud Runner pre-built S3 steps', () => { expect(build2ContainsBuildSucceeded).toBeTruthy(); const results = await CloudRunnerSystem.RunAndReadLines( - `aws s3 ls s3://game-ci-test-storage/cloud-runner-cache/${buildParameter2.cacheKey}/`, + `aws s3 ls s3://${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/${buildParameter2.cacheKey}/`, ); CloudRunnerLogger.log(results.join(`,`)); }, 1_000_000_000); diff --git a/src/model/cloud-runner/tests/shared-workspace-locking.test.ts b/src/model/cloud-runner/tests/shared-workspace-locking.test.ts index 21aadccd..703edf7b 100644 --- a/src/model/cloud-runner/tests/shared-workspace-locking.test.ts +++ b/src/model/cloud-runner/tests/shared-workspace-locking.test.ts @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import CloudRunnerOptions from '../cloud-runner-options'; import UnityVersioning from '../../unity-versioning'; import BuildParameters from '../../build-parameters'; +import CloudRunner from '../cloud-runner'; async function CreateParameters(overrides) { if (overrides) { @@ -32,6 +33,7 @@ describe('Cloud Runner Locking', () => { const newWorkspaceName = `test-workspace-${uuidv4()}`; const runId = uuidv4(); + CloudRunner.buildParameters = buildParameters; await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters); const isExpectedUnlockedBeforeLocking = (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; diff --git a/src/model/cloud-runner/workflows/async-workflow.ts b/src/model/cloud-runner/workflows/async-workflow.ts new file mode 100644 index 00000000..ffac8478 --- /dev/null +++ b/src/model/cloud-runner/workflows/async-workflow.ts @@ -0,0 +1,60 @@ +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import CloudRunner from '../cloud-runner'; + +export class AsyncWorkflow { + public static async runAsyncWorkflow( + environmentVariables: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ): Promise { + try { + CloudRunnerLogger.log(`Cloud Runner is running async mode`); + + let output = ''; + + output += await CloudRunner.Provider.runTaskInWorkflow( + CloudRunner.buildParameters.buildGuid, + `ubuntu`, + `apt-get update > /dev/null +apt-get install -y curl tar tree npm git git-lfs jq git > /dev/null +mkdir /builder +printenv +git config --global advice.detachedHead false +git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f" +git config --global filter.lfs.process "git-lfs filter-process --skip" +git clone -q -b ${CloudRunner.buildParameters.cloudRunnerBranch} ${CloudRunnerFolders.unityBuilderRepoUrl} /builder +git clone -q -b ${CloudRunner.buildParameters.branch} ${CloudRunnerFolders.targetBuildRepoUrl} /repo +cd /repo +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip +./aws/install +aws --version +node /builder/dist/index.js -m async-workflow`, + `/${CloudRunnerFolders.buildVolumeFolder}`, + `/${CloudRunnerFolders.buildVolumeFolder}/`, + environmentVariables, + [ + ...secrets, + ...[ + { + ParameterKey: `AWS_ACCESS_KEY_ID`, + EnvironmentVariable: `AWS_ACCESS_KEY_ID`, + ParameterValue: process.env.AWS_ACCESS_KEY_ID || ``, + }, + { + ParameterKey: `AWS_SECRET_ACCESS_KEY`, + EnvironmentVariable: `AWS_SECRET_ACCESS_KEY`, + ParameterValue: process.env.AWS_SECRET_ACCESS_KEY || ``, + }, + ], + ], + ); + + return output; + } catch (error) { + throw error; + } + } +} diff --git a/src/model/cloud-runner/workflows/custom-workflow.ts b/src/model/cloud-runner/workflows/custom-workflow.ts index 2392ec7c..8a98e0cf 100644 --- a/src/model/cloud-runner/workflows/custom-workflow.ts +++ b/src/model/cloud-runner/workflows/custom-workflow.ts @@ -2,7 +2,8 @@ import CloudRunnerLogger from '../services/cloud-runner-logger'; import CloudRunnerSecret from '../services/cloud-runner-secret'; import { CloudRunnerFolders } from '../services/cloud-runner-folders'; import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; -import { CloudRunnerCustomSteps, CustomStep } from '../services/cloud-runner-custom-steps'; +import { CloudRunnerCustomSteps } from '../services/cloud-runner-custom-steps'; +import { CustomStep } from '../services/custom-step'; import CloudRunner from '../cloud-runner'; export class CustomWorkflow { @@ -26,9 +27,10 @@ export class CustomWorkflow { try { CloudRunnerLogger.log(`Cloud Runner is running in custom job mode`); let output = ''; - if (CloudRunner.buildParameters?.cloudRunnerDebug) { - CloudRunnerLogger.log(`Custom Job Description \n${JSON.stringify(buildSteps, undefined, 4)}`); - } + + // if (CloudRunner.buildParameters?.cloudRunnerDebug) { + // CloudRunnerLogger.log(`Custom Job Description \n${JSON.stringify(buildSteps, undefined, 4)}`); + // } for (const step of buildSteps) { output += await CloudRunner.Provider.runTaskInWorkflow( CloudRunner.buildParameters.buildGuid, diff --git a/src/model/cloud-runner/workflows/workflow-composition-root.ts b/src/model/cloud-runner/workflows/workflow-composition-root.ts index c67b985f..d13cc06a 100644 --- a/src/model/cloud-runner/workflows/workflow-composition-root.ts +++ b/src/model/cloud-runner/workflows/workflow-composition-root.ts @@ -3,10 +3,16 @@ import { CustomWorkflow } from './custom-workflow'; import { WorkflowInterface } from './workflow-interface'; import { BuildAutomationWorkflow } from './build-automation-workflow'; import CloudRunner from '../cloud-runner'; +import CloudRunnerOptions from '../cloud-runner-options'; +import { AsyncWorkflow } from './async-workflow'; export class WorkflowCompositionRoot implements WorkflowInterface { async run(cloudRunnerStepState: CloudRunnerStepState) { try { + if (CloudRunnerOptions.asyncCloudRunner) { + return await AsyncWorkflow.runAsyncWorkflow(cloudRunnerStepState.environment, cloudRunnerStepState.secrets); + } + if (CloudRunner.buildParameters.customJob !== '') { return await CustomWorkflow.runCustomJobFromString( CloudRunner.buildParameters.customJob, diff --git a/src/model/github.ts b/src/model/github.ts index 4f98145e..75255f10 100644 --- a/src/model/github.ts +++ b/src/model/github.ts @@ -1,5 +1,168 @@ +import CloudRunnerLogger from './cloud-runner/services/cloud-runner-logger'; +import CloudRunner from './cloud-runner/cloud-runner'; +import CloudRunnerOptions from './cloud-runner/cloud-runner-options'; +import * as core from '@actions/core'; +import { Octokit } from '@octokit/core'; class GitHub { + private static readonly asyncChecksApiWorkflowName = `Async Checks API`; public static githubInputEnabled: boolean = true; + private static longDescriptionContent: string = ``; + private static startedDate: string; + private static endedDate: string; + private static get octokitDefaultToken() { + return new Octokit({ + auth: process.env.GITHUB_TOKEN, + }); + } + private static get octokitPAT() { + return new Octokit({ + auth: CloudRunner.buildParameters.gitPrivateToken, + }); + } + private static get sha() { + return CloudRunner.buildParameters.gitSha; + } + + private static get checkName() { + return `Cloud Runner (${CloudRunner.buildParameters.buildGuid})`; + } + + private static get nameReadable() { + return GitHub.checkName; + } + + private static get checkRunId() { + return CloudRunner.githubCheckId; + } + + private static get owner() { + return CloudRunnerOptions.githubOwner; + } + + private static get repo() { + return CloudRunnerOptions.githubRepoName; + } + + public static async createGitHubCheck(summary) { + if (!CloudRunnerOptions.githubChecks) { + return ``; + } + GitHub.startedDate = new Date().toISOString(); + + CloudRunnerLogger.log(`POST /repos/${GitHub.owner}/${GitHub.repo}/check-runs`); + + const data = { + owner: GitHub.owner, + repo: GitHub.repo, + name: GitHub.checkName, + // eslint-disable-next-line camelcase + head_sha: GitHub.sha, + status: 'queued', + // eslint-disable-next-line camelcase + external_id: CloudRunner.buildParameters.buildGuid, + // eslint-disable-next-line camelcase + started_at: GitHub.startedDate, + output: { + title: GitHub.nameReadable, + summary, + text: '', + images: [ + { + alt: 'Game-CI', + // eslint-disable-next-line camelcase + image_url: 'https://game.ci/assets/images/game-ci-brand-logo-wordmark.svg', + }, + ], + }, + }; + const result = await GitHub.createGitHubCheckRequest(data); + + return result.data.id; + } + + public static async updateGitHubCheck(longDescription, summary, result = `neutral`, status = `in_progress`) { + if (!CloudRunnerOptions.githubChecks) { + return; + } + GitHub.longDescriptionContent += `\n${longDescription}`; + + const data: any = { + owner: GitHub.owner, + repo: GitHub.repo, + // eslint-disable-next-line camelcase + check_run_id: GitHub.checkRunId, + name: GitHub.checkName, + // eslint-disable-next-line camelcase + head_sha: GitHub.sha, + // eslint-disable-next-line camelcase + started_at: GitHub.startedDate, + status, + output: { + title: GitHub.nameReadable, + summary, + text: GitHub.longDescriptionContent, + annotations: [], + }, + }; + + if (status === `completed`) { + if (GitHub.endedDate !== undefined) { + GitHub.endedDate = new Date().toISOString(); + } + // eslint-disable-next-line camelcase + data.completed_at = GitHub.endedDate || GitHub.startedDate; + data.conclusion = result; + } + + if (await CloudRunnerOptions.asyncCloudRunner) { + await GitHub.runUpdateAsyncChecksWorkflow(data, `update`); + + return; + } + await GitHub.updateGitHubCheckRequest(data); + } + + public static async updateGitHubCheckRequest(data) { + return await GitHub.octokitDefaultToken.request(`PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}`, data); + } + + public static async createGitHubCheckRequest(data) { + return await GitHub.octokitDefaultToken.request(`POST /repos/{owner}/{repo}/check-runs`, data); + } + + public static async runUpdateAsyncChecksWorkflow(data, mode) { + if (mode === `create`) { + throw new Error(`Not supported: only use update`); + } + const workflowsResult = await GitHub.octokitDefaultToken.request( + `GET /repos/${GitHub.owner}/${GitHub.repo}/actions/workflows`, + { + owner: GitHub.owner, + repo: GitHub.repo, + }, + ); + const workflows = workflowsResult.data.workflows; + let selectedId = ``; + for (let index = 0; index < workflowsResult.data.total_count; index++) { + if (workflows[index].name === GitHub.asyncChecksApiWorkflowName) { + selectedId = workflows[index].id; + } + } + if (selectedId === ``) { + core.info(JSON.stringify(workflows)); + throw new Error(`no workflow with name "${GitHub.asyncChecksApiWorkflowName}"`); + } + await GitHub.octokitPAT.request(`POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`, { + owner: GitHub.owner, + repo: GitHub.repo, + // eslint-disable-next-line camelcase + workflow_id: selectedId, + ref: CloudRunnerOptions.branch, + inputs: { + checksObject: JSON.stringify({ data, mode }), + }, + }); + } } export default GitHub;