From 7abb3a409d44478d9e651440fc1cb2f1cc1eea01 Mon Sep 17 00:00:00 2001 From: Frostebite Date: Mon, 27 Mar 2023 12:14:23 +0100 Subject: [PATCH] Cloud runner develop - latest fixes (#524) Cloud runner develop - latest fixes (#524) --- .github/workflows/build-tests-ubuntu.yml | 4 +- .../workflows/cloud-runner-async-checks.yml | 19 +- ...eline.yml => cloud-runner-ci-pipeline.yml} | 129 +++++++--- action.yml | 8 +- dist/index.js | Bin 19434192 -> 19458761 bytes dist/index.js.map | Bin 13727580 -> 13757420 bytes .../my-test-hook-post-build.yaml | 2 +- .../my-test-hook-pre-build.yaml | 2 +- .../my-test-step-post-build.yaml | 0 .../my-test-step-pre-build.yaml | 0 package.json | 12 +- src/index.ts | 2 +- src/model/build-parameters.ts | 78 +++--- src/model/cli/cli.ts | 44 ++-- src/model/cloud-runner/cloud-runner.ts | 107 +++++--- .../cloud-runner/error/cloud-runner-error.ts | 4 +- .../cloud-runner-constants.ts | 0 .../cloud-runner-environment-variable.ts | 0 .../cloud-runner-folders.ts | 7 +- .../cloud-runner-guid.ts | 0 .../cloud-runner-options-reader.ts | 2 +- .../{ => options}/cloud-runner-options.ts | 142 +++++------ .../cloud-runner-query-override.ts | 14 +- .../cloud-runner-secret.ts | 0 .../{ => options}/cloud-runner-statics.ts | 0 .../cloud-runner-step-parameters.ts} | 6 +- .../providers/aws/aws-base-stack.ts | 2 +- .../cloud-runner/providers/aws/aws-error.ts | 2 +- .../providers/aws/aws-job-stack.ts | 25 +- .../providers/aws/aws-task-runner.ts | 66 +++-- .../task-definition-formation.ts | 2 +- src/model/cloud-runner/providers/aws/index.ts | 21 +- .../services/garbage-collection-service.ts | 2 +- .../providers/aws/services/task-service.ts | 4 +- .../cloud-runner/providers/docker/index.ts | 22 +- src/model/cloud-runner/providers/k8s/index.ts | 153 +++++++----- .../k8s/kubernetes-job-spec-factory.ts | 80 ++---- .../providers/k8s/kubernetes-pods.ts | 8 +- .../providers/k8s/kubernetes-secret.ts | 4 +- .../providers/k8s/kubernetes-storage.ts | 2 +- .../providers/k8s/kubernetes-task-runner.ts | 168 ++++++++----- .../cloud-runner/providers/local/index.ts | 8 +- .../providers/provider-interface.ts | 4 +- .../cloud-runner/providers/test/index.ts | 6 +- .../cloud-runner/remote-client/caching.ts | 29 +-- src/model/cloud-runner/remote-client/index.ts | 232 ++++++++++-------- .../remote-client/remote-client-logger.ts | 2 +- .../services/cloud-runner-custom-hooks.ts | 114 --------- .../{ => core}/cloud-runner-logger.ts | 0 .../{ => core}/cloud-runner-system.ts | 2 +- .../core/follow-log-stream-service.ts | 57 +++++ .../{ => core}/shared-workspace-locking.ts | 189 +++++++------- .../{ => core}/task-parameter-serializer.ts | 107 ++++---- .../services/follow-log-stream-service.ts | 37 --- .../services/hooks/command-hook-service.ts | 118 +++++++++ .../services/hooks/command-hook.ts | 9 + .../container-hook-service.ts} | 128 +++++----- .../container-hook.ts} | 4 +- .../cloud-runner/services/lfs-hashing.ts | 47 ---- .../services/utility/lfs-hashing.ts | 43 ++++ .../tests/cloud-runner-async-workflow.test.ts | 4 +- ...g.test.ts => cloud-runner-caching.test.ts} | 7 +- ...loud-runner-environment-serializer.test.ts | 46 ---- ...st.ts => cloud-runner-environment.test.ts} | 57 ++++- .../tests/cloud-runner-hooks.test.ts | 114 +++++++++ .../cloud-runner-local-persistence.test.ts | 53 ++++ .../tests/cloud-runner-locking-core.test.ts | 115 +++++++++ .../cloud-runner-locking-get-locked.test.ts | 156 ++++++++++++ ...cloud-runner-run-once-custom-hooks.test.ts | 79 ------ ....test.ts => cloud-runner-s3-steps.test.ts} | 12 +- .../tests/create-test-parameter.ts | 8 + .../cloud-runner-end2end-caching.test.ts} | 35 +-- .../e2e/cloud-runner-end2end-locking.test.ts | 92 +++++++ .../cloud-runner-end2end-retaining.test.ts} | 37 ++- .../tests/shared-workspace-locking.test.ts | 102 -------- .../cloud-runner/workflows/async-workflow.ts | 13 +- .../workflows/build-automation-workflow.ts | 56 ++--- .../cloud-runner/workflows/custom-workflow.ts | 26 +- .../workflows/workflow-composition-root.ts | 16 +- .../workflows/workflow-interface.ts | 4 +- src/model/docker.ts | 5 +- src/model/exec-with-error-check.ts | 5 + src/model/github.ts | 85 +++++-- src/model/image-environment-factory.ts | 2 +- .../input-readers/generic-input-reader.ts | 6 +- src/model/input-readers/git-repo.test.ts | 8 +- src/model/input-readers/git-repo.ts | 10 +- src/model/input-readers/github-cli.ts | 6 +- .../input-readers/test-license-reader.ts | 4 +- src/model/input.ts | 2 +- yarn.lock | 5 + 91 files changed, 2010 insertions(+), 1439 deletions(-) rename .github/workflows/{cloud-runner-pipeline.yml => cloud-runner-ci-pipeline.yml} (53%) rename game-ci/{hooks => command-hooks}/my-test-hook-post-build.yaml (70%) rename game-ci/{hooks => command-hooks}/my-test-hook-pre-build.yaml (70%) rename game-ci/{steps => container-hooks}/my-test-step-post-build.yaml (100%) rename game-ci/{steps => container-hooks}/my-test-step-pre-build.yaml (100%) rename src/model/cloud-runner/{services => options}/cloud-runner-constants.ts (100%) rename src/model/cloud-runner/{services => options}/cloud-runner-environment-variable.ts (100%) rename src/model/cloud-runner/{services => options}/cloud-runner-folders.ts (91%) rename src/model/cloud-runner/{services => options}/cloud-runner-guid.ts (100%) rename src/model/cloud-runner/{services => options}/cloud-runner-options-reader.ts (80%) rename src/model/cloud-runner/{ => options}/cloud-runner-options.ts (52%) rename src/model/cloud-runner/{services => options}/cloud-runner-query-override.ts (76%) rename src/model/cloud-runner/{services => options}/cloud-runner-secret.ts (100%) rename src/model/cloud-runner/{ => options}/cloud-runner-statics.ts (100%) rename src/model/cloud-runner/{cloud-runner-step-state.ts => options/cloud-runner-step-parameters.ts} (64%) delete mode 100644 src/model/cloud-runner/services/cloud-runner-custom-hooks.ts rename src/model/cloud-runner/services/{ => core}/cloud-runner-logger.ts (100%) rename src/model/cloud-runner/services/{ => core}/cloud-runner-system.ts (95%) create mode 100644 src/model/cloud-runner/services/core/follow-log-stream-service.ts rename src/model/cloud-runner/services/{ => core}/shared-workspace-locking.ts (56%) rename src/model/cloud-runner/services/{ => core}/task-parameter-serializer.ts (57%) delete mode 100644 src/model/cloud-runner/services/follow-log-stream-service.ts create mode 100644 src/model/cloud-runner/services/hooks/command-hook-service.ts create mode 100644 src/model/cloud-runner/services/hooks/command-hook.ts rename src/model/cloud-runner/services/{cloud-runner-custom-steps.ts => hooks/container-hook-service.ts} (64%) rename src/model/cloud-runner/services/{custom-step.ts => hooks/container-hook.ts} (65%) delete mode 100644 src/model/cloud-runner/services/lfs-hashing.ts create mode 100644 src/model/cloud-runner/services/utility/lfs-hashing.ts rename src/model/cloud-runner/tests/{cloud-runner-remote-client-caching.test.ts => cloud-runner-caching.test.ts} (89%) delete mode 100644 src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts rename src/model/cloud-runner/tests/{cloud-runner-sync-environment.test.ts => cloud-runner-environment.test.ts} (56%) create mode 100644 src/model/cloud-runner/tests/cloud-runner-hooks.test.ts create mode 100644 src/model/cloud-runner/tests/cloud-runner-local-persistence.test.ts create mode 100644 src/model/cloud-runner/tests/cloud-runner-locking-core.test.ts create mode 100644 src/model/cloud-runner/tests/cloud-runner-locking-get-locked.test.ts delete mode 100644 src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts rename src/model/cloud-runner/tests/{cloud-runner-s3-prebuilt-steps.test.ts => cloud-runner-s3-steps.test.ts} (78%) create mode 100644 src/model/cloud-runner/tests/create-test-parameter.ts rename src/model/cloud-runner/tests/{cloud-runner-run-twice-caching.test.ts => e2e/cloud-runner-end2end-caching.test.ts} (69%) create mode 100644 src/model/cloud-runner/tests/e2e/cloud-runner-end2end-locking.test.ts rename src/model/cloud-runner/tests/{cloud-runner-run-twice-retaining.test.ts => e2e/cloud-runner-end2end-retaining.test.ts} (80%) delete mode 100644 src/model/cloud-runner/tests/shared-workspace-locking.test.ts diff --git a/.github/workflows/build-tests-ubuntu.yml b/.github/workflows/build-tests-ubuntu.yml index af59a755..db313fc5 100644 --- a/.github/workflows/build-tests-ubuntu.yml +++ b/.github/workflows/build-tests-ubuntu.yml @@ -49,7 +49,7 @@ jobs: exclude: - targetPlatform: Android unityVersion: 2022.2.7f1 - cloudRunnerCluster: + providerStrategy: # - local-docker - local projectPath: @@ -109,7 +109,7 @@ jobs: unityVersion: ${{ matrix.unityVersion }} targetPlatform: ${{ matrix.targetPlatform }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue - cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} + providerStrategy: ${{ matrix.providerStrategy }} ########################### # Upload # diff --git a/.github/workflows/cloud-runner-async-checks.yml b/.github/workflows/cloud-runner-async-checks.yml index b0735445..4ab2ef10 100644 --- a/.github/workflows/cloud-runner-async-checks.yml +++ b/.github/workflows/cloud-runner-async-checks.yml @@ -23,7 +23,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_STACK_NAME: game-ci-github-pipelines CLOUD_RUNNER_BRANCH: ${{ github.ref }} CLOUD_RUNNER_DEBUG: true CLOUD_RUNNER_DEBUG_TREE: true @@ -39,20 +39,21 @@ jobs: if: github.event.event_type != 'pull_request_target' runs-on: ubuntu-latest steps: - - name: Checkout (default) - uses: actions/checkout@v3 - with: - lfs: false - - run: yarn - - run: yarn run cli -m checks-update - timeout-minutes: 180 + - timeout-minutes: 180 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} PROJECT_PATH: test-project GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_PLATFORM: StandaloneWindows64 cloudRunnerTests: true versioning: None CLOUD_RUNNER_CLUSTER: local-docker - AWS_BASE_STACK_NAME: game-ci-github-pipelines + AWS_STACK_NAME: game-ci-github-pipelines CHECKS_UPDATE: ${{ github.event.inputs.checksObject }} + run: | + git clone -b cloud-runner-develop https://github.com/game-ci/unity-builder + cd unity-builder + yarn + ls + yarn run cli -m checks-update diff --git a/.github/workflows/cloud-runner-pipeline.yml b/.github/workflows/cloud-runner-ci-pipeline.yml similarity index 53% rename from .github/workflows/cloud-runner-pipeline.yml rename to .github/workflows/cloud-runner-ci-pipeline.yml index 943bfe39..4704defa 100644 --- a/.github/workflows/cloud-runner-pipeline.yml +++ b/.github/workflows/cloud-runner-ci-pipeline.yml @@ -21,81 +21,140 @@ 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-team-pipelines + AWS_STACK_NAME: game-ci-team-pipelines CLOUD_RUNNER_BRANCH: ${{ github.ref }} - CLOUD_RUNNER_DEBUG: true - CLOUD_RUNNER_DEBUG_TREE: true DEBUG: true UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} PROJECT_PATH: test-project UNITY_VERSION: 2019.3.15f1 USE_IL2CPP: false USE_GKE_GCLOUD_AUTH_PLUGIN: true + GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: - integrationTests: - name: Integration Tests + smokeTests: + name: Smoke Tests if: github.event.event_type != 'pull_request_target' runs-on: ubuntu-latest strategy: fail-fast: false matrix: - cloudRunnerCluster: - - aws + test: + #- 'cloud-runner-async-workflow' + - 'cloud-runner-caching' + # - 'cloud-runner-end2end-caching' + # - 'cloud-runner-end2end-retaining' + - 'cloud-runner-environment' + - 'cloud-runner-hooks' + - 'cloud-runner-local-persistence' + - 'cloud-runner-locking-core' + - 'cloud-runner-locking-get-locked' + providerStrategy: + #- aws - local-docker - - k8s + #- k8s steps: - name: Checkout (default) uses: actions/checkout@v3 with: lfs: false - - uses: google-github-actions/auth@v1 - with: - 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: | - 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: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-west-2 + - uses: google-github-actions/auth@v1 + if: matrix.providerStrategy == 'k8s' + with: + credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + - name: 'Set up Cloud SDK' + if: matrix.providerStrategy == 'k8s' + uses: 'google-github-actions/setup-gcloud@v1.1.0' + - name: Get GKE cluster credentials + if: matrix.providerStrategy == 'k8s' + 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 - run: yarn - - run: yarn run test "cloud-runner-async-workflow" --detectOpenHandles --forceExit --runInBand - if: matrix.CloudRunnerCluster != 'local-docker' - timeout-minutes: 180 + - run: yarn run test "${{ matrix.test }}" --detectOpenHandles --forceExit --runInBand + timeout-minutes: 35 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} PROJECT_PATH: test-project - GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_PLATFORM: StandaloneWindows64 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 + CLOUD_RUNNER_CLUSTER: ${{ matrix.providerStrategy }} + tests: + # needs: + # - smokeTests + # - buildTargetTests + name: Integration Tests + if: github.event.event_type != 'pull_request_target' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + providerStrategy: + - aws + - local-docker + - k8s + test: + - 'cloud-runner-async-workflow' + #- 'cloud-runner-caching' + - 'cloud-runner-end2end-locking' + - 'cloud-runner-end2end-caching' + - 'cloud-runner-end2end-retaining' + - 'cloud-runner-environment' + #- 'cloud-runner-hooks' + - 'cloud-runner-s3-steps' + #- 'cloud-runner-local-persistence' + #- 'cloud-runner-locking-core' + #- 'cloud-runner-locking-get-locked' + steps: + - name: Checkout (default) + uses: actions/checkout@v2 + with: + lfs: false + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-2 + - uses: google-github-actions/auth@v1 + if: matrix.providerStrategy == 'k8s' + with: + credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + - name: 'Set up Cloud SDK' + if: matrix.providerStrategy == 'k8s' + uses: 'google-github-actions/setup-gcloud@v1.1.0' + - name: Get GKE cluster credentials + if: matrix.providerStrategy == 'k8s' + 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 + - run: yarn + - run: yarn run test "${{ matrix.test }}" --detectOpenHandles --forceExit --runInBand + timeout-minutes: 60 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} PROJECT_PATH: test-project - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_PLATFORM: StandaloneWindows64 cloudRunnerTests: true versioning: None - CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} - localBuildTests: + PROVIDER_STRATEGY: ${{ matrix.providerStrategy }} + buildTargetTests: name: Local Build Target Tests runs-on: ubuntu-latest strategy: fail-fast: false matrix: - cloudRunnerCluster: + providerStrategy: #- aws - local-docker #- k8s @@ -114,20 +173,18 @@ jobs: - run: yarn - uses: ./ id: unity-build - timeout-minutes: 90 + timeout-minutes: 30 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} with: cloudRunnerTests: true versioning: None - projectPath: test-project - gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} targetPlatform: ${{ matrix.targetPlatform }} - cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} + providerStrategy: ${{ matrix.providerStrategy }} - run: | 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@v3 with: - name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }}) + name: ${{ matrix.providerStrategy }} Build (${{ matrix.targetPlatform }}) path: ${{ steps.unity-build.outputs.BUILD_ARTIFACT }} retention-days: 14 diff --git a/action.yml b/action.yml index 3478812a..ca447fae 100644 --- a/action.yml +++ b/action.yml @@ -118,7 +118,7 @@ inputs: description: '[CloudRunner] Run a pre build job after the repository setup but before the build job (in yaml format with the keys image, secrets (name, value object array), command line string)' - customStepFiles: + containerHookFiles: required: false default: '' description: @@ -130,7 +130,7 @@ inputs: description: '[CloudRunner] Specify the names (by file name) of custom hooks to run before or after cloud runner jobs, must match a yaml step file inside your repo in the folder .game-ci/hooks/' - customJobHooks: + customCommandHooks: required: false default: '' description: '[CloudRunner] Specify custom commands and trigger hooks (injects commands into jobs)' @@ -140,11 +140,11 @@ inputs: description: '[CloudRunner] Run a custom job instead of the standard build automation for cloud runner (in yaml format with the keys image, secrets (name, value object array), command line string)' - awsBaseStackName: + awsStackName: default: 'game-ci' required: false description: '[CloudRunner] The Cloud Formation stack name that must be setup before using this option.' - cloudRunnerCluster: + providerStrategy: default: 'local' required: false description: diff --git a/dist/index.js b/dist/index.js index 24a3505fb51741ce6c6973c211012cad800160d9..a965daa8cbcb2eef052aabf42e1965d3a1292dd5 100644 GIT binary patch delta 24200 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 delta 18439 zcmb`u2Y6If`ak~Md+zkir1y4H$Rr_U(i0*iAwUQv30ghA^PUIASxf}&Hco)e(ROUp=0gVn)&AZ>|A zR;1kN~c~?M)+YK?*SVNs4ul!6`h*sF$P+m+21x@ofZup zJBhZhGg_1x471AwPrB0-MO!j;u0K988C2$_j?rd^g-*&!=0?*gzl6{)O=D<9rk19q z#mkLQbDF>WVO82N*N5lQb_mm)7CWsnO>5$U>uQGO zrDz8=G$C(x=Wip7p+b6kZWct{PJ-yDnK{agG*eny0z~d3#dQD7SS2azc8)5{jplj_UK*NHlST`xeW2w9K8W60 zGYaoW3+m2Z>l2AlL=<7fG(u?&;bXBz%jjUC2cG|PS5AIl@*VFA=hRd~-^Q{fFVk)%#i&&}W z^Bxltnp4H(1{|4aOiW2Mipll$)&_BS>ut3`9655NXmmC< zS&ZV!mFRj?pEJqI=3+NQz&EaTkcNiSqDBMB6de02^5Y7);hY9}BvP=ks z?jHp`s}o9B9rC3eZ)@OJD#XfF@P{5oY!bq;UKq^ZB#83o&6|W^Fa-o^Xv*8Zbm#jc z=+#3WFsDo~&W!D*iGHC4$bkYsk1fJPHk#vur#{qF5P};4785l=s{C% zSJAE)1#D&sLZ@>Vh))n#Q1y`<>@yWb)lP&eOM#|~^SzYgeLh|?Jayi(uMNrb^-5t=eMNp_1tsG~q zgVh`#fR$og)OOhiX>vAA`ygL|OvF}Cdq22aQzQRm1hn^PHr>^&QOE=o3fpnzP9Igf zDla~kp|?4l{dC#E@}uayW6Hk#5iSzVzWd=OP71miJ)~``*;K-VZ&tv`+6eXT&+enN zbnAx&*MIH&;=?9R!;%?Vr6UHneypN9DErAzI6R@ z6JM18JuwhOKR+(YyRhiEhE{)UqLs&kc-O&nZ?R52#!}t1U~Iz@y8Q^t_on-gD+iSc zUTSvMEG!<_o$0o*4Yl%NSbRu?cZ)d@Dzdd>F&?Msp5p6Zskg`YQDM7VW~=>r`{e;_ngqsktd=sKZ5q2(1c7fH!iSL-~u2? zcVLTH-YAzh!i8ZE1<}+K2KqvOd0!37mv3NSwx_0_tAO`*TtjU6dbVG%Q{{OsozF;(j5=S=t?Zk z^2TDk-%d;Cog0qvLqsE3{78T=bT8AZY40$huW{J;(~b~$;%vGI_Bc^N_3JXA_eoA@ zpQ%vLQJX#Jly#}}{jFMhVyy?AYtzt2*F6d&zt^d$$GXgck_Xxs((ZNgu(b4dx=e%T z%W0`hOL6T^=ax+ed8+-Fj-J@;HI)4aLq#&Lq*ox5uGtVuPlJ-09}Z_T)Y2oH!=ds; z!S~wXcD$-HP_HWjTQ&OBmd3%y%m2|jCKxDb>K5ru0l>e!p1mDq7Dt(ei=PJ}wp{ zSI98sq+EjqKRRLwKL0S+^e3-m|(!NA4PyvhMIe+PgRL-eZ%FC7j zd-ri^aL3PNiq6I4Di@+^08#T)p+atMP6l}8aruhuOm^c|i{sq-(Y9H|(y2V|eggj} z;*9(XEpSHBS9)+H7s;hD`d-;63j~#e+H|nPM1ykgWrw`_0ZUVgbA~Dean0x|mxI_uC_!cohx=_s7IVhRJMM4M1 ztC5D7ObHP6Ar}CTOyN>tLNTX>igB1|HMp4DUJg*g!f{*z1dUL7!?tl;P$1Pakkvt!X1v(ubXXLdM|+d7*ZF3vS|Mp;>D zS&1bn8hSX|4CN_3OJ*rU1Fq^TL3De5UQXI}kYr_KT{N?d|^`W6<`b>Aq6#&kBJFACOee-RjvdhLoXP-<%9-_wfnsZ}I-pp>poF z+l6#fW-jdA$t6iM|H7T~aa~%eWFI%lt)0We`#A?Hx8)Zj;PWs(L@GSMZB@X@XZc77 z8OBeA`n7vCaQ^R{NgDkYH=TpAZ*tQi{zJ}Ndg^WNvJyO|lXz*u5$+A%-MIHr?pMA0 zw^t8m3c=RRKN+Fdi4@tijtJ23D|ZQcx`g+kCr+X&dc;ltsQz~425;Ye!~`~+_3N8~~Wl$-;u==lkd7OM1>Dh<3h2P4k7S`nqk zJ^5B1qK_+lq!J(AUjeV=a5>Uzf&4*kfK@`IFVcdB)%<99c_Hs9eHP5`(m?7zI3;hb zg|;^ZU!MW)BugJ+fe2<#E)eF%@tzR1lJ~_O(ZFMI{21`G-K&JPF?=#~$MAQ;#W?)+ zo1OQDY$JaQ?cS<{ig;cxO^oF?b71&$kP1eI@LK5}BR`6Rt~iAb9BKURuu^ru4)W6Z zU^t$}m%;XTknX&Zzz^?hK?nIsd;kO`@>4)Fg$r))B`Ro%*P=#pT%&@_d-y=mzS-u1 zd$BbT?0-{-z_tWF23iyORK^2AOyX7CNa#)A<6%k?KTd6|M_Yn1NrW!e>%O=k51Gdb z>Qq zqTD>J{*xgPdfrv5!1*BWn*qWS4Vl~R~sG!-a zzs>s}nFhDR%mohlczgM-=FioE-UBQ*hdjl&{n z*HzaEhu~x$4w zCq0_O^Biw9Lia6vu=GwYe^QnLhse(U(0eH|1j=6GW2CH6e1L*JBzoLLTxrYY#>2b8 zNW@B~^9BgY6TIQaV*UXUO%DB%nx$chwb9nV_+5&<$+0k=UB`|yk#Ra>N$K?QGe%cV z8(#z$M9vqwtVo6IWBF+4jzn2Otn^fR_Z6B|K6T=>ilXAN14Z@5)LLZzsf`x9&6ot- zR8O3)$l-Oyih5(k)ba^qi>Fss6ipvLPziorKs>3}8iDQ}9sq`^o}L4BbjHy$N+*rE zVT}TT-kMkh%C6i)T!mUOpKoGZj0UK6dJ~A&USJm(ct(H8p5RO;|Pt z_wl00c@OkrNU2$9TiSp&9E)SRZRi?6e1YHzGx9bLssUcd&^+HV+%Tkde`8VEn5pHZ zV=8CjkV?zTDvQg<3|$8(I4<~lq&RJO?U0V6*?Y+DLQRq21Cd37a>zho!*R6scqSW$ zEIRm(7J_b27S*a`wBB_#>bSYk8_xSlkB#AjIoS15n@T!6mVcR(Pm{pd8x)zG0qpjo z+&Z40lWR<_L53%r;E4Wl?Sp~m=a9NUOI{yLZmZ0`7#Mxb(!H?x3 zZwHzz3#KEbSbQ5F_4D}C-mb!EFfC@{jF#%+2a6mttiLvcUM7g-s2Ml2CX+p8Z678ZDqKh=is)GwnJ1LHh07n zrbz5(pmOCw=Pw1%Vf|F=f`!ibsyPi+3HeYo=bk|L#Yo1&bHj)he%Zjk0H@A}D4}t& z!5G;}EwLnt?DOI^qA!>(ga~lj@PG>1-fatn-gh}+W2+K;M|gOHJ%^W>kqWD^ zp27U4Aq;wsYt;Iv!3zZk{74WpZNQ47F4L*>>Q@F@qTji37)8#hPN1e7z5V08W|zRgV~H6 zHo`g56JEWQ*F(o9Od?Zo@Jt-7{10j&rc~4*zL~^_w2Qo|cp38eC2S#T%d77|e^QF< zbzoLMyRDXKLrfyNc_GyK%7B}Mvh4G(KI93u4^h`TZ6+RYB%c_dCt!jB#Gf`Pp|HZB zhpJ7nUeGn!Q|WhIgNx7sviC8A`=&C^Q5p)}}ua@7ugUf94>bvD!l$YkCA5dJB>!91?VqNx#E^V6v z3eTcB@6uPB4A647QmJ%p9<;vO7EN0cgw0h7C9Ql6JsH^s1(d&pQgJ~W+Q@!CgXHOI z52paB4R0%|<1y02jeHa*E3427?i&Qy$mv) z8TByVj4CB_kI^Zs#=5ACq}jrEs@=a2KFOEp2B}py^RB^bAE6-B=cyYbUE0lG6kx=Q z{HsG0JouoCAJ3*U+w)paWHYoJL=E`ZOZ-?4BG2+(&^l3#ixkI2OSek=0S=N=Q9bDT zg%4piC=pDB3Kgy0q^3}F0v=Zr+A>(4rauqPObUsHQ7>uK zQp79#0Rp?6N{k~i zLd4hnZLBOS;0+tT#+`gUBhUcbHU>w7?R_p33e!;d>1a?&uYb*Va*&kD>E#{w9HOjC zKYYgn2Yx&;4$XptucF3o;?QJ~dL9{D&shA0Q!a6>Bzh=Eg8qAcl1ELQ#oWM%W0Sq# zH5%XVukBxVq|ECB{ot;*@O&RV&sX3CkSuT{77qQ2hxFt-=$336gYvChV&4y_iVrT4 ziAD!p{Wmx=9uEJCGw$j|t@|)XZc)|Q8t}v}hN27n4&2RKSeK!FmE+;{A8{2e?_ia6 zzvG=gyi8Dm<|1zFiF0VxD7+6dZv8jj?$(RE;5r-fq(gq<({GkB7E<>0wHJ1dQPH3A zP+)r*5c6+bHMyz%-)VHv^CJnBPvW4vgX2s_H+D44`vymK@(hfP$*QW zVm#0z;UOjb%FpMZp%>4;=6Bu?5-#$oQ1LsOKCJJc!35W*3apm{_0aMenrtS2%7H;6=(N6);gh##Yz60}k^HV;~cfex!hwFWQ8n?&uGd}Wf3PPFEZ@)jO80-phjQ>}UZfWG9p^{Wm2-4p_?!=K&(|s2FX0K;zk#U1aSloQ zbC+;?{@hF^K}9O*&-8}FpY!>!ZJu5Mk*#>zM!k!{wcal$Re@h!6 zUqsMe;bYzZ$4zYK zSYV0GS}U6956vcc-bsE|y1W?72ss)OmyjYF@>gx*AalE05yl;uUOuK=L`lyknrmt- zcBd%Y{yRPbafDbEQ`PL&(c$<~5~b;@un;QFkQu%; zu5ctrU!m9)az-qyT1MjgdU|#li8Qz>T2Mh0qmz^2OgDNTdzO(z76S#|%Soylf5?+y z;(Q>~EGJXp@ng>^;e+KQ9d7O#+T8BX~@$t*h|2zt1GY5tZK<~Z858Cb| z@ziI5FI24{ljTdPN1vT*omp@Joc|23==zBYA9hN?Yb6S5zpX?*R1b5VljZKMJ`4+W^8;xcL;t4tf!bk{@= zh;Jc3s?pGgVx`jp=Wo@cHD@(3Y5G1jLd%$yey|>`6CTS_`oGSE!d*DNmR)2zq$*I6 z551c_17GYS?NHg?rh&?PNB}Kcqty*MQE>k~|yqD-Xs{dYV8dL~R z>W$q$`Pv%vl{iGUAFz6#DpuvN)F8`qK-F?w=Y%(wd1zT4Hh}*@k-!CDR83|Um>=e= zrHcDWqCg*gvTs0a=^fI#H6)#PIjiJYKd=W2scxl{d57Z$_Q~PC56s7Dtr4%;!}fKg z@FrUu=qC=eAsZn^FzRZ~6b-Uxi;YA6467$9B5!a66znKrh)604kV)8~bp$m1Rl&ZV z6v)q39RZ!PTe(XOa^OE};M~~F? zdoiRz$*_^UKh!}1-&0cLa^)O+7?-AD6EO{u9bxY#^4O4Xl6fMU_rxCCfMoZqSx`JV#0g{b_!VWDfi(ZGVn@%n584qyp&X5J5%wP=L(F{z^uSKV<_B(*&D2PU`Z;K%<)bh0J>@Y#yYq%H}C%T65a;q*Q-T2{tPZ=vaB#(t7Fpp0=<(6FBb5H^Ro@1SRdX=uJx zDRuK5$;ii%aVIZmveshYN)hrlrh4mO+kRr$m?*&R{baao9ou(+EQN{#1OxMi3GF-A zpwhKkP{67KD0ywWq!3`k0g}>J@)36-ls>vh;tgt~e#d#j)l^;uf9)cka49b-u9 z&En__aksGACMcLmln_>ixBKdNl)kc3@z*Qf!3I`+8az=#!l2HCJWiW}mcAAfNu?8Z z>se#)T^cIb-ZM#xWJ)JmP9M8ssXKb!(wus2%?MaDjc8O=l^9mL2z`*31N~VOgG0^W za0aUCeGxTifR)4XVM+lC`mz(QN}>0WDG;p8BvFIr-<3nWfDs(%$s}edyxrk4U%~7= z9QV>J6elYJNeEOFVA5(uPjsKB5Z&n2s7*4{k}C%8P%g=Zk=Z0q-hbwXXN_p!$!wAc zC$jM-_a2K@gC>VW_m$9tm`lP04E3&WKeb7=T%ySRiVl0Zh@K<#c!od^(MB@^Bxae+2ii<67JBBl2&fpNKzhVhfMo?^R$2LUIU9w|D63-< z?xdx*S%fDFag5?!VQ4qF%tcBiMdXy?zn=w2OF=sNS_wHU2i`)}SB)42H4P_nYCLH% zTyvJH;D;o^KR`C5Gr^XfV)QtVU52#K0jo+$PFz)G|Jg%-M8CVW8QlSvh6QL}XQplw zoH^d;3yxAU8V;8dQ+R)!>sn=zRaZ*MZ86MoP_w9%g*Q}T*y?~M*E(N}cV)p{YxzLe zddv4HH5;SCo_j>6bZ74^Ko^fq>5218L#ye^c}sN=Hj$*ltrN*{P5&5RLj|d5PZk9D zB9ox0c|56udDBTWT$x1PP*hv$Y)hrTPbRl;Y?)NBwVVXO{&MoswJl75Rsxo#3JBzi=t~;6l+;nmm z_|70R(Pm8|pl3RH@Y=4T<*v9WIFmG_Ge6*GlD`F~4&FsEIT>S7ni?I}B^If7Ch_D5 zJ|2N0qNrh-RDu8*x1n15OLIJ3BkFJLW0zl+;8PXXI|i3}wau4P)4ExyuD2<#?wkzw z2ctC?Kbzcy6CHB52u#m|y$f*7VrP?Ou>VYCke9JPfWhrz9Pqm{XEGSh@*em;YN#_= zKTr=nkwg8+tdfA=uXED=ceilrToi@vHFyVJnvW}bb`D9nt6!2C`q~Kt2V}PWKRH{O z1!bB`#f|?%wFB{Kg{-{ECW|2k7>)cf0cCvn;1{m|_^sdXG;AIka;D8A=3?2;6or?s z%I$%lP(Hc}g`55>$_ysUT<$2jSPA;)i%Tm9m4?cB$b|OKLmmHx1xPgJt;XYD^6^>& zteb}dLvbYuqZ_vPvj|kr2qrg`HPu&J8dI!}GILqH>yLO>7r>1Ixuj`TxY%0(s+FY;<5xdvqc>V;%c-HC;q&}Lu#x< z%|9w-q1nM~HaHGuvoWIqZ5BHC;*IW!8Da4j4?pNJlfx?aC)iz0?o#*dG<$#n7DTC&F?+#TChGlu%&V3*;f>WY@4w3CWQo~22m!mtM_83+p--b$-f5Wh1S?iV7 zd_q1W|3mo=pOdlwAHtX6_YX>@;}`o;rhq)cZYT)%Ar6&!F9B>lqR?xT=))Pom`UZIB$SRy(UtN|cJf zAV!Y4U%bF}j@NoOM~idwnJXM!RQG`#jOmqfx%n(_fKc3VJ+B$KJ|l2bKz=4h5nd-YV$no@~^%cVdVY z8fAj7A$e$k?)Qx_Pn7K8F*Gx>G30xtJ=DNH*EWgC3&msyK2nV8AxX4%w~sz`aE7ZH z_tQe(=BjJ@V>WK7>pSxG;9%Wry~@{}edM2+_`X5DkM-xtaJKLM=ZS#VZC=dUY5Q zm&AL~9Z!qW{TIk6GU$a4ocoDfb#wpTpUHc0yqNQ1u2*l#`U~;k+!JZOM1I#?`@jUY ze@^_R7cZ0F30(fC;30i=g?!7w+%*9@Xc0GR*&|2XQhXNzyZ$T;-#kf(f{WiRK{+3v zJifGBc+CqQtkoM}?p=BfR2)Rrs>55jMJn_WDhCrOjFQfSj-N^XfQV8ArO&FurHvuN zR-QhTr;|#<1vP4mCLbC(LI+DCgg<3fluw&3$`O7Jl&wmv&hbsvDR|H=jm-mpvjD*6 zR5{Kh86`T06cQ=)5UAdWuV0wjR*{HmNotHRqhHzT>j0IP=v993Nvu!=agoAoxEd#< zOSi`f!wJ~UX$r9A5Csi%=o!`2fISek3VA7xBnTNGbn)@%`NHR&L5V^H(^VwwDwu~& zi9$eZ$=Lrsa#)ij`~~h!7QTchU)2Sn9!(6=mK4FlgE>cNk$%n*e(-|t3J8>PIFrA|4@^Gm)Y3!u32!UiZ+Cx)D1BhtS^=RzD5Y&{Rrh-dS-F`e*u8bVQJV6Y z>Xq>iY%r=38fk-3J1$z0BWI`G@oXY=Jwk;C)DyG_9l}FWPtf~4{lJr{281WV3*n9M zLHHv45dMe&L?9vv5sU~ygd)Na;fM%CBtk?)A)*m6h*-ohL>$71h({zK5)s1@Nr+@b z3L+I@LZl(m5gCX~L>3|&k%P!Zn`)sj{RqXIA%zPQbjB^kC_URdV(d!yfcf4GvcPz9sR^Me6%l+gE02fHTB4}M^T z|9r#(X@vj65+$s^qDpHU82s>giq7mUhI1x>VC5WYnWr*d7 zI}s}oD-q3zRfxL~Er`{KyAk&w?nT^(xF7KV;!lVN5o-_+A=V<+A=V={AX*V^h>Zw} z*o4@OXh#6zVZ@ETM>^T9!ETZ*oN4ScoOjx;%US)h-VRhM(jZBMEnKuoYWEe z{M*F>JLHhI{jpoY-m5B>UOTU^78E(T*%*E4`JHNnA~!qJln3UE`ddg^dOB>os2`!o z$xBN&!Rd?od_{UzMn*b>|D-Pf?RP3KsQXEut<28Dr&<_y>8~zOq~+#inUJyQM+xaU znc4Wx!!RGd_*LaiGPAPbgW!l+FaKMg&kL*}DB8McG*tYiGDFWLy_H}E_~3Qd(5j&P zyh;gTiXn$!`%sZ&uuD(>sHzr7Ccb8EJ9GCiy6qXXzp&cfy(&+5{TI~^AO^f9y6kTe-7~V@UeDc83%I6Wg5W5k35FLoUh!+r@h!+trAznsEh`%CULA;81 z4e>f+A7Ve^0HOc_yX}I;w!}0h_i@q5Z@xcL!3i= zk2sI`0nv-NfcOz{5%Ck^XT-k|zaTClE+c+L{D$})aRu=o#MOtER&pBSrWsNSe6W9NgPXz+<~Q)@!Zsh{vTDGyO00? diff --git a/dist/index.js.map b/dist/index.js.map index 25907a9cf981acf7b481dda63af7f804725f7b12..1fe06b3b519ad6d1dc997988f6792ceed41f3849 100644 GIT binary patch delta 25297 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 PodBKGhMnrpz3=}3?pqB? delta 19794 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 O37|=PcjU>Gm;N8uX`#3P diff --git a/game-ci/hooks/my-test-hook-post-build.yaml b/game-ci/command-hooks/my-test-hook-post-build.yaml similarity index 70% rename from game-ci/hooks/my-test-hook-post-build.yaml rename to game-ci/command-hooks/my-test-hook-post-build.yaml index b0c5d846..221edd2d 100644 --- a/game-ci/hooks/my-test-hook-post-build.yaml +++ b/game-ci/command-hooks/my-test-hook-post-build.yaml @@ -1,3 +1,3 @@ -hook: after-build +hook: after commands: | echo "after-build hook test!" diff --git a/game-ci/hooks/my-test-hook-pre-build.yaml b/game-ci/command-hooks/my-test-hook-pre-build.yaml similarity index 70% rename from game-ci/hooks/my-test-hook-pre-build.yaml rename to game-ci/command-hooks/my-test-hook-pre-build.yaml index b3294634..9bb647ea 100644 --- a/game-ci/hooks/my-test-hook-pre-build.yaml +++ b/game-ci/command-hooks/my-test-hook-pre-build.yaml @@ -1,3 +1,3 @@ -hook: before-build +hook: before commands: | echo "before-build hook test!!" diff --git a/game-ci/steps/my-test-step-post-build.yaml b/game-ci/container-hooks/my-test-step-post-build.yaml similarity index 100% rename from game-ci/steps/my-test-step-post-build.yaml rename to game-ci/container-hooks/my-test-step-post-build.yaml diff --git a/game-ci/steps/my-test-step-pre-build.yaml b/game-ci/container-hooks/my-test-step-pre-build.yaml similarity index 100% rename from game-ci/steps/my-test-step-pre-build.yaml rename to game-ci/container-hooks/my-test-step-pre-build.yaml diff --git a/package.json b/package.json index b8e5619d..1ad142cf 100644 --- a/package.json +++ b/package.json @@ -12,17 +12,17 @@ "lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts", "format": "prettier --write \"src/**/*.{js,ts}\"", "cli": "yarn ts-node src/index.ts -m cli", - "gcp-secrets-tests": "cross-env cloudRunnerCluster=aws cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" populateOverride=true readInputFromOverrideList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"cloud runner\"", + "gcp-secrets-tests": "cross-env providerStrategy=aws cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" populateOverride=true readInputFromOverrideList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"cloud runner\"", "gcp-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD", "aws-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"aws-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD", - "cli-aws": "cross-env cloudRunnerCluster=aws yarn run test-cli", - "cli-k8s": "cross-env cloudRunnerCluster=k8s yarn run test-cli", + "cli-aws": "cross-env providerStrategy=aws yarn run test-cli", + "cli-k8s": "cross-env providerStrategy=k8s yarn run test-cli", "test-cli": "cross-env cloudRunnerTests=true yarn ts-node src/index.ts -m cli --projectPath test-project", "test": "jest", "test-i": "cross-env cloudRunnerTests=true yarn test -i -t \"cloud runner\"", "test-i-*": "yarn run test-i-aws && yarn run test-i-k8s", - "test-i-aws": "cross-env cloudRunnerTests=true cloudRunnerCluster=aws yarn test -i -t \"cloud runner\"", - "test-i-k8s": "cross-env cloudRunnerTests=true cloudRunnerCluster=k8s yarn test -i -t \"cloud runner\"" + "test-i-aws": "cross-env cloudRunnerTests=true providerStrategy=aws yarn test -i -t \"cloud runner\"", + "test-i-k8s": "cross-env cloudRunnerTests=true providerStrategy=k8s yarn test -i -t \"cloud runner\"" }, "engines": { "node": ">=16.x" @@ -44,7 +44,7 @@ "reflect-metadata": "^0.1.13", "semver": "^7.3.5", "unity-changeset": "^2.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "yaml": "^1.10.2" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts index 12f10e1b..3f621d8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ async function runMain() { const buildParameters = await BuildParameters.create(); const baseImage = new ImageTag(buildParameters); - if (buildParameters.cloudRunnerCluster === 'local') { + if (buildParameters.providerStrategy === 'local') { core.info('Building locally'); await PlatformSetup.setup(buildParameters, actionFolder); if (process.platform === 'darwin') { diff --git a/src/model/build-parameters.ts b/src/model/build-parameters.ts index b6fb7256..03f7378b 100644 --- a/src/model/build-parameters.ts +++ b/src/model/build-parameters.ts @@ -1,7 +1,7 @@ import { customAlphabet } from 'nanoid'; import AndroidVersioning from './android-versioning'; -import CloudRunnerConstants from './cloud-runner/services/cloud-runner-constants'; -import CloudRunnerBuildGuid from './cloud-runner/services/cloud-runner-guid'; +import CloudRunnerConstants from './cloud-runner/options/cloud-runner-constants'; +import CloudRunnerBuildGuid from './cloud-runner/options/cloud-runner-guid'; import Input from './input'; import Platform from './platform'; import UnityVersioning from './unity-versioning'; @@ -10,7 +10,8 @@ import { GitRepoReader } from './input-readers/git-repo'; import { GithubCliReader } from './input-readers/github-cli'; import { Cli } from './cli/cli'; import GitHub from './github'; -import CloudRunnerOptions from './cloud-runner/cloud-runner-options'; +import CloudRunnerOptions from './cloud-runner/options/cloud-runner-options'; +import CloudRunner from './cloud-runner/cloud-runner'; class BuildParameters { // eslint-disable-next-line no-undef @@ -41,24 +42,23 @@ class BuildParameters { public customParameters!: string; public sshAgent!: string; - public cloudRunnerCluster!: string; - public awsBaseStackName!: string; + public providerStrategy!: string; public gitPrivateToken!: string; public awsStackName!: string; public kubeConfig!: string; - public cloudRunnerMemory!: string | undefined; - public cloudRunnerCpu!: string | undefined; + public containerMemory!: string; + public containerCpu!: string; public kubeVolumeSize!: string; public kubeVolume!: string; public kubeStorageClass!: string; public chownFilesTo!: string; - public customJobHooks!: string; - public readInputFromOverrideList!: string; - public readInputOverrideCommand!: string; + public commandHooks!: string; + public pullInputList!: string[]; + public inputPullCommand!: string; public cacheKey!: string; - public postBuildSteps!: string; - public preBuildSteps!: string; + public postBuildContainerHooks!: string; + public preBuildContainerHooks!: string; public customJob!: string; public runNumber!: string; public branch!: string; @@ -68,17 +68,23 @@ class BuildParameters { public buildGuid!: string; public cloudRunnerBranch!: string; public cloudRunnerDebug!: boolean | undefined; - public cloudRunnerBuilderPlatform!: string | undefined; + public buildPlatform!: string | undefined; public isCliMode!: boolean; - public retainWorkspace!: boolean; public maxRetainedWorkspaces!: number; - public useSharedLargePackages!: boolean; - public useLz4Compression!: boolean; - public garbageCollectionMaxAge!: number; - public constantGarbageCollection!: boolean; + public useLargePackages!: boolean; + public useCompressionStrategy!: boolean; + public garbageMaxAge!: number; public githubChecks!: boolean; + public asyncWorkflow!: boolean; + public githubCheckId!: string; + public finalHooks!: string[]; + public skipLfs!: boolean; + public skipCache!: boolean; public cacheUnityInstallationOnMac!: boolean; public unityHubVersionOnMac!: string; + public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) { + return buildParameters.maxRetainedWorkspaces > 0 && CloudRunner.lockedWorkspace !== ``; + } static async create(): Promise { const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidExportType); @@ -144,16 +150,15 @@ class BuildParameters { sshAgent: Input.sshAgent, gitPrivateToken: Input.gitPrivateToken || (await GithubCliReader.GetGitHubAuthToken()), chownFilesTo: Input.chownFilesTo, - cloudRunnerCluster: CloudRunnerOptions.cloudRunnerCluster, - cloudRunnerBuilderPlatform: CloudRunnerOptions.cloudRunnerBuilderPlatform, - awsBaseStackName: CloudRunnerOptions.awsBaseStackName, + providerStrategy: CloudRunnerOptions.providerStrategy, + buildPlatform: CloudRunnerOptions.buildPlatform, kubeConfig: CloudRunnerOptions.kubeConfig, - cloudRunnerMemory: CloudRunnerOptions.cloudRunnerMemory, - cloudRunnerCpu: CloudRunnerOptions.cloudRunnerCpu, + containerMemory: CloudRunnerOptions.containerMemory, + containerCpu: CloudRunnerOptions.containerCpu, kubeVolumeSize: CloudRunnerOptions.kubeVolumeSize, kubeVolume: CloudRunnerOptions.kubeVolume, - postBuildSteps: CloudRunnerOptions.postBuildSteps, - preBuildSteps: CloudRunnerOptions.preBuildSteps, + postBuildContainerHooks: CloudRunnerOptions.postBuildContainerHooks, + preBuildContainerHooks: CloudRunnerOptions.preBuildContainerHooks, customJob: CloudRunnerOptions.customJob, runNumber: Input.runNumber, branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()), @@ -161,22 +166,25 @@ class BuildParameters { cloudRunnerDebug: CloudRunnerOptions.cloudRunnerDebug, githubRepo: Input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder', isCliMode: Cli.isCliMode, - awsStackName: CloudRunnerOptions.awsBaseStackName, + awsStackName: CloudRunnerOptions.awsStackName, gitSha: Input.gitSha, logId: customAlphabet(CloudRunnerConstants.alphabet, 9)(), buildGuid: CloudRunnerBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform), - customJobHooks: CloudRunnerOptions.customJobHooks(), - readInputOverrideCommand: CloudRunnerOptions.readInputOverrideCommand(), - readInputFromOverrideList: CloudRunnerOptions.readInputFromOverrideList(), + commandHooks: CloudRunnerOptions.commandHooks, + inputPullCommand: CloudRunnerOptions.inputPullCommand, + pullInputList: CloudRunnerOptions.pullInputList, kubeStorageClass: CloudRunnerOptions.kubeStorageClass, cacheKey: CloudRunnerOptions.cacheKey, - retainWorkspace: CloudRunnerOptions.retainWorkspaces, - useSharedLargePackages: CloudRunnerOptions.useSharedLargePackages, - useLz4Compression: CloudRunnerOptions.useLz4Compression, - maxRetainedWorkspaces: CloudRunnerOptions.maxRetainedWorkspaces, - constantGarbageCollection: CloudRunnerOptions.constantGarbageCollection, - garbageCollectionMaxAge: CloudRunnerOptions.garbageCollectionMaxAge, + maxRetainedWorkspaces: Number.parseInt(CloudRunnerOptions.maxRetainedWorkspaces), + useLargePackages: CloudRunnerOptions.useLargePackages, + useCompressionStrategy: CloudRunnerOptions.useCompressionStrategy, + garbageMaxAge: CloudRunnerOptions.garbageMaxAge, githubChecks: CloudRunnerOptions.githubChecks, + asyncWorkflow: CloudRunnerOptions.asyncCloudRunner, + githubCheckId: CloudRunnerOptions.githubCheckId, + finalHooks: CloudRunnerOptions.finalHooks, + skipLfs: CloudRunnerOptions.skipLfs, + skipCache: CloudRunnerOptions.skipCache, cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac, unityHubVersionOnMac: Input.unityHubVersionOnMac, }; diff --git a/src/model/cli/cli.ts b/src/model/cli/cli.ts index 7473b908..a2eed108 100644 --- a/src/model/cli/cli.ts +++ b/src/model/cli/cli.ts @@ -2,17 +2,16 @@ import { Command } from 'commander-ts'; import { BuildParameters, CloudRunner, ImageTag, Input } from '..'; import * as core from '@actions/core'; import { ActionYamlReader } from '../input-readers/action-yaml'; -import CloudRunnerLogger from '../cloud-runner/services/cloud-runner-logger'; -import CloudRunnerQueryOverride from '../cloud-runner/services/cloud-runner-query-override'; +import CloudRunnerLogger from '../cloud-runner/services/core/cloud-runner-logger'; +import CloudRunnerQueryOverride from '../cloud-runner/options/cloud-runner-query-override'; import { CliFunction, CliFunctionsRepository } from './cli-functions-repository'; import { Caching } from '../cloud-runner/remote-client/caching'; -import { LfsHashing } from '../cloud-runner/services/lfs-hashing'; +import { LfsHashing } from '../cloud-runner/services/utility/lfs-hashing'; import { RemoteClient } from '../cloud-runner/remote-client'; -import CloudRunnerOptionsReader from '../cloud-runner/services/cloud-runner-options-reader'; +import CloudRunnerOptionsReader from '../cloud-runner/options/cloud-runner-options-reader'; import GitHub from '../github'; -import { TaskParameterSerializer } from '../cloud-runner/services/task-parameter-serializer'; -import { CloudRunnerFolders } from '../cloud-runner/services/cloud-runner-folders'; -import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system'; +import { CloudRunnerFolders } from '../cloud-runner/options/cloud-runner-folders'; +import { CloudRunnerSystem } from '../cloud-runner/services/core/cloud-runner-system'; import { OptionValues } from 'commander'; import { InputKey } from '../input'; @@ -73,12 +72,14 @@ export class Cli { CloudRunnerLogger.log(`Entrypoint: ${results.key}`); Cli.options!.versioning = 'None'; - const buildParameter = TaskParameterSerializer.readBuildParameterFromEnvironment(); + CloudRunner.buildParameters = await BuildParameters.create(); + CloudRunner.buildParameters.buildGuid = process.env.BUILD_GUID || ``; CloudRunnerLogger.log(`Build Params: - ${JSON.stringify(buildParameter, undefined, 4)} + ${JSON.stringify(CloudRunner.buildParameters, undefined, 4)} `); - CloudRunner.buildParameters = buildParameter; - CloudRunner.lockedWorkspace = process.env.LOCKED_WORKSPACE; + CloudRunner.lockedWorkspace = process.env.LOCKED_WORKSPACE || ``; + CloudRunnerLogger.log(`Locked Workspace: ${CloudRunner.lockedWorkspace}`); + await CloudRunner.setup(CloudRunner.buildParameters); return await results.target[results.propertyKey](Cli.options); } @@ -116,12 +117,16 @@ export class Cli { public static async asyncronousWorkflow(): Promise { const buildParameter = await BuildParameters.create(); const baseImage = new ImageTag(buildParameter); + await CloudRunner.setup(buildParameter); return await CloudRunner.run(buildParameter, baseImage.toString()); } @CliFunction(`checks-update`, `runs a cloud runner build`) public static async checksUpdate() { + const buildParameter = await BuildParameters.create(); + + await CloudRunner.setup(buildParameter); const input = JSON.parse(process.env.CHECKS_UPDATE || ``); core.info(`Checks Update ${process.env.CHECKS_UPDATE}`); if (input.mode === `create`) { @@ -185,7 +190,7 @@ export class Cli { `build-${CloudRunner.buildParameters.buildGuid}`, ); - if (!CloudRunner.buildParameters.retainWorkspace) { + if (!BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters)) { await CloudRunnerSystem.Run( `rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`, ); @@ -193,21 +198,6 @@ export class Cli { await RemoteClient.runCustomHookFiles(`after-build`); - const parameters = await BuildParameters.create(); - CloudRunner.setup(parameters); - if (parameters.constantGarbageCollection) { - await CloudRunnerSystem.Run( - `find /${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.buildVolumeFolder)}/ -name '*.*' -mmin +${ - parameters.garbageCollectionMaxAge * 60 - } -delete`, - ); - await CloudRunnerSystem.Run( - `find ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForAllFull)} -name '*.*' -mmin +${ - parameters.garbageCollectionMaxAge * 60 - } -delete`, - ); - } - return new Promise((result) => result(``)); } } diff --git a/src/model/cloud-runner/cloud-runner.ts b/src/model/cloud-runner/cloud-runner.ts index bfa2272b..fa570a74 100644 --- a/src/model/cloud-runner/cloud-runner.ts +++ b/src/model/cloud-runner/cloud-runner.ts @@ -1,34 +1,42 @@ import AwsBuildPlatform from './providers/aws'; import { BuildParameters, Input } from '..'; import Kubernetes from './providers/k8s'; -import CloudRunnerLogger from './services/cloud-runner-logger'; -import { CloudRunnerStepState } from './cloud-runner-step-state'; +import CloudRunnerLogger from './services/core/cloud-runner-logger'; +import { CloudRunnerStepParameters } from './options/cloud-runner-step-parameters'; import { WorkflowCompositionRoot } from './workflows/workflow-composition-root'; import { CloudRunnerError } from './error/cloud-runner-error'; -import { TaskParameterSerializer } from './services/task-parameter-serializer'; +import { TaskParameterSerializer } from './services/core/task-parameter-serializer'; import * as core from '@actions/core'; -import CloudRunnerSecret from './services/cloud-runner-secret'; +import CloudRunnerSecret from './options/cloud-runner-secret'; import { ProviderInterface } from './providers/provider-interface'; -import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable'; +import CloudRunnerEnvironmentVariable from './options/cloud-runner-environment-variable'; import TestCloudRunner from './providers/test'; import LocalCloudRunner from './providers/local'; import LocalDockerCloudRunner from './providers/docker'; import GitHub from '../github'; -import SharedWorkspaceLocking from './services/shared-workspace-locking'; +import SharedWorkspaceLocking from './services/core/shared-workspace-locking'; +import { FollowLogStreamService } from './services/core/follow-log-stream-service'; class CloudRunner { public static Provider: ProviderInterface; public static buildParameters: BuildParameters; private static defaultSecrets: CloudRunnerSecret[]; private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[]; - static lockedWorkspace: string | undefined; + static lockedWorkspace: string = ``; public static readonly retainedWorkspacePrefix: string = `retained-workspace`; - public static githubCheckId: number | string; - - public static setup(buildParameters: BuildParameters) { + public static get isCloudRunnerEnvironment() { + return process.env[`GITHUB_ACTIONS`] !== `true`; + } + public static get isCloudRunnerAsyncEnvironment() { + return process.env[`ASYNC_WORKFLOW`] === `true`; + } + public static async setup(buildParameters: BuildParameters) { CloudRunnerLogger.setup(); CloudRunnerLogger.log(`Setting up cloud runner`); CloudRunner.buildParameters = buildParameters; + if (CloudRunner.buildParameters.githubCheckId === ``) { + CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid); + } CloudRunner.setupSelectedBuildPlatform(); CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets(); CloudRunner.cloudRunnerEnvironmentVariables = @@ -46,15 +54,16 @@ class CloudRunner { core.setOutput( Input.ToEnvVarFormat(`buildArtifact`), `build-${CloudRunner.buildParameters.buildGuid}.tar${ - CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' }`, ); } + FollowLogStreamService.Reset(); } private static setupSelectedBuildPlatform() { - CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.cloudRunnerCluster}`); - switch (CloudRunner.buildParameters.cloudRunnerCluster) { + CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`); + switch (CloudRunner.buildParameters.providerStrategy) { case 'k8s': CloudRunner.Provider = new Kubernetes(CloudRunner.buildParameters); break; @@ -74,14 +83,20 @@ class CloudRunner { } static async run(buildParameters: BuildParameters, baseImage: string) { - CloudRunner.setup(buildParameters); + await CloudRunner.setup(buildParameters); + if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Setup shared cloud runner resources'); + await CloudRunner.Provider.setupWorkflow( + CloudRunner.buildParameters.buildGuid, + CloudRunner.buildParameters, + CloudRunner.buildParameters.branch, + CloudRunner.defaultSecrets, + ); + if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); try { - CloudRunner.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid); + if (buildParameters.maxRetainedWorkspaces > 0) { + CloudRunner.lockedWorkspace = SharedWorkspaceLocking.NewWorkspaceName(); - if (buildParameters.retainWorkspace) { - CloudRunner.lockedWorkspace = `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`; - - const result = await SharedWorkspaceLocking.GetOrCreateLockedWorkspace( + const result = await SharedWorkspaceLocking.GetLockedWorkspace( CloudRunner.lockedWorkspace, CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters, @@ -95,21 +110,21 @@ class CloudRunner { ]; } else { CloudRunnerLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`); - buildParameters.retainWorkspace = false; - CloudRunner.lockedWorkspace = undefined; + buildParameters.maxRetainedWorkspaces = 0; + CloudRunner.lockedWorkspace = ``; } } - if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Setup shared cloud runner resources'); - await CloudRunner.Provider.setupWorkflow( - CloudRunner.buildParameters.buildGuid, - CloudRunner.buildParameters, - CloudRunner.buildParameters.branch, - CloudRunner.defaultSecrets, - ); - if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); - await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters.buildGuid); + const content = { ...CloudRunner.buildParameters }; + content.gitPrivateToken = ``; + content.unitySerial = ``; + const jsonContent = JSON.stringify(content, undefined, 4); + await GitHub.updateGitHubCheck(jsonContent, CloudRunner.buildParameters.buildGuid); const output = await new WorkflowCompositionRoot().run( - new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets), + new CloudRunnerStepParameters( + baseImage, + CloudRunner.cloudRunnerEnvironmentVariables, + CloudRunner.defaultSecrets, + ), ); if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Cleanup shared cloud runner resources'); await CloudRunner.Provider.cleanupWorkflow( @@ -122,22 +137,40 @@ class CloudRunner { if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, `success`, `success`, `completed`); - if (CloudRunner.buildParameters.retainWorkspace) { + if (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) { + const workspace = CloudRunner.lockedWorkspace || ``; await SharedWorkspaceLocking.ReleaseWorkspace( - CloudRunner.lockedWorkspace || ``, + workspace, CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters, ); - CloudRunner.lockedWorkspace = undefined; + const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(workspace, CloudRunner.buildParameters); + if (isLocked) { + throw new Error( + `still locked after releasing ${await SharedWorkspaceLocking.GetAllLocksForWorkspace( + workspace, + buildParameters, + )}`, + ); + } + CloudRunner.lockedWorkspace = ``; } + await GitHub.triggerWorkflowOnComplete(CloudRunner.buildParameters.finalHooks); + if (buildParameters.constantGarbageCollection) { - CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageCollectionMaxAge, true, true); + CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageMaxAge, true, true); } return output; - } catch (error) { - await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, error, `failure`, `completed`); + } catch (error: any) { + CloudRunnerLogger.log(JSON.stringify(error, undefined, 4)); + await GitHub.updateGitHubCheck( + CloudRunner.buildParameters.buildGuid, + `Failed - Error ${error?.message || 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/error/cloud-runner-error.ts b/src/model/cloud-runner/error/cloud-runner-error.ts index e9b838bc..d5b7b458 100644 --- a/src/model/cloud-runner/error/cloud-runner-error.ts +++ b/src/model/cloud-runner/error/cloud-runner-error.ts @@ -1,7 +1,7 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; import * as core from '@actions/core'; import CloudRunner from '../cloud-runner'; -import CloudRunnerSecret from '../services/cloud-runner-secret'; +import CloudRunnerSecret from '../options/cloud-runner-secret'; import BuildParameters from '../../build-parameters'; export class CloudRunnerError { diff --git a/src/model/cloud-runner/services/cloud-runner-constants.ts b/src/model/cloud-runner/options/cloud-runner-constants.ts similarity index 100% rename from src/model/cloud-runner/services/cloud-runner-constants.ts rename to src/model/cloud-runner/options/cloud-runner-constants.ts diff --git a/src/model/cloud-runner/services/cloud-runner-environment-variable.ts b/src/model/cloud-runner/options/cloud-runner-environment-variable.ts similarity index 100% rename from src/model/cloud-runner/services/cloud-runner-environment-variable.ts rename to src/model/cloud-runner/options/cloud-runner-environment-variable.ts diff --git a/src/model/cloud-runner/services/cloud-runner-folders.ts b/src/model/cloud-runner/options/cloud-runner-folders.ts similarity index 91% rename from src/model/cloud-runner/services/cloud-runner-folders.ts rename to src/model/cloud-runner/options/cloud-runner-folders.ts index fb43651f..afb8d038 100644 --- a/src/model/cloud-runner/services/cloud-runner-folders.ts +++ b/src/model/cloud-runner/options/cloud-runner-folders.ts @@ -1,6 +1,7 @@ import path from 'node:path'; -import CloudRunnerOptions from '../cloud-runner-options'; -import CloudRunner from './../cloud-runner'; +import CloudRunnerOptions from './cloud-runner-options'; +import CloudRunner from '../cloud-runner'; +import BuildParameters from '../../build-parameters'; export class CloudRunnerFolders { public static readonly repositoryFolder = 'repo'; @@ -12,7 +13,7 @@ export class CloudRunnerFolders { // Only the following paths that do not start a path.join with another "Full" suffixed property need to start with an absolute / public static get uniqueCloudRunnerJobFolderAbsolute(): string { - return CloudRunner.buildParameters && CloudRunner.buildParameters.retainWorkspace && CloudRunner.lockedWorkspace + return CloudRunner.buildParameters && BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters) ? path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.lockedWorkspace) : path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid); } diff --git a/src/model/cloud-runner/services/cloud-runner-guid.ts b/src/model/cloud-runner/options/cloud-runner-guid.ts similarity index 100% rename from src/model/cloud-runner/services/cloud-runner-guid.ts rename to src/model/cloud-runner/options/cloud-runner-guid.ts diff --git a/src/model/cloud-runner/services/cloud-runner-options-reader.ts b/src/model/cloud-runner/options/cloud-runner-options-reader.ts similarity index 80% rename from src/model/cloud-runner/services/cloud-runner-options-reader.ts rename to src/model/cloud-runner/options/cloud-runner-options-reader.ts index d8f40dbb..b903febf 100644 --- a/src/model/cloud-runner/services/cloud-runner-options-reader.ts +++ b/src/model/cloud-runner/options/cloud-runner-options-reader.ts @@ -1,5 +1,5 @@ import Input from '../../input'; -import CloudRunnerOptions from '../cloud-runner-options'; +import CloudRunnerOptions from './cloud-runner-options'; class CloudRunnerOptionsReader { static GetProperties() { diff --git a/src/model/cloud-runner/cloud-runner-options.ts b/src/model/cloud-runner/options/cloud-runner-options.ts similarity index 52% rename from src/model/cloud-runner/cloud-runner-options.ts rename to src/model/cloud-runner/options/cloud-runner-options.ts index c3f39801..830deb68 100644 --- a/src/model/cloud-runner/cloud-runner-options.ts +++ b/src/model/cloud-runner/options/cloud-runner-options.ts @@ -1,6 +1,6 @@ -import { Cli } from '../cli/cli'; -import CloudRunnerQueryOverride from './services/cloud-runner-query-override'; -import GitHub from '../github'; +import { Cli } from '../../cli/cli'; +import CloudRunnerQueryOverride from './cloud-runner-query-override'; +import GitHub from '../../github'; import * as core from '@actions/core'; class CloudRunnerOptions { @@ -58,7 +58,12 @@ class CloudRunnerOptions { // GitHub parameters // ### ### ### static get githubChecks(): boolean { - return CloudRunnerOptions.getInput('githubChecks') === 'true' || false; + const value = CloudRunnerOptions.getInput('githubChecks'); + + return value === `true` || false; + } + static get githubCheckId(): string { + return CloudRunnerOptions.getInput('githubCheckId') || ``; } static get githubOwner(): string { @@ -69,6 +74,10 @@ class CloudRunnerOptions { return CloudRunnerOptions.getInput('githubRepoName') || CloudRunnerOptions.githubRepo?.split(`/`)[1] || ''; } + static get finalHooks(): string[] { + return CloudRunnerOptions.getInput('finalHooks')?.split(',') || []; + } + // ### ### ### // Git syncronization parameters // ### ### ### @@ -76,59 +85,54 @@ class CloudRunnerOptions { static get githubRepo(): string | undefined { return CloudRunnerOptions.getInput('GITHUB_REPOSITORY') || CloudRunnerOptions.getInput('GITHUB_REPO') || undefined; } - static get branch(): string { if (CloudRunnerOptions.getInput(`GITHUB_REF`)) { - return CloudRunnerOptions.getInput(`GITHUB_REF`)!.replace('refs/', '').replace(`head/`, '').replace(`heads/`, ''); + return ( + CloudRunnerOptions.getInput(`GITHUB_REF`)?.replace('refs/', '').replace(`head/`, '').replace(`heads/`, '') || `` + ); } else if (CloudRunnerOptions.getInput('branch')) { - return CloudRunnerOptions.getInput('branch')!; + return CloudRunnerOptions.getInput('branch') || ``; } else { return ''; } } - static get gitSha(): string | undefined { - if (CloudRunnerOptions.getInput(`GITHUB_SHA`)) { - return CloudRunnerOptions.getInput(`GITHUB_SHA`)!; - } else if (CloudRunnerOptions.getInput(`GitSHA`)) { - return CloudRunnerOptions.getInput(`GitSHA`)!; - } - } - // ### ### ### // Cloud Runner parameters // ### ### ### - static get cloudRunnerBuilderPlatform(): string | undefined { - const input = CloudRunnerOptions.getInput('cloudRunnerBuilderPlatform'); + static get buildPlatform(): string { + const input = CloudRunnerOptions.getInput('buildPlatform'); if (input) { return input; } - if (CloudRunnerOptions.cloudRunnerCluster !== 'local') { + if (CloudRunnerOptions.providerStrategy !== 'local') { return 'linux'; } - return; + return ``; } static get cloudRunnerBranch(): string { return CloudRunnerOptions.getInput('cloudRunnerBranch') || 'main'; } - static get cloudRunnerCluster(): string { + static get providerStrategy(): string { + const provider = + CloudRunnerOptions.getInput('cloudRunnerCluster') || CloudRunnerOptions.getInput('providerStrategy'); if (Cli.isCliMode) { - return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'aws'; + return provider || 'aws'; } - return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'local'; + return provider || 'local'; } - static get cloudRunnerCpu(): string | undefined { - return CloudRunnerOptions.getInput('cloudRunnerCpu'); + static get containerCpu(): string { + return CloudRunnerOptions.getInput('containerCpu') || `1024`; } - static get cloudRunnerMemory(): string | undefined { - return CloudRunnerOptions.getInput('cloudRunnerMemory'); + static get containerMemory(): string { + return CloudRunnerOptions.getInput('containerMemory') || `3072`; } static get customJob(): string { @@ -139,40 +143,40 @@ class CloudRunnerOptions { // Custom commands from files parameters // ### ### ### - static get customStepFiles(): string[] { - return CloudRunnerOptions.getInput('customStepFiles')?.split(`,`) || []; + static get containerHookFiles(): string[] { + return CloudRunnerOptions.getInput('containerHookFiles')?.split(`,`) || []; } - static get customHookFiles(): string[] { - return CloudRunnerOptions.getInput('customHookFiles')?.split(`,`) || []; + static get commandHookFiles(): string[] { + return CloudRunnerOptions.getInput('commandHookFiles')?.split(`,`) || []; } // ### ### ### // Custom commands from yaml parameters // ### ### ### - static customJobHooks(): string { - return CloudRunnerOptions.getInput('customJobHooks') || ''; + static get commandHooks(): string { + return CloudRunnerOptions.getInput('commandHooks') || ''; } - static get postBuildSteps(): string { - return CloudRunnerOptions.getInput('postBuildSteps') || ''; + static get postBuildContainerHooks(): string { + return CloudRunnerOptions.getInput('postBuildContainerHooks') || ''; } - static get preBuildSteps(): string { - return CloudRunnerOptions.getInput('preBuildSteps') || ''; + static get preBuildContainerHooks(): string { + return CloudRunnerOptions.getInput('preBuildContainerHooks') || ''; } // ### ### ### // Input override handling // ### ### ### - static readInputFromOverrideList(): string { - return CloudRunnerOptions.getInput('readInputFromOverrideList') || ''; + static get pullInputList(): string[] { + return CloudRunnerOptions.getInput('pullInputList')?.split(`,`) || []; } - static readInputOverrideCommand(): string { - const value = CloudRunnerOptions.getInput('readInputOverrideCommand'); + static get inputPullCommand(): string { + const value = CloudRunnerOptions.getInput('inputPullCommand'); if (value === 'gcp-secret-manager') { return 'gcloud secrets versions access 1 --secret="{0}"'; @@ -187,8 +191,8 @@ class CloudRunnerOptions { // Aws // ### ### ### - static get awsBaseStackName(): string { - return CloudRunnerOptions.getInput('awsBaseStackName') || 'game-ci'; + static get awsStackName() { + return CloudRunnerOptions.getInput('awsStackName') || 'game-ci'; } // ### ### ### @@ -204,7 +208,7 @@ class CloudRunnerOptions { } static get kubeVolumeSize(): string { - return CloudRunnerOptions.getInput('kubeVolumeSize') || '5Gi'; + return CloudRunnerOptions.getInput('kubeVolumeSize') || '25Gi'; } static get kubeStorageClass(): string { @@ -225,40 +229,34 @@ class CloudRunnerOptions { static get cloudRunnerDebug(): boolean { return ( - CloudRunnerOptions.getInput(`cloudRunnerTests`) === 'true' || - CloudRunnerOptions.getInput(`cloudRunnerDebug`) === 'true' || + CloudRunnerOptions.getInput(`cloudRunnerTests`) === `true` || + CloudRunnerOptions.getInput(`cloudRunnerDebug`) === `true` || + CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) === `true` || + CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) === `true` || false ); } - static get cloudRunnerDebugTree(): string | boolean { - return CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) || false; + static get skipLfs(): boolean { + return CloudRunnerOptions.getInput(`skipLfs`) === `true`; } - static get cloudRunnerDebugEnv(): string | boolean { - return CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) || false; + static get skipCache(): boolean { + return CloudRunnerOptions.getInput(`skipCache`) === `true`; } - static get watchCloudRunnerToEnd(): boolean { - if (CloudRunnerOptions.asyncCloudRunner) { - return false; - } - - return CloudRunnerOptions.getInput(`watchToEnd`) === 'true' || true; + public static get asyncCloudRunner(): boolean { + return CloudRunnerOptions.getInput('asyncCloudRunner') === 'true'; } - static get asyncCloudRunner(): boolean { - return (CloudRunnerOptions.getInput('asyncCloudRunner') || `false`) === `true` || false; - } - - public static get useSharedLargePackages(): boolean { - return (CloudRunnerOptions.getInput(`useSharedLargePackages`) || 'false') === 'true'; + public static get useLargePackages(): boolean { + return CloudRunnerOptions.getInput(`useLargePackages`) === `true`; } public static get useSharedBuilder(): boolean { - return (CloudRunnerOptions.getInput(`useSharedBuilder`) || 'true') === 'true'; + return CloudRunnerOptions.getInput(`useSharedBuilder`) === `true`; } - public static get useLz4Compression(): boolean { - return (CloudRunnerOptions.getInput(`useLz4Compression`) || 'false') === 'true'; + public static get useCompressionStrategy(): boolean { + return CloudRunnerOptions.getInput(`useCompressionStrategy`) === `true`; } public static get useCleanupCron(): boolean { @@ -269,24 +267,16 @@ class CloudRunnerOptions { // Retained Workspace // ### ### ### - public static get retainWorkspaces(): boolean { - return CloudRunnerOptions.getInput(`retainWorkspaces`) === 'true' || false; - } - - static get maxRetainedWorkspaces(): number { - return Number(CloudRunnerOptions.getInput(`maxRetainedWorkspaces`)) || 3; + public static get maxRetainedWorkspaces(): string { + return CloudRunnerOptions.getInput(`maxRetainedWorkspaces`) || `0`; } // ### ### ### // Garbage Collection // ### ### ### - static get constantGarbageCollection(): boolean { - return CloudRunnerOptions.getInput(`constantGarbageCollection`) === 'true' || true; - } - - static get garbageCollectionMaxAge(): number { - return Number(CloudRunnerOptions.getInput(`garbageCollectionMaxAge`)) || 24; + static get garbageMaxAge(): number { + return Number(CloudRunnerOptions.getInput(`garbageMaxAge`)) || 24; } } diff --git a/src/model/cloud-runner/services/cloud-runner-query-override.ts b/src/model/cloud-runner/options/cloud-runner-query-override.ts similarity index 76% rename from src/model/cloud-runner/services/cloud-runner-query-override.ts rename to src/model/cloud-runner/options/cloud-runner-query-override.ts index 9fddcf2e..4d24b4f2 100644 --- a/src/model/cloud-runner/services/cloud-runner-query-override.ts +++ b/src/model/cloud-runner/options/cloud-runner-query-override.ts @@ -1,6 +1,6 @@ import Input from '../../input'; import { GenericInputReader } from '../../input-readers/generic-input-reader'; -import CloudRunnerOptions from '../cloud-runner-options'; +import CloudRunnerOptions from './cloud-runner-options'; const formatFunction = (value: string, arguments_: any[]) => { for (const element of arguments_) { @@ -31,11 +31,11 @@ class CloudRunnerQueryOverride { } private static shouldUseOverride(query: string) { - if (CloudRunnerOptions.readInputOverrideCommand() !== '') { - if (CloudRunnerOptions.readInputFromOverrideList() !== '') { + if (CloudRunnerOptions.inputPullCommand !== '') { + if (CloudRunnerOptions.pullInputList.length > 0) { const doesInclude = - CloudRunnerOptions.readInputFromOverrideList().split(',').includes(query) || - CloudRunnerOptions.readInputFromOverrideList().split(',').includes(Input.ToEnvVarFormat(query)); + CloudRunnerOptions.pullInputList.includes(query) || + CloudRunnerOptions.pullInputList.includes(Input.ToEnvVarFormat(query)); return doesInclude ? true : false; } else { @@ -50,12 +50,12 @@ class CloudRunnerQueryOverride { } return await GenericInputReader.Run( - formatFunction(CloudRunnerOptions.readInputOverrideCommand(), [{ key: 0, value: query }]), + formatFunction(CloudRunnerOptions.inputPullCommand, [{ key: 0, value: query }]), ); } public static async PopulateQueryOverrideInput() { - const queries = CloudRunnerOptions.readInputFromOverrideList().split(','); + const queries = CloudRunnerOptions.pullInputList; CloudRunnerQueryOverride.queryOverrides = {}; for (const element of queries) { if (CloudRunnerQueryOverride.shouldUseOverride(element)) { diff --git a/src/model/cloud-runner/services/cloud-runner-secret.ts b/src/model/cloud-runner/options/cloud-runner-secret.ts similarity index 100% rename from src/model/cloud-runner/services/cloud-runner-secret.ts rename to src/model/cloud-runner/options/cloud-runner-secret.ts diff --git a/src/model/cloud-runner/cloud-runner-statics.ts b/src/model/cloud-runner/options/cloud-runner-statics.ts similarity index 100% rename from src/model/cloud-runner/cloud-runner-statics.ts rename to src/model/cloud-runner/options/cloud-runner-statics.ts diff --git a/src/model/cloud-runner/cloud-runner-step-state.ts b/src/model/cloud-runner/options/cloud-runner-step-parameters.ts similarity index 64% rename from src/model/cloud-runner/cloud-runner-step-state.ts rename to src/model/cloud-runner/options/cloud-runner-step-parameters.ts index 94f744bb..110b7e79 100644 --- a/src/model/cloud-runner/cloud-runner-step-state.ts +++ b/src/model/cloud-runner/options/cloud-runner-step-parameters.ts @@ -1,7 +1,7 @@ -import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable'; -import CloudRunnerSecret from './services/cloud-runner-secret'; +import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable'; +import CloudRunnerSecret from './cloud-runner-secret'; -export class CloudRunnerStepState { +export class CloudRunnerStepParameters { public image: string; public environment: CloudRunnerEnvironmentVariable[]; public secrets: CloudRunnerSecret[]; diff --git a/src/model/cloud-runner/providers/aws/aws-base-stack.ts b/src/model/cloud-runner/providers/aws/aws-base-stack.ts index 11147ab9..6ddea5ff 100644 --- a/src/model/cloud-runner/providers/aws/aws-base-stack.ts +++ b/src/model/cloud-runner/providers/aws/aws-base-stack.ts @@ -1,4 +1,4 @@ -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import * as core from '@actions/core'; import * as SDK from 'aws-sdk'; import { BaseStackFormation } from './cloud-formations/base-stack-formation'; diff --git a/src/model/cloud-runner/providers/aws/aws-error.ts b/src/model/cloud-runner/providers/aws/aws-error.ts index 3b46875b..bf58e20b 100644 --- a/src/model/cloud-runner/providers/aws/aws-error.ts +++ b/src/model/cloud-runner/providers/aws/aws-error.ts @@ -1,4 +1,4 @@ -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import * as SDK from 'aws-sdk'; import * as core from '@actions/core'; import CloudRunner from '../../cloud-runner'; 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 002b3c0b..189155bc 100644 --- a/src/model/cloud-runner/providers/aws/aws-job-stack.ts +++ b/src/model/cloud-runner/providers/aws/aws-job-stack.ts @@ -1,12 +1,12 @@ import * as SDK from 'aws-sdk'; import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/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 CloudRunnerOptions from '../../options/cloud-runner-options'; import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation'; export class AWSJobStack { @@ -27,21 +27,19 @@ export class AWSJobStack { ): Promise { const taskDefStackName = `${this.baseStackName}-${buildGuid}`; let taskDefCloudFormation = AWSCloudFormationTemplates.readTaskCloudFormationTemplate(); - const cpu = CloudRunner.buildParameters.cloudRunnerCpu || '1024'; - const memory = CloudRunner.buildParameters.cloudRunnerMemory || '3072'; taskDefCloudFormation = taskDefCloudFormation.replace( `ContainerCpu: Default: 1024`, `ContainerCpu: - Default: ${Number.parseInt(cpu)}`, + Default: ${Number.parseInt(CloudRunner.buildParameters.containerCpu)}`, ); taskDefCloudFormation = taskDefCloudFormation.replace( `ContainerMemory: Default: 2048`, `ContainerMemory: - Default: ${Number.parseInt(memory)}`, + Default: ${Number.parseInt(CloudRunner.buildParameters.containerMemory)}`, ); - if (CloudRunnerOptions.watchCloudRunnerToEnd) { + if (!CloudRunnerOptions.asyncCloudRunner) { taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate( taskDefCloudFormation, '# template resources logstream', @@ -116,7 +114,7 @@ export class AWSJobStack { ...secretsMappedToCloudFormationParameters, ]; CloudRunnerLogger.log( - `Starting AWS job with memory: ${CloudRunner.buildParameters.cloudRunnerMemory} cpu: ${CloudRunner.buildParameters.cloudRunnerCpu}`, + `Starting AWS job with memory: ${CloudRunner.buildParameters.containerMemory} cpu: ${CloudRunner.buildParameters.containerCpu}`, ); let previousStackExists = true; while (previousStackExists) { @@ -140,11 +138,16 @@ export class AWSJobStack { Capabilities: ['CAPABILITY_IAM'], Parameters: parameters, }; - try { CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`); await CF.createStack(createStackInput).promise(); await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise(); + const describeStack = await CF.describeStacks({ StackName: taskDefStackName }).promise(); + for (const parameter of parameters) { + if (!describeStack.Stacks?.[0].Parameters?.some((x) => x.ParameterKey === parameter.ParameterKey)) { + throw new Error(`Parameter ${parameter.ParameterKey} not found in stack`); + } + } } catch (error) { await AWSError.handleStackCreationFailure(error, CF, taskDefStackName); throw error; @@ -180,7 +183,7 @@ export class AWSJobStack { if (CloudRunnerOptions.useCleanupCron) { try { CloudRunnerLogger.log(`Creating job cleanup formation`); - CF.createStack(createCleanupStackInput).promise(); + await CF.createStack(createCleanupStackInput).promise(); // await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise(); } catch (error) { 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 8fb5cde0..2030d845 100644 --- a/src/model/cloud-runner/providers/aws/aws-task-runner.ts +++ b/src/model/cloud-runner/providers/aws/aws-task-runner.ts @@ -1,14 +1,14 @@ import * as AWS from 'aws-sdk'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; import * as core from '@actions/core'; import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; import * as zlib from 'node:zlib'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { Input } from '../../..'; 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 { CommandHookService } from '../../services/hooks/command-hook-service'; +import { FollowLogStreamService } from '../../services/core/follow-log-stream-service'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; import GitHub from '../../../github'; class AWSTaskRunner { @@ -32,7 +32,7 @@ class AWSTaskRunner { const streamName = taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || ''; - const task = await AWSTaskRunner.ECS.runTask({ + const runParameters = { cluster, taskDefinition, platformVersion: '1.4.0', @@ -41,7 +41,7 @@ class AWSTaskRunner { { name: taskDef.taskDefStackName, environment, - command: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(commands, CloudRunner.buildParameters)], + command: ['-c', CommandHookService.ApplyHooksToCommands(commands, CloudRunner.buildParameters)], }, ], }, @@ -53,16 +53,23 @@ class AWSTaskRunner { securityGroups: [ContainerSecurityGroup], }, }, - }).promise(); + }; + + if (JSON.stringify(runParameters.overrides.containerOverrides).length > 8192) { + CloudRunnerLogger.log(JSON.stringify(runParameters.overrides.containerOverrides, undefined, 4)); + throw new Error(`Container Overrides length must be at most 8192`); + } + + const task = await AWSTaskRunner.ECS.runTask(runParameters).promise(); const taskArn = task.tasks?.[0].taskArn || ''; 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} Watch:${ - CloudRunnerOptions.watchCloudRunnerToEnd - } Async:${CloudRunnerOptions.asyncCloudRunner}`, + `Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus} Async:${ + CloudRunnerOptions.asyncCloudRunner + }`, ); - if (!CloudRunnerOptions.watchCloudRunnerToEnd) { + if (CloudRunnerOptions.asyncCloudRunner) { const shouldCleanup: boolean = false; const output: string = ''; CloudRunnerLogger.log(`Watch Cloud Runner To End: false`); @@ -72,26 +79,31 @@ class AWSTaskRunner { CloudRunnerLogger.log(`Streaming...`); const { output, shouldCleanup } = await this.streamLogsUntilTaskStops(cluster, taskArn, streamName); - await new Promise((resolve) => resolve(5000)); - const taskData = await AWSTaskRunner.describeTasks(cluster, taskArn); - const containerState = taskData.containers?.[0]; - const exitCode = containerState?.exitCode || undefined; + let exitCode; + let containerState; + let taskData; + while (exitCode === undefined) { + await new Promise((resolve) => resolve(10000)); + taskData = await AWSTaskRunner.describeTasks(cluster, taskArn); + containerState = taskData.containers?.[0]; + exitCode = containerState?.exitCode; + } CloudRunnerLogger.log(`Container State: ${JSON.stringify(containerState, undefined, 4)}`); - const wasSuccessful = exitCode === 0 || (exitCode === undefined && taskData.lastStatus === 'RUNNING'); + if (exitCode === undefined) { + CloudRunnerLogger.logWarning(`Undefined exitcode for container`); + } + const wasSuccessful = exitCode === 0; if (wasSuccessful) { CloudRunnerLogger.log(`Cloud runner job has finished successfully`); return { output, shouldCleanup }; - } else { - if (taskData.stoppedReason === 'Essential container in task exited' && exitCode === 1) { - throw new Error('Container exited with code 1'); - } - const message = `Cloud runner job exit code ${exitCode}`; - taskData.overrides = undefined; - taskData.attachments = undefined; - CloudRunnerLogger.log(`${message} ${JSON.stringify(taskData, undefined, 4)}`); - throw new Error(message); } + + if (taskData?.stoppedReason === 'Essential container in task exited' && exitCode === 1) { + throw new Error('Container exited with code 1'); + } + + throw new Error(`Task failed`); } private static async waitUntilTaskRunning(taskArn: string, cluster: string) { @@ -129,7 +141,7 @@ 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}${AWSTaskRunner.encodedUnderscore}${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.awsStackName}${AWSTaskRunner.encodedUnderscore}${CloudRunner.buildParameters.awsStackName}-${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; 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 4d792b09..f7284ed2 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 @@ -30,7 +30,7 @@ Parameters: Type: Number Description: How much CPU to give the container. 1024 is 1 CPU ContainerMemory: - Default: 2048 + Default: 4096 Type: Number Description: How much memory in megabytes to give the container BUILDGUID: diff --git a/src/model/cloud-runner/providers/aws/index.ts b/src/model/cloud-runner/providers/aws/index.ts index dc8c2322..ab36fb37 100644 --- a/src/model/cloud-runner/providers/aws/index.ts +++ b/src/model/cloud-runner/providers/aws/index.ts @@ -1,11 +1,11 @@ import * as SDK from 'aws-sdk'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; import AwsTaskRunner from './aws-task-runner'; import { ProviderInterface } from '../provider-interface'; import BuildParameters from '../../../build-parameters'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { AWSJobStack as AwsJobStack } from './aws-job-stack'; import { AWSBaseStack as AwsBaseStack } from './aws-base-stack'; import { Input } from '../../..'; @@ -13,13 +13,13 @@ 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'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; class AWSBuildEnvironment implements ProviderInterface { private baseStackName: string; constructor(buildParameters: BuildParameters) { - this.baseStackName = buildParameters.awsBaseStackName; + this.baseStackName = buildParameters.awsStackName; } async listResources(): Promise { await TaskService.getCloudFormationJobStacks(); @@ -75,7 +75,11 @@ class AWSBuildEnvironment implements ProviderInterface { branchName: string, // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) {} + ) { + process.env.AWS_REGION = Input.region; + const CF = new SDK.CloudFormation(); + await new AwsBaseStack(this.baseStackName).setupBaseStack(CF); + } async runTaskInWorkflow( buildGuid: string, @@ -94,8 +98,6 @@ class AWSBuildEnvironment implements ProviderInterface { CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`); const entrypoint = ['/bin/sh']; const startTimeMs = Date.now(); - - await new AwsBaseStack(this.baseStackName).setupBaseStack(CF); const taskDef = await new AwsJobStack(this.baseStackName).setupCloudFormations( CF, buildGuid, @@ -143,6 +145,9 @@ class AWSBuildEnvironment implements ProviderInterface { await CF.waitFor('stackDeleteComplete', { StackName: taskDef.taskDefStackName, }).promise(); + await CF.waitFor('stackDeleteComplete', { + StackName: `${taskDef.taskDefStackName}-cleanup`, + }).promise(); CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`); CloudRunnerLogger.log('Cleanup complete'); } diff --git a/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts b/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts index 087e97e5..27d299da 100644 --- a/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts +++ b/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts @@ -1,6 +1,6 @@ import AWS from 'aws-sdk'; import Input from '../../../../input'; -import CloudRunnerLogger from '../../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../../services/core/cloud-runner-logger'; import { TaskService } from './task-service'; export class GarbageCollectionService { 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 87c570c7..ee5fb5c0 100644 --- a/src/model/cloud-runner/providers/aws/services/task-service.ts +++ b/src/model/cloud-runner/providers/aws/services/task-service.ts @@ -1,6 +1,6 @@ import AWS from 'aws-sdk'; import Input from '../../../../input'; -import CloudRunnerLogger from '../../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../../services/core/cloud-runner-logger'; import { BaseStackFormation } from '../cloud-formations/base-stack-formation'; import AwsTaskRunner from '../aws-task-runner'; import { ListObjectsRequest } from 'aws-sdk/clients/s3'; @@ -161,7 +161,7 @@ export class TaskService { process.env.AWS_REGION = Input.region; const s3 = new AWS.S3(); const listRequest: ListObjectsRequest = { - Bucket: CloudRunner.buildParameters.awsBaseStackName, + Bucket: CloudRunner.buildParameters.awsStackName, }; 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 5fe1eeec..6837b1a9 100644 --- a/src/model/cloud-runner/providers/docker/index.ts +++ b/src/model/cloud-runner/providers/docker/index.ts @@ -1,20 +1,21 @@ import BuildParameters from '../../../build-parameters'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { ProviderInterface } from '../provider-interface'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import Docker from '../../../docker'; import { Action } from '../../..'; -import { writeFileSync } from 'fs'; +import { writeFileSync } from 'node:fs'; import CloudRunner from '../../cloud-runner'; import { ProviderResource } from '../provider-resource'; import { ProviderWorkflow } from '../provider-workflow'; -import { CloudRunnerSystem } from '../../services/cloud-runner-system'; -import fs from 'node:fs'; +import { CloudRunnerSystem } from '../../services/core/cloud-runner-system'; +import * as fs from 'node:fs'; +import { CommandHookService } from '../../services/hooks/command-hook-service'; import { StringKeyValuePair } from '../../../shared-types'; class LocalDockerCloudRunner implements ProviderInterface { - public buildParameters: BuildParameters | undefined; + public buildParameters!: BuildParameters; listResources(): Promise { return new Promise((resolve) => resolve([])); @@ -51,14 +52,14 @@ class LocalDockerCloudRunner implements ProviderInterface { if ( fs.existsSync( `${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${ - CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.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${ - CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' }`, ); } @@ -118,7 +119,7 @@ set -e mkdir -p /github/workspace/cloud-runner-cache mkdir -p /data/cache cp -a /github/workspace/cloud-runner-cache/. ${sharedFolder} -${commands} +${CommandHookService.ApplyHooksToCommands(commands, this.buildParameters)} cp -a ${sharedFolder}. /github/workspace/cloud-runner-cache/ `; writeFileSync(`${workspace}/${entrypointFilePath}`, fileContents, { @@ -149,6 +150,7 @@ cp -a ${sharedFolder}. /github/workspace/cloud-runner-cache/ }, }, true, + false, ); return myOutput; diff --git a/src/model/cloud-runner/providers/k8s/index.ts b/src/model/cloud-runner/providers/k8s/index.ts index c06d1682..5a352798 100644 --- a/src/model/cloud-runner/providers/k8s/index.ts +++ b/src/model/cloud-runner/providers/k8s/index.ts @@ -2,19 +2,18 @@ import * as k8s from '@kubernetes/client-node'; import { BuildParameters } from '../../..'; import * as core from '@actions/core'; import { ProviderInterface } from '../provider-interface'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import KubernetesStorage from './kubernetes-storage'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; import KubernetesTaskRunner from './kubernetes-task-runner'; import KubernetesSecret from './kubernetes-secret'; import KubernetesJobSpecFactory from './kubernetes-job-spec-factory'; import KubernetesServiceAccount from './kubernetes-service-account'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { CoreV1Api } from '@kubernetes/client-node'; import CloudRunner from '../../cloud-runner'; import { ProviderResource } from '../provider-resource'; import { ProviderWorkflow } from '../provider-workflow'; -import KubernetesPods from './kubernetes-pods'; class Kubernetes implements ProviderInterface { public static Instance: Kubernetes; @@ -94,16 +93,8 @@ class Kubernetes implements ProviderInterface { ) { try { this.buildParameters = buildParameters; - const id = buildParameters.retainWorkspace ? CloudRunner.lockedWorkspace : buildParameters.buildGuid; - this.pvcName = `unity-builder-pvc-${id}`; - this.cleanupCronJobName = `unity-builder-cronjob-${id}`; + this.cleanupCronJobName = `unity-builder-cronjob-${buildParameters.buildGuid}`; this.serviceAccountName = `service-account-${buildParameters.buildGuid}`; - await KubernetesStorage.createPersistentVolumeClaim( - buildParameters, - this.pvcName, - this.kubeClient, - this.namespace, - ); await KubernetesServiceAccount.createServiceAccount(this.serviceAccountName, this.namespace, this.kubeClient); } catch (error) { @@ -124,74 +115,99 @@ class Kubernetes implements ProviderInterface { CloudRunnerLogger.log('Cloud Runner K8s workflow!'); // Setup + const id = BuildParameters.shouldUseRetainedWorkspaceMode(this.buildParameters) + ? CloudRunner.lockedWorkspace + : this.buildParameters.buildGuid; + this.pvcName = `unity-builder-pvc-${id}`; + await KubernetesStorage.createPersistentVolumeClaim( + this.buildParameters, + this.pvcName, + this.kubeClient, + this.namespace, + ); this.buildGuid = buildGuid; this.secretName = `build-credentials-${this.buildGuid}`; this.jobName = `unity-builder-job-${this.buildGuid}`; this.containerName = `main`; await KubernetesSecret.createSecret(secrets, this.secretName, this.namespace, this.kubeClient); - await this.createNamespacedJob(commands, image, mountdir, workingdir, environment, secrets); - this.setPodNameAndContainerName(await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace)); - CloudRunnerLogger.log('Watching pod until running'); - await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace); let output = ''; - // eslint-disable-next-line no-constant-condition - while (true) { - try { - CloudRunnerLogger.log('Pod running, streaming logs'); - output = await KubernetesTaskRunner.runTask( - this.kubeConfig, - this.kubeClient, - this.jobName, - this.podName, - 'main', - this.namespace, - ); - const running = await KubernetesPods.IsPodRunning(this.podName, this.namespace, this.kubeClient); + try { + CloudRunnerLogger.log('Job does not exist'); + await this.createJob(commands, image, mountdir, workingdir, environment, secrets); + CloudRunnerLogger.log('Watching pod until running'); + await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace); - if (!running) { - CloudRunnerLogger.log(`Pod not found, assumed ended!`); - break; - } else { - CloudRunnerLogger.log('Pod still running, recovering stream...'); - } - await this.cleanupTaskResources(); - } catch (error: any) { - let errorParsed; - try { - errorParsed = JSON.parse(error); - } catch { - errorParsed = error; - } - - 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)); - continue; - } else { - CloudRunnerLogger.log(`error running k8s workflow ${error}`); - throw error; - } - } + CloudRunnerLogger.log('Pod running, streaming logs'); + CloudRunnerLogger.log( + `Starting logs follow for pod: ${this.podName} container: ${this.containerName} namespace: ${this.namespace} pvc: ${this.pvcName} ${CloudRunner.buildParameters.kubeVolumeSize}/${CloudRunner.buildParameters.containerCpu}/${CloudRunner.buildParameters.containerMemory}`, + ); + output += await KubernetesTaskRunner.runTask( + this.kubeConfig, + this.kubeClient, + this.jobName, + this.podName, + this.containerName, + this.namespace, + ); + } catch (error: any) { + CloudRunnerLogger.log(`error running k8s workflow ${error}`); + await new Promise((resolve) => setTimeout(resolve, 3000)); + CloudRunnerLogger.log( + JSON.stringify( + (await this.kubeClient.listNamespacedEvent(this.namespace)).body.items + .map((x) => { + return { + message: x.message || ``, + name: x.metadata.name || ``, + reason: x.reason || ``, + }; + }) + .filter((x) => x.name.includes(this.podName)), + undefined, + 4, + ), + ); + await this.cleanupTaskResources(); + throw error; } + await this.cleanupTaskResources(); + return output; } catch (error) { CloudRunnerLogger.log('Running job failed'); core.error(JSON.stringify(error, undefined, 4)); - await this.cleanupTaskResources(); + + // await this.cleanupTaskResources(); throw error; } } + private async createJob( + commands: string, + image: string, + mountdir: string, + workingdir: string, + environment: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ) { + await this.createNamespacedJob(commands, image, mountdir, workingdir, environment, secrets); + const find = await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace); + this.setPodNameAndContainerName(find); + } + + private async doesJobExist(name: string) { + const jobs = await this.kubeClientBatch.listNamespacedJob(this.namespace); + + return jobs.body.items.some((x) => x.metadata?.name === name); + } + + private async doesFailedJobExist() { + const podStatus = await this.kubeClient.readNamespacedPodStatus(this.podName, this.namespace); + + return podStatus.body.status?.phase === `Failed`; + } + private async createNamespacedJob( commands: string, image: string, @@ -215,14 +231,15 @@ class Kubernetes implements ProviderInterface { this.pvcName, this.jobName, k8s, + this.containerName, ); await new Promise((promise) => setTimeout(promise, 15000)); - await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec); + const result = await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec); CloudRunnerLogger.log(`Build job created`); await new Promise((promise) => setTimeout(promise, 5000)); CloudRunnerLogger.log('Job created'); - return; + return result.body.metadata?.name; } catch (error) { CloudRunnerLogger.log(`Error occured creating job: ${error}`); throw error; @@ -232,7 +249,7 @@ class Kubernetes implements ProviderInterface { setPodNameAndContainerName(pod: k8s.V1Pod) { this.podName = pod.metadata?.name || ''; - this.containerName = pod.status?.containerStatuses?.[0].name || ''; + this.containerName = pod.status?.containerStatuses?.[0].name || this.containerName; } async cleanupTaskResources() { @@ -265,7 +282,7 @@ class Kubernetes implements ProviderInterface { // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) { - if (buildParameters.retainWorkspace) { + if (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) { return; } CloudRunnerLogger.log(`deleting PVC`); 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 0f62f17a..52b928f9 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 @@ -1,8 +1,8 @@ import { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } from '@kubernetes/client-node'; import BuildParameters from '../../../build-parameters'; -import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import { CommandHookService } from '../../services/hooks/command-hook-service'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import CloudRunner from '../../cloud-runner'; class KubernetesJobSpecFactory { @@ -19,63 +19,8 @@ class KubernetesJobSpecFactory { pvcName: string, jobName: string, k8s: any, + containerName: string, ) { - environment.push( - ...[ - { - name: 'GITHUB_SHA', - value: buildGuid, - }, - { - name: 'GITHUB_WORKSPACE', - value: '/data/repo', - }, - { - name: 'PROJECT_PATH', - value: buildParameters.projectPath, - }, - { - name: 'BUILD_PATH', - value: buildParameters.buildPath, - }, - { - name: 'BUILD_FILE', - value: buildParameters.buildFile, - }, - { - name: 'BUILD_NAME', - value: buildParameters.buildName, - }, - { - name: 'BUILD_METHOD', - value: buildParameters.buildMethod, - }, - { - name: 'CUSTOM_PARAMETERS', - value: buildParameters.customParameters, - }, - { - name: 'CHOWN_FILES_TO', - value: buildParameters.chownFilesTo, - }, - { - name: 'BUILD_TARGET', - value: buildParameters.targetPlatform, - }, - { - name: 'ANDROID_VERSION_CODE', - value: buildParameters.androidVersionCode.toString(), - }, - { - name: 'ANDROID_KEYSTORE_NAME', - value: buildParameters.androidKeystoreName, - }, - { - name: 'ANDROID_KEYALIAS_NAME', - value: buildParameters.androidKeyaliasName, - }, - ], - ); const job = new k8s.V1Job(); job.apiVersion = 'batch/v1'; job.kind = 'Job'; @@ -87,6 +32,7 @@ class KubernetesJobSpecFactory { }, }; job.spec = { + ttlSecondsAfterFinished: 9999, backoffLimit: 0, template: { spec: { @@ -100,16 +46,20 @@ class KubernetesJobSpecFactory { ], containers: [ { - name: 'main', + ttlSecondsAfterFinished: 9999, + name: containerName, image, command: ['/bin/sh'], - args: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(command, CloudRunner.buildParameters)], + args: [ + '-c', + `${CommandHookService.ApplyHooksToCommands(`${command}\nsleep 2m`, CloudRunner.buildParameters)}`, + ], workingDir: `${workingDirectory}`, resources: { requests: { - memory: buildParameters.cloudRunnerMemory || '750M', - cpu: buildParameters.cloudRunnerCpu || '1', + memory: `${Number.parseInt(buildParameters.containerMemory) / 1024}G` || '750M', + cpu: Number.parseInt(buildParameters.containerCpu) / 1024 || '1', }, }, env: [ @@ -135,7 +85,7 @@ class KubernetesJobSpecFactory { volumeMounts: [ { name: 'build-mount', - mountPath: `/${mountdir}`, + mountPath: `${mountdir}`, }, ], lifecycle: { @@ -158,7 +108,7 @@ class KubernetesJobSpecFactory { }, }; - job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '5Gi'; + job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '10Gi'; 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 a496b88c..0911f39a 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts @@ -1,4 +1,4 @@ -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { CoreV1Api } from '@kubernetes/client-node'; class KubernetesPods { public static async IsPodRunning(podName: string, namespace: string, kubeClient: CoreV1Api) { @@ -12,6 +12,12 @@ class KubernetesPods { return running; } + public static async GetPodStatus(podName: string, namespace: string, kubeClient: CoreV1Api) { + const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.find((x) => podName === x.metadata?.name); + const phase = pods?.status?.phase || 'undefined status'; + + return phase; + } } export default KubernetesPods; diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts b/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts index a8dd9f46..3a4bb5ae 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts @@ -1,7 +1,7 @@ import { CoreV1Api } from '@kubernetes/client-node'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import * as k8s from '@kubernetes/client-node'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import * as base64 from 'base-64'; class KubernetesSecret { diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts b/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts index 2c3e04f0..7f6f642d 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts @@ -2,7 +2,7 @@ import waitUntil from 'async-wait-until'; import * as core from '@actions/core'; import * as k8s from '@kubernetes/client-node'; import BuildParameters from '../../../build-parameters'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { IncomingMessage } from 'node:http'; import GitHub from '../../../github'; diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts b/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts index da70be19..dd7450e8 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts @@ -1,12 +1,15 @@ -import { CoreV1Api, KubeConfig, Log } from '@kubernetes/client-node'; -import { Writable } from 'stream'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; -import * as core from '@actions/core'; -import { CloudRunnerStatics } from '../../cloud-runner-statics'; +import { CoreV1Api, KubeConfig } from '@kubernetes/client-node'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import waitUntil from 'async-wait-until'; -import { FollowLogStreamService } from '../../services/follow-log-stream-service'; +import { CloudRunnerSystem } from '../../services/core/cloud-runner-system'; +import CloudRunner from '../../cloud-runner'; +import KubernetesPods from './kubernetes-pods'; +import { FollowLogStreamService } from '../../services/core/follow-log-stream-service'; class KubernetesTaskRunner { + static lastReceivedTimestamp: number = 0; + static readonly maxRetry: number = 3; + static lastReceivedMessage: string = ``; static async runTask( kubeConfig: KubeConfig, kubeClient: CoreV1Api, @@ -15,84 +18,120 @@ class KubernetesTaskRunner { containerName: string, namespace: string, ) { - CloudRunnerLogger.log(`Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace}`); - const stream = new Writable(); let output = ''; - let didStreamAnyLogs: boolean = false; let shouldReadLogs = true; let shouldCleanup = true; - stream._write = (chunk, encoding, next) => { - didStreamAnyLogs = true; - let message = chunk.toString().trimRight(`\n`); - message = `[${CloudRunnerStatics.logPrefix}] ${message}`; - ({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration( - message, - shouldReadLogs, - shouldCleanup, - output, - )); - next(); - }; - const logOptions = { - follow: true, - pretty: false, - previous: false, - }; - try { - const resultError = await new Promise((resolve) => - new Log(kubeConfig).log(namespace, podName, containerName, stream, resolve, logOptions), + let sinceTime = ``; + let retriesAfterFinish = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + const lastReceivedMessage = + KubernetesTaskRunner.lastReceivedTimestamp > 0 + ? `\nLast Log Message "${this.lastReceivedMessage}" ${this.lastReceivedTimestamp}` + : ``; + CloudRunnerLogger.log( + `Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace} ${CloudRunner.buildParameters.kubeVolumeSize}/${CloudRunner.buildParameters.containerCpu}/${CloudRunner.buildParameters.containerMemory}\n${lastReceivedMessage}`, ); - stream.destroy(); - if (resultError) { - throw resultError; + if (KubernetesTaskRunner.lastReceivedTimestamp > 0) { + const currentDate = new Date(KubernetesTaskRunner.lastReceivedTimestamp); + const dateTimeIsoString = currentDate.toISOString(); + sinceTime = ` --since-time="${dateTimeIsoString}"`; } - if (!didStreamAnyLogs) { - core.error('Failed to stream any logs, listing namespace events, check for an error with the container'); - core.error( - JSON.stringify( - { - events: (await kubeClient.listNamespacedEvent(namespace)).body.items - .filter((x) => { - return x.involvedObject.name === podName || x.involvedObject.name === jobName; - }) - .map((x) => { - return { - type: x.involvedObject.kind, - name: x.involvedObject.name, - message: x.message, - }; - }), - }, - undefined, - 4, - ), + let extraFlags = ``; + extraFlags += (await KubernetesPods.IsPodRunning(podName, namespace, kubeClient)) + ? ` -f -c ${containerName}` + : ` --previous`; + let lastMessageSeenIncludedInChunk = false; + let lastMessageSeen = false; + + let logs; + + try { + logs = await CloudRunnerSystem.Run( + `kubectl logs ${podName}${extraFlags} --timestamps${sinceTime}`, + false, + true, ); - throw new Error(`No logs streamed from k8s`); + } catch (error: any) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + const continueStreaming = await KubernetesPods.IsPodRunning(podName, namespace, kubeClient); + CloudRunnerLogger.log(`K8s logging error ${error} ${continueStreaming}`); + if (continueStreaming) { + continue; + } + if (retriesAfterFinish < KubernetesTaskRunner.maxRetry) { + retriesAfterFinish++; + + continue; + } + throw error; } - } catch (error) { - if (stream) { - stream.destroy(); + const splitLogs = logs.split(`\n`); + for (const chunk of splitLogs) { + if ( + chunk.replace(/\s/g, ``) === KubernetesTaskRunner.lastReceivedMessage.replace(/\s/g, ``) && + KubernetesTaskRunner.lastReceivedMessage.replace(/\s/g, ``) !== `` + ) { + CloudRunnerLogger.log(`Previous log message found ${chunk}`); + lastMessageSeenIncludedInChunk = true; + } + } + for (const chunk of splitLogs) { + const newDate = Date.parse(`${chunk.toString().split(`Z `)[0]}Z`); + if (chunk.replace(/\s/g, ``) === KubernetesTaskRunner.lastReceivedMessage.replace(/\s/g, ``)) { + lastMessageSeen = true; + } + if (lastMessageSeenIncludedInChunk && !lastMessageSeen) { + continue; + } + const message = CloudRunner.buildParameters.cloudRunnerDebug ? chunk : chunk.split(`Z `)[1]; + KubernetesTaskRunner.lastReceivedMessage = chunk; + KubernetesTaskRunner.lastReceivedTimestamp = newDate; + ({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration( + message, + shouldReadLogs, + shouldCleanup, + output, + )); + } + if (FollowLogStreamService.DidReceiveEndOfTransmission) { + CloudRunnerLogger.log('end of log stream'); + break; } - throw error; } - CloudRunnerLogger.log('end of log stream'); return output; } static async watchUntilPodRunning(kubeClient: CoreV1Api, podName: string, namespace: string) { let success: boolean = false; + let message = ``; CloudRunnerLogger.log(`Watching ${podName} ${namespace}`); await waitUntil( async () => { const status = await kubeClient.readNamespacedPodStatus(podName, namespace); const phase = status?.body.status?.phase; success = phase === 'Running'; - CloudRunnerLogger.log( - `${status.body.status?.phase} ${status.body.status?.conditions?.[0].reason || ''} ${ - status.body.status?.conditions?.[0].message || '' - }`, - ); + message = `Phase:${status.body.status?.phase} \n Reason:${ + status.body.status?.conditions?.[0].reason || '' + } \n Message:${status.body.status?.conditions?.[0].message || ''}`; + + // CloudRunnerLogger.log( + // JSON.stringify( + // (await kubeClient.listNamespacedEvent(namespace)).body.items + // .map((x) => { + // return { + // message: x.message || ``, + // name: x.metadata.name || ``, + // reason: x.reason || ``, + // }; + // }) + // .filter((x) => x.name.includes(podName)), + // undefined, + // 4, + // ), + // ); if (success || phase !== 'Pending') return true; return false; @@ -102,6 +141,9 @@ class KubernetesTaskRunner { intervalBetweenAttempts: 15000, }, ); + if (!success) { + CloudRunnerLogger.log(message); + } return success; } diff --git a/src/model/cloud-runner/providers/local/index.ts b/src/model/cloud-runner/providers/local/index.ts index 9effda2e..feaa3fdc 100644 --- a/src/model/cloud-runner/providers/local/index.ts +++ b/src/model/cloud-runner/providers/local/index.ts @@ -1,9 +1,9 @@ import BuildParameters from '../../../build-parameters'; -import { CloudRunnerSystem } from '../../services/cloud-runner-system'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { CloudRunnerSystem } from '../../services/core/cloud-runner-system'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { ProviderInterface } from '../provider-interface'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import { ProviderResource } from '../provider-resource'; import { ProviderWorkflow } from '../provider-workflow'; diff --git a/src/model/cloud-runner/providers/provider-interface.ts b/src/model/cloud-runner/providers/provider-interface.ts index 359d4c7f..7de6aa0c 100644 --- a/src/model/cloud-runner/providers/provider-interface.ts +++ b/src/model/cloud-runner/providers/provider-interface.ts @@ -1,6 +1,6 @@ import BuildParameters from '../../build-parameters'; -import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; -import CloudRunnerSecret from '../services/cloud-runner-secret'; +import CloudRunnerEnvironmentVariable from '../options/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../options/cloud-runner-secret'; import { ProviderResource } from './provider-resource'; import { ProviderWorkflow } from './provider-workflow'; diff --git a/src/model/cloud-runner/providers/test/index.ts b/src/model/cloud-runner/providers/test/index.ts index ccabb834..853a00d5 100644 --- a/src/model/cloud-runner/providers/test/index.ts +++ b/src/model/cloud-runner/providers/test/index.ts @@ -1,8 +1,8 @@ import BuildParameters from '../../../build-parameters'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; -import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { ProviderInterface } from '../provider-interface'; -import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; import { ProviderResource } from '../provider-resource'; import { ProviderWorkflow } from '../provider-workflow'; diff --git a/src/model/cloud-runner/remote-client/caching.ts b/src/model/cloud-runner/remote-client/caching.ts index 73be5186..8fc8fd6a 100644 --- a/src/model/cloud-runner/remote-client/caching.ts +++ b/src/model/cloud-runner/remote-client/caching.ts @@ -2,10 +2,10 @@ import { assert } from 'node:console'; import fs from 'node:fs'; import path from 'node:path'; import CloudRunner from '../cloud-runner'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders'; -import { CloudRunnerSystem } from '../services/cloud-runner-system'; -import { LfsHashing } from '../services/lfs-hashing'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; +import { CloudRunnerFolders } from '../options/cloud-runner-folders'; +import { CloudRunnerSystem } from '../services/core/cloud-runner-system'; +import { LfsHashing } from '../services/utility/lfs-hashing'; import { RemoteClientLogger } from './remote-client-logger'; import { Cli } from '../../cli/cli'; import { CliFunction } from '../../cli/cli-functions-repository'; @@ -44,20 +44,21 @@ export class Caching { } public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheArtifactName: string) { + CloudRunnerLogger.log(`Pushing to cache ${sourceFolder}`); cacheArtifactName = cacheArtifactName.replace(' ', ''); const startPath = process.cwd(); let compressionSuffix = ''; - if (CloudRunner.buildParameters.useLz4Compression === true) { + if (CloudRunner.buildParameters.useCompressionStrategy === true) { compressionSuffix = `.lz4`; } - CloudRunnerLogger.log(`Compression: ${CloudRunner.buildParameters.useLz4Compression} ${compressionSuffix}`); + CloudRunnerLogger.log(`Compression: ${CloudRunner.buildParameters.useCompressionStrategy} ${compressionSuffix}`); try { if (!(await fileExists(cacheFolder))) { await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`); } process.chdir(path.resolve(sourceFolder, '..')); - if (CloudRunner.buildParameters.cloudRunnerDebug) { + if (CloudRunner.buildParameters.cloudRunnerDebug === true) { CloudRunnerLogger.log( `Hashed cache folder ${await LfsHashing.hashAllFiles(sourceFolder)} ${sourceFolder} ${path.basename( sourceFolder, @@ -69,11 +70,6 @@ export class Caching { `There is ${contents.length} files/dir in the source folder ${path.basename(sourceFolder)}`, ); - if (CloudRunner.buildParameters.cloudRunnerDebug) { - // await CloudRunnerSystem.Run(`tree -L 2 ./..`); - // await CloudRunnerSystem.Run(`tree -L 2`); - } - if (contents.length === 0) { CloudRunnerLogger.log( `Did not push source folder to cache because it was empty ${path.basename(sourceFolder)}`, @@ -102,9 +98,15 @@ export class Caching { process.chdir(`${startPath}`); } public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheArtifactName: string = ``) { + CloudRunnerLogger.log(`Pulling from cache ${destinationFolder} ${CloudRunner.buildParameters.skipCache}`); + if (`${CloudRunner.buildParameters.skipCache}` === `true`) { + CloudRunnerLogger.log(`Skipping cache debugSkipCache is true`); + + return; + } cacheArtifactName = cacheArtifactName.replace(' ', ''); let compressionSuffix = ''; - if (CloudRunner.buildParameters.useLz4Compression === true) { + if (CloudRunner.buildParameters.useCompressionStrategy === true) { compressionSuffix = `.lz4`; } const startPath = process.cwd(); @@ -160,7 +162,6 @@ 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/remote-client/index.ts b/src/model/cloud-runner/remote-client/index.ts index e62d45c1..520b3614 100644 --- a/src/model/cloud-runner/remote-client/index.ts +++ b/src/model/cloud-runner/remote-client/index.ts @@ -1,56 +1,80 @@ import fs from 'node:fs'; import CloudRunner from '../cloud-runner'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import { CloudRunnerFolders } from '../options/cloud-runner-folders'; import { Caching } from './caching'; -import { LfsHashing } from '../services/lfs-hashing'; +import { LfsHashing } from '../services/utility/lfs-hashing'; import { RemoteClientLogger } from './remote-client-logger'; import path from 'node:path'; import { assert } from 'node:console'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; import { CliFunction } from '../../cli/cli-functions-repository'; -import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import { CloudRunnerSystem } from '../services/core/cloud-runner-system'; import YAML from 'yaml'; +import GitHub from '../../github'; +import BuildParameters from '../../build-parameters'; export class RemoteClient { - public static async bootstrapRepository() { - try { - await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}`); - await CloudRunnerSystem.Run( - `mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForCacheKeyFull)}`, - ); - process.chdir(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)); - await RemoteClient.cloneRepoWithoutLFSFiles(); - RemoteClient.replaceLargePackageReferencesWithSharedReferences(); - await RemoteClient.sizeOfFolder( - 'repo before lfs cache pull', - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute), - ); - const lfsHashes = await LfsHashing.createLFSHashFiles(); - if (fs.existsSync(CloudRunnerFolders.libraryFolderAbsolute)) { - RemoteClientLogger.logWarning(`!Warning!: The Unity library was included in the git repository`); - } - await Caching.PullFromCache( - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull), - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute), - `${lfsHashes.lfsGuidSum}`, - ); - await RemoteClient.sizeOfFolder('repo after lfs cache pull', CloudRunnerFolders.repoPathAbsolute); - await RemoteClient.pullLatestLFS(); - await RemoteClient.sizeOfFolder('repo before lfs git pull', CloudRunnerFolders.repoPathAbsolute); - await Caching.PushToCache( - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull), - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute), - `${lfsHashes.lfsGuidSum}`, - ); - await Caching.PullFromCache( - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryCacheFolderFull), - CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute), - ); - await RemoteClient.sizeOfFolder('repo after library cache pull', CloudRunnerFolders.repoPathAbsolute); - await Caching.handleCachePurging(); - } catch (error) { - throw error; + @CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`) + static async runRemoteClientJob() { + CloudRunnerLogger.log(`bootstrap game ci cloud runner...`); + if (!(await RemoteClient.handleRetainedWorkspace())) { + await RemoteClient.bootstrapRepository(); } + await RemoteClient.runCustomHookFiles(`before-build`); + } + static async runCustomHookFiles(hookLifecycle: string) { + RemoteClientLogger.log(`RunCustomHookFiles: ${hookLifecycle}`); + const gameCiCustomHooksPath = path.join(CloudRunnerFolders.repoPathAbsolute, `game-ci`, `hooks`); + try { + const files = fs.readdirSync(gameCiCustomHooksPath); + for (const file of files) { + const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`); + const fileContentsObject = YAML.parse(fileContents.toString()); + if (fileContentsObject.hook === hookLifecycle) { + RemoteClientLogger.log(`Active Hook File ${file} \n \n file contents: \n ${fileContents}`); + await CloudRunnerSystem.Run(fileContentsObject.commands); + } + } + } catch (error) { + RemoteClientLogger.log(JSON.stringify(error, undefined, 4)); + } + } + public static async bootstrapRepository() { + await CloudRunnerSystem.Run( + `mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`, + ); + await CloudRunnerSystem.Run( + `mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForCacheKeyFull)}`, + ); + await RemoteClient.cloneRepoWithoutLFSFiles(); + await RemoteClient.replaceLargePackageReferencesWithSharedReferences(); + await RemoteClient.sizeOfFolder( + 'repo before lfs cache pull', + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute), + ); + const lfsHashes = await LfsHashing.createLFSHashFiles(); + if (fs.existsSync(CloudRunnerFolders.libraryFolderAbsolute)) { + RemoteClientLogger.logWarning(`!Warning!: The Unity library was included in the git repository`); + } + await Caching.PullFromCache( + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull), + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute), + `${lfsHashes.lfsGuidSum}`, + ); + await RemoteClient.sizeOfFolder('repo after lfs cache pull', CloudRunnerFolders.repoPathAbsolute); + await RemoteClient.pullLatestLFS(); + await RemoteClient.sizeOfFolder('repo before lfs git pull', CloudRunnerFolders.repoPathAbsolute); + await Caching.PushToCache( + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull), + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute), + `${lfsHashes.lfsGuidSum}`, + ); + await Caching.PullFromCache( + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryCacheFolderFull), + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute), + ); + await RemoteClient.sizeOfFolder('repo after library cache pull', CloudRunnerFolders.repoPathAbsolute); + await Caching.handleCachePurging(); } private static async sizeOfFolder(message: string, folder: string) { @@ -62,58 +86,68 @@ export class RemoteClient { private static async cloneRepoWithoutLFSFiles() { process.chdir(`${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); + if ( + fs.existsSync(CloudRunnerFolders.repoPathAbsolute) && + !fs.existsSync(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`)) + ) { + await CloudRunnerSystem.Run(`rm -r ${CloudRunnerFolders.repoPathAbsolute}`); + CloudRunnerLogger.log(`${CloudRunnerFolders.repoPathAbsolute} repo exists, but no git folder, cleaning up`); + } if ( - CloudRunner.buildParameters.retainWorkspace && + BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters) && fs.existsSync(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`)) ) { process.chdir(CloudRunnerFolders.repoPathAbsolute); RemoteClientLogger.log( - `${CloudRunnerFolders.repoPathAbsolute} repo exists - skipping clone - retained workspace mode ${CloudRunner.buildParameters.retainWorkspace}`, + `${ + CloudRunnerFolders.repoPathAbsolute + } repo exists - skipping clone - retained workspace mode ${BuildParameters.shouldUseRetainedWorkspaceMode( + CloudRunner.buildParameters, + )}`, ); await CloudRunnerSystem.Run(`git fetch && git reset --hard ${CloudRunner.buildParameters.gitSha}`); return; } - if (fs.existsSync(CloudRunnerFolders.repoPathAbsolute)) { - RemoteClientLogger.log(`${CloudRunnerFolders.repoPathAbsolute} repo exists cleaning up`); - await CloudRunnerSystem.Run(`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}`); - } - + RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`); + await CloudRunnerSystem.Run(`git config --global advice.detachedHead false`); + RemoteClientLogger.log(`Cloning the repository being built:`); + await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`); + await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`); try { - RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`); - await CloudRunnerSystem.Run(`git config --global advice.detachedHead false`); - RemoteClientLogger.log(`Cloning the repository being built:`); - await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`); - await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`); await CloudRunnerSystem.Run( - `git clone -q ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(CloudRunnerFolders.repoPathAbsolute)}`, + `git clone ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(CloudRunnerFolders.repoPathAbsolute)}`, ); - process.chdir(CloudRunnerFolders.repoPathAbsolute); - await CloudRunnerSystem.Run(`git lfs install`); - assert(fs.existsSync(`.git`), 'git folder exists'); - RemoteClientLogger.log(`${CloudRunner.buildParameters.branch}`); - await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.branch}`); - await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`); - assert(fs.existsSync(path.join(`.git`, `lfs`)), 'LFS folder should not exist before caching'); - RemoteClientLogger.log(`Checked out ${CloudRunner.buildParameters.branch}`); - } catch (error) { - await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); + } catch (error: any) { throw error; } + process.chdir(CloudRunnerFolders.repoPathAbsolute); + await CloudRunnerSystem.Run(`git lfs install`); + assert(fs.existsSync(`.git`), 'git folder exists'); + RemoteClientLogger.log(`${CloudRunner.buildParameters.branch}`); + if (CloudRunner.buildParameters.gitSha !== undefined) { + await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`); + } else { + await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.branch}`); + RemoteClientLogger.log(`buildParameter Git Sha is empty`); + } + + assert(fs.existsSync(path.join(`.git`, `lfs`)), 'LFS folder should not exist before caching'); + RemoteClientLogger.log(`Checked out ${CloudRunner.buildParameters.branch}`); } - static replaceLargePackageReferencesWithSharedReferences() { - if (CloudRunner.buildParameters.useSharedLargePackages) { + static async replaceLargePackageReferencesWithSharedReferences() { + CloudRunnerLogger.log(`Use Shared Pkgs ${CloudRunner.buildParameters.useLargePackages}`); + GitHub.updateGitHubCheck(`Use Shared Pkgs ${CloudRunner.buildParameters.useLargePackages}`, ``); + if (CloudRunner.buildParameters.useLargePackages) { const filePath = path.join(CloudRunnerFolders.projectPathAbsolute, `Packages/manifest.json`); let manifest = fs.readFileSync(filePath, 'utf8'); manifest = manifest.replace(/LargeContent/g, '../../../LargeContent'); fs.writeFileSync(filePath, manifest); - if (CloudRunner.buildParameters.cloudRunnerDebug) { - CloudRunnerLogger.log(`Package Manifest`); - CloudRunnerLogger.log(manifest); - } + CloudRunnerLogger.log(`Package Manifest \n ${manifest}`); + GitHub.updateGitHubCheck(`Package Manifest \n ${manifest}`, ``); } } @@ -121,41 +155,31 @@ export class RemoteClient { process.chdir(CloudRunnerFolders.repoPathAbsolute); await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge -- %f"`); await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process"`); - await CloudRunnerSystem.Run(`git lfs pull`); - RemoteClientLogger.log(`pulled latest LFS files`); - assert(fs.existsSync(CloudRunnerFolders.lfsFolderAbsolute)); - } - - @CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`) - static async runRemoteClientJob() { - // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); - RemoteClient.handleRetainedWorkspace(); - - // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); - await RemoteClient.bootstrapRepository(); - - // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); - await RemoteClient.runCustomHookFiles(`before-build`); - - // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); - } - static async runCustomHookFiles(hookLifecycle: string) { - RemoteClientLogger.log(`RunCustomHookFiles: ${hookLifecycle}`); - const gameCiCustomHooksPath = path.join(CloudRunnerFolders.repoPathAbsolute, `game-ci`, `hooks`); - const files = fs.readdirSync(gameCiCustomHooksPath); - for (const file of files) { - const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`); - const fileContentsObject = YAML.parse(fileContents.toString()); - if (fileContentsObject.hook === hookLifecycle) { - RemoteClientLogger.log(`Active Hook File ${file} \n \n file contents: \n ${fileContents}`); - await CloudRunnerSystem.Run(fileContentsObject.commands); - } + if (!CloudRunner.buildParameters.skipLfs) { + await CloudRunnerSystem.Run(`git lfs pull`); + RemoteClientLogger.log(`pulled latest LFS files`); + assert(fs.existsSync(CloudRunnerFolders.lfsFolderAbsolute)); } } - static handleRetainedWorkspace() { - if (!CloudRunner.buildParameters.retainWorkspace) { - return; + static async handleRetainedWorkspace() { + RemoteClientLogger.log( + `Retained Workspace: ${BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters)}`, + ); + if ( + BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters) && + fs.existsSync(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)) && + fs.existsSync(CloudRunnerFolders.ToLinuxFolder(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`))) + ) { + CloudRunnerLogger.log(`Retained Workspace Already Exists!`); + process.chdir(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)); + await CloudRunnerSystem.Run(`git fetch`); + await CloudRunnerSystem.Run(`git lfs pull`); + await CloudRunnerSystem.Run(`git reset --hard "${CloudRunner.buildParameters.gitSha}"`); + await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`); + + return true; } - RemoteClientLogger.log(`Retained Workspace: ${CloudRunner.lockedWorkspace}`); + + return false; } } diff --git a/src/model/cloud-runner/remote-client/remote-client-logger.ts b/src/model/cloud-runner/remote-client/remote-client-logger.ts index 122969d0..2bac8ef3 100644 --- a/src/model/cloud-runner/remote-client/remote-client-logger.ts +++ b/src/model/cloud-runner/remote-client/remote-client-logger.ts @@ -1,4 +1,4 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; export class RemoteClientLogger { public static log(message: string) { diff --git a/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts b/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts deleted file mode 100644 index bdc291cb..00000000 --- a/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { BuildParameters, Input } from '../..'; -import YAML from 'yaml'; -import CloudRunnerSecret from './cloud-runner-secret'; -import { RemoteClientLogger } from '../remote-client/remote-client-logger'; -import path from 'node:path'; -import CloudRunnerOptions from '../cloud-runner-options'; -import fs from 'node:fs'; - -// import CloudRunnerLogger from './cloud-runner-logger'; - -export class CloudRunnerCustomHooks { - // TODO also accept hooks as yaml files in the repo - public static ApplyHooksToCommands(commands: string, buildParameters: BuildParameters): string { - const hooks = CloudRunnerCustomHooks.getHooks(buildParameters.customJobHooks).filter((x) => x.step.includes(`all`)); - - return `echo "---" - echo "start cloud runner init" - ${CloudRunnerOptions.cloudRunnerDebugEnv ? `printenv` : `#`} - echo "start of cloud runner job" - ${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} - ${commands} - ${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} - echo "end of cloud runner job" - echo "---${buildParameters.logId}"`; - } - - public static getHooks(customJobHooks: string): Hook[] { - const experimentHooks = customJobHooks; - let output = new Array(); - if (experimentHooks && experimentHooks !== '') { - output = YAML.parse(experimentHooks); - } - - return output.filter((x) => x.step !== undefined && x.hook !== undefined && x.hook.length > 0); - } - - static GetCustomHooksFromFiles(hookLifecycle: string): Hook[] { - const results: Hook[] = []; - RemoteClientLogger.log(`GetCustomStepFiles: ${hookLifecycle}`); - try { - const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `hooks`); - const files = fs.readdirSync(gameCiCustomStepsPath); - for (const file of files) { - if (!CloudRunnerOptions.customHookFiles.includes(file.replace(`.yaml`, ``))) { - continue; - } - const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`); - const fileContentsObject = CloudRunnerCustomHooks.ParseHooks(fileContents)[0]; - if (fileContentsObject.hook.includes(hookLifecycle)) { - results.push(fileContentsObject); - } - } - } catch (error) { - RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`); - } - RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`); - - return results; - } - - private static ConvertYamlSecrets(object: Hook) { - if (object.secrets === undefined) { - object.secrets = []; - - return; - } - object.secrets = object.secrets.map((x) => { - return { - ParameterKey: x.ParameterKey, - EnvironmentVariable: Input.ToEnvVarFormat(x.ParameterKey), - ParameterValue: x.ParameterValue, - }; - }); - } - - public static ParseHooks(steps: string): Hook[] { - if (steps === '') { - return []; - } - - // if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) { - - // CloudRunnerLogger.log(`Parsing build hooks: ${steps}`); - - // } - const isArray = steps.replace(/\s/g, ``)[0] === `-`; - const object: Hook[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)]; - for (const hook of object) { - CloudRunnerCustomHooks.ConvertYamlSecrets(hook); - if (hook.secrets === undefined) { - hook.secrets = []; - } - } - if (object === undefined) { - throw new Error(`Failed to parse ${steps}`); - } - - return object; - } - - public static getSecrets(hooks: Hook[]) { - const secrets = hooks.map((x) => x.secrets).filter((x) => x !== undefined && x.length > 0); - - // eslint-disable-next-line unicorn/no-array-reduce - return secrets.length > 0 ? secrets.reduce((x, y) => [...x, ...y]) : []; - } -} -export class Hook { - public commands!: string[]; - public secrets: CloudRunnerSecret[] = new Array(); - public name!: string; - public hook!: string[]; - public step!: string[]; -} diff --git a/src/model/cloud-runner/services/cloud-runner-logger.ts b/src/model/cloud-runner/services/core/cloud-runner-logger.ts similarity index 100% rename from src/model/cloud-runner/services/cloud-runner-logger.ts rename to src/model/cloud-runner/services/core/cloud-runner-logger.ts diff --git a/src/model/cloud-runner/services/cloud-runner-system.ts b/src/model/cloud-runner/services/core/cloud-runner-system.ts similarity index 95% rename from src/model/cloud-runner/services/cloud-runner-system.ts rename to src/model/cloud-runner/services/core/cloud-runner-system.ts index 02798a73..0f80e8ad 100644 --- a/src/model/cloud-runner/services/cloud-runner-system.ts +++ b/src/model/cloud-runner/services/core/cloud-runner-system.ts @@ -1,5 +1,5 @@ import { exec } from 'child_process'; -import { RemoteClientLogger } from '../remote-client/remote-client-logger'; +import { RemoteClientLogger } from '../../remote-client/remote-client-logger'; export class CloudRunnerSystem { public static async RunAndReadLines(command: string): Promise { diff --git a/src/model/cloud-runner/services/core/follow-log-stream-service.ts b/src/model/cloud-runner/services/core/follow-log-stream-service.ts new file mode 100644 index 00000000..53d73d12 --- /dev/null +++ b/src/model/cloud-runner/services/core/follow-log-stream-service.ts @@ -0,0 +1,57 @@ +import GitHub from '../../../github'; +import CloudRunner from '../../cloud-runner'; +import { CloudRunnerStatics } from '../../options/cloud-runner-statics'; +import CloudRunnerLogger from './cloud-runner-logger'; +import * as core from '@actions/core'; + +export class FollowLogStreamService { + static Reset() { + FollowLogStreamService.DidReceiveEndOfTransmission = false; + } + static errors = ``; + public static DidReceiveEndOfTransmission = false; + public static handleIteration(message: string, shouldReadLogs: boolean, shouldCleanup: boolean, output: string) { + if (message.includes(`---${CloudRunner.buildParameters.logId}`)) { + CloudRunnerLogger.log('End of log transmission received'); + FollowLogStreamService.DidReceiveEndOfTransmission = true; + 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\n${FollowLogStreamService.errors}`, + `Build failed`, + `failure`, + `completed`, + ); + core.setOutput('build-result', 'failed'); + core.setFailed('unity build failed'); + core.error('BUILD FAILED!'); + } else if (message.toLowerCase().includes('error ')) { + core.error(message); + FollowLogStreamService.errors += `\n${message}`; + } else if (message.toLowerCase().includes('error: ')) { + core.error(message); + FollowLogStreamService.errors += `\n${message}`; + } else if (message.toLowerCase().includes('command failed: ')) { + FollowLogStreamService.errors += `\n${message}`; + } else if (message.toLowerCase().includes('invalid ')) { + FollowLogStreamService.errors += `\n${message}`; + } else if (message.toLowerCase().includes('incompatible ')) { + FollowLogStreamService.errors += `\n${message}`; + } else if (message.toLowerCase().includes('cannot be found')) { + FollowLogStreamService.errors += `\n${message}`; + } + if (CloudRunner.buildParameters.cloudRunnerDebug) { + output += `${message}\n`; + } + CloudRunnerLogger.log(`[${CloudRunnerStatics.logPrefix}] ${message}`); + + return { shouldReadLogs, shouldCleanup, output }; + } +} diff --git a/src/model/cloud-runner/services/shared-workspace-locking.ts b/src/model/cloud-runner/services/core/shared-workspace-locking.ts similarity index 56% rename from src/model/cloud-runner/services/shared-workspace-locking.ts rename to src/model/cloud-runner/services/core/shared-workspace-locking.ts index 0866c2ba..b4bd38c2 100644 --- a/src/model/cloud-runner/services/shared-workspace-locking.ts +++ b/src/model/cloud-runner/services/core/shared-workspace-locking.ts @@ -1,18 +1,17 @@ import { CloudRunnerSystem } from './cloud-runner-system'; import fs from 'node:fs'; import CloudRunnerLogger from './cloud-runner-logger'; -import CloudRunnerOptions from '../cloud-runner-options'; -import BuildParameters from '../../build-parameters'; -import CloudRunner from '../cloud-runner'; +import BuildParameters from '../../../build-parameters'; +import CloudRunner from '../../cloud-runner'; export class SharedWorkspaceLocking { - private static get workspaceBucketRoot() { - return `s3://${CloudRunner.buildParameters.awsBaseStackName}/`; + public static get workspaceBucketRoot() { + return `s3://${CloudRunner.buildParameters.awsStackName}/`; } - private static get workspaceRoot() { + public static get workspaceRoot() { return `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`; } public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise { - if (!(await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext))) { + if (!(await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(buildParametersContext))) { return []; } @@ -20,79 +19,95 @@ export class SharedWorkspaceLocking { await SharedWorkspaceLocking.ReadLines( `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, ) - ).map((x) => x.replace(`/`, ``)); - } - public static async DoesWorkspaceTopLevelExist(buildParametersContext: BuildParameters) { - await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`); - - return (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`)) + ) .map((x) => x.replace(`/`, ``)) - .includes(buildParametersContext.cacheKey); + .filter((x) => x.endsWith(`_workspace`)) + .map((x) => x.split(`_`)[1]); } - public static async GetAllLocks(workspace: string, buildParametersContext: BuildParameters): Promise { + public static async DoesCacheKeyTopLevelExist(buildParametersContext: BuildParameters) { + try { + const rootLines = await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`, + ); + const lockFolderExists = rootLines.map((x) => x.replace(`/`, ``)).includes(`locks`); + + if (lockFolderExists) { + const lines = await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`); + + return lines.map((x) => x.replace(`/`, ``)).includes(buildParametersContext.cacheKey); + } else { + return false; + } + } catch { + return false; + } + } + + public static NewWorkspaceName() { + return `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`; + } + public static async GetAllLocksForWorkspace( + workspace: string, + buildParametersContext: BuildParameters, + ): Promise { if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { return []; } return ( await SharedWorkspaceLocking.ReadLines( - `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`, + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, ) ) .map((x) => x.replace(`/`, ``)) - .filter((x) => x.includes(`_lock`)); + .filter((x) => x.includes(workspace) && x.endsWith(`_lock`)); } - public static async GetOrCreateLockedWorkspace( - workspace: string, - runId: string, - buildParametersContext: BuildParameters, - ) { - if (!CloudRunnerOptions.retainWorkspaces) { - return; + public static async GetLockedWorkspace(workspace: string, runId: string, buildParametersContext: BuildParameters) { + if (buildParametersContext.maxRetainedWorkspaces === 0) { + return false; } - try { - if (await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext)) { - const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext); + if (await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(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) { + const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext); CloudRunnerLogger.log( - `run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`, + `run agent: ${runId} try lock workspace: ${element} locking attempt result: ${lockResult}`, ); - 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; - - return true; - } + if (lockResult) { + return true; } } - } catch { - return; } - const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext, runId); + if (await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext)) { + workspace = SharedWorkspaceLocking.NewWorkspaceName(); + CloudRunner.lockedWorkspace = workspace; + } + + const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext); + const lockResult = await SharedWorkspaceLocking.LockWorkspace(workspace, runId, buildParametersContext); CloudRunnerLogger.log( - `run agent ${runId} didn't find a free workspace so created: ${workspace} createWorkspaceSuccess: ${createResult}`, + `run agent ${runId} didn't find a free workspace so created: ${workspace} createWorkspaceSuccess: ${createResult} Lock:${lockResult}`, ); - return createResult; + return createResult && lockResult; } public static async DoesWorkspaceExist(workspace: string, buildParametersContext: BuildParameters) { - return (await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext)).includes(workspace); + return ( + (await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext)).filter((x) => x.includes(workspace)) + .length > 0 + ); } public static async HasWorkspaceLock( workspace: string, runId: string, buildParametersContext: BuildParameters, ): Promise { - if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { - return false; - } - const locks = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext)) + const locks = (await SharedWorkspaceLocking.GetAllLocksForWorkspace(workspace, buildParametersContext)) .map((x) => { return { name: x, @@ -115,14 +130,11 @@ export class SharedWorkspaceLocking { const result: string[] = []; const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext); for (const element of workspaces) { - await new Promise((promise) => setTimeout(promise, 1500)); const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(element, buildParametersContext); const isBelowMax = await SharedWorkspaceLocking.IsWorkspaceBelowMax(element, buildParametersContext); + CloudRunnerLogger.log(`workspace ${element} locked:${isLocked} below max:${isBelowMax}`); if (!isLocked && isBelowMax) { result.push(element); - CloudRunnerLogger.log(`workspace ${element} is free`); - } else { - CloudRunnerLogger.log(`workspace ${element} is NOT free ${!isLocked} ${isBelowMax}`); } } @@ -171,60 +183,49 @@ export class SharedWorkspaceLocking { return ( await SharedWorkspaceLocking.ReadLines( - `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`, + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, ) ) .map((x) => x.replace(`/`, ``)) - .filter((x) => x.includes(`_workspace`)) + .filter((x) => x.includes(workspace) && x.endsWith(`_workspace`)) .map((x) => Number(x))[0]; } public static async IsWorkspaceLocked(workspace: string, buildParametersContext: BuildParameters): Promise { if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { - return false; + throw new Error(`workspace doesn't exist ${workspace}`); } const files = await SharedWorkspaceLocking.ReadLines( - `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`, + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, ); - const workspaceFileDoesNotExists = - files.filter((x) => { - return x.includes(`_workspace`); - }).length === 0; - const lockFilesExist = files.filter((x) => { - return x.includes(`_lock`); + return x.includes(workspace) && x.endsWith(`_lock`); }).length > 0; - return workspaceFileDoesNotExists || lockFilesExist; + return lockFilesExist; } - public static async CreateWorkspace( - workspace: string, - buildParametersContext: BuildParameters, - lockId: string = ``, - ): Promise { - if (lockId !== ``) { - await SharedWorkspaceLocking.LockWorkspace(workspace, lockId, buildParametersContext); + public static async CreateWorkspace(workspace: string, buildParametersContext: BuildParameters): Promise { + if (await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext)) { + throw new Error(`${workspace} already exists`); } const timestamp = Date.now(); - const file = `${timestamp}_workspace`; + const file = `${timestamp}_${workspace}_workspace`; fs.writeFileSync(file, ''); await CloudRunnerSystem.Run( - `aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + `aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`, false, true, ); fs.rmSync(file); - const workspaces = await SharedWorkspaceLocking.ReadLines( - `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, - ); + const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext); CloudRunnerLogger.log(`All workspaces ${workspaces}`); if (!(await SharedWorkspaceLocking.IsWorkspaceBelowMax(workspace, buildParametersContext))) { - CloudRunnerLogger.log(`Workspace is below max ${workspaces} ${buildParametersContext.maxRetainedWorkspaces}`); + CloudRunnerLogger.log(`Workspace is above max ${workspaces} ${buildParametersContext.maxRetainedWorkspaces}`); await SharedWorkspaceLocking.CleanupWorkspace(workspace, buildParametersContext); return false; @@ -238,16 +239,30 @@ export class SharedWorkspaceLocking { runId: string, buildParametersContext: BuildParameters, ): Promise { - const file = `${Date.now()}_${runId}_lock`; + const existingWorkspace = workspace.endsWith(`_workspace`); + const ending = existingWorkspace ? workspace : `${workspace}_workspace`; + const file = `${Date.now()}_${runId}_${ending}_lock`; fs.writeFileSync(file, ''); await CloudRunnerSystem.Run( - `aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + `aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`, false, true, ); fs.rmSync(file); - return SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext); + const hasLock = await SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext); + + if (hasLock) { + CloudRunner.lockedWorkspace = workspace; + } else { + await CloudRunnerSystem.Run( + `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`, + false, + true, + ); + } + + return hasLock; } public static async ReleaseWorkspace( @@ -255,31 +270,29 @@ export class SharedWorkspaceLocking { runId: string, buildParametersContext: BuildParameters, ): Promise { - const file = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext)).filter((x) => - x.includes(`_${runId}_lock`), - ); + const files = await SharedWorkspaceLocking.GetAllLocksForWorkspace(workspace, buildParametersContext); + const file = files.find((x) => x.includes(workspace) && x.endsWith(`_lock`) && x.includes(runId)); + CloudRunnerLogger.log(`All Locks ${files} ${workspace} ${runId}`); CloudRunnerLogger.log(`Deleting lock ${workspace}/${file}`); - CloudRunnerLogger.log( - `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, - ); + CloudRunnerLogger.log(`rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`); await CloudRunnerSystem.Run( - `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`, false, true, ); - return !SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext); + return !(await SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext)); } public static async CleanupWorkspace(workspace: string, buildParametersContext: BuildParameters) { await CloudRunnerSystem.Run( - `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace} --recursive`, + `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey} --exclude "*" --include "*_${workspace}_*"`, false, true, ); } - private static async ReadLines(command: string): Promise { + public static async ReadLines(command: string): Promise { return CloudRunnerSystem.RunAndReadLines(command); } } diff --git a/src/model/cloud-runner/services/task-parameter-serializer.ts b/src/model/cloud-runner/services/core/task-parameter-serializer.ts similarity index 57% rename from src/model/cloud-runner/services/task-parameter-serializer.ts rename to src/model/cloud-runner/services/core/task-parameter-serializer.ts index d74ae64b..a8ba1722 100644 --- a/src/model/cloud-runner/services/task-parameter-serializer.ts +++ b/src/model/cloud-runner/services/core/task-parameter-serializer.ts @@ -1,53 +1,50 @@ -import { Input } from '../..'; -import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable'; -import { CloudRunnerCustomHooks } from './cloud-runner-custom-hooks'; -import CloudRunnerSecret from './cloud-runner-secret'; -import CloudRunnerQueryOverride from './cloud-runner-query-override'; -import CloudRunnerOptionsReader from './cloud-runner-options-reader'; -import BuildParameters from '../../build-parameters'; -import CloudRunnerOptions from '../cloud-runner-options'; -import * as core from '@actions/core'; +import BuildParameters from '../../../build-parameters'; +import Input from '../../../input'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; +import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; +import CloudRunnerOptionsReader from '../../options/cloud-runner-options-reader'; +import CloudRunnerQueryOverride from '../../options/cloud-runner-query-override'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; +import { CommandHookService } from '../hooks/command-hook-service'; export class TaskParameterSerializer { - static readonly blocked = new Set(['0', 'length', 'prototype', '', 'unityVersion']); + static readonly blockedParameterNames: Set = new Set([ + '0', + 'length', + 'prototype', + '', + 'unityVersion', + 'CACHE_UNITY_INSTALLATION_ON_MAC', + 'RUNNER_TEMP_PATH', + 'NAME', + 'CUSTOM_JOB', + ]); public static createCloudRunnerEnvironmentVariables( buildParameters: BuildParameters, ): CloudRunnerEnvironmentVariable[] { - const result = this.uniqBy( + const result: CloudRunnerEnvironmentVariable[] = this.uniqBy( [ - { - name: 'ContainerMemory', - value: buildParameters.cloudRunnerMemory, - }, - { - name: 'ContainerCpu', - value: buildParameters.cloudRunnerCpu, - }, - { - name: 'BUILD_TARGET', - value: buildParameters.targetPlatform, - }, + ...[ + { name: 'BUILD_TARGET', value: buildParameters.targetPlatform }, + { name: 'UNITY_VERSION', value: buildParameters.editorVersion }, + { name: 'GITHUB_TOKEN', value: process.env.GITHUB_TOKEN }, + ], ...TaskParameterSerializer.serializeFromObject(buildParameters), - ...TaskParameterSerializer.readInput(), - ...CloudRunnerCustomHooks.getSecrets(CloudRunnerCustomHooks.getHooks(buildParameters.customJobHooks)), + ...TaskParameterSerializer.serializeInput(), + ...TaskParameterSerializer.serializeCloudRunnerOptions(), + ...CommandHookService.getSecrets(CommandHookService.getHooks(buildParameters.commandHooks)), ] .filter( (x) => - !TaskParameterSerializer.blocked.has(x.name) && + !TaskParameterSerializer.blockedParameterNames.has(x.name) && x.value !== '' && x.value !== undefined && - x.name !== `CUSTOM_JOB` && - x.name !== `GAMECI_CUSTOM_JOB` && x.value !== `undefined`, ) .map((x) => { - x.name = TaskParameterSerializer.ToEnvVarFormat(x.name); + x.name = `${TaskParameterSerializer.ToEnvVarFormat(x.name)}`; x.value = `${x.value}`; - if (buildParameters.cloudRunnerDebug && Number.isNaN(Number(x.name))) { - core.info(`[ERROR] found a number in task param serializer ${JSON.stringify(x)}`); - } - return x; }), (item: CloudRunnerEnvironmentVariable) => item.name, @@ -72,54 +69,58 @@ export class TaskParameterSerializer { const keys = [ ...new Set( Object.getOwnPropertyNames(process.env) - .filter((x) => !this.blocked.has(x) && x.startsWith('GAMECI_')) + .filter((x) => !this.blockedParameterNames.has(x) && x.startsWith('')) .map((x) => TaskParameterSerializer.UndoEnvVarFormat(x)), ), ]; for (const element of keys) { if (element !== `customJob`) { - buildParameters[element] = process.env[`GAMECI_${TaskParameterSerializer.ToEnvVarFormat(element)}`]; + buildParameters[element] = process.env[`${TaskParameterSerializer.ToEnvVarFormat(element)}`]; } } return buildParameters; } - private static readInput() { + private static serializeInput() { return TaskParameterSerializer.serializeFromType(Input); } + private static serializeCloudRunnerOptions() { + return TaskParameterSerializer.serializeFromType(CloudRunnerOptions); + } + public static ToEnvVarFormat(input: string): string { return CloudRunnerOptions.ToEnvVarFormat(input); } public static UndoEnvVarFormat(element: string): string { - return this.camelize(element.replace('GAMECI_', '').toLowerCase().replace(/_+/g, ' ')); + return this.camelize(element.toLowerCase().replace(/_+/g, ' ')); } private static camelize(string: string) { - return string - .replace(/(^\w)|([A-Z])|(\b\w)/g, function (word: string, index: number) { - return index === 0 ? word.toLowerCase() : word.toUpperCase(); - }) - .replace(/\s+/g, ''); + return TaskParameterSerializer.uncapitalizeFirstLetter( + string + .replace(/(^\w)|([A-Z])|(\b\w)/g, function (word: string, index: number) { + return index === 0 ? word.toLowerCase() : word.toUpperCase(); + }) + .replace(/\s+/g, ''), + ); + } + + private static uncapitalizeFirstLetter(string: string) { + return string.charAt(0).toLowerCase() + string.slice(1); } private static serializeFromObject(buildParameters: any) { const array: any[] = []; - const keys = Object.getOwnPropertyNames(buildParameters).filter((x) => !this.blocked.has(x)); + const keys = Object.getOwnPropertyNames(buildParameters).filter((x) => !this.blockedParameterNames.has(x)); for (const element of keys) { - array.push( - { - name: `GAMECI_${TaskParameterSerializer.ToEnvVarFormat(element)}`, - value: buildParameters[element], - }, - { - name: element, - value: buildParameters[element], - }, - ); + array.push({ + name: TaskParameterSerializer.ToEnvVarFormat(element), + value: buildParameters[element], + }); } return array; diff --git a/src/model/cloud-runner/services/follow-log-stream-service.ts b/src/model/cloud-runner/services/follow-log-stream-service.ts deleted file mode 100644 index e824fd64..00000000 --- a/src/model/cloud-runner/services/follow-log-stream-service.ts +++ /dev/null @@ -1,37 +0,0 @@ -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: string, shouldReadLogs: boolean, shouldCleanup: boolean, output: string) { - if (message.includes(`---${CloudRunner.buildParameters.logId}`)) { - 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!'); - } else if (CloudRunner.buildParameters.cloudRunnerDebug && message.includes(': Listening for Jobs')) { - core.setOutput('cloud runner stop watching', 'true'); - shouldReadLogs = false; - shouldCleanup = false; - core.warning('cloud runner stop watching'); - } - if (CloudRunner.buildParameters.cloudRunnerDebug) { - output += `${message}\n`; - } - CloudRunnerLogger.log(`[${CloudRunnerStatics.logPrefix}] ${message}`); - - return { shouldReadLogs, shouldCleanup, output }; - } -} diff --git a/src/model/cloud-runner/services/hooks/command-hook-service.ts b/src/model/cloud-runner/services/hooks/command-hook-service.ts new file mode 100644 index 00000000..c5ddc518 --- /dev/null +++ b/src/model/cloud-runner/services/hooks/command-hook-service.ts @@ -0,0 +1,118 @@ +import { BuildParameters, Input } from '../../..'; +import YAML from 'yaml'; +import { RemoteClientLogger } from '../../remote-client/remote-client-logger'; +import path from 'node:path'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; +import * as fs from 'node:fs'; +import CloudRunnerLogger from '../core/cloud-runner-logger'; +import { CommandHook } from './command-hook'; + +// import CloudRunnerLogger from './cloud-runner-logger'; + +export class CommandHookService { + public static ApplyHooksToCommands(commands: string, buildParameters: BuildParameters): string { + const hooks = CommandHookService.getHooks(buildParameters.commandHooks); + CloudRunnerLogger.log(`Applying hooks ${hooks.length}`); + + return `echo "---" +echo "start cloud runner init" +${CloudRunnerOptions.cloudRunnerDebug ? `printenv` : `#`} +echo "start of cloud runner job" +${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} +${commands} +${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} +echo "end of cloud runner job" +echo "---${buildParameters.logId}"`; + } + + public static getHooks(customCommandHooks: string): CommandHook[] { + const experimentHooks = customCommandHooks; + let output = new Array(); + if (experimentHooks && experimentHooks !== '') { + try { + output = YAML.parse(experimentHooks); + } catch (error) { + throw error; + } + } + + return [ + ...output.filter((x) => x.hook !== undefined && x.hook.length > 0), + ...CommandHookService.GetCustomHooksFromFiles(`before`), + ...CommandHookService.GetCustomHooksFromFiles(`after`), + ]; + } + + static GetCustomHooksFromFiles(hookLifecycle: string): CommandHook[] { + const results: CommandHook[] = []; + + // RemoteClientLogger.log(`GetCustomHookFiles: ${hookLifecycle}`); + try { + const gameCiCustomHooksPath = path.join(process.cwd(), `game-ci`, `command-hooks`); + const files = fs.readdirSync(gameCiCustomHooksPath); + for (const file of files) { + if (!CloudRunnerOptions.commandHookFiles.includes(file.replace(`.yaml`, ``))) { + continue; + } + const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`); + const fileContentsObject = CommandHookService.ParseHooks(fileContents)[0]; + if (fileContentsObject.hook.includes(hookLifecycle)) { + results.push(fileContentsObject); + } + } + } catch (error) { + RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`); + } + + // RemoteClientLogger.log(`Active Steps From Hooks: \n ${JSON.stringify(results, undefined, 4)}`); + + return results; + } + + private static ConvertYamlSecrets(object: CommandHook) { + if (object.secrets === undefined) { + object.secrets = []; + + return; + } + object.secrets = object.secrets.map((x: any) => { + return { + ParameterKey: x.name, + EnvironmentVariable: Input.ToEnvVarFormat(x.name), + ParameterValue: x.value, + }; + }); + } + + public static ParseHooks(hooks: string): CommandHook[] { + if (hooks === '') { + return []; + } + + // if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) { + + // CloudRunnerLogger.log(`Parsing build hooks: ${steps}`); + + // } + const isArray = hooks.replace(/\s/g, ``)[0] === `-`; + const object: CommandHook[] = isArray ? YAML.parse(hooks) : [YAML.parse(hooks)]; + for (const hook of object) { + CommandHookService.ConvertYamlSecrets(hook); + if (hook.secrets === undefined) { + hook.secrets = []; + } + } + if (object === undefined) { + throw new Error(`Failed to parse ${hooks}`); + } + + return object; + } + + public static getSecrets(hooks: any) { + const secrets = hooks.map((x: any) => x.secrets).filter((x: any) => x !== undefined && x.length > 0); + + // eslint-disable-next-line unicorn/no-array-reduce + return secrets.length > 0 ? secrets.reduce((x: any, y: any) => [...x, ...y]) : []; + } +} diff --git a/src/model/cloud-runner/services/hooks/command-hook.ts b/src/model/cloud-runner/services/hooks/command-hook.ts new file mode 100644 index 00000000..73e54d75 --- /dev/null +++ b/src/model/cloud-runner/services/hooks/command-hook.ts @@ -0,0 +1,9 @@ +import CloudRunnerSecret from '../../options/cloud-runner-secret'; + +export class CommandHook { + public commands: string[] = new Array(); + public secrets: CloudRunnerSecret[] = new Array(); + public name!: string; + public hook!: string[]; + public step!: string[]; +} diff --git a/src/model/cloud-runner/services/cloud-runner-custom-steps.ts b/src/model/cloud-runner/services/hooks/container-hook-service.ts similarity index 64% rename from src/model/cloud-runner/services/cloud-runner-custom-steps.ts rename to src/model/cloud-runner/services/hooks/container-hook-service.ts index 0a05e45d..343429c8 100644 --- a/src/model/cloud-runner/services/cloud-runner-custom-steps.ts +++ b/src/model/cloud-runner/services/hooks/container-hook-service.ts @@ -1,32 +1,28 @@ import YAML from 'yaml'; -import CloudRunner from '../cloud-runner'; +import CloudRunner from '../../cloud-runner'; import * as core from '@actions/core'; -import { CustomWorkflow } from '../workflows/custom-workflow'; -import { RemoteClientLogger } from '../remote-client/remote-client-logger'; +import { CustomWorkflow } from '../../workflows/custom-workflow'; +import { RemoteClientLogger } from '../../remote-client/remote-client-logger'; import path from 'node:path'; import fs from 'node:fs'; -import Input from '../../input'; -import CloudRunnerOptions from '../cloud-runner-options'; -import CloudRunnerLogger from './cloud-runner-logger'; -import { CustomStep } from './custom-step'; -import { CloudRunnerStepState } from '../cloud-runner-step-state'; +import Input from '../../../input'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; +import { ContainerHook as ContainerHook } from './container-hook'; +import { CloudRunnerStepParameters } from '../../options/cloud-runner-step-parameters'; -export class CloudRunnerCustomSteps { - static GetCustomStepsFromFiles(hookLifecycle: string): CustomStep[] { - const results: CustomStep[] = []; - RemoteClientLogger.log( - `GetCustomStepFiles: ${hookLifecycle} CustomStepFiles: ${CloudRunnerOptions.customStepFiles}`, - ); +export class ContainerHookService { + static GetContainerHooksFromFiles(hookLifecycle: string): ContainerHook[] { + const results: ContainerHook[] = []; try { - const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `steps`); + const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `container-hooks`); const files = fs.readdirSync(gameCiCustomStepsPath); for (const file of files) { - if (!CloudRunnerOptions.customStepFiles.includes(file.replace(`.yaml`, ``))) { - RemoteClientLogger.log(`Skipping CustomStepFile: ${file}`); + if (!CloudRunnerOptions.containerHookFiles.includes(file.replace(`.yaml`, ``))) { + // RemoteClientLogger.log(`Skipping CustomStepFile: ${file}`); continue; } const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`); - const fileContentsObject = CloudRunnerCustomSteps.ParseSteps(fileContents)[0]; + const fileContentsObject = ContainerHookService.ParseContainerHooks(fileContents)[0]; if (fileContentsObject.hook === hookLifecycle) { results.push(fileContentsObject); } @@ -34,9 +30,10 @@ export class CloudRunnerCustomSteps { } catch (error) { RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`); } - RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`); - const builtInCustomSteps: CustomStep[] = CloudRunnerCustomSteps.ParseSteps( + // RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`); + + const builtInContainerHooks: ContainerHook[] = ContainerHookService.ParseContainerHooks( `- name: aws-s3-upload-build image: amazon/aws-cli hook: after @@ -45,12 +42,12 @@ export class CloudRunnerCustomSteps { 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-${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' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' + } s3://${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar${ + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' } rm /data/cache/$CACHE_KEY/build/build-${CloudRunner.buildParameters.buildGuid}.tar${ - CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' } secrets: - name: awsAccessKeyId @@ -65,19 +62,20 @@ 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 ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/ || true - aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/build || true + aws s3 ls ${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/ || true + aws s3 ls ${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/$CACHE_KEY/build || true + mkdir -p /data/cache/$CACHE_KEY/build/ aws s3 cp s3://${ - CloudRunner.buildParameters.awsBaseStackName + CloudRunner.buildParameters.awsStackName }/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${ - CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' } /data/cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${ - CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '' + CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : '' } secrets: - - name: awsAccessKeyId - - name: awsSecretAccessKey - - name: awsDefaultRegion + - name: AWS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + - name: AWS_DEFAULT_REGION - name: BUILD_GUID_TARGET - name: steam-deploy-client image: steamcmd/steamcmd @@ -123,19 +121,19 @@ export class CloudRunnerCustomSteps { 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://${ - CloudRunner.buildParameters.awsBaseStackName + CloudRunner.buildParameters.awsStackName }/cloud-runner-cache/$CACHE_KEY/lfs rm -r /data/cache/$CACHE_KEY/lfs aws s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://${ - CloudRunner.buildParameters.awsBaseStackName + CloudRunner.buildParameters.awsStackName }/cloud-runner-cache/$CACHE_KEY/Library rm -r /data/cache/$CACHE_KEY/Library secrets: - - name: awsAccessKeyId + - name: AWS_ACCESS_KEY_ID value: ${process.env.AWS_ACCESS_KEY_ID || ``} - - name: awsSecretAccessKey + - name: AWS_SECRET_ACCESS_KEY value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} - - name: awsDefaultRegion + - name: AWS_DEFAULT_REGION value: ${process.env.AWS_REGION || ``} - name: aws-s3-pull-cache image: amazon/aws-cli @@ -144,30 +142,32 @@ 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 ${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/" + mkdir -p /data/cache/$CACHE_KEY/Library/ + mkdir -p /data/cache/$CACHE_KEY/lfs/ + aws s3 ls ${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/ || true + aws s3 ls ${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/$CACHE_KEY/ || true + BUCKET1="${CloudRunner.buildParameters.awsStackName}/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="${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/lfs/" + BUCKET2="${CloudRunner.buildParameters.awsStackName}/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 secrets: - - name: awsAccessKeyId + - name: AWS_ACCESS_KEY_ID value: ${process.env.AWS_ACCESS_KEY_ID || ``} - - name: awsSecretAccessKey + - name: AWS_SECRET_ACCESS_KEY value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} - - name: awsDefaultRegion + - name: AWS_DEFAULT_REGION value: ${process.env.AWS_REGION || ``} - name: debug-cache image: ubuntu hook: after commands: | apt-get update > /dev/null - ${CloudRunnerOptions.cloudRunnerDebugTree ? `apt-get install -y tree > /dev/null` : `#`} - ${CloudRunnerOptions.cloudRunnerDebugTree ? `tree -L 3 /data/cache` : `#`} + ${CloudRunnerOptions.cloudRunnerDebug ? `apt-get install -y tree > /dev/null` : `#`} + ${CloudRunnerOptions.cloudRunnerDebug ? `tree -L 3 /data/cache` : `#`} secrets: - name: awsAccessKeyId value: ${process.env.AWS_ACCESS_KEY_ID || ``} @@ -175,15 +175,15 @@ export class CloudRunnerCustomSteps { value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} - name: awsDefaultRegion value: ${process.env.AWS_REGION || ``}`, - ).filter((x) => CloudRunnerOptions.customStepFiles.includes(x.name) && x.hook === hookLifecycle); - if (builtInCustomSteps.length > 0) { - results.push(...builtInCustomSteps); + ).filter((x) => CloudRunnerOptions.containerHookFiles.includes(x.name) && x.hook === hookLifecycle); + if (builtInContainerHooks.length > 0) { + results.push(...builtInContainerHooks); } return results; } - private static ConvertYamlSecrets(object: CustomStep) { + private static ConvertYamlSecrets(object: ContainerHook) { if (object.secrets === undefined) { object.secrets = []; @@ -198,21 +198,21 @@ export class CloudRunnerCustomSteps { }); } - public static ParseSteps(steps: string): CustomStep[] { + public static ParseContainerHooks(steps: string): ContainerHook[] { if (steps === '') { return []; } const isArray = steps.replace(/\s/g, ``)[0] === `-`; - const object: CustomStep[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)]; + const object: ContainerHook[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)]; for (const step of object) { - CloudRunnerCustomSteps.ConvertYamlSecrets(step); + ContainerHookService.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}`); + // CloudRunnerLogger.log(`Injecting custom step ${step.name} from env var ${secret.ParameterKey}`); } secret.ParameterValue = process.env[secret.ParameterKey] || ``; } @@ -229,16 +229,16 @@ export class CloudRunnerCustomSteps { return object; } - static async RunPostBuildSteps(cloudRunnerStepState: CloudRunnerStepState) { + static async RunPostBuildSteps(cloudRunnerStepState: CloudRunnerStepParameters) { let output = ``; - const steps: CustomStep[] = [ - ...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.postBuildSteps), - ...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`after`), + const steps: ContainerHook[] = [ + ...ContainerHookService.ParseContainerHooks(CloudRunner.buildParameters.postBuildContainerHooks), + ...ContainerHookService.GetContainerHooksFromFiles(`after`), ]; if (steps.length > 0) { if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps'); - output += await CustomWorkflow.runCustomJob( + output += await CustomWorkflow.runContainerJob( steps, cloudRunnerStepState.environment, cloudRunnerStepState.secrets, @@ -248,16 +248,16 @@ export class CloudRunnerCustomSteps { return output; } - static async RunPreBuildSteps(cloudRunnerStepState: CloudRunnerStepState) { + static async RunPreBuildSteps(cloudRunnerStepState: CloudRunnerStepParameters) { let output = ``; - const steps: CustomStep[] = [ - ...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.preBuildSteps), - ...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`before`), + const steps: ContainerHook[] = [ + ...ContainerHookService.ParseContainerHooks(CloudRunner.buildParameters.preBuildContainerHooks), + ...ContainerHookService.GetContainerHooksFromFiles(`before`), ]; if (steps.length > 0) { if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps'); - output += await CustomWorkflow.runCustomJob( + output += await CustomWorkflow.runContainerJob( steps, cloudRunnerStepState.environment, cloudRunnerStepState.secrets, diff --git a/src/model/cloud-runner/services/custom-step.ts b/src/model/cloud-runner/services/hooks/container-hook.ts similarity index 65% rename from src/model/cloud-runner/services/custom-step.ts rename to src/model/cloud-runner/services/hooks/container-hook.ts index a5c8f03a..5e50af38 100644 --- a/src/model/cloud-runner/services/custom-step.ts +++ b/src/model/cloud-runner/services/hooks/container-hook.ts @@ -1,6 +1,6 @@ -import CloudRunnerSecret from './cloud-runner-secret'; +import CloudRunnerSecret from '../../options/cloud-runner-secret'; -export class CustomStep { +export class ContainerHook { public commands!: string; public secrets: CloudRunnerSecret[] = new Array(); public name!: string; diff --git a/src/model/cloud-runner/services/lfs-hashing.ts b/src/model/cloud-runner/services/lfs-hashing.ts deleted file mode 100644 index 07f9d96d..00000000 --- a/src/model/cloud-runner/services/lfs-hashing.ts +++ /dev/null @@ -1,47 +0,0 @@ -import path from 'node:path'; -import { CloudRunnerFolders } from './cloud-runner-folders'; -import { CloudRunnerSystem } from './cloud-runner-system'; -import fs from 'node:fs'; -import { assert } from 'node:console'; -import { Cli } from '../../cli/cli'; -import { CliFunction } from '../../cli/cli-functions-repository'; - -export class LfsHashing { - public static async createLFSHashFiles() { - try { - await CloudRunnerSystem.Run(`git lfs ls-files -l | cut -d ' ' -f1 | sort > .lfs-assets-guid`); - await CloudRunnerSystem.Run(`md5sum .lfs-assets-guid > .lfs-assets-guid-sum`); - assert(fs.existsSync(`.lfs-assets-guid-sum`)); - assert(fs.existsSync(`.lfs-assets-guid`)); - const lfsHashes = { - lfsGuid: fs - .readFileSync(`${path.join(CloudRunnerFolders.repoPathAbsolute, `.lfs-assets-guid`)}`, 'utf8') - .replace(/\n/g, ``), - lfsGuidSum: fs - .readFileSync(`${path.join(CloudRunnerFolders.repoPathAbsolute, `.lfs-assets-guid-sum`)}`, 'utf8') - .replace(' .lfs-assets-guid', '') - .replace(/\n/g, ``), - }; - - return lfsHashes; - } catch (error) { - throw error; - } - } - public static async hashAllFiles(folder: string) { - const startPath = process.cwd(); - process.chdir(folder); - const result = await (await CloudRunnerSystem.Run(`find -type f -exec md5sum "{}" + | sort | md5sum`)) - .replace(/\n/g, '') - .split(` `)[0]; - process.chdir(startPath); - - return result; - } - - @CliFunction(`hash`, `hash all folder contents`) - static async hash() { - const folder = Cli.options!['cachePushFrom']; - LfsHashing.hashAllFiles(folder); - } -} diff --git a/src/model/cloud-runner/services/utility/lfs-hashing.ts b/src/model/cloud-runner/services/utility/lfs-hashing.ts new file mode 100644 index 00000000..91553400 --- /dev/null +++ b/src/model/cloud-runner/services/utility/lfs-hashing.ts @@ -0,0 +1,43 @@ +import path from 'node:path'; +import { CloudRunnerFolders } from '../../options/cloud-runner-folders'; +import { CloudRunnerSystem } from '../core/cloud-runner-system'; +import fs from 'node:fs'; +import { Cli } from '../../../cli/cli'; +import { CliFunction } from '../../../cli/cli-functions-repository'; + +export class LfsHashing { + public static async createLFSHashFiles() { + await CloudRunnerSystem.Run(`git lfs ls-files -l | cut -d ' ' -f1 | sort > .lfs-assets-guid`); + await CloudRunnerSystem.Run(`md5sum .lfs-assets-guid > .lfs-assets-guid-sum`); + const lfsHashes = { + lfsGuid: fs + .readFileSync(`${path.join(CloudRunnerFolders.repoPathAbsolute, `.lfs-assets-guid`)}`, 'utf8') + .replace(/\n/g, ``), + lfsGuidSum: fs + .readFileSync(`${path.join(CloudRunnerFolders.repoPathAbsolute, `.lfs-assets-guid-sum`)}`, 'utf8') + .replace(' .lfs-assets-guid', '') + .replace(/\n/g, ``), + }; + + return lfsHashes; + } + public static async hashAllFiles(folder: string) { + const startPath = process.cwd(); + process.chdir(folder); + const result = await (await CloudRunnerSystem.Run(`find -type f -exec md5sum "{}" + | sort | md5sum`)) + .replace(/\n/g, '') + .split(` `)[0]; + process.chdir(startPath); + + return result; + } + + @CliFunction(`hash`, `hash all folder contents`) + static async hash() { + if (!Cli.options) { + return; + } + const folder = Cli.options['cachePushFrom']; + LfsHashing.hashAllFiles(folder); + } +} 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 index 4070e3bb..dd5fe7d8 100644 --- a/src/model/cloud-runner/tests/cloud-runner-async-workflow.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-async-workflow.test.ts @@ -2,7 +2,7 @@ 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 CloudRunnerOptions from '../options/cloud-runner-options'; import setups from './cloud-runner-suite.test'; import { OptionValues } from 'commander'; @@ -15,7 +15,7 @@ describe('Cloud Runner Async Workflows', () => { setups(); it('Responds', () => {}); - if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `local-docker`) { + if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.providerStrategy !== `local-docker`) { it('Async Workflows', async () => { // Setup parameters const buildParameter = await CreateParameters({ diff --git a/src/model/cloud-runner/tests/cloud-runner-remote-client-caching.test.ts b/src/model/cloud-runner/tests/cloud-runner-caching.test.ts similarity index 89% rename from src/model/cloud-runner/tests/cloud-runner-remote-client-caching.test.ts rename to src/model/cloud-runner/tests/cloud-runner-caching.test.ts index bc56e333..eb6b653d 100644 --- a/src/model/cloud-runner/tests/cloud-runner-remote-client-caching.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-caching.test.ts @@ -4,14 +4,15 @@ import BuildParameters from '../../build-parameters'; import { Cli } from '../../cli/cli'; import UnityVersioning from '../../unity-versioning'; import CloudRunner from '../cloud-runner'; -import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import { CloudRunnerSystem } from '../services/core/cloud-runner-system'; import { Caching } from '../remote-client/caching'; import { v4 as uuidv4 } from 'uuid'; import GitHub from '../../github'; +import CloudRunnerOptions from '../options/cloud-runner-options'; describe('Cloud Runner (Remote Client) Caching', () => { it('responds', () => {}); - if (process.platform === 'linux') { - it.skip('Simple caching works', async () => { + if (CloudRunnerOptions.providerStrategy === `local-docker`) { + it('Simple caching works', async () => { Cli.options = { versioning: 'None', projectPath: 'test-project', diff --git a/src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts b/src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts deleted file mode 100644 index 89510a7b..00000000 --- a/src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { BuildParameters } from '../..'; -import { TaskParameterSerializer } from '../services/task-parameter-serializer'; -import UnityVersioning from '../../unity-versioning'; -import { Cli } from '../../cli/cli'; -import GitHub from '../../github'; -import setups from './cloud-runner-suite.test'; -import { OptionValues } from 'commander'; - -async function CreateParameters(overrides: OptionValues | undefined) { - if (overrides) { - Cli.options = overrides; - } - const originalValue = GitHub.githubInputEnabled; - GitHub.githubInputEnabled = false; - const results = await BuildParameters.create(); - GitHub.githubInputEnabled = originalValue; - delete Cli.options; - - return results; -} -describe('Cloud Runner Environment Serializer', () => { - setups(); - const testSecretName = 'testSecretName'; - const testSecretValue = 'testSecretValue'; - it('Cloud Runner Parameter Serialization', async () => { - // Setup parameters - const buildParameter = await CreateParameters({ - versioning: 'None', - projectPath: 'test-project', - unityVersion: UnityVersioning.read('test-project'), - customJob: ` - - name: 'step 1' - image: 'alpine' - commands: 'printenv' - secrets: - - name: '${testSecretName}' - value: '${testSecretValue}' - `, - }); - - const result = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); - expect(result.find((x) => Number.parseInt(x.name)) !== undefined).toBeFalsy(); - const result2 = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); - expect(result2.find((x) => Number.parseInt(x.name)) !== undefined).toBeFalsy(); - }); -}); diff --git a/src/model/cloud-runner/tests/cloud-runner-sync-environment.test.ts b/src/model/cloud-runner/tests/cloud-runner-environment.test.ts similarity index 56% rename from src/model/cloud-runner/tests/cloud-runner-sync-environment.test.ts rename to src/model/cloud-runner/tests/cloud-runner-environment.test.ts index 94d9577c..4966d4e5 100644 --- a/src/model/cloud-runner/tests/cloud-runner-sync-environment.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-environment.test.ts @@ -1,20 +1,26 @@ -import { BuildParameters, ImageTag } from '../..'; -import CloudRunner from '../cloud-runner'; -import Input from '../../input'; -import { CloudRunnerStatics } from '../cloud-runner-statics'; -import { TaskParameterSerializer } from '../services/task-parameter-serializer'; +import { BuildParameters, CloudRunner, ImageTag, Input } from '../..'; +import { TaskParameterSerializer } from '../services/core/task-parameter-serializer'; import UnityVersioning from '../../unity-versioning'; import { Cli } from '../../cli/cli'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; -import CloudRunnerOptions from '../cloud-runner-options'; +import GitHub from '../../github'; import setups from './cloud-runner-suite.test'; -import { OptionValues } from 'commander'; +import { CloudRunnerStatics } from '../options/cloud-runner-statics'; +import CloudRunnerOptions from '../options/cloud-runner-options'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; -async function CreateParameters(overrides: OptionValues | undefined) { - if (overrides) Cli.options = overrides; +async function CreateParameters(overrides: any) { + if (overrides) { + Cli.options = overrides; + } + const originalValue = GitHub.githubInputEnabled; + GitHub.githubInputEnabled = false; + const results = await BuildParameters.create(); + GitHub.githubInputEnabled = originalValue; + delete Cli.options; - return BuildParameters.create(); + return results; } + describe('Cloud Runner Sync Environments', () => { setups(); const testSecretName = 'testSecretName'; @@ -62,7 +68,7 @@ describe('Cloud Runner Sync Environments', () => { return x; }) .filter((element) => { - return !['UNITY_LICENSE', 'CUSTOM_JOB'].includes(element.name); + return !['UNITY_LICENSE', 'UNITY_LICENSE', 'CUSTOM_JOB', 'CUSTOM_JOB'].includes(element.name); }); const newLinePurgedFile = file .replace(/\s+/g, '') @@ -76,3 +82,30 @@ describe('Cloud Runner Sync Environments', () => { }, 1_000_000_000); } }); + +describe('Cloud Runner Environment Serializer', () => { + setups(); + const testSecretName = 'testSecretName'; + const testSecretValue = 'testSecretValue'; + it('Cloud Runner Parameter Serialization', async () => { + // Setup parameters + const buildParameter = await CreateParameters({ + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + customJob: ` + - name: 'step 1' + image: 'alpine' + commands: 'printenv' + secrets: + - name: '${testSecretName}' + value: '${testSecretValue}' + `, + }); + + const result = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); + expect(result.find((x) => Number.parseInt(x.name)) !== undefined).toBeFalsy(); + const result2 = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); + expect(result2.find((x) => Number.parseInt(x.name)) !== undefined).toBeFalsy(); + }); +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-hooks.test.ts b/src/model/cloud-runner/tests/cloud-runner-hooks.test.ts new file mode 100644 index 00000000..7a1ff9c6 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-hooks.test.ts @@ -0,0 +1,114 @@ +import CloudRunner from '../cloud-runner'; +import { BuildParameters, ImageTag } from '../..'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../options/cloud-runner-options'; +import setups from './cloud-runner-suite.test'; +import { ContainerHookService } from '../services/hooks/container-hook-service'; +import { CommandHookService } from '../services/hooks/command-hook-service'; + +async function CreateParameters(overrides: any) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Custom Hooks And Steps', () => { + it('Responds', () => {}); + setups(); + it('Check parsing and reading of steps', async () => { + const yamlString = `hook: before +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 = ContainerHookService.ParseContainerHooks(yamlString); + const stringObject2 = ContainerHookService.ParseContainerHooks(yamlString2); + + CloudRunnerLogger.log(yamlString); + CloudRunnerLogger.log(JSON.stringify(stringObject, undefined, 4)); + + expect(stringObject.length).toBe(1); + expect(stringObject[0].hook).toBe(`before`); + expect(stringObject2.length).toBe(1); + expect(stringObject2[0].hook).toBe(`before`); + + const getCustomStepsFromFiles = ContainerHookService.GetContainerHooksFromFiles(`before`); + CloudRunnerLogger.log(JSON.stringify(getCustomStepsFromFiles, undefined, 4)); + }); + if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.providerStrategy !== `k8s`) { + it('Should be 1 before and 1 after hook', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + containerHookFiles: `my-test-step-pre-build,my-test-step-post-build`, + commandHookFiles: `my-test-hook-pre-build,my-test-hook-post-build`, + }; + const buildParameter2 = await CreateParameters(overrides); + await CloudRunner.setup(buildParameter2); + const beforeHooks = CommandHookService.GetCustomHooksFromFiles(`before`); + const afterHooks = CommandHookService.GetCustomHooksFromFiles(`after`); + expect(beforeHooks).toHaveLength(1); + expect(afterHooks).toHaveLength(1); + }); + it('Should be 1 before and 1 after step', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + containerHookFiles: `my-test-step-pre-build,my-test-step-post-build`, + commandHookFiles: `my-test-hook-pre-build,my-test-hook-post-build`, + }; + const buildParameter2 = await CreateParameters(overrides); + await CloudRunner.setup(buildParameter2); + const beforeSteps = ContainerHookService.GetContainerHooksFromFiles(`before`); + const afterSteps = ContainerHookService.GetContainerHooksFromFiles(`after`); + expect(beforeSteps).toHaveLength(1); + expect(afterSteps).toHaveLength(1); + }); + it('Run build once - check for pre and post custom hooks run contents', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + containerHookFiles: `my-test-step-pre-build,my-test-step-post-build`, + commandHookFiles: `my-test-hook-pre-build,my-test-hook-post-build`, + }; + const buildParameter2 = await CreateParameters(overrides); + const baseImage2 = new ImageTag(buildParameter2); + const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); + CloudRunnerLogger.log(`run 2 succeeded`); + + const buildContainsBuildSucceeded = results2.includes('Build succeeded'); + const buildContainsPreBuildHookRunMessage = results2.includes('before-build hook test!'); + const buildContainsPostBuildHookRunMessage = results2.includes('after-build hook test!'); + + const buildContainsPreBuildStepMessage = results2.includes('before-build step test!'); + const buildContainsPostBuildStepMessage = results2.includes('after-build step test!'); + + expect(buildContainsBuildSucceeded).toBeTruthy(); + expect(buildContainsPreBuildHookRunMessage).toBeTruthy(); + expect(buildContainsPostBuildHookRunMessage).toBeTruthy(); + expect(buildContainsPreBuildStepMessage).toBeTruthy(); + expect(buildContainsPostBuildStepMessage).toBeTruthy(); + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-local-persistence.test.ts b/src/model/cloud-runner/tests/cloud-runner-local-persistence.test.ts new file mode 100644 index 00000000..4d8d5330 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-local-persistence.test.ts @@ -0,0 +1,53 @@ +import { ImageTag } from '../..'; +import CloudRunner from '../cloud-runner'; +import UnityVersioning from '../../unity-versioning'; +import CloudRunnerOptions from '../options/cloud-runner-options'; +import setups from './cloud-runner-suite.test'; +import fs from 'node:fs'; +import { CreateParameters } from './create-test-parameter'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; + +describe('Cloud Runner Local Docker Workflows', () => { + setups(); + it('Responds', () => {}); + + if (CloudRunnerOptions.providerStrategy === `local-docker`) { + it('inspect stateful folder of workflows', async () => { + const testValue = `the state in a job exits in the expected local-docker folder`; + + // Setup parameters + const buildParameter = await CreateParameters({ + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + customJob: ` + - name: 'step 1' + image: 'ubuntu' + commands: 'echo "${testValue}" >> /data/test-out-state.txt' + `, + }); + const buildParameter2 = await CreateParameters({ + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + customJob: ` + - name: 'step 1' + image: 'ubuntu' + commands: 'cat /data/test-out-state.txt >> /data/test-out-state-2.txt' + `, + }); + const baseImage = new ImageTag(buildParameter); + + // Run the job + await CloudRunner.run(buildParameter, baseImage.toString()); + await CloudRunner.run(buildParameter2, baseImage.toString()); + + const outputFile = fs.readFileSync(`./cloud-runner-cache/test-out-state.txt`, `utf-8`); + expect(outputFile).toMatch(testValue); + + const outputFile2 = fs.readFileSync(`./cloud-runner-cache/test-out-state-2.txt`, `utf-8`); + expect(outputFile2).toMatch(testValue); + CloudRunnerLogger.log(outputFile); + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-locking-core.test.ts b/src/model/cloud-runner/tests/cloud-runner-locking-core.test.ts new file mode 100644 index 00000000..9e56e18c --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-locking-core.test.ts @@ -0,0 +1,115 @@ +import SharedWorkspaceLocking from '../services/core/shared-workspace-locking'; +import { Cli } from '../../cli/cli'; +import setups from './cloud-runner-suite.test'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../options/cloud-runner-options'; +import UnityVersioning from '../../unity-versioning'; +import BuildParameters from '../../build-parameters'; +import CloudRunner from '../cloud-runner'; + +async function CreateParameters(overrides: any) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Locking Core', () => { + setups(); + it('Responds', () => {}); + if (CloudRunnerOptions.cloudRunnerDebug) { + it(`Create Workspace`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + CloudRunner.buildParameters = buildParameters; + const newWorkspaceName = `test-workspace-${uuidv4()}`; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + }, 150000); + it(`Create Workspace And Lock Workspace`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const runId = uuidv4(); + const buildParameters = await CreateParameters(overrides); + CloudRunner.buildParameters = buildParameters; + const newWorkspaceName = `test-workspace-${uuidv4()}`; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + }, 150000); + it(`0 free workspaces after locking`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.HasWorkspaceLock(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.GetAllWorkspaces(buildParameters)).toHaveLength(1); + expect(await SharedWorkspaceLocking.GetAllLocksForWorkspace(newWorkspaceName, buildParameters)).toHaveLength(1); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeTruthy(); + + const files = await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParameters.cacheKey}/`, + ); + + const lockFilesExist = + files.filter((x) => { + return x.includes(newWorkspaceName) && x.endsWith(`_lock`); + }).length > 0; + + expect(files).toHaveLength(2); + expect( + files.filter((x) => { + return x.includes(newWorkspaceName) && x.endsWith(`_lock`); + }), + ).toHaveLength(1); + expect(lockFilesExist).toBeTruthy(); + const result: string[] = []; + const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParameters); + for (const element of workspaces) { + expect((await SharedWorkspaceLocking.GetAllWorkspaces(buildParameters)).join()).toContain(element); + expect(await SharedWorkspaceLocking.GetAllWorkspaces(buildParameters)).toHaveLength(1); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(element, buildParameters)).toBeTruthy(); + await new Promise((promise) => setTimeout(promise, 1500)); + const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(element, buildParameters); + const isBelowMax = await SharedWorkspaceLocking.IsWorkspaceBelowMax(element, buildParameters); + CloudRunnerLogger.log(`workspace ${element} locked:${isLocked} below max:${isBelowMax}`); + const lock = files.find((x) => { + return x.endsWith(`_lock`); + }); + expect(lock).toContain(element); + expect(isLocked).toBeTruthy(); + expect(isBelowMax).toBeTruthy(); + if (!isLocked && isBelowMax) { + result.push(element); + } + } + expect(result).toHaveLength(0); + expect(await SharedWorkspaceLocking.GetFreeWorkspaces(buildParameters)).toHaveLength(0); + }, 300000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-locking-get-locked.test.ts b/src/model/cloud-runner/tests/cloud-runner-locking-get-locked.test.ts new file mode 100644 index 00000000..69ab1d89 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-locking-get-locked.test.ts @@ -0,0 +1,156 @@ +import SharedWorkspaceLocking from '../services/core/shared-workspace-locking'; +import { Cli } from '../../cli/cli'; +import setups from './cloud-runner-suite.test'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../options/cloud-runner-options'; +import UnityVersioning from '../../unity-versioning'; +import BuildParameters from '../../build-parameters'; +import CloudRunner from '../cloud-runner'; + +async function CreateParameters(overrides: any) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Locking Get Locked Workspace', () => { + setups(); + it('Responds', () => {}); + if (CloudRunnerOptions.cloudRunnerDebug) { + it(`Get locked workspace From No Workspace`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + }, 150000); + it(`Get locked workspace from unlocked`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(CloudRunner.lockedWorkspace).toMatch(newWorkspaceName); + }, 300000); + it(`Get locked workspace from locked`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + const runId2 = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.HasWorkspaceLock(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceBelowMax(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId2, buildParameters)).toBeTruthy(); + expect(CloudRunner.lockedWorkspace).not.toMatch(newWorkspaceName); + }, 300000); + it(`Get locked workspace after double lock and one unlock`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + const runId2 = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeFalsy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.HasWorkspaceLock(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId2, buildParameters)).toBeTruthy(); + expect(CloudRunner.lockedWorkspace).not.toContain(newWorkspaceName); + }, 300000); + it(`Get locked workspace after double lock and unlock`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + const runId2 = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeFalsy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.HasWorkspaceLock(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)).toBeFalsy(); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId2, buildParameters)).toBeTruthy(); + expect(CloudRunner.lockedWorkspace).toContain(newWorkspaceName); + }, 300000); + it(`Get locked workspace from unlocked was locked`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + CloudRunner.buildParameters = buildParameters; + expect(await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + expect(CloudRunner.lockedWorkspace).toMatch(newWorkspaceName); + }, 300000); + } +}); 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 deleted file mode 100644 index 85fa46bb..00000000 --- a/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import CloudRunner from '../cloud-runner'; -import { BuildParameters, ImageTag } from '../..'; -import UnityVersioning from '../../unity-versioning'; -import { Cli } from '../../cli/cli'; -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 { CloudRunnerCustomSteps } from '../services/cloud-runner-custom-steps'; -import { OptionValues } from 'commander'; - -async function CreateParameters(overrides: OptionValues | undefined) { - if (overrides) { - Cli.options = overrides; - } - - return await BuildParameters.create(); -} - -describe('Cloud Runner Custom Hooks And Steps', () => { - it('Responds', () => {}); - setups(); - it('Check parsing and reading of steps', async () => { - const yamlString = `hook: before -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); - - CloudRunnerLogger.log(yamlString); - CloudRunnerLogger.log(JSON.stringify(stringObject, undefined, 4)); - - expect(stringObject.length).toBe(1); - expect(stringObject[0].hook).toBe(`before`); - expect(stringObject2.length).toBe(1); - expect(stringObject2[0].hook).toBe(`before`); - - const getCustomStepsFromFiles = CloudRunnerCustomSteps.GetCustomStepsFromFiles(`before`); - CloudRunnerLogger.log(JSON.stringify(getCustomStepsFromFiles, undefined, 4)); - }); - if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `k8s`) { - it('Run build once - check for pre and post custom hooks run contents', async () => { - const overrides = { - versioning: 'None', - projectPath: 'test-project', - unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), - targetPlatform: 'StandaloneLinux64', - cacheKey: `test-case-${uuidv4()}`, - customStepFiles: `my-test-step-pre-build,my-test-step-post-build`, - }; - const buildParameter2 = await CreateParameters(overrides); - const baseImage2 = new ImageTag(buildParameter2); - const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); - CloudRunnerLogger.log(`run 2 succeeded`); - - const build2ContainsBuildSucceeded = results2.includes('Build succeeded'); - const build2ContainsPreBuildHookRunMessage = results2.includes('before-build hook test!'); - const build2ContainsPostBuildHookRunMessage = results2.includes('after-build hook test!'); - - const build2ContainsPreBuildStepMessage = results2.includes('before-build step test!'); - const build2ContainsPostBuildStepMessage = results2.includes('after-build step test!'); - - expect(build2ContainsBuildSucceeded).toBeTruthy(); - expect(build2ContainsPreBuildHookRunMessage).toBeTruthy(); - expect(build2ContainsPostBuildHookRunMessage).toBeTruthy(); - expect(build2ContainsPreBuildStepMessage).toBeTruthy(); - expect(build2ContainsPostBuildStepMessage).toBeTruthy(); - }, 1_000_000_000); - } -}); diff --git a/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts b/src/model/cloud-runner/tests/cloud-runner-s3-steps.test.ts similarity index 78% rename from src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts rename to src/model/cloud-runner/tests/cloud-runner-s3-steps.test.ts index f33b679e..e17d9bda 100644 --- a/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-s3-steps.test.ts @@ -2,11 +2,11 @@ import CloudRunner from '../cloud-runner'; import { BuildParameters, ImageTag } from '../..'; import UnityVersioning from '../../unity-versioning'; import { Cli } from '../../cli/cli'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; import { v4 as uuidv4 } from 'uuid'; -import CloudRunnerOptions from '../cloud-runner-options'; +import CloudRunnerOptions from '../options/cloud-runner-options'; import setups from './cloud-runner-suite.test'; -import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import { CloudRunnerSystem } from '../services/core/cloud-runner-system'; import { OptionValues } from 'commander'; async function CreateParameters(overrides: OptionValues | undefined) { @@ -20,7 +20,7 @@ async function CreateParameters(overrides: OptionValues | undefined) { describe('Cloud Runner pre-built S3 steps', () => { it('Responds', () => {}); setups(); - if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `local-docker`) { + if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.providerStrategy !== `local-docker`) { it('Run build and prebuilt s3 cache pull, cache push and upload build', async () => { const overrides = { versioning: 'None', @@ -28,7 +28,7 @@ describe('Cloud Runner pre-built S3 steps', () => { unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), targetPlatform: 'StandaloneLinux64', cacheKey: `test-case-${uuidv4()}`, - customStepFiles: `aws-s3-pull-cache,aws-s3-upload-cache,aws-s3-upload-build`, + containerHookFiles: `aws-s3-pull-cache,aws-s3-upload-cache,aws-s3-upload-build`, }; const buildParameter2 = await CreateParameters(overrides); const baseImage2 = new ImageTag(buildParameter2); @@ -39,7 +39,7 @@ describe('Cloud Runner pre-built S3 steps', () => { expect(build2ContainsBuildSucceeded).toBeTruthy(); const results = await CloudRunnerSystem.RunAndReadLines( - `aws s3 ls s3://${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/${buildParameter2.cacheKey}/`, + `aws s3 ls s3://${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/`, ); CloudRunnerLogger.log(results.join(`,`)); }, 1_000_000_000); diff --git a/src/model/cloud-runner/tests/create-test-parameter.ts b/src/model/cloud-runner/tests/create-test-parameter.ts new file mode 100644 index 00000000..26f0bdb2 --- /dev/null +++ b/src/model/cloud-runner/tests/create-test-parameter.ts @@ -0,0 +1,8 @@ +import BuildParameters from '../../build-parameters'; +import { Cli } from '../../cli/cli'; + +export async function CreateParameters(overrides: any) { + if (overrides) Cli.options = overrides; + + return BuildParameters.create(); +} diff --git a/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts b/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-caching.test.ts similarity index 69% rename from src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts rename to src/model/cloud-runner/tests/e2e/cloud-runner-end2end-caching.test.ts index 381deaf3..365368e1 100644 --- a/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts +++ b/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-caching.test.ts @@ -1,15 +1,15 @@ -import CloudRunner from '../cloud-runner'; -import { BuildParameters, ImageTag } from '../..'; -import UnityVersioning from '../../unity-versioning'; -import { Cli } from '../../cli/cli'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunner from '../../cloud-runner'; +import { BuildParameters, ImageTag } from '../../..'; +import UnityVersioning from '../../../unity-versioning'; +import { Cli } from '../../../cli/cli'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { v4 as uuidv4 } from 'uuid'; -import CloudRunnerOptions from '../cloud-runner-options'; -import setups from './cloud-runner-suite.test'; -import fs from 'node:fs'; -import { OptionValues } from 'commander'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; +import setups from '../cloud-runner-suite.test'; +import * as fs from 'node:fs'; +import { CloudRunnerSystem } from '../../services/core/cloud-runner-system'; -async function CreateParameters(overrides: OptionValues | undefined) { +async function CreateParameters(overrides: any) { if (overrides) { Cli.options = overrides; } @@ -28,10 +28,10 @@ describe('Cloud Runner Caching', () => { unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), targetPlatform: 'StandaloneLinux64', cacheKey: `test-case-${uuidv4()}`, - customStepFiles: `debug-cache`, + containerHookFiles: `debug-cache`, }; - if (CloudRunnerOptions.cloudRunnerCluster === `k8s`) { - overrides.customStepFiles += `,aws-s3-pull-cache,aws-s3-upload-cache`; + if (CloudRunnerOptions.providerStrategy === `k8s`) { + overrides.containerHookFiles += `,aws-s3-pull-cache,aws-s3-upload-cache`; } const buildParameter = await CreateParameters(overrides); expect(buildParameter.projectPath).toEqual(overrides.projectPath); @@ -48,7 +48,14 @@ describe('Cloud Runner Caching', () => { CloudRunnerLogger.log(`run 1 succeeded`); - if (CloudRunnerOptions.cloudRunnerCluster === `local-docker`) { + if (CloudRunnerOptions.providerStrategy === `local-docker`) { + await CloudRunnerSystem.Run(`tree ./cloud-runner-cache/cache`); + await CloudRunnerSystem.Run( + `cp ./cloud-runner-cache/cache/${buildParameter.cacheKey}/Library/lib-${buildParameter.buildGuid}.tar ./`, + ); + await CloudRunnerSystem.Run(`mkdir results`); + await CloudRunnerSystem.Run(`tar -xf lib-${buildParameter.buildGuid}.tar -C ./results`); + await CloudRunnerSystem.Run(`tree -d ./results`); const cacheFolderExists = fs.existsSync(`cloud-runner-cache/cache/${overrides.cacheKey}`); expect(cacheFolderExists).toBeTruthy(); } diff --git a/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-locking.test.ts b/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-locking.test.ts new file mode 100644 index 00000000..6f8e5344 --- /dev/null +++ b/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-locking.test.ts @@ -0,0 +1,92 @@ +import CloudRunner from '../../cloud-runner'; +import { BuildParameters } from '../../..'; +import UnityVersioning from '../../../unity-versioning'; +import { Cli } from '../../../cli/cli'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; +import setups from '../cloud-runner-suite.test'; +import SharedWorkspaceLocking from '../../services/core/shared-workspace-locking'; + +async function CreateParameters(overrides: any) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Locking', () => { + setups(); + it('Responds', () => {}); + if (CloudRunnerOptions.cloudRunnerDebug) { + it(`Simple Locking End2End Flow`, async () => { + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + maxRetainedWorkspaces: 3, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + CloudRunner.buildParameters = buildParameters; + await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters); + expect(await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(newWorkspaceName, buildParameters)).toBeTruthy(); + const isExpectedUnlockedBeforeLocking = + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; + expect(isExpectedUnlockedBeforeLocking).toBeTruthy(); + const result = await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters); + expect(result).toBeTruthy(); + const lines = await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`); + expect(lines.map((x) => x.replace(`/`, ``)).includes(buildParameters.cacheKey)); + expect(await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(buildParameters)).toBeTruthy(); + expect(await SharedWorkspaceLocking.DoesWorkspaceExist(newWorkspaceName, buildParameters)).toBeTruthy(); + const allLocks = await SharedWorkspaceLocking.GetAllLocksForWorkspace(newWorkspaceName, buildParameters); + expect( + ( + await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParameters.cacheKey}/`, + ) + ).filter((x) => x.endsWith(`${newWorkspaceName}_workspace_lock`)), + ).toHaveLength(1); + expect( + ( + await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParameters.cacheKey}/`, + ) + ).filter((x) => x.endsWith(`${newWorkspaceName}_workspace`)), + ).toHaveLength(1); + expect(allLocks.filter((x) => x.endsWith(`${newWorkspaceName}_workspace_lock`)).length).toBeGreaterThan(0); + const isExpectedLockedAfterLocking = + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === true; + expect(isExpectedLockedAfterLocking).toBeTruthy(); + const locksBeforeRelease = await SharedWorkspaceLocking.GetAllLocksForWorkspace( + newWorkspaceName, + buildParameters, + ); + CloudRunnerLogger.log(JSON.stringify(locksBeforeRelease, undefined, 4)); + expect(locksBeforeRelease.length).toBe(1); + await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters); + const locks = await SharedWorkspaceLocking.GetAllLocksForWorkspace(newWorkspaceName, buildParameters); + expect(locks.length).toBe(0); + const isExpectedNotLockedAfterReleasing = + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; + expect(isExpectedNotLockedAfterReleasing).toBeTruthy(); + const lockingResult2 = await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters); + expect(lockingResult2).toBeTruthy(); + expect((await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === true).toBeTruthy(); + await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters); + expect( + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false, + ).toBeTruthy(); + await SharedWorkspaceLocking.CleanupWorkspace(newWorkspaceName, buildParameters); + CloudRunnerLogger.log(`Starting get or create`); + expect(await SharedWorkspaceLocking.GetLockedWorkspace(newWorkspaceName, runId, buildParameters)).toBeTruthy(); + }, 350000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts b/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-retaining.test.ts similarity index 80% rename from src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts rename to src/model/cloud-runner/tests/e2e/cloud-runner-end2end-retaining.test.ts index c30e6b7d..8494527e 100644 --- a/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts +++ b/src/model/cloud-runner/tests/e2e/cloud-runner-end2end-retaining.test.ts @@ -1,24 +1,16 @@ -import CloudRunner from '../cloud-runner'; -import { BuildParameters, ImageTag } from '../..'; -import UnityVersioning from '../../unity-versioning'; -import { Cli } from '../../cli/cli'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunner from '../../cloud-runner'; +import { ImageTag } from '../../..'; +import UnityVersioning from '../../../unity-versioning'; +import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import { v4 as uuidv4 } from 'uuid'; -import CloudRunnerOptions from '../cloud-runner-options'; -import setups from './cloud-runner-suite.test'; -import fs from 'node:fs'; +import CloudRunnerOptions from '../../options/cloud-runner-options'; +import setups from './../cloud-runner-suite.test'; +import * as fs from 'node:fs'; import path from 'node:path'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders'; -import SharedWorkspaceLocking from '../services/shared-workspace-locking'; -import { OptionValues } from 'commander'; - -async function CreateParameters(overrides: OptionValues | undefined) { - if (overrides) { - Cli.options = overrides; - } - - return await BuildParameters.create(); -} +import { CloudRunnerFolders } from '../../options/cloud-runner-folders'; +import SharedWorkspaceLocking from '../../services/core/shared-workspace-locking'; +import { CreateParameters } from '../create-test-parameter'; +import { CloudRunnerSystem } from '../../services/core/cloud-runner-system'; describe('Cloud Runner Retain Workspace', () => { it('Responds', () => {}); @@ -31,7 +23,7 @@ describe('Cloud Runner Retain Workspace', () => { unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), targetPlatform: 'StandaloneLinux64', cacheKey: `test-case-${uuidv4()}`, - retainWorkspaces: true, + maxRetainedWorkspaces: 1, }; const buildParameter = await CreateParameters(overrides); expect(buildParameter.projectPath).toEqual(overrides.projectPath); @@ -46,12 +38,15 @@ describe('Cloud Runner Retain Workspace', () => { expect(results).toContain(buildSucceededString); expect(results).not.toContain(cachePushFail); - if (CloudRunnerOptions.cloudRunnerCluster === `local-docker`) { + if (CloudRunnerOptions.providerStrategy === `local-docker`) { const cacheFolderExists = fs.existsSync(`cloud-runner-cache/cache/${overrides.cacheKey}`); expect(cacheFolderExists).toBeTruthy(); + await CloudRunnerSystem.Run(`tree -d ./cloud-runner-cache`); } CloudRunnerLogger.log(`run 1 succeeded`); + + // await CloudRunnerSystem.Run(`tree -d ./cloud-runner-cache/${}`); const buildParameter2 = await CreateParameters(overrides); buildParameter2.cacheKey = buildParameter.cacheKey; diff --git a/src/model/cloud-runner/tests/shared-workspace-locking.test.ts b/src/model/cloud-runner/tests/shared-workspace-locking.test.ts deleted file mode 100644 index 0945e224..00000000 --- a/src/model/cloud-runner/tests/shared-workspace-locking.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import SharedWorkspaceLocking from '../services/shared-workspace-locking'; -import { Cli } from '../../cli/cli'; -import setups from './cloud-runner-suite.test'; -import CloudRunnerLogger from '../services/cloud-runner-logger'; -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'; -import { OptionValues } from 'commander'; - -async function CreateParameters(overrides: OptionValues | undefined) { - if (overrides) { - Cli.options = overrides; - } - - return await BuildParameters.create(); -} - -describe('Cloud Runner Locking', () => { - setups(); - it('Responds', () => {}); - if (CloudRunnerOptions.cloudRunnerDebug) { - it(`Simple Locking Flow`, async () => { - Cli.options!.retainWorkspaces = true; - const overrides: any = { - versioning: 'None', - projectPath: 'test-project', - unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), - targetPlatform: 'StandaloneLinux64', - cacheKey: `test-case-${uuidv4()}`, - }; - const buildParameters = await CreateParameters(overrides); - - const newWorkspaceName = `test-workspace-${uuidv4()}`; - const runId = uuidv4(); - CloudRunner.buildParameters = buildParameters; - await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters); - const isExpectedUnlockedBeforeLocking = - (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; - expect(isExpectedUnlockedBeforeLocking).toBeTruthy(); - await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters); - const isExpectedLockedAfterLocking = - (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === true; - expect(isExpectedLockedAfterLocking).toBeTruthy(); - const locksBeforeRelease = await SharedWorkspaceLocking.GetAllLocks(newWorkspaceName, buildParameters); - CloudRunnerLogger.log(JSON.stringify(locksBeforeRelease, undefined, 4)); - expect(locksBeforeRelease.length).toBe(1); - await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters); - const locks = await SharedWorkspaceLocking.GetAllLocks(newWorkspaceName, buildParameters); - expect(locks.length).toBe(0); - const isExpectedLockedAfterReleasing = - (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; - expect(isExpectedLockedAfterReleasing).toBeTruthy(); - }, 150000); - it.skip('All Locking Actions', async () => { - Cli.options!.retainWorkspaces = true; - const overrides: OptionValues = { - versioning: 'None', - projectPath: 'test-project', - unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), - targetPlatform: 'StandaloneLinux64', - cacheKey: `test-case-${uuidv4()}`, - }; - const buildParameters = await CreateParameters(overrides); - - CloudRunnerLogger.log( - `GetAllWorkspaces ${JSON.stringify(await SharedWorkspaceLocking.GetAllWorkspaces(buildParameters))}`, - ); - CloudRunnerLogger.log( - `GetFreeWorkspaces ${JSON.stringify(await SharedWorkspaceLocking.GetFreeWorkspaces(buildParameters))}`, - ); - CloudRunnerLogger.log( - `IsWorkspaceLocked ${JSON.stringify( - await SharedWorkspaceLocking.IsWorkspaceLocked(`test-workspace-${uuidv4()}`, buildParameters), - )}`, - ); - CloudRunnerLogger.log( - `GetFreeWorkspaces ${JSON.stringify(await SharedWorkspaceLocking.GetFreeWorkspaces(buildParameters))}`, - ); - CloudRunnerLogger.log( - `LockWorkspace ${JSON.stringify( - await SharedWorkspaceLocking.LockWorkspace(`test-workspace-${uuidv4()}`, uuidv4(), buildParameters), - )}`, - ); - CloudRunnerLogger.log( - `CreateLockableWorkspace ${JSON.stringify( - await SharedWorkspaceLocking.CreateWorkspace(`test-workspace-${uuidv4()}`, buildParameters), - )}`, - ); - CloudRunnerLogger.log( - `GetLockedWorkspace ${JSON.stringify( - await SharedWorkspaceLocking.GetOrCreateLockedWorkspace( - `test-workspace-${uuidv4()}`, - uuidv4(), - buildParameters, - ), - )}`, - ); - }, 3000000); - } -}); diff --git a/src/model/cloud-runner/workflows/async-workflow.ts b/src/model/cloud-runner/workflows/async-workflow.ts index ffac8478..1d864b29 100644 --- a/src/model/cloud-runner/workflows/async-workflow.ts +++ b/src/model/cloud-runner/workflows/async-workflow.ts @@ -1,7 +1,7 @@ -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 CloudRunnerSecret from '../options/cloud-runner-secret'; +import CloudRunnerEnvironmentVariable from '../options/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; +import { CloudRunnerFolders } from '../options/cloud-runner-folders'; import CloudRunner from '../cloud-runner'; export class AsyncWorkflow { @@ -11,6 +11,9 @@ export class AsyncWorkflow { ): Promise { try { CloudRunnerLogger.log(`Cloud Runner is running async mode`); + const asyncEnvironmentVariable = new CloudRunnerEnvironmentVariable(); + asyncEnvironmentVariable.name = `ASYNC_WORKFLOW`; + asyncEnvironmentVariable.value = `true`; let output = ''; @@ -34,7 +37,7 @@ aws --version node /builder/dist/index.js -m async-workflow`, `/${CloudRunnerFolders.buildVolumeFolder}`, `/${CloudRunnerFolders.buildVolumeFolder}/`, - environmentVariables, + [...environmentVariables, asyncEnvironmentVariable], [ ...secrets, ...[ diff --git a/src/model/cloud-runner/workflows/build-automation-workflow.ts b/src/model/cloud-runner/workflows/build-automation-workflow.ts index 78cf57bc..22da3680 100644 --- a/src/model/cloud-runner/workflows/build-automation-workflow.ts +++ b/src/model/cloud-runner/workflows/build-automation-workflow.ts @@ -1,26 +1,25 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders'; -import { CloudRunnerStepState } from '../cloud-runner-step-state'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; +import { CloudRunnerFolders } from '../options/cloud-runner-folders'; +import { CloudRunnerStepParameters } from '../options/cloud-runner-step-parameters'; import { WorkflowInterface } from './workflow-interface'; import * as core from '@actions/core'; -import { CloudRunnerCustomHooks } from '../services/cloud-runner-custom-hooks'; +import { CommandHookService } from '../services/hooks/command-hook-service'; import path from 'node:path'; import CloudRunner from '../cloud-runner'; -import CloudRunnerOptions from '../cloud-runner-options'; -import { CloudRunnerCustomSteps } from '../services/cloud-runner-custom-steps'; +import { ContainerHookService } from '../services/hooks/container-hook-service'; export class BuildAutomationWorkflow implements WorkflowInterface { - async run(cloudRunnerStepState: CloudRunnerStepState) { + async run(cloudRunnerStepState: CloudRunnerStepParameters) { return await BuildAutomationWorkflow.standardBuildAutomation(cloudRunnerStepState.image, cloudRunnerStepState); } - private static async standardBuildAutomation(baseImage: string, cloudRunnerStepState: CloudRunnerStepState) { + private static async standardBuildAutomation(baseImage: string, cloudRunnerStepState: CloudRunnerStepParameters) { // TODO accept post and pre build steps as yaml files in the repo CloudRunnerLogger.log(`Cloud Runner is running standard build automation`); let output = ''; - output += await CloudRunnerCustomSteps.RunPreBuildSteps(cloudRunnerStepState); + output += await ContainerHookService.RunPreBuildSteps(cloudRunnerStepState); CloudRunnerLogger.logWithTime('Configurable pre build step(s) time'); if (!CloudRunner.buildParameters.isCliMode) core.startGroup('build'); @@ -40,7 +39,7 @@ export class BuildAutomationWorkflow implements WorkflowInterface { if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); CloudRunnerLogger.logWithTime('Build time'); - output += await CloudRunnerCustomSteps.RunPostBuildSteps(cloudRunnerStepState); + output += await ContainerHookService.RunPostBuildSteps(cloudRunnerStepState); CloudRunnerLogger.logWithTime('Configurable post build step(s) time'); CloudRunnerLogger.log(`Cloud Runner finished running standard build automation`); @@ -49,11 +48,11 @@ export class BuildAutomationWorkflow implements WorkflowInterface { } private static get BuildWorkflow() { - const setupHooks = CloudRunnerCustomHooks.getHooks(CloudRunner.buildParameters.customJobHooks).filter((x) => - x.step.includes(`setup`), + const setupHooks = CommandHookService.getHooks(CloudRunner.buildParameters.commandHooks).filter((x) => + x.step?.includes(`setup`), ); - const buildHooks = CloudRunnerCustomHooks.getHooks(CloudRunner.buildParameters.customJobHooks).filter((x) => - x.step.includes(`build`), + const buildHooks = CommandHookService.getHooks(CloudRunner.buildParameters.commandHooks).filter((x) => + x.step?.includes(`build`), ); const builderPath = CloudRunnerFolders.ToLinuxFolder( path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', `index.js`), @@ -65,16 +64,14 @@ export class BuildAutomationWorkflow implements WorkflowInterface { n 16.15.1 > /dev/null npm --version node --version - ${BuildAutomationWorkflow.TreeCommand} ${setupHooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} export GITHUB_WORKSPACE="${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}" + df -H /data/ ${BuildAutomationWorkflow.setupCommands(builderPath)} ${setupHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} - ${BuildAutomationWorkflow.TreeCommand} ${buildHooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} ${BuildAutomationWorkflow.BuildCommands(builderPath)} - ${buildHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} - ${BuildAutomationWorkflow.TreeCommand}`; + ${buildHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '}`; } private static setupCommands(builderPath: string) { @@ -84,24 +81,17 @@ export class BuildAutomationWorkflow implements WorkflowInterface { CloudRunnerFolders.unityBuilderRepoUrl } "${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.builderPathAbsolute)}" && chmod +x ${builderPath}`; - const retainedWorkspaceCommands = `if [ -e "${CloudRunnerFolders.ToLinuxFolder( - CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, - )}" ] && [ -e "${CloudRunnerFolders.ToLinuxFolder( - path.join(CloudRunnerFolders.repoPathAbsolute, `.git`), - )}" ]; then echo "Retained Workspace Already Exists!" ; fi`; - const cloneBuilderCommands = `if [ -e "${CloudRunnerFolders.ToLinuxFolder( CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, )}" ] && [ -e "${CloudRunnerFolders.ToLinuxFolder( path.join(CloudRunnerFolders.builderPathAbsolute, `.git`), - )}" ]; then echo "Builder Already Exists!"; else ${commands}; fi`; + )}" ] ; then echo "Builder Already Exists!" && tree ${ + CloudRunnerFolders.builderPathAbsolute + }; else ${commands} ; fi`; return `export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 - echo "downloading game-ci..." - ${retainedWorkspaceCommands} - ${cloneBuilderCommands} - echo "bootstrap game ci cloud runner..." - node ${builderPath} -m remote-cli-pre-build`; +${cloneBuilderCommands} +node ${builderPath} -m remote-cli-pre-build`; } private static BuildCommands(builderPath: string) { @@ -122,10 +112,4 @@ export class BuildAutomationWorkflow implements WorkflowInterface { chmod +x ${builderPath} node ${builderPath} -m remote-cli-post-build`; } - - private static get TreeCommand(): string { - return CloudRunnerOptions.cloudRunnerDebugTree - ? `tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute} && tree -L 2 ${CloudRunnerFolders.cacheFolderForCacheKeyFull} && du -h -s /${CloudRunnerFolders.buildVolumeFolder}/ && du -h -s ${CloudRunnerFolders.cacheFolderForAllFull}` - : `#`; - } } diff --git a/src/model/cloud-runner/workflows/custom-workflow.ts b/src/model/cloud-runner/workflows/custom-workflow.ts index 8a98e0cf..590a7c18 100644 --- a/src/model/cloud-runner/workflows/custom-workflow.ts +++ b/src/model/cloud-runner/workflows/custom-workflow.ts @@ -1,37 +1,37 @@ -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 } from '../services/cloud-runner-custom-steps'; -import { CustomStep } from '../services/custom-step'; +import CloudRunnerLogger from '../services/core/cloud-runner-logger'; +import CloudRunnerSecret from '../options/cloud-runner-secret'; +import { CloudRunnerFolders } from '../options/cloud-runner-folders'; +import CloudRunnerEnvironmentVariable from '../options/cloud-runner-environment-variable'; +import { ContainerHookService } from '../services/hooks/container-hook-service'; +import { ContainerHook } from '../services/hooks/container-hook'; import CloudRunner from '../cloud-runner'; export class CustomWorkflow { - public static async runCustomJobFromString( + public static async runContainerJobFromString( buildSteps: string, environmentVariables: CloudRunnerEnvironmentVariable[], secrets: CloudRunnerSecret[], ): Promise { - return await CustomWorkflow.runCustomJob( - CloudRunnerCustomSteps.ParseSteps(buildSteps), + return await CustomWorkflow.runContainerJob( + ContainerHookService.ParseContainerHooks(buildSteps), environmentVariables, secrets, ); } - public static async runCustomJob( - buildSteps: CustomStep[], + public static async runContainerJob( + steps: ContainerHook[], environmentVariables: CloudRunnerEnvironmentVariable[], secrets: CloudRunnerSecret[], ) { 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)}`); // } - for (const step of buildSteps) { + for (const step of steps) { + CloudRunnerLogger.log(`Cloud Runner is running in custom job mode`); output += await CloudRunner.Provider.runTaskInWorkflow( CloudRunner.buildParameters.buildGuid, step.image, diff --git a/src/model/cloud-runner/workflows/workflow-composition-root.ts b/src/model/cloud-runner/workflows/workflow-composition-root.ts index d13cc06a..b47d9b81 100644 --- a/src/model/cloud-runner/workflows/workflow-composition-root.ts +++ b/src/model/cloud-runner/workflows/workflow-composition-root.ts @@ -1,20 +1,24 @@ -import { CloudRunnerStepState } from '../cloud-runner-step-state'; +import { CloudRunnerStepParameters } from '../options/cloud-runner-step-parameters'; 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 CloudRunnerOptions from '../options/cloud-runner-options'; import { AsyncWorkflow } from './async-workflow'; export class WorkflowCompositionRoot implements WorkflowInterface { - async run(cloudRunnerStepState: CloudRunnerStepState) { + async run(cloudRunnerStepState: CloudRunnerStepParameters) { try { - if (CloudRunnerOptions.asyncCloudRunner) { + if ( + CloudRunnerOptions.asyncCloudRunner && + !CloudRunner.isCloudRunnerAsyncEnvironment && + !CloudRunner.isCloudRunnerEnvironment + ) { return await AsyncWorkflow.runAsyncWorkflow(cloudRunnerStepState.environment, cloudRunnerStepState.secrets); } if (CloudRunner.buildParameters.customJob !== '') { - return await CustomWorkflow.runCustomJobFromString( + return await CustomWorkflow.runContainerJobFromString( CloudRunner.buildParameters.customJob, cloudRunnerStepState.environment, cloudRunnerStepState.secrets, @@ -22,7 +26,7 @@ export class WorkflowCompositionRoot implements WorkflowInterface { } return await new BuildAutomationWorkflow().run( - new CloudRunnerStepState( + new CloudRunnerStepParameters( cloudRunnerStepState.image.toString(), cloudRunnerStepState.environment, cloudRunnerStepState.secrets, diff --git a/src/model/cloud-runner/workflows/workflow-interface.ts b/src/model/cloud-runner/workflows/workflow-interface.ts index fe60f5eb..f82b76db 100644 --- a/src/model/cloud-runner/workflows/workflow-interface.ts +++ b/src/model/cloud-runner/workflows/workflow-interface.ts @@ -1,8 +1,8 @@ -import { CloudRunnerStepState } from '../cloud-runner-step-state'; +import { CloudRunnerStepParameters } from '../options/cloud-runner-step-parameters'; export interface WorkflowInterface { run( // eslint-disable-next-line no-unused-vars - cloudRunnerStepState: CloudRunnerStepState, + cloudRunnerStepState: CloudRunnerStepParameters, ): Promise; } diff --git a/src/model/docker.ts b/src/model/docker.ts index 60d32e5c..9b438a0f 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -15,6 +15,7 @@ class Docker { // eslint-disable-next-line unicorn/no-useless-undefined options: ExecOptions | undefined = undefined, entrypointBash: boolean = false, + errorWhenMissingUnityBuildResults: boolean = true, ) { let runCommand = ''; switch (process.platform) { @@ -26,9 +27,9 @@ class Docker { } if (options) { options.silent = silent; - await execWithErrorCheck(runCommand, undefined, options); + await execWithErrorCheck(runCommand, undefined, options, errorWhenMissingUnityBuildResults); } else { - await execWithErrorCheck(runCommand, undefined, { silent }); + await execWithErrorCheck(runCommand, undefined, { silent }, errorWhenMissingUnityBuildResults); } } diff --git a/src/model/exec-with-error-check.ts b/src/model/exec-with-error-check.ts index 529a2d7a..3240a07d 100644 --- a/src/model/exec-with-error-check.ts +++ b/src/model/exec-with-error-check.ts @@ -4,9 +4,14 @@ export async function execWithErrorCheck( commandLine: string, arguments_?: string[], options?: ExecOptions, + errorWhenMissingUnityBuildResults: boolean = true, ): Promise { const result = await getExecOutput(commandLine, arguments_, options); + if (!errorWhenMissingUnityBuildResults) { + return result.exitCode; + } + // Check for errors in the Build Results section const match = result.stdout.match(/^#\s*Build results\s*#(.*)^Size:/ms); diff --git a/src/model/github.ts b/src/model/github.ts index 4f2166b3..fe9795c1 100644 --- a/src/model/github.ts +++ b/src/model/github.ts @@ -1,6 +1,6 @@ -import CloudRunnerLogger from './cloud-runner/services/cloud-runner-logger'; +import CloudRunnerLogger from './cloud-runner/services/core/cloud-runner-logger'; import CloudRunner from './cloud-runner/cloud-runner'; -import CloudRunnerOptions from './cloud-runner/cloud-runner-options'; +import CloudRunnerOptions from './cloud-runner/options/cloud-runner-options'; import * as core from '@actions/core'; import { Octokit } from '@octokit/core'; @@ -10,6 +10,7 @@ class GitHub { private static longDescriptionContent: string = ``; private static startedDate: string; private static endedDate: string; + static result: string = ``; private static get octokitDefaultToken() { return new Octokit({ auth: process.env.GITHUB_TOKEN, @@ -33,7 +34,7 @@ class GitHub { } private static get checkRunId() { - return CloudRunner.githubCheckId; + return CloudRunner.buildParameters.githubCheckId; } private static get owner() { @@ -45,13 +46,12 @@ class GitHub { } public static async createGitHubCheck(summary: string) { - if (!CloudRunnerOptions.githubChecks) { + if (!CloudRunner.buildParameters.githubChecks) { return ``; } GitHub.startedDate = new Date().toISOString(); - CloudRunnerLogger.log(`POST /repos/${GitHub.owner}/${GitHub.repo}/check-runs`); - + CloudRunnerLogger.log(`Creating inital github check`); const data = { owner: GitHub.owner, repo: GitHub.repo, @@ -78,20 +78,27 @@ class GitHub { }; const result = await GitHub.createGitHubCheckRequest(data); - return result.data.id; + return result.data.id.toString(); } public static async updateGitHubCheck( longDescription: string, - summary: any, + summary: string, result = `neutral`, status = `in_progress`, ) { - if (!CloudRunnerOptions.githubChecks) { + if (`${CloudRunner.buildParameters.githubChecks}` !== `true`) { return; } + CloudRunnerLogger.log( + `githubChecks: ${CloudRunner.buildParameters.githubChecks} checkRunId: ${GitHub.checkRunId} sha: ${GitHub.sha} async: ${CloudRunner.isCloudRunnerAsyncEnvironment}`, + ); GitHub.longDescriptionContent += `\n${longDescription}`; - + if (GitHub.result !== `success` && GitHub.result !== `failure`) { + GitHub.result = result; + } else { + result = GitHub.result; + } const data: any = { owner: GitHub.owner, repo: GitHub.repo, @@ -120,12 +127,9 @@ class GitHub { data.conclusion = result; } - if (await CloudRunnerOptions.asyncCloudRunner) { - await GitHub.runUpdateAsyncChecksWorkflow(data, `update`); - - return; - } - await GitHub.updateGitHubCheckRequest(data); + await (CloudRunner.isCloudRunnerAsyncEnvironment + ? GitHub.runUpdateAsyncChecksWorkflow(data, `update`) + : GitHub.updateGitHubCheckRequest(data)); } public static async updateGitHubCheckRequest(data: any) { @@ -140,18 +144,16 @@ class GitHub { 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 workflowsResult = await GitHub.octokitPAT.request(`GET /repos/{owner}/{repo}/actions/workflows`, { + owner: GitHub.owner, + repo: GitHub.repo, + }); const workflows = workflowsResult.data.workflows; + CloudRunnerLogger.log(`Got ${workflows.length} workflows`); let selectedId = ``; for (let index = 0; index < workflowsResult.data.total_count; index++) { if (workflows[index].name === GitHub.asyncChecksApiWorkflowName) { - selectedId = workflows[index].id; + selectedId = workflows[index].id.toString(); } } if (selectedId === ``) { @@ -169,6 +171,41 @@ class GitHub { }, }); } + + static async triggerWorkflowOnComplete(triggerWorkflowOnComplete: string[]) { + const isLocalAsync = CloudRunner.buildParameters.asyncWorkflow && !CloudRunner.isCloudRunnerAsyncEnvironment; + if (isLocalAsync) { + return; + } + const workflowsResult = await GitHub.octokitPAT.request(`GET /repos/{owner}/{repo}/actions/workflows`, { + owner: GitHub.owner, + repo: GitHub.repo, + }); + const workflows = workflowsResult.data.workflows; + CloudRunnerLogger.log(`Got ${workflows.length} workflows`); + for (const element of triggerWorkflowOnComplete) { + let selectedId = ``; + for (let index = 0; index < workflowsResult.data.total_count; index++) { + if (workflows[index].name === element) { + selectedId = workflows[index].id.toString(); + } + } + 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: { + buildGuid: CloudRunner.buildParameters.buildGuid, + }, + }); + } + } } export default GitHub; diff --git a/src/model/image-environment-factory.ts b/src/model/image-environment-factory.ts index c5d14a68..ed780e48 100644 --- a/src/model/image-environment-factory.ts +++ b/src/model/image-environment-factory.ts @@ -65,7 +65,7 @@ class ImageEnvironmentFactory { { name: 'RUNNER_TEMP', value: process.env.RUNNER_TEMP }, { name: 'RUNNER_WORKSPACE', value: process.env.RUNNER_WORKSPACE }, ]; - if (parameters.cloudRunnerCluster === 'local-docker') { + if (parameters.providerStrategy === 'local-docker') { for (const element of additionalVariables) { if ( environmentVariables.find( diff --git a/src/model/input-readers/generic-input-reader.ts b/src/model/input-readers/generic-input-reader.ts index 90a8caad..25b2f798 100644 --- a/src/model/input-readers/generic-input-reader.ts +++ b/src/model/input-readers/generic-input-reader.ts @@ -1,9 +1,9 @@ -import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system'; -import CloudRunnerOptions from '../cloud-runner/cloud-runner-options'; +import { CloudRunnerSystem } from '../cloud-runner/services/core/cloud-runner-system'; +import CloudRunnerOptions from '../cloud-runner/options/cloud-runner-options'; export class GenericInputReader { public static async Run(command: string) { - if (CloudRunnerOptions.cloudRunnerCluster === 'local') { + if (CloudRunnerOptions.providerStrategy === 'local') { return ''; } diff --git a/src/model/input-readers/git-repo.test.ts b/src/model/input-readers/git-repo.test.ts index 30ed2028..72a7f8f0 100644 --- a/src/model/input-readers/git-repo.test.ts +++ b/src/model/input-readers/git-repo.test.ts @@ -1,6 +1,6 @@ import { GitRepoReader } from './git-repo'; -import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system'; -import CloudRunnerOptions from '../cloud-runner/cloud-runner-options'; +import { CloudRunnerSystem } from '../cloud-runner/services/core/cloud-runner-system'; +import CloudRunnerOptions from '../cloud-runner/options/cloud-runner-options'; describe(`git repo tests`, () => { it(`Branch value parsed from CLI to not contain illegal characters`, async () => { @@ -11,14 +11,14 @@ describe(`git repo tests`, () => { it(`returns valid branch name when using https`, async () => { const mockValue = 'https://github.com/example/example.git'; await jest.spyOn(CloudRunnerSystem, 'Run').mockReturnValue(Promise.resolve(mockValue)); - await jest.spyOn(CloudRunnerOptions, 'cloudRunnerCluster', 'get').mockReturnValue('not-local'); + await jest.spyOn(CloudRunnerOptions, 'providerStrategy', 'get').mockReturnValue('not-local'); expect(await GitRepoReader.GetRemote()).toEqual(`example/example`); }); it(`returns valid branch name when using ssh`, async () => { const mockValue = 'git@github.com:example/example.git'; await jest.spyOn(CloudRunnerSystem, 'Run').mockReturnValue(Promise.resolve(mockValue)); - await jest.spyOn(CloudRunnerOptions, 'cloudRunnerCluster', 'get').mockReturnValue('not-local'); + await jest.spyOn(CloudRunnerOptions, 'providerStrategy', 'get').mockReturnValue('not-local'); expect(await GitRepoReader.GetRemote()).toEqual(`example/example`); }); }); diff --git a/src/model/input-readers/git-repo.ts b/src/model/input-readers/git-repo.ts index 3d8307d7..2c06b37c 100644 --- a/src/model/input-readers/git-repo.ts +++ b/src/model/input-readers/git-repo.ts @@ -1,13 +1,13 @@ import { assert } from 'node:console'; import fs from 'node:fs'; -import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system'; -import CloudRunnerLogger from '../cloud-runner/services/cloud-runner-logger'; -import CloudRunnerOptions from '../cloud-runner/cloud-runner-options'; +import { CloudRunnerSystem } from '../cloud-runner/services/core/cloud-runner-system'; +import CloudRunnerLogger from '../cloud-runner/services/core/cloud-runner-logger'; +import CloudRunnerOptions from '../cloud-runner/options/cloud-runner-options'; import Input from '../input'; export class GitRepoReader { public static async GetRemote() { - if (CloudRunnerOptions.cloudRunnerCluster === 'local') { + if (CloudRunnerOptions.providerStrategy === 'local') { return ''; } assert(fs.existsSync(`.git`)); @@ -22,7 +22,7 @@ export class GitRepoReader { } public static async GetBranch() { - if (CloudRunnerOptions.cloudRunnerCluster === 'local') { + if (CloudRunnerOptions.providerStrategy === 'local') { return ''; } assert(fs.existsSync(`.git`)); diff --git a/src/model/input-readers/github-cli.ts b/src/model/input-readers/github-cli.ts index 5c832be8..6f177d0d 100644 --- a/src/model/input-readers/github-cli.ts +++ b/src/model/input-readers/github-cli.ts @@ -1,10 +1,10 @@ -import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system'; +import { CloudRunnerSystem } from '../cloud-runner/services/core/cloud-runner-system'; import * as core from '@actions/core'; -import CloudRunnerOptions from '../cloud-runner/cloud-runner-options'; +import CloudRunnerOptions from '../cloud-runner/options/cloud-runner-options'; export class GithubCliReader { static async GetGitHubAuthToken() { - if (CloudRunnerOptions.cloudRunnerCluster === 'local') { + if (CloudRunnerOptions.providerStrategy === 'local') { return ''; } try { diff --git a/src/model/input-readers/test-license-reader.ts b/src/model/input-readers/test-license-reader.ts index 13819495..785d0bae 100644 --- a/src/model/input-readers/test-license-reader.ts +++ b/src/model/input-readers/test-license-reader.ts @@ -1,10 +1,10 @@ import path from 'node:path'; import fs from 'node:fs'; import YAML from 'yaml'; -import CloudRunnerOptions from '../cloud-runner/cloud-runner-options'; +import CloudRunnerOptions from '../cloud-runner/options/cloud-runner-options'; export function ReadLicense(): string { - if (CloudRunnerOptions.cloudRunnerCluster === 'local') { + if (CloudRunnerOptions.providerStrategy === 'local') { return ''; } const pipelineFile = path.join(__dirname, `.github`, `workflows`, `cloud-runner-k8s-pipeline.yml`); diff --git a/src/model/input.ts b/src/model/input.ts index f3169d61..45f39c76 100644 --- a/src/model/input.ts +++ b/src/model/input.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { Cli } from './cli/cli'; -import CloudRunnerQueryOverride from './cloud-runner/services/cloud-runner-query-override'; +import CloudRunnerQueryOverride from './cloud-runner/options/cloud-runner-query-override'; import Platform from './platform'; import GitHub from './github'; diff --git a/yarn.lock b/yarn.lock index 54729da1..3b943522 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5946,6 +5946,11 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"