mirror of
https://github.com/game-ci/unity-builder.git
synced 2025-07-04 12:25:19 -04:00
Cloud Runner Develop - 1.0 R.C 1 (#437)
Release Candidate changeset 1 - For 1.0 Cloud Runner
This commit is contained in:
parent
96555a0945
commit
4cca069ebb
29
.github/workflows/build-tests.yml
vendored
29
.github/workflows/build-tests.yml
vendored
@ -7,7 +7,30 @@ on:
|
||||
- '.github/**'
|
||||
|
||||
env:
|
||||
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\" Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature Value=\"62\"/>\n </Features>\n <DeveloperData Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\" Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
|
||||
UNITY_LICENSE:
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
|
||||
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
|
||||
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
|
||||
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
|
||||
Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash
|
||||
Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature
|
||||
Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature
|
||||
Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature
|
||||
Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature
|
||||
Value=\"62\"/>\n </Features>\n <DeveloperData
|
||||
Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked
|
||||
Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate
|
||||
Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate
|
||||
Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion
|
||||
Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement
|
||||
Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\"
|
||||
ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\"
|
||||
Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature
|
||||
xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod
|
||||
Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod
|
||||
Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform
|
||||
Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod
|
||||
Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
|
||||
|
||||
jobs:
|
||||
buildForAllPlatformsUbuntu:
|
||||
@ -16,6 +39,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
# - local-docker
|
||||
- local
|
||||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
@ -62,6 +88,7 @@ jobs:
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
|
||||
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
|
||||
|
||||
###########################
|
||||
# Upload #
|
||||
|
2
.github/workflows/cleanup.yml
vendored
2
.github/workflows/cleanup.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: eu-west-2
|
||||
- run: yarn run cli -m aws-list-all
|
||||
- run: yarn run cli -m list-resources
|
||||
env:
|
||||
AWS_REGION: eu-west-2
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
|
96
.github/workflows/cloud-runner-local-pipeline.yml
vendored
Normal file
96
.github/workflows/cloud-runner-local-pipeline.yml
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
name: Cloud Runner Local
|
||||
|
||||
on:
|
||||
push: { branches: ['!cloud-runner-develop', '!cloud-runner-preview', '!main'] }
|
||||
# push: { branches: [main] }
|
||||
# pull_request:
|
||||
# paths-ignore:
|
||||
# - '.github/**'
|
||||
|
||||
jobs:
|
||||
integrationTests:
|
||||
name: Integration Tests
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
- local-docker
|
||||
targetPlatform:
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
# steps
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
- run: yarn
|
||||
- run: yarn run cli --help
|
||||
- run: yarn run test-i --detectOpenHandles --forceExit --runInBand
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: ${{ matrix.projectPath }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: ${{ matrix.targetPlatform }}
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
buildTests:
|
||||
name: Build Tests
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
- local-docker
|
||||
targetPlatform:
|
||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- WebGL # WebGL.
|
||||
- iOS # Build an iOS player.
|
||||
- Android # Build an Android .apk.
|
||||
# - StandaloneWindows # Build a Windows standalone.
|
||||
# - WSAPlayer # Build an Windows Store Apps player.
|
||||
# - PS4 # Build a PS4 Standalone.
|
||||
# - XboxOne # Build a Xbox One Standalone.
|
||||
# - tvOS # Build to Apple's tvOS platform.
|
||||
# - Switch # Build a Nintendo Switch player
|
||||
# steps
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
- uses: ./
|
||||
id: unity-build
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CLOUD_RUNNER_BRANCH: ${{ github.ref }}
|
||||
CLOUD_RUNNER_DEBUG: true
|
||||
CLOUD_RUNNER_DEBUG_TREE: true
|
||||
DEBUG: true
|
||||
PROJECT_PATH: test-project
|
||||
UNITY_VERSION: 2019.3.15f1
|
||||
USE_IL2CPP: false
|
||||
with:
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
gitPrivateToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
|
||||
- run: |
|
||||
mv ./cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4
|
||||
ls
|
||||
- run: yarn run cli -m list-resources
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: AWS Build (${{ matrix.targetPlatform }})
|
||||
path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4
|
||||
retention-days: 14
|
285
.github/workflows/cloud-runner-pipeline.yml
vendored
285
.github/workflows/cloud-runner-pipeline.yml
vendored
@ -1,11 +1,7 @@
|
||||
name: Cloud Runner
|
||||
name: Cloud Runner CI Pipeline
|
||||
|
||||
on:
|
||||
push: { branches: [cloud-runner-develop, main] }
|
||||
# push: { branches: [main] }
|
||||
# pull_request:
|
||||
# paths-ignore:
|
||||
# - '.github/**'
|
||||
push: { branches: [cloud-runner-develop, cloud-runner-preview] }
|
||||
|
||||
env:
|
||||
GKE_ZONE: 'us-central1'
|
||||
@ -21,43 +17,38 @@ env:
|
||||
AWS_DEFAULT_REGION: eu-west-2
|
||||
AWS_BASE_STACK_NAME: game-ci-github-pipelines
|
||||
CLOUD_RUNNER_BRANCH: ${{ github.ref }}
|
||||
CLOUD_RUNNER_TESTS: true
|
||||
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
|
||||
|
||||
jobs:
|
||||
awsBuild:
|
||||
name: AWS Fargate Build
|
||||
if: github.event.pull_request.draft == false
|
||||
integrationTests:
|
||||
name: Integration Tests
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
# - 2019.2.11f1
|
||||
- 2019.3.15f1
|
||||
targetPlatform:
|
||||
#- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- WebGL # WebGL.
|
||||
#- iOS # Build an iOS player.
|
||||
#- Android # Build an Android .apk.
|
||||
# - StandaloneWindows # Build a Windows standalone.
|
||||
# - WSAPlayer # Build an Windows Store Apps player.
|
||||
# - PS4 # Build a PS4 Standalone.
|
||||
# - XboxOne # Build a Xbox One Standalone.
|
||||
# - tvOS # Build to Apple's tvOS platform.
|
||||
# - Switch # Build a Nintendo Switch player
|
||||
# steps
|
||||
cloudRunnerCluster:
|
||||
- aws
|
||||
- local-docker
|
||||
- k8s
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v2
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
with:
|
||||
lfs: true
|
||||
- uses: google-github-actions/setup-gcloud@v0
|
||||
with:
|
||||
version: '288.0.0'
|
||||
service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}
|
||||
service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||
- name: Get GKE cluster credentials
|
||||
run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
@ -66,81 +57,117 @@ jobs:
|
||||
aws-region: eu-west-2
|
||||
- run: yarn
|
||||
- run: yarn run cli --help
|
||||
- run: yarn run test "caching"
|
||||
- run: yarn run test-i-aws
|
||||
- run: yarn run test "cloud-runner-run-twice-retaining" --detectOpenHandles --forceExit --runInBand
|
||||
if: matrix.CloudRunnerCluster == 'aws' || matrix.CloudRunnerCluster == 'k8s'
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: ${{ matrix.projectPath }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: ${{ matrix.targetPlatform }}
|
||||
PROJECT_PATH: test-project
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
- uses: ./
|
||||
id: aws-fargate-unity-build
|
||||
timeout-minutes: 25
|
||||
with:
|
||||
cloudRunnerCluster: aws
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
- run: yarn run test-i --detectOpenHandles --forceExit --runInBand
|
||||
if: matrix.CloudRunnerCluster == 'local-docker'
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: test-project
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
postBuildSteps: |
|
||||
- name: upload
|
||||
image: amazon/aws-cli
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 ls
|
||||
aws s3 ls game-ci-test-storage
|
||||
ls /data/cache/$CACHE_KEY
|
||||
ls /data/cache/$CACHE_KEY/build
|
||||
aws s3 cp /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar s3://game-ci-test-storage/$CACHE_KEY/build-$BUILD_GUID.tar
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: awsDefaultRegion
|
||||
value: eu-west-2
|
||||
- run: |
|
||||
aws s3 cp s3://game-ci-test-storage/${{ steps.aws-fargate-unity-build.outputs.CACHE_KEY }}/build-${{ steps.aws-fargate-unity-build.outputs.BUILD_GUID }}.tar build-${{ steps.aws-fargate-unity-build.outputs.BUILD_GUID }}.tar
|
||||
ls
|
||||
- run: yarn run cli -m aws-garbage-collect
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
# download from cloud storage
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: AWS Build (${{ matrix.targetPlatform }})
|
||||
path: build-${{ steps.aws-fargate-unity-build.outputs.BUILD_GUID }}.tar
|
||||
retention-days: 14
|
||||
k8sBuilds:
|
||||
name: K8s (GKE Autopilot) build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
|
||||
buildTargetTests:
|
||||
name: Build Tests - Targets
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
unityVersion:
|
||||
# - 2019.2.11f1
|
||||
- 2019.3.15f1
|
||||
cloudRunnerCluster:
|
||||
#- aws
|
||||
- local-docker
|
||||
#- k8s
|
||||
targetPlatform:
|
||||
# - StandaloneWindows64
|
||||
- StandaloneLinux64
|
||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
# - StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- WebGL # WebGL.
|
||||
- iOS # Build an iOS player.
|
||||
- Android # Build an Android .apk.
|
||||
steps:
|
||||
###########################
|
||||
# Checkout #
|
||||
###########################
|
||||
- uses: actions/checkout@v2
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- uses: google-github-actions/setup-gcloud@v0
|
||||
with:
|
||||
version: '288.0.0'
|
||||
service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}
|
||||
service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||
- name: Get GKE cluster credentials
|
||||
run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-2
|
||||
- run: yarn
|
||||
- uses: ./
|
||||
id: unity-build
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
projectPath: test-project
|
||||
gitPrivateToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
|
||||
customStepFiles: aws-s3-upload-build,aws-s3-pull-cache,aws-s3-upload-cache
|
||||
- run: |
|
||||
aws s3 cp s3://game-ci-test-storage/cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4
|
||||
ls
|
||||
- run: yarn run cli -m list-resources
|
||||
env:
|
||||
cloudRunnerTests: true
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }})
|
||||
path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4
|
||||
retention-days: 14
|
||||
buildTests:
|
||||
name: Build Tests - Providers
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
- aws
|
||||
- local-docker
|
||||
- k8s
|
||||
targetPlatform:
|
||||
#- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
#- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
#- WebGL # WebGL.
|
||||
#- iOS # Build an iOS player.
|
||||
#- Android # Build an Android .apk.
|
||||
# steps
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Setup #
|
||||
###########################
|
||||
- uses: google-github-actions/setup-gcloud@v0
|
||||
with:
|
||||
version: '288.0.0'
|
||||
@ -149,70 +176,36 @@ jobs:
|
||||
- name: Get GKE cluster credentials
|
||||
run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT
|
||||
|
||||
###########################
|
||||
# Cloud Runner Test Suite #
|
||||
###########################
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-2
|
||||
- run: yarn
|
||||
- run: yarn run cli --help
|
||||
- run: yarn run test "caching"
|
||||
- name: Cloud Runner Test Suite
|
||||
run: yarn run test-i-k8s --detectOpenHandles --forceExit
|
||||
- uses: ./
|
||||
id: unity-build
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: ${{ matrix.projectPath }}
|
||||
TARGET_PLATFORM: ${{ matrix.targetPlatform }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
KUBE_CONFIG: ${{ steps.read-base64.outputs.base64 }}
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
with:
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
|
||||
###########################
|
||||
# Cloud Runner Build Test #
|
||||
###########################
|
||||
- name: Cloud Runner Build Test
|
||||
uses: ./
|
||||
id: k8s-unity-build
|
||||
timeout-minutes: 30
|
||||
with:
|
||||
cloudRunnerCluster: k8s
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
kubeConfig: ${{ steps.read-base64.outputs.base64 }}
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
projectPath: test-project
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
versioning: None
|
||||
postBuildSteps: |
|
||||
- name: upload
|
||||
image: amazon/aws-cli
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 ls
|
||||
aws s3 ls game-ci-test-storage
|
||||
ls /data/cache/$CACHE_KEY
|
||||
aws s3 cp /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar s3://game-ci-test-storage/$CACHE_KEY/build-$BUILD_GUID.tar
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: awsDefaultRegion
|
||||
value: eu-west-2
|
||||
gitPrivateToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
|
||||
customStepFiles: aws-s3-upload-build,aws-s3-pull-cache,aws-s3-upload-cache
|
||||
- run: |
|
||||
aws s3 cp s3://game-ci-test-storage/${{ steps.k8s-unity-build.outputs.CACHE_KEY }}/build-${{ steps.k8s-unity-build.outputs.BUILD_GUID }}.tar build-${{ steps.k8s-unity-build.outputs.BUILD_GUID }}.tar
|
||||
aws s3 cp s3://game-ci-test-storage/cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4
|
||||
ls
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
# download from cloud storage
|
||||
- run: yarn run cli -m list-resources
|
||||
if: always()
|
||||
env:
|
||||
cloudRunnerTests: true
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: K8s Build (${{ matrix.targetPlatform }})
|
||||
path: build-${{ steps.k8s-unity-build.outputs.BUILD_GUID }}.tar
|
||||
name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }})
|
||||
path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4
|
||||
retention-days: 14
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,4 +1,5 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"god.tsconfig": "./tsconfig.json",
|
||||
"yaml.customTags": [
|
||||
"!And",
|
||||
|
85
action.yml
85
action.yml
@ -9,7 +9,8 @@ inputs:
|
||||
unityVersion:
|
||||
required: false
|
||||
default: 'auto'
|
||||
description: 'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||
description:
|
||||
'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||
customImage:
|
||||
required: false
|
||||
default: ''
|
||||
@ -81,91 +82,107 @@ inputs:
|
||||
gitPrivateToken:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Github private token to pull from github'
|
||||
description: '[CloudRunner] Github private token to pull from github'
|
||||
chownFilesTo:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||
description:
|
||||
'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||
allowDirtyBuild:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Allows the branch of the build to be dirty, and still generate the build.'
|
||||
description: '[CloudRunner] Allows the branch of the build to be dirty, and still generate the build.'
|
||||
postBuildSteps:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'run a post build job in yaml format with the keys image, secrets (name, value object array), command string'
|
||||
description:
|
||||
'[CloudRunner] run a post build job in yaml format with the keys image, secrets (name, value object array),
|
||||
command string'
|
||||
preBuildSteps:
|
||||
required: false
|
||||
default: ''
|
||||
description: '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)'
|
||||
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:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[CloudRunner] Specify the names (by file name) of custom steps to run before or after cloud runner jobs, must
|
||||
match a yaml step file inside your repo in the folder .game-ci/steps/'
|
||||
customHookFiles:
|
||||
required: false
|
||||
default: ''
|
||||
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:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Specify custom commands and trigger hooks (injects commands into jobs)'
|
||||
description: '[CloudRunner] Specify custom commands and trigger hooks (injects commands into jobs)'
|
||||
customJob:
|
||||
required: false
|
||||
default: ''
|
||||
description: '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)'
|
||||
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:
|
||||
default: 'game-ci'
|
||||
required: false
|
||||
description: 'The Cloud Formation stack name that must be setup before using this option.'
|
||||
description: '[CloudRunner] The Cloud Formation stack name that must be setup before using this option.'
|
||||
cloudRunnerCluster:
|
||||
default: 'local'
|
||||
required: false
|
||||
description: 'Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must be configured.'
|
||||
description:
|
||||
'[CloudRunner] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
|
||||
be configured.'
|
||||
cloudRunnerCpu:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Amount of CPU time to assign the remote build container'
|
||||
description: '[CloudRunner] Amount of CPU time to assign the remote build container'
|
||||
cloudRunnerMemory:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Amount of memory to assign the remote build container'
|
||||
cachePushOverrideCommand:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'A command run every time a file is pushed to cache, formatted with input file path and remote cache path'
|
||||
cachePullOverrideCommand:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'A command run every time before a file is being pulled from cache, formatted with request cache file and destination path'
|
||||
description: '[CloudRunner] Amount of memory to assign the remote build container'
|
||||
readInputFromOverrideList:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Comma separated list of input value names to read from "input override command"'
|
||||
description: '[CloudRunner] Comma separated list of input value names to read from "input override command"'
|
||||
readInputOverrideCommand:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Extend game ci by specifying a command to execute to pull input from external source e.g cloud provider secret managers'
|
||||
description:
|
||||
'[CloudRunner] Extend game ci by specifying a command to execute to pull input from external source e.g cloud
|
||||
provider secret managers'
|
||||
kubeConfig:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until completion.'
|
||||
description:
|
||||
'[CloudRunner] Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until
|
||||
completion.'
|
||||
kubeVolume:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Supply a Persistent Volume Claim name to use for the Unity build.'
|
||||
description: '[CloudRunner] Supply a Persistent Volume Claim name to use for the Unity build.'
|
||||
kubeStorageClass:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Kubernetes storage class to use for cloud runner jobs, leave empty to install rook cluster.'
|
||||
description:
|
||||
'[CloudRunner] Kubernetes storage class to use for cloud runner jobs, leave empty to install rook cluster.'
|
||||
kubeVolumeSize:
|
||||
default: '5Gi'
|
||||
required: false
|
||||
description: 'Amount of disc space to assign the Kubernetes Persistent Volume'
|
||||
description: '[CloudRunner] Amount of disc space to assign the Kubernetes Persistent Volume'
|
||||
cacheKey:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Cache key to indicate bucket for cache'
|
||||
checkDependencyHealthOverride:
|
||||
default: ''
|
||||
description: '[CloudRunner] Cache key to indicate bucket for cache'
|
||||
watchToEnd:
|
||||
default: 'true'
|
||||
required: false
|
||||
description: 'Use to specify a way to check depdency services health to enable resilient self-starting jobs'
|
||||
startDependenciesOverride:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Use to specify a way to start depdency services health to enable resilient self-starting jobs'
|
||||
description:
|
||||
'[CloudRunner] Whether or not to watch the build to the end. Can be used for especially long running jobs e.g
|
||||
imports or self-hosted ephemeral runners.'
|
||||
outputs:
|
||||
volume:
|
||||
description: 'The Persistent Volume (PV) where the build artifacts have been stored by Kubernetes'
|
||||
|
BIN
dist/index.js
generated
vendored
BIN
dist/index.js
generated
vendored
Binary file not shown.
BIN
dist/index.js.map
generated
vendored
BIN
dist/index.js.map
generated
vendored
Binary file not shown.
3
game-ci/hooks/my-test-hook-post-build.yaml
Normal file
3
game-ci/hooks/my-test-hook-post-build.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
hook: after-build
|
||||
commands: |
|
||||
echo "after-build hook test!"
|
3
game-ci/hooks/my-test-hook-pre-build.yaml
Normal file
3
game-ci/hooks/my-test-hook-pre-build.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
hook: before-build
|
||||
commands: |
|
||||
echo "before-build hook test!!"
|
3
game-ci/steps/my-test-step-post-build.yaml
Normal file
3
game-ci/steps/my-test-step-post-build.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
hook: after
|
||||
commands: |
|
||||
echo "after-build step test!"
|
3
game-ci/steps/my-test-step-pre-build.yaml
Normal file
3
game-ci/steps/my-test-step-pre-build.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
hook: before
|
||||
commands: |
|
||||
echo "before-build step test!"
|
@ -18,6 +18,7 @@ module.exports = {
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
},
|
||||
autoRun: false,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
verbose: true,
|
||||
|
10
package.json
10
package.json
@ -12,15 +12,15 @@
|
||||
"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=\"gcloud secrets versions access 1 --secret=\"{0}\"\" populateOverride=true readInputFromOverrideList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"cloud runner\"",
|
||||
"gcp-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"gcloud secrets versions access 1 --secret=\"{0}\"\" 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 secretsmanager get-secret-value --secret-id {0}\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
|
||||
"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-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",
|
||||
"test-cli": "cross-env cloudRunnerTests=true yarn ts-node src/index.ts -m cli --projectPath test-project",
|
||||
"test": "jest",
|
||||
"test-i": "yarn run test-i-aws && yarn run test-i-k8s",
|
||||
"test-i-f": "yarn run test-i-aws && yarn run test-i-k8s && yarn run cli-k8s && yarn run cli-aws",
|
||||
"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\""
|
||||
},
|
||||
|
@ -9,6 +9,8 @@ import Versioning from './versioning';
|
||||
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';
|
||||
|
||||
class BuildParameters {
|
||||
public editorVersion!: string;
|
||||
@ -45,12 +47,8 @@ class BuildParameters {
|
||||
public kubeStorageClass!: string;
|
||||
public chownFilesTo!: string;
|
||||
public customJobHooks!: string;
|
||||
public cachePushOverrideCommand!: string;
|
||||
public cachePullOverrideCommand!: string;
|
||||
public readInputFromOverrideList!: string;
|
||||
public readInputOverrideCommand!: string;
|
||||
public checkDependencyHealthOverride!: string;
|
||||
public startDependenciesOverride!: string;
|
||||
public cacheKey!: string;
|
||||
|
||||
public postBuildSteps!: string;
|
||||
@ -63,9 +61,15 @@ class BuildParameters {
|
||||
public logId!: string;
|
||||
public buildGuid!: string;
|
||||
public cloudRunnerBranch!: string;
|
||||
public cloudRunnerIntegrationTests!: boolean;
|
||||
public cloudRunnerDebug!: boolean;
|
||||
public cloudRunnerBuilderPlatform!: string | undefined;
|
||||
public isCliMode!: boolean;
|
||||
public retainWorkspace!: boolean;
|
||||
public maxRetainedWorkspaces!: number;
|
||||
public useSharedLargePackages!: boolean;
|
||||
public useLz4Compression!: boolean;
|
||||
public garbageCollectionMaxAge!: number;
|
||||
public constantGarbageCollection!: boolean;
|
||||
|
||||
static async create(): Promise<BuildParameters> {
|
||||
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle);
|
||||
@ -78,7 +82,7 @@ class BuildParameters {
|
||||
// ---
|
||||
let unitySerial = '';
|
||||
if (Input.unityLicensingServer === '') {
|
||||
if (!process.env.UNITY_SERIAL && Input.githubInputEnabled) {
|
||||
if (!process.env.UNITY_SERIAL && GitHub.githubInputEnabled) {
|
||||
// No serial was present, so it is a personal license that we need to convert
|
||||
if (!process.env.UNITY_LICENSE) {
|
||||
throw new Error(`Missing Unity License File and no Serial was found. If this
|
||||
@ -117,36 +121,38 @@ class BuildParameters {
|
||||
sshAgent: Input.sshAgent,
|
||||
gitPrivateToken: Input.gitPrivateToken || (await GithubCliReader.GetGitHubAuthToken()),
|
||||
chownFilesTo: Input.chownFilesTo,
|
||||
cloudRunnerCluster: Input.cloudRunnerCluster,
|
||||
cloudRunnerBuilderPlatform: Input.cloudRunnerBuilderPlatform,
|
||||
awsBaseStackName: Input.awsBaseStackName,
|
||||
kubeConfig: Input.kubeConfig,
|
||||
cloudRunnerMemory: Input.cloudRunnerMemory,
|
||||
cloudRunnerCpu: Input.cloudRunnerCpu,
|
||||
kubeVolumeSize: Input.kubeVolumeSize,
|
||||
kubeVolume: Input.kubeVolume,
|
||||
postBuildSteps: Input.postBuildSteps,
|
||||
preBuildSteps: Input.preBuildSteps,
|
||||
customJob: Input.customJob,
|
||||
cloudRunnerCluster: CloudRunnerOptions.cloudRunnerCluster,
|
||||
cloudRunnerBuilderPlatform: CloudRunnerOptions.cloudRunnerBuilderPlatform,
|
||||
awsBaseStackName: CloudRunnerOptions.awsBaseStackName,
|
||||
kubeConfig: CloudRunnerOptions.kubeConfig,
|
||||
cloudRunnerMemory: CloudRunnerOptions.cloudRunnerMemory,
|
||||
cloudRunnerCpu: CloudRunnerOptions.cloudRunnerCpu,
|
||||
kubeVolumeSize: CloudRunnerOptions.kubeVolumeSize,
|
||||
kubeVolume: CloudRunnerOptions.kubeVolume,
|
||||
postBuildSteps: CloudRunnerOptions.postBuildSteps,
|
||||
preBuildSteps: CloudRunnerOptions.preBuildSteps,
|
||||
customJob: CloudRunnerOptions.customJob,
|
||||
runNumber: Input.runNumber,
|
||||
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
|
||||
cloudRunnerBranch: Input.cloudRunnerBranch.split('/').reverse()[0],
|
||||
cloudRunnerIntegrationTests: Input.cloudRunnerTests,
|
||||
cloudRunnerBranch: CloudRunnerOptions.cloudRunnerBranch.split('/').reverse()[0],
|
||||
cloudRunnerDebug: CloudRunnerOptions.cloudRunnerDebug,
|
||||
githubRepo: Input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder',
|
||||
isCliMode: Cli.isCliMode,
|
||||
awsStackName: Input.awsBaseStackName,
|
||||
awsStackName: CloudRunnerOptions.awsBaseStackName,
|
||||
gitSha: Input.gitSha,
|
||||
logId: customAlphabet(CloudRunnerConstants.alphabet, 9)(),
|
||||
buildGuid: CloudRunnerBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform),
|
||||
customJobHooks: Input.customJobHooks(),
|
||||
cachePullOverrideCommand: Input.cachePullOverrideCommand(),
|
||||
cachePushOverrideCommand: Input.cachePushOverrideCommand(),
|
||||
readInputOverrideCommand: Input.readInputOverrideCommand(),
|
||||
readInputFromOverrideList: Input.readInputFromOverrideList(),
|
||||
kubeStorageClass: Input.kubeStorageClass,
|
||||
checkDependencyHealthOverride: Input.checkDependencyHealthOverride,
|
||||
startDependenciesOverride: Input.startDependenciesOverride,
|
||||
cacheKey: Input.cacheKey,
|
||||
customJobHooks: CloudRunnerOptions.customJobHooks(),
|
||||
readInputOverrideCommand: CloudRunnerOptions.readInputOverrideCommand(),
|
||||
readInputFromOverrideList: CloudRunnerOptions.readInputFromOverrideList(),
|
||||
kubeStorageClass: CloudRunnerOptions.kubeStorageClass,
|
||||
cacheKey: CloudRunnerOptions.cacheKey,
|
||||
retainWorkspace: CloudRunnerOptions.retainWorkspaces,
|
||||
useSharedLargePackages: CloudRunnerOptions.useSharedLargePackages,
|
||||
useLz4Compression: CloudRunnerOptions.useLz4Compression,
|
||||
maxRetainedWorkspaces: CloudRunnerOptions.maxRetainedWorkspaces,
|
||||
constantGarbageCollection: CloudRunnerOptions.constantGarbageCollection,
|
||||
garbageCollectionMaxAge: CloudRunnerOptions.garbageCollectionMaxAge,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,14 @@ 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 { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
|
||||
import { AwsCliCommands } from '../cloud-runner/providers/aws/commands/aws-cli-commands';
|
||||
import { Caching } from '../cloud-runner/remote-client/caching';
|
||||
import { LfsHashing } from '../cloud-runner/services/lfs-hashing';
|
||||
import { RemoteClient } from '../cloud-runner/remote-client';
|
||||
import CloudRunnerOptionsReader from '../cloud-runner/services/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';
|
||||
|
||||
export class Cli {
|
||||
public static options;
|
||||
@ -27,13 +31,13 @@ export class Cli {
|
||||
}
|
||||
|
||||
public static InitCliMode() {
|
||||
CliFunctionsRepository.PushCliFunctionSource(AwsCliCommands);
|
||||
CliFunctionsRepository.PushCliFunctionSource(RemoteClient);
|
||||
CliFunctionsRepository.PushCliFunctionSource(Caching);
|
||||
CliFunctionsRepository.PushCliFunctionSource(LfsHashing);
|
||||
CliFunctionsRepository.PushCliFunctionSource(RemoteClient);
|
||||
const program = new Command();
|
||||
program.version('0.0.1');
|
||||
const properties = Object.getOwnPropertyNames(Input);
|
||||
|
||||
const properties = CloudRunnerOptionsReader.GetProperties();
|
||||
const actionYamlReader: ActionYamlReader = new ActionYamlReader();
|
||||
for (const element of properties) {
|
||||
program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element));
|
||||
@ -48,6 +52,7 @@ export class Cli {
|
||||
program.option('--cachePushFrom <cachePushFrom>', 'cache push from source folder');
|
||||
program.option('--cachePushTo <cachePushTo>', 'cache push to caching folder');
|
||||
program.option('--artifactName <artifactName>', 'caching artifact name');
|
||||
program.option('--select <select>', 'select a particular resource');
|
||||
program.parse(process.argv);
|
||||
Cli.options = program.opts();
|
||||
|
||||
@ -55,23 +60,32 @@ export class Cli {
|
||||
}
|
||||
|
||||
static async RunCli(): Promise<void> {
|
||||
Input.githubInputEnabled = false;
|
||||
GitHub.githubInputEnabled = false;
|
||||
if (Cli.options['populateOverride'] === `true`) {
|
||||
await CloudRunnerQueryOverride.PopulateQueryOverrideInput();
|
||||
}
|
||||
if (Cli.options['logInput']) {
|
||||
Cli.logInput();
|
||||
}
|
||||
const results = CliFunctionsRepository.GetCliFunctions(Cli.options.mode);
|
||||
CloudRunnerLogger.log(`Entrypoint: ${results.key}`);
|
||||
Cli.options.versioning = 'None';
|
||||
|
||||
return await results.target[results.propertyKey]();
|
||||
const buildParameter = TaskParameterSerializer.readBuildParameterFromEnvironment();
|
||||
CloudRunnerLogger.log(`Build Params:
|
||||
${JSON.stringify(buildParameter, undefined, 4)}
|
||||
`);
|
||||
CloudRunner.buildParameters = buildParameter;
|
||||
CloudRunner.lockedWorkspace = process.env.LOCKED_WORKSPACE;
|
||||
|
||||
return await results.target[results.propertyKey](Cli.options);
|
||||
}
|
||||
|
||||
@CliFunction(`print-input`, `prints all input`)
|
||||
private static logInput() {
|
||||
core.info(`\n`);
|
||||
core.info(`INPUT:`);
|
||||
const properties = Object.getOwnPropertyNames(Input);
|
||||
const properties = CloudRunnerOptionsReader.GetProperties();
|
||||
for (const element of properties) {
|
||||
if (
|
||||
Input[element] !== undefined &&
|
||||
@ -87,11 +101,91 @@ export class Cli {
|
||||
core.info(`\n`);
|
||||
}
|
||||
|
||||
@CliFunction(`cli`, `runs a cloud runner build`)
|
||||
@CliFunction(`cli-build`, `runs a cloud runner build`)
|
||||
public static async CLIBuild(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
return await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
}
|
||||
|
||||
@CliFunction(`garbage-collect`, `runs garbage collection`)
|
||||
public static async GarbageCollect(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
return await CloudRunner.Provider.garbageCollect(``, false, 0, false, false);
|
||||
}
|
||||
|
||||
@CliFunction(`list-resources`, `lists active resources`)
|
||||
public static async ListResources(): Promise<string[]> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
const result = await CloudRunner.Provider.listResources();
|
||||
CloudRunnerLogger.log(JSON.stringify(result, undefined, 4));
|
||||
|
||||
return result.map((x) => x.Name);
|
||||
}
|
||||
|
||||
@CliFunction(`list-worfklow`, `lists running workflows`)
|
||||
public static async ListWorfklow(): Promise<string[]> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
return (await CloudRunner.Provider.listWorkflow()).map((x) => x.Name);
|
||||
}
|
||||
|
||||
@CliFunction(`watch`, `follows logs of a running workflow`)
|
||||
public static async Watch(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
return await CloudRunner.Provider.watchWorkflow();
|
||||
}
|
||||
|
||||
@CliFunction(`remote-cli-post-build`, `runs a cloud runner build`)
|
||||
public static async PostCLIBuild(): Promise<string> {
|
||||
core.info(`Running POST build tasks`);
|
||||
|
||||
await Caching.PushToCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(`${CloudRunnerFolders.cacheFolderForCacheKeyFull}/Library`),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute),
|
||||
`lib-${CloudRunner.buildParameters.buildGuid}`,
|
||||
);
|
||||
|
||||
await Caching.PushToCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(`${CloudRunnerFolders.cacheFolderForCacheKeyFull}/build`),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectBuildFolderAbsolute),
|
||||
`build-${CloudRunner.buildParameters.buildGuid}`,
|
||||
);
|
||||
|
||||
if (!CloudRunner.buildParameters.retainWorkspace) {
|
||||
await CloudRunnerSystem.Run(
|
||||
`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
|
||||
);
|
||||
}
|
||||
|
||||
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(``));
|
||||
}
|
||||
}
|
||||
|
265
src/model/cloud-runner/cloud-runner-options.ts
Normal file
265
src/model/cloud-runner/cloud-runner-options.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { Cli } from '../cli/cli';
|
||||
import CloudRunnerQueryOverride from './services/cloud-runner-query-override';
|
||||
import GitHub from '../github';
|
||||
const core = require('@actions/core');
|
||||
|
||||
class CloudRunnerOptions {
|
||||
// ### ### ###
|
||||
// Input Handling
|
||||
// ### ### ###
|
||||
public static getInput(query) {
|
||||
if (GitHub.githubInputEnabled) {
|
||||
const coreInput = core.getInput(query);
|
||||
if (coreInput && coreInput !== '') {
|
||||
return coreInput;
|
||||
}
|
||||
}
|
||||
const alternativeQuery = CloudRunnerOptions.ToEnvVarFormat(query);
|
||||
|
||||
// Query input sources
|
||||
if (Cli.query(query, alternativeQuery)) {
|
||||
return Cli.query(query, alternativeQuery);
|
||||
}
|
||||
|
||||
if (CloudRunnerQueryOverride.query(query, alternativeQuery)) {
|
||||
return CloudRunnerQueryOverride.query(query, alternativeQuery);
|
||||
}
|
||||
|
||||
if (process.env[query] !== undefined) {
|
||||
return process.env[query];
|
||||
}
|
||||
|
||||
if (alternativeQuery !== query && process.env[alternativeQuery] !== undefined) {
|
||||
return process.env[alternativeQuery];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input: string) {
|
||||
if (input.toUpperCase() === input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return input
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/ /g, '_');
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Provider parameters
|
||||
// ### ### ###
|
||||
|
||||
static get region(): string {
|
||||
return CloudRunnerOptions.getInput('region') || 'eu-west-2';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Git syncronization parameters
|
||||
// ### ### ###
|
||||
|
||||
static get githubRepo() {
|
||||
return CloudRunnerOptions.getInput('GITHUB_REPOSITORY') || CloudRunnerOptions.getInput('GITHUB_REPO') || undefined;
|
||||
}
|
||||
static get branch() {
|
||||
if (CloudRunnerOptions.getInput(`GITHUB_REF`)) {
|
||||
return CloudRunnerOptions.getInput(`GITHUB_REF`).replace('refs/', '').replace(`head/`, '').replace(`heads/`, '');
|
||||
} else if (CloudRunnerOptions.getInput('branch')) {
|
||||
return CloudRunnerOptions.getInput('branch');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static get gitSha() {
|
||||
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() {
|
||||
const input = CloudRunnerOptions.getInput('cloudRunnerBuilderPlatform');
|
||||
if (input) {
|
||||
return input;
|
||||
}
|
||||
if (CloudRunnerOptions.cloudRunnerCluster !== 'local') {
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static get cloudRunnerBranch() {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerBranch') || 'cloud-runner-develop';
|
||||
}
|
||||
|
||||
static get cloudRunnerCluster() {
|
||||
if (Cli.isCliMode) {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'aws';
|
||||
}
|
||||
|
||||
return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'local';
|
||||
}
|
||||
|
||||
static get cloudRunnerCpu() {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerCpu');
|
||||
}
|
||||
|
||||
static get cloudRunnerMemory() {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerMemory');
|
||||
}
|
||||
|
||||
static get customJob() {
|
||||
return CloudRunnerOptions.getInput('customJob') || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Custom commands from files parameters
|
||||
// ### ### ###
|
||||
|
||||
static get customStepFiles() {
|
||||
return CloudRunnerOptions.getInput('customStepFiles')?.split(`,`) || [];
|
||||
}
|
||||
|
||||
static get customHookFiles() {
|
||||
return CloudRunnerOptions.getInput('customHookFiles')?.split(`,`) || [];
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Custom commands from yaml parameters
|
||||
// ### ### ###
|
||||
|
||||
static customJobHooks() {
|
||||
return CloudRunnerOptions.getInput('customJobHooks') || '';
|
||||
}
|
||||
|
||||
static get postBuildSteps() {
|
||||
return CloudRunnerOptions.getInput('postBuildSteps') || '';
|
||||
}
|
||||
|
||||
static get preBuildSteps() {
|
||||
return CloudRunnerOptions.getInput('preBuildSteps') || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Input override handling
|
||||
// ### ### ###
|
||||
|
||||
static readInputFromOverrideList() {
|
||||
return CloudRunnerOptions.getInput('readInputFromOverrideList') || '';
|
||||
}
|
||||
|
||||
static readInputOverrideCommand() {
|
||||
const value = CloudRunnerOptions.getInput('readInputOverrideCommand');
|
||||
|
||||
if (value === 'gcp-secret-manager') {
|
||||
return 'gcloud secrets versions access 1 --secret="{0}"';
|
||||
} else if (value === 'aws-secret-manager') {
|
||||
return 'aws secretsmanager get-secret-value --secret-id {0}';
|
||||
}
|
||||
|
||||
return value || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Aws
|
||||
// ### ### ###
|
||||
|
||||
static get awsBaseStackName() {
|
||||
return CloudRunnerOptions.getInput('awsBaseStackName') || 'game-ci';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// K8s
|
||||
// ### ### ###
|
||||
|
||||
static get kubeConfig() {
|
||||
return CloudRunnerOptions.getInput('kubeConfig') || '';
|
||||
}
|
||||
|
||||
static get kubeVolume() {
|
||||
return CloudRunnerOptions.getInput('kubeVolume') || '';
|
||||
}
|
||||
|
||||
static get kubeVolumeSize() {
|
||||
return CloudRunnerOptions.getInput('kubeVolumeSize') || '5Gi';
|
||||
}
|
||||
|
||||
static get kubeStorageClass(): string {
|
||||
return CloudRunnerOptions.getInput('kubeStorageClass') || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Caching
|
||||
// ### ### ###
|
||||
|
||||
static get cacheKey(): string {
|
||||
return CloudRunnerOptions.getInput('cacheKey') || CloudRunnerOptions.branch;
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Utility Parameters
|
||||
// ### ### ###
|
||||
|
||||
static get cloudRunnerDebug(): boolean {
|
||||
return CloudRunnerOptions.getInput(`cloudRunnerTests`) || CloudRunnerOptions.getInput(`cloudRunnerDebug`) || false;
|
||||
}
|
||||
static get cloudRunnerDebugTree(): boolean {
|
||||
return CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) || false;
|
||||
}
|
||||
static get cloudRunnerDebugEnv(): boolean {
|
||||
return CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) || false;
|
||||
}
|
||||
|
||||
static get watchCloudRunnerToEnd(): boolean {
|
||||
const input = CloudRunnerOptions.getInput(`watchToEnd`);
|
||||
|
||||
return input !== 'false';
|
||||
}
|
||||
|
||||
public static get useSharedLargePackages(): boolean {
|
||||
return CloudRunnerOptions.getInput(`useSharedLargePackages`) || false;
|
||||
}
|
||||
|
||||
public static get useSharedBuilder(): boolean {
|
||||
return CloudRunnerOptions.getInput(`useSharedBuilder`) || true;
|
||||
}
|
||||
|
||||
public static get useLz4Compression(): boolean {
|
||||
return CloudRunnerOptions.getInput(`useLz4Compression`) || true;
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Retained Workspace
|
||||
// ### ### ###
|
||||
|
||||
public static get retainWorkspaces(): boolean {
|
||||
return CloudRunnerOptions.getInput(`retainWorkspaces`) || false;
|
||||
}
|
||||
|
||||
static get maxRetainedWorkspaces(): number {
|
||||
return Number(CloudRunnerOptions.getInput(`maxRetainedWorkspaces`)) || 3;
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Garbage Collection
|
||||
// ### ### ###
|
||||
|
||||
static get constantGarbageCollection(): boolean {
|
||||
return CloudRunnerOptions.getInput(`constantGarbageCollection`) || true;
|
||||
}
|
||||
|
||||
static get garbageCollectionMaxAge(): number {
|
||||
return Number(CloudRunnerOptions.getInput(`garbageCollectionMaxAge`)) || 24;
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudRunnerOptions;
|
@ -1,144 +0,0 @@
|
||||
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 UnityVersioning from '../unity-versioning';
|
||||
import { Cli } from '../cli/cli';
|
||||
import CloudRunnerLogger from './services/cloud-runner-logger';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
describe('Cloud Runner', () => {
|
||||
it('responds', () => {});
|
||||
});
|
||||
describe('Cloud Runner', () => {
|
||||
const testSecretName = 'testSecretName';
|
||||
const testSecretValue = 'testSecretValue';
|
||||
if (Input.cloudRunnerTests) {
|
||||
it('All build parameters sent to cloud runner as env vars', async () => {
|
||||
// Build parameters
|
||||
Cli.options = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
customJob: `
|
||||
- name: 'step 1'
|
||||
image: 'alpine'
|
||||
commands: 'printenv'
|
||||
secrets:
|
||||
- name: '${testSecretName}'
|
||||
value: '${testSecretValue}'
|
||||
`,
|
||||
};
|
||||
Input.githubInputEnabled = false;
|
||||
|
||||
// Setup parameters
|
||||
const buildParameter = await BuildParameters.create();
|
||||
Input.githubInputEnabled = true;
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
// Run the job
|
||||
const file = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
|
||||
// Assert results
|
||||
expect(file).toContain(JSON.stringify(buildParameter));
|
||||
expect(file).toContain(`${Input.ToEnvVarFormat(testSecretName)}=${testSecretValue}`);
|
||||
const environmentVariables = TaskParameterSerializer.readBuildEnvironmentVariables();
|
||||
const newLinePurgedFile = file
|
||||
.replace(/\s+/g, '')
|
||||
.replace(new RegExp(`\\[${CloudRunnerStatics.logPrefix}\\]`, 'g'), '');
|
||||
for (const element of environmentVariables) {
|
||||
if (element.value !== undefined && typeof element.value !== 'function') {
|
||||
if (typeof element.value === `string`) {
|
||||
element.value = element.value.replace(/\s+/g, '');
|
||||
}
|
||||
CloudRunnerLogger.log(`checking input/build param ${element.name} ${element.value}`);
|
||||
}
|
||||
}
|
||||
for (const element of environmentVariables) {
|
||||
if (element.value !== undefined && typeof element.value !== 'function') {
|
||||
expect(newLinePurgedFile).toContain(`${element.name}`);
|
||||
expect(newLinePurgedFile).toContain(`${element.name}=${element.value}`);
|
||||
}
|
||||
}
|
||||
delete Cli.options;
|
||||
}, 1000000);
|
||||
it('Run one build it should not use cache, run subsequent build which should use cache', async () => {
|
||||
Cli.options = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
};
|
||||
Input.githubInputEnabled = false;
|
||||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
const results = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const libraryString = 'Rebuilding Library because the asset database could not be found!';
|
||||
const buildSucceededString = 'Build succeeded';
|
||||
expect(results).toContain(libraryString);
|
||||
expect(results).toContain(buildSucceededString);
|
||||
CloudRunnerLogger.log(`run 1 succeeded`);
|
||||
const buildParameter2 = await BuildParameters.create();
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
expect(results2).toContain(buildSucceededString);
|
||||
expect(results2).toEqual(expect.not.stringContaining(libraryString));
|
||||
Input.githubInputEnabled = true;
|
||||
delete Cli.options;
|
||||
}, 1000000);
|
||||
}
|
||||
|
||||
it('Local cloud runner returns commands', async () => {
|
||||
// Build parameters
|
||||
Cli.options = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
cloudRunnerCluster: 'local-system',
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
customJob: `
|
||||
- name: 'step 1'
|
||||
image: 'alpine'
|
||||
commands: 'dir'
|
||||
secrets:
|
||||
- name: '${testSecretName}'
|
||||
value: '${testSecretValue}'
|
||||
`,
|
||||
};
|
||||
Input.githubInputEnabled = false;
|
||||
|
||||
// Setup parameters
|
||||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
// Run the job
|
||||
await expect(CloudRunner.run(buildParameter, baseImage.toString())).resolves.not.toThrow();
|
||||
Input.githubInputEnabled = true;
|
||||
delete Cli.options;
|
||||
}, 1000000);
|
||||
|
||||
it('Test cloud runner returns commands', async () => {
|
||||
// Build parameters
|
||||
Cli.options = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
cloudRunnerCluster: 'test',
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
};
|
||||
Input.githubInputEnabled = false;
|
||||
|
||||
// Setup parameters
|
||||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
// Run the job
|
||||
await expect(CloudRunner.run(buildParameter, baseImage.toString())).resolves.not.toThrow();
|
||||
Input.githubInputEnabled = true;
|
||||
delete Cli.options;
|
||||
}, 1000000);
|
||||
});
|
@ -12,31 +12,39 @@ import { ProviderInterface } from './providers/provider-interface';
|
||||
import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable';
|
||||
import TestCloudRunner from './providers/test';
|
||||
import LocalCloudRunner from './providers/local';
|
||||
import LocalDockerCloudRunner from './providers/local-docker';
|
||||
import LocalDockerCloudRunner from './providers/docker';
|
||||
import GitHub from '../github';
|
||||
import SharedWorkspaceLocking from './services/shared-workspace-locking';
|
||||
|
||||
class CloudRunner {
|
||||
public static Provider: ProviderInterface;
|
||||
static buildParameters: BuildParameters;
|
||||
public static defaultSecrets: CloudRunnerSecret[];
|
||||
public static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
|
||||
private static setup(buildParameters: BuildParameters) {
|
||||
public static buildParameters: BuildParameters;
|
||||
private static defaultSecrets: CloudRunnerSecret[];
|
||||
private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
|
||||
static lockedWorkspace: string | undefined;
|
||||
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
|
||||
public static setup(buildParameters: BuildParameters) {
|
||||
CloudRunnerLogger.setup();
|
||||
CloudRunnerLogger.log(`Setting up cloud runner`);
|
||||
CloudRunner.buildParameters = buildParameters;
|
||||
CloudRunner.setupBuildPlatform();
|
||||
CloudRunner.setupSelectedBuildPlatform();
|
||||
CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets();
|
||||
CloudRunner.cloudRunnerEnvironmentVariables = TaskParameterSerializer.readBuildEnvironmentVariables();
|
||||
if (!buildParameters.isCliMode) {
|
||||
CloudRunner.cloudRunnerEnvironmentVariables =
|
||||
TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters);
|
||||
if (GitHub.githubInputEnabled) {
|
||||
const buildParameterPropertyNames = Object.getOwnPropertyNames(buildParameters);
|
||||
for (const element of CloudRunner.cloudRunnerEnvironmentVariables) {
|
||||
// CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element.name)} = ${element.value}`);
|
||||
core.setOutput(Input.ToEnvVarFormat(element.name), element.value);
|
||||
}
|
||||
for (const element of buildParameterPropertyNames) {
|
||||
// CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`);
|
||||
core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static setupBuildPlatform() {
|
||||
private static setupSelectedBuildPlatform() {
|
||||
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.cloudRunnerCluster}`);
|
||||
switch (CloudRunner.buildParameters.cloudRunnerCluster) {
|
||||
case 'k8s':
|
||||
@ -48,20 +56,41 @@ class CloudRunner {
|
||||
case 'test':
|
||||
CloudRunner.Provider = new TestCloudRunner();
|
||||
break;
|
||||
case 'local-system':
|
||||
CloudRunner.Provider = new LocalCloudRunner();
|
||||
break;
|
||||
case 'local-docker':
|
||||
CloudRunner.Provider = new LocalDockerCloudRunner();
|
||||
break;
|
||||
case 'local-system':
|
||||
CloudRunner.Provider = new LocalCloudRunner();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static async run(buildParameters: BuildParameters, baseImage: string) {
|
||||
CloudRunner.setup(buildParameters);
|
||||
try {
|
||||
if (buildParameters.retainWorkspace) {
|
||||
CloudRunner.lockedWorkspace = `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`;
|
||||
|
||||
const result = await SharedWorkspaceLocking.GetOrCreateLockedWorkspace(
|
||||
CloudRunner.lockedWorkspace,
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
CloudRunnerLogger.logLine(`Using retained workspace ${CloudRunner.lockedWorkspace}`);
|
||||
CloudRunner.cloudRunnerEnvironmentVariables = [
|
||||
...CloudRunner.cloudRunnerEnvironmentVariables,
|
||||
{ name: `LOCKED_WORKSPACE`, value: CloudRunner.lockedWorkspace },
|
||||
];
|
||||
} else {
|
||||
CloudRunnerLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`);
|
||||
buildParameters.retainWorkspace = false;
|
||||
CloudRunner.lockedWorkspace = undefined;
|
||||
}
|
||||
}
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Setup shared cloud runner resources');
|
||||
await CloudRunner.Provider.setup(
|
||||
await CloudRunner.Provider.setupWorkflow(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
CloudRunner.buildParameters.branch,
|
||||
@ -72,7 +101,7 @@ class CloudRunner {
|
||||
new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets),
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Cleanup shared cloud runner resources');
|
||||
await CloudRunner.Provider.cleanup(
|
||||
await CloudRunner.Provider.cleanupWorkflow(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
CloudRunner.buildParameters.branch,
|
||||
@ -81,10 +110,23 @@ class CloudRunner {
|
||||
CloudRunnerLogger.log(`Cleanup complete`);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
|
||||
if (CloudRunner.buildParameters.retainWorkspace) {
|
||||
await SharedWorkspaceLocking.ReleaseWorkspace(
|
||||
CloudRunner.lockedWorkspace || ``,
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
);
|
||||
CloudRunner.lockedWorkspace = undefined;
|
||||
}
|
||||
|
||||
if (buildParameters.constantGarbageCollection) {
|
||||
CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageCollectionMaxAge, true, true);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
await CloudRunnerError.handleException(error);
|
||||
await CloudRunnerError.handleException(error, CloudRunner.buildParameters, CloudRunner.defaultSecrets);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import * as core from '@actions/core';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import BuildParameters from '../../build-parameters';
|
||||
|
||||
export class CloudRunnerError {
|
||||
public static async handleException(error: unknown) {
|
||||
public static async handleException(error: unknown, buildParameters: BuildParameters, secrets: CloudRunnerSecret[]) {
|
||||
CloudRunnerLogger.error(JSON.stringify(error, undefined, 4));
|
||||
core.setFailed('Cloud Runner failed');
|
||||
await CloudRunner.Provider.cleanup(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
CloudRunner.buildParameters.branch,
|
||||
CloudRunner.defaultSecrets,
|
||||
if (CloudRunner.Provider !== undefined) {
|
||||
await CloudRunner.Provider.cleanupWorkflow(
|
||||
buildParameters.buildGuid,
|
||||
buildParameters,
|
||||
buildParameters.branch,
|
||||
secrets,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export class AWSError {
|
||||
static async handleStackCreationFailure(error: any, CF: SDK.CloudFormation, taskDefStackName: string) {
|
||||
CloudRunnerLogger.log('aws error: ');
|
||||
core.error(JSON.stringify(error, undefined, 4));
|
||||
if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) {
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log('Getting events and resources for task stack');
|
||||
const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents;
|
||||
CloudRunnerLogger.log(JSON.stringify(events, undefined, 4));
|
||||
|
@ -69,6 +69,7 @@ export class AWSJobStack {
|
||||
const secretsMappedToCloudFormationParameters = secrets.map((x) => {
|
||||
return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue };
|
||||
});
|
||||
const logGroupName = `${this.baseStackName}/${taskDefStackName}`;
|
||||
const parameters = [
|
||||
{
|
||||
ParameterKey: 'EnvironmentName',
|
||||
@ -82,6 +83,10 @@ export class AWSJobStack {
|
||||
ParameterKey: 'ServiceName',
|
||||
ParameterValue: taskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'LogGroupName',
|
||||
ParameterValue: logGroupName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'Command',
|
||||
ParameterValue: 'echo "this template should be overwritten when running a task"',
|
||||
@ -115,6 +120,7 @@ export class AWSJobStack {
|
||||
if (element.StackName === taskDefStackName && element.StackStatus !== 'DELETE_COMPLETE') {
|
||||
previousStackExists = true;
|
||||
CloudRunnerLogger.log(`Previous stack still exists: ${JSON.stringify(element)}`);
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,18 +6,18 @@ import * as zlib from 'zlib';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { Input } from '../../..';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import { CloudRunnerBuildCommandProcessor } from '../../services/cloud-runner-build-command-process';
|
||||
import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks';
|
||||
import { FollowLogStreamService } from '../../services/follow-log-stream-service';
|
||||
import CloudRunnerOptions from '../../cloud-runner-options';
|
||||
|
||||
class AWSTaskRunner {
|
||||
public static ECS: AWS.ECS;
|
||||
public static Kinesis: AWS.Kinesis;
|
||||
static async runTask(
|
||||
taskDef: CloudRunnerAWSTaskDef,
|
||||
ECS: AWS.ECS,
|
||||
CF: AWS.CloudFormation,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
buildGuid: string,
|
||||
commands: string,
|
||||
) {
|
||||
): Promise<{ output: string; shouldCleanup: boolean }> {
|
||||
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
|
||||
const taskDefinition =
|
||||
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || '';
|
||||
@ -30,7 +30,7 @@ class AWSTaskRunner {
|
||||
const streamName =
|
||||
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || '';
|
||||
|
||||
const task = await ECS.runTask({
|
||||
const task = await AWSTaskRunner.ECS.runTask({
|
||||
cluster,
|
||||
taskDefinition,
|
||||
platformVersion: '1.4.0',
|
||||
@ -39,7 +39,7 @@ class AWSTaskRunner {
|
||||
{
|
||||
name: taskDef.taskDefStackName,
|
||||
environment,
|
||||
command: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(commands, CloudRunner.buildParameters)],
|
||||
command: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(commands, CloudRunner.buildParameters)],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -54,20 +54,25 @@ class AWSTaskRunner {
|
||||
}).promise();
|
||||
const taskArn = task.tasks?.[0].taskArn || '';
|
||||
CloudRunnerLogger.log('Cloud runner job is starting');
|
||||
await AWSTaskRunner.waitUntilTaskRunning(ECS, taskArn, cluster);
|
||||
await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster);
|
||||
CloudRunnerLogger.log(
|
||||
`Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(ECS, cluster, taskArn))?.lastStatus}`,
|
||||
`Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus}`,
|
||||
);
|
||||
const { output, shouldCleanup } = await this.streamLogsUntilTaskStops(
|
||||
ECS,
|
||||
CF,
|
||||
taskDef,
|
||||
cluster,
|
||||
taskArn,
|
||||
streamName,
|
||||
);
|
||||
const taskData = await AWSTaskRunner.describeTasks(ECS, cluster, taskArn);
|
||||
const exitCode = taskData.containers?.[0].exitCode;
|
||||
if (!CloudRunnerOptions.watchCloudRunnerToEnd) {
|
||||
const shouldCleanup: boolean = false;
|
||||
const output: string = '';
|
||||
CloudRunnerLogger.log(`Watch Cloud Runner To End: false`);
|
||||
|
||||
return { output, shouldCleanup };
|
||||
}
|
||||
|
||||
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;
|
||||
CloudRunnerLogger.log(`Container State: ${JSON.stringify(containerState, undefined, 4)}`);
|
||||
const wasSuccessful = exitCode === 0 || (exitCode === undefined && taskData.lastStatus === 'RUNNING');
|
||||
if (wasSuccessful) {
|
||||
CloudRunnerLogger.log(`Cloud runner job has finished successfully`);
|
||||
@ -85,15 +90,15 @@ class AWSTaskRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private static async waitUntilTaskRunning(ECS: AWS.ECS, taskArn: string, cluster: string) {
|
||||
private static async waitUntilTaskRunning(taskArn: string, cluster: string) {
|
||||
try {
|
||||
await ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
|
||||
await AWSTaskRunner.ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
|
||||
} catch (error_) {
|
||||
const error = error_ as Error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
CloudRunnerLogger.log(
|
||||
`Cloud runner job has ended ${
|
||||
(await AWSTaskRunner.describeTasks(ECS, cluster, taskArn)).containers?.[0].lastStatus
|
||||
(await AWSTaskRunner.describeTasks(cluster, taskArn)).containers?.[0].lastStatus
|
||||
}`,
|
||||
);
|
||||
|
||||
@ -102,8 +107,8 @@ class AWSTaskRunner {
|
||||
}
|
||||
}
|
||||
|
||||
static async describeTasks(ECS: AWS.ECS, clusterName: string, taskArn: string) {
|
||||
const tasks = await ECS.describeTasks({
|
||||
static async describeTasks(clusterName: string, taskArn: string) {
|
||||
const tasks = await AWSTaskRunner.ECS.describeTasks({
|
||||
cluster: clusterName,
|
||||
tasks: [taskArn],
|
||||
}).promise();
|
||||
@ -114,17 +119,11 @@ class AWSTaskRunner {
|
||||
}
|
||||
}
|
||||
|
||||
static async streamLogsUntilTaskStops(
|
||||
ECS: AWS.ECS,
|
||||
CF: AWS.CloudFormation,
|
||||
taskDef: CloudRunnerAWSTaskDef,
|
||||
clusterName: string,
|
||||
taskArn: string,
|
||||
kinesisStreamName: string,
|
||||
) {
|
||||
const kinesis = new AWS.Kinesis();
|
||||
const stream = await AWSTaskRunner.getLogStream(kinesis, kinesisStreamName);
|
||||
let iterator = await AWSTaskRunner.getLogIterator(kinesis, stream);
|
||||
static async streamLogsUntilTaskStops(clusterName: string, taskArn: string, kinesisStreamName: string) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
CloudRunnerLogger.log(`Streaming...`);
|
||||
const stream = await AWSTaskRunner.getLogStream(kinesisStreamName);
|
||||
let iterator = await AWSTaskRunner.getLogIterator(stream);
|
||||
|
||||
const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${Input.region}#logsV2:log-groups/log-group/${CloudRunner.buildParameters.awsBaseStackName}-${CloudRunner.buildParameters.buildGuid}`;
|
||||
CloudRunnerLogger.log(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`);
|
||||
@ -134,13 +133,11 @@ class AWSTaskRunner {
|
||||
let output = '';
|
||||
while (shouldReadLogs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const taskData = await AWSTaskRunner.describeTasks(ECS, clusterName, taskArn);
|
||||
const taskData = await AWSTaskRunner.describeTasks(clusterName, taskArn);
|
||||
({ timestamp, shouldReadLogs } = AWSTaskRunner.checkStreamingShouldContinue(taskData, timestamp, shouldReadLogs));
|
||||
({ iterator, shouldReadLogs, output, shouldCleanup } = await AWSTaskRunner.handleLogStreamIteration(
|
||||
kinesis,
|
||||
iterator,
|
||||
shouldReadLogs,
|
||||
taskDef,
|
||||
output,
|
||||
shouldCleanup,
|
||||
));
|
||||
@ -150,23 +147,18 @@ class AWSTaskRunner {
|
||||
}
|
||||
|
||||
private static async handleLogStreamIteration(
|
||||
kinesis: AWS.Kinesis,
|
||||
iterator: string,
|
||||
shouldReadLogs: boolean,
|
||||
taskDef: CloudRunnerAWSTaskDef,
|
||||
output: string,
|
||||
shouldCleanup: boolean,
|
||||
) {
|
||||
const records = await kinesis
|
||||
.getRecords({
|
||||
const records = await AWSTaskRunner.Kinesis.getRecords({
|
||||
ShardIterator: iterator,
|
||||
})
|
||||
.promise();
|
||||
}).promise();
|
||||
iterator = records.NextShardIterator || '';
|
||||
({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords(
|
||||
records,
|
||||
iterator,
|
||||
taskDef,
|
||||
shouldReadLogs,
|
||||
output,
|
||||
shouldCleanup,
|
||||
@ -197,7 +189,6 @@ class AWSTaskRunner {
|
||||
private static logRecords(
|
||||
records,
|
||||
iterator: string,
|
||||
taskDef: CloudRunnerAWSTaskDef,
|
||||
shouldReadLogs: boolean,
|
||||
output: string,
|
||||
shouldCleanup: boolean,
|
||||
@ -224,24 +215,20 @@ class AWSTaskRunner {
|
||||
return { shouldReadLogs, output, shouldCleanup };
|
||||
}
|
||||
|
||||
private static async getLogStream(kinesis: AWS.Kinesis, kinesisStreamName: string) {
|
||||
return await kinesis
|
||||
.describeStream({
|
||||
private static async getLogStream(kinesisStreamName: string) {
|
||||
return await AWSTaskRunner.Kinesis.describeStream({
|
||||
StreamName: kinesisStreamName,
|
||||
})
|
||||
.promise();
|
||||
}).promise();
|
||||
}
|
||||
|
||||
private static async getLogIterator(kinesis: AWS.Kinesis, stream) {
|
||||
private static async getLogIterator(stream) {
|
||||
return (
|
||||
(
|
||||
await kinesis
|
||||
.getShardIterator({
|
||||
await AWSTaskRunner.Kinesis.getShardIterator({
|
||||
ShardIteratorType: 'TRIM_HORIZON',
|
||||
StreamName: stream.StreamDescription.StreamName,
|
||||
ShardId: stream.StreamDescription.Shards[0].ShardId,
|
||||
})
|
||||
.promise()
|
||||
}).promise()
|
||||
).ShardIterator || ''
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ Parameters:
|
||||
Type: String
|
||||
Default: example
|
||||
Description: A name for the service
|
||||
LogGroupName:
|
||||
Type: String
|
||||
Default: example
|
||||
Description: Name to use for the log group created for this task
|
||||
ImageUrl:
|
||||
Type: String
|
||||
Default: nginx
|
||||
@ -68,7 +72,7 @@ Resources:
|
||||
LogGroup:
|
||||
Type: 'AWS::Logs::LogGroup'
|
||||
Properties:
|
||||
LogGroupName: !Ref ServiceName
|
||||
LogGroupName: !Ref LogGroupName
|
||||
Metadata:
|
||||
'AWS::CloudFormation::Designer':
|
||||
id: aece53ae-b82d-4267-bc16-ed964b05db27
|
||||
@ -78,7 +82,7 @@ Resources:
|
||||
FilterPattern: ''
|
||||
RoleArn:
|
||||
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:CloudWatchIAMRole'
|
||||
LogGroupName: !Ref ServiceName
|
||||
LogGroupName: !Ref LogGroupName
|
||||
DestinationArn:
|
||||
'Fn::GetAtt':
|
||||
- KinesisStream
|
||||
@ -147,7 +151,7 @@ Resources:
|
||||
LogConfiguration:
|
||||
LogDriver: awslogs
|
||||
Options:
|
||||
awslogs-group: !Ref ServiceName
|
||||
awslogs-group: !Ref LogGroupName
|
||||
awslogs-region: !Ref 'AWS::Region'
|
||||
awslogs-stream-prefix: !Ref ServiceName
|
||||
DependsOn:
|
||||
|
@ -1,170 +0,0 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import { CliFunction } from '../../../../cli/cli-functions-repository';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
|
||||
import { BaseStackFormation } from '../cloud-formations/base-stack-formation';
|
||||
|
||||
export class AwsCliCommands {
|
||||
@CliFunction(`aws-list-all`, `List all resources`)
|
||||
static async awsListAll() {
|
||||
await AwsCliCommands.awsListStacks(undefined, true);
|
||||
await AwsCliCommands.awsListTasks();
|
||||
await AwsCliCommands.awsListLogGroups(undefined, true);
|
||||
}
|
||||
@CliFunction(`aws-garbage-collect`, `garbage collect aws resources not in use !WIP!`)
|
||||
static async garbageCollectAws() {
|
||||
await AwsCliCommands.cleanup(false);
|
||||
}
|
||||
@CliFunction(`aws-garbage-collect-all`, `garbage collect aws resources regardless of whether they are in use`)
|
||||
static async garbageCollectAwsAll() {
|
||||
await AwsCliCommands.cleanup(true);
|
||||
}
|
||||
@CliFunction(
|
||||
`aws-garbage-collect-all-1d-older`,
|
||||
`garbage collect aws resources created more than 1d ago (ignore if they are in use)`,
|
||||
)
|
||||
static async garbageCollectAwsAllOlderThanOneDay() {
|
||||
await AwsCliCommands.cleanup(true, true);
|
||||
}
|
||||
static isOlderThan1day(date: any) {
|
||||
const ageDate = new Date(date.getTime() - Date.now());
|
||||
|
||||
return ageDate.getDay() > 0;
|
||||
}
|
||||
@CliFunction(`aws-list-stacks`, `List stacks`)
|
||||
static async awsListStacks(perResultCallback: any = false, verbose: boolean = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const stacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) => _x.StackStatus !== 'DELETE_COMPLETE', // &&
|
||||
// _x.TemplateDescription === TaskDefinitionFormation.description.replace('\n', ''),
|
||||
) || [];
|
||||
CloudRunnerLogger.log(`Stacks ${stacks.length}`);
|
||||
for (const element of stacks) {
|
||||
const ageDate = new Date(element.CreationTime.getTime() - Date.now());
|
||||
if (verbose)
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${ageDate.getDay()} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element);
|
||||
}
|
||||
const baseStacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) =>
|
||||
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription,
|
||||
) || [];
|
||||
CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`);
|
||||
for (const element of baseStacks) {
|
||||
const ageDate = new Date(element.CreationTime.getTime() - Date.now());
|
||||
if (verbose)
|
||||
CloudRunnerLogger.log(
|
||||
`Base Stack ${
|
||||
element.StackName
|
||||
} - Age D${ageDate.getHours()} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element);
|
||||
}
|
||||
if (stacks === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@CliFunction(`aws-list-tasks`, `List tasks`)
|
||||
static async awsListTasks(perResultCallback: any = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.ECS();
|
||||
const clusters = (await ecs.listClusters().promise()).clusterArns || [];
|
||||
CloudRunnerLogger.log(`Clusters ${clusters.length}`);
|
||||
for (const element of clusters) {
|
||||
const input: AWS.ECS.ListTasksRequest = {
|
||||
cluster: element,
|
||||
};
|
||||
|
||||
const list = (await ecs.listTasks(input).promise()).taskArns || [];
|
||||
if (list.length > 0) {
|
||||
const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element };
|
||||
const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || [];
|
||||
if (describeList === []) {
|
||||
continue;
|
||||
}
|
||||
CloudRunnerLogger.log(`Tasks ${describeList.length}`);
|
||||
for (const taskElement of describeList) {
|
||||
if (taskElement === undefined) {
|
||||
continue;
|
||||
}
|
||||
taskElement.overrides = {};
|
||||
taskElement.attachments = [];
|
||||
if (taskElement.createdAt === undefined) {
|
||||
CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`);
|
||||
continue;
|
||||
}
|
||||
if (perResultCallback) await perResultCallback(taskElement, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@CliFunction(`aws-list-log-groups`, `List tasks`)
|
||||
static async awsListLogGroups(perResultCallback: any = false, verbose: boolean = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.CloudWatchLogs();
|
||||
let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = {
|
||||
/* logGroupNamePrefix: 'game-ci' */
|
||||
};
|
||||
let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
const logGroups = logGroupsDescribe.logGroups || [];
|
||||
while (logGroupsDescribe.nextToken) {
|
||||
logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken };
|
||||
logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
logGroups.push(...(logGroupsDescribe?.logGroups || []));
|
||||
}
|
||||
|
||||
CloudRunnerLogger.log(`Log Groups ${logGroups.length}`);
|
||||
for (const element of logGroups) {
|
||||
if (element.creationTime === undefined) {
|
||||
CloudRunnerLogger.log(`Skipping ${element.logGroupName} no createdAt date`);
|
||||
continue;
|
||||
}
|
||||
const ageDate = new Date(new Date(element.creationTime).getTime() - Date.now());
|
||||
if (verbose)
|
||||
CloudRunnerLogger.log(
|
||||
`Log Group Name ${
|
||||
element.logGroupName
|
||||
} - Age D${ageDate.getDay()} H${ageDate.getHours()} M${ageDate.getMinutes()} - 1d old ${AwsCliCommands.isOlderThan1day(
|
||||
new Date(element.creationTime),
|
||||
)}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element, element);
|
||||
}
|
||||
}
|
||||
|
||||
private static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const ecs = new AWS.ECS();
|
||||
const cwl = new AWS.CloudWatchLogs();
|
||||
await AwsCliCommands.awsListStacks(async (element) => {
|
||||
if (deleteResources && (!OneDayOlderOnly || AwsCliCommands.isOlderThan1day(element.CreationTime))) {
|
||||
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
|
||||
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
|
||||
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName };
|
||||
await CF.deleteStack(deleteStackInput).promise();
|
||||
}
|
||||
});
|
||||
await AwsCliCommands.awsListTasks(async (taskElement, element) => {
|
||||
if (deleteResources && (!OneDayOlderOnly || AwsCliCommands.isOlderThan1day(taskElement.CreatedAt))) {
|
||||
CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
|
||||
await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise();
|
||||
}
|
||||
});
|
||||
await AwsCliCommands.awsListLogGroups(async (element) => {
|
||||
if (deleteResources && (!OneDayOlderOnly || AwsCliCommands.isOlderThan1day(new Date(element.createdAt)))) {
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -2,13 +2,18 @@ import * as SDK from 'aws-sdk';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
|
||||
import AWSTaskRunner from './aws-task-runner';
|
||||
import AwsTaskRunner from './aws-task-runner';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { AWSJobStack } from './aws-job-stack';
|
||||
import { AWSBaseStack } from './aws-base-stack';
|
||||
import { AWSJobStack as AwsJobStack } from './aws-job-stack';
|
||||
import { AWSBaseStack as AwsBaseStack } from './aws-base-stack';
|
||||
import { Input } from '../../..';
|
||||
import { TertiaryResourcesService } from './services/tertiary-resources-service';
|
||||
import { GarbageCollectionService } from './services/garbage-collection-service';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import { TaskService } from './services/task-service';
|
||||
|
||||
class AWSBuildEnvironment implements ProviderInterface {
|
||||
private baseStackName: string;
|
||||
@ -16,7 +21,43 @@ class AWSBuildEnvironment implements ProviderInterface {
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
this.baseStackName = buildParameters.awsBaseStackName;
|
||||
}
|
||||
async cleanup(
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
await TaskService.awsListJobs();
|
||||
await TertiaryResourcesService.awsListLogGroups();
|
||||
await TaskService.awsListTasks();
|
||||
await TaskService.awsListStacks();
|
||||
|
||||
return [];
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async watchWorkflow(): Promise<string> {
|
||||
return await TaskService.watch();
|
||||
}
|
||||
|
||||
async listOtherResources(): Promise<string> {
|
||||
await TertiaryResourcesService.awsListLogGroups();
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
filter: string,
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
await GarbageCollectionService.cleanup(!previewOnly);
|
||||
|
||||
return ``;
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -26,7 +67,7 @@ class AWSBuildEnvironment implements ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
async setup(
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -37,7 +78,7 @@ class AWSBuildEnvironment implements ProviderInterface {
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
|
||||
async runTask(
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
@ -49,12 +90,14 @@ class AWSBuildEnvironment implements ProviderInterface {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ECS = new SDK.ECS();
|
||||
const CF = new SDK.CloudFormation();
|
||||
AwsTaskRunner.ECS = ECS;
|
||||
AwsTaskRunner.Kinesis = new SDK.Kinesis();
|
||||
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(
|
||||
await new AwsBaseStack(this.baseStackName).setupBaseStack(CF);
|
||||
const taskDef = await new AwsJobStack(this.baseStackName).setupCloudFormations(
|
||||
CF,
|
||||
buildGuid,
|
||||
image,
|
||||
@ -69,7 +112,7 @@ class AWSBuildEnvironment implements ProviderInterface {
|
||||
try {
|
||||
const postSetupStacksTimeMs = Date.now();
|
||||
CloudRunnerLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`);
|
||||
const { output, shouldCleanup } = await AWSTaskRunner.runTask(taskDef, ECS, CF, environment, buildGuid, commands);
|
||||
const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, commands);
|
||||
postRunTaskTimeMs = Date.now();
|
||||
CloudRunnerLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`);
|
||||
if (shouldCleanup) {
|
||||
@ -81,6 +124,7 @@ class AWSBuildEnvironment implements ProviderInterface {
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`error running task ${error}`);
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
throw error;
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
|
||||
import { TaskService } from './task-service';
|
||||
import { TertiaryResourcesService } from './tertiary-resources-service';
|
||||
|
||||
export class GarbageCollectionService {
|
||||
static isOlderThan1day(date: any) {
|
||||
const ageDate = new Date(date.getTime() - Date.now());
|
||||
|
||||
return ageDate.getDay() > 0;
|
||||
}
|
||||
|
||||
public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const ecs = new AWS.ECS();
|
||||
const cwl = new AWS.CloudWatchLogs();
|
||||
const taskDefinitionsInUse = new Array();
|
||||
await TaskService.awsListTasks(async (taskElement, element) => {
|
||||
taskDefinitionsInUse.push(taskElement.taskDefinitionArn);
|
||||
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.CreatedAt))) {
|
||||
CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
|
||||
await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise();
|
||||
}
|
||||
});
|
||||
await TaskService.awsListStacks(async (element) => {
|
||||
if (
|
||||
(await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some(
|
||||
(x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId),
|
||||
)
|
||||
) {
|
||||
CloudRunnerLogger.log(`Skipping ${element.StackName} - active task was running not deleting`);
|
||||
|
||||
return;
|
||||
}
|
||||
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) {
|
||||
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
|
||||
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
|
||||
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName };
|
||||
await CF.deleteStack(deleteStackInput).promise();
|
||||
}
|
||||
});
|
||||
await TertiaryResourcesService.awsListLogGroups(async (element) => {
|
||||
if (
|
||||
deleteResources &&
|
||||
(!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.createdAt)))
|
||||
) {
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
131
src/model/cloud-runner/providers/aws/services/task-service.ts
Normal file
131
src/model/cloud-runner/providers/aws/services/task-service.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
|
||||
import { BaseStackFormation } from '../cloud-formations/base-stack-formation';
|
||||
import AwsTaskRunner from '../aws-task-runner';
|
||||
|
||||
export class TaskService {
|
||||
static async watch() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { output, shouldCleanup } = await AwsTaskRunner.streamLogsUntilTaskStops(
|
||||
process.env.cluster || ``,
|
||||
process.env.taskArn || ``,
|
||||
process.env.streamName || ``,
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
public static async awsListStacks(perResultCallback: any = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const stacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) => _x.StackStatus !== 'DELETE_COMPLETE', // &&
|
||||
// _x.TemplateDescription === TaskDefinitionFormation.description.replace('\n', ''),
|
||||
) || [];
|
||||
CloudRunnerLogger.log(`Stacks ${stacks.length}`);
|
||||
for (const element of stacks) {
|
||||
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element);
|
||||
}
|
||||
const baseStacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) =>
|
||||
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription,
|
||||
) || [];
|
||||
CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`);
|
||||
for (const element of baseStacks) {
|
||||
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element);
|
||||
}
|
||||
if (stacks === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
public static async awsListTasks(perResultCallback: any = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.ECS();
|
||||
const clusters = (await ecs.listClusters().promise()).clusterArns || [];
|
||||
CloudRunnerLogger.log(`Clusters ${clusters.length}`);
|
||||
for (const element of clusters) {
|
||||
const input: AWS.ECS.ListTasksRequest = {
|
||||
cluster: element,
|
||||
};
|
||||
|
||||
const list = (await ecs.listTasks(input).promise()).taskArns || [];
|
||||
if (list.length > 0) {
|
||||
const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element };
|
||||
const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || [];
|
||||
if (describeList.length === 0) {
|
||||
continue;
|
||||
}
|
||||
CloudRunnerLogger.log(`Tasks ${describeList.length}`);
|
||||
for (const taskElement of describeList) {
|
||||
if (taskElement === undefined) {
|
||||
continue;
|
||||
}
|
||||
taskElement.overrides = {};
|
||||
taskElement.attachments = [];
|
||||
if (taskElement.createdAt === undefined) {
|
||||
CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`);
|
||||
continue;
|
||||
}
|
||||
if (perResultCallback) await perResultCallback(taskElement, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static async awsListJobs(perResultCallback: any = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const stacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) =>
|
||||
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription !== BaseStackFormation.baseStackDecription,
|
||||
) || [];
|
||||
CloudRunnerLogger.log(`Stacks ${stacks.length}`);
|
||||
for (const element of stacks) {
|
||||
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element);
|
||||
}
|
||||
}
|
||||
public static async awsDescribeJob(job: string) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const stack = (await CF.listStacks().promise()).StackSummaries?.find((_x) => _x.StackName === job) || undefined;
|
||||
const stackInfo = (await CF.describeStackResources({ StackName: job }).promise()) || undefined;
|
||||
const stackInfo2 = (await CF.describeStacks({ StackName: job }).promise()) || undefined;
|
||||
if (stack === undefined) {
|
||||
throw new Error('stack not defined');
|
||||
}
|
||||
const ageDate: Date = new Date(Date.now() - stack.CreationTime.getTime());
|
||||
const message = `
|
||||
Task Stack ${stack.StackName}
|
||||
Age D${Math.floor(ageDate.getHours() / 24)} H${ageDate.getHours()} M${ageDate.getMinutes()}
|
||||
${JSON.stringify(stack, undefined, 4)}
|
||||
${JSON.stringify(stackInfo, undefined, 4)}
|
||||
${JSON.stringify(stackInfo2, undefined, 4)}
|
||||
`;
|
||||
CloudRunnerLogger.log(message);
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
|
||||
|
||||
export class TertiaryResourcesService {
|
||||
public static async awsListLogGroups(perResultCallback: any = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.CloudWatchLogs();
|
||||
let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = {
|
||||
/* logGroupNamePrefix: 'game-ci' */
|
||||
};
|
||||
let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
const logGroups = logGroupsDescribe.logGroups || [];
|
||||
while (logGroupsDescribe.nextToken) {
|
||||
logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken };
|
||||
logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
logGroups.push(...(logGroupsDescribe?.logGroups || []));
|
||||
}
|
||||
|
||||
CloudRunnerLogger.log(`Log Groups ${logGroups.length}`);
|
||||
for (const element of logGroups) {
|
||||
if (element.creationTime === undefined) {
|
||||
CloudRunnerLogger.log(`Skipping ${element.logGroupName} no createdAt date`);
|
||||
continue;
|
||||
}
|
||||
const ageDate: Date = new Date(Date.now() - element.creationTime);
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.logGroupName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
if (perResultCallback) await perResultCallback(element, element);
|
||||
}
|
||||
}
|
||||
}
|
148
src/model/cloud-runner/providers/docker/index.ts
Normal file
148
src/model/cloud-runner/providers/docker/index.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import Docker from '../../../docker';
|
||||
import { Action } from '../../..';
|
||||
import { writeFileSync } from 'fs';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import { CloudRunnerSystem } from '../../services/cloud-runner-system';
|
||||
import * as fs from 'fs';
|
||||
|
||||
class LocalDockerCloudRunner implements ProviderInterface {
|
||||
public buildParameters: BuildParameters | undefined;
|
||||
|
||||
listResources(): Promise<ProviderResource[]> {
|
||||
return new Promise((resolve) => resolve([]));
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return new Promise((result) => result(``));
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
const { workspace } = Action;
|
||||
if (fs.existsSync(`${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar.lz4`)) {
|
||||
await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache/cache/build/`);
|
||||
await CloudRunnerSystem.Run(
|
||||
`rm -r ${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar.lz4`,
|
||||
);
|
||||
}
|
||||
}
|
||||
setupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
this.buildParameters = buildParameters;
|
||||
}
|
||||
|
||||
public async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
CloudRunnerLogger.log(buildGuid);
|
||||
CloudRunnerLogger.log(commands);
|
||||
|
||||
const { workspace, actionFolder } = Action;
|
||||
const content: any[] = [];
|
||||
for (const x of secrets) {
|
||||
content.push({ name: x.EnvironmentVariable, value: x.ParameterValue });
|
||||
}
|
||||
for (const x of environment) {
|
||||
content.push({ name: x.name, value: x.value });
|
||||
}
|
||||
|
||||
// if (this.buildParameters?.cloudRunnerIntegrationTests) {
|
||||
// core.info(JSON.stringify(content, undefined, 4));
|
||||
// core.info(JSON.stringify(secrets, undefined, 4));
|
||||
// core.info(JSON.stringify(environment, undefined, 4));
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line unicorn/no-for-loop
|
||||
for (let index = 0; index < content.length; index++) {
|
||||
if (content[index] === undefined) {
|
||||
delete content[index];
|
||||
}
|
||||
}
|
||||
let myOutput = '';
|
||||
const sharedFolder = `/data/`;
|
||||
|
||||
// core.info(JSON.stringify({ workspace, actionFolder, ...this.buildParameters, ...content }, undefined, 4));
|
||||
const entrypointFilePath = `start.sh`;
|
||||
const fileContents = `#!/bin/bash
|
||||
set -e
|
||||
|
||||
mkdir -p /github/workspace/cloud-runner-cache
|
||||
mkdir -p /data/cache
|
||||
cp -a /github/workspace/cloud-runner-cache/. ${sharedFolder}
|
||||
${commands}
|
||||
cp -a ${sharedFolder}. /github/workspace/cloud-runner-cache/
|
||||
`;
|
||||
writeFileSync(`${workspace}/${entrypointFilePath}`, fileContents, {
|
||||
flag: 'w',
|
||||
});
|
||||
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Running local-docker: \n ${fileContents}`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(`${workspace}/cloud-runner-cache`)) {
|
||||
await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache && du -sh ${workspace}/cloud-runner-cache`);
|
||||
}
|
||||
await Docker.run(
|
||||
image,
|
||||
{ workspace, actionFolder, ...this.buildParameters },
|
||||
false,
|
||||
`chmod +x /github/workspace/${entrypointFilePath} && /github/workspace/${entrypointFilePath}`,
|
||||
content,
|
||||
{
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
myOutput += data.toString();
|
||||
},
|
||||
stderr: (data: Buffer) => {
|
||||
myOutput += `[LOCAL-DOCKER-ERROR]${data.toString()}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return myOutput;
|
||||
}
|
||||
}
|
||||
export default LocalDockerCloudRunner;
|
@ -1,5 +1,5 @@
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import { BuildParameters, Output } from '../../..';
|
||||
import { BuildParameters } from '../../..';
|
||||
import * as core from '@actions/core';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
@ -7,39 +7,84 @@ import KubernetesStorage from './kubernetes-storage';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import KubernetesTaskRunner from './kubernetes-task-runner';
|
||||
import KubernetesSecret from './kubernetes-secret';
|
||||
import waitUntil from 'async-wait-until';
|
||||
import KubernetesJobSpecFactory from './kubernetes-job-spec-factory';
|
||||
import KubernetesServiceAccount from './kubernetes-service-account';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
import DependencyOverrideService from '../../services/depdency-override-service';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import KubernetesPods from './kubernetes-pods';
|
||||
|
||||
class Kubernetes implements ProviderInterface {
|
||||
private kubeConfig: k8s.KubeConfig;
|
||||
private kubeClient: k8s.CoreV1Api;
|
||||
private kubeClientBatch: k8s.BatchV1Api;
|
||||
private buildGuid: string = '';
|
||||
private buildParameters: BuildParameters;
|
||||
private pvcName: string = '';
|
||||
private secretName: string = '';
|
||||
private jobName: string = '';
|
||||
private namespace: string;
|
||||
private podName: string = '';
|
||||
private containerName: string = '';
|
||||
private cleanupCronJobName: string = '';
|
||||
private serviceAccountName: string = '';
|
||||
public static Instance: Kubernetes;
|
||||
public kubeConfig!: k8s.KubeConfig;
|
||||
public kubeClient!: k8s.CoreV1Api;
|
||||
public kubeClientBatch!: k8s.BatchV1Api;
|
||||
public buildGuid: string = '';
|
||||
public buildParameters!: BuildParameters;
|
||||
public pvcName: string = '';
|
||||
public secretName: string = '';
|
||||
public jobName: string = '';
|
||||
public namespace!: string;
|
||||
public podName: string = '';
|
||||
public containerName: string = '';
|
||||
public cleanupCronJobName: string = '';
|
||||
public serviceAccountName: string = '';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
Kubernetes.Instance = this;
|
||||
this.kubeConfig = new k8s.KubeConfig();
|
||||
this.kubeConfig.loadFromDefault();
|
||||
this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
|
||||
this.kubeClientBatch = this.kubeConfig.makeApiClient(k8s.BatchV1Api);
|
||||
CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment');
|
||||
|
||||
this.namespace = 'default';
|
||||
this.buildParameters = buildParameters;
|
||||
CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment');
|
||||
}
|
||||
public async setup(
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
const pods = await this.kubeClient.listNamespacedPod(this.namespace);
|
||||
const serviceAccounts = await this.kubeClient.listNamespacedServiceAccount(this.namespace);
|
||||
const secrets = await this.kubeClient.listNamespacedSecret(this.namespace);
|
||||
const jobs = await this.kubeClientBatch.listNamespacedJob(this.namespace);
|
||||
|
||||
return [
|
||||
...pods.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
...serviceAccounts.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
...secrets.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
...jobs.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
];
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return new Promise((result) => result(``));
|
||||
}
|
||||
public async setupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -48,12 +93,11 @@ class Kubernetes implements ProviderInterface {
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
try {
|
||||
this.pvcName = `unity-builder-pvc-${buildGuid}`;
|
||||
this.cleanupCronJobName = `unity-builder-cronjob-${buildGuid}`;
|
||||
this.serviceAccountName = `service-account-${buildGuid}`;
|
||||
if (await DependencyOverrideService.CheckHealth()) {
|
||||
await DependencyOverrideService.TryStartDependencies();
|
||||
}
|
||||
this.buildParameters = buildParameters;
|
||||
const id = buildParameters.retainWorkspace ? CloudRunner.lockedWorkspace : buildParameters.buildGuid;
|
||||
this.pvcName = `unity-builder-pvc-${id}`;
|
||||
this.cleanupCronJobName = `unity-builder-cronjob-${id}`;
|
||||
this.serviceAccountName = `service-account-${buildParameters.buildGuid}`;
|
||||
await KubernetesStorage.createPersistentVolumeClaim(
|
||||
buildParameters,
|
||||
this.pvcName,
|
||||
@ -67,7 +111,7 @@ class Kubernetes implements ProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async runTask(
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
@ -77,40 +121,22 @@ class Kubernetes implements ProviderInterface {
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
try {
|
||||
CloudRunnerLogger.log('Cloud Runner K8s workflow!');
|
||||
|
||||
// Setup
|
||||
this.buildGuid = buildGuid;
|
||||
this.secretName = `build-credentials-${buildGuid}`;
|
||||
this.jobName = `unity-builder-job-${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);
|
||||
const jobSpec = KubernetesJobSpecFactory.getJobSpec(
|
||||
commands,
|
||||
image,
|
||||
mountdir,
|
||||
workingdir,
|
||||
environment,
|
||||
secrets,
|
||||
this.buildGuid,
|
||||
this.buildParameters,
|
||||
this.secretName,
|
||||
this.pvcName,
|
||||
this.jobName,
|
||||
k8s,
|
||||
);
|
||||
|
||||
// Run
|
||||
const jobResult = await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
|
||||
CloudRunnerLogger.log(`Creating build job ${JSON.stringify(jobResult.body.metadata, undefined, 4)}`);
|
||||
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
CloudRunnerLogger.log('Job created');
|
||||
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 {
|
||||
await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace);
|
||||
CloudRunnerLogger.log('Pod running, streaming logs');
|
||||
output = await KubernetesTaskRunner.runTask(
|
||||
this.kubeConfig,
|
||||
@ -120,11 +146,25 @@ class Kubernetes implements ProviderInterface {
|
||||
'main',
|
||||
this.namespace,
|
||||
);
|
||||
const running = await KubernetesPods.IsPodRunning(this.podName, this.namespace, this.kubeClient);
|
||||
|
||||
if (!running) {
|
||||
CloudRunnerLogger.log(`Pod not found, assumed ended!`);
|
||||
break;
|
||||
} else {
|
||||
CloudRunnerLogger.log('Pod still running, recovering stream...');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message.includes(`HTTP`)) {
|
||||
const reason = error.response?.body?.reason || error.reason || ``;
|
||||
const errorMessage = error.message || ``;
|
||||
|
||||
const continueStreaming = reason === `NotFound` || errorMessage.includes(`dial timeout, backstop`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -140,6 +180,44 @@ class Kubernetes implements ProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private async createNamespacedJob(
|
||||
commands: string,
|
||||
image: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
) {
|
||||
for (let index = 0; index < 3; index++) {
|
||||
try {
|
||||
const jobSpec = KubernetesJobSpecFactory.getJobSpec(
|
||||
commands,
|
||||
image,
|
||||
mountdir,
|
||||
workingdir,
|
||||
environment,
|
||||
secrets,
|
||||
this.buildGuid,
|
||||
this.buildParameters,
|
||||
this.secretName,
|
||||
this.pvcName,
|
||||
this.jobName,
|
||||
k8s,
|
||||
);
|
||||
await new Promise((promise) => setTimeout(promise, 15000));
|
||||
await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
|
||||
CloudRunnerLogger.log(`Build job created`);
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
CloudRunnerLogger.log('Job created');
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`Error occured creating job: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPodNameAndContainerName(pod: k8s.V1Pod) {
|
||||
this.podName = pod.metadata?.name || '';
|
||||
this.containerName = pod.status?.containerStatuses?.[0].name || '';
|
||||
@ -150,32 +228,24 @@ class Kubernetes implements ProviderInterface {
|
||||
try {
|
||||
await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
|
||||
await this.kubeClient.deleteNamespacedPod(this.podName, this.namespace);
|
||||
await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log('Failed to cleanup, error:');
|
||||
core.error(JSON.stringify(error, undefined, 4));
|
||||
CloudRunnerLogger.log('Abandoning cleanup, build error:');
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup`);
|
||||
if (error.response.body.reason !== `NotFound`) {
|
||||
CloudRunnerLogger.log(`Wasn't a not found error: ${error.response.body.reason}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await waitUntil(
|
||||
async () => {
|
||||
const jobBody = (await this.kubeClientBatch.readNamespacedJob(this.jobName, this.namespace)).body;
|
||||
const podBody = (await this.kubeClient.readNamespacedPod(this.podName, this.namespace)).body;
|
||||
|
||||
return (jobBody === null || jobBody.status?.active === 0) && podBody === null;
|
||||
},
|
||||
{
|
||||
timeout: 500000,
|
||||
intervalBetweenAttempts: 15000,
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup secret`);
|
||||
CloudRunnerLogger.log(error.response.body.reason);
|
||||
}
|
||||
CloudRunnerLogger.log('cleaned up Secret, Job and Pod');
|
||||
CloudRunnerLogger.log('cleaning up finished');
|
||||
}
|
||||
|
||||
async cleanup(
|
||||
async cleanupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -183,11 +253,19 @@ class Kubernetes implements ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
if (buildParameters.retainWorkspace) {
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`deleting PVC`);
|
||||
|
||||
try {
|
||||
await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
|
||||
await Output.setBuildVersion(buildParameters.buildVersion);
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit();
|
||||
await this.kubeClient.deleteNamespacedServiceAccount(this.serviceAccountName, this.namespace);
|
||||
CloudRunnerLogger.log('cleaned up PVC and Service Account');
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Cleanup failed ${JSON.stringify(error, undefined, 4)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } from '@kubernetes/client-node';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { CloudRunnerBuildCommandProcessor } from '../../services/cloud-runner-build-command-process';
|
||||
import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
@ -103,7 +103,7 @@ class KubernetesJobSpecFactory {
|
||||
name: 'main',
|
||||
image,
|
||||
command: ['/bin/sh'],
|
||||
args: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(command, CloudRunner.buildParameters)],
|
||||
args: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(command, CloudRunner.buildParameters)],
|
||||
|
||||
workingDir: `${workingDirectory}`,
|
||||
resources: {
|
||||
|
14
src/model/cloud-runner/providers/k8s/kubernetes-pods.ts
Normal file
14
src/model/cloud-runner/providers/k8s/kubernetes-pods.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
class KubernetesPods {
|
||||
public static async IsPodRunning(podName: string, namespace: string, kubeClient: CoreV1Api) {
|
||||
const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.filter((x) => podName === x.metadata?.name);
|
||||
const running = pods.length > 0 && (pods[0].status?.phase === `Running` || pods[0].status?.phase === `Pending`);
|
||||
const phase = pods[0].status?.phase || 'undefined status';
|
||||
CloudRunnerLogger.log(`Getting pod status: ${phase}`);
|
||||
|
||||
return running;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesPods;
|
@ -1,6 +1,7 @@
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
const base64 = require('base-64');
|
||||
|
||||
class KubernetesSecret {
|
||||
@ -10,6 +11,7 @@ class KubernetesSecret {
|
||||
namespace: string,
|
||||
kubeClient: CoreV1Api,
|
||||
) {
|
||||
try {
|
||||
const secret = new k8s.V1Secret();
|
||||
secret.apiVersion = 'v1';
|
||||
secret.kind = 'Secret';
|
||||
@ -21,8 +23,22 @@ class KubernetesSecret {
|
||||
for (const buildSecret of secrets) {
|
||||
secret.data[buildSecret.ParameterKey] = base64.encode(buildSecret.ParameterValue);
|
||||
}
|
||||
CloudRunnerLogger.log(`Creating secret: ${secretName}`);
|
||||
const existingSecrets = await kubeClient.listNamespacedSecret(namespace);
|
||||
const mappedSecrets = existingSecrets.body.items.map((x) => {
|
||||
return x.metadata?.name || `no name`;
|
||||
});
|
||||
|
||||
return kubeClient.createNamespacedSecret(namespace, secret);
|
||||
CloudRunnerLogger.log(
|
||||
`ExistsAlready: ${mappedSecrets.includes(secretName)} SecretsCount: ${mappedSecrets.length}`,
|
||||
);
|
||||
await new Promise((promise) => setTimeout(promise, 15000));
|
||||
await kubeClient.createNamespacedSecret(namespace, secret);
|
||||
CloudRunnerLogger.log('Created secret');
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`Created secret failed ${error}`);
|
||||
throw new Error(`Failed to create kubernetes secret`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ 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 YAML from 'yaml';
|
||||
import { IncomingMessage } from 'http';
|
||||
import GitHub from '../../../github';
|
||||
|
||||
class KubernetesStorage {
|
||||
public static async createPersistentVolumeClaim(
|
||||
@ -13,20 +13,19 @@ class KubernetesStorage {
|
||||
kubeClient: k8s.CoreV1Api,
|
||||
namespace: string,
|
||||
) {
|
||||
if (buildParameters.kubeVolume) {
|
||||
CloudRunnerLogger.log(buildParameters.kubeVolume);
|
||||
if (buildParameters.kubeVolume !== ``) {
|
||||
CloudRunnerLogger.log(`Kube Volume was input was set ${buildParameters.kubeVolume} overriding ${pvcName}`);
|
||||
pvcName = buildParameters.kubeVolume;
|
||||
|
||||
return;
|
||||
}
|
||||
const pvcList = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items.map(
|
||||
(x) => x.metadata?.name,
|
||||
);
|
||||
const allPvc = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items;
|
||||
const pvcList = allPvc.map((x) => x.metadata?.name);
|
||||
CloudRunnerLogger.log(`Current PVCs in namespace ${namespace}`);
|
||||
CloudRunnerLogger.log(JSON.stringify(pvcList, undefined, 4));
|
||||
if (pvcList.includes(pvcName)) {
|
||||
CloudRunnerLogger.log(`pvc ${pvcName} already exists`);
|
||||
if (!buildParameters.isCliMode) {
|
||||
if (GitHub.githubInputEnabled) {
|
||||
core.setOutput('volume', pvcName);
|
||||
}
|
||||
|
||||
@ -95,9 +94,6 @@ class KubernetesStorage {
|
||||
},
|
||||
},
|
||||
};
|
||||
if (process.env.K8s_STORAGE_PVC_SPEC) {
|
||||
YAML.parse(process.env.K8s_STORAGE_PVC_SPEC);
|
||||
}
|
||||
const result = await kubeClient.createNamespacedPersistentVolumeClaim(namespace, pvc);
|
||||
|
||||
return result;
|
||||
|
@ -1,49 +0,0 @@
|
||||
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 { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
|
||||
class LocalDockerCloudRunner implements ProviderInterface {
|
||||
cleanup(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
setup(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public runTask(
|
||||
commands: string,
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
image: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
mountdir: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
workingdir: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
CloudRunnerLogger.log(buildGuid);
|
||||
CloudRunnerLogger.log(commands);
|
||||
|
||||
return CloudRunnerSystem.Run(commands, false, false);
|
||||
}
|
||||
}
|
||||
export default LocalDockerCloudRunner;
|
@ -4,9 +4,34 @@ import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environm
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
class LocalCloudRunner implements ProviderInterface {
|
||||
cleanup(
|
||||
listResources(): Promise<ProviderResource[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -16,7 +41,7 @@ class LocalCloudRunner implements ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public setup(
|
||||
public setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -26,7 +51,7 @@ class LocalCloudRunner implements ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public async runTask(
|
||||
public async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import BuildParameters from '../../build-parameters';
|
||||
import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import { ProviderResource } from './provider-resource';
|
||||
import { ProviderWorkflow } from './provider-workflow';
|
||||
|
||||
export interface ProviderInterface {
|
||||
cleanup(
|
||||
cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -13,7 +15,7 @@ export interface ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
);
|
||||
setup(
|
||||
setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -23,7 +25,7 @@ export interface ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
);
|
||||
runTask(
|
||||
runTaskInWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -39,4 +41,19 @@ export interface ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string>;
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string>;
|
||||
listResources(): Promise<ProviderResource[]>;
|
||||
listWorkflow(): Promise<ProviderWorkflow[]>;
|
||||
watchWorkflow(): Promise<string>;
|
||||
}
|
||||
|
3
src/model/cloud-runner/providers/provider-resource.ts
Normal file
3
src/model/cloud-runner/providers/provider-resource.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export class ProviderResource {
|
||||
public Name!: string;
|
||||
}
|
3
src/model/cloud-runner/providers/provider-workflow.ts
Normal file
3
src/model/cloud-runner/providers/provider-workflow.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export class ProviderWorkflow {
|
||||
public Name!: string;
|
||||
}
|
@ -3,9 +3,28 @@ import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environm
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
class TestCloudRunner implements ProviderInterface {
|
||||
cleanup(
|
||||
listResources(): Promise<ProviderResource[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -15,7 +34,7 @@ class TestCloudRunner implements ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
setup(
|
||||
setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -25,7 +44,7 @@ class TestCloudRunner implements ProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public async runTask(
|
||||
public async runTaskInWorkflow(
|
||||
commands: string,
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
|
@ -46,40 +46,49 @@ export class Caching {
|
||||
public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheArtifactName: string) {
|
||||
cacheArtifactName = cacheArtifactName.replace(' ', '');
|
||||
const startPath = process.cwd();
|
||||
const compressionSuffix = CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '';
|
||||
try {
|
||||
if (!(await fileExists(cacheFolder))) {
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`);
|
||||
}
|
||||
process.chdir(path.resolve(sourceFolder, '..'));
|
||||
|
||||
if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) {
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(
|
||||
`Hashed cache folder ${await LfsHashing.hashAllFiles(sourceFolder)} ${sourceFolder} ${path.basename(
|
||||
sourceFolder,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line func-style
|
||||
const formatFunction = function (format: string) {
|
||||
const arguments_ = Array.prototype.slice.call(
|
||||
[path.resolve(sourceFolder, '..'), cacheFolder, cacheArtifactName],
|
||||
1,
|
||||
const contents = await fs.promises.readdir(path.basename(sourceFolder));
|
||||
CloudRunnerLogger.log(
|
||||
`There is ${contents.length} files/dir in the source folder ${path.basename(sourceFolder)}`,
|
||||
);
|
||||
|
||||
return format.replace(/{(\d+)}/g, function (match, number) {
|
||||
return typeof arguments_[number] != 'undefined' ? arguments_[number] : match;
|
||||
});
|
||||
};
|
||||
await CloudRunnerSystem.Run(`tar -cf ${cacheArtifactName}.tar ${path.basename(sourceFolder)}`);
|
||||
assert(await fileExists(`${cacheArtifactName}.tar`), 'cache archive exists');
|
||||
assert(await fileExists(path.basename(sourceFolder)), 'source folder exists');
|
||||
if (CloudRunner.buildParameters.cachePushOverrideCommand) {
|
||||
await CloudRunnerSystem.Run(formatFunction(CloudRunner.buildParameters.cachePushOverrideCommand));
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
// await CloudRunnerSystem.Run(`tree -L 2 ./..`);
|
||||
// await CloudRunnerSystem.Run(`tree -L 2`);
|
||||
}
|
||||
await CloudRunnerSystem.Run(`mv ${cacheArtifactName}.tar ${cacheFolder}`);
|
||||
|
||||
if (contents.length === 0) {
|
||||
CloudRunnerLogger.log(
|
||||
`Did not push source folder to cache because it was empty ${path.basename(sourceFolder)}`,
|
||||
);
|
||||
process.chdir(`${startPath}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await CloudRunnerSystem.Run(
|
||||
`tar -cf ${cacheArtifactName}.tar${compressionSuffix} ${path.basename(sourceFolder)}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`du ${cacheArtifactName}.tar${compressionSuffix}`);
|
||||
assert(await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`), 'cache archive exists');
|
||||
assert(await fileExists(path.basename(sourceFolder)), 'source folder exists');
|
||||
await CloudRunnerSystem.Run(`mv ${cacheArtifactName}.tar${compressionSuffix} ${cacheFolder}`);
|
||||
RemoteClientLogger.log(`moved cache entry ${cacheArtifactName} to ${cacheFolder}`);
|
||||
assert(
|
||||
await fileExists(`${path.join(cacheFolder, cacheArtifactName)}.tar`),
|
||||
await fileExists(`${path.join(cacheFolder, cacheArtifactName)}.tar${compressionSuffix}`),
|
||||
'cache archive exists inside cache folder',
|
||||
);
|
||||
} catch (error) {
|
||||
@ -90,6 +99,7 @@ export class Caching {
|
||||
}
|
||||
public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheArtifactName: string = ``) {
|
||||
cacheArtifactName = cacheArtifactName.replace(' ', '');
|
||||
const compressionSuffix = CloudRunner.buildParameters.useLz4Compression ? '.lz4' : '';
|
||||
const startPath = process.cwd();
|
||||
RemoteClientLogger.log(`Caching for ${path.basename(destinationFolder)}`);
|
||||
try {
|
||||
@ -101,38 +111,26 @@ export class Caching {
|
||||
await fs.promises.mkdir(destinationFolder);
|
||||
}
|
||||
|
||||
const latestInBranch = await (await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .tar$ | head -1`))
|
||||
const latestInBranch = await (
|
||||
await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .tar${compressionSuffix}$ | head -1`)
|
||||
)
|
||||
.replace(/\n/g, ``)
|
||||
.replace('.tar', '');
|
||||
.replace(`.tar${compressionSuffix}`, '');
|
||||
|
||||
process.chdir(cacheFolder);
|
||||
|
||||
const cacheSelection =
|
||||
cacheArtifactName !== `` && (await fileExists(`${cacheArtifactName}.tar`)) ? cacheArtifactName : latestInBranch;
|
||||
cacheArtifactName !== `` && (await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`))
|
||||
? cacheArtifactName
|
||||
: latestInBranch;
|
||||
await CloudRunnerLogger.log(`cache key ${cacheArtifactName} selection ${cacheSelection}`);
|
||||
|
||||
// eslint-disable-next-line func-style
|
||||
const formatFunction = function (format: string) {
|
||||
const arguments_ = Array.prototype.slice.call(
|
||||
[path.resolve(destinationFolder, '..'), cacheFolder, cacheArtifactName],
|
||||
1,
|
||||
);
|
||||
|
||||
return format.replace(/{(\d+)}/g, function (match, number) {
|
||||
return typeof arguments_[number] != 'undefined' ? arguments_[number] : match;
|
||||
});
|
||||
};
|
||||
|
||||
if (CloudRunner.buildParameters.cachePullOverrideCommand) {
|
||||
await CloudRunnerSystem.Run(formatFunction(CloudRunner.buildParameters.cachePullOverrideCommand));
|
||||
}
|
||||
|
||||
if (await fileExists(`${cacheSelection}.tar`)) {
|
||||
if (await fileExists(`${cacheSelection}.tar${compressionSuffix}`)) {
|
||||
const resultsFolder = `results${CloudRunner.buildParameters.buildGuid}`;
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${resultsFolder}`);
|
||||
RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.tar`);
|
||||
RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.tar${compressionSuffix}`);
|
||||
const fullResultsFolder = path.join(cacheFolder, resultsFolder);
|
||||
await CloudRunnerSystem.Run(`tar -xf ${cacheSelection}.tar -C ${fullResultsFolder}`);
|
||||
await CloudRunnerSystem.Run(`tar -xf ${cacheSelection}.tar${compressionSuffix} -C ${fullResultsFolder}`);
|
||||
RemoteClientLogger.log(`cache item extracted to ${fullResultsFolder}`);
|
||||
assert(await fileExists(fullResultsFolder), `cache extraction results folder exists`);
|
||||
const destinationParentFolder = path.resolve(destinationFolder, '..');
|
||||
@ -152,15 +150,17 @@ export class Caching {
|
||||
} else {
|
||||
RemoteClientLogger.logWarning(`cache item ${cacheArtifactName} doesn't exist ${destinationFolder}`);
|
||||
if (cacheSelection !== ``) {
|
||||
RemoteClientLogger.logWarning(`cache item ${cacheArtifactName}.tar doesn't exist ${destinationFolder}`);
|
||||
RemoteClientLogger.logWarning(
|
||||
`cache item ${cacheArtifactName}.tar${compressionSuffix} doesn't exist ${destinationFolder}`,
|
||||
);
|
||||
throw new Error(`Failed to get cache item, but cache hit was found: ${cacheSelection}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
process.chdir(`${startPath}`);
|
||||
process.chdir(startPath);
|
||||
throw error;
|
||||
}
|
||||
process.chdir(`${startPath}`);
|
||||
process.chdir(startPath);
|
||||
}
|
||||
|
||||
public static async handleCachePurging() {
|
||||
|
@ -9,34 +9,43 @@ import { assert } from 'console';
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import { CliFunction } from '../../cli/cli-functions-repository';
|
||||
import { CloudRunnerSystem } from '../services/cloud-runner-system';
|
||||
import YAML from 'yaml';
|
||||
|
||||
export class RemoteClient {
|
||||
public static async bootstrapRepository() {
|
||||
try {
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.repoPathAbsolute}`);
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.cacheFolderFull}`);
|
||||
process.chdir(CloudRunnerFolders.repoPathAbsolute);
|
||||
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();
|
||||
await RemoteClient.sizeOfFolder('repo before lfs cache pull', CloudRunnerFolders.repoPathAbsolute);
|
||||
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.lfsCacheFolderFull,
|
||||
CloudRunnerFolders.lfsFolderAbsolute,
|
||||
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.lfsCacheFolderFull,
|
||||
CloudRunnerFolders.lfsFolderAbsolute,
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute),
|
||||
`${lfsHashes.lfsGuidSum}`,
|
||||
);
|
||||
await Caching.PullFromCache(CloudRunnerFolders.libraryCacheFolderFull, CloudRunnerFolders.libraryFolderAbsolute);
|
||||
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) {
|
||||
@ -45,37 +54,69 @@ export class RemoteClient {
|
||||
}
|
||||
|
||||
private static async sizeOfFolder(message: string, folder: string) {
|
||||
if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) {
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Size of ${message}`);
|
||||
await CloudRunnerSystem.Run(`du -sh ${folder}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async cloneRepoWithoutLFSFiles() {
|
||||
process.chdir(`${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
|
||||
if (
|
||||
CloudRunner.buildParameters.retainWorkspace &&
|
||||
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}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`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)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
process.chdir(`${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"`);
|
||||
await CloudRunnerSystem.Run(
|
||||
`git clone -q ${CloudRunnerFolders.targetBuildRepoUrl} ${path.resolve(
|
||||
`..`,
|
||||
path.basename(CloudRunnerFolders.repoPathAbsolute),
|
||||
)}`,
|
||||
`git clone -q ${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 ${process.env.GITHUB_SHA}`);
|
||||
RemoteClientLogger.log(`Checked out ${CloudRunner.buildParameters.branch}`);
|
||||
} catch (error) {
|
||||
await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static replaceLargePackageReferencesWithSharedReferences() {
|
||||
const manifest = fs.readFileSync(
|
||||
path.join(CloudRunnerFolders.projectPathAbsolute, `Packages/manifest.json`),
|
||||
'utf8',
|
||||
);
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(manifest);
|
||||
}
|
||||
if (CloudRunner.buildParameters.useSharedLargePackages) {
|
||||
manifest.replace(/LargePackages/g, '../../LargePackages');
|
||||
}
|
||||
}
|
||||
|
||||
private static async pullLatestLFS() {
|
||||
process.chdir(CloudRunnerFolders.repoPathAbsolute);
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge -- %f"`);
|
||||
@ -85,13 +126,36 @@ export class RemoteClient {
|
||||
assert(fs.existsSync(CloudRunnerFolders.lfsFolderAbsolute));
|
||||
}
|
||||
|
||||
@CliFunction(`remote-cli`, `sets up a repository, usually before a game-ci build`)
|
||||
@CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
|
||||
static async runRemoteClientJob() {
|
||||
const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}');
|
||||
RemoteClientLogger.log(`Build Params:
|
||||
${JSON.stringify(buildParameter, undefined, 4)}
|
||||
`);
|
||||
CloudRunner.buildParameters = buildParameter;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
static handleRetainedWorkspace() {
|
||||
if (!CloudRunner.buildParameters.retainWorkspace) {
|
||||
return;
|
||||
}
|
||||
RemoteClientLogger.log(`Retained Workspace: ${CloudRunner.lockedWorkspace}`);
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { BuildParameters } from '../..';
|
||||
import YAML from 'yaml';
|
||||
import CloudRunnerSecret from './cloud-runner-secret';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
|
||||
export class CloudRunnerBuildCommandProcessor {
|
||||
public static ProcessCommands(commands: string, buildParameters: BuildParameters): string {
|
||||
const hooks = CloudRunnerBuildCommandProcessor.getHooks(buildParameters.customJobHooks).filter((x) =>
|
||||
x.step.includes(`all`),
|
||||
);
|
||||
|
||||
return `echo "---"
|
||||
echo "start cloud runner init"
|
||||
${CloudRunner.buildParameters.cloudRunnerIntegrationTests ? '' : '#'} 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): Hook[] {
|
||||
const experimentHooks = customJobHooks;
|
||||
let output = new Array<Hook>();
|
||||
if (experimentHooks && experimentHooks !== '') {
|
||||
try {
|
||||
output = YAML.parse(experimentHooks);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return output.filter((x) => x.step !== undefined && x.hook !== undefined && x.hook.length > 0);
|
||||
}
|
||||
}
|
||||
export class Hook {
|
||||
public commands;
|
||||
public secrets: CloudRunnerSecret[] = new Array<CloudRunnerSecret>();
|
||||
public name;
|
||||
public hook!: string[];
|
||||
public step!: string[];
|
||||
}
|
117
src/model/cloud-runner/services/cloud-runner-custom-hooks.ts
Normal file
117
src/model/cloud-runner/services/cloud-runner-custom-hooks.ts
Normal file
@ -0,0 +1,117 @@
|
||||
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 'path';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import * as fs from '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): Hook[] {
|
||||
const experimentHooks = customJobHooks;
|
||||
let output = new Array<Hook>();
|
||||
if (experimentHooks && experimentHooks !== '') {
|
||||
try {
|
||||
output = YAML.parse(experimentHooks);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (object.secrets === undefined) {
|
||||
object.secrets = [];
|
||||
|
||||
return;
|
||||
}
|
||||
object.secrets = object.secrets.map((x) => {
|
||||
return {
|
||||
ParameterKey: x.name,
|
||||
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
|
||||
ParameterValue: x.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
public secrets: CloudRunnerSecret[] = new Array<CloudRunnerSecret>();
|
||||
public name;
|
||||
public hook!: string[];
|
||||
public step!: string[];
|
||||
}
|
206
src/model/cloud-runner/services/cloud-runner-custom-steps.ts
Normal file
206
src/model/cloud-runner/services/cloud-runner-custom-steps.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import YAML from 'yaml';
|
||||
import CloudRunnerSecret from './cloud-runner-secret';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import * as core from '@actions/core';
|
||||
import { CustomWorkflow } from '../workflows/custom-workflow';
|
||||
import { RemoteClientLogger } from '../remote-client/remote-client-logger';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import Input from '../../input';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
|
||||
export class CloudRunnerCustomSteps {
|
||||
static GetCustomStepsFromFiles(hookLifecycle: string): CustomStep[] {
|
||||
const results: CustomStep[] = [];
|
||||
RemoteClientLogger.log(
|
||||
`GetCustomStepFiles: ${hookLifecycle} CustomStepFiles: ${CloudRunnerOptions.customStepFiles}`,
|
||||
);
|
||||
try {
|
||||
const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `steps`);
|
||||
const files = fs.readdirSync(gameCiCustomStepsPath);
|
||||
for (const file of files) {
|
||||
if (!CloudRunnerOptions.customStepFiles.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];
|
||||
if (fileContentsObject.hook === 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)}`);
|
||||
|
||||
const builtInCustomSteps: CustomStep[] = CloudRunnerCustomSteps.ParseSteps(
|
||||
`- name: aws-s3-upload-build
|
||||
image: amazon/aws-cli
|
||||
hook: after
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 cp /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4
|
||||
rm /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
value: ${process.env.AWS_REGION || ``}
|
||||
- name: aws-s3-upload-cache
|
||||
image: amazon/aws-cli
|
||||
hook: after
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/lfs
|
||||
rm -r /data/cache/$CACHE_KEY/lfs
|
||||
aws s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/Library
|
||||
rm -r /data/cache/$CACHE_KEY/Library
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
value: ${process.env.AWS_REGION || ``}
|
||||
- name: aws-s3-pull-cache
|
||||
image: amazon/aws-cli
|
||||
hook: before
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 ls game-ci-test-storage/cloud-runner-cache/ || true
|
||||
aws s3 ls game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/ || true
|
||||
BUCKET1="game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/Library/"
|
||||
aws s3 ls $BUCKET1 || true
|
||||
OBJECT1="$(aws s3 ls $BUCKET1 | sort | tail -n 1 | awk '{print $4}' || '')"
|
||||
aws s3 cp s3://$BUCKET1$OBJECT1 /data/cache/$CACHE_KEY/Library/ || true
|
||||
BUCKET2="game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/lfs/"
|
||||
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
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
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` : `#`}
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
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);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ConvertYamlSecrets(object) {
|
||||
if (object.secrets === undefined) {
|
||||
object.secrets = [];
|
||||
|
||||
return;
|
||||
}
|
||||
object.secrets = object.secrets.map((x) => {
|
||||
return {
|
||||
ParameterKey: x.name,
|
||||
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
|
||||
ParameterValue: x.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static ParseSteps(steps: string): CustomStep[] {
|
||||
if (steps === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) {
|
||||
|
||||
// CloudRunnerLogger.log(`Parsing build steps: ${steps}`);
|
||||
|
||||
// }
|
||||
const isArray = steps.replace(/\s/g, ``)[0] === `-`;
|
||||
const object: CustomStep[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)];
|
||||
for (const step of object) {
|
||||
CloudRunnerCustomSteps.ConvertYamlSecrets(step);
|
||||
if (step.secrets === undefined) {
|
||||
step.secrets = [];
|
||||
}
|
||||
if (step.image === undefined) {
|
||||
step.image = `ubuntu`;
|
||||
}
|
||||
}
|
||||
if (object === undefined) {
|
||||
throw new Error(`Failed to parse ${steps}`);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
static async RunPostBuildSteps(cloudRunnerStepState) {
|
||||
let output = ``;
|
||||
const steps: CustomStep[] = [
|
||||
...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.postBuildSteps),
|
||||
...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`after`),
|
||||
];
|
||||
|
||||
if (steps.length > 0) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps');
|
||||
output += await CustomWorkflow.runCustomJob(
|
||||
steps,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
static async RunPreBuildSteps(cloudRunnerStepState) {
|
||||
let output = ``;
|
||||
const steps: CustomStep[] = [
|
||||
...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.preBuildSteps),
|
||||
...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`before`),
|
||||
];
|
||||
|
||||
if (steps.length > 0) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps');
|
||||
output += await CustomWorkflow.runCustomJob(
|
||||
steps,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
export class CustomStep {
|
||||
public commands;
|
||||
public secrets: CloudRunnerSecret[] = new Array<CloudRunnerSecret>();
|
||||
public name;
|
||||
public image: string = `ubuntu`;
|
||||
public hook!: string;
|
||||
}
|
@ -1,16 +1,27 @@
|
||||
import path from 'path';
|
||||
import { CloudRunner } from '../..';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import CloudRunner from './../cloud-runner';
|
||||
|
||||
export class CloudRunnerFolders {
|
||||
public static readonly repositoryFolder = 'repo';
|
||||
|
||||
public static ToLinuxFolder(folder: string) {
|
||||
return folder.replace(/\\/g, `/`);
|
||||
}
|
||||
|
||||
// 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 path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid);
|
||||
return CloudRunner.buildParameters && CloudRunner.buildParameters.retainWorkspace && CloudRunner.lockedWorkspace
|
||||
? path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.lockedWorkspace)
|
||||
: path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid);
|
||||
}
|
||||
|
||||
public static get cacheFolderFull(): string {
|
||||
public static get cacheFolderForAllFull(): string {
|
||||
return path.join('/', CloudRunnerFolders.buildVolumeFolder, CloudRunnerFolders.cacheFolder);
|
||||
}
|
||||
|
||||
public static get cacheFolderForCacheKeyFull(): string {
|
||||
return path.join(
|
||||
'/',
|
||||
CloudRunnerFolders.buildVolumeFolder,
|
||||
@ -20,7 +31,12 @@ export class CloudRunnerFolders {
|
||||
}
|
||||
|
||||
public static get builderPathAbsolute(): string {
|
||||
return path.join(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, `builder`);
|
||||
return path.join(
|
||||
CloudRunnerOptions.useSharedBuilder
|
||||
? `/${CloudRunnerFolders.buildVolumeFolder}`
|
||||
: CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute,
|
||||
`builder`,
|
||||
);
|
||||
}
|
||||
|
||||
public static get repoPathAbsolute(): string {
|
||||
@ -48,11 +64,11 @@ export class CloudRunnerFolders {
|
||||
}
|
||||
|
||||
public static get lfsCacheFolderFull() {
|
||||
return path.join(CloudRunnerFolders.cacheFolderFull, `lfs`);
|
||||
return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `lfs`);
|
||||
}
|
||||
|
||||
public static get libraryCacheFolderFull() {
|
||||
return path.join(CloudRunnerFolders.cacheFolderFull, `Library`);
|
||||
return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `Library`);
|
||||
}
|
||||
|
||||
public static get unityBuilderRepoUrl(): string {
|
||||
|
@ -0,0 +1,10 @@
|
||||
import Input from '../../input';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
|
||||
class CloudRunnerOptionsReader {
|
||||
static GetProperties() {
|
||||
return [...Object.getOwnPropertyNames(Input), ...Object.getOwnPropertyNames(CloudRunnerOptions)];
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudRunnerOptionsReader;
|
@ -1,5 +1,6 @@
|
||||
import Input from '../../input';
|
||||
import { GenericInputReader } from '../../input-readers/generic-input-reader';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
|
||||
const formatFunction = (value, arguments_) => {
|
||||
for (const element of arguments_) {
|
||||
@ -12,6 +13,8 @@ const formatFunction = (value, arguments_) => {
|
||||
class CloudRunnerQueryOverride {
|
||||
static queryOverrides: any;
|
||||
|
||||
// TODO accept premade secret sources or custom secret source definition yamls
|
||||
|
||||
public static query(key, alternativeKey) {
|
||||
if (CloudRunnerQueryOverride.queryOverrides && CloudRunnerQueryOverride.queryOverrides[key] !== undefined) {
|
||||
return CloudRunnerQueryOverride.queryOverrides[key];
|
||||
@ -28,11 +31,11 @@ class CloudRunnerQueryOverride {
|
||||
}
|
||||
|
||||
private static shouldUseOverride(query) {
|
||||
if (Input.readInputOverrideCommand() !== '') {
|
||||
if (Input.readInputFromOverrideList() !== '') {
|
||||
if (CloudRunnerOptions.readInputOverrideCommand() !== '') {
|
||||
if (CloudRunnerOptions.readInputFromOverrideList() !== '') {
|
||||
const doesInclude =
|
||||
Input.readInputFromOverrideList().split(',').includes(query) ||
|
||||
Input.readInputFromOverrideList().split(',').includes(Input.ToEnvVarFormat(query));
|
||||
CloudRunnerOptions.readInputFromOverrideList().split(',').includes(query) ||
|
||||
CloudRunnerOptions.readInputFromOverrideList().split(',').includes(Input.ToEnvVarFormat(query));
|
||||
|
||||
return doesInclude ? true : false;
|
||||
} else {
|
||||
@ -46,11 +49,13 @@ class CloudRunnerQueryOverride {
|
||||
throw new Error(`Should not be trying to run override query on ${query}`);
|
||||
}
|
||||
|
||||
return await GenericInputReader.Run(formatFunction(Input.readInputOverrideCommand(), [{ key: 0, value: query }]));
|
||||
return await GenericInputReader.Run(
|
||||
formatFunction(CloudRunnerOptions.readInputOverrideCommand(), [{ key: 0, value: query }]),
|
||||
);
|
||||
}
|
||||
|
||||
public static async PopulateQueryOverrideInput() {
|
||||
const queries = Input.readInputFromOverrideList().split(',');
|
||||
const queries = CloudRunnerOptions.readInputFromOverrideList().split(',');
|
||||
CloudRunnerQueryOverride.queryOverrides = new Array();
|
||||
for (const element of queries) {
|
||||
if (CloudRunnerQueryOverride.shouldUseOverride(element)) {
|
||||
|
@ -2,6 +2,20 @@ import { exec } from 'child_process';
|
||||
import { RemoteClientLogger } from '../remote-client/remote-client-logger';
|
||||
|
||||
export class CloudRunnerSystem {
|
||||
public static async RunAndReadLines(command: string): Promise<string[]> {
|
||||
const result = await CloudRunnerSystem.Run(command, false, true);
|
||||
|
||||
return result
|
||||
.split(`\n`)
|
||||
.map((x) => x.replace(`\r`, ``))
|
||||
.filter((x) => x !== ``)
|
||||
.map((x) => {
|
||||
const lineValues = x.split(` `);
|
||||
|
||||
return lineValues[lineValues.length - 1];
|
||||
});
|
||||
}
|
||||
|
||||
public static async Run(command: string, suppressError = false, suppressLogs = false) {
|
||||
for (const element of command.split(`\n`)) {
|
||||
if (!suppressLogs) {
|
||||
|
@ -1,22 +0,0 @@
|
||||
import Input from '../../input';
|
||||
import { CloudRunnerSystem } from './cloud-runner-system';
|
||||
|
||||
class DependencyOverrideService {
|
||||
public static async CheckHealth() {
|
||||
if (Input.checkDependencyHealthOverride) {
|
||||
try {
|
||||
await CloudRunnerSystem.Run(Input.checkDependencyHealthOverride);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public static async TryStartDependencies() {
|
||||
if (Input.startDependenciesOverride) {
|
||||
await CloudRunnerSystem.Run(Input.startDependenciesOverride);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default DependencyOverrideService;
|
@ -17,17 +17,16 @@ export class FollowLogStreamService {
|
||||
core.setOutput('build-result', 'failed');
|
||||
core.setFailed('unity build failed');
|
||||
core.error('BUILD FAILED!');
|
||||
} else if (CloudRunner.buildParameters.cloudRunnerIntegrationTests && message.includes(': Listening for Jobs')) {
|
||||
} 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');
|
||||
}
|
||||
message = `[${CloudRunnerStatics.logPrefix}] ${message}`;
|
||||
if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) {
|
||||
output += message;
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
output += `${message}\n`;
|
||||
}
|
||||
CloudRunnerLogger.log(message);
|
||||
CloudRunnerLogger.log(`[${CloudRunnerStatics.logPrefix}] ${message}`);
|
||||
|
||||
return { shouldReadLogs, shouldCleanup, output };
|
||||
}
|
||||
|
279
src/model/cloud-runner/services/shared-workspace-locking.ts
Normal file
279
src/model/cloud-runner/services/shared-workspace-locking.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import { CloudRunnerSystem } from './cloud-runner-system';
|
||||
import * as fs from 'fs';
|
||||
import CloudRunnerLogger from './cloud-runner-logger';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import BuildParameters from '../../build-parameters';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
export class SharedWorkspaceLocking {
|
||||
private static readonly workspaceBucketRoot = `s3://game-ci-test-storage/`;
|
||||
private static readonly workspaceRoot = `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`;
|
||||
public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`,
|
||||
)
|
||||
).map((x) => x.replace(`/`, ``));
|
||||
}
|
||||
public static async DoesWorkspaceTopLevelExist(buildParametersContext: BuildParameters) {
|
||||
return (
|
||||
(await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`))
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.includes(`locks`) &&
|
||||
(await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`))
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.includes(buildParametersContext.cacheKey)
|
||||
);
|
||||
}
|
||||
public static async GetAllLocks(workspace: string, buildParametersContext: BuildParameters): Promise<string[]> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`,
|
||||
)
|
||||
)
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.filter((x) => x.includes(`_lock`));
|
||||
}
|
||||
public static async GetOrCreateLockedWorkspace(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
) {
|
||||
if (!CloudRunnerOptions.retainWorkspaces) {
|
||||
return;
|
||||
}
|
||||
if (await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext)) {
|
||||
const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext);
|
||||
CloudRunnerLogger.log(`run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`);
|
||||
for (const element of workspaces) {
|
||||
await new Promise((promise) => setTimeout(promise, 1000));
|
||||
const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext);
|
||||
CloudRunnerLogger.log(`run agent: ${runId} try lock workspace: ${element} result: ${lockResult}`);
|
||||
|
||||
if (lockResult) {
|
||||
CloudRunner.lockedWorkspace = element;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext, runId);
|
||||
CloudRunnerLogger.log(
|
||||
`run agent ${runId} didn't find a free workspace so created: ${workspace} createWorkspaceSuccess: ${createResult}`,
|
||||
);
|
||||
|
||||
return createResult;
|
||||
}
|
||||
|
||||
public static async DoesWorkspaceExist(workspace: string, buildParametersContext: BuildParameters) {
|
||||
return (await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext)).includes(workspace);
|
||||
}
|
||||
public static async HasWorkspaceLock(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
return false;
|
||||
}
|
||||
const locks = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext))
|
||||
.map((x) => {
|
||||
return {
|
||||
name: x,
|
||||
timestamp: Number(x.split(`_`)[0]),
|
||||
};
|
||||
})
|
||||
.sort((x) => x.timestamp);
|
||||
const lockMatches = locks.filter((x) => x.name.includes(runId));
|
||||
const includesRunLock = lockMatches.length > 0 && locks.indexOf(lockMatches[0]) === 0;
|
||||
CloudRunnerLogger.log(
|
||||
`Checking has workspace lock, runId: ${runId}, workspace: ${workspace}, success: ${includesRunLock} \n- Num of locks created by Run Agent: ${
|
||||
lockMatches.length
|
||||
} Num of Locks: ${locks.length}, Time ordered index for Run Agent: ${locks.indexOf(lockMatches[0])} \n \n`,
|
||||
);
|
||||
|
||||
return includesRunLock;
|
||||
}
|
||||
|
||||
public static async GetFreeWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
|
||||
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);
|
||||
if (!isLocked && isBelowMax) {
|
||||
result.push(element);
|
||||
CloudRunnerLogger.log(`workspace ${element} is free`);
|
||||
} else {
|
||||
CloudRunnerLogger.log(`workspace ${element} is NOT free ${!isLocked} ${isBelowMax}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async IsWorkspaceBelowMax(
|
||||
workspace: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
|
||||
if (workspace === ``) {
|
||||
return (
|
||||
workspaces.length < buildParametersContext.maxRetainedWorkspaces ||
|
||||
buildParametersContext.maxRetainedWorkspaces === 0
|
||||
);
|
||||
}
|
||||
const ordered: any[] = [];
|
||||
for (const ws of workspaces) {
|
||||
ordered.push({
|
||||
name: ws,
|
||||
timestamp: await SharedWorkspaceLocking.GetWorkspaceTimestamp(ws, buildParametersContext),
|
||||
});
|
||||
}
|
||||
ordered.sort((x) => x.timestamp);
|
||||
const matches = ordered.filter((x) => x.name.includes(workspace));
|
||||
const isWorkspaceBelowMax =
|
||||
matches.length > 0 &&
|
||||
(ordered.indexOf(matches[0]) < buildParametersContext.maxRetainedWorkspaces ||
|
||||
buildParametersContext.maxRetainedWorkspaces === 0);
|
||||
|
||||
return isWorkspaceBelowMax;
|
||||
}
|
||||
|
||||
public static async GetWorkspaceTimestamp(
|
||||
workspace: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<Number> {
|
||||
if (workspace.split(`_`).length > 0) {
|
||||
return Number(workspace.split(`_`)[1]);
|
||||
}
|
||||
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
throw new Error("Workspace doesn't exist, can't call get all locks");
|
||||
}
|
||||
|
||||
return (
|
||||
await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`,
|
||||
)
|
||||
)
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.filter((x) => x.includes(`_workspace`))
|
||||
.map((x) => Number(x))[0];
|
||||
}
|
||||
|
||||
public static async IsWorkspaceLocked(workspace: string, buildParametersContext: BuildParameters): Promise<boolean> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
return false;
|
||||
}
|
||||
const files = await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`,
|
||||
);
|
||||
|
||||
const workspaceFileDoesNotExists =
|
||||
files.filter((x) => {
|
||||
return x.includes(`_workspace`);
|
||||
}).length === 0;
|
||||
|
||||
const lockFilesExist =
|
||||
files.filter((x) => {
|
||||
return x.includes(`_lock`);
|
||||
}).length > 0;
|
||||
|
||||
return workspaceFileDoesNotExists || lockFilesExist;
|
||||
}
|
||||
|
||||
public static async CreateWorkspace(
|
||||
workspace: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
lockId: string = ``,
|
||||
): Promise<boolean> {
|
||||
if (lockId !== ``) {
|
||||
await SharedWorkspaceLocking.LockWorkspace(workspace, lockId, buildParametersContext);
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const file = `${timestamp}_workspace`;
|
||||
fs.writeFileSync(file, '');
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
fs.rmSync(file);
|
||||
|
||||
const workspaces = await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`,
|
||||
);
|
||||
|
||||
CloudRunnerLogger.log(`All workspaces ${workspaces}`);
|
||||
if (!(await SharedWorkspaceLocking.IsWorkspaceBelowMax(workspace, buildParametersContext))) {
|
||||
CloudRunnerLogger.log(`Workspace is below max ${workspaces} ${buildParametersContext.maxRetainedWorkspaces}`);
|
||||
await SharedWorkspaceLocking.CleanupWorkspace(workspace, buildParametersContext);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async LockWorkspace(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
const file = `${Date.now()}_${runId}_lock`;
|
||||
fs.writeFileSync(file, '');
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
fs.rmSync(file);
|
||||
|
||||
return SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext);
|
||||
}
|
||||
|
||||
public static async ReleaseWorkspace(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
const file = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext)).filter((x) =>
|
||||
x.includes(`_${runId}_lock`),
|
||||
);
|
||||
CloudRunnerLogger.log(`Deleting lock ${workspace}/${file}`);
|
||||
CloudRunnerLogger.log(
|
||||
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
return !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`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private static async ReadLines(command: string): Promise<string[]> {
|
||||
return CloudRunnerSystem.RunAndReadLines(command);
|
||||
}
|
||||
}
|
||||
|
||||
export default SharedWorkspaceLocking;
|
@ -1,72 +1,137 @@
|
||||
import { CloudRunner, Input } from '../..';
|
||||
import ImageEnvironmentFactory from '../../image-environment-factory';
|
||||
import { Input } from '../..';
|
||||
import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable';
|
||||
import { CloudRunnerBuildCommandProcessor } from './cloud-runner-build-command-process';
|
||||
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';
|
||||
|
||||
export class TaskParameterSerializer {
|
||||
public static readBuildEnvironmentVariables(): CloudRunnerEnvironmentVariable[] {
|
||||
return [
|
||||
static readonly blocked = new Set(['0', 'length', 'prototype', '', 'unityVersion']);
|
||||
public static createCloudRunnerEnvironmentVariables(
|
||||
buildParameters: BuildParameters,
|
||||
): CloudRunnerEnvironmentVariable[] {
|
||||
const result = this.uniqBy(
|
||||
[
|
||||
{
|
||||
name: 'ContainerMemory',
|
||||
value: CloudRunner.buildParameters.cloudRunnerMemory,
|
||||
value: buildParameters.cloudRunnerMemory,
|
||||
},
|
||||
{
|
||||
name: 'ContainerCpu',
|
||||
value: CloudRunner.buildParameters.cloudRunnerCpu,
|
||||
value: buildParameters.cloudRunnerCpu,
|
||||
},
|
||||
{
|
||||
name: 'BUILD_TARGET',
|
||||
value: CloudRunner.buildParameters.targetPlatform,
|
||||
value: buildParameters.targetPlatform,
|
||||
},
|
||||
...TaskParameterSerializer.serializeBuildParamsAndInput,
|
||||
];
|
||||
}
|
||||
private static get serializeBuildParamsAndInput() {
|
||||
let array = new Array();
|
||||
array = TaskParameterSerializer.readBuildParameters(array);
|
||||
array = TaskParameterSerializer.readInput(array);
|
||||
const configurableHooks = CloudRunnerBuildCommandProcessor.getHooks(CloudRunner.buildParameters.customJobHooks);
|
||||
const secrets = configurableHooks.map((x) => x.secrets).filter((x) => x !== undefined && x.length > 0);
|
||||
if (secrets.length > 0) {
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
array.push(secrets.reduce((x, y) => [...x, ...y]));
|
||||
}
|
||||
|
||||
array = array.filter(
|
||||
(x) => x.value !== undefined && x.name !== '0' && x.value !== '' && x.name !== 'prototype' && x.name !== 'length',
|
||||
);
|
||||
array = array.map((x) => {
|
||||
x.name = Input.ToEnvVarFormat(x.name);
|
||||
...TaskParameterSerializer.serializeFromObject(buildParameters),
|
||||
...TaskParameterSerializer.readInput(),
|
||||
...CloudRunnerCustomHooks.getSecrets(CloudRunnerCustomHooks.getHooks(buildParameters.customJobHooks)),
|
||||
]
|
||||
.filter(
|
||||
(x) =>
|
||||
!TaskParameterSerializer.blocked.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.value = `${x.value}`;
|
||||
|
||||
if (buildParameters.cloudRunnerDebug && Number(x.name) === Number.NaN) {
|
||||
core.info(`[ERROR] found a number in task param serializer ${JSON.stringify(x)}`);
|
||||
}
|
||||
|
||||
return x;
|
||||
});
|
||||
}),
|
||||
(item) => item.name,
|
||||
);
|
||||
|
||||
return array;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static readBuildParameters(array: any[]) {
|
||||
const keys = Object.keys(CloudRunner.buildParameters);
|
||||
static uniqBy(a, key) {
|
||||
const seen = {};
|
||||
|
||||
return a.filter(function (item) {
|
||||
const k = key(item);
|
||||
|
||||
return seen.hasOwnProperty(k) ? false : (seen[k] = true);
|
||||
});
|
||||
}
|
||||
|
||||
public static readBuildParameterFromEnvironment(): BuildParameters {
|
||||
const buildParameters = new BuildParameters();
|
||||
const keys = [
|
||||
...new Set(
|
||||
Object.getOwnPropertyNames(process.env)
|
||||
.filter((x) => !this.blocked.has(x) && x.startsWith('GAMECI_'))
|
||||
.map((x) => TaskParameterSerializer.UndoEnvVarFormat(x)),
|
||||
),
|
||||
];
|
||||
|
||||
for (const element of keys) {
|
||||
array.push({
|
||||
name: element,
|
||||
value: CloudRunner.buildParameters[element],
|
||||
});
|
||||
if (element !== `customJob`) {
|
||||
buildParameters[element] = process.env[`GAMECI_${TaskParameterSerializer.ToEnvVarFormat(element)}`];
|
||||
}
|
||||
}
|
||||
|
||||
return buildParameters;
|
||||
}
|
||||
|
||||
private static readInput() {
|
||||
return TaskParameterSerializer.serializeFromType(Input);
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input): string {
|
||||
return CloudRunnerOptions.ToEnvVarFormat(input);
|
||||
}
|
||||
|
||||
public static UndoEnvVarFormat(element): string {
|
||||
return this.camelize(element.replace('GAMECI_', '').toLowerCase().replace(/_+/g, ' '));
|
||||
}
|
||||
|
||||
private static camelize(string) {
|
||||
return string
|
||||
.replace(/^\w|[A-Z]|\b\w/g, function (word, index) {
|
||||
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
private static serializeFromObject(buildParameters) {
|
||||
const array: any[] = [];
|
||||
const keys = Object.getOwnPropertyNames(buildParameters).filter((x) => !this.blocked.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: 'buildParameters', value: JSON.stringify(CloudRunner.buildParameters) });
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private static readInput(array: any[]) {
|
||||
const input = Object.getOwnPropertyNames(Input);
|
||||
private static serializeFromType(type) {
|
||||
const array: any[] = [];
|
||||
const input = CloudRunnerOptionsReader.GetProperties();
|
||||
for (const element of input) {
|
||||
if (typeof Input[element] !== 'function' && array.filter((x) => x.name === element).length === 0) {
|
||||
if (typeof type[element] !== 'function' && array.filter((x) => x.name === element).length === 0) {
|
||||
array.push({
|
||||
name: element,
|
||||
value: `${Input[element]}`,
|
||||
value: `${type[element]}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -79,17 +144,7 @@ export class TaskParameterSerializer {
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_SERIAL');
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_EMAIL');
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_PASSWORD');
|
||||
array.push(
|
||||
...ImageEnvironmentFactory.getEnvironmentVariables(CloudRunner.buildParameters)
|
||||
.filter((x) => array.every((y) => y.ParameterKey !== x.name))
|
||||
.map((x) => {
|
||||
return {
|
||||
ParameterKey: x.name,
|
||||
EnvironmentVariable: x.name,
|
||||
ParameterValue: x.value,
|
||||
};
|
||||
}),
|
||||
);
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_LICENSE');
|
||||
|
||||
return array;
|
||||
}
|
||||
@ -102,7 +157,7 @@ export class TaskParameterSerializer {
|
||||
s;
|
||||
private static tryAddInput(array, key): CloudRunnerSecret[] {
|
||||
const value = TaskParameterSerializer.getValue(key);
|
||||
if (value !== undefined && value !== '') {
|
||||
if (value !== undefined && value !== '' && value !== 'null') {
|
||||
array.push({
|
||||
ParameterKey: key,
|
||||
EnvironmentVariable: key,
|
||||
|
@ -0,0 +1,45 @@
|
||||
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';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
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();
|
||||
});
|
||||
});
|
@ -2,19 +2,16 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import BuildParameters from '../../build-parameters';
|
||||
import { Cli } from '../../cli/cli';
|
||||
import Input from '../../input';
|
||||
import UnityVersioning from '../../unity-versioning';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import { CloudRunnerSystem } from '../services/cloud-runner-system';
|
||||
import { Caching } from './caching';
|
||||
import { Caching } from '../remote-client/caching';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
describe('Cloud Runner Caching', () => {
|
||||
import GitHub from '../../github';
|
||||
describe('Cloud Runner (Remote Client) Caching', () => {
|
||||
it('responds', () => {});
|
||||
});
|
||||
describe('Cloud Runner Caching', () => {
|
||||
if (process.platform === 'linux') {
|
||||
it('Simple caching works', async () => {
|
||||
it.skip('Simple caching works', async () => {
|
||||
Cli.options = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
@ -22,7 +19,7 @@ describe('Cloud Runner Caching', () => {
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
};
|
||||
Input.githubInputEnabled = false;
|
||||
GitHub.githubInputEnabled = false;
|
||||
const buildParameter = await BuildParameters.create();
|
||||
CloudRunner.buildParameters = buildParameter;
|
||||
|
||||
@ -46,8 +43,6 @@ describe('Cloud Runner Caching', () => {
|
||||
`${Cli.options.cacheKey}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`du -h ${__dirname}`);
|
||||
await CloudRunnerSystem.Run(`tree ${testFolder}`);
|
||||
await CloudRunnerSystem.Run(`tree ${cacheFolder}`);
|
||||
|
||||
// Compare validity to original hash
|
||||
expect(fs.readFileSync(path.resolve(testFolder, 'test.txt'), { encoding: 'utf8' }).toString()).toContain(
|
||||
@ -56,7 +51,7 @@ describe('Cloud Runner Caching', () => {
|
||||
fs.rmdirSync(testFolder, { recursive: true });
|
||||
fs.rmdirSync(cacheFolder, { recursive: true });
|
||||
|
||||
Input.githubInputEnabled = true;
|
||||
GitHub.githubInputEnabled = true;
|
||||
delete Cli.options;
|
||||
}, 1000000);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
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';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
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 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);
|
||||
}
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
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';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
if (overrides) {
|
||||
Cli.options = overrides;
|
||||
}
|
||||
|
||||
return await BuildParameters.create();
|
||||
}
|
||||
|
||||
describe('Cloud Runner Caching', () => {
|
||||
it('Responds', () => {});
|
||||
setups();
|
||||
if (CloudRunnerOptions.cloudRunnerDebug) {
|
||||
it('Run one build it should not use cache, run subsequent build which should use cache', async () => {
|
||||
const overrides = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
customStepFiles: `debug-cache`,
|
||||
};
|
||||
if (CloudRunnerOptions.cloudRunnerCluster === `k8s`) {
|
||||
overrides.customStepFiles += `,aws-s3-pull-cache,aws-s3-upload-cache`;
|
||||
}
|
||||
const buildParameter = await CreateParameters(overrides);
|
||||
expect(buildParameter.projectPath).toEqual(overrides.projectPath);
|
||||
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
const results = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const libraryString = 'Rebuilding Library because the asset database could not be found!';
|
||||
const cachePushFail = 'Did not push source folder to cache because it was empty Library';
|
||||
const buildSucceededString = 'Build succeeded';
|
||||
|
||||
expect(results).toContain(libraryString);
|
||||
expect(results).toContain(buildSucceededString);
|
||||
expect(results).not.toContain(cachePushFail);
|
||||
|
||||
CloudRunnerLogger.log(`run 1 succeeded`);
|
||||
const buildParameter2 = await CreateParameters(overrides);
|
||||
|
||||
buildParameter2.cacheKey = buildParameter.cacheKey;
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
|
||||
const build2ContainsCacheKey = results2.includes(buildParameter.cacheKey);
|
||||
const build2ContainsBuildSucceeded = results2.includes(buildSucceededString);
|
||||
const build2NotContainsNoLibraryMessage = !results2.includes(libraryString);
|
||||
const build2NotContainsZeroLibraryCacheFilesMessage = !results2.includes(
|
||||
'There is 0 files/dir in the cache pulled contents for Library',
|
||||
);
|
||||
const build2NotContainsZeroLFSCacheFilesMessage = !results2.includes(
|
||||
'There is 0 files/dir in the cache pulled contents for LFS',
|
||||
);
|
||||
|
||||
expect(build2ContainsCacheKey).toBeTruthy();
|
||||
expect(build2ContainsBuildSucceeded).toBeTruthy();
|
||||
expect(build2NotContainsZeroLibraryCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsZeroLFSCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsNoLibraryMessage).toBeTruthy();
|
||||
}, 1_000_000_000);
|
||||
}
|
||||
});
|
@ -0,0 +1,93 @@
|
||||
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 { CloudRunnerSystem } from '../services/cloud-runner-system';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { CloudRunnerFolders } from '../services/cloud-runner-folders';
|
||||
import SharedWorkspaceLocking from '../services/shared-workspace-locking';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
if (overrides) {
|
||||
Cli.options = overrides;
|
||||
}
|
||||
|
||||
return await BuildParameters.create();
|
||||
}
|
||||
|
||||
describe('Cloud Runner Retain Workspace', () => {
|
||||
it('Responds', () => {});
|
||||
setups();
|
||||
if (CloudRunnerOptions.cloudRunnerDebug) {
|
||||
it('Run one build it should not already be retained, run subsequent build which should use retained workspace', async () => {
|
||||
const overrides = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
retainWorkspaces: true,
|
||||
};
|
||||
const buildParameter = await CreateParameters(overrides);
|
||||
expect(buildParameter.projectPath).toEqual(overrides.projectPath);
|
||||
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
const results = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const libraryString = 'Rebuilding Library because the asset database could not be found!';
|
||||
const cachePushFail = 'Did not push source folder to cache because it was empty Library';
|
||||
const buildSucceededString = 'Build succeeded';
|
||||
|
||||
expect(results).toContain(libraryString);
|
||||
expect(results).toContain(buildSucceededString);
|
||||
expect(results).not.toContain(cachePushFail);
|
||||
|
||||
CloudRunnerLogger.log(`run 1 succeeded`);
|
||||
const buildParameter2 = await CreateParameters(overrides);
|
||||
|
||||
buildParameter2.cacheKey = buildParameter.cacheKey;
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
|
||||
const build2ContainsCacheKey = results2.includes(buildParameter.cacheKey);
|
||||
const build2ContainsBuildGuid1FromRetainedWorkspace = results2.includes(buildParameter.buildGuid);
|
||||
const build2ContainsRetainedWorkspacePhrase = results2.includes(`Retained Workspace:`);
|
||||
const build2ContainsWorkspaceExistsAlreadyPhrase = results2.includes(`Retained Workspace Already Exists!`);
|
||||
const build2ContainsBuildSucceeded = results2.includes(buildSucceededString);
|
||||
const build2NotContainsNoLibraryMessage = !results2.includes(libraryString);
|
||||
const build2NotContainsZeroLibraryCacheFilesMessage = !results2.includes(
|
||||
'There is 0 files/dir in the cache pulled contents for Library',
|
||||
);
|
||||
const build2NotContainsZeroLFSCacheFilesMessage = !results2.includes(
|
||||
'There is 0 files/dir in the cache pulled contents for LFS',
|
||||
);
|
||||
|
||||
expect(build2ContainsCacheKey).toBeTruthy();
|
||||
expect(build2ContainsRetainedWorkspacePhrase).toBeTruthy();
|
||||
expect(build2ContainsWorkspaceExistsAlreadyPhrase).toBeTruthy();
|
||||
expect(build2ContainsBuildGuid1FromRetainedWorkspace).toBeTruthy();
|
||||
expect(build2ContainsBuildSucceeded).toBeTruthy();
|
||||
expect(build2NotContainsZeroLibraryCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsZeroLFSCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsNoLibraryMessage).toBeTruthy();
|
||||
}, 1_000_000_000);
|
||||
afterAll(async () => {
|
||||
await SharedWorkspaceLocking.CleanupWorkspace(CloudRunner.lockedWorkspace || ``, CloudRunner.buildParameters);
|
||||
if (
|
||||
fs.existsSync(`./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`)
|
||||
) {
|
||||
CloudRunnerLogger.log(
|
||||
`Cleaning up ./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(
|
||||
`rm -r ./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,46 @@
|
||||
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 { CloudRunnerSystem } from '../services/cloud-runner-system';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
if (overrides) {
|
||||
Cli.options = overrides;
|
||||
}
|
||||
|
||||
return await BuildParameters.create();
|
||||
}
|
||||
|
||||
describe('Cloud Runner pre-built S3 steps', () => {
|
||||
it('Responds', () => {});
|
||||
setups();
|
||||
if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `local-docker`) {
|
||||
it('Run build and prebuilt s3 cache pull, cache push and upload build', async () => {
|
||||
const overrides = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
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`,
|
||||
};
|
||||
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');
|
||||
expect(build2ContainsBuildSucceeded).toBeTruthy();
|
||||
|
||||
const results = await CloudRunnerSystem.RunAndReadLines(
|
||||
`aws s3 ls s3://game-ci-test-storage/cloud-runner-cache/${buildParameter2.cacheKey}/`,
|
||||
);
|
||||
CloudRunnerLogger.log(results.join(`,`));
|
||||
}, 1_000_000_000);
|
||||
}
|
||||
});
|
25
src/model/cloud-runner/tests/cloud-runner-suite.test.ts
Normal file
25
src/model/cloud-runner/tests/cloud-runner-suite.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Cli } from '../../cli/cli';
|
||||
import GitHub from '../../github';
|
||||
|
||||
describe('Cloud Runner', () => {
|
||||
it('Responds', () => {});
|
||||
});
|
||||
|
||||
const setups = () => {
|
||||
beforeAll(() => {
|
||||
GitHub.githubInputEnabled = false;
|
||||
});
|
||||
beforeEach(() => {
|
||||
Cli.options = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
if (Cli.options !== undefined) {
|
||||
delete Cli.options;
|
||||
}
|
||||
});
|
||||
afterAll(() => {
|
||||
GitHub.githubInputEnabled = true;
|
||||
});
|
||||
};
|
||||
|
||||
export default setups;
|
@ -0,0 +1,77 @@
|
||||
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 UnityVersioning from '../../unity-versioning';
|
||||
import { Cli } from '../../cli/cli';
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import setups from './cloud-runner-suite.test';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
if (overrides) Cli.options = overrides;
|
||||
|
||||
return BuildParameters.create();
|
||||
}
|
||||
describe('Cloud Runner Sync Environments', () => {
|
||||
setups();
|
||||
const testSecretName = 'testSecretName';
|
||||
const testSecretValue = 'testSecretValue';
|
||||
it('Responds', () => {});
|
||||
|
||||
if (CloudRunnerOptions.cloudRunnerDebug) {
|
||||
it('All build parameters sent to cloud runner as env vars', async () => {
|
||||
// Setup parameters
|
||||
const buildParameter = await CreateParameters({
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
customJob: `
|
||||
- name: 'step 1'
|
||||
image: 'ubuntu'
|
||||
commands: 'printenv'
|
||||
secrets:
|
||||
- name: '${testSecretName}'
|
||||
value: '${testSecretValue}'
|
||||
`,
|
||||
});
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
// Run the job
|
||||
const file = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
|
||||
// Assert results
|
||||
// expect(file).toContain(JSON.stringify(buildParameter));
|
||||
expect(file).toContain(`${Input.ToEnvVarFormat(testSecretName)}=${testSecretValue}`);
|
||||
const environmentVariables = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter);
|
||||
const secrets = TaskParameterSerializer.readDefaultSecrets().map((x) => {
|
||||
return {
|
||||
name: x.EnvironmentVariable,
|
||||
value: x.ParameterValue,
|
||||
};
|
||||
});
|
||||
const combined = [...environmentVariables, ...secrets]
|
||||
.filter((element) => element.value !== undefined && element.value !== '' && typeof element.value !== 'function')
|
||||
.map((x) => {
|
||||
if (typeof x.value === `string`) {
|
||||
x.value = x.value.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
return x;
|
||||
})
|
||||
.filter((element) => {
|
||||
return !['UNITY_LICENSE', 'CUSTOM_JOB'].includes(element.name);
|
||||
});
|
||||
const newLinePurgedFile = file
|
||||
.replace(/\s+/g, '')
|
||||
.replace(new RegExp(`\\[${CloudRunnerStatics.logPrefix}\\]`, 'g'), '');
|
||||
for (const element of combined) {
|
||||
expect(newLinePurgedFile).toContain(`${element.name}`);
|
||||
CloudRunnerLogger.log(`Contains ${element.name}`);
|
||||
const fullNameEqualValue = `${element.name}=${element.value}`;
|
||||
expect(newLinePurgedFile).toContain(fullNameEqualValue);
|
||||
}
|
||||
}, 1_000_000_000);
|
||||
}
|
||||
});
|
@ -0,0 +1,99 @@
|
||||
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';
|
||||
|
||||
async function CreateParameters(overrides) {
|
||||
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();
|
||||
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: 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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
@ -1,32 +1,31 @@
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import { CloudRunnerFolders } from '../services/cloud-runner-folders';
|
||||
import { CloudRunnerStepState } from '../cloud-runner-step-state';
|
||||
import { CustomWorkflow } from './custom-workflow';
|
||||
import { WorkflowInterface } from './workflow-interface';
|
||||
import * as core from '@actions/core';
|
||||
import { CloudRunnerBuildCommandProcessor } from '../services/cloud-runner-build-command-process';
|
||||
import { CloudRunnerCustomHooks } from '../services/cloud-runner-custom-hooks';
|
||||
import path from 'path';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import { CloudRunnerCustomSteps } from '../services/cloud-runner-custom-steps';
|
||||
|
||||
export class BuildAutomationWorkflow implements WorkflowInterface {
|
||||
async run(cloudRunnerStepState: CloudRunnerStepState) {
|
||||
try {
|
||||
return await BuildAutomationWorkflow.standardBuildAutomation(cloudRunnerStepState.image);
|
||||
return await BuildAutomationWorkflow.standardBuildAutomation(cloudRunnerStepState.image, cloudRunnerStepState);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async standardBuildAutomation(baseImage: any) {
|
||||
private static async standardBuildAutomation(baseImage: any, cloudRunnerStepState: CloudRunnerStepState) {
|
||||
// TODO accept post and pre build steps as yaml files in the repo
|
||||
try {
|
||||
CloudRunnerLogger.log(`Cloud Runner is running standard build automation`);
|
||||
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps');
|
||||
let output = '';
|
||||
if (CloudRunner.buildParameters.preBuildSteps !== '') {
|
||||
output += await CustomWorkflow.runCustomJob(CloudRunner.buildParameters.preBuildSteps);
|
||||
}
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
|
||||
output += await CloudRunnerCustomSteps.RunPreBuildSteps(cloudRunnerStepState);
|
||||
CloudRunnerLogger.logWithTime('Configurable pre build step(s) time');
|
||||
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('build');
|
||||
@ -34,23 +33,19 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
||||
CloudRunnerLogger.logLine(` `);
|
||||
CloudRunnerLogger.logLine('Starting build automation job');
|
||||
|
||||
output += await CloudRunner.Provider.runTask(
|
||||
output += await CloudRunner.Provider.runTaskInWorkflow(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
baseImage.toString(),
|
||||
BuildAutomationWorkflow.BuildWorkflow,
|
||||
`/${CloudRunnerFolders.buildVolumeFolder}`,
|
||||
`/${CloudRunnerFolders.buildVolumeFolder}/`,
|
||||
CloudRunner.cloudRunnerEnvironmentVariables,
|
||||
CloudRunner.defaultSecrets,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
CloudRunnerLogger.logWithTime('Build time');
|
||||
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps');
|
||||
if (CloudRunner.buildParameters.postBuildSteps !== '') {
|
||||
output += await CustomWorkflow.runCustomJob(CloudRunner.buildParameters.postBuildSteps);
|
||||
}
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
output += await CloudRunnerCustomSteps.RunPostBuildSteps(cloudRunnerStepState);
|
||||
CloudRunnerLogger.logWithTime('Configurable post build step(s) time');
|
||||
|
||||
CloudRunnerLogger.log(`Cloud Runner finished running standard build automation`);
|
||||
@ -62,62 +57,83 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
||||
}
|
||||
|
||||
private static get BuildWorkflow() {
|
||||
const setupHooks = CloudRunnerBuildCommandProcessor.getHooks(CloudRunner.buildParameters.customJobHooks).filter(
|
||||
(x) => x.step.includes(`setup`),
|
||||
const setupHooks = CloudRunnerCustomHooks.getHooks(CloudRunner.buildParameters.customJobHooks).filter((x) =>
|
||||
x.step.includes(`setup`),
|
||||
);
|
||||
const buildHooks = CloudRunnerBuildCommandProcessor.getHooks(CloudRunner.buildParameters.customJobHooks).filter(
|
||||
(x) => x.step.includes(`build`),
|
||||
const buildHooks = CloudRunnerCustomHooks.getHooks(CloudRunner.buildParameters.customJobHooks).filter((x) =>
|
||||
x.step.includes(`build`),
|
||||
);
|
||||
const builderPath = CloudRunnerFolders.ToLinuxFolder(
|
||||
path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', `index.js`),
|
||||
);
|
||||
const builderPath = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', `index.js`).replace(/\\/g, `/`);
|
||||
|
||||
return `apt-get update > /dev/null
|
||||
apt-get install -y tar tree npm git-lfs jq git > /dev/null
|
||||
npm install -g n > /dev/null
|
||||
n stable > /dev/null
|
||||
apt-get install -y curl tar tree npm git-lfs jq git > /dev/null
|
||||
npm i -g n > /dev/null
|
||||
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.repoPathAbsolute.replace(/\\/g, `/`)}"
|
||||
export GITHUB_WORKSPACE="${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}"
|
||||
${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, CloudRunner.buildParameters.buildGuid)}
|
||||
${buildHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '}`;
|
||||
${BuildAutomationWorkflow.BuildCommands(builderPath)}
|
||||
${buildHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '}
|
||||
${BuildAutomationWorkflow.TreeCommand}`;
|
||||
}
|
||||
|
||||
private static setupCommands(builderPath) {
|
||||
return `export GIT_DISCOVERY_ACROSS_FILESYSTEM=1
|
||||
echo "game ci cloud runner clone"
|
||||
mkdir -p ${CloudRunnerFolders.builderPathAbsolute.replace(/\\/g, `/`)}
|
||||
git clone -q -b ${CloudRunner.buildParameters.cloudRunnerBranch} ${
|
||||
const commands = `mkdir -p ${CloudRunnerFolders.ToLinuxFolder(
|
||||
CloudRunnerFolders.builderPathAbsolute,
|
||||
)} && git clone -q -b ${CloudRunner.buildParameters.cloudRunnerBranch} ${
|
||||
CloudRunnerFolders.unityBuilderRepoUrl
|
||||
} "${CloudRunnerFolders.builderPathAbsolute.replace(/\\/g, `/`)}"
|
||||
chmod +x ${builderPath}
|
||||
echo "game ci cloud runner bootstrap"
|
||||
node ${builderPath} -m remote-cli`;
|
||||
} "${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`;
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
private static BuildCommands(builderPath, guid) {
|
||||
const linuxCacheFolder = CloudRunnerFolders.cacheFolderFull.replace(/\\/g, `/`);
|
||||
private static BuildCommands(builderPath) {
|
||||
const distFolder = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist');
|
||||
const ubuntuPlatformsFolder = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', 'platforms', 'ubuntu');
|
||||
|
||||
return `echo "game ci cloud runner init"
|
||||
mkdir -p ${`${CloudRunnerFolders.projectBuildFolderAbsolute}/build`.replace(/\\/g, `/`)}
|
||||
cd ${CloudRunnerFolders.projectPathAbsolute}
|
||||
cp -r "${path.join(distFolder, 'default-build-script').replace(/\\/g, `/`)}" "/UnityBuilderAction"
|
||||
cp -r "${path.join(ubuntuPlatformsFolder, 'entrypoint.sh').replace(/\\/g, `/`)}" "/entrypoint.sh"
|
||||
cp -r "${path.join(ubuntuPlatformsFolder, 'steps').replace(/\\/g, `/`)}" "/steps"
|
||||
return `echo "game ci cloud runner initalized"
|
||||
mkdir -p ${`${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectBuildFolderAbsolute)}/build`}
|
||||
cd ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectPathAbsolute)}
|
||||
cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(distFolder, 'default-build-script'))}" "/UnityBuilderAction"
|
||||
cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(ubuntuPlatformsFolder, 'entrypoint.sh'))}" "/entrypoint.sh"
|
||||
cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(ubuntuPlatformsFolder, 'steps'))}" "/steps"
|
||||
chmod -R +x "/entrypoint.sh"
|
||||
chmod -R +x "/steps"
|
||||
echo "game ci cloud runner start"
|
||||
echo "game ci start"
|
||||
/entrypoint.sh
|
||||
echo "game ci cloud runner push library to cache"
|
||||
echo "game ci caching results"
|
||||
chmod +x ${builderPath}
|
||||
node ${builderPath} -m cache-push --cachePushFrom ${
|
||||
CloudRunnerFolders.libraryFolderAbsolute
|
||||
} --artifactName lib-${guid} --cachePushTo ${linuxCacheFolder}/Library
|
||||
echo "game ci cloud runner push build to cache"
|
||||
node ${builderPath} -m cache-push --cachePushFrom ${
|
||||
CloudRunnerFolders.projectBuildFolderAbsolute
|
||||
} --artifactName build-${guid} --cachePushTo ${`${linuxCacheFolder}/build`.replace(/\\/g, `/`)}`;
|
||||
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}`
|
||||
: `#`;
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,43 @@
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import { CloudRunnerFolders } from '../services/cloud-runner-folders';
|
||||
import YAML from 'yaml';
|
||||
import { CloudRunner, Input } from '../..';
|
||||
import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable';
|
||||
import { CloudRunnerCustomSteps, CustomStep } from '../services/cloud-runner-custom-steps';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
|
||||
export class CustomWorkflow {
|
||||
public static async runCustomJob(buildSteps) {
|
||||
public static async runCustomJobFromString(
|
||||
buildSteps: string,
|
||||
environmentVariables: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
return await CustomWorkflow.runCustomJob(
|
||||
CloudRunnerCustomSteps.ParseSteps(buildSteps),
|
||||
environmentVariables,
|
||||
secrets,
|
||||
);
|
||||
}
|
||||
|
||||
public static async runCustomJob(
|
||||
buildSteps: CustomStep[],
|
||||
environmentVariables: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
) {
|
||||
try {
|
||||
CloudRunnerLogger.log(`Cloud Runner is running in custom job mode`);
|
||||
if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) {
|
||||
CloudRunnerLogger.log(`Parsing build steps: ${buildSteps}`);
|
||||
}
|
||||
try {
|
||||
buildSteps = YAML.parse(buildSteps);
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`failed to parse a custom job "${buildSteps}"`);
|
||||
throw error;
|
||||
}
|
||||
let output = '';
|
||||
if (CloudRunner.buildParameters?.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Custom Job Description \n${JSON.stringify(buildSteps, undefined, 4)}`);
|
||||
}
|
||||
for (const step of buildSteps) {
|
||||
const stepSecrets: CloudRunnerSecret[] = step.secrets.map((x) => {
|
||||
const secret: CloudRunnerSecret = {
|
||||
ParameterKey: x.name,
|
||||
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
|
||||
ParameterValue: x.value,
|
||||
};
|
||||
|
||||
return secret;
|
||||
});
|
||||
output += await CloudRunner.Provider.runTask(
|
||||
output += await CloudRunner.Provider.runTaskInWorkflow(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
step['image'],
|
||||
step['commands'],
|
||||
step.image,
|
||||
step.commands,
|
||||
`/${CloudRunnerFolders.buildVolumeFolder}`,
|
||||
`/${CloudRunnerFolders.buildVolumeFolder}/`,
|
||||
CloudRunner.cloudRunnerEnvironmentVariables,
|
||||
[...CloudRunner.defaultSecrets, ...stepSecrets],
|
||||
`/${CloudRunnerFolders.projectPathAbsolute}/`,
|
||||
environmentVariables,
|
||||
[...secrets, ...step.secrets],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,21 +6,21 @@ import CloudRunner from '../cloud-runner';
|
||||
|
||||
export class WorkflowCompositionRoot implements WorkflowInterface {
|
||||
async run(cloudRunnerStepState: CloudRunnerStepState) {
|
||||
try {
|
||||
return await WorkflowCompositionRoot.runJob(cloudRunnerStepState.image.toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async runJob(baseImage: any) {
|
||||
try {
|
||||
if (CloudRunner.buildParameters.customJob !== '') {
|
||||
return await CustomWorkflow.runCustomJob(CloudRunner.buildParameters.customJob);
|
||||
return await CustomWorkflow.runCustomJobFromString(
|
||||
CloudRunner.buildParameters.customJob,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
}
|
||||
|
||||
return await new BuildAutomationWorkflow().run(
|
||||
new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets),
|
||||
new CloudRunnerStepState(
|
||||
cloudRunnerStepState.image.toString(),
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
@ -4,30 +4,50 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
class Docker {
|
||||
static async run(image, parameters, silent = false) {
|
||||
static async run(
|
||||
image,
|
||||
parameters,
|
||||
silent = false,
|
||||
overrideCommands = '',
|
||||
additionalVariables: any[] = [],
|
||||
options: any = false,
|
||||
entrypointBash: boolean = false,
|
||||
) {
|
||||
let runCommand = '';
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
runCommand = this.getLinuxCommand(image, parameters);
|
||||
runCommand = this.getLinuxCommand(image, parameters, overrideCommands, additionalVariables, entrypointBash);
|
||||
break;
|
||||
case 'win32':
|
||||
runCommand = this.getWindowsCommand(image, parameters);
|
||||
}
|
||||
if (options !== false) {
|
||||
options.silent = silent;
|
||||
await exec(runCommand, undefined, options);
|
||||
} else {
|
||||
await exec(runCommand, undefined, { silent });
|
||||
}
|
||||
}
|
||||
|
||||
static getLinuxCommand(image, parameters): string {
|
||||
static getLinuxCommand(
|
||||
image,
|
||||
parameters,
|
||||
overrideCommands = '',
|
||||
additionalVariables: any[] = [],
|
||||
entrypointBash: boolean = false,
|
||||
): string {
|
||||
const { workspace, actionFolder, runnerTempPath, sshAgent, gitPrivateToken } = parameters;
|
||||
|
||||
const githubHome = path.join(runnerTempPath, '_github_home');
|
||||
if (!existsSync(githubHome)) mkdirSync(githubHome);
|
||||
const githubWorkflow = path.join(runnerTempPath, '_github_workflow');
|
||||
if (!existsSync(githubWorkflow)) mkdirSync(githubWorkflow);
|
||||
const commandPrefix = image === `alpine` ? `/bin/sh` : `/bin/bash`;
|
||||
|
||||
return `docker run \
|
||||
--workdir /github/workspace \
|
||||
--rm \
|
||||
${ImageEnvironmentFactory.getEnvVarString(parameters)} \
|
||||
${ImageEnvironmentFactory.getEnvVarString(parameters, additionalVariables)} \
|
||||
--env UNITY_SERIAL \
|
||||
--env GITHUB_WORKSPACE=/github/workspace \
|
||||
${gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : ''} \
|
||||
@ -41,8 +61,10 @@ class Docker {
|
||||
--volume "${actionFolder}/unity-config:/usr/share/unity3d/config/:z" \
|
||||
${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \
|
||||
${sshAgent ? '--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro' : ''} \
|
||||
${entrypointBash ? `--entrypoint ${commandPrefix}` : ``} \
|
||||
${image} \
|
||||
/bin/bash -c /entrypoint.sh`;
|
||||
${entrypointBash ? `-c` : `${commandPrefix} -c`} \
|
||||
"${overrideCommands !== '' ? overrideCommands : `/entrypoint.sh`}"`;
|
||||
}
|
||||
|
||||
static getWindowsCommand(image: any, parameters: any): string {
|
||||
|
5
src/model/github.ts
Normal file
5
src/model/github.ts
Normal file
@ -0,0 +1,5 @@
|
||||
class GitHub {
|
||||
public static githubInputEnabled: boolean = true;
|
||||
}
|
||||
|
||||
export default GitHub;
|
@ -7,8 +7,8 @@ class Parameter {
|
||||
}
|
||||
|
||||
class ImageEnvironmentFactory {
|
||||
public static getEnvVarString(parameters) {
|
||||
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(parameters);
|
||||
public static getEnvVarString(parameters, additionalVariables: any[] = []) {
|
||||
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(parameters, additionalVariables);
|
||||
let string = '';
|
||||
for (const p of environmentVariables) {
|
||||
if (p.value === '' || p.value === undefined) {
|
||||
@ -16,6 +16,7 @@ class ImageEnvironmentFactory {
|
||||
}
|
||||
if (p.name !== 'ANDROID_KEYSTORE_BASE64' && p.value.toString().includes(`\n`)) {
|
||||
string += `--env ${p.name} `;
|
||||
process.env[p.name] = p.value.toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -24,8 +25,8 @@ class ImageEnvironmentFactory {
|
||||
|
||||
return string;
|
||||
}
|
||||
public static getEnvironmentVariables(parameters: BuildParameters) {
|
||||
const environmentVariables: Parameter[] = [
|
||||
public static getEnvironmentVariables(parameters: BuildParameters, additionalVariables: any[] = []) {
|
||||
let environmentVariables: Parameter[] = [
|
||||
{ name: 'UNITY_LICENSE', value: process.env.UNITY_LICENSE || ReadLicense() },
|
||||
{ name: 'UNITY_LICENSE_FILE', value: process.env.UNITY_LICENSE_FILE },
|
||||
{ name: 'UNITY_EMAIL', value: process.env.UNITY_EMAIL },
|
||||
@ -67,6 +68,26 @@ class ImageEnvironmentFactory {
|
||||
{ name: 'RUNNER_TEMP', value: process.env.RUNNER_TEMP },
|
||||
{ name: 'RUNNER_WORKSPACE', value: process.env.RUNNER_WORKSPACE },
|
||||
];
|
||||
if (parameters.cloudRunnerCluster === 'local-docker') {
|
||||
for (const element of additionalVariables) {
|
||||
if (
|
||||
environmentVariables.find(
|
||||
(x) => element !== undefined && element.name !== undefined && x.name === element.name,
|
||||
) === undefined
|
||||
) {
|
||||
environmentVariables.push(element);
|
||||
}
|
||||
}
|
||||
for (const variable of environmentVariables) {
|
||||
if (
|
||||
environmentVariables.find(
|
||||
(x) => variable !== undefined && variable.name !== undefined && x.name === variable.name,
|
||||
) === undefined
|
||||
) {
|
||||
environmentVariables = environmentVariables.filter((x) => x !== variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parameters.sshAgent) environmentVariables.push({ name: 'SSH_AUTH_SOCK', value: '/ssh-agent' });
|
||||
|
||||
return environmentVariables;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Platform from './platform';
|
||||
|
||||
import BuildParameters from './build-parameters';
|
||||
import Input from './input';
|
||||
|
||||
class ImageTag {
|
||||
public repository: string;
|
||||
@ -83,7 +84,7 @@ class ImageTag {
|
||||
case Platform.types.StandaloneWindows:
|
||||
case Platform.types.StandaloneWindows64:
|
||||
// Can only build windows-il2cpp on a windows based system
|
||||
if (process.platform === 'win32') {
|
||||
if (Input.useIL2Cpp && process.platform === 'win32') {
|
||||
// Unity versions before 2019.3 do not support il2cpp
|
||||
if (major >= 2020 || (major === 2019 && minor >= 3)) {
|
||||
return windowsIl2cpp;
|
||||
@ -96,7 +97,7 @@ class ImageTag {
|
||||
return windows;
|
||||
case Platform.types.StandaloneLinux64: {
|
||||
// Unity versions before 2019.3 do not support il2cpp
|
||||
if (major >= 2020 || (major === 2019 && minor >= 3)) {
|
||||
if ((Input.useIL2Cpp && major >= 2020) || (major === 2019 && minor >= 3)) {
|
||||
return linuxIl2cpp;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system';
|
||||
import Input from '../input';
|
||||
import CloudRunnerOptions from '../cloud-runner/cloud-runner-options';
|
||||
|
||||
export class GenericInputReader {
|
||||
public static async Run(command) {
|
||||
if (Input.cloudRunnerCluster === 'local') {
|
||||
if (CloudRunnerOptions.cloudRunnerCluster === 'local') {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GitRepoReader } from './git-repo';
|
||||
import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system';
|
||||
import Input from '../input';
|
||||
import CloudRunnerOptions from '../cloud-runner/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(Input, 'cloudRunnerCluster', 'get').mockReturnValue('not-local');
|
||||
await jest.spyOn(CloudRunnerOptions, 'cloudRunnerCluster', '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(Input, 'cloudRunnerCluster', 'get').mockReturnValue('not-local');
|
||||
await jest.spyOn(CloudRunnerOptions, 'cloudRunnerCluster', 'get').mockReturnValue('not-local');
|
||||
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
||||
});
|
||||
});
|
||||
|
@ -2,15 +2,19 @@ import { assert } from 'console';
|
||||
import fs from '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 Input from '../input';
|
||||
|
||||
export class GitRepoReader {
|
||||
public static async GetRemote() {
|
||||
if (Input.cloudRunnerCluster === 'local') {
|
||||
if (CloudRunnerOptions.cloudRunnerCluster === 'local') {
|
||||
return '';
|
||||
}
|
||||
assert(fs.existsSync(`.git`));
|
||||
const value = (await CloudRunnerSystem.Run(`git remote -v`, false, true)).replace(/ /g, ``);
|
||||
const value = (await CloudRunnerSystem.Run(`cd ${Input.projectPath} && git remote -v`, false, true)).replace(
|
||||
/ /g,
|
||||
``,
|
||||
);
|
||||
CloudRunnerLogger.log(`value ${value}`);
|
||||
assert(value.includes('github.com'));
|
||||
|
||||
@ -18,12 +22,12 @@ export class GitRepoReader {
|
||||
}
|
||||
|
||||
public static async GetBranch() {
|
||||
if (Input.cloudRunnerCluster === 'local') {
|
||||
if (CloudRunnerOptions.cloudRunnerCluster === 'local') {
|
||||
return '';
|
||||
}
|
||||
assert(fs.existsSync(`.git`));
|
||||
|
||||
return (await CloudRunnerSystem.Run(`git branch --show-current`, false, true))
|
||||
return (await CloudRunnerSystem.Run(`cd ${Input.projectPath} && git branch --show-current`, false, true))
|
||||
.split('\n')[0]
|
||||
.replace(/ /g, ``)
|
||||
.replace('/head', '');
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system';
|
||||
import * as core from '@actions/core';
|
||||
import Input from '../input';
|
||||
import CloudRunnerOptions from '../cloud-runner/cloud-runner-options';
|
||||
|
||||
export class GithubCliReader {
|
||||
static async GetGitHubAuthToken() {
|
||||
if (Input.cloudRunnerCluster === 'local') {
|
||||
if (CloudRunnerOptions.cloudRunnerCluster === 'local') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import YAML from 'yaml';
|
||||
import Input from '../input';
|
||||
import CloudRunnerOptions from '../cloud-runner/cloud-runner-options';
|
||||
|
||||
export function ReadLicense() {
|
||||
if (Input.cloudRunnerCluster === 'local') {
|
||||
if (CloudRunnerOptions.cloudRunnerCluster === 'local') {
|
||||
return '';
|
||||
}
|
||||
const pipelineFile = path.join(__dirname, `.github`, `workflows`, `cloud-runner-k8s-pipeline.yml`);
|
||||
|
@ -3,6 +3,7 @@ import path from 'path';
|
||||
import { Cli } from './cli/cli';
|
||||
import CloudRunnerQueryOverride from './cloud-runner/services/cloud-runner-query-override';
|
||||
import Platform from './platform';
|
||||
import GitHub from './github';
|
||||
|
||||
const core = require('@actions/core');
|
||||
|
||||
@ -14,10 +15,8 @@ const core = require('@actions/core');
|
||||
* Todo: rename to UserInput and remove anything that is not direct input from the user / ci workflow
|
||||
*/
|
||||
class Input {
|
||||
public static githubInputEnabled: boolean = true;
|
||||
|
||||
public static getInput(query) {
|
||||
if (Input.githubInputEnabled) {
|
||||
if (GitHub.githubInputEnabled) {
|
||||
const coreInput = core.getInput(query);
|
||||
if (coreInput && coreInput !== '') {
|
||||
return coreInput;
|
||||
@ -61,17 +60,6 @@ class Input {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
static get cloudRunnerBuilderPlatform() {
|
||||
const input = Input.getInput('cloudRunnerBuilderPlatform');
|
||||
if (input) {
|
||||
return input;
|
||||
}
|
||||
if (Input.cloudRunnerCluster !== 'local') {
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static get gitSha() {
|
||||
if (Input.getInput(`GITHUB_SHA`)) {
|
||||
@ -81,6 +69,10 @@ class Input {
|
||||
}
|
||||
}
|
||||
|
||||
static get useIL2Cpp() {
|
||||
return Input.getInput(`useIL2Cpp`) || true;
|
||||
}
|
||||
|
||||
static get runNumber() {
|
||||
return Input.getInput('GITHUB_RUN_NUMBER') || '0';
|
||||
}
|
||||
@ -179,34 +171,6 @@ class Input {
|
||||
return core.getInput('gitPrivateToken') || false;
|
||||
}
|
||||
|
||||
static get customJob() {
|
||||
return Input.getInput('customJob') || '';
|
||||
}
|
||||
|
||||
static customJobHooks() {
|
||||
return Input.getInput('customJobHooks') || '';
|
||||
}
|
||||
|
||||
static cachePushOverrideCommand() {
|
||||
return Input.getInput('cachePushOverrideCommand') || '';
|
||||
}
|
||||
|
||||
static cachePullOverrideCommand() {
|
||||
return Input.getInput('cachePullOverrideCommand') || '';
|
||||
}
|
||||
|
||||
static readInputFromOverrideList() {
|
||||
return Input.getInput('readInputFromOverrideList') || '';
|
||||
}
|
||||
|
||||
static readInputOverrideCommand() {
|
||||
return Input.getInput('readInputOverrideCommand') || '';
|
||||
}
|
||||
|
||||
static get cloudRunnerBranch() {
|
||||
return Input.getInput('cloudRunnerBranch') || 'cloud-runner-develop';
|
||||
}
|
||||
|
||||
static get chownFilesTo() {
|
||||
return Input.getInput('chownFilesTo') || '';
|
||||
}
|
||||
@ -217,66 +181,6 @@ class Input {
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get postBuildSteps() {
|
||||
return Input.getInput('postBuildSteps') || '';
|
||||
}
|
||||
|
||||
static get preBuildSteps() {
|
||||
return Input.getInput('preBuildSteps') || '';
|
||||
}
|
||||
|
||||
static get awsBaseStackName() {
|
||||
return Input.getInput('awsBaseStackName') || 'game-ci';
|
||||
}
|
||||
|
||||
static get cloudRunnerCluster() {
|
||||
if (Cli.isCliMode) {
|
||||
return Input.getInput('cloudRunnerCluster') || 'aws';
|
||||
}
|
||||
|
||||
return Input.getInput('cloudRunnerCluster') || 'local';
|
||||
}
|
||||
|
||||
static get cloudRunnerCpu() {
|
||||
return Input.getInput('cloudRunnerCpu');
|
||||
}
|
||||
|
||||
static get cloudRunnerMemory() {
|
||||
return Input.getInput('cloudRunnerMemory');
|
||||
}
|
||||
|
||||
static get kubeConfig() {
|
||||
return Input.getInput('kubeConfig') || '';
|
||||
}
|
||||
|
||||
static get kubeVolume() {
|
||||
return Input.getInput('kubeVolume') || '';
|
||||
}
|
||||
|
||||
static get kubeVolumeSize() {
|
||||
return Input.getInput('kubeVolumeSize') || '5Gi';
|
||||
}
|
||||
|
||||
static get kubeStorageClass(): string {
|
||||
return Input.getInput('kubeStorageClass') || '';
|
||||
}
|
||||
|
||||
static get checkDependencyHealthOverride(): string {
|
||||
return Input.getInput('checkDependencyHealthOverride') || '';
|
||||
}
|
||||
|
||||
static get startDependenciesOverride(): string {
|
||||
return Input.getInput('startDependenciesOverride') || '';
|
||||
}
|
||||
|
||||
static get cacheKey(): string {
|
||||
return Input.getInput('cacheKey') || Input.branch;
|
||||
}
|
||||
|
||||
static get cloudRunnerTests(): boolean {
|
||||
return Input.getInput(`cloudRunnerTests`) || false;
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input: string) {
|
||||
if (input.toUpperCase() === input) {
|
||||
return input;
|
||||
|
Loading…
Reference in New Issue
Block a user