diff --git a/.github/workflows/aws-tests.yml b/.github/workflows/aws-tests.yml deleted file mode 100644 index 0c08dd1f..00000000 --- a/.github/workflows/aws-tests.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: AWS - -on: - push: { branches: [aws, remote-builder/refactor] } - -env: - AWS_REGION: 'eu-west-1' - -jobs: - buildForAllPlatforms: - name: AWS Fargate Build - if: github.event.pull_request.draft == false - 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. - #- iOS # Build an iOS player. - #- Android # Build an Android .apk. - #- WebGL # WebGL. - # - 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 - if: github.event.event_type != 'pull_request_target' - with: - lfs: true - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-2 - - uses: ./ - id: aws-fargate-unity-build - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: eu-west-2 - with: - remoteBuildCluster: aws - projectPath: ${{ matrix.projectPath }} - unityVersion: ${{ matrix.unityVersion }} - targetPlatform: ${{ matrix.targetPlatform }} - githubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cloud-runner-aws-pipeline.yml b/.github/workflows/cloud-runner-aws-pipeline.yml new file mode 100644 index 00000000..3debe66c --- /dev/null +++ b/.github/workflows/cloud-runner-aws-pipeline.yml @@ -0,0 +1,111 @@ +name: Cloud Runner - AWS Tests + +on: + push: { branches: [main, aws, remote-builder/unified-providers] } + +env: + GKE_ZONE: 'us-central1' + GKE_REGION: 'us-central1' + GKE_PROJECT: 'unitykubernetesbuilder' + GKE_CLUSTER: 'unity-builder-cluster' + GCP_LOGGING: true + GCP_PROJECT: unitykubernetesbuilder + AWS_REGION: eu-west-2 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: eu-west-2 + AWS_BASE_STACK_NAME: game-ci-github-pipelines + CLOUD_RUNNER_BRANCH: remote-builder/unified-providers + CLOUD_RUNNER_TESTS: true + DEBUG: true + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + +jobs: + buildForAllPlatforms: + name: AWS Fargate Build + if: github.event.pull_request.draft == false + 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. + #- iOS # Build an iOS player. + #- Android # Build an Android .apk. + #- WebGL # WebGL. + # - 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 + if: github.event.event_type != 'pull_request_target' + with: + lfs: true + - 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 + - run: yarn run cli --help + - run: yarn run test-i-aws + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + PROJECT_PATH: ${{ matrix.projectPath }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET_PLATFORM: ${{ matrix.targetPlatform }} + - uses: ./ + id: aws-fargate-unity-build + timeout-minutes: 25 + with: + cloudRunnerCluster: aws + 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/$BRANCH + echo "/data/cache/$BRANCH/build-$BUILD_GUID.zip s3://game-ci-test-storage/$BRANCH/$BUILD_FILE" + aws s3 cp /data/cache/$BRANCH/build-$BUILD_GUID.zip s3://game-ci-test-storage/$BRANCH/build-$BUILD_GUID.zip + aws s3 cp /data/cache/$BRANCH s3://game-ci-test-storage/$BRANCH/$BUILD_GUID + 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.BRANCH }}/build-${{ steps.aws-fargate-unity-build.outputs.BUILD_GUID }}.zip build-${{ steps.aws-fargate-unity-build.outputs.BUILD_GUID }}.zip + ls + ########################### + # 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 }}.zip + retention-days: 14 diff --git a/.github/workflows/cloud-runner-k8s-pipeline.yml b/.github/workflows/cloud-runner-k8s-pipeline.yml new file mode 100644 index 00000000..00ad445f --- /dev/null +++ b/.github/workflows/cloud-runner-k8s-pipeline.yml @@ -0,0 +1,125 @@ +name: Cloud Runner - K8s Tests + +on: + push: { branches: [remote-builder/k8s, remote-builder/unified-providers] } +# push: { branches: [main] } +# pull_request: +# paths-ignore: +# - '.github/**' + +env: + GKE_ZONE: 'us-central1' + GKE_REGION: 'us-central1' + GKE_PROJECT: 'unitykubernetesbuilder' + GKE_CLUSTER: 'game-ci-github-pipelines' + GCP_LOGGING: true + GCP_PROJECT: unitykubernetesbuilder + GCP_LOG_FILE: ${{ github.workspace }}/cloud-runner-logs.txt + AWS_REGION: eu-west-2 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: eu-west-2 + AWS_BASE_STACK_NAME: game-ci-github-pipelines + CLOUD_RUNNER_BRANCH: remote-builder/unified-providers + CLOUD_RUNNER_TESTS: true + DEBUG: true + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + +jobs: + k8sBuilds: + name: K8s (GKE Autopilot) build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + unityVersion: + # - 2019.2.11f1 + - 2019.3.15f1 + targetPlatform: + # - StandaloneWindows64 + - StandaloneLinux64 + steps: + ########################### + # Checkout # + ########################### + - uses: actions/checkout@v2 + if: github.event.event_type != 'pull_request_target' + with: + lfs: true + + ########################### + # Setup # + ########################### + - uses: google-github-actions/setup-gcloud@master + 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 + + ########################### + # Cloud Runner Test Suite # + ########################### + - uses: actions/setup-node@v2 + with: + node-version: 12.x + - run: yarn + - run: yarn run cli --help + - name: Cloud Runner Test Suite + run: yarn run test-i-k8s --detectOpenHandles --forceExit + 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 }} + + ########################### + # 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/$BRANCH + echo "/data/cache/$BRANCH/build-$BUILD_GUID.zip s3://game-ci-test-storage/$BRANCH/$BUILD_FILE" + aws s3 cp /data/cache/$BRANCH/build-$BUILD_GUID.zip s3://game-ci-test-storage/$BRANCH/build-$BUILD_GUID.zip + 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.k8s-unity-build.outputs.BRANCH }}/build-${{ steps.k8s-unity-build.outputs.BUILD_GUID }}.zip build-${{ steps.k8s-unity-build.outputs.BUILD_GUID }}.zip + ls + ########################### + # Upload # + ########################### + # download from cloud storage + - uses: actions/upload-artifact@v2 + with: + name: K8s Build (${{ matrix.targetPlatform }}) + path: build-${{ steps.k8s-unity-build.outputs.BUILD_GUID }}.zip + retention-days: 14 diff --git a/.github/workflows/kubernetes-tests.yml b/.github/workflows/kubernetes-tests.yml deleted file mode 100644 index bff52418..00000000 --- a/.github/workflows/kubernetes-tests.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Kubernetes - -on: - workflow_dispatch: {} -# push: { branches: [main] } -# pull_request: -# paths-ignore: -# - '.github/**' - -env: - GKE_ZONE: 'us-central1-c' - GKE_REGION: 'us-central1' - GKE_PROJECT: 'unitykubernetesbuilder' - GKE_CLUSTER: 'unity-builder-cluster' - UNITY_LICENSE: "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \nm0Db8UK+ktnOLJBtHybkfetpcKo=o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==" - -jobs: - k8sBuilds: - name: K8s build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }} - runs-on: ubuntu-latest - continue-on-error: true - strategy: - fail-fast: false - matrix: - targetPlatform: - - StandaloneLinux64 - - StandaloneWindows64 - steps: - ########################### - # Checkout # - ########################### - - uses: actions/checkout@v2 - with: - lfs: true - - ########################### - # Spin up # - ########################### - - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master - with: - version: '288.0.0' - service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} - service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} - - run: ./dist/bootstrapper/ApplyClusterAndAcquireLock.sh ${{ env.GKE_PROJECT }} ${{ env.GKE_CLUSTER }} ${{ env.GKE_ZONE }} - - ########################### - # Build # - ########################### - - uses: frostebite/File-To-Base64@master - id: read-base64 - with: - filePath: ~/.kube/config - - uses: ./ - id: k8s-unity-build - with: - targetPlatform: ${{ matrix.targetPlatform }} - kubeConfig: ${{ steps.read-base64.outputs.base64 }} - githubToken: ${{ secrets.GITHUB_TOKEN }} - projectPath: test-project - unityVersion: 2019.3.15f1 - - ########################### - # Upload # - ########################### - - uses: frostebite/K8s-Download-Volume@master - with: - kubeConfig: ${{ steps.read-base64.outputs.base64 }} - volume: ${{ steps.k8s-unity-build.outputs.volume }} - sourcePath: repo/build/ - - uses: actions/upload-artifact@v2 - with: - name: Kubernetes Build (${{ matrix.targetPlatform }}) - path: k8s-volume-download - retention-days: 14 - - ########################### - # Spin down # - ########################### - - run: ./dist/bootstrapper/ReleaseLockAndAttemptShutdown.sh ${{ env.GKE_PROJECT }} ${{ env.GKE_CLUSTER }} ${{ env.GKE_ZONE }} - if: ${{ always() }} diff --git a/.gitignore b/.gitignore index 3fbcc036..6d0f09f8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules coverage/ lib/ .vsconfig +yarn-error.log diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e22e08b0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "god.tsconfig": "./tsconfig.json", + "yaml.customTags": [ + "!And", + "!And sequence", + "!If", + "!If sequence", + "!Not", + "!Not sequence", + "!Equals", + "!Equals sequence", + "!Or", + "!Or sequence", + "!FindInMap", + "!FindInMap sequence", + "!Base64", + "!Join", + "!Join sequence", + "!Cidr", + "!Ref", + "!Sub", + "!Sub sequence", + "!GetAtt", + "!GetAZs", + "!ImportValue", + "!ImportValue sequence", + "!Select", + "!Select sequence", + "!Split", + "!Split sequence" + ] +} diff --git a/action.yml b/action.yml index 6d778ac2..9db67e8b 100644 --- a/action.yml +++ b/action.yml @@ -22,6 +22,18 @@ inputs: required: false default: '' description: 'Name of 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' + 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)' + 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)' buildsPath: required: false default: '' @@ -30,7 +42,7 @@ inputs: required: false default: '' description: 'Path to a Namespace.Class.StaticMethod to run to perform the build.' - remoteBuildCluster: + 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.' @@ -46,12 +58,12 @@ inputs: default: '' required: false description: 'Supply a Persistent Volume Claim name to use for the Unity build.' - remoteBuildMemory: - default: '800M' + cloudRunnerMemory: + default: '750M' required: false description: 'Amount of memory to assign the remote build container' - remoteBuildCpu: - default: '0.25' + cloudRunnerCpu: + default: '1.0' required: false description: 'Amount of CPU time to assign the remote build container' kubeVolumeSize: diff --git a/cloud-runner-logs b/cloud-runner-logs new file mode 100644 index 00000000..af8c1034 --- /dev/null +++ b/cloud-runner-logs @@ -0,0 +1,13 @@ +Cloud Runner platform selected AWS +Cloud Runner is running in custom job mode +AWS Region: eu-west-2 +Parsing build steps: + - name: 'step 1' + image: 'alpine' + commands: 'printenv' + secrets: + - name: 'testSecretName' + value: 'testSecretValue' + +game-ci stack does not exist (["game-ci-github-automation-424-linux64-a9hz-cleanup","game-ci-github-automation-423-linux64-v34g-cleanup","game-ci-github-automation-423-linux64-v34g","game-ci-github-automation-422-linux64-7x6i-cleanup","game-ci-github-automation-422-linux64-7x6i","game-ci-github-automation-414-linux64-j21p-cleanup","game-ci-github-automation-414-linux64-j21p","game-ci-github-automation-413-linux64-tcih-cleanup","game-ci-github-automation-413-linux64-tcih","game-ci-github-automation-411-linux64-0s69-cleanup","game-ci-github-automation-411-linux64-0s69","game-ci-github-automation-410-linux64-1tli-cleanup","game-ci-github-automation-410-linux64-1tli","game-ci-github-automation-408-linux64-8pbw-cleanup","game-ci-github-automation-408-linux64-8pbw","game-ci-github-automation-407-linux64-21un-cleanup","game-ci-github-automation-407-linux64-21un","game-ci-github-automation-406-linux64-dizb-cleanup","game-ci-github-automation-406-linux64-dizb","game-ci-github-automation-405-linux64-9xj5-cleanup","game-ci-github-automation-405-linux64-9xj5","game-ci-github-automation-402-linux64-0bym-cleanup","game-ci-github-automation-402-linux64-0bym","game-ci-github-automation-400-linux64-arqv-cleanup","game-ci-github-automation-400-linux64-arqv","game-ci-github-automation-399-linux64-utkt-cleanup","game-ci-github-automation-399-linux64-utkt","game-ci-github-automation-397-linux64-xwfu-cleanup","game-ci-github-automation-397-linux64-xwfu","game-ci-github-automation-396-linux64-2g3q-cleanup","game-ci-github-automation-396-linux64-2g3q","game-ci-github-automation","game-ci-stack-integration-tests-390-linux64-mcdw-cleanup","game-ci-stack-integration-tests-390-linux64-mcdw","game-ci-stack-integration-tests-391-linux64-2arq-cleanup","game-ci-stack-integration-tests-391-linux64-2arq","game-ci-stack-integration-tests-390-linux64-awd0-cleanup","game-ci-stack-integration-tests-390-linux64-awd0","game-ci-stack-integration-tests"]) +created stack (version: eedce7440581ab2e8a80cee59e34ed64) diff --git a/dist/bootstrapper/ApplyClusterAndAcquireLock.sh b/dist/bootstrapper/ApplyClusterAndAcquireLock.sh deleted file mode 100755 index 8f0c0db9..00000000 --- a/dist/bootstrapper/ApplyClusterAndAcquireLock.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/sh - -# This creates a GKE Cluster -# - Will wait for any deletion to complete on a cluster with the same name before creating -# - Will wait for completion before continuing -# - If the script is run concurrently multiple times, only one cluster will be created, all instances will wait for availability -# Requires GCP Cloud SDK -# Installs retry https://github.com/kadwanev/retry - -GKE_PROJECT=$1 -GKE_CLUSTER=$2 -GKE_ZONE=$3 - -# may update this to avoid repeated install, drop me a comment if needed -sudo sh -c "curl https://raw.githubusercontent.com/kadwanev/retry/master/retry -o /usr/local/bin/retry && chmod +x /usr/local/bin/retry" - -attempts=0 -while [ $attempts -le 1 ] -do -retry -s 15 -t 20 -v ' - STATUS=$(gcloud container clusters list --format="json" --project $GKE_PROJECT | - jq " - .[] | - {name: .name, status: .status} | - select(.name == \"$GKE_CLUSTER\") - " | - jq ".status") - if [ "$STATUS" == "\"STOPPING\"" ]; then echo "Cluster stopping waiting for completion" && exit 1; fi - exit 0 - ' -cluster=$(gcloud container clusters list --project $GKE_PROJECT --format="json" | jq '.[] | select(.name == "${GKE_CLUSTER}")') - -if [ -z "$cluster" ]; -then - echo "No clusters found for \"$GKE_CLUSTER\" in project \"$GKE_CLUSTER\" in zone \"$GKE_ZONE\"" - # you may not need this, it installs GCP beta for additional command line options - gcloud components install beta -q - # replace this line with whatever type of cluster you want to create - gcloud beta container --project $GKE_PROJECT clusters create $GKE_CLUSTER --zone $GKE_ZONE --no-enable-basic-auth --cluster-version "1.15.12-gke.2" --machine-type "custom-1-3072" --image-type "COS" --disk-type "pd-standard" --disk-size "15" --metadata disable-legacy-endpoints=true --scopes "https://www.googleapis.com/auth/devstorage.read_only","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/monitoring","https://www.googleapis.com/auth/servicecontrol","https://www.googleapis.com/auth/service.management.readonly","https://www.googleapis.com/auth/trace.append" --num-nodes "1" --enable-stackdriver-kubernetes --enable-ip-alias --default-max-pods-per-node "110" --enable-autoscaling --min-nodes "0" --max-nodes "3" --no-enable-master-authorized-networks --addons HorizontalPodAutoscaling,HttpLoadBalancing --enable-autoupgrade --enable-autorepair --max-surge-upgrade 1 --max-unavailable-upgrade 0 -fi; -retry -s 15 -t 20 -v ' - STATUS=$(gcloud container clusters list --format="json" --project $GKE_PROJECT | - jq " - .[] | - {name: .name, status: .status} | - select(.name == \"$GKE_CLUSTER\") - " | - jq ".status") - if [ "$STATUS" == "\"PROVISIONING\"" ]; then echo "Cluster provisioning waiting for available" && exit 1; fi - exit 0 -' -echo "Cluster is available" -gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT -kubectl version -NSID=$(cat /proc/sys/kernel/random/uuid) -echo "::set-env name=NSID::"$NSID -{ -cat <- (Optional) An IAM role to give the service's containers if the code within - needs to access other AWS resources like S3 buckets, DynamoDB tables, etc + needs to access other AWS resources EFSMountDirectory: Type: String Default: '/efsdata' @@ -98,7 +98,7 @@ Resources: Metadata: 'AWS::CloudFormation::Designer': id: c6f18447-b879-4696-8873-f981b2cedd2b - + # template secrets p2 - secret TaskDefinition: diff --git a/dist/index.js b/dist/index.js index 459ce17c..f2f73943 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/dist/index.js.map b/dist/index.js.map index bbd86d36..fe27d15f 100644 Binary files a/dist/index.js.map and b/dist/index.js.map differ diff --git a/dist/licenses.txt b/dist/licenses.txt index bbeddbef..c57e9408 100644 Binary files a/dist/licenses.txt and b/dist/licenses.txt differ diff --git a/dist/platforms/ubuntu/entrypoint.sh b/dist/platforms/ubuntu/entrypoint.sh old mode 100644 new mode 100755 diff --git a/dist/platforms/ubuntu/steps/activate.sh b/dist/platforms/ubuntu/steps/activate.sh old mode 100644 new mode 100755 diff --git a/dist/platforms/ubuntu/steps/build.sh b/dist/platforms/ubuntu/steps/build.sh old mode 100644 new mode 100755 diff --git a/dist/platforms/ubuntu/steps/return_license.sh b/dist/platforms/ubuntu/steps/return_license.sh old mode 100644 new mode 100755 diff --git a/dist/platforms/ubuntu/steps/set_gitcredential.sh b/dist/platforms/ubuntu/steps/set_gitcredential.sh old mode 100644 new mode 100755 diff --git a/package.json b/package.json index 1c2d3f1f..50c66114 100644 --- a/package.json +++ b/package.json @@ -11,26 +11,41 @@ "build": "tsc && ncc build lib --source-map --license licenses.txt", "lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts", "format": "prettier --write \"src/**/*.{js,ts}\"", + "prepare": "husky install", + "cli": "yarn ts-node src/index.ts -m cli", + "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", - "prepare": "husky install" + "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-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\"" }, "dependencies": { "@actions/core": "^1.2.6", "@actions/exec": "^1.0.4", "@actions/github": "^2.2.0", + "@kubernetes/client-node": "^0.14.3", "@octokit/core": "^3.5.1", + "async-wait-until": "^2.0.7", "aws-sdk": "^2.812.0", "base-64": "^1.0.0", + "commander": "^8.3.0", + "commander-ts": "^0.2.0", "kubernetes-client": "^9.0.0", - "nanoid": "^3.1.31", - "semver": "^7.3.5" + "reflect-metadata": "^0.1.13", + "semver": "^7.3.5", + "yaml": "^1.10.2", + "nanoid": "^3.1.31" }, "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^27.0.3", "@types/node": "^14.14.9", "@types/semver": "^7.3.5", "@typescript-eslint/parser": "^4.8.1", "@vercel/ncc": "^0.25.1", + "cross-env": "^7.0.3", "eslint": "^7.17.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-github": "^4.1.1", @@ -43,7 +58,8 @@ "js-yaml": "^3.14.0", "lint-staged": "^12.1.2", "prettier": "^2.2.1", - "ts-jest": "^26.4.4", + "ts-jest": "^26.4.2", + "ts-node": "^10.4.0", "typescript": "^4.1.3" }, "lint-staged": { diff --git a/scripts/cleanupGCPResources.sh b/scripts/cleanupGCPResources.sh new file mode 100755 index 00000000..f3c7cf22 --- /dev/null +++ b/scripts/cleanupGCPResources.sh @@ -0,0 +1,5 @@ +kubectl delete job $(kubectl get jobs -o custom-columns=:.metadata.name) +kubectl delete cronjob $(kubectl get cronjobs -o custom-columns=:.metadata.name) +kubectl delete pod $(kubectl get pods -o custom-columns=:.metadata.name) +kubectl delete pvc $(kubectl get pvc -o custom-columns=:.metadata.name) +kubectl delete secret $(kubectl get secrets -o custom-columns=:.metadata.name) diff --git a/src/index.ts b/src/index.ts index 156ef238..f534c5ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import * as core from '@actions/core'; -import { Action, BuildParameters, Cache, Docker, ImageTag, Kubernetes, Output, RemoteBuilder } from './model'; +import { Action, BuildParameters, Cache, Docker, ImageTag, Output, CloudRunner } from './model'; +import { CLI } from './model/cli/cli'; import PlatformSetup from './model/platform-setup'; - -async function run() { +async function runMain() { try { Action.checkCompatibility(); Cache.verify(); @@ -11,33 +11,28 @@ async function run() { const buildParameters = await BuildParameters.create(); const baseImage = new ImageTag(buildParameters); - let builtImage; - - switch (buildParameters.remoteBuildCluster) { - case 'k8s': - core.info('Building with Kubernetes'); - await Kubernetes.runBuildJob(buildParameters, baseImage); - break; - - case 'aws': - core.info('Building with AWS'); - await RemoteBuilder.build(buildParameters, baseImage); - break; - - // default and local case - default: - core.info('Building locally'); - PlatformSetup.setup(buildParameters); - builtImage = await Docker.build({ path: actionFolder, dockerfile, baseImage }); - await Docker.run(builtImage, { workspace, ...buildParameters }); - break; + if ( + buildParameters.cloudRunnerCluster && + buildParameters.cloudRunnerCluster !== '' && + buildParameters.cloudRunnerCluster !== 'local' + ) { + await CloudRunner.run(buildParameters, baseImage.toString()); + } else { + core.info('Building locally'); + PlatformSetup.setup(buildParameters); + const builtImage = await Docker.build({ path: actionFolder, dockerfile, baseImage }); + await Docker.run(builtImage, { workspace, ...buildParameters }); } // Set output await Output.setBuildVersion(buildParameters.buildVersion); } catch (error) { - core.setFailed(error.message); + core.setFailed((error as Error).message); } } - -run(); +const options = CLI.SetupCli(); +if (CLI.isCliMode(options)) { + CLI.RunCli(options); +} else { + runMain(); +} diff --git a/src/model/build-parameters.test.ts b/src/model/build-parameters.test.ts index edd98c93..c4ab5659 100644 --- a/src/model/build-parameters.test.ts +++ b/src/model/build-parameters.test.ts @@ -42,15 +42,13 @@ describe('BuildParameters', () => { it('returns the android version code with provided input', async () => { const mockValue = '42'; jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ androidVersionCode: mockValue }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: mockValue })); }); it('returns the android version code from version by default', async () => { const mockValue = ''; jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 })); }); it('determines the android sdk manager parameters only once', async () => { @@ -61,19 +59,19 @@ describe('BuildParameters', () => { it('returns the platform', async () => { const mockValue = 'somePlatform'; jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ platform: mockValue })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ platform: mockValue })); }); it('returns the project path', async () => { const mockValue = 'path/to/project'; jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue })); }); it('returns the build name', async () => { const mockValue = 'someBuildName'; jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue })); }); it('returns the build path', async () => { @@ -82,15 +80,13 @@ describe('BuildParameters', () => { const expectedBuildPath = `${mockPath}/${mockPlatform}`; jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath); jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ buildPath: expectedBuildPath }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildPath: expectedBuildPath })); }); it('returns the build file', async () => { const mockValue = 'someBuildName'; jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue })); }); test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])( @@ -98,7 +94,7 @@ describe('BuildParameters', () => { async (targetPlatform) => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); - await expect(BuildParameters.create()).resolves.toEqual( + expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ buildFile: `${targetPlatform}.exe` }), ); }, @@ -108,7 +104,7 @@ describe('BuildParameters', () => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false); - await expect(BuildParameters.create()).resolves.toEqual( + expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ buildFile: `${targetPlatform}.apk` }), ); }); @@ -117,7 +113,7 @@ describe('BuildParameters', () => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true); - await expect(BuildParameters.create()).resolves.toEqual( + expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ buildFile: `${targetPlatform}.aab` }), ); }); @@ -125,53 +121,43 @@ describe('BuildParameters', () => { it('returns the build method', async () => { const mockValue = 'Namespace.ClassName.BuildMethod'; jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue })); }); it('returns the android keystore name', async () => { const mockValue = 'keystore.keystore'; jest.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ androidKeystoreName: mockValue }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystoreName: mockValue })); }); it('returns the android keystore base64-encoded content', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ androidKeystoreBase64: mockValue }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystoreBase64: mockValue })); }); it('returns the android keystore pass', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ androidKeystorePass: mockValue }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystorePass: mockValue })); }); it('returns the android keyalias name', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ androidKeyaliasName: mockValue }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeyaliasName: mockValue })); }); it('returns the android keyalias pass', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( - expect.objectContaining({ androidKeyaliasPass: mockValue }), - ); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeyaliasPass: mockValue })); }); it('returns the android target sdk version', async () => { const mockValue = 'AndroidApiLevelAuto'; jest.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual( + expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ androidTargetSdkVersion: mockValue }), ); }); @@ -179,7 +165,7 @@ describe('BuildParameters', () => { it('returns the custom parameters', async () => { const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue'; jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue); - await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue })); + expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue })); }); }); }); diff --git a/src/model/build-parameters.ts b/src/model/build-parameters.ts index bc4c4ca3..be4770b4 100644 --- a/src/model/build-parameters.ts +++ b/src/model/build-parameters.ts @@ -1,5 +1,8 @@ +import { customAlphabet } from 'nanoid'; import * as core from '@actions/core'; import AndroidVersioning from './android-versioning'; +import CloudRunnerConstants from './cloud-runner/services/cloud-runner-constants'; +import CloudRunnerNamespace from './cloud-runner/services/cloud-runner-namespace'; import Input from './input'; import Platform from './platform'; import UnityVersioning from './unity-versioning'; @@ -27,17 +30,29 @@ class BuildParameters { public androidSdkManagerParameters!: string; public customParameters!: string; public sshAgent!: string; + public cloudRunnerCluster!: string; + public awsBaseStackName!: string; public gitPrivateToken!: string; public remoteBuildCluster!: string; public awsStackName!: string; public kubeConfig!: string; public githubToken!: string; - public remoteBuildMemory!: string; - public remoteBuildCpu!: string; + public cloudRunnerMemory!: string; + public cloudRunnerCpu!: string; public kubeVolumeSize!: string; public kubeVolume!: string; public chownFilesTo!: string; + public postBuildSteps!: string; + public preBuildSteps!: string; + public customJob!: string; + public runNumber!: string; + public branch!: string; + public githubRepo!: string; + public gitSha!: string; + public logId!: string; + public buildGuid!: string; + static async create(): Promise { const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle); @@ -87,16 +102,27 @@ class BuildParameters { androidSdkManagerParameters, customParameters: Input.customParameters, sshAgent: Input.sshAgent, - gitPrivateToken: Input.gitPrivateToken, + gitPrivateToken: await Input.gitPrivateToken(), chownFilesTo: Input.chownFilesTo, - remoteBuildCluster: Input.remoteBuildCluster, - awsStackName: Input.awsStackName, + cloudRunnerCluster: Input.cloudRunnerCluster, + awsBaseStackName: Input.awsBaseStackName, kubeConfig: Input.kubeConfig, - githubToken: Input.githubToken, - remoteBuildMemory: Input.remoteBuildMemory, - remoteBuildCpu: Input.remoteBuildCpu, + githubToken: await Input.githubToken(), + cloudRunnerMemory: Input.cloudRunnerMemory, + cloudRunnerCpu: Input.cloudRunnerCpu, kubeVolumeSize: Input.kubeVolumeSize, kubeVolume: Input.kubeVolume, + postBuildSteps: Input.postBuildSteps, + preBuildSteps: Input.preBuildSteps, + customJob: Input.customJob, + runNumber: Input.runNumber, + branch: await Input.branch(), + githubRepo: await Input.githubRepo(), + remoteBuildCluster: Input.cloudRunnerCluster, + awsStackName: Input.awsBaseStackName, + gitSha: Input.gitSha, + logId: customAlphabet(CloudRunnerConstants.alphabet, 9)(), + buildGuid: CloudRunnerNamespace.generateBuildName(Input.runNumber, Input.targetPlatform), }; } diff --git a/src/model/cli/cli-decorator.ts b/src/model/cli/cli-decorator.ts new file mode 100644 index 00000000..7a2d54cd --- /dev/null +++ b/src/model/cli/cli-decorator.ts @@ -0,0 +1,23 @@ +const targets = new Array(); +export function CliFunction(key: string, description: string) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + targets.push({ + target, + propertyKey, + descriptor, + key, + description, + }); + }; +} +export function GetCliFunctions(key) { + return targets.find((x) => x.key === key); +} +export function GetAllCliModes() { + return targets.map((x) => { + return { + key: x.key, + description: x.description, + }; + }); +} diff --git a/src/model/cli/cli.ts b/src/model/cli/cli.ts new file mode 100644 index 00000000..ed1b39d5 --- /dev/null +++ b/src/model/cli/cli.ts @@ -0,0 +1,88 @@ +import { Command } from 'commander-ts'; +import { BuildParameters, CloudRunner, ImageTag, Input } from '..'; +import * as core from '@actions/core'; +import { ActionYamlReader } from '../input-readers/action-yaml'; +import CloudRunnerLogger from '../cloud-runner/services/cloud-runner-logger'; +import { CliFunction, GetAllCliModes, GetCliFunctions } from './cli-decorator'; +import { RemoteClientLogger } from './remote-client/remote-client-services/remote-client-logger'; +import { CloudRunnerState } from '../cloud-runner/state/cloud-runner-state'; +import { SetupCloudRunnerRepository } from './remote-client/setup-cloud-runner-repository'; +import * as SDK from 'aws-sdk'; + +export class CLI { + static async RunCli(options: any): Promise { + Input.githubInputEnabled = false; + + const results = GetCliFunctions(options.mode); + + if (results === undefined || results.length === 0) { + throw new Error('no CLI mode found'); + } + + CloudRunnerLogger.log(`Entrypoint: ${results.key}`); + + options.versioning = 'None'; + Input.cliOptions = options; + return await results.target[results.propertyKey](); + } + static isCliMode(options: any) { + return options.mode !== undefined && options.mode !== ''; + } + + public static SetupCli() { + const program = new Command(); + program.version('0.0.1'); + const properties = Object.getOwnPropertyNames(Input); + core.info(`\n`); + core.info(`INPUT:`); + const actionYamlReader: ActionYamlReader = new ActionYamlReader(); + for (const element of properties) { + program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element)); + if (Input[element] !== undefined && Input[element] !== '' && typeof Input[element] !== `function`) { + core.info(`${element} ${Input[element]}`); + } + } + core.info(`\n`); + program.option( + '-m, --mode ', + GetAllCliModes() + .map((x) => `${x.key} (${x.description})`) + .join(` | `), + ); + program.parse(process.argv); + + return program.opts(); + } + + @CliFunction(`cli`, `runs a cloud runner build`) + public static async CLIBuild(): Promise { + const buildParameter = await BuildParameters.create(); + const baseImage = new ImageTag(buildParameter); + return await CloudRunner.run(buildParameter, baseImage.toString()); + } + + @CliFunction(`remote-cli`, `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)} + `); + CloudRunnerState.setup(buildParameter); + await SetupCloudRunnerRepository.run(); + } + + @CliFunction(`cach-push`, `push to cache`) + static async cachePush() {} + + @CliFunction(`cach-pull`, `pull from cache`) + static async cachePull() {} + + @CliFunction(`garbage-collect-aws`, `garbage collect aws`) + static async garbageCollectAws() { + process.env.AWS_REGION = Input.region; + const CF = new SDK.CloudFormation(); + + const stacks = await CF.listStacks().promise(); + CloudRunnerLogger.log(JSON.stringify(stacks, undefined, 4)); + } +} diff --git a/src/model/cli/remote-client/remote-client-services/caching.ts b/src/model/cli/remote-client/remote-client-services/caching.ts new file mode 100644 index 00000000..dda7feeb --- /dev/null +++ b/src/model/cli/remote-client/remote-client-services/caching.ts @@ -0,0 +1,117 @@ +import { assert } from 'console'; +import fs from 'fs'; +import path from 'path'; +import { Input } from '../../..'; +import CloudRunnerLogger from '../../../cloud-runner/services/cloud-runner-logger'; +import { CloudRunnerState } from '../../../cloud-runner/state/cloud-runner-state'; +import { CloudRunnerSystem } from './cloud-runner-system'; +import { LFSHashing } from './lfs-hashing'; +import { RemoteClientLogger } from './remote-client-logger'; + +export class Caching { + public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheKey: string) { + const startPath = process.cwd(); + try { + if (!fs.existsSync(cacheFolder)) { + await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`); + } + process.chdir(path.resolve(sourceFolder, '..')); + + if (Input.cloudRunnerTests) { + CloudRunnerLogger.log( + `Hashed cache folder ${await LFSHashing.hashAllFiles(sourceFolder)} ${sourceFolder} ${path.basename( + sourceFolder, + )}`, + ); + } + + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`ls ${path.basename(sourceFolder)}`); + } + await CloudRunnerSystem.Run(`zip ${cacheKey}.zip ${path.basename(sourceFolder)}`); + assert(fs.existsSync(`${cacheKey}.zip`), 'cache zip exists'); + assert(fs.existsSync(path.basename(sourceFolder)), 'source folder exists'); + await CloudRunnerSystem.Run(`mv ${cacheKey}.zip ${cacheFolder}`); + RemoteClientLogger.log(`moved ${cacheKey}.zip to ${cacheFolder}`); + assert(fs.existsSync(`${path.join(cacheFolder, cacheKey)}.zip`), 'cache zip exists inside cache folder'); + + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`ls ${cacheFolder}`); + } + } catch (error) { + process.chdir(`${startPath}`); + throw error; + } + process.chdir(`${startPath}`); + } + public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheKey: string = ``) { + const startPath = process.cwd(); + RemoteClientLogger.log(`Caching for ${path.basename(destinationFolder)}`); + try { + if (!fs.existsSync(cacheFolder)) { + fs.mkdirSync(cacheFolder); + } + + if (!fs.existsSync(destinationFolder)) { + fs.mkdirSync(destinationFolder); + } + + const latestInBranch = await (await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .zip$ | head -1`)) + .replace(/\n/g, ``) + .replace('.zip', ''); + + process.chdir(cacheFolder); + + const cacheSelection = cacheKey !== `` && fs.existsSync(`${cacheKey}.zip`) ? cacheKey : latestInBranch; + await CloudRunnerLogger.log(`cache key ${cacheKey} selection ${cacheSelection}`); + + if (fs.existsSync(`${cacheSelection}.zip`)) { + const resultsFolder = `results${CloudRunnerState.buildParams.buildGuid}`; + await CloudRunnerSystem.Run(`mkdir -p ${resultsFolder}`); + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`tree ${destinationFolder}`); + } + RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.zip`); + assert(`${fs.existsSync(destinationFolder)}`); + assert(`${fs.existsSync(`${cacheSelection}.zip`)}`); + const fullResultsFolder = path.join(cacheFolder, resultsFolder); + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`tree ${cacheFolder}`); + } + await CloudRunnerSystem.Run(`unzip ${cacheSelection}.zip -d ${path.basename(resultsFolder)}`); + RemoteClientLogger.log(`cache item extracted to ${fullResultsFolder}`); + assert(`${fs.existsSync(fullResultsFolder)}`); + const destinationParentFolder = path.resolve(destinationFolder, '..'); + if (fs.existsSync(destinationFolder)) { + fs.rmSync(destinationFolder, { recursive: true, force: true }); + } + await CloudRunnerSystem.Run( + `mv "${fullResultsFolder}/${path.basename(destinationFolder)}" "${destinationParentFolder}"`, + ); + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`tree ${destinationParentFolder}`); + } + } else { + RemoteClientLogger.logWarning(`cache item ${cacheKey} doesn't exist ${destinationFolder}`); + if (cacheSelection !== ``) { + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`tree ${cacheFolder}`); + } + RemoteClientLogger.logWarning(`cache item ${cacheKey}.zip doesn't exist ${destinationFolder}`); + throw new Error(`Failed to get cache item, but cache hit was found: ${cacheSelection}`); + } + } + } catch (error) { + process.chdir(`${startPath}`); + throw error; + } + process.chdir(`${startPath}`); + } + + public static handleCachePurging() { + if (process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined) { + RemoteClientLogger.log(`purging ${CloudRunnerState.purgeRemoteCaching}`); + fs.rmdirSync(CloudRunnerState.cacheFolder, { recursive: true }); + } + } +} diff --git a/src/model/cli/remote-client/remote-client-services/cloud-runner-system.ts b/src/model/cli/remote-client/remote-client-services/cloud-runner-system.ts new file mode 100644 index 00000000..ed119487 --- /dev/null +++ b/src/model/cli/remote-client/remote-client-services/cloud-runner-system.ts @@ -0,0 +1,37 @@ +import { exec } from 'child_process'; +import { RemoteClientLogger } from './remote-client-logger'; + +export class CloudRunnerSystem { + public static async Run(command: string, suppressError = false) { + for (const element of command.split(`\n`)) { + RemoteClientLogger.log(element); + } + return await new Promise((promise) => { + let output = ''; + const child = exec(command, (error, stdout, stderr) => { + if (error && !suppressError) { + throw error; + } + if (stderr) { + const diagnosticOutput = `${stderr.toString()}`; + RemoteClientLogger.logCliDiagnostic(diagnosticOutput); + output += diagnosticOutput; + return; + } + const outputChunk = `${stdout}`; + output += outputChunk; + }); + child.on('close', function (code) { + RemoteClientLogger.log(`[Exit code ${code}]`); + if (code !== 0 && !suppressError) { + throw new Error(output); + } + const outputLines = output.split(`\n`); + for (const element of outputLines) { + RemoteClientLogger.log(element); + } + promise(output); + }); + }); + } +} diff --git a/src/model/cli/remote-client/remote-client-services/lfs-hashing.ts b/src/model/cli/remote-client/remote-client-services/lfs-hashing.ts new file mode 100644 index 00000000..343d852f --- /dev/null +++ b/src/model/cli/remote-client/remote-client-services/lfs-hashing.ts @@ -0,0 +1,42 @@ +import path from 'path'; +import { CloudRunnerState } from '../../../cloud-runner/state/cloud-runner-state'; +import { CloudRunnerSystem } from './cloud-runner-system'; +import fs from 'fs'; +import { assert } from 'console'; +import { Input } from '../../..'; +import { RemoteClientLogger } from './remote-client-logger'; + +export class LFSHashing { + public static async createLFSHashFiles() { + try { + await CloudRunnerSystem.Run(`git lfs ls-files -l | cut -d ' ' -f1 | sort > .lfs-assets-guid`); + await CloudRunnerSystem.Run(`md5sum .lfs-assets-guid > .lfs-assets-guid-sum`); + assert(fs.existsSync(`.lfs-assets-guid-sum`)); + assert(fs.existsSync(`.lfs-assets-guid`)); + const lfsHashes = { + lfsGuid: fs + .readFileSync(`${path.join(CloudRunnerState.repoPathFull, `.lfs-assets-guid`)}`, 'utf8') + .replace(/\n/g, ``), + lfsGuidSum: fs + .readFileSync(`${path.join(CloudRunnerState.repoPathFull, `.lfs-assets-guid-sum`)}`, 'utf8') + .replace(/\n/g, ``), + }; + if (Input.cloudRunnerTests) { + RemoteClientLogger.log(lfsHashes.lfsGuid); + RemoteClientLogger.log(lfsHashes.lfsGuidSum); + } + return lfsHashes; + } catch (error) { + throw error; + } + } + public static async hashAllFiles(folder: string) { + const startPath = process.cwd(); + process.chdir(folder); + const result = await (await CloudRunnerSystem.Run(`find -type f -exec md5sum "{}" + | sort | md5sum`)) + .replace(/\n/g, '') + .split(` `)[0]; + process.chdir(startPath); + return result; + } +} diff --git a/src/model/cli/remote-client/remote-client-services/remote-client-logger.ts b/src/model/cli/remote-client/remote-client-services/remote-client-logger.ts new file mode 100644 index 00000000..b46d184c --- /dev/null +++ b/src/model/cli/remote-client/remote-client-services/remote-client-logger.ts @@ -0,0 +1,19 @@ +import CloudRunnerLogger from '../../../cloud-runner/services/cloud-runner-logger'; + +export class RemoteClientLogger { + public static log(message: string) { + CloudRunnerLogger.log(`[Client] ${message}`); + } + + public static logCliError(message: string) { + CloudRunnerLogger.log(`[Client][Error] ${message}`); + } + + public static logCliDiagnostic(message: string) { + CloudRunnerLogger.log(`[Client][Diagnostic] ${message}`); + } + + public static logWarning(message) { + CloudRunnerLogger.logWarning(message); + } +} diff --git a/src/model/cli/remote-client/setup-cloud-runner-repository.ts b/src/model/cli/remote-client/setup-cloud-runner-repository.ts new file mode 100644 index 00000000..e8240d88 --- /dev/null +++ b/src/model/cli/remote-client/setup-cloud-runner-repository.ts @@ -0,0 +1,81 @@ +import fs from 'fs'; +import { CloudRunnerState } from '../../cloud-runner/state/cloud-runner-state'; +import { Caching } from './remote-client-services/caching'; +import { LFSHashing } from './remote-client-services/lfs-hashing'; +import { CloudRunnerSystem } from './remote-client-services/cloud-runner-system'; +import { Input } from '../..'; +import { RemoteClientLogger } from './remote-client-services/remote-client-logger'; +import path from 'path'; +import { assert } from 'console'; + +export class SetupCloudRunnerRepository { + public static async run() { + try { + await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerState.buildPathFull}`); + await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerState.repoPathFull}`); + await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerState.cacheFolderFull}`); + + process.chdir(CloudRunnerState.repoPathFull); + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`ls -lh`); + await CloudRunnerSystem.Run(`tree`); + } + await SetupCloudRunnerRepository.cloneRepoWithoutLFSFiles(); + if (Input.cloudRunnerTests) { + await CloudRunnerSystem.Run(`ls -lh`); + await CloudRunnerSystem.Run(`tree`); + } + const lfsHashes = await LFSHashing.createLFSHashFiles(); + if (fs.existsSync(CloudRunnerState.libraryFolderFull)) { + RemoteClientLogger.logWarning(`!Warning!: The Unity library was included in the git repository`); + } + await Caching.PullFromCache( + CloudRunnerState.lfsCacheFolderFull, + CloudRunnerState.lfsDirectoryFull, + `${lfsHashes.lfsGuid}`, + ); + await SetupCloudRunnerRepository.pullLatestLFS(); + await Caching.PushToCache( + CloudRunnerState.lfsCacheFolderFull, + CloudRunnerState.lfsDirectoryFull, + `${lfsHashes.lfsGuid}`, + ); + await Caching.PullFromCache(CloudRunnerState.libraryCacheFolderFull, CloudRunnerState.libraryFolderFull); + Caching.handleCachePurging(); + } catch (error) { + throw error; + } + } + + private static async cloneRepoWithoutLFSFiles() { + try { + process.chdir(`${CloudRunnerState.repoPathFull}`); + 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 lfs install --skip-smudge`); + await CloudRunnerSystem.Run( + `git clone ${CloudRunnerState.targetBuildRepoUrl} ${path.resolve( + `..`, + path.basename(CloudRunnerState.repoPathFull), + )}`, + ); + assert(fs.existsSync(`.git`)); + RemoteClientLogger.log(`${CloudRunnerState.buildParams.branch}`); + await CloudRunnerSystem.Run(`git checkout ${CloudRunnerState.buildParams.branch}`); + assert(fs.existsSync(path.join(`.git`, `lfs`)), 'LFS folder should not exist before caching'); + RemoteClientLogger.log(`Checked out ${process.env.GITHUB_SHA}`); + } catch (error) { + throw error; + } + } + + private static async pullLatestLFS() { + await CloudRunnerSystem.Run(`ls -lh ${CloudRunnerState.lfsDirectoryFull}/..`); + process.chdir(CloudRunnerState.repoPathFull); + await CloudRunnerSystem.Run(`git lfs pull`); + RemoteClientLogger.log(`pulled latest LFS files`); + assert(fs.existsSync(CloudRunnerState.lfsDirectoryFull)); + await CloudRunnerSystem.Run(`ls -lh ${CloudRunnerState.lfsDirectoryFull}/..`); + } +} diff --git a/src/model/cloud-runner/aws/aws-base-stack.ts b/src/model/cloud-runner/aws/aws-base-stack.ts new file mode 100644 index 00000000..55bb4412 --- /dev/null +++ b/src/model/cloud-runner/aws/aws-base-stack.ts @@ -0,0 +1,106 @@ +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import * as core from '@actions/core'; +import * as SDK from 'aws-sdk'; +import * as fs from 'fs'; +import path from 'path'; +const crypto = require('crypto'); + +export class AWSBaseStack { + constructor(baseStackName: string) { + this.baseStackName = baseStackName; + } + private baseStackName: string; + + async setupBaseStack(CF: SDK.CloudFormation) { + const baseStackName = this.baseStackName; + + const baseStack = fs.readFileSync(path.join(__dirname, 'cloud-formations', 'base-setup.yml'), 'utf8'); + + // Cloud Formation Input + const describeStackInput: SDK.CloudFormation.DescribeStacksInput = { + StackName: baseStackName, + }; + const parametersWithoutHash: SDK.CloudFormation.Parameter[] = [ + { ParameterKey: 'EnvironmentName', ParameterValue: baseStackName }, + ]; + const parametersHash = crypto + .createHash('md5') + .update(baseStack + JSON.stringify(parametersWithoutHash)) + .digest('hex'); + const parameters: SDK.CloudFormation.Parameter[] = [ + ...parametersWithoutHash, + ...[{ ParameterKey: 'Version', ParameterValue: parametersHash }], + ]; + const updateInput: SDK.CloudFormation.UpdateStackInput = { + StackName: baseStackName, + TemplateBody: baseStack, + Parameters: parameters, + Capabilities: ['CAPABILITY_IAM'], + }; + const createStackInput: SDK.CloudFormation.CreateStackInput = { + StackName: baseStackName, + TemplateBody: baseStack, + Parameters: parameters, + Capabilities: ['CAPABILITY_IAM'], + }; + + const stacks = await CF.listStacks({ + StackStatusFilter: ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE'], + }).promise(); + const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || []; + const stackExists: Boolean = stackNames.includes(baseStackName) || false; + const describeStack = async () => { + return await CF.describeStacks(describeStackInput).promise(); + }; + try { + if (!stackExists) { + CloudRunnerLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`); + await CF.createStack(createStackInput).promise(); + CloudRunnerLogger.log(`created stack (version: ${parametersHash})`); + } + const CFState = await describeStack(); + let stack = CFState.Stacks?.[0]; + if (!stack) { + throw new Error(`Base stack doesn't exist, even after creation, stackExists check: ${stackExists}`); + } + const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue; + + if (stack.StackStatus === 'CREATE_IN_PROGRESS') { + await CF.waitFor('stackCreateComplete', describeStackInput).promise(); + } + + if (stackExists) { + CloudRunnerLogger.log(`Base stack exists (version: ${stackVersion}, local version: ${parametersHash})`); + if (parametersHash !== stackVersion) { + CloudRunnerLogger.log(`Attempting update of base stack`); + try { + await CF.updateStack(updateInput).promise(); + } catch (error: any) { + if (error['message'].includes('No updates are to be performed')) { + CloudRunnerLogger.log(`No updates are to be performed`); + } else { + CloudRunnerLogger.log(`Update Failed (Stack name: ${baseStackName})`); + CloudRunnerLogger.log(error['message']); + } + CloudRunnerLogger.log(`Continuing...`); + } + } else { + CloudRunnerLogger.log(`No update required`); + } + stack = (await describeStack()).Stacks?.[0]; + if (!stack) { + throw new Error( + `Base stack doesn't exist, even after updating and creation, stackExists check: ${stackExists}`, + ); + } + if (stack.StackStatus === 'UPDATE_IN_PROGRESS') { + await CF.waitFor('stackUpdateComplete', describeStackInput).promise(); + } + } + CloudRunnerLogger.log('base stack is now ready'); + } catch (error) { + core.error(JSON.stringify(await describeStack(), undefined, 4)); + throw error; + } + } +} diff --git a/src/model/cloud-runner/aws/aws-error.ts b/src/model/cloud-runner/aws/aws-error.ts new file mode 100644 index 00000000..d84868be --- /dev/null +++ b/src/model/cloud-runner/aws/aws-error.ts @@ -0,0 +1,16 @@ +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import * as SDK from 'aws-sdk'; +import * as core from '@actions/core'; +import { Input } from '../..'; + +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 (Input.cloudRunnerTests) { + 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)); + } + } +} diff --git a/src/model/cloud-runner/aws/aws-job-stack.ts b/src/model/cloud-runner/aws/aws-job-stack.ts new file mode 100644 index 00000000..0350a9dd --- /dev/null +++ b/src/model/cloud-runner/aws/aws-job-stack.ts @@ -0,0 +1,141 @@ +import * as SDK from 'aws-sdk'; +import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { AWSTemplates } from './aws-templates'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { AWSError } from './aws-error'; + +export class AWSJobStack { + private baseStackName: string; + constructor(baseStackName: string) { + this.baseStackName = baseStackName; + } + + public async setupCloudFormations( + CF: SDK.CloudFormation, + buildGuid: string, + image: string, + entrypoint: string[], + commands: string, + mountdir: string, + workingdir: string, + secrets: CloudRunnerSecret[], + ): Promise { + const taskDefStackName = `${this.baseStackName}-${buildGuid}`; + let taskDefCloudFormation = AWSTemplates.readTaskCloudFormationTemplate(); + for (const secret of secrets) { + secret.ParameterKey = `${buildGuid.replace(/[^\dA-Za-z]/g, '')}${secret.ParameterKey.replace( + /[^\dA-Za-z]/g, + '', + )}`; + if (typeof secret.ParameterValue == 'number') { + secret.ParameterValue = `${secret.ParameterValue}`; + } + if (!secret.ParameterValue || secret.ParameterValue === '') { + secrets = secrets.filter((x) => x !== secret); + continue; + } + taskDefCloudFormation = AWSTemplates.insertAtTemplate( + taskDefCloudFormation, + 'p1 - input', + AWSTemplates.getParameterTemplate(secret.ParameterKey), + ); + taskDefCloudFormation = AWSTemplates.insertAtTemplate( + taskDefCloudFormation, + 'p2 - secret', + AWSTemplates.getSecretTemplate(`${secret.ParameterKey}`), + ); + taskDefCloudFormation = AWSTemplates.insertAtTemplate( + taskDefCloudFormation, + 'p3 - container def', + AWSTemplates.getSecretDefinitionTemplate(secret.EnvironmentVariable, secret.ParameterKey), + ); + } + const secretsMappedToCloudFormationParameters = secrets.map((x) => { + return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue }; + }); + const parameters = [ + { + ParameterKey: 'EnvironmentName', + ParameterValue: this.baseStackName, + }, + { + ParameterKey: 'ImageUrl', + ParameterValue: image, + }, + { + ParameterKey: 'ServiceName', + ParameterValue: taskDefStackName, + }, + { + ParameterKey: 'Command', + ParameterValue: 'echo "this template should be overwritten when running a task"', + }, + { + ParameterKey: 'EntryPoint', + ParameterValue: entrypoint.join(','), + }, + { + ParameterKey: 'WorkingDirectory', + ParameterValue: workingdir, + }, + { + ParameterKey: 'EFSMountDirectory', + ParameterValue: mountdir, + }, + ...secretsMappedToCloudFormationParameters, + ]; + + let previousStackExists = true; + while (previousStackExists) { + previousStackExists = false; + const stacks = await CF.listStacks().promise(); + if (!stacks.StackSummaries) { + throw new Error('Faild to get stacks'); + } + for (let index = 0; index < stacks.StackSummaries.length; index++) { + const element = stacks.StackSummaries[index]; + if (element.StackName === taskDefStackName && element.StackStatus !== 'DELETE_COMPLETE') { + previousStackExists = true; + CloudRunnerLogger.log(`Previous stack still exists: ${JSON.stringify(element)}`); + } + } + } + + try { + await CF.createStack({ + StackName: taskDefStackName, + TemplateBody: taskDefCloudFormation, + Capabilities: ['CAPABILITY_IAM'], + Parameters: parameters, + }).promise(); + CloudRunnerLogger.log('Creating cloud runner job'); + await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise(); + } catch (error) { + await AWSError.handleStackCreationFailure( + error, + CF, + taskDefStackName, + //taskDefCloudFormation, + //parameters, + //secrets, + ); + throw error; + } + + const taskDefResources = ( + await CF.describeStackResources({ + StackName: taskDefStackName, + }).promise() + ).StackResources; + + const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources; + + return { + taskDefStackName, + taskDefCloudFormation, + taskDefResources, + baseResources, + }; + } +} diff --git a/src/model/cloud-runner/aws/aws-task-runner.ts b/src/model/cloud-runner/aws/aws-task-runner.ts new file mode 100644 index 00000000..115106f1 --- /dev/null +++ b/src/model/cloud-runner/aws/aws-task-runner.ts @@ -0,0 +1,228 @@ +import * as AWS from 'aws-sdk'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import * as core from '@actions/core'; +import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; +import * as zlib from 'zlib'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { Input } from '../..'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import { CloudRunnerStatics } from '../cloud-runner-statics'; +import { CloudRunnerBuildCommandProcessor } from '../services/cloud-runner-build-command-process'; + +class AWSTaskRunner { + static async runTask( + taskDef: CloudRunnerAWSTaskDef, + ECS: AWS.ECS, + CF: AWS.CloudFormation, + environment: CloudRunnerEnvironmentVariable[], + buildGuid: string, + commands: string, + ) { + const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || ''; + const taskDefinition = + taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || ''; + const SubnetOne = + taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || ''; + const SubnetTwo = + taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')?.PhysicalResourceId || ''; + const ContainerSecurityGroup = + taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')?.PhysicalResourceId || ''; + const streamName = + taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || ''; + + const task = await ECS.runTask({ + cluster, + taskDefinition, + platformVersion: '1.4.0', + overrides: { + containerOverrides: [ + { + name: taskDef.taskDefStackName, + environment, + command: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(commands, CloudRunnerState.buildParams)], + }, + ], + }, + launchType: 'FARGATE', + networkConfiguration: { + awsvpcConfiguration: { + subnets: [SubnetOne, SubnetTwo], + assignPublicIp: 'ENABLED', + securityGroups: [ContainerSecurityGroup], + }, + }, + }).promise(); + + CloudRunnerLogger.log('Cloud runner job is starting'); + const taskArn = task.tasks?.[0].taskArn || ''; + + try { + await 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 + }`, + ); + + core.setFailed(error); + core.error(error); + } + CloudRunnerLogger.log(`Cloud runner job is running`); + + const output = await this.streamLogsUntilTaskStops(ECS, CF, taskDef, cluster, taskArn, streamName); + const exitCode = (await AWSTaskRunner.describeTasks(ECS, cluster, taskArn)).containers?.[0].exitCode; + CloudRunnerLogger.log(`Cloud runner job exit code ${exitCode}`); + if (exitCode !== 0 && exitCode !== undefined) { + core.error( + `job failed with exit code ${exitCode} ${JSON.stringify( + await ECS.describeTasks({ tasks: [taskArn], cluster }).promise(), + undefined, + 4, + )}`, + ); + throw new Error(`job failed with exit code ${exitCode}`); + } else { + CloudRunnerLogger.log(`Cloud runner job has finished successfully`); + return output; + } + } + + static async describeTasks(ECS: AWS.ECS, clusterName: string, taskArn: string) { + const tasks = await ECS.describeTasks({ + cluster: clusterName, + tasks: [taskArn], + }).promise(); + if (tasks.tasks?.[0]) { + return tasks.tasks?.[0]; + } else { + throw new Error('No task found'); + } + } + + 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); + + CloudRunnerLogger.log( + `Cloud runner job status is ${(await AWSTaskRunner.describeTasks(ECS, clusterName, taskArn))?.lastStatus}`, + ); + + const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${CF.config.region}#logsV2:log-groups/log-group/${taskDef.taskDefStackName}`; + CloudRunnerLogger.log(`You can also see the logs at AWS Cloud Watch: ${logBaseUrl}`); + let shouldReadLogs = true; + let timestamp: number = 0; + let output = ''; + while (shouldReadLogs) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + const taskData = await AWSTaskRunner.describeTasks(ECS, clusterName, taskArn); + ({ timestamp, shouldReadLogs } = AWSTaskRunner.checkStreamingShouldContinue(taskData, timestamp, shouldReadLogs)); + ({ iterator, shouldReadLogs, output } = await AWSTaskRunner.handleLogStreamIteration( + kinesis, + iterator, + shouldReadLogs, + taskDef, + output, + )); + } + return output; + } + + private static async handleLogStreamIteration( + kinesis: AWS.Kinesis, + iterator: string, + shouldReadLogs: boolean, + taskDef: CloudRunnerAWSTaskDef, + output: string, + ) { + const records = await kinesis + .getRecords({ + ShardIterator: iterator, + }) + .promise(); + iterator = records.NextShardIterator || ''; + ({ shouldReadLogs, output } = AWSTaskRunner.logRecords(records, iterator, taskDef, shouldReadLogs, output)); + return { iterator, shouldReadLogs, output }; + } + + private static checkStreamingShouldContinue(taskData: AWS.ECS.Task, timestamp: number, shouldReadLogs: boolean) { + if (taskData?.lastStatus !== 'RUNNING') { + if (timestamp === 0) { + CloudRunnerLogger.log('## Cloud runner job stopped, streaming end of logs'); + timestamp = Date.now(); + } + if (timestamp !== 0 && Date.now() - timestamp > 30000) { + CloudRunnerLogger.log('## Cloud runner status is not RUNNING for 30 seconds, last query for logs'); + shouldReadLogs = false; + } + CloudRunnerLogger.log(`## Status of job: ${taskData.lastStatus}`); + } + return { timestamp, shouldReadLogs }; + } + + private static logRecords( + records, + iterator: string, + taskDef: CloudRunnerAWSTaskDef, + shouldReadLogs: boolean, + output: string, + ) { + if (records.Records.length > 0 && iterator) { + for (let index = 0; index < records.Records.length; index++) { + const json = JSON.parse( + zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'), + ); + if (json.messageType === 'DATA_MESSAGE') { + for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) { + let message = json.logEvents[logEventsIndex].message; + if (json.logEvents[logEventsIndex].message.includes(`---${CloudRunnerState.buildParams.logId}`)) { + CloudRunnerLogger.log('End of log transmission received'); + shouldReadLogs = false; + } else if (message.includes('Rebuilding Library because the asset database could not be found!')) { + core.warning('LIBRARY NOT FOUND!'); + } + message = `[${CloudRunnerStatics.logPrefix}] ${message}`; + if (Input.cloudRunnerTests) { + output += message; + } + CloudRunnerLogger.log(message); + } + } + } + } + return { shouldReadLogs, output }; + } + + private static async getLogStream(kinesis: AWS.Kinesis, kinesisStreamName: string) { + return await kinesis + .describeStream({ + StreamName: kinesisStreamName, + }) + .promise(); + } + + private static async getLogIterator(kinesis: AWS.Kinesis, stream) { + return ( + ( + await kinesis + .getShardIterator({ + ShardIteratorType: 'TRIM_HORIZON', + StreamName: stream.StreamDescription.StreamName, + ShardId: stream.StreamDescription.Shards[0].ShardId, + }) + .promise() + ).ShardIterator || '' + ); + } +} +export default AWSTaskRunner; diff --git a/src/model/cloud-runner/aws/aws-templates.ts b/src/model/cloud-runner/aws/aws-templates.ts new file mode 100644 index 00000000..efd25815 --- /dev/null +++ b/src/model/cloud-runner/aws/aws-templates.ts @@ -0,0 +1,38 @@ +import * as fs from 'fs'; + +export class AWSTemplates { + public static getParameterTemplate(p1) { + return ` + ${p1}: + Type: String + Default: '' +`; + } + + public static getSecretTemplate(p1) { + return ` + ${p1}Secret: + Type: AWS::SecretsManager::Secret + Properties: + Name: '${p1}' + SecretString: !Ref ${p1} +`; + } + + public static getSecretDefinitionTemplate(p1, p2) { + return ` + - Name: '${p1}' + ValueFrom: !Ref ${p2}Secret +`; + } + + public static insertAtTemplate(template, insertionKey, insertion) { + const index = template.search(insertionKey) + insertionKey.length + '\n'.length; + template = [template.slice(0, index), insertion, template.slice(index)].join(''); + return template; + } + + public static readTaskCloudFormationTemplate(): string { + return fs.readFileSync(`${__dirname}/cloud-formations/task-def-formation.yml`, 'utf8'); + } +} diff --git a/src/model/cloud-runner/aws/cloud-formations/base-setup.yml b/src/model/cloud-runner/aws/cloud-formations/base-setup.yml new file mode 100644 index 00000000..3941aeb0 --- /dev/null +++ b/src/model/cloud-runner/aws/cloud-formations/base-setup.yml @@ -0,0 +1,391 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: AWS Fargate cluster that can span public and private subnets. Supports + public facing load balancers, private internal load balancers, and + both internal and external service discovery namespaces. +Parameters: + EnvironmentName: + Type: String + Default: development + Description: 'Your deployment environment: DEV, QA , PROD' + Version: + Type: String + Description: 'hash of template' + + # ContainerPort: + # Type: Number + # Default: 80 + # Description: What port number the application inside the docker container is binding to + +Mappings: + # Hard values for the subnet masks. These masks define + # the range of internal IP addresses that can be assigned. + # The VPC can have all IP's from 10.0.0.0 to 10.0.255.255 + # There are four subnets which cover the ranges: + # + # 10.0.0.0 - 10.0.0.255 + # 10.0.1.0 - 10.0.1.255 + # 10.0.2.0 - 10.0.2.255 + # 10.0.3.0 - 10.0.3.255 + + SubnetConfig: + VPC: + CIDR: '10.0.0.0/16' + PublicOne: + CIDR: '10.0.0.0/24' + PublicTwo: + CIDR: '10.0.1.0/24' + +Resources: + # VPC in which containers will be networked. + # It has two public subnets, and two private subnets. + # We distribute the subnets across the first two available subnets + # for the region, for high availability. + VPC: + Type: AWS::EC2::VPC + Properties: + EnableDnsSupport: true + EnableDnsHostnames: true + CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] + + EFSServerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: 'efs-server-endpoints' + GroupDescription: Which client ip addrs are allowed to access EFS server + VpcId: !Ref 'VPC' + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + SourceSecurityGroupId: !Ref ContainerSecurityGroup + #CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] + # A security group for the containers we will run in Fargate. + # Rules are added to this security group based on what ingress you + # add for the cluster. + ContainerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: 'task security group' + GroupDescription: Access to the Fargate containers + VpcId: !Ref 'VPC' + # SecurityGroupIngress: + # - IpProtocol: tcp + # FromPort: !Ref ContainerPort + # ToPort: !Ref ContainerPort + # CidrIp: 0.0.0.0/0 + SecurityGroupEgress: + - IpProtocol: -1 + FromPort: 2049 + ToPort: 2049 + CidrIp: '0.0.0.0/0' + + # Two public subnets, where containers can have public IP addresses + PublicSubnetOne: + Type: AWS::EC2::Subnet + Properties: + AvailabilityZone: !Select + - 0 + - Fn::GetAZs: !Ref 'AWS::Region' + VpcId: !Ref 'VPC' + CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] + # MapPublicIpOnLaunch: true + + PublicSubnetTwo: + Type: AWS::EC2::Subnet + Properties: + AvailabilityZone: !Select + - 1 + - Fn::GetAZs: !Ref 'AWS::Region' + VpcId: !Ref 'VPC' + CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] + # MapPublicIpOnLaunch: true + + # Setup networking resources for the public subnets. Containers + # in the public subnets have public IP addresses and the routing table + # sends network traffic via the internet gateway. + InternetGateway: + Type: AWS::EC2::InternetGateway + GatewayAttachement: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref 'VPC' + InternetGatewayId: !Ref 'InternetGateway' + + # Attaching a Internet Gateway to route table makes it public. + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref 'VPC' + PublicRoute: + Type: AWS::EC2::Route + DependsOn: GatewayAttachement + Properties: + RouteTableId: !Ref 'PublicRouteTable' + DestinationCidrBlock: '0.0.0.0/0' + GatewayId: !Ref 'InternetGateway' + + # Attaching a public route table makes a subnet public. + PublicSubnetOneRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnetOne + RouteTableId: !Ref PublicRouteTable + PublicSubnetTwoRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnetTwo + RouteTableId: !Ref PublicRouteTable + + # ECS Resources + ECSCluster: + Type: AWS::ECS::Cluster + + # A role used to allow AWS Autoscaling to inspect stats and adjust scaleable targets + # on your AWS account + AutoscalingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [application-autoscaling.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: service-autoscaling + PolicyDocument: + Statement: + - Effect: Allow + Action: + - 'application-autoscaling:*' + - 'cloudwatch:DescribeAlarms' + - 'cloudwatch:PutMetricAlarm' + - 'ecs:DescribeServices' + - 'ecs:UpdateService' + Resource: '*' + + # This is an IAM role which authorizes ECS to manage resources on your + # account on your behalf, such as updating your load balancer with the + # details of where your containers are, so that traffic can reach your + # containers. + ECSRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [ecs.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: ecs-service + PolicyDocument: + Statement: + - Effect: Allow + Action: + # Rules which allow ECS to attach network interfaces to instances + # on your behalf in order for awsvpc networking mode to work right + - 'ec2:AttachNetworkInterface' + - 'ec2:CreateNetworkInterface' + - 'ec2:CreateNetworkInterfacePermission' + - 'ec2:DeleteNetworkInterface' + - 'ec2:DeleteNetworkInterfacePermission' + - 'ec2:Describe*' + - 'ec2:DetachNetworkInterface' + + # Rules which allow ECS to update load balancers on your behalf + # with the information sabout how to send traffic to your containers + - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' + - 'elasticloadbalancing:DeregisterTargets' + - 'elasticloadbalancing:Describe*' + - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' + - 'elasticloadbalancing:RegisterTargets' + Resource: '*' + + # This is a role which is used by the ECS tasks themselves. + ECSTaskExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [ecs-tasks.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: AmazonECSTaskExecutionRolePolicy + PolicyDocument: + Statement: + - Effect: Allow + Action: + # Allow the use of secret manager + - 'secretsmanager:GetSecretValue' + - 'kms:Decrypt' + + # Allow the ECS Tasks to download images from ECR + - 'ecr:GetAuthorizationToken' + - 'ecr:BatchCheckLayerAvailability' + - 'ecr:GetDownloadUrlForLayer' + - 'ecr:BatchGetImage' + + # Allow the ECS tasks to upload logs to CloudWatch + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + Resource: '*' + + DeleteCFNLambdaExecutionRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: 'Allow' + Principal: + Service: ['lambda.amazonaws.com'] + Action: 'sts:AssumeRole' + Path: '/' + Policies: + - PolicyName: DeleteCFNLambdaExecutionRole + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: 'Allow' + Action: + - 'logs:CreateLogGroup' + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + Resource: 'arn:aws:logs:*:*:*' + - Effect: 'Allow' + Action: + - 'cloudformation:DeleteStack' + - 'kinesis:DeleteStream' + - 'secretsmanager:DeleteSecret' + - 'kinesis:DescribeStreamSummary' + - 'logs:DeleteLogGroup' + - 'logs:DeleteSubscriptionFilter' + - 'ecs:DeregisterTaskDefinition' + - 'lambda:DeleteFunction' + - 'lambda:InvokeFunction' + - 'events:RemoveTargets' + - 'events:DeleteRule' + - 'lambda:RemovePermission' + Resource: '*' + + ### cloud watch to kinesis role + CloudWatchIAMRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [logs.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: service-autoscaling + PolicyDocument: + Statement: + - Effect: Allow + Action: + - 'kinesis:PutRecord' + Resource: '*' + + #####################EFS##################### + EfsFileStorage: + Type: 'AWS::EFS::FileSystem' + Properties: + BackupPolicy: + Status: ENABLED + PerformanceMode: maxIO + Encrypted: false + + FileSystemPolicy: + Version: '2012-10-17' + Statement: + - Effect: 'Allow' + Action: + - 'elasticfilesystem:ClientMount' + - 'elasticfilesystem:ClientWrite' + - 'elasticfilesystem:ClientRootAccess' + Principal: + AWS: '*' + + MountTargetResource1: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref EfsFileStorage + SubnetId: !Ref PublicSubnetOne + SecurityGroups: + - !Ref EFSServerSecurityGroup + + MountTargetResource2: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref EfsFileStorage + SubnetId: !Ref PublicSubnetTwo + SecurityGroups: + - !Ref EFSServerSecurityGroup + +Outputs: + EfsFileStorageId: + Description: 'The connection endpoint for the database.' + Value: !Ref EfsFileStorage + Export: + Name: !Sub ${EnvironmentName}:EfsFileStorageId + ClusterName: + Description: The name of the ECS cluster + Value: !Ref 'ECSCluster' + Export: + Name: !Sub ${EnvironmentName}:ClusterName + AutoscalingRole: + Description: The ARN of the role used for autoscaling + Value: !GetAtt 'AutoscalingRole.Arn' + Export: + Name: !Sub ${EnvironmentName}:AutoscalingRole + ECSRole: + Description: The ARN of the ECS role + Value: !GetAtt 'ECSRole.Arn' + Export: + Name: !Sub ${EnvironmentName}:ECSRole + ECSTaskExecutionRole: + Description: The ARN of the ECS role tsk execution role + Value: !GetAtt 'ECSTaskExecutionRole.Arn' + Export: + Name: !Sub ${EnvironmentName}:ECSTaskExecutionRole + + DeleteCFNLambdaExecutionRole: + Description: Lambda execution role for cleaning up cloud formations + Value: !GetAtt 'DeleteCFNLambdaExecutionRole.Arn' + Export: + Name: !Sub ${EnvironmentName}:DeleteCFNLambdaExecutionRole + + CloudWatchIAMRole: + Description: The ARN of the CloudWatch role for subscription filter + Value: !GetAtt 'CloudWatchIAMRole.Arn' + Export: + Name: !Sub ${EnvironmentName}:CloudWatchIAMRole + VpcId: + Description: The ID of the VPC that this stack is deployed in + Value: !Ref 'VPC' + Export: + Name: !Sub ${EnvironmentName}:VpcId + PublicSubnetOne: + Description: Public subnet one + Value: !Ref 'PublicSubnetOne' + Export: + Name: !Sub ${EnvironmentName}:PublicSubnetOne + PublicSubnetTwo: + Description: Public subnet two + Value: !Ref 'PublicSubnetTwo' + Export: + Name: !Sub ${EnvironmentName}:PublicSubnetTwo + + ContainerSecurityGroup: + Description: A security group used to allow Fargate containers to receive traffic + Value: !Ref 'ContainerSecurityGroup' + Export: + Name: !Sub ${EnvironmentName}:ContainerSecurityGroup diff --git a/src/model/cloud-runner/aws/cloud-formations/task-def-formation.yml b/src/model/cloud-runner/aws/cloud-formations/task-def-formation.yml new file mode 100644 index 00000000..fae6cf4e --- /dev/null +++ b/src/model/cloud-runner/aws/cloud-formations/task-def-formation.yml @@ -0,0 +1,221 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + AWS Fargate cluster that can span public and private subnets. Supports public + facing load balancers, private internal load balancers, and both internal and + external service discovery namespaces. +Parameters: + EnvironmentName: + Type: String + Default: development + Description: 'Your deployment environment: DEV, QA , PROD' + ServiceName: + Type: String + Default: example + Description: A name for the service + ImageUrl: + Type: String + Default: nginx + Description: >- + The url of a docker image that contains the application process that will + handle the traffic for this service + ContainerPort: + Type: Number + Default: 80 + Description: What port number the application inside the docker container is binding to + ContainerCpu: + Type: Number + Default: 1024 + Description: How much CPU to give the container. 1024 is 1 CPU + ContainerMemory: + Type: Number + Default: 2048 + Description: How much memory in megabytes to give the container + BUILDGUID: + Type: String + Default: '' + Command: + Type: String + Default: 'ls' + EntryPoint: + Type: String + Default: '/bin/sh' + WorkingDirectory: + Type: String + Default: '/efsdata/' + Role: + Type: String + Default: '' + Description: >- + (Optional) An IAM role to give the service's containers if the code within + needs to access other AWS resources like S3 buckets, DynamoDB tables, etc + EFSMountDirectory: + Type: String + Default: '/efsdata' + # template secrets p1 - input +Mappings: + SubnetConfig: + VPC: + CIDR: 10.0.0.0/16 + PublicOne: + CIDR: 10.0.0.0/24 + PublicTwo: + CIDR: 10.0.1.0/24 +Conditions: + HasCustomRole: !Not + - !Equals + - Ref: Role + - '' +Resources: + LogGroup: + Type: 'AWS::Logs::LogGroup' + Properties: + LogGroupName: !Ref ServiceName + Metadata: + 'AWS::CloudFormation::Designer': + id: aece53ae-b82d-4267-bc16-ed964b05db27 + SubscriptionFilter: + Type: 'AWS::Logs::SubscriptionFilter' + Properties: + FilterPattern: '' + RoleArn: + 'Fn::ImportValue': !Sub '${EnvironmentName}:CloudWatchIAMRole' + LogGroupName: !Ref ServiceName + DestinationArn: + 'Fn::GetAtt': + - KinesisStream + - Arn + Metadata: + 'AWS::CloudFormation::Designer': + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + DependsOn: + - LogGroup + - KinesisStream + KinesisStream: + Type: 'AWS::Kinesis::Stream' + Properties: + Name: !Ref ServiceName + ShardCount: 1 + Metadata: + 'AWS::CloudFormation::Designer': + id: c6f18447-b879-4696-8873-f981b2cedd2b + + # template secrets p2 - secret + + TaskDefinition: + Type: 'AWS::ECS::TaskDefinition' + Properties: + Family: !Ref ServiceName + Cpu: !Ref ContainerCpu + Memory: !Ref ContainerMemory + NetworkMode: awsvpc + Volumes: + - Name: efs-data + EFSVolumeConfiguration: + FilesystemId: + 'Fn::ImportValue': !Sub '${EnvironmentName}:EfsFileStorageId' + TransitEncryption: ENABLED + RequiresCompatibilities: + - FARGATE + ExecutionRoleArn: + 'Fn::ImportValue': !Sub '${EnvironmentName}:ECSTaskExecutionRole' + TaskRoleArn: + 'Fn::If': + - HasCustomRole + - !Ref Role + - !Ref 'AWS::NoValue' + ContainerDefinitions: + - Name: !Ref ServiceName + Cpu: !Ref ContainerCpu + Memory: !Ref ContainerMemory + Image: !Ref ImageUrl + EntryPoint: + Fn::Split: + - ',' + - !Ref EntryPoint + Command: + Fn::Split: + - ',' + - !Ref Command + WorkingDirectory: !Ref WorkingDirectory + Environment: + - Name: ALLOW_EMPTY_PASSWORD + Value: 'yes' + # template - env vars + MountPoints: + - SourceVolume: efs-data + ContainerPath: !Ref EFSMountDirectory + ReadOnly: false + Secrets: + # template secrets p3 - container def + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref ServiceName + awslogs-region: !Ref 'AWS::Region' + awslogs-stream-prefix: !Ref ServiceName + Metadata: + 'AWS::CloudFormation::Designer': + id: dabb0116-abe0-48a6-a8af-cf9111c879a5 + DependsOn: + - LogGroup +Metadata: + 'AWS::CloudFormation::Designer': + dabb0116-abe0-48a6-a8af-cf9111c879a5: + size: + width: 60 + height: 60 + position: + x: 270 + 'y': 90 + z: 1 + embeds: [] + dependson: + - aece53ae-b82d-4267-bc16-ed964b05db27 + c6f18447-b879-4696-8873-f981b2cedd2b: + size: + width: 60 + height: 60 + position: + x: 270 + 'y': 210 + z: 1 + embeds: [] + 7f809e91-9e5d-4678-98c1-c5085956c480: + size: + width: 60 + height: 60 + position: + x: 60 + 'y': 300 + z: 1 + embeds: [] + dependson: + - aece53ae-b82d-4267-bc16-ed964b05db27 + - c6f18447-b879-4696-8873-f981b2cedd2b + aece53ae-b82d-4267-bc16-ed964b05db27: + size: + width: 150 + height: 150 + position: + x: 60 + 'y': 90 + z: 1 + embeds: [] + 4d2da56c-3643-46b8-aaee-e46e19f95fcc: + source: + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + target: + id: aece53ae-b82d-4267-bc16-ed964b05db27 + z: 11 + 14eb957b-f094-4653-93c4-77b2f851953c: + source: + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + target: + id: c6f18447-b879-4696-8873-f981b2cedd2b + z: 12 + 85c57444-e5bb-4230-bc85-e545cd4558f6: + source: + id: dabb0116-abe0-48a6-a8af-cf9111c879a5 + target: + id: aece53ae-b82d-4267-bc16-ed964b05db27 + z: 13 diff --git a/src/model/remote-builder/remote-builder-task-def.ts b/src/model/cloud-runner/aws/cloud-runner-aws-task-def.ts similarity index 60% rename from src/model/remote-builder/remote-builder-task-def.ts rename to src/model/cloud-runner/aws/cloud-runner-aws-task-def.ts index e5566128..4ec16196 100644 --- a/src/model/remote-builder/remote-builder-task-def.ts +++ b/src/model/cloud-runner/aws/cloud-runner-aws-task-def.ts @@ -1,12 +1,9 @@ import * as AWS from 'aws-sdk'; -class RemoteBuilderTaskDef { +class CloudRunnerAWSTaskDef { public taskDefStackName!: string; public taskDefCloudFormation!: string; - public taskDefStackNameTTL!: string; - public ttlCloudFormation!: string; public taskDefResources: AWS.CloudFormation.StackResources | undefined; public baseResources: AWS.CloudFormation.StackResources | undefined; - public logid!: string; } -export default RemoteBuilderTaskDef; +export default CloudRunnerAWSTaskDef; diff --git a/src/model/cloud-runner/aws/index.ts b/src/model/cloud-runner/aws/index.ts new file mode 100644 index 00000000..597a0026 --- /dev/null +++ b/src/model/cloud-runner/aws/index.ts @@ -0,0 +1,98 @@ +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 { CloudRunnerProviderInterface } from '../services/cloud-runner-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 { Input } from '../..'; + +class AWSBuildEnvironment implements CloudRunnerProviderInterface { + private baseStackName: string; + + constructor(buildParameters: BuildParameters) { + this.baseStackName = buildParameters.awsBaseStackName; + } + async cleanupSharedResources( + // 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 }[], + ) {} + async setupSharedResources( + // 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 }[], + ) {} + + async runTask( + buildGuid: string, + image: string, + commands: string, + mountdir: string, + workingdir: string, + environment: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ): Promise { + process.env.AWS_REGION = Input.region; + const ECS = new SDK.ECS(); + const CF = new SDK.CloudFormation(); + CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`); + const entrypoint = ['/bin/sh']; + const startTimeMs = Date.now(); + + await new AWSBaseStack(this.baseStackName).setupBaseStack(CF); + const taskDef = await new AWSJobStack(this.baseStackName).setupCloudFormations( + CF, + buildGuid, + image, + entrypoint, + commands, + mountdir, + workingdir, + secrets, + ); + + let postRunTaskTimeMs; + let output = ''; + try { + const postSetupStacksTimeMs = Date.now(); + CloudRunnerLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`); + output = await AWSTaskRunner.runTask(taskDef, ECS, CF, environment, buildGuid, commands); + postRunTaskTimeMs = Date.now(); + CloudRunnerLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`); + } finally { + await this.cleanupResources(CF, taskDef); + const postCleanupTimeMs = Date.now(); + if (postRunTaskTimeMs !== undefined) + CloudRunnerLogger.log(`Cleanup job time: ${Math.floor((postCleanupTimeMs - postRunTaskTimeMs) / 1000)}s`); + } + return output; + } + + async cleanupResources(CF: SDK.CloudFormation, taskDef: CloudRunnerAWSTaskDef) { + CloudRunnerLogger.log('Cleanup starting'); + await CF.deleteStack({ + StackName: taskDef.taskDefStackName, + }).promise(); + + await CF.waitFor('stackDeleteComplete', { + StackName: taskDef.taskDefStackName, + }).promise(); + CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`); + CloudRunnerLogger.log('Cleanup complete'); + } +} +export default AWSBuildEnvironment; diff --git a/src/model/cloud-runner/cloud-runner-statics.ts b/src/model/cloud-runner/cloud-runner-statics.ts new file mode 100644 index 00000000..964a8980 --- /dev/null +++ b/src/model/cloud-runner/cloud-runner-statics.ts @@ -0,0 +1,3 @@ +export class CloudRunnerStatics { + public static readonly logPrefix = `Cloud-Runner-System`; +} diff --git a/src/model/cloud-runner/cloud-runner.test.ts b/src/model/cloud-runner/cloud-runner.test.ts new file mode 100644 index 00000000..2be7e292 --- /dev/null +++ b/src/model/cloud-runner/cloud-runner.test.ts @@ -0,0 +1,50 @@ +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'; + +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 () => { + Input.cliOptions = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + customJob: ` + - name: 'step 1' + image: 'alpine' + commands: 'printenv' + secrets: + - name: '${testSecretName}' + value: '${testSecretValue}' + `, + }; + Input.githubInputEnabled = false; + const buildParameter = await BuildParameters.create(); + const baseImage = new ImageTag(buildParameter); + const file = await CloudRunner.run(buildParameter, baseImage.toString()); + 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, ''); + } + expect(newLinePurgedFile).toContain(`${element.name}=${element.value}`); + } + } + Input.githubInputEnabled = true; + }, 1000000); + } +}); diff --git a/src/model/cloud-runner/cloud-runner.ts b/src/model/cloud-runner/cloud-runner.ts new file mode 100644 index 00000000..f6d1285c --- /dev/null +++ b/src/model/cloud-runner/cloud-runner.ts @@ -0,0 +1,72 @@ +import AWSBuildPlatform from './aws'; +import { BuildParameters } from '..'; +import { CloudRunnerState } from './state/cloud-runner-state'; +import Kubernetes from './k8s'; +import CloudRunnerLogger from './services/cloud-runner-logger'; +import { CloudRunnerStepState } from './state/cloud-runner-step-state'; +import { WorkflowCompositionRoot } from './workflows/workflow-composition-root'; +import { CloudRunnerError } from './error/cloud-runner-error'; +import { TaskParameterSerializer } from './services/task-parameter-serializer'; +import * as core from '@actions/core'; + +class CloudRunner { + private static setup(buildParameters: BuildParameters) { + CloudRunnerLogger.setup(); + CloudRunnerState.setup(buildParameters); + CloudRunner.setupBuildPlatform(); + const parameters = TaskParameterSerializer.readBuildEnvironmentVariables(); + for (const element of parameters) { + core.setOutput(element.name, element.value); + } + } + + private static setupBuildPlatform() { + switch (CloudRunnerState.buildParams.cloudRunnerCluster) { + case 'k8s': + CloudRunnerLogger.log('Cloud Runner platform selected Kubernetes'); + CloudRunnerState.CloudRunnerProviderPlatform = new Kubernetes(CloudRunnerState.buildParams); + break; + default: + case 'aws': + CloudRunnerLogger.log('Cloud Runner platform selected AWS'); + CloudRunnerState.CloudRunnerProviderPlatform = new AWSBuildPlatform(CloudRunnerState.buildParams); + break; + } + } + + static async run(buildParameters: BuildParameters, baseImage: string) { + CloudRunner.setup(buildParameters); + try { + core.startGroup('Setup remote runner'); + await CloudRunnerState.CloudRunnerProviderPlatform.setupSharedResources( + CloudRunnerState.buildParams.buildGuid, + CloudRunnerState.buildParams, + CloudRunnerState.branchName, + CloudRunnerState.defaultSecrets, + ); + core.endGroup(); + const output = await new WorkflowCompositionRoot().run( + new CloudRunnerStepState( + baseImage, + TaskParameterSerializer.readBuildEnvironmentVariables(), + CloudRunnerState.defaultSecrets, + ), + ); + core.startGroup('Cleanup'); + await CloudRunnerState.CloudRunnerProviderPlatform.cleanupSharedResources( + CloudRunnerState.buildParams.buildGuid, + CloudRunnerState.buildParams, + CloudRunnerState.branchName, + CloudRunnerState.defaultSecrets, + ); + CloudRunnerLogger.log(`Cleanup complete`); + core.endGroup(); + return output; + } catch (error) { + core.endGroup(); + await CloudRunnerError.handleException(error); + throw error; + } + } +} +export default CloudRunner; diff --git a/src/model/cloud-runner/error/cloud-runner-error.ts b/src/model/cloud-runner/error/cloud-runner-error.ts new file mode 100644 index 00000000..9486b9c1 --- /dev/null +++ b/src/model/cloud-runner/error/cloud-runner-error.ts @@ -0,0 +1,16 @@ +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import * as core from '@actions/core'; + +export class CloudRunnerError { + public static async handleException(error: unknown) { + CloudRunnerLogger.error(JSON.stringify(error, undefined, 4)); + core.setFailed('Cloud Runner failed'); + await CloudRunnerState.CloudRunnerProviderPlatform.cleanupSharedResources( + CloudRunnerState.buildParams.buildGuid, + CloudRunnerState.buildParams, + CloudRunnerState.branchName, + CloudRunnerState.defaultSecrets, + ); + } +} diff --git a/src/model/cloud-runner/k8s/index.ts b/src/model/cloud-runner/k8s/index.ts new file mode 100644 index 00000000..5fe6931b --- /dev/null +++ b/src/model/cloud-runner/k8s/index.ts @@ -0,0 +1,197 @@ +import * as k8s from '@kubernetes/client-node'; +import { BuildParameters, Output } from '../..'; +import * as core from '@actions/core'; +import { CloudRunnerProviderInterface } from '../services/cloud-runner-provider-interface'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +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'; + +class Kubernetes implements CloudRunnerProviderInterface { + 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 = ''; + + constructor(buildParameters: BuildParameters) { + 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; + } + public async setupSharedResources( + 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 }[], + ) { + try { + this.pvcName = `unity-builder-pvc-${buildGuid}`; + this.cleanupCronJobName = `unity-builder-cronjob-${buildGuid}`; + this.serviceAccountName = `service-account-${buildGuid}`; + await KubernetesStorage.createPersistentVolumeClaim( + buildParameters, + this.pvcName, + this.kubeClient, + this.namespace, + ); + + await KubernetesServiceAccount.createServiceAccount(this.serviceAccountName, this.namespace, this.kubeClient); + } catch (error) { + throw error; + } + } + + async runTask( + buildGuid: string, + image: string, + commands: string, + mountdir: string, + workingdir: string, + environment: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ): Promise { + try { + // setup + this.buildGuid = buildGuid; + this.secretName = `build-credentials-${buildGuid}`; + this.jobName = `unity-builder-job-${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'); + this.setPodNameAndContainerName(await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace)); + CloudRunnerLogger.log('Watching pod until running'); + 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, + this.kubeClient, + this.jobName, + this.podName, + 'main', + this.namespace, + CloudRunnerLogger.log, + ); + break; + } catch (error: any) { + if (error.message.includes(`HTTP`)) { + continue; + } else { + throw error; + } + } + } + await this.cleanupTaskResources(); + return output; + } catch (error) { + CloudRunnerLogger.log('Running job failed'); + core.error(JSON.stringify(error, undefined, 4)); + await this.cleanupTaskResources(); + throw error; + } + } + + setPodNameAndContainerName(pod: k8s.V1Pod) { + this.podName = pod.metadata?.name || ''; + this.containerName = pod.status?.containerStatuses?.[0].name || ''; + } + + async cleanupTaskResources() { + CloudRunnerLogger.log('cleaning up'); + 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:'); + 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 {} + } + + async cleanupSharedResources( + 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 }[], + ) { + CloudRunnerLogger.log(`deleting PVC`); + await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace); + await Output.setBuildVersion(buildParameters.buildVersion); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(); + } + + static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) { + const namespacedPods = await kubeClient.listNamespacedPod(namespace); + const pod = namespacedPods.body.items.find((x) => x.metadata?.labels?.['job-name'] === jobName); + if (pod === undefined) { + throw new Error("pod with job-name label doesn't exist"); + } + return pod; + } +} +export default Kubernetes; diff --git a/src/model/cloud-runner/k8s/kubernetes-job-spec-factory.ts b/src/model/cloud-runner/k8s/kubernetes-job-spec-factory.ts new file mode 100644 index 00000000..9b4765af --- /dev/null +++ b/src/model/cloud-runner/k8s/kubernetes-job-spec-factory.ts @@ -0,0 +1,161 @@ +import { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } from '@kubernetes/client-node'; +import BuildParameters from '../../build-parameters'; +import { CloudRunnerBuildCommandProcessor } from '../services/cloud-runner-build-command-process'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { CloudRunnerState } from '../state/cloud-runner-state'; + +class KubernetesJobSpecFactory { + static getJobSpec( + command: string, + image: string, + mountdir: string, + workingDirectory: string, + environment: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + buildGuid: string, + buildParameters: BuildParameters, + secretName, + pvcName, + jobName, + k8s, + ) { + environment.push( + ...[ + { + name: 'GITHUB_SHA', + value: buildGuid, + }, + { + name: 'GITHUB_WORKSPACE', + value: '/data/repo', + }, + { + name: 'PROJECT_PATH', + value: buildParameters.projectPath, + }, + { + name: 'BUILD_PATH', + value: buildParameters.buildPath, + }, + { + name: 'BUILD_FILE', + value: buildParameters.buildFile, + }, + { + name: 'BUILD_NAME', + value: buildParameters.buildName, + }, + { + name: 'BUILD_METHOD', + value: buildParameters.buildMethod, + }, + { + name: 'CUSTOM_PARAMETERS', + value: buildParameters.customParameters, + }, + { + name: 'CHOWN_FILES_TO', + value: buildParameters.chownFilesTo, + }, + { + name: 'BUILD_TARGET', + value: buildParameters.platform, + }, + { + name: 'ANDROID_VERSION_CODE', + value: buildParameters.androidVersionCode.toString(), + }, + { + name: 'ANDROID_KEYSTORE_NAME', + value: buildParameters.androidKeystoreName, + }, + { + name: 'ANDROID_KEYALIAS_NAME', + value: buildParameters.androidKeyaliasName, + }, + ], + ); + const job = new k8s.V1Job(); + job.apiVersion = 'batch/v1'; + job.kind = 'Job'; + job.metadata = { + name: jobName, + labels: { + app: 'unity-builder', + buildGuid, + }, + }; + job.spec = { + backoffLimit: 0, + template: { + spec: { + volumes: [ + { + name: 'build-mount', + persistentVolumeClaim: { + claimName: pvcName, + }, + }, + ], + containers: [ + { + name: 'main', + image, + command: ['/bin/sh'], + args: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(command, CloudRunnerState.buildParams)], + + workingDir: `${workingDirectory}`, + resources: { + requests: { + memory: buildParameters.cloudRunnerMemory, + cpu: buildParameters.cloudRunnerCpu, + }, + }, + env: [ + ...environment.map((x) => { + const environmentVariable = new V1EnvVar(); + environmentVariable.name = x.name; + environmentVariable.value = x.value; + return environmentVariable; + }), + ...secrets.map((x) => { + const secret = new V1EnvVarSource(); + secret.secretKeyRef = new V1SecretKeySelector(); + secret.secretKeyRef.key = x.ParameterKey; + secret.secretKeyRef.name = secretName; + const environmentVariable = new V1EnvVar(); + environmentVariable.name = x.EnvironmentVariable; + environmentVariable.valueFrom = secret; + return environmentVariable; + }), + ], + volumeMounts: [ + { + name: 'build-mount', + mountPath: `/${mountdir}`, + }, + ], + lifecycle: { + preStop: { + exec: { + command: [ + 'bin/bash', + '-c', + `cd /data/builder/action/steps; + chmod +x /return_license.sh; + /return_license.sh;`, + ], + }, + }, + }, + }, + ], + restartPolicy: 'Never', + }, + }, + }; + return job; + } +} +export default KubernetesJobSpecFactory; diff --git a/src/model/cloud-runner/k8s/kubernetes-secret.ts b/src/model/cloud-runner/k8s/kubernetes-secret.ts new file mode 100644 index 00000000..fcac7345 --- /dev/null +++ b/src/model/cloud-runner/k8s/kubernetes-secret.ts @@ -0,0 +1,28 @@ +import { CoreV1Api } from '@kubernetes/client-node'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import * as k8s from '@kubernetes/client-node'; +const base64 = require('base-64'); + +class KubernetesSecret { + static async createSecret( + secrets: CloudRunnerSecret[], + secretName: string, + namespace: string, + kubeClient: CoreV1Api, + ) { + const secret = new k8s.V1Secret(); + secret.apiVersion = 'v1'; + secret.kind = 'Secret'; + secret.type = 'Opaque'; + secret.metadata = { + name: secretName, + }; + secret.data = {}; + for (const buildSecret of secrets) { + secret.data[buildSecret.ParameterKey] = base64.encode(buildSecret.ParameterValue); + } + return kubeClient.createNamespacedSecret(namespace, secret); + } +} + +export default KubernetesSecret; diff --git a/src/model/cloud-runner/k8s/kubernetes-service-account.ts b/src/model/cloud-runner/k8s/kubernetes-service-account.ts new file mode 100644 index 00000000..3fad6e05 --- /dev/null +++ b/src/model/cloud-runner/k8s/kubernetes-service-account.ts @@ -0,0 +1,17 @@ +import { CoreV1Api } from '@kubernetes/client-node'; +import * as k8s from '@kubernetes/client-node'; + +class KubernetesServiceAccount { + static async createServiceAccount(serviceAccountName: string, namespace: string, kubeClient: CoreV1Api) { + const serviceAccount = new k8s.V1ServiceAccount(); + serviceAccount.apiVersion = 'v1'; + serviceAccount.kind = 'ServiceAccount'; + serviceAccount.metadata = { + name: serviceAccountName, + }; + serviceAccount.automountServiceAccountToken = false; + return kubeClient.createNamespacedServiceAccount(namespace, serviceAccount); + } +} + +export default KubernetesServiceAccount; diff --git a/src/model/cloud-runner/k8s/kubernetes-storage.ts b/src/model/cloud-runner/k8s/kubernetes-storage.ts new file mode 100644 index 00000000..3ed889dc --- /dev/null +++ b/src/model/cloud-runner/k8s/kubernetes-storage.ts @@ -0,0 +1,114 @@ +import waitUntil from 'async-wait-until'; +import * as core from '@actions/core'; +import * as k8s from '@kubernetes/client-node'; +import BuildParameters from '../../build-parameters'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import YAML from 'yaml'; + +class KubernetesStorage { + public static async createPersistentVolumeClaim( + buildParameters: BuildParameters, + pvcName: string, + kubeClient: k8s.CoreV1Api, + namespace: string, + ) { + if (buildParameters.kubeVolume) { + CloudRunnerLogger.log(buildParameters.kubeVolume); + pvcName = buildParameters.kubeVolume; + return; + } + const pvcList = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items.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`); + core.setOutput('volume', pvcName); + return; + } + CloudRunnerLogger.log(`Creating PVC ${pvcName} (does not exist)`); + const result = await KubernetesStorage.createPVC(pvcName, buildParameters, kubeClient, namespace); + await KubernetesStorage.handleResult(result, kubeClient, namespace, pvcName); + } + + public static async getPVCPhase(kubeClient: k8s.CoreV1Api, name: string, namespace: string) { + try { + return (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body.status?.phase; + } catch (error) { + core.error('Failed to get PVC phase'); + core.error(JSON.stringify(error, undefined, 4)); + throw error; + } + } + + public static async watchUntilPVCNotPending(kubeClient: k8s.CoreV1Api, name: string, namespace: string) { + try { + CloudRunnerLogger.log(`watch Until PVC Not Pending ${name} ${namespace}`); + CloudRunnerLogger.log(`${await this.getPVCPhase(kubeClient, name, namespace)}`); + await waitUntil( + async () => { + return (await this.getPVCPhase(kubeClient, name, namespace)) !== 'Pending'; + }, + { + timeout: 500000, + intervalBetweenAttempts: 15000, + }, + ); + } catch (error: any) { + core.error('Failed to watch PVC'); + core.error(error.toString()); + core.error( + `PVC Body: ${JSON.stringify( + (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body, + undefined, + 4, + )}`, + ); + throw error; + } + } + + private static async createPVC( + pvcName: string, + buildParameters: BuildParameters, + kubeClient: k8s.CoreV1Api, + namespace: string, + ) { + const pvc = new k8s.V1PersistentVolumeClaim(); + pvc.apiVersion = 'v1'; + pvc.kind = 'PersistentVolumeClaim'; + pvc.metadata = { + name: pvcName, + }; + pvc.spec = { + accessModes: ['ReadWriteOnce'], + storageClassName: process.env.K8s_STORAGE_CLASS || 'standard', + resources: { + requests: { + storage: buildParameters.kubeVolumeSize, + }, + }, + }; + if (process.env.K8s_STORAGE_PVC_SPEC) { + YAML.parse(process.env.K8s_STORAGE_PVC_SPEC); + } + const result = await kubeClient.createNamespacedPersistentVolumeClaim(namespace, pvc); + return result; + } + + private static async handleResult( + result: { response: import('http').IncomingMessage; body: k8s.V1PersistentVolumeClaim }, + kubeClient: k8s.CoreV1Api, + namespace: string, + pvcName: string, + ) { + const name = result.body.metadata?.name || ''; + CloudRunnerLogger.log(`PVC ${name} created`); + await this.watchUntilPVCNotPending(kubeClient, name, namespace); + CloudRunnerLogger.log(`PVC ${name} is ready and not pending`); + core.setOutput('volume', pvcName); + } +} + +export default KubernetesStorage; diff --git a/src/model/cloud-runner/k8s/kubernetes-task-runner.ts b/src/model/cloud-runner/k8s/kubernetes-task-runner.ts new file mode 100644 index 00000000..7dece875 --- /dev/null +++ b/src/model/cloud-runner/k8s/kubernetes-task-runner.ts @@ -0,0 +1,104 @@ +import { CoreV1Api, KubeConfig, Log } from '@kubernetes/client-node'; +import { Writable } from 'stream'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import * as core from '@actions/core'; +import { CloudRunnerStatics } from '../cloud-runner-statics'; +import waitUntil from 'async-wait-until'; +import { Input } from '../..'; + +class KubernetesTaskRunner { + static async runTask( + kubeConfig: KubeConfig, + kubeClient: CoreV1Api, + jobName: string, + podName: string, + containerName: string, + namespace: string, + logCallback: any, + ) { + CloudRunnerLogger.log(`Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace}`); + const stream = new Writable(); + let output = ''; + let didStreamAnyLogs: boolean = false; + stream._write = (chunk, encoding, next) => { + didStreamAnyLogs = true; + let message = chunk.toString().trimRight(`\n`); + message = `[${CloudRunnerStatics.logPrefix}] ${message}`; + if (Input.cloudRunnerTests) { + output += message; + } + logCallback(message); + next(); + }; + const logOptions = { + follow: true, + pretty: false, + previous: false, + }; + try { + const resultError = await new Promise((resolve) => + new Log(kubeConfig).log(namespace, podName, containerName, stream, resolve, logOptions), + ); + stream.destroy(); + if (resultError) { + throw resultError; + } + if (!didStreamAnyLogs) { + core.error('Failed to stream any logs, listing namespace events, check for an error with the container'); + core.error( + JSON.stringify( + { + events: (await kubeClient.listNamespacedEvent(namespace)).body.items + .filter((x) => { + return x.involvedObject.name === podName || x.involvedObject.name === jobName; + }) + .map((x) => { + return { + type: x.involvedObject.kind, + name: x.involvedObject.name, + message: x.message, + }; + }), + }, + undefined, + 4, + ), + ); + throw new Error(`No logs streamed from k8s`); + } + } catch (error) { + if (stream) { + stream.destroy(); + } + throw error; + } + CloudRunnerLogger.log('end of log stream'); + return output; + } + + static async watchUntilPodRunning(kubeClient: CoreV1Api, podName: string, namespace: string) { + let success: boolean = false; + CloudRunnerLogger.log(`Watching ${podName} ${namespace}`); + await waitUntil( + async () => { + const status = await kubeClient.readNamespacedPodStatus(podName, namespace); + const phase = status?.body.status?.phase; + success = phase === 'Running'; + CloudRunnerLogger.log( + `${status.body.status?.phase} ${status.body.status?.conditions?.[0].reason || ''} ${ + status.body.status?.conditions?.[0].message || '' + }`, + ); + if (success || phase !== 'Pending') return true; + return false; + }, + { + timeout: 2000000, + intervalBetweenAttempts: 15000, + }, + ); + return success; + } +} + +export default KubernetesTaskRunner; diff --git a/src/model/cloud-runner/services/cloud-runner-build-command-process.ts b/src/model/cloud-runner/services/cloud-runner-build-command-process.ts new file mode 100644 index 00000000..e6149588 --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-build-command-process.ts @@ -0,0 +1,40 @@ +import { BuildParameters, Input } from '../..'; +import YAML from 'yaml'; +import CloudRunnerSecret from './cloud-runner-secret'; + +export class CloudRunnerBuildCommandProcessor { + public static ProcessCommands(commands: string, buildParameters: BuildParameters): string { + const hooks = CloudRunnerBuildCommandProcessor.getHooks().filter((x) => x.step.includes(`all`)); + + return `echo "---" + echo "start cloud runner init" + ${Input.cloudRunnerTests ? '' : '#'} printenv + echo "start 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 + ---${buildParameters.logId}" + `; + } + + public static getHooks(): Hook[] { + const experimentHooks = process.env.EXPERIMENTAL_HOOKS; + let output = new Array(); + 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(); + public name; + public hook!: string[]; + public step!: string[]; +} diff --git a/src/model/cloud-runner/services/cloud-runner-constants.ts b/src/model/cloud-runner/services/cloud-runner-constants.ts new file mode 100644 index 00000000..76feb3d6 --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-constants.ts @@ -0,0 +1,4 @@ +class CloudRunnerConstants { + static alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; +} +export default CloudRunnerConstants; diff --git a/src/model/cloud-runner/services/cloud-runner-environment-variable.ts b/src/model/cloud-runner/services/cloud-runner-environment-variable.ts new file mode 100644 index 00000000..fb58e300 --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-environment-variable.ts @@ -0,0 +1,5 @@ +class CloudRunnerEnvironmentVariable { + public name!: string; + public value!: string; +} +export default CloudRunnerEnvironmentVariable; diff --git a/src/model/cloud-runner/services/cloud-runner-logger.ts b/src/model/cloud-runner/services/cloud-runner-logger.ts new file mode 100644 index 00000000..53158843 --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-logger.ts @@ -0,0 +1,47 @@ +import * as core from '@actions/core'; + +class CloudRunnerLogger { + private static timestamp: number; + private static globalTimestamp: number; + + public static setup() { + this.timestamp = this.createTimestamp(); + this.globalTimestamp = this.timestamp; + } + + public static log(message: string) { + core.info(message); + } + + public static logWarning(message: string) { + core.warning(message); + } + + public static logLine(message: string) { + core.info(`${message}\n`); + } + + public static error(message: string) { + core.error(message); + } + + public static logWithTime(message: string) { + const newTimestamp = this.createTimestamp(); + core.info( + `${message} (Since previous: ${this.calculateTimeDiff( + newTimestamp, + this.timestamp, + )}, Total time: ${this.calculateTimeDiff(newTimestamp, this.globalTimestamp)})`, + ); + this.timestamp = newTimestamp; + } + + private static calculateTimeDiff(x: number, y: number) { + return Math.floor((x - y) / 1000); + } + + private static createTimestamp() { + return Date.now(); + } +} +export default CloudRunnerLogger; diff --git a/src/model/cloud-runner/services/cloud-runner-namespace.ts b/src/model/cloud-runner/services/cloud-runner-namespace.ts new file mode 100644 index 00000000..ab5b9059 --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-namespace.ts @@ -0,0 +1,10 @@ +import { customAlphabet } from 'nanoid'; +import CloudRunnerConstants from './cloud-runner-constants'; + +class CloudRunnerNamespace { + static generateBuildName(runNumber: string | number, platform: string) { + const nanoid = customAlphabet(CloudRunnerConstants.alphabet, 4); + return `${runNumber}-${platform.toLowerCase().replace('standalone', '')}-${nanoid()}`; + } +} +export default CloudRunnerNamespace; diff --git a/src/model/cloud-runner/services/cloud-runner-provider-interface.ts b/src/model/cloud-runner/services/cloud-runner-provider-interface.ts new file mode 100644 index 00000000..b89bfa6e --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-provider-interface.ts @@ -0,0 +1,42 @@ +import BuildParameters from '../../build-parameters'; +import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable'; +import CloudRunnerSecret from './cloud-runner-secret'; + +export interface CloudRunnerProviderInterface { + cleanupSharedResources( + // 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 }[], + ); + setupSharedResources( + // 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 }[], + ); + runTask( + // eslint-disable-next-line no-unused-vars + buildGuid: string, + // eslint-disable-next-line no-unused-vars + image: string, + // eslint-disable-next-line no-unused-vars + commands: 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; +} diff --git a/src/model/remote-builder/remote-builder-secret.ts b/src/model/cloud-runner/services/cloud-runner-secret.ts similarity index 62% rename from src/model/remote-builder/remote-builder-secret.ts rename to src/model/cloud-runner/services/cloud-runner-secret.ts index bc102834..b27093d7 100644 --- a/src/model/remote-builder/remote-builder-secret.ts +++ b/src/model/cloud-runner/services/cloud-runner-secret.ts @@ -1,6 +1,6 @@ -class RemoteBuilderSecret { +class CloudRunnerSecret { public ParameterKey!: string; public EnvironmentVariable!: string; public ParameterValue!: string; } -export default RemoteBuilderSecret; +export default CloudRunnerSecret; diff --git a/src/model/cloud-runner/services/task-parameter-serializer.ts b/src/model/cloud-runner/services/task-parameter-serializer.ts new file mode 100644 index 00000000..e1bdcc4a --- /dev/null +++ b/src/model/cloud-runner/services/task-parameter-serializer.ts @@ -0,0 +1,85 @@ +import { Input } from '../..'; +import ImageEnvironmentFactory from '../../image-environment-factory'; +import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import { CloudRunnerBuildCommandProcessor } from './cloud-runner-build-command-process'; + +export class TaskParameterSerializer { + public static readBuildEnvironmentVariables(): CloudRunnerEnvironmentVariable[] { + TaskParameterSerializer.setupDefaultSecrets(); + return [ + { + name: 'ContainerMemory', + value: CloudRunnerState.buildParams.cloudRunnerMemory, + }, + { + name: 'ContainerCpu', + value: CloudRunnerState.buildParams.cloudRunnerCpu, + }, + { + name: 'BUILD_TARGET', + value: CloudRunnerState.buildParams.platform, + }, + ...TaskParameterSerializer.serializeBuildParamsAndInput, + ]; + } + private static get serializeBuildParamsAndInput() { + let array = new Array(); + array = TaskParameterSerializer.readBuildParameters(array); + array = TaskParameterSerializer.readInput(array); + const configurableHooks = CloudRunnerBuildCommandProcessor.getHooks(); + 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); + x.value = `${x.value}`; + return x; + }); + return array; + } + + private static readBuildParameters(array: any[]) { + const keys = Object.keys(CloudRunnerState.buildParams); + for (const element of keys) { + array.push({ + name: element, + value: CloudRunnerState.buildParams[element], + }); + } + array.push({ name: 'buildParameters', value: JSON.stringify(CloudRunnerState.buildParams) }); + return array; + } + + private static readInput(array: any[]) { + const input = Object.getOwnPropertyNames(Input); + for (const element of input) { + if (typeof Input[element] !== 'function' && array.filter((x) => x.name === element).length === 0) { + array.push({ + name: element, + value: `${Input[element]}`, + }); + } + } + return array; + } + + private static setupDefaultSecrets() { + if (CloudRunnerState.defaultSecrets === undefined) + CloudRunnerState.defaultSecrets = ImageEnvironmentFactory.getEnvironmentVariables( + CloudRunnerState.buildParams, + ).map((x) => { + return { + ParameterKey: x.name, + EnvironmentVariable: x.name, + ParameterValue: x.value, + }; + }); + } +} diff --git a/src/model/cloud-runner/state/cloud-runner-state.ts b/src/model/cloud-runner/state/cloud-runner-state.ts new file mode 100644 index 00000000..26648b93 --- /dev/null +++ b/src/model/cloud-runner/state/cloud-runner-state.ts @@ -0,0 +1,81 @@ +import path from 'path'; +import { BuildParameters } from '../..'; +import { CloudRunnerProviderInterface } from '../services/cloud-runner-provider-interface'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; + +export class CloudRunnerState { + public static CloudRunnerProviderPlatform: CloudRunnerProviderInterface; + public static buildParams: BuildParameters; + public static defaultSecrets: CloudRunnerSecret[]; + public static readonly repositoryFolder = 'repo'; + + // 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 buildPathFull(): string { + return path.join(`/`, CloudRunnerState.buildVolumeFolder, CloudRunnerState.buildParams.buildGuid); + } + + public static get cacheFolderFull(): string { + return path.join( + '/', + CloudRunnerState.buildVolumeFolder, + CloudRunnerState.cacheFolder, + CloudRunnerState.branchName, + ); + } + + static setup(buildParameters: BuildParameters) { + CloudRunnerState.buildParams = buildParameters; + } + + public static get branchName(): string { + return CloudRunnerState.buildParams.branch; + } + public static get builderPathFull(): string { + return path.join(CloudRunnerState.buildPathFull, `builder`); + } + + public static get repoPathFull(): string { + return path.join(CloudRunnerState.buildPathFull, CloudRunnerState.repositoryFolder); + } + + public static get projectPathFull(): string { + return path.join(CloudRunnerState.repoPathFull, CloudRunnerState.buildParams.projectPath); + } + + public static get libraryFolderFull(): string { + return path.join(CloudRunnerState.projectPathFull, `Library`); + } + + public static get lfsDirectoryFull(): string { + return path.join(CloudRunnerState.repoPathFull, `.git`, `lfs`); + } + + public static get purgeRemoteCaching(): boolean { + return process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined; + } + + public static get lfsCacheFolderFull() { + return path.join(CloudRunnerState.cacheFolderFull, `lfs`); + } + + public static get libraryCacheFolderFull() { + return path.join(CloudRunnerState.cacheFolderFull, `Library`); + } + + public static get unityBuilderRepoUrl(): string { + return `https://${CloudRunnerState.buildParams.githubToken}@github.com/game-ci/unity-builder.git`; + } + + public static get targetBuildRepoUrl(): string { + return `https://${CloudRunnerState.buildParams.githubToken}@github.com/${CloudRunnerState.buildParams.githubRepo}.git`; + } + + public static get buildVolumeFolder() { + return 'data'; + } + + public static get cacheFolder() { + return 'cache'; + } +} diff --git a/src/model/cloud-runner/state/cloud-runner-step-state.ts b/src/model/cloud-runner/state/cloud-runner-step-state.ts new file mode 100644 index 00000000..d09461b8 --- /dev/null +++ b/src/model/cloud-runner/state/cloud-runner-step-state.ts @@ -0,0 +1,13 @@ +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; + +export class CloudRunnerStepState { + public image: string; + public environment: CloudRunnerEnvironmentVariable[]; + public secrets: CloudRunnerSecret[]; + constructor(image: string, environmentVariables: CloudRunnerEnvironmentVariable[], secrets: CloudRunnerSecret[]) { + this.image = image; + this.environment = environmentVariables; + this.secrets = secrets; + } +} diff --git a/src/model/cloud-runner/steps/build-step.ts b/src/model/cloud-runner/steps/build-step.ts new file mode 100644 index 00000000..0b85d2aa --- /dev/null +++ b/src/model/cloud-runner/steps/build-step.ts @@ -0,0 +1,77 @@ +import path from 'path'; +import { Input } from '../..'; +import { CloudRunnerBuildCommandProcessor } from '../services/cloud-runner-build-command-process'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import { CloudRunnerStepState } from '../state/cloud-runner-step-state'; +import { StepInterface } from './step-interface'; + +export class BuildStep implements StepInterface { + async run(cloudRunnerStepState: CloudRunnerStepState) { + return await BuildStep.BuildStep( + cloudRunnerStepState.image, + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, + ); + } + + private static async BuildStep( + image: string, + environmentVariables: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ) { + CloudRunnerLogger.logLine(` `); + CloudRunnerLogger.logLine('Starting part 2/2 (build unity project)'); + const hooks = CloudRunnerBuildCommandProcessor.getHooks().filter((x) => x.step.includes(`setup`)); + return await CloudRunnerState.CloudRunnerProviderPlatform.runTask( + CloudRunnerState.buildParams.buildGuid, + image, + `${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} + export GITHUB_WORKSPACE="${CloudRunnerState.repoPathFull}" + cp -r "${path + .join(CloudRunnerState.builderPathFull, 'dist', 'default-build-script') + .replace(/\\/g, `/`)}" "/UnityBuilderAction" + cp -r "${path + .join(CloudRunnerState.builderPathFull, 'dist', 'platforms', 'ubuntu', 'entrypoint.sh') + .replace(/\\/g, `/`)}" "/entrypoint.sh" + cp -r "${path + .join(CloudRunnerState.builderPathFull, 'dist', 'platforms', 'ubuntu', 'steps') + .replace(/\\/g, `/`)}" "/steps" + chmod -R +x "/entrypoint.sh" + chmod -R +x "/steps" + /entrypoint.sh + apt-get update + apt-get install -y -q zip tree + cd "${CloudRunnerState.libraryFolderFull.replace(/\\/g, `/`)}/.." + zip -r "lib-${CloudRunnerState.buildParams.buildGuid}.zip" "Library" + mv "lib-${CloudRunnerState.buildParams.buildGuid}.zip" "${CloudRunnerState.cacheFolderFull.replace( + /\\/g, + `/`, + )}/Library" + cd "${CloudRunnerState.repoPathFull.replace(/\\/g, `/`)}" + ${Input.cloudRunnerTests ? '' : '#'} tree -lh + zip -r "build-${CloudRunnerState.buildParams.buildGuid}.zip" "build" + ${Input.cloudRunnerTests ? '' : '#'} tree -lh + ${Input.cloudRunnerTests ? '' : '#'} tree -lh "${CloudRunnerState.cacheFolderFull.replace(/\\/g, `/`)}" + mv "build-${CloudRunnerState.buildParams.buildGuid}.zip" "${CloudRunnerState.cacheFolderFull.replace( + /\\/g, + `/`, + )}" + chmod +x ${path.join(CloudRunnerState.builderPathFull, 'dist', `index.js`).replace(/\\/g, `/`)} + node ${path + .join(CloudRunnerState.builderPathFull, 'dist', `index.js`) + .replace(/\\/g, `/`)} -m cache-push "Library" "lib-${ + CloudRunnerState.buildParams.buildGuid + }.zip" "${CloudRunnerState.cacheFolderFull.replace(/\\/g, `/`)}/Library" + ${Input.cloudRunnerTests ? '' : '#'} tree -lh "${CloudRunnerState.cacheFolderFull}" + ${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} + `, + `/${CloudRunnerState.buildVolumeFolder}`, + `/${CloudRunnerState.projectPathFull}`, + environmentVariables, + secrets, + ); + } +} diff --git a/src/model/cloud-runner/steps/setup-step.ts b/src/model/cloud-runner/steps/setup-step.ts new file mode 100644 index 00000000..cef427c7 --- /dev/null +++ b/src/model/cloud-runner/steps/setup-step.ts @@ -0,0 +1,59 @@ +import path from 'path'; +import { Input } from '../..'; +import { CloudRunnerBuildCommandProcessor } from '../services/cloud-runner-build-command-process'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import { CloudRunnerStepState } from '../state/cloud-runner-step-state'; +import { StepInterface } from './step-interface'; + +export class SetupStep implements StepInterface { + async run(cloudRunnerStepState: CloudRunnerStepState) { + try { + return await SetupStep.downloadRepository( + cloudRunnerStepState.image, + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, + ); + } catch (error) { + throw error; + } + } + + private static async downloadRepository( + image: string, + environmentVariables: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ) { + try { + CloudRunnerLogger.log(` `); + CloudRunnerLogger.logLine('Starting step 1/2 (setup game files from repository)'); + const hooks = CloudRunnerBuildCommandProcessor.getHooks().filter((x) => x.step.includes(`setup`)); + return await CloudRunnerState.CloudRunnerProviderPlatform.runTask( + CloudRunnerState.buildParams.buildGuid, + image, + `apk update -q + apk add git-lfs jq tree zip unzip nodejs -q + ${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} + export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 + mkdir -p ${CloudRunnerState.builderPathFull.replace(/\\/g, `/`)} + git clone -q -b ${CloudRunnerState.branchName} ${ + CloudRunnerState.unityBuilderRepoUrl + } "${CloudRunnerState.builderPathFull.replace(/\\/g, `/`)}" + ${Input.cloudRunnerTests ? '' : '#'} tree ${CloudRunnerState.builderPathFull.replace(/\\/g, `/`)} + chmod +x ${path.join(CloudRunnerState.builderPathFull, 'dist', `index.js`).replace(/\\/g, `/`)} + node ${path.join(CloudRunnerState.builderPathFull, 'dist', `index.js`).replace(/\\/g, `/`)} -m remote-cli + ${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} + `, + `/${CloudRunnerState.buildVolumeFolder}`, + `/${CloudRunnerState.buildVolumeFolder}/`, + environmentVariables, + secrets, + ); + } catch (error) { + CloudRunnerLogger.logLine(`Failed download repository step 1/2`); + throw error; + } + } +} diff --git a/src/model/cloud-runner/steps/step-interface.ts b/src/model/cloud-runner/steps/step-interface.ts new file mode 100644 index 00000000..7e6fc450 --- /dev/null +++ b/src/model/cloud-runner/steps/step-interface.ts @@ -0,0 +1,8 @@ +import { CloudRunnerStepState } from '../state/cloud-runner-step-state'; + +export interface StepInterface { + run( + // eslint-disable-next-line no-unused-vars + cloudRunnerStepState: CloudRunnerStepState, + ); +} diff --git a/src/model/cloud-runner/workflows/build-automation-workflow.ts b/src/model/cloud-runner/workflows/build-automation-workflow.ts new file mode 100644 index 00000000..22bc8ded --- /dev/null +++ b/src/model/cloud-runner/workflows/build-automation-workflow.ts @@ -0,0 +1,68 @@ +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { TaskParameterSerializer } from '../services/task-parameter-serializer'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import { CloudRunnerStepState } from '../state/cloud-runner-step-state'; +import { BuildStep } from '../steps/build-step'; +import { SetupStep } from '../steps/setup-step'; +import { CustomWorkflow } from './custom-workflow'; +import { WorkflowInterface } from './workflow-interface'; +import * as core from '@actions/core'; + +export class BuildAutomationWorkflow implements WorkflowInterface { + async run(cloudRunnerStepState: CloudRunnerStepState) { + try { + return await BuildAutomationWorkflow.standardBuildAutomation(cloudRunnerStepState.image); + } catch (error) { + throw error; + } + } + + private static async standardBuildAutomation(baseImage: any) { + try { + CloudRunnerLogger.log(`Cloud Runner is running standard build automation`); + + core.startGroup('pre build steps'); + let output = ''; + if (CloudRunnerState.buildParams.preBuildSteps !== '') { + output += await CustomWorkflow.runCustomJob(CloudRunnerState.buildParams.preBuildSteps); + } + core.endGroup(); + CloudRunnerLogger.logWithTime('Configurable pre build step(s) time'); + + core.startGroup('setup'); + output += await new SetupStep().run( + new CloudRunnerStepState( + 'alpine/git', + TaskParameterSerializer.readBuildEnvironmentVariables(), + CloudRunnerState.defaultSecrets, + ), + ); + core.endGroup(); + CloudRunnerLogger.logWithTime('Download repository step time'); + + core.startGroup('build'); + output += await new BuildStep().run( + new CloudRunnerStepState( + baseImage, + TaskParameterSerializer.readBuildEnvironmentVariables(), + CloudRunnerState.defaultSecrets, + ), + ); + core.endGroup(); + CloudRunnerLogger.logWithTime('Build time'); + + core.startGroup('post build steps'); + if (CloudRunnerState.buildParams.postBuildSteps !== '') { + output += await CustomWorkflow.runCustomJob(CloudRunnerState.buildParams.postBuildSteps); + } + core.endGroup(); + CloudRunnerLogger.logWithTime('Configurable post build step(s) time'); + + CloudRunnerLogger.log(`Cloud Runner finished running standard build automation`); + + return output; + } catch (error) { + throw error; + } + } +} diff --git a/src/model/cloud-runner/workflows/custom-workflow.ts b/src/model/cloud-runner/workflows/custom-workflow.ts new file mode 100644 index 00000000..996b0cb5 --- /dev/null +++ b/src/model/cloud-runner/workflows/custom-workflow.ts @@ -0,0 +1,46 @@ +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { CloudRunnerState } from '../state/cloud-runner-state'; +import YAML from 'yaml'; +import { Input } from '../..'; +import { TaskParameterSerializer } from '../services/task-parameter-serializer'; + +export class CustomWorkflow { + public static async runCustomJob(buildSteps) { + try { + CloudRunnerLogger.log(`Cloud Runner is running in custom job mode`); + if (Input.cloudRunnerTests) { + CloudRunnerLogger.log(`Parsing build steps: ${buildSteps}`); + } + try { + buildSteps = YAML.parse(buildSteps); + let output = ''; + 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 CloudRunnerState.CloudRunnerProviderPlatform.runTask( + CloudRunnerState.buildParams.buildGuid, + step['image'], + step['commands'], + `/${CloudRunnerState.buildVolumeFolder}`, + `/${CloudRunnerState.buildVolumeFolder}/`, + TaskParameterSerializer.readBuildEnvironmentVariables(), + [...CloudRunnerState.defaultSecrets, ...stepSecrets], + ); + } + return output; + } catch (error) { + CloudRunnerLogger.log(`failed to parse a custom job "${buildSteps}"`); + throw error; + } + } catch (error) { + throw error; + } + } +} diff --git a/src/model/cloud-runner/workflows/workflow-composition-root.ts b/src/model/cloud-runner/workflows/workflow-composition-root.ts new file mode 100644 index 00000000..f4b01fdd --- /dev/null +++ b/src/model/cloud-runner/workflows/workflow-composition-root.ts @@ -0,0 +1,42 @@ +import { CloudRunnerState } from '../state/cloud-runner-state'; +import { CloudRunnerStepState } from '../state/cloud-runner-step-state'; +import { CustomWorkflow } from './custom-workflow'; +import { WorkflowInterface } from './workflow-interface'; +import { BuildAutomationWorkflow } from './build-automation-workflow'; +import { TaskParameterSerializer } from '../services/task-parameter-serializer'; +import { SetupStep } from '../steps/setup-step'; + +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 (CloudRunnerState.buildParams.customJob === `setup`) { + return await new SetupStep().run( + new CloudRunnerStepState( + baseImage, + TaskParameterSerializer.readBuildEnvironmentVariables(), + CloudRunnerState.defaultSecrets, + ), + ); + } else if (CloudRunnerState.buildParams.customJob !== '') { + return await CustomWorkflow.runCustomJob(CloudRunnerState.buildParams.customJob); + } + return await new BuildAutomationWorkflow().run( + new CloudRunnerStepState( + baseImage, + TaskParameterSerializer.readBuildEnvironmentVariables(), + CloudRunnerState.defaultSecrets, + ), + ); + } catch (error) { + throw error; + } + } +} diff --git a/src/model/cloud-runner/workflows/workflow-interface.ts b/src/model/cloud-runner/workflows/workflow-interface.ts new file mode 100644 index 00000000..0192a60e --- /dev/null +++ b/src/model/cloud-runner/workflows/workflow-interface.ts @@ -0,0 +1,8 @@ +import { CloudRunnerStepState } from '../state/cloud-runner-step-state'; + +export interface WorkflowInterface { + run( + // eslint-disable-next-line no-unused-vars + cloudRunnerStepState: CloudRunnerStepState, + ); +} diff --git a/src/model/docker.ts b/src/model/docker.ts index 7f82ab5e..a7ea514e 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -1,5 +1,6 @@ import { exec } from '@actions/exec'; import ImageTag from './image-tag'; +import ImageEnvironmentFactory from './image-environment-factory'; class Docker { static async build(buildParameters, silent = false) { @@ -18,31 +19,7 @@ class Docker { } static async run(image, parameters, silent = false) { - const { - version, - workspace, - unitySerial, - runnerTempPath, - platform, - projectPath, - buildName, - buildPath, - buildFile, - buildMethod, - buildVersion, - androidVersionCode, - androidKeystoreName, - androidKeystoreBase64, - androidKeystorePass, - androidKeyaliasName, - androidKeyaliasPass, - androidTargetSdkVersion, - androidSdkManagerParameters, - customParameters, - sshAgent, - gitPrivateToken, - chownFilesTo, - } = parameters; + const { workspace, unitySerial, runnerTempPath, sshAgent } = parameters; const baseOsSpecificArguments = this.getBaseOsSpecificArguments( process.platform, @@ -55,45 +32,7 @@ class Docker { const runCommand = `docker run \ --workdir /github/workspace \ --rm \ - --env UNITY_LICENSE \ - --env UNITY_LICENSE_FILE \ - --env UNITY_EMAIL \ - --env UNITY_PASSWORD \ - --env UNITY_VERSION="${version}" \ - --env USYM_UPLOAD_AUTH_TOKEN \ - --env PROJECT_PATH="${projectPath}" \ - --env BUILD_TARGET="${platform}" \ - --env BUILD_NAME="${buildName}" \ - --env BUILD_PATH="${buildPath}" \ - --env BUILD_FILE="${buildFile}" \ - --env BUILD_METHOD="${buildMethod}" \ - --env VERSION="${buildVersion}" \ - --env ANDROID_VERSION_CODE="${androidVersionCode}" \ - --env ANDROID_KEYSTORE_NAME="${androidKeystoreName}" \ - --env ANDROID_KEYSTORE_BASE64="${androidKeystoreBase64}" \ - --env ANDROID_KEYSTORE_PASS="${androidKeystorePass}" \ - --env ANDROID_KEYALIAS_NAME="${androidKeyaliasName}" \ - --env ANDROID_KEYALIAS_PASS="${androidKeyaliasPass}" \ - --env ANDROID_TARGET_SDK_VERSION="${androidTargetSdkVersion}" \ - --env ANDROID_SDK_MANAGER_PARAMETERS="${androidSdkManagerParameters}" \ - --env CUSTOM_PARAMETERS="${customParameters}" \ - --env CHOWN_FILES_TO="${chownFilesTo}" \ - --env GITHUB_REF \ - --env GITHUB_SHA \ - --env GITHUB_REPOSITORY \ - --env GITHUB_ACTOR \ - --env GITHUB_WORKFLOW \ - --env GITHUB_HEAD_REF \ - --env GITHUB_BASE_REF \ - --env GITHUB_EVENT_NAME \ - --env GITHUB_WORKSPACE=/github/workspace \ - --env GITHUB_ACTION \ - --env GITHUB_EVENT_PATH \ - --env RUNNER_OS \ - --env RUNNER_TOOL_CACHE \ - --env RUNNER_TEMP \ - --env RUNNER_WORKSPACE \ - --env GIT_PRIVATE_TOKEN="${gitPrivateToken}" \ + ${ImageEnvironmentFactory.getEnvVarString(parameters)} \ ${baseOsSpecificArguments} \ ${image}`; diff --git a/src/model/image-environment-factory.ts b/src/model/image-environment-factory.ts new file mode 100644 index 00000000..2b198018 --- /dev/null +++ b/src/model/image-environment-factory.ts @@ -0,0 +1,70 @@ +import BuildParameters from './build-parameters'; +import { ReadLicense } from './input-readers/test-license-reader'; + +class Parameter { + public name; + public value; +} + +class ImageEnvironmentFactory { + public static getEnvVarString(parameters) { + const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(parameters); + let string = ''; + for (const p of environmentVariables) { + if (p.value === '' || p.value === undefined) { + continue; + } + if (p.value.toString().includes(`\n`)) { + string += `--env ${p.name} `; + continue; + } + string += `--env ${p.name}="${p.value}" `; + } + return string; + } + public static getEnvironmentVariables(parameters: BuildParameters) { + const 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 }, + { name: 'UNITY_PASSWORD', value: process.env.UNITY_PASSWORD }, + { name: 'UNITY_SERIAL', value: parameters.unitySerial }, + { name: 'UNITY_VERSION', value: parameters.version }, + { name: 'USYM_UPLOAD_AUTH_TOKEN', value: process.env.USYM_UPLOAD_AUTH_TOKEN }, + { name: 'PROJECT_PATH', value: parameters.projectPath }, + { name: 'BUILD_TARGET', value: parameters.platform }, + { name: 'BUILD_NAME', value: parameters.buildName }, + { name: 'BUILD_PATH', value: parameters.buildPath }, + { name: 'BUILD_FILE', value: parameters.buildFile }, + { name: 'BUILD_METHOD', value: parameters.buildMethod }, + { name: 'VERSION', value: parameters.buildVersion }, + { name: 'ANDROID_VERSION_CODE', value: parameters.androidVersionCode }, + { name: 'ANDROID_KEYSTORE_NAME', value: parameters.androidKeystoreName }, + { name: 'ANDROID_KEYSTORE_BASE64', value: parameters.androidKeystoreBase64 }, + { name: 'ANDROID_KEYSTORE_PASS', value: parameters.androidKeystorePass }, + { name: 'ANDROID_KEYALIAS_NAME', value: parameters.androidKeyaliasName }, + { name: 'ANDROID_KEYALIAS_PASS', value: parameters.androidKeyaliasPass }, + { name: 'CUSTOM_PARAMETERS', value: parameters.customParameters }, + { name: 'CHOWN_FILES_TO', value: parameters.chownFilesTo }, + { name: 'GITHUB_REF', value: process.env.GITHUB_REF }, + { name: 'GITHUB_SHA', value: process.env.GITHUB_SHA }, + { name: 'GITHUB_REPOSITORY', value: process.env.GITHUB_REPOSITORY }, + { name: 'GITHUB_ACTOR', value: process.env.GITHUB_ACTOR }, + { name: 'GITHUB_WORKFLOW', value: process.env.GITHUB_WORKFLOW }, + { name: 'GITHUB_HEAD_REF', value: process.env.GITHUB_HEAD_REF }, + { name: 'GITHUB_BASE_REF', value: process.env.GITHUB_BASE_REF }, + { name: 'GITHUB_EVENT_NAME', value: process.env.GITHUB_EVENT_NAME }, + { name: 'GITHUB_WORKSPACE', value: '/github/workspace' }, + { name: 'GITHUB_ACTION', value: process.env.GITHUB_ACTION }, + { name: 'GITHUB_EVENT_PATH', value: process.env.GITHUB_EVENT_PATH }, + { name: 'RUNNER_OS', value: process.env.RUNNER_OS }, + { name: 'RUNNER_TOOL_CACHE', value: process.env.RUNNER_TOOL_CACHE }, + { name: 'RUNNER_TEMP', value: process.env.RUNNER_TEMP }, + { name: 'RUNNER_WORKSPACE', value: process.env.RUNNER_WORKSPACE }, + ]; + if (parameters.sshAgent) environmentVariables.push({ name: 'SSH_AUTH_SOCK', value: '/ssh-agent' }); + return environmentVariables; + } +} + +export default ImageEnvironmentFactory; diff --git a/src/model/index.ts b/src/model/index.ts index f277b85d..dfd5b2f1 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -9,8 +9,7 @@ import Platform from './platform'; import Project from './project'; import Unity from './unity'; import Versioning from './versioning'; -import Kubernetes from './kubernetes'; -import RemoteBuilder from './remote-builder/remote-builder'; +import CloudRunner from './cloud-runner/cloud-runner'; export { Action, @@ -24,6 +23,5 @@ export { Project, Unity, Versioning, - Kubernetes, - RemoteBuilder, + CloudRunner as CloudRunner, }; diff --git a/src/model/input-readers/action-yaml.ts b/src/model/input-readers/action-yaml.ts new file mode 100644 index 00000000..8c46bb66 --- /dev/null +++ b/src/model/input-readers/action-yaml.ts @@ -0,0 +1,17 @@ +import fs from 'fs'; +import path from 'path'; +import YAML from 'yaml'; + +export class ActionYamlReader { + private actionYamlParsed: any; + public constructor() { + let filename = `action.yml`; + if (!fs.existsSync(filename)) { + filename = path.join(__dirname, `..`, filename); + } + this.actionYamlParsed = YAML.parse(fs.readFileSync(filename).toString()); + } + public GetActionYamlValue(key: string) { + return this.actionYamlParsed.inputs[key]?.description || 'No description found in action.yml'; + } +} diff --git a/src/model/input-readers/git-repo.test.ts b/src/model/input-readers/git-repo.test.ts new file mode 100644 index 00000000..1e96db60 --- /dev/null +++ b/src/model/input-readers/git-repo.test.ts @@ -0,0 +1,8 @@ +import { GitRepoReader } from './git-repo'; + +describe(`git repo tests`, () => { + it(`Branch value parsed from CLI to not contain illegal characters`, async () => { + expect(await GitRepoReader.GetBranch()).not.toContain(`\n`); + expect(await GitRepoReader.GetBranch()).not.toContain(` `); + }); +}); diff --git a/src/model/input-readers/git-repo.ts b/src/model/input-readers/git-repo.ts new file mode 100644 index 00000000..9b6c902f --- /dev/null +++ b/src/model/input-readers/git-repo.ts @@ -0,0 +1,20 @@ +import { assert } from 'console'; +import System from '../system'; +import fs from 'fs'; +import { CloudRunnerSystem } from '../cli/remote-client/remote-client-services/cloud-runner-system'; + +export class GitRepoReader { + static GetSha() { + return ''; + } + public static async GetRemote() { + return (await CloudRunnerSystem.Run(`git remote -v`)) + .split(' ')[1] + .split('https://github.com/')[1] + .split('.git')[0]; + } + public static async GetBranch() { + assert(fs.existsSync(`.git`)); + return (await System.run(`git branch`, [], {}, false)).split('*')[1].split(`\n`)[0].replace(/ /g, ``); + } +} diff --git a/src/model/input-readers/github-cli.test.ts b/src/model/input-readers/github-cli.test.ts new file mode 100644 index 00000000..ca4b6b72 --- /dev/null +++ b/src/model/input-readers/github-cli.test.ts @@ -0,0 +1,9 @@ +import { GithubCliReader } from './github-cli'; +import * as core from '@actions/core'; + +describe(`github cli`, () => { + it(`returns`, async () => { + const token = await GithubCliReader.GetGitHubAuthToken(); + core.info(token); + }); +}); diff --git a/src/model/input-readers/github-cli.ts b/src/model/input-readers/github-cli.ts new file mode 100644 index 00000000..5b56eb29 --- /dev/null +++ b/src/model/input-readers/github-cli.ts @@ -0,0 +1,20 @@ +import { CloudRunnerSystem } from '../cli/remote-client/remote-client-services/cloud-runner-system'; +import * as core from '@actions/core'; + +export class GithubCliReader { + static async GetGitHubAuthToken() { + try { + const authStatus = await CloudRunnerSystem.Run(`gh auth status`, true); + if (authStatus.includes('You are not logged') || authStatus === '') { + return ''; + } + return (await CloudRunnerSystem.Run(`gh auth status -t`)) + .split(`Token: `)[1] + .replace(/ /g, '') + .replace(/\n/g, ''); + } catch (error: any) { + core.info(error || 'Failed to get github auth token from gh cli'); + return ''; + } + } +} diff --git a/src/model/input-readers/test-license-reader.ts b/src/model/input-readers/test-license-reader.ts new file mode 100644 index 00000000..c4b4cbeb --- /dev/null +++ b/src/model/input-readers/test-license-reader.ts @@ -0,0 +1,8 @@ +import path from 'path'; +import fs from 'fs'; +import YAML from 'yaml'; + +export function ReadLicense() { + const pipelineFile = path.join(__dirname, `.github`, `workflows`, `cloud-runner-k8s-pipeline.yml`); + return fs.existsSync(pipelineFile) ? YAML.parse(fs.readFileSync(pipelineFile, 'utf8')).env.UNITY_LICENSE : ''; +} diff --git a/src/model/input.test.ts b/src/model/input.test.ts index f2eb7467..7cf2fc54 100644 --- a/src/model/input.test.ts +++ b/src/model/input.test.ts @@ -48,7 +48,7 @@ describe('Input', () => { describe('projectPath', () => { it('returns the default value', () => { - expect(Input.projectPath).toStrictEqual('.'); + expect(Input.projectPath).toStrictEqual('test-project'); }); it('takes input from the users workflow', () => { diff --git a/src/model/input.ts b/src/model/input.ts index 339318fa..0150d46d 100644 --- a/src/model/input.ts +++ b/src/model/input.ts @@ -1,3 +1,7 @@ +import fs from 'fs'; +import path from 'path'; +import { GitRepoReader } from './input-readers/git-repo'; +import { GithubCliReader } from './input-readers/github-cli'; import Platform from './platform'; const core = require('@actions/core'); @@ -8,71 +12,134 @@ const core = require('@actions/core'); * Note that input is always passed as a string, even booleans. */ class Input { + public static cliOptions; + public static githubInputEnabled: boolean = true; + + // also enabled debug logging for cloud runner + static get cloudRunnerTests(): boolean { + return Input.getInput(`cloudRunnerTests`) || Input.getInput(`CloudRunnerTests`) || false; + } + private static getInput(query) { + const coreInput = core.getInput(query); + if (Input.githubInputEnabled && coreInput && coreInput !== '') { + return coreInput; + } + + return Input.cliOptions !== undefined && Input.cliOptions[query] !== undefined + ? Input.cliOptions[query] + : process.env[query] !== undefined + ? process.env[query] + : process.env[Input.ToEnvVarFormat(query)] + ? process.env[Input.ToEnvVarFormat(query)] + : ''; + } + static get region(): string { + return Input.getInput('region') || 'eu-west-2'; + } + static async githubRepo() { + return ( + Input.getInput('GITHUB_REPOSITORY') || + Input.getInput('GITHUB_REPO') || + (await GitRepoReader.GetRemote()) || + 'game-ci/unity-builder' + ); + } + static async branch() { + if (await GitRepoReader.GetBranch()) { + return await GitRepoReader.GetBranch(); + } else if (Input.getInput(`GITHUB_REF`)) { + return Input.getInput(`GITHUB_REF`).replace('refs/', '').replace(`head/`, ''); + } else if (Input.getInput('branch')) { + return Input.getInput('branch'); + } else { + return 'main'; + } + } + + static get gitSha() { + if (Input.getInput(`GITHUB_SHA`)) { + return Input.getInput(`GITHUB_SHA`); + } else if (Input.getInput(`GitSHA`)) { + return Input.getInput(`GitSHA`); + } else if (GitRepoReader.GetSha()) { + return GitRepoReader.GetSha(); + } + } + static get runNumber() { + return Input.getInput('GITHUB_RUN_NUMBER') || '0'; + } + static get unityVersion() { - return core.getInput('unityVersion') || 'auto'; + return Input.getInput('unityVersion') || 'auto'; } static get customImage() { - return core.getInput('customImage'); + return Input.getInput('customImage'); } static get targetPlatform() { - return core.getInput('targetPlatform') || Platform.default; + return Input.getInput('targetPlatform') || Platform.default; } static get projectPath() { - const rawProjectPath = core.getInput('projectPath') || '.'; + const input = Input.getInput('projectPath'); + const rawProjectPath = input + ? input + : fs.existsSync(path.join('test-project', 'ProjectSettings', 'ProjectVersion.txt')) && + !fs.existsSync(path.join('ProjectSettings', 'ProjectVersion.txt')) + ? 'test-project' + : '.'; return rawProjectPath.replace(/\/$/, ''); } static get buildName() { - return core.getInput('buildName') || this.targetPlatform; + return Input.getInput('buildName') || this.targetPlatform; } static get buildsPath() { - return core.getInput('buildsPath') || 'build'; + return Input.getInput('buildsPath') || 'build'; } static get buildMethod() { - return core.getInput('buildMethod'); // processed in docker file + return Input.getInput('buildMethod') || ''; // processed in docker file } static get versioningStrategy() { - return core.getInput('versioning') || 'Semantic'; + return Input.getInput('versioning') || 'Semantic'; } static get specifiedVersion() { - return core.getInput('version') || ''; + return Input.getInput('version') || ''; } static get androidVersionCode() { - return core.getInput('androidVersionCode'); + return Input.getInput('androidVersionCode'); } static get androidAppBundle() { - const input = core.getInput('androidAppBundle') || false; + const input = Input.getInput('androidAppBundle') || false; return input === 'true'; } static get androidKeystoreName() { - return core.getInput('androidKeystoreName') || ''; + return Input.getInput('androidKeystoreName') || ''; } static get androidKeystoreBase64() { - return core.getInput('androidKeystoreBase64') || ''; + return Input.getInput('androidKeystoreBase64') || ''; } static get androidKeystorePass() { - return core.getInput('androidKeystorePass') || ''; + return Input.getInput('androidKeystorePass') || ''; } static get androidKeyaliasName() { - return core.getInput('androidKeyaliasName') || ''; + return Input.getInput('androidKeyaliasName') || ''; } static get androidKeyaliasPass() { - return core.getInput('androidKeyaliasPass') || ''; + return Input.getInput('androidKeyaliasPass') || ''; } static get androidTargetSdkVersion() { @@ -80,57 +147,77 @@ class Input { } static get allowDirtyBuild() { - const input = core.getInput('allowDirtyBuild') || false; + const input = Input.getInput('allowDirtyBuild') || false; return input === 'true'; } static get customParameters() { - return core.getInput('customParameters') || ''; + return Input.getInput('customParameters') || ''; } static get sshAgent() { - return core.getInput('sshAgent') || ''; + return Input.getInput('sshAgent') || ''; } - static get gitPrivateToken() { - return core.getInput('gitPrivateToken') || ''; + static async githubToken() { + return Input.getInput('githubToken') || (await GithubCliReader.GetGitHubAuthToken()) || ''; + } + + static async gitPrivateToken() { + return core.getInput('gitPrivateToken') || (await Input.githubToken()); } static get chownFilesTo() { - return core.getInput('chownFilesTo') || ''; + return Input.getInput('chownFilesTo') || ''; } - static get remoteBuildCluster() { - return core.getInput('remoteBuildCluster') || ''; + static get postBuildSteps() { + return Input.getInput('postBuildSteps') || ''; } - static get awsStackName() { - return core.getInput('awsStackName') || ''; + static get preBuildSteps() { + return Input.getInput('preBuildSteps') || ''; + } + + static get customJob() { + return Input.getInput('customJob') || ''; + } + + static get cloudRunnerCluster() { + return Input.getInput('cloudRunnerCluster') || ''; + } + + static get awsBaseStackName() { + return Input.getInput('awsBaseStackName') || 'game-ci'; } static get kubeConfig() { - return core.getInput('kubeConfig') || ''; + return Input.getInput('kubeConfig') || ''; } - static get githubToken() { - return core.getInput('githubToken') || ''; + static get cloudRunnerMemory() { + return Input.getInput('cloudRunnerMemory') || '750M'; } - static get remoteBuildMemory() { - return core.getInput('remoteBuildMemory') || '800M'; - } - - static get remoteBuildCpu() { - return core.getInput('remoteBuildCpu') || '0.25'; + static get cloudRunnerCpu() { + return Input.getInput('cloudRunnerCpu') || '1.0'; } static get kubeVolumeSize() { - return core.getInput('kubeVolumeSize') || '5Gi'; + return Input.getInput('kubeVolumeSize') || '5Gi'; } static get kubeVolume() { - return core.getInput('kubeVolume') || ''; + return Input.getInput('kubeVolume') || ''; + } + + public static ToEnvVarFormat(input: string) { + return input + .replace(/([A-Z])/g, ' $1') + .trim() + .toUpperCase() + .replace(/ /g, '_'); } } diff --git a/src/model/kubernetes.ts b/src/model/kubernetes.ts deleted file mode 100644 index 1e31bb44..00000000 --- a/src/model/kubernetes.ts +++ /dev/null @@ -1,354 +0,0 @@ -// @ts-ignore -import { Client, KubeConfig } from 'kubernetes-client'; -import Request from 'kubernetes-client/backends/request'; - -const core = require('@actions/core'); -const base64 = require('base-64'); - -const pollInterval = 10000; - -class Kubernetes { - private static kubeClient: any; - private static buildId: string; - private static buildParameters: any; - private static baseImage: any; - private static pvcName: string; - private static secretName: string; - private static jobName: string; - private static namespace: string; - - static async runBuildJob(buildParameters, baseImage) { - const kubeconfig = new KubeConfig(); - kubeconfig.loadFromString(base64.decode(buildParameters.kubeConfig)); - const backend = new Request({ kubeconfig }); - const kubeClient = new Client(backend); - await kubeClient.loadSpec(); - - const buildId = Kubernetes.uuidv4(); - const pvcName = `unity-builder-pvc-${buildId}`; - const secretName = `build-credentials-${buildId}`; - const jobName = `unity-builder-job-${buildId}`; - const namespace = 'default'; - - this.kubeClient = kubeClient; - this.buildId = buildId; - this.buildParameters = buildParameters; - this.baseImage = baseImage; - this.pvcName = pvcName; - this.secretName = secretName; - this.jobName = jobName; - this.namespace = namespace; - - await Kubernetes.createSecret(); - await Kubernetes.createPersistentVolumeClaim(); - await Kubernetes.scheduleBuildJob(); - await Kubernetes.watchBuildJobUntilFinished(); - await Kubernetes.cleanup(); - - core.setOutput('volume', pvcName); - } - - static async createSecret() { - const secretManifest = { - apiVersion: 'v1', - kind: 'Secret', - metadata: { - name: this.secretName, - }, - type: 'Opaque', - data: { - GITHUB_TOKEN: base64.encode(this.buildParameters.githubToken), - UNITY_LICENSE: base64.encode(process.env.UNITY_LICENSE), - ANDROID_KEYSTORE_BASE64: base64.encode(this.buildParameters.androidKeystoreBase64), - ANDROID_KEYSTORE_PASS: base64.encode(this.buildParameters.androidKeystorePass), - ANDROID_KEYALIAS_PASS: base64.encode(this.buildParameters.androidKeyaliasPass), - }, - }; - await this.kubeClient.api.v1.namespaces(this.namespace).secrets.post({ body: secretManifest }); - } - - static async createPersistentVolumeClaim() { - if (this.buildParameters.kubeVolume) { - core.info(this.buildParameters.kubeVolume); - this.pvcName = this.buildParameters.kubeVolume; - return; - } - const pvcManifest = { - apiVersion: 'v1', - kind: 'PersistentVolumeClaim', - metadata: { - name: this.pvcName, - }, - spec: { - accessModes: ['ReadWriteOnce'], - volumeMode: 'Filesystem', - resources: { - requests: { - storage: this.buildParameters.kubeVolumeSize, - }, - }, - }, - }; - await this.kubeClient.api.v1.namespaces(this.namespace).persistentvolumeclaims.post({ body: pvcManifest }); - core.info('Persistent Volume created, waiting for ready state...'); - await Kubernetes.watchPersistentVolumeClaimUntilReady(); - core.info('Persistent Volume ready for claims'); - } - - static async watchPersistentVolumeClaimUntilReady() { - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - const queryResult = await this.kubeClient.api.v1 - .namespaces(this.namespace) - .persistentvolumeclaims(this.pvcName) - .get(); - if (queryResult.body.status.phase === 'Pending') { - await Kubernetes.watchPersistentVolumeClaimUntilReady(); - } - } - - static async scheduleBuildJob() { - core.info('Creating build job'); - const jobManifest = { - apiVersion: 'batch/v1', - kind: 'Job', - metadata: { - name: this.jobName, - labels: { - app: 'unity-builder', - }, - }, - spec: { - template: { - backoffLimit: 1, - spec: { - volumes: [ - { - name: 'data', - persistentVolumeClaim: { - claimName: this.pvcName, - }, - }, - { - name: 'credentials', - secret: { - secretName: this.secretName, - }, - }, - ], - initContainers: [ - { - name: 'clone', - image: 'alpine/git', - command: [ - '/bin/sh', - '-c', - `apk update; - apk add git-lfs; - export GITHUB_TOKEN=$(cat /credentials/GITHUB_TOKEN); - cd /data; - git clone https://github.com/${process.env.GITHUB_REPOSITORY}.git repo; - git clone https://github.com/webbertakken/unity-builder.git builder; - cd repo; - git checkout $GITHUB_SHA; - ls`, - ], - volumeMounts: [ - { - name: 'data', - mountPath: '/data', - }, - { - name: 'credentials', - mountPath: '/credentials', - readOnly: true, - }, - ], - env: [ - { - name: 'GITHUB_SHA', - value: this.buildId, - }, - ], - }, - ], - containers: [ - { - name: 'main', - image: `${this.baseImage.toString()}`, - command: [ - 'bin/bash', - '-c', - `for f in ./credentials/*; do export $(basename $f)="$(cat $f)"; done - cp -r /data/builder/action/default-build-script /UnityBuilderAction - cp -r /data/builder/action/entrypoint.sh /entrypoint.sh - cp -r /data/builder/action/steps /steps - chmod -R +x /entrypoint.sh; - chmod -R +x /steps; - /entrypoint.sh; - `, - ], - resources: { - requests: { - memory: this.buildParameters.kubeContainerMemory, - cpu: this.buildParameters.kubeContainerCPU, - }, - }, - env: [ - { - name: 'GITHUB_WORKSPACE', - value: '/data/repo', - }, - { - name: 'PROJECT_PATH', - value: this.buildParameters.projectPath, - }, - { - name: 'BUILD_PATH', - value: this.buildParameters.buildPath, - }, - { - name: 'BUILD_FILE', - value: this.buildParameters.buildFile, - }, - { - name: 'BUILD_NAME', - value: this.buildParameters.buildName, - }, - { - name: 'BUILD_METHOD', - value: this.buildParameters.buildMethod, - }, - { - name: 'CUSTOM_PARAMETERS', - value: this.buildParameters.customParameters, - }, - { - name: 'CHOWN_FILES_TO', - value: this.buildParameters.chownFilesTo, - }, - { - name: 'BUILD_TARGET', - value: this.buildParameters.platform, - }, - { - name: 'ANDROID_VERSION_CODE', - value: this.buildParameters.androidVersionCode.toString(), - }, - { - name: 'ANDROID_KEYSTORE_NAME', - value: this.buildParameters.androidKeystoreName, - }, - { - name: 'ANDROID_KEYALIAS_NAME', - value: this.buildParameters.androidKeyaliasName, - }, - ], - volumeMounts: [ - { - name: 'data', - mountPath: '/data', - }, - { - name: 'credentials', - mountPath: '/credentials', - readOnly: true, - }, - ], - lifeCycle: { - preStop: { - exec: { - command: [ - 'bin/bash', - '-c', - `cd /data/builder/action/steps; - chmod +x /return_license.sh; - /return_license.sh;`, - ], - }, - }, - }, - }, - ], - restartPolicy: 'Never', - }, - }, - }, - }; - await this.kubeClient.apis.batch.v1.namespaces(this.namespace).jobs.post({ body: jobManifest }); - core.info('Job created'); - } - - static async watchBuildJobUntilFinished() { - let podname; - let ready = false; - while (!ready) { - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - const pods = await this.kubeClient.api.v1.namespaces(this.namespace).pods.get(); - for (let index = 0; index < pods.body.items.length; index++) { - const element = pods.body.items[index]; - if (element.metadata.labels['job-name'] === this.jobName && element.status.phase !== 'Pending') { - core.info('Pod no longer pending'); - if (element.status.phase === 'Failure') { - core.error('Kubernetes job failed'); - } else { - ready = true; - podname = element.metadata.name; - } - } - } - } - - core.info(`Watching build job ${podname}`); - let logQueryTime; - let complete = false; - while (!complete) { - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - - const podStatus = await this.kubeClient.api.v1.namespaces(this.namespace).pod(podname).get(); - if (podStatus.body.status.phase !== 'Running') { - complete = true; - } - - const logs = await this.kubeClient.api.v1 - .namespaces(this.namespace) - .pod(podname) - .log.get({ - qs: { - sinceTime: logQueryTime, - timestamps: true, - }, - }); - if (logs.body !== undefined) { - const arrayOfLines = logs.body.match(/[^\n\r]+/g).reverse(); - for (const element of arrayOfLines) { - const [time, ...line] = element.split(' '); - if (time !== logQueryTime) { - core.info(line.join(' ')); - } else { - break; - } - } - - if (podStatus.body.status.phase === 'Failed') { - throw new Error('Kubernetes job failed'); - } - - logQueryTime = arrayOfLines[0].split(' ')[0]; - } - } - } - - static async cleanup() { - await this.kubeClient.apis.batch.v1.namespaces(this.namespace).jobs(this.jobName).delete(); - await this.kubeClient.api.v1.namespaces(this.namespace).secrets(this.secretName).delete(); - } - - static uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = Math.trunc(Math.random() * 16); - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } -} -export default Kubernetes; diff --git a/src/model/remote-builder/aws-build-platform.ts b/src/model/remote-builder/aws-build-platform.ts deleted file mode 100644 index b4e1262b..00000000 --- a/src/model/remote-builder/aws-build-platform.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as SDK from 'aws-sdk'; -import { customAlphabet } from 'nanoid'; -import RemoteBuilderSecret from './remote-builder-secret'; -import RemoteBuilderEnvironmentVariable from './remote-builder-environment-variable'; -import * as fs from 'fs'; -import * as core from '@actions/core'; -import RemoteBuilderTaskDef from './remote-builder-task-def'; -import RemoteBuilderConstants from './remote-builder-constants'; -import AWSBuildRunner from './aws-build-runner'; - -class AWSBuildEnvironment { - static async runBuild( - buildId: string, - stackName: string, - image: string, - commands: string[], - mountdir: string, - workingdir: string, - environment: RemoteBuilderEnvironmentVariable[], - secrets: RemoteBuilderSecret[], - ) { - const ECS = new SDK.ECS(); - const CF = new SDK.CloudFormation(); - const entrypoint = ['/bin/sh']; - - const taskDef = await this.setupCloudFormations( - CF, - buildId, - stackName, - image, - entrypoint, - commands, - mountdir, - workingdir, - secrets, - ); - try { - await AWSBuildRunner.runTask(taskDef, ECS, CF, environment, buildId); - } finally { - await this.cleanupResources(CF, taskDef); - } - } - - // static async setupPlatformResources() { - // throw new Error('Method not implemented.'); - // } - - static getParameterTemplate(p1) { - return ` - ${p1}: - Type: String - Default: '' -`; - } - - static getSecretTemplate(p1) { - return ` - ${p1}Secret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Join [ "", [ '${p1}', !Ref BUILDID ] ] - SecretString: !Ref ${p1} -`; - } - - static getSecretDefinitionTemplate(p1, p2) { - return ` - - Name: '${p1}' - ValueFrom: !Ref ${p2}Secret -`; - } - - static insertAtTemplate(template, insertionKey, insertion) { - const index = template.search(insertionKey) + insertionKey.length + '\n'.length; - template = [template.slice(0, index), insertion, template.slice(index)].join(''); - return template; - } - - static async setupCloudFormations( - CF: SDK.CloudFormation, - buildUid: string, - stackName: string, - image: string, - entrypoint: string[], - commands: string[], - mountdir: string, - workingdir: string, - secrets: RemoteBuilderSecret[], - ): Promise { - const logid = customAlphabet(RemoteBuilderConstants.alphabet, 9)(); - commands[1] += ` - echo "${logid}" - `; - const taskDefStackName = `${stackName}-${buildUid}`; - let taskDefCloudFormation = this.readTaskCloudFormationTemplate(); - const cleanupTaskDefStackName = `${taskDefStackName}-cleanup`; - const cleanupCloudFormation = fs.readFileSync(`${__dirname}/cloud-formations/cloudformation-stack-ttl.yml`, 'utf8'); - - try { - for (const secret of secrets) { - taskDefCloudFormation = this.insertAtTemplate( - taskDefCloudFormation, - 'p1 - input', - this.getParameterTemplate(secret.ParameterKey.replace(/[^\dA-Za-z]/g, '')), - ); - taskDefCloudFormation = this.insertAtTemplate( - taskDefCloudFormation, - 'p2 - secret', - this.getSecretTemplate(secret.ParameterKey.replace(/[^\dA-Za-z]/g, '')), - ); - taskDefCloudFormation = this.insertAtTemplate( - taskDefCloudFormation, - 'p3 - container def', - this.getSecretDefinitionTemplate(secret.EnvironmentVariable, secret.ParameterKey.replace(/[^\dA-Za-z]/g, '')), - ); - } - const mappedSecrets = secrets.map((x) => { - return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue }; - }); - - await CF.createStack({ - StackName: taskDefStackName, - TemplateBody: taskDefCloudFormation, - Parameters: [ - { - ParameterKey: 'ImageUrl', - ParameterValue: image, - }, - { - ParameterKey: 'ServiceName', - ParameterValue: taskDefStackName, - }, - { - ParameterKey: 'Command', - ParameterValue: commands.join(','), - }, - { - ParameterKey: 'EntryPoint', - ParameterValue: entrypoint.join(','), - }, - { - ParameterKey: 'WorkingDirectory', - ParameterValue: workingdir, - }, - { - ParameterKey: 'EFSMountDirectory', - ParameterValue: mountdir, - }, - { - ParameterKey: 'BUILDID', - ParameterValue: buildUid, - }, - ...mappedSecrets, - ], - }).promise(); - core.info('Creating worker cluster...'); - await CF.createStack({ - StackName: cleanupTaskDefStackName, - TemplateBody: cleanupCloudFormation, - Capabilities: ['CAPABILITY_IAM'], - Parameters: [ - { - ParameterKey: 'StackName', - ParameterValue: taskDefStackName, - }, - { - ParameterKey: 'DeleteStackName', - ParameterValue: cleanupTaskDefStackName, - }, - { - ParameterKey: 'TTL', - ParameterValue: '100', - }, - { - ParameterKey: 'BUILDID', - ParameterValue: buildUid, - }, - ], - }).promise(); - core.info('Creating cleanup cluster...'); - - await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise(); - } catch (error) { - await AWSBuildEnvironment.handleStackCreationFailure(error, CF, taskDefStackName, taskDefCloudFormation, secrets); - - throw error; - } - - const taskDefResources = ( - await CF.describeStackResources({ - StackName: taskDefStackName, - }).promise() - ).StackResources; - - const baseResources = (await CF.describeStackResources({ StackName: stackName }).promise()).StackResources; - - // in the future we should offer a parameter to choose if you want the guarnteed shutdown. - core.info('Worker cluster created successfully (skipping wait for cleanup cluster to be ready)'); - - return { - taskDefStackName, - taskDefCloudFormation, - taskDefStackNameTTL: cleanupTaskDefStackName, - ttlCloudFormation: cleanupCloudFormation, - taskDefResources, - baseResources, - logid, - }; - } - - private static async handleStackCreationFailure( - error: any, - CF: SDK.CloudFormation, - taskDefStackName: string, - taskDefCloudFormation: string, - secrets: RemoteBuilderSecret[], - ) { - core.info(JSON.stringify(secrets, undefined, 4)); - core.info(taskDefCloudFormation); - const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents; - const resources = (await CF.describeStackResources({ StackName: taskDefStackName }).promise()).StackResources; - core.info(JSON.stringify(events, undefined, 4)); - core.info(JSON.stringify(resources, undefined, 4)); - core.error(error); - } - - static readTaskCloudFormationTemplate(): string { - return fs.readFileSync(`${__dirname}/cloud-formations/task-def-formation.yml`, 'utf8'); - } - - static async cleanupResources(CF: SDK.CloudFormation, taskDef: RemoteBuilderTaskDef) { - core.info('Cleanup starting'); - await CF.deleteStack({ - StackName: taskDef.taskDefStackName, - }).promise(); - - await CF.deleteStack({ - StackName: taskDef.taskDefStackNameTTL, - }).promise(); - - await CF.waitFor('stackDeleteComplete', { - StackName: taskDef.taskDefStackName, - }).promise(); - - // Currently too slow and causes too much waiting - await CF.waitFor('stackDeleteComplete', { - StackName: taskDef.taskDefStackNameTTL, - }).promise(); - - core.info('Cleanup complete'); - } -} -export default AWSBuildEnvironment; diff --git a/src/model/remote-builder/aws-build-runner.ts b/src/model/remote-builder/aws-build-runner.ts deleted file mode 100644 index f52fd1a6..00000000 --- a/src/model/remote-builder/aws-build-runner.ts +++ /dev/null @@ -1,165 +0,0 @@ -import * as AWS from 'aws-sdk'; -import RemoteBuilderEnvironmentVariable from './remote-builder-environment-variable'; -import * as core from '@actions/core'; -import RemoteBuilderTaskDef from './remote-builder-task-def'; -import * as zlib from 'zlib'; - -class AWSBuildRunner { - static async runTask( - taskDef: RemoteBuilderTaskDef, - ECS: AWS.ECS, - CF: AWS.CloudFormation, - environment: RemoteBuilderEnvironmentVariable[], - buildUid: string, - ) { - const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || ''; - const taskDefinition = - taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || ''; - const SubnetOne = - taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || ''; - const SubnetTwo = - taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')?.PhysicalResourceId || ''; - const ContainerSecurityGroup = - taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')?.PhysicalResourceId || ''; - const streamName = - taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || ''; - - const task = await ECS.runTask({ - cluster, - taskDefinition, - platformVersion: '1.4.0', - overrides: { - containerOverrides: [ - { - name: taskDef.taskDefStackName, - environment: [...environment, { name: 'BUILDID', value: buildUid }], - }, - ], - }, - launchType: 'FARGATE', - networkConfiguration: { - awsvpcConfiguration: { - subnets: [SubnetOne, SubnetTwo], - assignPublicIp: 'ENABLED', - securityGroups: [ContainerSecurityGroup], - }, - }, - }).promise(); - - core.info('Task is starting on worker cluster'); - const taskArn = task.tasks?.[0].taskArn || ''; - - try { - await ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise(); - } catch (error) { - await new Promise((resolve) => setTimeout(resolve, 3000)); - const describeTasks = await ECS.describeTasks({ - tasks: [taskArn], - cluster, - }).promise(); - core.info(`Task has ended ${describeTasks.tasks?.[0].containers?.[0].lastStatus}`); - core.setFailed(error); - core.error(error); - } - core.info(`Task is running on worker cluster`); - await this.streamLogsUntilTaskStops(ECS, CF, taskDef, cluster, taskArn, streamName); - await ECS.waitFor('tasksStopped', { cluster, tasks: [taskArn] }).promise(); - const exitCode = ( - await ECS.describeTasks({ - tasks: [taskArn], - cluster, - }).promise() - ).tasks?.[0].containers?.[0].exitCode; - if (exitCode !== 0) { - core.error(`job failed with exit code ${exitCode}`); - throw new Error(`job failed with exit code ${exitCode}`); - } else { - core.info(`Task has finished successfully`); - } - } - - static async streamLogsUntilTaskStops( - ECS: AWS.ECS, - CF: AWS.CloudFormation, - taskDef: RemoteBuilderTaskDef, - clusterName: string, - taskArn: string, - kinesisStreamName: string, - ) { - // watching logs - const kinesis = new AWS.Kinesis(); - - const getTaskData = async () => { - const tasks = await ECS.describeTasks({ - cluster: clusterName, - tasks: [taskArn], - }).promise(); - return tasks.tasks?.[0]; - }; - - const stream = await kinesis - .describeStream({ - StreamName: kinesisStreamName, - }) - .promise(); - - let iterator = - ( - await kinesis - .getShardIterator({ - ShardIteratorType: 'TRIM_HORIZON', - StreamName: stream.StreamDescription.StreamName, - ShardId: stream.StreamDescription.Shards[0].ShardId, - }) - .promise() - ).ShardIterator || ''; - - await CF.waitFor('stackCreateComplete', { StackName: taskDef.taskDefStackNameTTL }).promise(); - - core.info(`Task status is ${(await getTaskData())?.lastStatus}`); - - const logBaseUrl = `https://${AWS.config.region}.console.aws.amazon.com/cloudwatch/home?region=${AWS.config.region}#logsV2:log-groups/log-group/${taskDef.taskDefStackName}`; - core.info(`You can also see the logs at AWS Cloud Watch: ${logBaseUrl}`); - - let readingLogs = true; - let timestamp: number = 0; - while (readingLogs) { - await new Promise((resolve) => setTimeout(resolve, 1500)); - const taskData = await getTaskData(); - if (taskData?.lastStatus !== 'RUNNING') { - if (timestamp === 0) { - core.info('Task stopped, streaming end of logs'); - timestamp = Date.now(); - } - if (timestamp !== 0 && Date.now() - timestamp < 30000) { - core.info('Task status is not RUNNING for 30 seconds, last query for logs'); - readingLogs = false; - } - } - const records = await kinesis - .getRecords({ - ShardIterator: iterator, - }) - .promise(); - iterator = records.NextShardIterator || ''; - if (records.Records.length > 0 && iterator) { - for (let index = 0; index < records.Records.length; index++) { - const json = JSON.parse( - zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'), - ); - if (json.messageType === 'DATA_MESSAGE') { - for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) { - if (json.logEvents[logEventsIndex].message.includes(taskDef.logid)) { - core.info('End of task logs'); - readingLogs = false; - } else { - core.info(json.logEvents[logEventsIndex].message); - } - } - } - } - } - } - } -} -export default AWSBuildRunner; diff --git a/src/model/remote-builder/remote-builder-constants.ts b/src/model/remote-builder/remote-builder-constants.ts deleted file mode 100644 index f28d02d6..00000000 --- a/src/model/remote-builder/remote-builder-constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -class RemoteBuilderConstants { - static alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; -} -export default RemoteBuilderConstants; diff --git a/src/model/remote-builder/remote-builder-environment-variable.ts b/src/model/remote-builder/remote-builder-environment-variable.ts deleted file mode 100644 index 212007a1..00000000 --- a/src/model/remote-builder/remote-builder-environment-variable.ts +++ /dev/null @@ -1,5 +0,0 @@ -class RemoteBuilderEnvironmentVariable { - public name!: string; - public value!: string; -} -export default RemoteBuilderEnvironmentVariable; diff --git a/src/model/remote-builder/remote-builder.ts b/src/model/remote-builder/remote-builder.ts deleted file mode 100644 index 88b0c50a..00000000 --- a/src/model/remote-builder/remote-builder.ts +++ /dev/null @@ -1,419 +0,0 @@ -import { customAlphabet } from 'nanoid'; -import AWSBuildPlatform from './aws-build-platform'; -import * as core from '@actions/core'; -import RemoteBuilderConstants from './remote-builder-constants'; -import { BuildParameters } from '..'; -const repositoryDirectoryName = 'repo'; -const efsDirectoryName = 'data'; -const cacheDirectoryName = 'cache'; - -class RemoteBuilder { - static SteamDeploy: boolean = false; - static async build(buildParameters: BuildParameters, baseImage) { - try { - this.SteamDeploy = process.env.STEAM_DEPLOY !== undefined || false; - const nanoid = customAlphabet(RemoteBuilderConstants.alphabet, 4); - const buildUid = `${process.env.GITHUB_RUN_NUMBER}-${buildParameters.platform - .replace('Standalone', '') - .replace('standalone', '')}-${nanoid()}`; - const defaultBranchName = - process.env.GITHUB_REF?.split('/') - .filter((x) => { - x = x[0].toUpperCase() + x.slice(1); - return x; - }) - .join('') || ''; - const branchName = - process.env.REMOTE_BUILDER_CACHE !== undefined ? process.env.REMOTE_BUILDER_CACHE : defaultBranchName; - const token: string = buildParameters.githubToken; - const defaultSecretsArray = [ - { - ParameterKey: 'GithubToken', - EnvironmentVariable: 'GITHUB_TOKEN', - ParameterValue: token, - }, - ]; - await RemoteBuilder.SetupStep(buildUid, buildParameters, branchName, defaultSecretsArray); - await RemoteBuilder.BuildStep(buildUid, buildParameters, baseImage, defaultSecretsArray); - await RemoteBuilder.CompressionStep(buildUid, buildParameters, branchName, defaultSecretsArray); - await RemoteBuilder.UploadArtifacts(buildUid, buildParameters, branchName, defaultSecretsArray); - if (this.SteamDeploy) { - await RemoteBuilder.DeployToSteam(buildUid, buildParameters, defaultSecretsArray); - } - } catch (error) { - core.setFailed(error); - core.error(error); - } - } - - private static async SetupStep( - buildUid: string, - buildParameters: BuildParameters, - branchName: string | undefined, - defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) { - core.info('Starting step 1/4 clone and restore cache)'); - await AWSBuildPlatform.runBuild( - buildUid, - buildParameters.awsStackName, - 'alpine/git', - [ - '-c', - `apk update; - apk add unzip; - apk add git-lfs; - apk add jq; - # Get source repo for project to be built and game-ci repo for utilties - git clone https://${buildParameters.githubToken}@github.com/${ - process.env.GITHUB_REPOSITORY - }.git ${buildUid}/${repositoryDirectoryName} -q - git clone https://${buildParameters.githubToken}@github.com/game-ci/unity-builder.git ${buildUid}/builder -q - git clone https://${buildParameters.githubToken}@github.com/game-ci/steam-deploy.git ${buildUid}/steam -q - cd /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/ - git checkout $GITHUB_SHA - cd /${efsDirectoryName}/ - # Look for usable cache - if [ ! -d ${cacheDirectoryName} ]; then - mkdir ${cacheDirectoryName} - fi - cd ${cacheDirectoryName} - if [ ! -d "${branchName}" ]; then - mkdir "${branchName}" - fi - cd "${branchName}" - echo '' - echo "Cached Libraries for ${branchName} from previous builds:" - ls - echo '' - ls "/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}" - libDir="/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library" - if [ -d "$libDir" ]; then - rm -r "$libDir" - echo "Setup .gitignore to ignore Library folder and remove it from builds" - fi - echo 'Checking cache' - # Restore cache - latest=$(ls -t | head -1) - if [ ! -z "$latest" ]; then - echo "Library cache exists from build $latest from ${branchName}" - echo 'Creating empty Library folder for cache' - mkdir $libDir - unzip -q $latest -d $libDir - # purge cache - ${process.env.PURGE_REMOTE_BUILDER_CACHE === undefined ? '#' : ''} rm -r $libDir - else - echo 'Cache does not exist' - fi - # Print out important directories - echo '' - echo 'Repo:' - ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/ - echo '' - echo 'Project:' - ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath} - echo '' - echo 'Library:' - ls /${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}/Library/ - echo '' - `, - ], - `/${efsDirectoryName}`, - `/${efsDirectoryName}/`, - [ - { - name: 'GITHUB_SHA', - value: process.env.GITHUB_SHA || '', - }, - ], - defaultSecretsArray, - ); - } - - private static async BuildStep( - buildUid: string, - buildParameters: BuildParameters, - baseImage: any, - defaultSecretsArray: any[], - ) { - const buildSecrets = new Array(); - - buildSecrets.push(...defaultSecretsArray); - - if (process.env.UNITY_LICENSE) - buildSecrets.push({ - ParameterKey: 'UnityLicense', - EnvironmentVariable: 'UNITY_LICENSE', - ParameterValue: process.env.UNITY_LICENSE, - }); - - if (process.env.UNITY_EMAIL) - buildSecrets.push({ - ParameterKey: 'UnityEmail', - EnvironmentVariable: 'UNITY_EMAIL', - ParameterValue: process.env.UNITY_EMAIL, - }); - - if (process.env.UNITY_PASSWORD) - buildSecrets.push({ - ParameterKey: 'UnityPassword', - EnvironmentVariable: 'UNITY_PASSWORD', - ParameterValue: process.env.UNITY_PASSWORD, - }); - - if (process.env.UNITY_SERIAL) - buildSecrets.push({ - ParameterKey: 'UnitySerial', - EnvironmentVariable: 'UNITY_SERIAL', - ParameterValue: process.env.UNITY_SERIAL, - }); - - if (buildParameters.androidKeystoreBase64) - buildSecrets.push({ - ParameterKey: 'AndroidKeystoreBase64', - EnvironmentVariable: 'ANDROID_KEYSTORE_BASE64', - ParameterValue: buildParameters.androidKeystoreBase64, - }); - - if (buildParameters.androidKeystorePass) - buildSecrets.push({ - ParameterKey: 'AndroidKeystorePass', - EnvironmentVariable: 'ANDROID_KEYSTORE_PASS', - ParameterValue: buildParameters.androidKeystorePass, - }); - - if (buildParameters.androidKeyaliasPass) - buildSecrets.push({ - ParameterKey: 'AndroidKeyAliasPass', - EnvironmentVariable: 'AWS_ACCESS_KEY_ALIAS_PASS', - ParameterValue: buildParameters.androidKeyaliasPass, - }); - core.info('Starting part 2/4 (build unity project)'); - await AWSBuildPlatform.runBuild( - buildUid, - buildParameters.awsStackName, - baseImage.toString(), - [ - '-c', - ` - cp -r /${efsDirectoryName}/${buildUid}/builder/dist/default-build-script/ /UnityBuilderAction; - cp -r /${efsDirectoryName}/${buildUid}/builder/dist/entrypoint.sh /entrypoint.sh; - cp -r /${efsDirectoryName}/${buildUid}/builder/dist/steps/ /steps; - chmod -R +x /entrypoint.sh; - chmod -R +x /steps; - /entrypoint.sh; - `, - ], - `/${efsDirectoryName}`, - `/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/`, - [ - { - name: 'ContainerMemory', - value: buildParameters.remoteBuildMemory, - }, - { - name: 'ContainerCpu', - value: buildParameters.remoteBuildCpu, - }, - { - name: 'GITHUB_WORKSPACE', - value: `/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/`, - }, - { - name: 'PROJECT_PATH', - value: buildParameters.projectPath, - }, - { - name: 'BUILD_PATH', - value: buildParameters.buildPath, - }, - { - name: 'BUILD_FILE', - value: buildParameters.buildFile, - }, - { - name: 'BUILD_NAME', - value: buildParameters.buildName, - }, - { - name: 'BUILD_METHOD', - value: buildParameters.buildMethod, - }, - { - name: 'CUSTOM_PARAMETERS', - value: buildParameters.customParameters, - }, - { - name: 'BUILD_TARGET', - value: buildParameters.platform, - }, - { - name: 'ANDROID_VERSION_CODE', - value: buildParameters.androidVersionCode.toString(), - }, - { - name: 'ANDROID_KEYSTORE_NAME', - value: buildParameters.androidKeystoreName, - }, - { - name: 'ANDROID_KEYALIAS_NAME', - value: buildParameters.androidKeyaliasName, - }, - ], - buildSecrets, - ); - } - - private static async CompressionStep( - buildUid: string, - buildParameters: BuildParameters, - branchName: string | undefined, - defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) { - core.info('Starting step 3/4 build compression'); - // Cleanup - await AWSBuildPlatform.runBuild( - buildUid, - buildParameters.awsStackName, - 'alpine', - [ - '-c', - ` - apk update - apk add zip - cd Library - zip -r lib-${buildUid}.zip .* - mv lib-${buildUid}.zip /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip - cd ../../ - zip -r build-${buildUid}.zip ${buildParameters.buildPath}/* - mv build-${buildUid}.zip /${efsDirectoryName}/${buildUid}/build-${buildUid}.zip - `, - ], - `/${efsDirectoryName}`, - `/${efsDirectoryName}/${buildUid}/${repositoryDirectoryName}/${buildParameters.projectPath}`, - [ - { - name: 'GITHUB_SHA', - value: process.env.GITHUB_SHA || '', - }, - ], - defaultSecretsArray, - ); - core.info('compression step complete'); - } - - private static async UploadArtifacts( - buildUid: string, - buildParameters: BuildParameters, - branchName: string | undefined, - defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) { - core.info('Starting step 4/4 upload build to s3'); - await AWSBuildPlatform.runBuild( - buildUid, - buildParameters.awsStackName, - 'amazon/aws-cli', - [ - '-c', - ` - aws s3 cp ${buildUid}/build-${buildUid}.zip s3://game-ci-storage/ - # no need to upload Library cache for now - # aws s3 cp /${efsDirectoryName}/${cacheDirectoryName}/${branchName}/lib-${buildUid}.zip s3://game-ci-storage/ - ${this.SteamDeploy ? '#' : ''} rm -r ${buildUid} - `, - ], - `/${efsDirectoryName}`, - `/${efsDirectoryName}/`, - [ - { - name: 'GITHUB_SHA', - value: process.env.GITHUB_SHA || '', - }, - { - name: 'AWS_DEFAULT_REGION', - value: process.env.AWS_DEFAULT_REGION || '', - }, - ], - [ - { - ParameterKey: 'AWSAccessKeyID', - EnvironmentVariable: 'AWS_ACCESS_KEY_ID', - ParameterValue: process.env.AWS_ACCESS_KEY_ID || '', - }, - { - ParameterKey: 'AWSSecretAccessKey', - EnvironmentVariable: 'AWS_SECRET_ACCESS_KEY', - ParameterValue: process.env.AWS_SECRET_ACCESS_KEY || '', - }, - ...defaultSecretsArray, - ], - ); - } - - private static async DeployToSteam( - buildUid: string, - buildParameters: BuildParameters, - defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) { - core.info('Starting steam deployment'); - await AWSBuildPlatform.runBuild( - buildUid, - buildParameters.awsStackName, - 'cm2network/steamcmd:root', - [ - '-c', - ` - ls - ls / - cp -r /${efsDirectoryName}/${buildUid}/steam/action/entrypoint.sh /entrypoint.sh; - cp -r /${efsDirectoryName}/${buildUid}/steam/action/steps/ /steps; - chmod -R +x /entrypoint.sh; - chmod -R +x /steps; - /entrypoint.sh; - rm -r /${efsDirectoryName}/${buildUid} - `, - ], - `/${efsDirectoryName}`, - `/${efsDirectoryName}/${buildUid}/steam/action/`, - [ - { - name: 'GITHUB_SHA', - value: process.env.GITHUB_SHA || '', - }, - ], - [ - { - EnvironmentVariable: 'INPUT_APPID', - ParameterKey: 'appId', - ParameterValue: process.env.APP_ID || '', - }, - { - EnvironmentVariable: 'INPUT_BUILDDESCRIPTION', - ParameterKey: 'buildDescription', - ParameterValue: process.env.BUILD_DESCRIPTION || '', - }, - { - EnvironmentVariable: 'INPUT_ROOTPATH', - ParameterKey: 'rootPath', - ParameterValue: process.env.ROOT_PATH || '', - }, - { - EnvironmentVariable: 'INPUT_RELEASEBRANCH', - ParameterKey: 'releaseBranch', - ParameterValue: process.env.RELEASE_BRANCH || '', - }, - { - EnvironmentVariable: 'INPUT_LOCALCONTENTSERVER', - ParameterKey: 'localContentServer', - ParameterValue: process.env.LOCAL_CONTENT_SERVER || '', - }, - { - EnvironmentVariable: 'INPUT_PREVIEWENABLED', - ParameterKey: 'previewEnabled', - ParameterValue: process.env.PREVIEW_ENABLED || '', - }, - ...defaultSecretsArray, - ], - ); - } -} -export default RemoteBuilder; diff --git a/src/model/system.ts b/src/model/system.ts index 020557dc..c9f0c8bf 100644 --- a/src/model/system.ts +++ b/src/model/system.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core'; import { exec } from '@actions/exec'; class System { - static async run(command, arguments_: any = [], options = {}) { + static async run(command, arguments_: any = [], options = {}, shouldLog = true) { let result = ''; let error = ''; let debug = ''; @@ -20,15 +20,15 @@ class System { }; const showOutput = () => { - if (debug !== '') { + if (debug !== '' && shouldLog) { core.debug(debug); } - if (result !== '') { + if (result !== '' && shouldLog) { core.info(result); } - if (error !== '') { + if (error !== '' && shouldLog) { core.warning(error); } }; diff --git a/test-project/Assets/LFS_Test_File.jpg b/test-project/Assets/LFS_Test_File.jpg new file mode 100755 index 00000000..df596e4c --- /dev/null +++ b/test-project/Assets/LFS_Test_File.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06ca06aa412dabebde872ff580b2ae9a812dea77918378462d69fe094fee1bd8 +size 669 diff --git a/tsconfig.json b/tsconfig.json index 24ae1733..c1ae307a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,13 @@ { "compilerOptions": { - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Re-enable after fixing compatibility */ /* Raise error on expressions and declarations with an implied 'any' type. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "experimentalDecorators": true, + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "outDir": "./lib" /* Redirect output structure to the directory. */, + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": false /* Re-enable after fixing compatibility */ /* Raise error on expressions and declarations with an implied 'any' type. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ }, "exclude": ["node_modules", "**/*.test.ts"] } diff --git a/yarn.lock b/yarn.lock index cadacaa9..3e060452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -344,6 +344,18 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@eslint/eslintrc@^0.4.0": version "0.4.0" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz" @@ -546,6 +558,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5" + integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@kubernetes/client-node@0.10.2": version "0.10.2" resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.10.2.tgz" @@ -566,6 +589,34 @@ underscore "^1.9.1" ws "^6.1.0" +"@kubernetes/client-node@^0.14.3": + version "0.14.3" + resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.14.3.tgz#5ed9b88873419080547f22cb74eb502bf6671fca" + integrity sha512-9hHGDNm2JEFQcRTpDxVoAVr0fowU+JH/l5atCXY9VXwvFM18pW5wr2LzLP+Q2Rh+uQv7Moz4gEjEKSCgVKykEQ== + dependencies: + "@types/js-yaml" "^3.12.1" + "@types/node" "^10.12.0" + "@types/request" "^2.47.1" + "@types/stream-buffers" "^3.0.3" + "@types/tar" "^4.0.3" + "@types/underscore" "^1.8.9" + "@types/ws" "^6.0.1" + byline "^5.0.0" + execa "1.0.0" + isomorphic-ws "^4.0.1" + js-yaml "^3.13.1" + jsonpath-plus "^0.19.0" + openid-client "^4.1.1" + request "^2.88.0" + rfc4648 "^1.3.0" + shelljs "^0.8.2" + stream-buffers "^3.0.2" + tar "^6.0.2" + tmp-promise "^3.0.2" + tslib "^1.9.3" + underscore "^1.9.1" + ws "^7.3.1" + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz" @@ -775,6 +826,11 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sindresorhus/is@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5" + integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" @@ -796,6 +852,33 @@ dependencies: defer-to-connect "^1.0.1" +"@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz" @@ -829,15 +912,25 @@ dependencies: "@babel/types" "^7.3.0" +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + "@types/caseless@*": version "0.12.2" resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz" integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== "@types/got@^9.6.9": - version "9.6.11" - resolved "https://registry.npmjs.org/@types/got/-/got-9.6.11.tgz" - integrity sha512-dr3IiDNg5TDesGyuwTrN77E1Cd7DCdmCFtEfSGqr83jMMtcwhf/SGPbN2goY4JUWQfvxwY56+e5tjfi+oXeSdA== + version "9.6.12" + resolved "https://registry.yarnpkg.com/@types/got/-/got-9.6.12.tgz#fd42a6e1f5f64cd6bb422279b08c30bb5a15a56f" + integrity sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA== dependencies: "@types/node" "*" "@types/tough-cookie" "*" @@ -850,6 +943,11 @@ dependencies: "@types/node" "*" +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz" @@ -869,13 +967,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.15": - version "26.0.22" - resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz" - integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw== +"@types/jest@^27.0.3": + version "27.0.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a" + integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg== dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" + jest-diff "^27.0.0" + pretty-format "^27.0.0" "@types/js-yaml@^3.12.1": version "3.12.6" @@ -892,6 +990,20 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + +"@types/minipass@*": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651" + integrity sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>= 8", "@types/node@^14.14.9": version "14.14.41" resolved "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz" @@ -922,6 +1034,13 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/semver@^7.3.5": version "7.3.9" resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz" @@ -932,6 +1051,21 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/stream-buffers@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b" + integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ== + dependencies: + "@types/node" "*" + +"@types/tar@^4.0.3": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.4.tgz#d680de60855e7778a51c672b755869a3b8d2889f" + integrity sha512-0Xv+xcmkTsOZdIF4yCnd7RkOOyfyqPaqJ7RZFKnwdxfDbkN3eAAE9sHl8zJFqBz4VhxolW9EErbjR1oyH7jK2A== + dependencies: + "@types/minipass" "*" + "@types/node" "*" + "@types/tough-cookie@*": version "4.0.0" resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz" @@ -961,6 +1095,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^4.20.0": version "4.22.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz" @@ -1059,6 +1200,11 @@ acorn-walk@^7.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" @@ -1069,7 +1215,12 @@ acorn@^8.1.0: resolved "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz" integrity sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g== -aggregate-error@^3.0.0: +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + +aggregate-error@^3.0.0, aggregate-error@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== @@ -1109,7 +1260,7 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: dependencies: type-fest "^0.21.3" -ansi-regex@^5.0.0: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -1133,6 +1284,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-styles@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz" @@ -1154,6 +1310,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" @@ -1233,6 +1394,11 @@ async-limiter@~1.0.0: resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-wait-until@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/async-wait-until/-/async-wait-until-2.0.7.tgz#ed4ccfe076850105c1de555381630b9fad882f5d" + integrity sha512-SjHxM2f5ev4o87gYppr8HmWPjOHw06Pg5KZvkSl6FMqa3TTHzDGIWCZx61XWjxO5ArPcZBuJYbAa809FNyx3QQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -1336,7 +1502,7 @@ babel-preset-jest@^26.6.2: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base-64@^1.0.0: @@ -1386,7 +1552,7 @@ before-after-hook@^2.2.0: brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -1450,7 +1616,12 @@ btoa-lite@^1.0.0: resolved "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buffer-from@1.x, buffer-from@^1.0.0: +buffer-from@1.x: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -1464,6 +1635,11 @@ buffer@4.9.2: ieee754 "^1.1.4" isarray "^1.0.0" +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz" @@ -1479,6 +1655,11 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" @@ -1492,6 +1673,19 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -1554,6 +1748,11 @@ char-regex@^1.0.2: resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" @@ -1684,6 +1883,18 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander-ts@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/commander-ts/-/commander-ts-0.2.0.tgz#14391337c1c725399cdfca5717da8e4fd0688eda" + integrity sha512-9XaUF3/3nmVtkDmAkijjhgEcwrMwKewaAJtN+GyTJBG3CJ5DfGB/JsXCVZcBW6SZB+QqiJxbK3e2/62tweMO8g== + dependencies: + commander "^7.2.0" + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^8.3.0: version "8.3.0" resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" @@ -1696,7 +1907,7 @@ component-emitter@^1.2.1: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= contains-path@^0.1.0: @@ -1721,6 +1932,18 @@ core-util-is@1.0.2: resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" @@ -1732,7 +1955,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1810,6 +2033,13 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" @@ -1830,6 +2060,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" @@ -1884,6 +2119,16 @@ diff-sequences@^26.6.2: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" + integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -2258,7 +2503,7 @@ exec-sh@^0.3.2: resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz" integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== -execa@^1.0.0: +execa@1.0.0, execa@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz" integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== @@ -2508,9 +2753,16 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^2.1.2: @@ -2520,7 +2772,7 @@ fsevents@^2.1.2: function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== functional-red-black-tree@^1.0.1: @@ -2591,9 +2843,9 @@ glob-parent@^5.0.0, glob-parent@^5.1.0: is-glob "^4.0.1" glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2633,6 +2885,23 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" +got@^11.8.0: + version "11.8.2" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" + integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" @@ -2726,7 +2995,7 @@ has-values@^1.0.0: has@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" @@ -2762,6 +3031,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz" @@ -2837,7 +3114,7 @@ indent-string@^4.0.0: inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" @@ -2845,12 +3122,12 @@ inflight@^1.0.4: inherits@2: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== interpret@^1.0.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== is-accessor-descriptor@^0.1.6: @@ -3239,7 +3516,7 @@ jest-config@^26.6.3: micromatch "^4.0.2" pretty-format "^26.6.2" -jest-diff@^26.0.0, jest-diff@^26.6.2: +jest-diff@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz" integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== @@ -3249,6 +3526,16 @@ jest-diff@^26.0.0, jest-diff@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-diff@^27.0.0: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.2.tgz#786b2a5211d854f848e2dcc1e324448e9481f36f" + integrity sha512-ujc9ToyUZDh9KcqvQDkk/gkbf6zSaeEg9AiBxtttXW59H/AcqEYp1ciXAtJp+jXWva5nAf/ePtSsgWwE5mqp4Q== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.4.0" + jest-get-type "^27.4.0" + pretty-format "^27.4.2" + jest-docblock@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz" @@ -3297,6 +3584,11 @@ jest-get-type@^26.3.0: resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5" + integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== + jest-haste-map@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz" @@ -3572,6 +3864,13 @@ jose@^1.27.1: dependencies: "@panva/asn1.js" "^1.0.0" +jose@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.5.tgz#29746a18d9fff7dcf9d5d2a6f62cb0c7cd27abd3" + integrity sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA== + dependencies: + "@panva/asn1.js" "^1.0.0" + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -3632,6 +3931,11 @@ json-buffer@3.0.0: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" @@ -3703,6 +4007,13 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" + integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + dependencies: + json-buffer "3.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" @@ -3915,7 +4226,7 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@1.x, make-error@^1.3.6: +make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -3998,9 +4309,14 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + minimatch@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" @@ -4010,6 +4326,21 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" @@ -4018,7 +4349,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@1.x: +mkdirp@1.x, mkdirp@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -4218,7 +4549,7 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== -oidc-token-hash@^5.0.0: +oidc-token-hash@^5.0.0, oidc-token-hash@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz" integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ== @@ -4252,6 +4583,19 @@ openid-client@^3.14.0: oidc-token-hash "^5.0.0" p-any "^3.0.0" +openid-client@^4.1.1: + version "4.7.3" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-4.7.3.tgz#ea4f6f9ff6203dfbbe3d9bb4415d5dce751b0a70" + integrity sha512-YLwZQLSjo3gdSVxw/G25ddoRp9oCpXkREZXssmenlejZQPsnTq+yQtFUcBmC7u3VVkx+gwqXZF7X0CtAAJrRRg== + dependencies: + aggregate-error "^3.1.0" + got "^11.8.0" + jose "^2.0.5" + lru-cache "^6.0.0" + make-error "^1.3.6" + object-hash "^2.0.1" + oidc-token-hash "^5.0.1" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" @@ -4411,7 +4755,7 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-key@^2.0.0, path-key@^2.0.1: @@ -4426,7 +4770,7 @@ path-key@^3.0.0, path-key@^3.1.0: path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-type@^2.0.0: @@ -4514,7 +4858,7 @@ prettier@^2.2.1: resolved "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== -pretty-format@^26.0.0, pretty-format@^26.6.2: +pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -4524,6 +4868,16 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.0, pretty-format@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.2.tgz#e4ce92ad66c3888423d332b40477c87d1dac1fb8" + integrity sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw== + dependencies: + "@jest/types" "^27.4.2" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + progress@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" @@ -4582,6 +4936,11 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + react-is@^17.0.1: version "17.0.2" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" @@ -4625,11 +4984,16 @@ read-pkg@^5.2.0: rechoir@^0.6.2: version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= dependencies: resolve "^1.1.6" +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz" @@ -4725,6 +5089,11 @@ reserved-words@^0.1.2: resolved "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz" integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE= +resolve-alpn@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.1.2.tgz#30b60cfbb0c0b8dc897940fe13fe255afcdd4d28" + integrity sha512-8OyfzhAtA32LVUsJSke3auIyINcwdh5l3cvYKdKO0nvsYSKuiLfTM5i78PJswFPT8y6cPW+L1v6/hE95chcpDA== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -4771,6 +5140,13 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" @@ -4789,6 +5165,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc4648@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" + integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== + rfdc@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" @@ -4815,7 +5196,7 @@ run-parallel@^1.1.9: rxjs@^7.4.0: version "7.4.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w== dependencies: tslib "~2.1.0" @@ -4938,9 +5319,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shelljs@^0.8.2: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + version "0.8.4" + resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -5143,6 +5524,11 @@ stealthy-require@^1.1.1: resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + string-argv@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" @@ -5295,6 +5681,18 @@ table@^6.0.4: slice-ansi "^4.0.0" string-width "^4.2.0" +tar@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" @@ -5327,6 +5725,20 @@ through@^2.3.8: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tmp-promise@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a" + integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA== + dependencies: + tmp "^0.2.0" + +tmp@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.x: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -5403,10 +5815,10 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -ts-jest@^26.4.4: - version "26.5.5" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.5.tgz" - integrity sha512-7tP4m+silwt1NHqzNRAPjW1BswnAhopTdc2K3HEkRZjF0ZG2F/e/ypVH0xiZIMfItFtD3CX0XFbwPzp9fIEUVg== +ts-jest@^26.4.2: + version "26.5.6" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.6.tgz#c32e0746425274e1dfe333f43cd3c800e014ec35" + integrity sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -5419,6 +5831,24 @@ ts-jest@^26.4.4: semver "7.x" yargs-parser "20.x" +ts-node@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + yn "3.1.1" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz" @@ -5774,7 +6204,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^3.0.0: @@ -5799,6 +6229,11 @@ ws@^7.2.3, ws@^7.4.4: resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz" integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== +ws@^7.3.1: + version "7.4.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" + integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" @@ -5866,3 +6301,8 @@ yargs@^15.4.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==