mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-01-30 04:49:08 +08:00
Compare commits
41 Commits
v2.0.2
...
davidmfino
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e53060e37 | ||
|
|
8c9dcf076d | ||
|
|
f35829a9d4 | ||
|
|
fb5b03c3b8 | ||
|
|
584c0366c6 | ||
|
|
28147e5e1b | ||
|
|
00c5685d03 | ||
|
|
e334dc785a | ||
|
|
a45155c578 | ||
|
|
f68f14ff9f | ||
|
|
9329b7369c | ||
|
|
5bd589e19f | ||
|
|
f03bee03f6 | ||
|
|
2a32a9d870 | ||
|
|
d6ac850da1 | ||
|
|
f300dd27bb | ||
|
|
4cca069ebb | ||
|
|
96555a0945 | ||
|
|
4cb3e593f5 | ||
|
|
9f79830454 | ||
|
|
f054aaa35d | ||
|
|
0756841374 | ||
|
|
579daa93a6 | ||
|
|
1adfabf512 | ||
|
|
cdee7d1d9a | ||
|
|
fb633660a4 | ||
|
|
c15f955669 | ||
|
|
2684be2dee | ||
|
|
db2d8b6dbd | ||
|
|
c068855899 | ||
|
|
5b8bf1c8b7 | ||
|
|
47b25cf3b1 | ||
|
|
36891ec921 | ||
|
|
2652cb78a7 | ||
|
|
f77696efae | ||
|
|
4556fc4ff1 | ||
|
|
d066039c26 | ||
|
|
8abce48a48 | ||
|
|
5ae03dfef6 | ||
|
|
4be5d2ddf4 | ||
|
|
a61c02481f |
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint", "prettier", "unicorn"],
|
||||
"extends": ["plugin:unicorn/recommended", "plugin:github/recommended", "prettier"],
|
||||
"extends": ["plugin:unicorn/recommended", "plugin:github/recommended", "plugin:prettier/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module"
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"extraFileExtensions": [".mjs"],
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
},
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
@@ -12,9 +17,44 @@
|
||||
"jest/globals": true
|
||||
},
|
||||
"rules": {
|
||||
// Error out for code formatting errors
|
||||
"prettier/prettier": "error",
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
// Namespaces or sometimes needed
|
||||
"import/no-namespace": "off",
|
||||
"no-undef": "off" // TODO: REMOVE THIS LINE WHEN UPDATING ESLINT RULES
|
||||
// Properly format comments
|
||||
"spaced-comment": ["error", "always"],
|
||||
"lines-around-comment": [
|
||||
"error",
|
||||
{
|
||||
"beforeBlockComment": true,
|
||||
"beforeLineComment": true,
|
||||
"allowBlockStart": true,
|
||||
"allowObjectStart": true,
|
||||
"allowArrayStart": true,
|
||||
"allowClassStart": true,
|
||||
"ignorePattern": "pragma|ts-ignore"
|
||||
}
|
||||
],
|
||||
// Mandatory spacing
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||
{ "blankLine": "always", "prev": "directive", "next": "*" },
|
||||
{ "blankLine": "any", "prev": "directive", "next": "directive" }
|
||||
],
|
||||
// Enforce camelCase
|
||||
"camelcase": "error",
|
||||
// Allow forOfStatements
|
||||
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
|
||||
// Continue is viable in forOf loops in generators
|
||||
"no-continue": "off",
|
||||
// From experience, named exports are almost always desired. I got tired of this rule
|
||||
"import/prefer-default-export": "off",
|
||||
// Unused vars are useful to keep method signatures consistent and documented
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
// For this project only use kebab-case
|
||||
"unicorn/filename-case": ["error", { "cases": { "kebabCase": true } }],
|
||||
// Allow Array.from(set) mitigate TS2569 which would require '--downlevelIteration'
|
||||
"unicorn/prefer-spread": "off"
|
||||
}
|
||||
}
|
||||
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,8 +1,8 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: game-ci
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: game-ci
|
||||
open_collective: # replace with a single OpenCollective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
|
||||
35
.github/workflows/build-tests.yml
vendored
35
.github/workflows/build-tests.yml
vendored
@@ -7,7 +7,30 @@ on:
|
||||
- '.github/**'
|
||||
|
||||
env:
|
||||
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\" Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature Value=\"62\"/>\n </Features>\n <DeveloperData Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\" Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
|
||||
UNITY_LICENSE:
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
|
||||
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
|
||||
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
|
||||
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
|
||||
Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash
|
||||
Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature
|
||||
Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature
|
||||
Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature
|
||||
Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature
|
||||
Value=\"62\"/>\n </Features>\n <DeveloperData
|
||||
Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked
|
||||
Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate
|
||||
Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate
|
||||
Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion
|
||||
Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement
|
||||
Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\"
|
||||
ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\"
|
||||
Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature
|
||||
xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod
|
||||
Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod
|
||||
Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform
|
||||
Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod
|
||||
Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
|
||||
|
||||
jobs:
|
||||
buildForAllPlatformsUbuntu:
|
||||
@@ -16,6 +39,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
# - local-docker
|
||||
- local
|
||||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
@@ -38,14 +64,14 @@ jobs:
|
||||
###########################
|
||||
# Checkout #
|
||||
###########################
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Cache #
|
||||
###########################
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}
|
||||
@@ -62,11 +88,12 @@ jobs:
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
|
||||
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
|
||||
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Build Ubuntu (${{ matrix.unityVersion }})
|
||||
path: build
|
||||
|
||||
2
.github/workflows/cats.yml
vendored
2
.github/workflows/cats.yml
vendored
@@ -11,6 +11,6 @@ jobs:
|
||||
name: A cat for your effort!
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ruairidhwm/action-cats@1.0.1
|
||||
- uses: ruairidhwm/action-cats@1.0.2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
23
.github/workflows/cleanup.yml
vendored
23
.github/workflows/cleanup.yml
vendored
@@ -12,3 +12,26 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
expire-in: 21 days
|
||||
cleanupCloudRunner:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
with:
|
||||
lfs: true
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: yarn
|
||||
- run: yarn run cli --help
|
||||
env:
|
||||
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
|
||||
- run: yarn run cli -m list-resources
|
||||
env:
|
||||
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
|
||||
|
||||
58
.github/workflows/cloud-runner-async-checks.yml
vendored
Normal file
58
.github/workflows/cloud-runner-async-checks.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Async Checks API
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
checksObject:
|
||||
description: ''
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
|
||||
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: ${{ github.ref }}
|
||||
CLOUD_RUNNER_DEBUG: true
|
||||
CLOUD_RUNNER_DEBUG_TREE: true
|
||||
DEBUG: true
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: test-project
|
||||
UNITY_VERSION: 2019.3.15f1
|
||||
USE_IL2CPP: false
|
||||
|
||||
jobs:
|
||||
asyncChecks:
|
||||
name: Async Checks
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: false
|
||||
- run: yarn
|
||||
- run: yarn run cli -m checks-update
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: test-project
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
CLOUD_RUNNER_CLUSTER: local-docker
|
||||
AWS_BASE_STACK_NAME: game-ci-github-pipelines
|
||||
CHECKS_UPDATE: ${{ github.event.inputs.checksObject }}
|
||||
111
.github/workflows/cloud-runner-aws-pipeline.yml
vendored
111
.github/workflows/cloud-runner-aws-pipeline.yml
vendored
@@ -1,111 +0,0 @@
|
||||
name: Cloud Runner - AWS Tests
|
||||
|
||||
on:
|
||||
push: { branches: [main, cloud-runner-develop] }
|
||||
|
||||
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: ${{ github.ref }}
|
||||
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/$CACHE_KEY
|
||||
echo "/data/cache/$CACHE_KEY/build-$BUILD_GUID.zip s3://game-ci-test-storage/$CACHE_KEY/$BUILD_FILE"
|
||||
aws s3 cp /data/cache/$CACHE_KEY/build-$BUILD_GUID.zip s3://game-ci-test-storage/$CACHE_KEY/build-$BUILD_GUID.zip
|
||||
aws s3 cp /data/cache/$CACHE_KEY s3://game-ci-test-storage/$CACHE_KEY/$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
|
||||
125
.github/workflows/cloud-runner-k8s-pipeline.yml
vendored
125
.github/workflows/cloud-runner-k8s-pipeline.yml
vendored
@@ -1,125 +0,0 @@
|
||||
name: Cloud Runner - K8s Tests
|
||||
|
||||
on:
|
||||
push: { branches: [cloud-runner-develop] }
|
||||
# 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: ${{ github.ref }}
|
||||
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
|
||||
133
.github/workflows/cloud-runner-pipeline.yml
vendored
Normal file
133
.github/workflows/cloud-runner-pipeline.yml
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
name: Cloud Runner CI Pipeline
|
||||
|
||||
on:
|
||||
push: { branches: [cloud-runner-develop, cloud-runner-preview, main] }
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
env:
|
||||
GKE_ZONE: 'us-central1'
|
||||
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-team-pipelines
|
||||
CLOUD_RUNNER_BRANCH: ${{ github.ref }}
|
||||
CLOUD_RUNNER_DEBUG: true
|
||||
CLOUD_RUNNER_DEBUG_TREE: true
|
||||
DEBUG: true
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: test-project
|
||||
UNITY_VERSION: 2019.3.15f1
|
||||
USE_IL2CPP: false
|
||||
USE_GKE_GCLOUD_AUTH_PLUGIN: true
|
||||
|
||||
jobs:
|
||||
integrationTests:
|
||||
name: Integration Tests
|
||||
if: github.event.event_type != 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
- aws
|
||||
- local-docker
|
||||
- k8s
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: false
|
||||
- uses: google-github-actions/auth@v1
|
||||
with:
|
||||
credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||
- name: 'Set up Cloud SDK'
|
||||
uses: 'google-github-actions/setup-gcloud@v1'
|
||||
- name: Get GKE cluster credentials
|
||||
run: |
|
||||
export USE_GKE_GCLOUD_AUTH_PLUGIN=True
|
||||
gcloud components install gke-gcloud-auth-plugin
|
||||
gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-2
|
||||
- run: yarn
|
||||
- run: yarn run test "cloud-runner-async-workflow" --detectOpenHandles --forceExit --runInBand
|
||||
if: matrix.CloudRunnerCluster != 'local-docker'
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: test-project
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: yarn run test-i --detectOpenHandles --forceExit --runInBand
|
||||
if: matrix.CloudRunnerCluster == 'local-docker'
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
PROJECT_PATH: test-project
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
|
||||
localBuildTests:
|
||||
name: Local Build Target Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cloudRunnerCluster:
|
||||
#- aws
|
||||
- local-docker
|
||||
#- k8s
|
||||
targetPlatform:
|
||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- WebGL # WebGL.
|
||||
- iOS # Build an iOS player.
|
||||
- Android # Build an Android .apk.
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: false
|
||||
- run: yarn
|
||||
- uses: ./
|
||||
id: unity-build
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
projectPath: test-project
|
||||
gitPrivateToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
|
||||
- run: |
|
||||
cp ./cloud-runner-cache/cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/${{ steps.unity-build.outputs.BUILD_ARTIFACT }} ${{ steps.unity-build.outputs.BUILD_ARTIFACT }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }})
|
||||
path: ${{ steps.unity-build.outputs.BUILD_ARTIFACT }}
|
||||
retention-days: 14
|
||||
6
.github/workflows/integrity-check.yml
vendored
6
.github/workflows/integrity-check.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: '16'
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
- run: yarn test --coverage
|
||||
|
||||
6
.github/workflows/mac-build-tests.yml
vendored
6
.github/workflows/mac-build-tests.yml
vendored
@@ -26,14 +26,14 @@ jobs:
|
||||
###########################
|
||||
# Checkout #
|
||||
###########################
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Cache #
|
||||
###########################
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Build MacOS (${{ matrix.unityVersion }})
|
||||
path: build
|
||||
|
||||
6
.github/workflows/windows-build-tests.yml
vendored
6
.github/workflows/windows-build-tests.yml
vendored
@@ -29,14 +29,14 @@ jobs:
|
||||
###########################
|
||||
# Checkout #
|
||||
###########################
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Cache #
|
||||
###########################
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Build Windows (${{ matrix.unityVersion }})
|
||||
path: build
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
"printWidth": 120,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"god.tsconfig": "./tsconfig.json",
|
||||
"yaml.customTags": [
|
||||
"!And",
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
|
||||
#### Code of Conduct
|
||||
|
||||
This repository has adopted the Contributor Covenant as it's
|
||||
Code of Conduct. It is expected that participants adhere to it.
|
||||
This repository has adopted the Contributor Covenant as it's Code of Conduct. It is expected that participants adhere to
|
||||
it.
|
||||
|
||||
#### Proposing a Change
|
||||
|
||||
If you are unsure about whether or not a change is desired,
|
||||
you can create an issue. This is useful because it creates
|
||||
If you are unsure about whether or not a change is desired, you can create an issue. This is useful because it creates
|
||||
the possibility for a discussion that's visible to everyone.
|
||||
|
||||
When fixing a bug it is fine to submit a pull request right away.
|
||||
@@ -34,6 +33,11 @@ Please note that commit hooks will run automatically to perform some tasks;
|
||||
- run tests
|
||||
- build distributable files
|
||||
|
||||
#### Windows users
|
||||
|
||||
Make sure your editor and terminal that run the tests are set to `Powershell 7` or above with
|
||||
`Git's Unix tools for Windows` installed. Some tests require you to be able to run `sh` and other unix commands.
|
||||
|
||||
#### License
|
||||
|
||||
By contributing to this repository, you agree that your contributions will be licensed under its MIT license.
|
||||
|
||||
99
action.yml
99
action.yml
@@ -9,7 +9,8 @@ inputs:
|
||||
unityVersion:
|
||||
required: false
|
||||
default: 'auto'
|
||||
description: 'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||
description:
|
||||
'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||
customImage:
|
||||
required: false
|
||||
default: ''
|
||||
@@ -81,67 +82,121 @@ inputs:
|
||||
gitPrivateToken:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Github private token to pull from github'
|
||||
description: '[CloudRunner] Github private token to pull from github'
|
||||
githubOwner:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[CloudRunner] GitHub owner name or organization/team name'
|
||||
chownFilesTo:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||
description:
|
||||
'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||
allowDirtyBuild:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Allows the branch of the build to be dirty, and still generate the build.'
|
||||
description: '[CloudRunner] Allows the branch of the build to be dirty, and still generate the build.'
|
||||
postBuildSteps:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'run a post build job in yaml format with the keys image, secrets (name, value object array), command string'
|
||||
description:
|
||||
'[CloudRunner] run a post build job in yaml format with the keys image, secrets (name, value object array),
|
||||
command string'
|
||||
preBuildSteps:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Run a pre build job after the repository setup but before the build job (in yaml format with the keys image, secrets (name, value object array), command line string)'
|
||||
description:
|
||||
'[CloudRunner] Run a pre build job after the repository setup but before the build job (in yaml format with the
|
||||
keys image, secrets (name, value object array), command line string)'
|
||||
customStepFiles:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[CloudRunner] Specify the names (by file name) of custom steps to run before or after cloud runner jobs, must
|
||||
match a yaml step file inside your repo in the folder .game-ci/steps/'
|
||||
customHookFiles:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[CloudRunner] Specify the names (by file name) of custom hooks to run before or after cloud runner jobs, must
|
||||
match a yaml step file inside your repo in the folder .game-ci/hooks/'
|
||||
customJobHooks:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[CloudRunner] Specify custom commands and trigger hooks (injects commands into jobs)'
|
||||
customJob:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Run a custom job instead of the standard build automation for cloud runner (in yaml format with the keys image, secrets (name, value object array), command line string)'
|
||||
description:
|
||||
'[CloudRunner] Run a custom job instead of the standard build automation for cloud runner (in yaml format with the
|
||||
keys image, secrets (name, value object array), command line string)'
|
||||
awsBaseStackName:
|
||||
default: 'game-ci'
|
||||
required: false
|
||||
description: 'The Cloud Formation stack name that must be setup before using this option.'
|
||||
description: '[CloudRunner] The Cloud Formation stack name that must be setup before using this option.'
|
||||
cloudRunnerCluster:
|
||||
default: 'local'
|
||||
required: false
|
||||
description: 'Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must be configured.'
|
||||
description:
|
||||
'[CloudRunner] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
|
||||
be configured.'
|
||||
cloudRunnerCpu:
|
||||
default: '1.0'
|
||||
required: false
|
||||
description: 'Amount of CPU time to assign the remote build container'
|
||||
cloudRunnerMemory:
|
||||
default: '750M'
|
||||
required: false
|
||||
description: 'Amount of memory to assign the remote build container'
|
||||
githubToken:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'GitHub token for cloning, only needed when kubeconfig is used.'
|
||||
description: '[CloudRunner] Amount of CPU time to assign the remote build container'
|
||||
cloudRunnerMemory:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[CloudRunner] Amount of memory to assign the remote build container'
|
||||
readInputFromOverrideList:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[CloudRunner] Comma separated list of input value names to read from "input override command"'
|
||||
readInputOverrideCommand:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
'[CloudRunner] Extend game ci by specifying a command to execute to pull input from external source e.g cloud
|
||||
provider secret managers'
|
||||
kubeConfig:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until completion.'
|
||||
description:
|
||||
'[CloudRunner] Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until
|
||||
completion.'
|
||||
kubeVolume:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'Supply a Persistent Volume Claim name to use for the Unity build.'
|
||||
description: '[CloudRunner] Supply a Persistent Volume Claim name to use for the Unity build.'
|
||||
kubeStorageClass:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
'[CloudRunner] Kubernetes storage class to use for cloud runner jobs, leave empty to install rook cluster.'
|
||||
kubeVolumeSize:
|
||||
default: '5Gi'
|
||||
required: false
|
||||
description: 'Amount of disc space to assign the Kubernetes Persistent Volume'
|
||||
description: '[CloudRunner] Amount of disc space to assign the Kubernetes Persistent Volume'
|
||||
cacheKey:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[CloudRunner] Cache key to indicate bucket for cache'
|
||||
watchToEnd:
|
||||
default: 'true'
|
||||
required: false
|
||||
description:
|
||||
'[CloudRunner] Whether or not to watch the build to the end. Can be used for especially long running jobs e.g
|
||||
imports or self-hosted ephemeral runners.'
|
||||
outputs:
|
||||
volume:
|
||||
description: 'The Persistent Volume (PV) where the build artifacts have been stored by Kubernetes'
|
||||
buildVersion:
|
||||
description: 'The generated version used for the Unity build'
|
||||
androidVersionCode:
|
||||
description: 'The generated versionCode used for the Android Unity build'
|
||||
branding:
|
||||
icon: 'box'
|
||||
color: 'gray-dark'
|
||||
runs:
|
||||
using: 'node12'
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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)
|
||||
416
dist/cloud-formations/base-setup.yml
vendored
416
dist/cloud-formations/base-setup.yml
vendored
@@ -1,416 +0,0 @@
|
||||
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
|
||||
221
dist/cloud-formations/task-def-formation.yml
vendored
221
dist/cloud-formations/task-def-formation.yml
vendored
@@ -1,221 +0,0 @@
|
||||
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
|
||||
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
|
||||
@@ -9,8 +9,13 @@ namespace UnityBuilderAction.Input
|
||||
public static void Apply(Dictionary<string, string> options)
|
||||
{
|
||||
EditorUserBuildSettings.buildAppBundle = options["customBuildPath"].EndsWith(".aab");
|
||||
#if UNITY_2019_1_OR_NEWER
|
||||
if (options.TryGetValue("androidKeystoreName", out string keystoreName) && !string.IsNullOrEmpty(keystoreName))
|
||||
{
|
||||
PlayerSettings.Android.useCustomKeystore = true;
|
||||
PlayerSettings.Android.keystoreName = keystoreName;
|
||||
}
|
||||
#endif
|
||||
if (options.TryGetValue("androidKeystorePass", out string keystorePass) && !string.IsNullOrEmpty(keystorePass))
|
||||
PlayerSettings.Android.keystorePass = keystorePass;
|
||||
if (options.TryGetValue("androidKeyaliasName", out string keyaliasName) && !string.IsNullOrEmpty(keyaliasName))
|
||||
@@ -18,7 +23,19 @@ namespace UnityBuilderAction.Input
|
||||
if (options.TryGetValue("androidKeyaliasPass", out string keyaliasPass) && !string.IsNullOrEmpty(keyaliasPass))
|
||||
PlayerSettings.Android.keyaliasPass = keyaliasPass;
|
||||
if (options.TryGetValue("androidTargetSdkVersion", out string androidTargetSdkVersion) && !string.IsNullOrEmpty(androidTargetSdkVersion))
|
||||
PlayerSettings.Android.targetSdkVersion = (AndroidSdkVersions) Enum.Parse(typeof(AndroidSdkVersions), androidTargetSdkVersion);
|
||||
{
|
||||
var targetSdkVersion = AndroidSdkVersions.AndroidApiLevelAuto;
|
||||
try
|
||||
{
|
||||
targetSdkVersion =
|
||||
(AndroidSdkVersions) Enum.Parse(typeof(AndroidSdkVersions), androidTargetSdkVersion);
|
||||
}
|
||||
catch
|
||||
{
|
||||
UnityEngine.Debug.Log("Failed to parse androidTargetSdkVersion! Fallback to AndroidApiLevelAuto");
|
||||
}
|
||||
PlayerSettings.Android.targetSdkVersion = targetSdkVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10077
dist/index.js
generated
vendored
10077
dist/index.js
generated
vendored
File diff suppressed because it is too large
Load Diff
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
430
dist/licenses.txt
generated
vendored
430
dist/licenses.txt
generated
vendored
@@ -264,6 +264,156 @@ Apache-2.0
|
||||
limitations under the License.
|
||||
|
||||
|
||||
@octokit/auth-token
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@octokit/core
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@octokit/endpoint
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@octokit/graphql
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@octokit/request
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@octokit/request-error
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@panva/asn1.js
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
@@ -876,6 +1026,211 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
before-after-hook
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Gregor Martynus and other contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
brace-expansion
|
||||
MIT
|
||||
MIT License
|
||||
@@ -1359,6 +1714,25 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
deprecation
|
||||
ISC
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Gregor Martynus and contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
domexception
|
||||
MIT
|
||||
MIT License
|
||||
@@ -2126,6 +2500,31 @@ PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
|
||||
is-plain-object
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2017, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
is-potential-custom-element-name
|
||||
MIT
|
||||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
@@ -4151,6 +4550,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
universal-user-agent
|
||||
ISC
|
||||
# [ISC License](https://spdx.org/licenses/ISC)
|
||||
|
||||
Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
universalify
|
||||
MIT
|
||||
(The MIT License)
|
||||
@@ -4194,25 +4604,13 @@ uuid
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
verror
|
||||
|
||||
15
dist/platforms/mac/steps/build.sh
vendored
15
dist/platforms/mac/steps/build.sh
vendored
@@ -76,8 +76,10 @@ fi
|
||||
|
||||
if [[ "$BUILD_TARGET" == "Android" && -n "$ANDROID_SDK_MANAGER_PARAMETERS" ]]; then
|
||||
echo "Updating Android SDK with parameters: $ANDROID_SDK_MANAGER_PARAMETERS"
|
||||
export JAVA_HOME="$(awk -F'=' '/JAVA_HOME=/{print $2}' /usr/bin/unity-editor.d/*)"
|
||||
"$(awk -F'=' '/ANDROID_HOME=/{print $2}' /usr/bin/unity-editor.d/*)/tools/bin/sdkmanager" "$ANDROID_SDK_MANAGER_PARAMETERS"
|
||||
ANDROID_INSTALL_LOCATION="/Applications/Unity/Hub/Editor/$UNITY_VERSION/PlaybackEngines/AndroidPlayer"
|
||||
export JAVA_HOME="$ANDROID_INSTALL_LOCATION/OpenJDK"
|
||||
export ANDROID_HOME="$ANDROID_INSTALL_LOCATION/SDK"
|
||||
yes | "$ANDROID_HOME/tools/bin/sdkmanager" "$ANDROID_SDK_MANAGER_PARAMETERS"
|
||||
echo "Updated Android SDK."
|
||||
else
|
||||
echo "Not updating Android SDK."
|
||||
@@ -126,10 +128,11 @@ echo ""
|
||||
# Reference: https://docs.unity3d.com/2019.3/Documentation/Manual/CommandLineArguments.html
|
||||
|
||||
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
|
||||
-logfile /dev/stdout \
|
||||
-quit \
|
||||
-batchmode \
|
||||
-nographics \
|
||||
-username "$UNITY_EMAIL" \
|
||||
-password "$UNITY_PASSWORD" \
|
||||
-customBuildName "$BUILD_NAME" \
|
||||
-projectPath "$UNITY_PROJECT_PATH" \
|
||||
-buildTarget "$BUILD_TARGET" \
|
||||
@@ -143,11 +146,15 @@ echo ""
|
||||
-androidKeyaliasName "$ANDROID_KEYALIAS_NAME" \
|
||||
-androidKeyaliasPass "$ANDROID_KEYALIAS_PASS" \
|
||||
-androidTargetSdkVersion "$ANDROID_TARGET_SDK_VERSION" \
|
||||
$CUSTOM_PARAMETERS
|
||||
$CUSTOM_PARAMETERS \
|
||||
> "$UNITY_PROJECT_PATH/out.log" 2>&1
|
||||
|
||||
# Catch exit code
|
||||
BUILD_EXIT_CODE=$?
|
||||
|
||||
# Display logs
|
||||
cat "$UNITY_PROJECT_PATH/out.log"
|
||||
|
||||
# Display results
|
||||
if [ $BUILD_EXIT_CODE -eq 0 ]; then
|
||||
echo "Build succeeded";
|
||||
|
||||
2
dist/platforms/mac/steps/return_license.sh
vendored
2
dist/platforms/mac/steps/return_license.sh
vendored
@@ -9,6 +9,8 @@ pushd "$ACTIVATE_LICENSE_PATH"
|
||||
-batchmode \
|
||||
-nographics \
|
||||
-quit \
|
||||
-username "$UNITY_EMAIL" \
|
||||
-password "$UNITY_PASSWORD" \
|
||||
-returnlicense \
|
||||
-projectPath "$ACTIVATE_LICENSE_PATH"
|
||||
|
||||
|
||||
15
dist/platforms/ubuntu/steps/activate.sh
vendored
15
dist/platforms/ubuntu/steps/activate.sh
vendored
@@ -74,6 +74,21 @@ elif [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
|
||||
# Store the exit code from the verify command
|
||||
UNITY_EXIT_CODE=$?
|
||||
|
||||
elif [[ -n "$UNITY_LICENSING_SERVER" ]]; then
|
||||
#
|
||||
# Custom Unity License Server
|
||||
#
|
||||
echo "Adding licensing server config"
|
||||
|
||||
/opt/unity/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client --acquire-floating > license.txt #is this accessible in a env variable?
|
||||
PARSEDFILE=$(grep -oP '\".*?\"' < license.txt | tr -d '"')
|
||||
export FLOATING_LICENSE
|
||||
FLOATING_LICENSE=$(sed -n 2p <<< "$PARSEDFILE")
|
||||
FLOATING_LICENSE_TIMEOUT=$(sed -n 4p <<< "$PARSEDFILE")
|
||||
|
||||
echo "Acquired floating license: \"$FLOATING_LICENSE\" with timeout $FLOATING_LICENSE_TIMEOUT"
|
||||
# Store the exit code from the verify command
|
||||
UNITY_EXIT_CODE=$?
|
||||
else
|
||||
#
|
||||
# NO LICENSE ACTIVATION STRATEGY MATCHED
|
||||
|
||||
10
dist/platforms/ubuntu/steps/build.sh
vendored
10
dist/platforms/ubuntu/steps/build.sh
vendored
@@ -63,17 +63,9 @@ else
|
||||
fi
|
||||
|
||||
#
|
||||
# Prepare Android keystore and SDK, if needed
|
||||
# Prepare Android SDK, if needed
|
||||
#
|
||||
|
||||
if [[ "$BUILD_TARGET" == "Android" && -n "$ANDROID_KEYSTORE_NAME" && -n "$ANDROID_KEYSTORE_BASE64" ]]; then
|
||||
echo "Creating Android keystore."
|
||||
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$UNITY_PROJECT_PATH/$ANDROID_KEYSTORE_NAME"
|
||||
echo "Created Android keystore."
|
||||
else
|
||||
echo "Not creating Android keystore."
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_TARGET" == "Android" && -n "$ANDROID_SDK_MANAGER_PARAMETERS" ]]; then
|
||||
echo "Updating Android SDK with parameters: $ANDROID_SDK_MANAGER_PARAMETERS"
|
||||
export JAVA_HOME="$(awk -F'=' '/JAVA_HOME=/{print $2}' /usr/bin/unity-editor.d/*)"
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
|
||||
pushd "$ACTIVATE_LICENSE_PATH"
|
||||
|
||||
if [[ -n "$UNITY_SERIAL" ]]; then
|
||||
|
||||
if [[ -n "$UNITY_LICENSING_SERVER" ]]; then #
|
||||
#
|
||||
# Return any floating license used.
|
||||
#
|
||||
echo "Returning floating license: \"$FLOATING_LICENSE\""
|
||||
/opt/unity/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client --return-floating "$FLOATING_LICENSE"
|
||||
elif [[ -n "$UNITY_SERIAL" ]]; then
|
||||
#
|
||||
# PROFESSIONAL (SERIAL) LICENSE MODE
|
||||
#
|
||||
|
||||
@@ -10,7 +10,7 @@ else
|
||||
git config --global --replace-all url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf ssh://git@github.com/
|
||||
git config --global --add url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf git@github.com
|
||||
|
||||
git config --global url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "https://github.com/"
|
||||
git config --global --add url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "https://github.com/"
|
||||
git config --global url."https://ssh:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "ssh://git@github.com/"
|
||||
git config --global url."https://git:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "git@github.com:"
|
||||
|
||||
|
||||
12
dist/platforms/windows/build.ps1
vendored
12
dist/platforms/windows/build.ps1
vendored
@@ -109,6 +109,10 @@ Write-Output "# Building project #"
|
||||
Write-Output "###########################"
|
||||
Write-Output ""
|
||||
|
||||
# If $Env:CUSTOM_PARAMETERS contains spaces and is passed directly on the command line to Unity, powershell will wrap it
|
||||
# in double quotes. To avoid this, parse $Env:CUSTOM_PARAMETERS into an array, while respecting any quotations within the string.
|
||||
$_, $customParametersArray = Invoke-Expression('Write-Output -- "" ' + $Env:CUSTOM_PARAMETERS)
|
||||
|
||||
& "C:\Program Files\Unity\Hub\Editor\$Env:UNITY_VERSION\Editor\Unity.exe" -quit -batchmode -nographics `
|
||||
-projectPath $Env:UNITY_PROJECT_PATH `
|
||||
-executeMethod $Env:BUILD_METHOD `
|
||||
@@ -116,7 +120,13 @@ Write-Output ""
|
||||
-customBuildTarget $Env:BUILD_TARGET `
|
||||
-customBuildPath $Env:CUSTOM_BUILD_PATH `
|
||||
-buildVersion $Env:VERSION `
|
||||
$Env:CUSTOM_PARAMETERS `
|
||||
-androidVersionCode $Env:ANDROID_VERSION_CODE `
|
||||
-androidKeystoreName $Env:ANDROID_KEYSTORE_NAME `
|
||||
-androidKeystorePass $Env:ANDROID_KEYSTORE_PASS `
|
||||
-androidKeyaliasName $Env:ANDROID_KEYALIAS_NAME `
|
||||
-androidKeyaliasPass $Env:ANDROID_KEYALIAS_PASS `
|
||||
-androidTargetSdkVersion $Env:ANDROID_TARGET_SDK_VERSION `
|
||||
$customParametersArray `
|
||||
-logfile | Out-Host
|
||||
|
||||
# Catch exit code
|
||||
|
||||
7
dist/unity-config/services-config.json.template
vendored
Normal file
7
dist/unity-config/services-config.json.template
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"licensingServiceBaseUrl": "%URL%",
|
||||
"enableEntitlementLicensing": true,
|
||||
"enableFloatingApi": true,
|
||||
"clientConnectTimeoutSec": 5,
|
||||
"clientHandshakeTimeoutSec": 10
|
||||
}
|
||||
3
game-ci/hooks/my-test-hook-post-build.yaml
Normal file
3
game-ci/hooks/my-test-hook-post-build.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
hook: after-build
|
||||
commands: |
|
||||
echo "after-build hook test!"
|
||||
3
game-ci/hooks/my-test-hook-pre-build.yaml
Normal file
3
game-ci/hooks/my-test-hook-pre-build.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
hook: before-build
|
||||
commands: |
|
||||
echo "before-build hook test!!"
|
||||
3
game-ci/steps/my-test-step-post-build.yaml
Normal file
3
game-ci/steps/my-test-step-post-build.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
hook: after
|
||||
commands: |
|
||||
echo "after-build step test!"
|
||||
3
game-ci/steps/my-test-step-pre-build.yaml
Normal file
3
game-ci/steps/my-test-step-pre-build.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
hook: before
|
||||
commands: |
|
||||
echo "before-build step test!"
|
||||
@@ -18,6 +18,7 @@ module.exports = {
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
},
|
||||
autoRun: false,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
verbose: true,
|
||||
|
||||
@@ -6,13 +6,6 @@
|
||||
color: true
|
||||
extends: {}
|
||||
|
||||
pre-push:
|
||||
parallel: true
|
||||
commands:
|
||||
packages-audit:
|
||||
tags: security
|
||||
run: yarn audit
|
||||
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
|
||||
14
package.json
14
package.json
@@ -7,22 +7,25 @@
|
||||
"author": "Webber <webber@takken.io>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "lefthook install",
|
||||
"prepare": "lefthook install && npx husky uninstall -y",
|
||||
"build": "yarn && tsc && ncc build lib --source-map --license licenses.txt",
|
||||
"lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts",
|
||||
"format": "prettier --write \"src/**/*.{js,ts}\"",
|
||||
"cli": "yarn ts-node src/index.ts -m cli",
|
||||
"gcp-secrets-tests": "cross-env cloudRunnerCluster=aws cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" populateOverride=true readInputFromOverrideList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"cloud runner\"",
|
||||
"gcp-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
|
||||
"aws-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"aws-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
|
||||
"cli-aws": "cross-env cloudRunnerCluster=aws yarn run test-cli",
|
||||
"cli-k8s": "cross-env cloudRunnerCluster=k8s yarn run test-cli",
|
||||
"test-cli": "cross-env cloudRunnerTests=true yarn ts-node src/index.ts -m cli --projectPath test-project",
|
||||
"test": "jest",
|
||||
"test-i": "yarn run test-i-aws && yarn run test-i-k8s",
|
||||
"test-i-f": "yarn run test-i-aws && yarn run test-i-k8s && yarn run cli-k8s && yarn run cli-aws",
|
||||
"test-i": "cross-env cloudRunnerTests=true yarn test -i -t \"cloud runner\"",
|
||||
"test-i-*": "yarn run test-i-aws && yarn run test-i-k8s",
|
||||
"test-i-aws": "cross-env cloudRunnerTests=true cloudRunnerCluster=aws yarn test -i -t \"cloud runner\"",
|
||||
"test-i-k8s": "cross-env cloudRunnerTests=true cloudRunnerCluster=k8s yarn test -i -t \"cloud runner\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.1.0",
|
||||
"@actions/github": "^5.0.0",
|
||||
"@kubernetes/client-node": "^0.16.3",
|
||||
@@ -37,12 +40,13 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"semver": "^7.3.5",
|
||||
"unity-changeset": "^1.6.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arkweid/lefthook": "^0.7.7",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@typescript-eslint/parser": "4.8.1",
|
||||
"@vercel/ncc": "^0.33.3",
|
||||
|
||||
23
src/index.ts
23
src/index.ts
@@ -1,10 +1,15 @@
|
||||
import * as core from '@actions/core';
|
||||
import { Action, BuildParameters, Cache, Docker, ImageTag, Output, CloudRunner } from './model';
|
||||
import { CLI } from './model/cli/cli';
|
||||
import { Action, BuildParameters, Cache, CloudRunner, Docker, ImageTag, Output } from './model';
|
||||
import { Cli } from './model/cli/cli';
|
||||
import MacBuilder from './model/mac-builder';
|
||||
import PlatformSetup from './model/platform-setup';
|
||||
async function runMain() {
|
||||
try {
|
||||
if (Cli.InitCliMode()) {
|
||||
await Cli.RunCli();
|
||||
|
||||
return;
|
||||
}
|
||||
Action.checkCompatibility();
|
||||
Cache.verify();
|
||||
|
||||
@@ -13,11 +18,7 @@ async function runMain() {
|
||||
const buildParameters = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameters);
|
||||
|
||||
if (
|
||||
buildParameters.cloudRunnerCluster &&
|
||||
buildParameters.cloudRunnerCluster !== '' &&
|
||||
buildParameters.cloudRunnerCluster !== 'local'
|
||||
) {
|
||||
if (buildParameters.cloudRunnerCluster !== 'local') {
|
||||
await CloudRunner.run(buildParameters, baseImage.toString());
|
||||
} else {
|
||||
core.info('Building locally');
|
||||
@@ -31,13 +32,9 @@ async function runMain() {
|
||||
|
||||
// Set output
|
||||
await Output.setBuildVersion(buildParameters.buildVersion);
|
||||
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
}
|
||||
const options = CLI.SetupCli();
|
||||
if (CLI.isCliMode(options)) {
|
||||
CLI.RunCli(options);
|
||||
} else {
|
||||
runMain();
|
||||
}
|
||||
runMain();
|
||||
|
||||
9
src/integrity.test.ts
Normal file
9
src/integrity.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { stat } from 'fs/promises';
|
||||
|
||||
describe('Integrity tests', () => {
|
||||
describe('package-lock.json', () => {
|
||||
it('does not exist', async () => {
|
||||
await expect(stat(`${process.cwd()}/package-lock.json`)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
112
src/model/__data__/versions.ts
Normal file
112
src/model/__data__/versions.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export const completelyValidSemanticVersions = [
|
||||
'0.0.4',
|
||||
'1.2.3',
|
||||
'10.20.30',
|
||||
'1.1.2-prerelease+meta',
|
||||
'1.1.2+meta',
|
||||
'1.1.2+meta-valid',
|
||||
'1.0.0-alpha',
|
||||
'1.0.0-beta',
|
||||
'1.0.0-alpha.beta',
|
||||
'1.0.0-alpha.beta.1',
|
||||
'1.0.0-alpha.1',
|
||||
'1.0.0-alpha0.valid',
|
||||
'1.0.0-alpha.0valid',
|
||||
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
|
||||
'1.0.0-rc.1+build.1',
|
||||
'2.0.0-rc.1+build.123',
|
||||
'1.2.3-beta',
|
||||
'10.2.3-DEV-SNAPSHOT',
|
||||
'1.2.3-SNAPSHOT-123',
|
||||
'1.0.0',
|
||||
'2.0.0',
|
||||
'1.1.7',
|
||||
'2.0.0+build.1848',
|
||||
'2.0.1-alpha.1227',
|
||||
'1.0.0-alpha+beta',
|
||||
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
|
||||
'1.2.3----R-S.12.9.1--.12+meta',
|
||||
'1.2.3----RC-SNAPSHOT.12.9.1--.12',
|
||||
'1.0.0+0.build.1-rc.10000aaa-kk-0.1',
|
||||
'99999999999999999999999.999999999999999999.99999999999999999',
|
||||
'1.0.0-0A.is.legal',
|
||||
];
|
||||
|
||||
export const notCompletelyValidSemanticVersions = [
|
||||
'1',
|
||||
'1.2',
|
||||
'1.2.3-0123',
|
||||
'1.2.3-0123.0123',
|
||||
'1.1.2+.123',
|
||||
'+invalid',
|
||||
'-invalid',
|
||||
'-invalid+invalid',
|
||||
'-invalid.01',
|
||||
'alpha',
|
||||
'alpha.beta',
|
||||
'alpha.beta.1',
|
||||
'alpha.1',
|
||||
'alpha+beta',
|
||||
'alpha_beta',
|
||||
'alpha.',
|
||||
'alpha..',
|
||||
'beta',
|
||||
'1.0.0-alpha_beta',
|
||||
'-alpha.',
|
||||
'1.0.0-alpha..',
|
||||
'1.0.0-alpha..1',
|
||||
'1.0.0-alpha...1',
|
||||
'1.0.0-alpha....1',
|
||||
'1.0.0-alpha.....1',
|
||||
'1.0.0-alpha......1',
|
||||
'1.0.0-alpha.......1',
|
||||
'01.1.1',
|
||||
'1.01.1',
|
||||
'1.1.01',
|
||||
'1.2',
|
||||
'1.2.3.DEV',
|
||||
'1.2-SNAPSHOT',
|
||||
'1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788',
|
||||
'1.2-RC-SNAPSHOT',
|
||||
'-1.0.3-gamma+b7718',
|
||||
'+justmeta',
|
||||
'9.8.7+meta+meta',
|
||||
'9.8.7-whatever+meta+meta',
|
||||
'99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12',
|
||||
];
|
||||
|
||||
const addVariantsPrependingV = (array: string[]) => array.map((tag) => [tag, `v${tag}`]).flat();
|
||||
|
||||
/**
|
||||
* Array of versions that will be detected as version tags. Not all of these are
|
||||
* "semantic versions", but can be used to generate one. Especially using the
|
||||
* `versioning: Semantic` option.
|
||||
*/
|
||||
export const validVersionTagInputs = addVariantsPrependingV([
|
||||
'0',
|
||||
'1',
|
||||
'0.1',
|
||||
'1.0',
|
||||
'1.1.0',
|
||||
'1.2.3',
|
||||
...completelyValidSemanticVersions,
|
||||
]);
|
||||
|
||||
export const invalidVersionTagInputs = addVariantsPrependingV([
|
||||
'+invalid',
|
||||
'-invalid',
|
||||
'-invalid+invalid',
|
||||
'-invalid.01',
|
||||
'alpha',
|
||||
'alpha.beta',
|
||||
'alpha.beta.1',
|
||||
'alpha.1',
|
||||
'alpha+beta',
|
||||
'alpha_beta',
|
||||
'alpha.',
|
||||
'alpha..',
|
||||
'beta',
|
||||
'-alpha.',
|
||||
'-1.0.3-gamma+b7718',
|
||||
'+justmeta',
|
||||
]);
|
||||
@@ -6,12 +6,14 @@ export default class AndroidVersioning {
|
||||
if (!inputVersionCode) {
|
||||
return AndroidVersioning.versionToVersionCode(version);
|
||||
}
|
||||
|
||||
return inputVersionCode;
|
||||
}
|
||||
|
||||
static versionToVersionCode(version) {
|
||||
if (version === 'none') {
|
||||
core.info(`Versioning strategy is set to ${version}, so android version code should not be applied.`);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -19,6 +21,7 @@ export default class AndroidVersioning {
|
||||
|
||||
if (!parsedVersion) {
|
||||
core.warning(`Could not parse "${version}" to semver, defaulting android version code to 1`);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -32,11 +35,13 @@ export default class AndroidVersioning {
|
||||
);
|
||||
}
|
||||
core.info(`Using android versionCode ${versionCode}`);
|
||||
|
||||
return versionCode;
|
||||
}
|
||||
|
||||
static determineSdkManagerParameters(targetSdkVersion) {
|
||||
const parsedVersion = Number.parseInt(targetSdkVersion.slice(-2), 10);
|
||||
|
||||
return Number.isNaN(parsedVersion) ? '' : `platforms;android-${parsedVersion}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,17 @@ import BuildParameters from './build-parameters';
|
||||
import Input from './input';
|
||||
import Platform from './platform';
|
||||
|
||||
// Todo - Don't use process.env directly, that's what the input model class is for.
|
||||
const testLicense =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
|
||||
process.env.UNITY_LICENSE = testLicense;
|
||||
|
||||
const determineVersion = jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||
const determineUnityVersion = jest
|
||||
.spyOn(UnityVersioning, 'determineUnityVersion')
|
||||
.mockImplementation(() => '2019.2.11f1');
|
||||
const determineSdkManagerParameters = jest
|
||||
.spyOn(AndroidVersioning, 'determineSdkManagerParameters')
|
||||
.mockImplementation(() => 'platforms;android-30');
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||
process.env.UNITY_LICENSE = testLicense; // Todo - Don't use process.env directly, that's what the input model class is for.
|
||||
});
|
||||
|
||||
describe('BuildParameters', () => {
|
||||
@@ -29,48 +25,54 @@ describe('BuildParameters', () => {
|
||||
});
|
||||
|
||||
it('determines the version only once', async () => {
|
||||
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||
await BuildParameters.create();
|
||||
expect(determineVersion).toHaveBeenCalledTimes(1);
|
||||
await expect(Versioning.determineBuildVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('determines the unity version only once', async () => {
|
||||
jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
||||
await BuildParameters.create();
|
||||
expect(determineUnityVersion).toHaveBeenCalledTimes(1);
|
||||
await expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns the android version code with provided input', async () => {
|
||||
const mockValue = '42';
|
||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 }));
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 }));
|
||||
});
|
||||
|
||||
it('determines the android sdk manager parameters only once', async () => {
|
||||
jest.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(() => 'platforms;android-30');
|
||||
await BuildParameters.create();
|
||||
expect(determineSdkManagerParameters).toHaveBeenCalledTimes(1);
|
||||
await expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns the targetPlatform', async () => {
|
||||
const mockValue = 'somePlatform';
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ targetPlatform: mockValue }));
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ targetPlatform: mockValue }));
|
||||
});
|
||||
|
||||
it('returns the project path', async () => {
|
||||
const mockValue = 'path/to/project';
|
||||
jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
||||
jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue }));
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue }));
|
||||
});
|
||||
|
||||
it('returns the build path', async () => {
|
||||
@@ -79,13 +81,18 @@ describe('BuildParameters', () => {
|
||||
const expectedBuildPath = `${mockPath}/${mockPlatform}`;
|
||||
jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildPath: expectedBuildPath }));
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildPath: expectedBuildPath }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the build file', async () => {
|
||||
const mockValue = 'someBuildName';
|
||||
const mockPlatform = 'somePlatform';
|
||||
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue }));
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue }));
|
||||
});
|
||||
|
||||
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])(
|
||||
@@ -93,7 +100,7 @@ describe('BuildParameters', () => {
|
||||
async (targetPlatform) => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
expect(BuildParameters.create()).resolves.toEqual(
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: `${targetPlatform}.exe` }),
|
||||
);
|
||||
},
|
||||
@@ -103,7 +110,7 @@ describe('BuildParameters', () => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false);
|
||||
expect(BuildParameters.create()).resolves.toEqual(
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: `${targetPlatform}.apk` }),
|
||||
);
|
||||
});
|
||||
@@ -112,7 +119,7 @@ describe('BuildParameters', () => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true);
|
||||
expect(BuildParameters.create()).resolves.toEqual(
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: `${targetPlatform}.aab` }),
|
||||
);
|
||||
});
|
||||
@@ -120,51 +127,82 @@ describe('BuildParameters', () => {
|
||||
it('returns the build method', async () => {
|
||||
const mockValue = 'Namespace.ClassName.BuildMethod';
|
||||
jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystoreName: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystoreBase64: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystorePass: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeyaliasName: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeyaliasPass: mockValue }));
|
||||
await 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);
|
||||
expect(BuildParameters.create()).resolves.toEqual(
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidTargetSdkVersion: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the unity licensing server address', async () => {
|
||||
const mockValue = 'http://example.com';
|
||||
jest.spyOn(Input, 'unityLicensingServer', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ unityLicensingServer: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error when no unity license provider provided', async () => {
|
||||
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
||||
await expect(BuildParameters.create()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('return serial when no license server is provided', async () => {
|
||||
const mockValue = '123';
|
||||
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
||||
process.env.UNITY_SERIAL = mockValue;
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ unitySerial: mockValue }));
|
||||
delete process.env.UNITY_SERIAL;
|
||||
});
|
||||
|
||||
it('returns the custom parameters', async () => {
|
||||
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
|
||||
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
||||
expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue }));
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
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 CloudRunnerBuildGuid from './cloud-runner/services/cloud-runner-guid';
|
||||
import Input from './input';
|
||||
import Platform from './platform';
|
||||
import UnityVersioning from './unity-versioning';
|
||||
import Versioning from './versioning';
|
||||
import { GitRepoReader } from './input-readers/git-repo';
|
||||
import { GithubCliReader } from './input-readers/github-cli';
|
||||
import { Cli } from './cli/cli';
|
||||
import GitHub from './github';
|
||||
import CloudRunnerOptions from './cloud-runner/cloud-runner-options';
|
||||
|
||||
class BuildParameters {
|
||||
public editorVersion!: string;
|
||||
public customImage!: string;
|
||||
public unitySerial!: string;
|
||||
public unityLicensingServer!: string;
|
||||
public runnerTempPath: string | undefined;
|
||||
public targetPlatform!: string;
|
||||
public projectPath!: string;
|
||||
@@ -33,15 +38,18 @@ class BuildParameters {
|
||||
public cloudRunnerCluster!: string;
|
||||
public awsBaseStackName!: string;
|
||||
public gitPrivateToken!: string;
|
||||
public remoteBuildCluster!: string;
|
||||
public awsStackName!: string;
|
||||
public kubeConfig!: string;
|
||||
public githubToken!: string;
|
||||
public cloudRunnerMemory!: string;
|
||||
public cloudRunnerCpu!: string;
|
||||
public kubeVolumeSize!: string;
|
||||
public kubeVolume!: string;
|
||||
public kubeStorageClass!: string;
|
||||
public chownFilesTo!: string;
|
||||
public customJobHooks!: string;
|
||||
public readInputFromOverrideList!: string;
|
||||
public readInputOverrideCommand!: string;
|
||||
public cacheKey!: string;
|
||||
|
||||
public postBuildSteps!: string;
|
||||
public preBuildSteps!: string;
|
||||
@@ -52,6 +60,17 @@ class BuildParameters {
|
||||
public gitSha!: string;
|
||||
public logId!: string;
|
||||
public buildGuid!: string;
|
||||
public cloudRunnerBranch!: string;
|
||||
public cloudRunnerDebug!: boolean;
|
||||
public cloudRunnerBuilderPlatform!: string | undefined;
|
||||
public isCliMode!: boolean;
|
||||
public retainWorkspace!: boolean;
|
||||
public maxRetainedWorkspaces!: number;
|
||||
public useSharedLargePackages!: boolean;
|
||||
public useLz4Compression!: boolean;
|
||||
public garbageCollectionMaxAge!: number;
|
||||
public constantGarbageCollection!: boolean;
|
||||
public githubChecks!: boolean;
|
||||
|
||||
static async create(): Promise<BuildParameters> {
|
||||
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle);
|
||||
@@ -63,26 +82,26 @@ class BuildParameters {
|
||||
// Todo - Don't use process.env directly, that's what the input model class is for.
|
||||
// ---
|
||||
let unitySerial = '';
|
||||
if (!process.env.UNITY_SERIAL) {
|
||||
//No serial was present so it is a personal license that we need to convert
|
||||
if (!process.env.UNITY_LICENSE) {
|
||||
throw new Error(`Missing Unity License File and no Serial was found. If this
|
||||
is a personal license, make sure to follow the activation
|
||||
steps and set the UNITY_LICENSE GitHub secret or enter a Unity
|
||||
serial number inside the UNITY_SERIAL GitHub secret.`);
|
||||
if (Input.unityLicensingServer === '') {
|
||||
if (!process.env.UNITY_SERIAL && GitHub.githubInputEnabled) {
|
||||
// No serial was present, so it is a personal license that we need to convert
|
||||
if (!process.env.UNITY_LICENSE) {
|
||||
throw new Error(`Missing Unity License File and no Serial was found. If this
|
||||
is a personal license, make sure to follow the activation
|
||||
steps and set the UNITY_LICENSE GitHub secret or enter a Unity
|
||||
serial number inside the UNITY_SERIAL GitHub secret.`);
|
||||
}
|
||||
unitySerial = this.getSerialFromLicenseFile(process.env.UNITY_LICENSE);
|
||||
} else {
|
||||
unitySerial = process.env.UNITY_SERIAL!;
|
||||
}
|
||||
unitySerial = this.getSerialFromLicenseFile(process.env.UNITY_LICENSE);
|
||||
} else {
|
||||
unitySerial = process.env.UNITY_SERIAL!;
|
||||
}
|
||||
core.setSecret(unitySerial);
|
||||
// ---
|
||||
|
||||
return {
|
||||
editorVersion,
|
||||
customImage: Input.customImage,
|
||||
unitySerial,
|
||||
|
||||
unityLicensingServer: Input.unityLicensingServer,
|
||||
runnerTempPath: process.env.RUNNER_TEMP,
|
||||
targetPlatform: Input.targetPlatform,
|
||||
projectPath: Input.projectPath,
|
||||
@@ -101,28 +120,41 @@ class BuildParameters {
|
||||
androidSdkManagerParameters,
|
||||
customParameters: Input.customParameters,
|
||||
sshAgent: Input.sshAgent,
|
||||
gitPrivateToken: await Input.gitPrivateToken(),
|
||||
gitPrivateToken: Input.gitPrivateToken || (await GithubCliReader.GetGitHubAuthToken()),
|
||||
chownFilesTo: Input.chownFilesTo,
|
||||
cloudRunnerCluster: Input.cloudRunnerCluster,
|
||||
awsBaseStackName: Input.awsBaseStackName,
|
||||
kubeConfig: Input.kubeConfig,
|
||||
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,
|
||||
cloudRunnerCluster: CloudRunnerOptions.cloudRunnerCluster,
|
||||
cloudRunnerBuilderPlatform: CloudRunnerOptions.cloudRunnerBuilderPlatform,
|
||||
awsBaseStackName: CloudRunnerOptions.awsBaseStackName,
|
||||
kubeConfig: CloudRunnerOptions.kubeConfig,
|
||||
cloudRunnerMemory: CloudRunnerOptions.cloudRunnerMemory,
|
||||
cloudRunnerCpu: CloudRunnerOptions.cloudRunnerCpu,
|
||||
kubeVolumeSize: CloudRunnerOptions.kubeVolumeSize,
|
||||
kubeVolume: CloudRunnerOptions.kubeVolume,
|
||||
postBuildSteps: CloudRunnerOptions.postBuildSteps,
|
||||
preBuildSteps: CloudRunnerOptions.preBuildSteps,
|
||||
customJob: CloudRunnerOptions.customJob,
|
||||
runNumber: Input.runNumber,
|
||||
branch: await Input.branch(),
|
||||
// Todo - move this out of UserInput and into some class that determines additional information (as needed)
|
||||
githubRepo: await Input.githubRepo(),
|
||||
remoteBuildCluster: Input.cloudRunnerCluster,
|
||||
awsStackName: Input.awsBaseStackName,
|
||||
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
|
||||
cloudRunnerBranch: CloudRunnerOptions.cloudRunnerBranch.split('/').reverse()[0],
|
||||
cloudRunnerDebug: CloudRunnerOptions.cloudRunnerDebug,
|
||||
githubRepo: Input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder',
|
||||
isCliMode: Cli.isCliMode,
|
||||
awsStackName: CloudRunnerOptions.awsBaseStackName,
|
||||
gitSha: Input.gitSha,
|
||||
logId: customAlphabet(CloudRunnerConstants.alphabet, 9)(),
|
||||
buildGuid: CloudRunnerNamespace.generateBuildName(Input.runNumber, Input.targetPlatform),
|
||||
buildGuid: CloudRunnerBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform),
|
||||
customJobHooks: CloudRunnerOptions.customJobHooks(),
|
||||
readInputOverrideCommand: CloudRunnerOptions.readInputOverrideCommand(),
|
||||
readInputFromOverrideList: CloudRunnerOptions.readInputFromOverrideList(),
|
||||
kubeStorageClass: CloudRunnerOptions.kubeStorageClass,
|
||||
cacheKey: CloudRunnerOptions.cacheKey,
|
||||
retainWorkspace: CloudRunnerOptions.retainWorkspaces,
|
||||
useSharedLargePackages: CloudRunnerOptions.useSharedLargePackages,
|
||||
useLz4Compression: CloudRunnerOptions.useLz4Compression,
|
||||
maxRetainedWorkspaces: CloudRunnerOptions.maxRetainedWorkspaces,
|
||||
constantGarbageCollection: CloudRunnerOptions.constantGarbageCollection,
|
||||
garbageCollectionMaxAge: CloudRunnerOptions.garbageCollectionMaxAge,
|
||||
githubChecks: CloudRunnerOptions.githubChecks,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,6 +178,7 @@ class BuildParameters {
|
||||
throw new Error(`License File was corrupted, unable to locate serial`);
|
||||
}
|
||||
const endIndex = license.indexOf(endKey, startIndex);
|
||||
|
||||
// Slice off the first 4 characters as they are garbage values
|
||||
return Buffer.from(license.slice(startIndex, endIndex), 'base64').toString('binary').slice(4);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
45
src/model/cli/cli-functions-repository.ts
Normal file
45
src/model/cli/cli-functions-repository.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export class CliFunctionsRepository {
|
||||
private static targets: any[] = [];
|
||||
public static PushCliFunction(
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
key: string,
|
||||
description: string,
|
||||
) {
|
||||
CliFunctionsRepository.targets.push({
|
||||
target,
|
||||
propertyKey,
|
||||
descriptor,
|
||||
key,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
public static GetCliFunctions(key) {
|
||||
const results = CliFunctionsRepository.targets.find((x) => x.key === key);
|
||||
if (results === undefined || results.length === 0) {
|
||||
throw new Error(`no CLI mode found for ${key}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static GetAllCliModes() {
|
||||
return CliFunctionsRepository.targets.map((x) => {
|
||||
return {
|
||||
key: x.key,
|
||||
description: x.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
public static PushCliFunctionSource(cliFunction: any) {}
|
||||
}
|
||||
|
||||
export function CliFunction(key: string, description: string) {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
CliFunctionsRepository.PushCliFunction(target, propertyKey, descriptor, key, description);
|
||||
};
|
||||
}
|
||||
@@ -3,86 +3,208 @@ 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';
|
||||
import CloudRunnerQueryOverride from '../cloud-runner/services/cloud-runner-query-override';
|
||||
import { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
|
||||
import { Caching } from '../cloud-runner/remote-client/caching';
|
||||
import { LfsHashing } from '../cloud-runner/services/lfs-hashing';
|
||||
import { RemoteClient } from '../cloud-runner/remote-client';
|
||||
import CloudRunnerOptionsReader from '../cloud-runner/services/cloud-runner-options-reader';
|
||||
import GitHub from '../github';
|
||||
import { TaskParameterSerializer } from '../cloud-runner/services/task-parameter-serializer';
|
||||
import { CloudRunnerFolders } from '../cloud-runner/services/cloud-runner-folders';
|
||||
import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system';
|
||||
|
||||
export class CLI {
|
||||
static async RunCli(options: any): Promise<void> {
|
||||
Input.githubInputEnabled = false;
|
||||
|
||||
const results = GetCliFunctions(options.mode);
|
||||
|
||||
if (results === undefined || results.length === 0) {
|
||||
throw new Error('no CLI mode found');
|
||||
export class Cli {
|
||||
public static options;
|
||||
static get isCliMode() {
|
||||
return Cli.options !== undefined && Cli.options.mode !== undefined && Cli.options.mode !== '';
|
||||
}
|
||||
public static query(key, alternativeKey) {
|
||||
if (Cli.options && Cli.options[key] !== undefined) {
|
||||
return Cli.options[key];
|
||||
}
|
||||
if (Cli.options && alternativeKey && Cli.options[alternativeKey] !== undefined) {
|
||||
return Cli.options[alternativeKey];
|
||||
}
|
||||
|
||||
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 !== '';
|
||||
return;
|
||||
}
|
||||
|
||||
public static SetupCli() {
|
||||
public static InitCliMode() {
|
||||
CliFunctionsRepository.PushCliFunctionSource(RemoteClient);
|
||||
CliFunctionsRepository.PushCliFunctionSource(Caching);
|
||||
CliFunctionsRepository.PushCliFunctionSource(LfsHashing);
|
||||
const program = new Command();
|
||||
program.version('0.0.1');
|
||||
const properties = Object.getOwnPropertyNames(Input);
|
||||
core.info(`\n`);
|
||||
core.info(`INPUT:`);
|
||||
|
||||
const properties = CloudRunnerOptionsReader.GetProperties();
|
||||
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`) {
|
||||
}
|
||||
program.option(
|
||||
'-m, --mode <mode>',
|
||||
CliFunctionsRepository.GetAllCliModes()
|
||||
.map((x) => `${x.key} (${x.description})`)
|
||||
.join(` | `),
|
||||
);
|
||||
program.option('--populateOverride <populateOverride>', 'should use override query to pull input false by default');
|
||||
program.option('--cachePushFrom <cachePushFrom>', 'cache push from source folder');
|
||||
program.option('--cachePushTo <cachePushTo>', 'cache push to caching folder');
|
||||
program.option('--artifactName <artifactName>', 'caching artifact name');
|
||||
program.option('--select <select>', 'select a particular resource');
|
||||
program.parse(process.argv);
|
||||
Cli.options = program.opts();
|
||||
|
||||
return Cli.isCliMode;
|
||||
}
|
||||
|
||||
static async RunCli(): Promise<void> {
|
||||
GitHub.githubInputEnabled = false;
|
||||
if (Cli.options['populateOverride'] === `true`) {
|
||||
await CloudRunnerQueryOverride.PopulateQueryOverrideInput();
|
||||
}
|
||||
if (Cli.options['logInput']) {
|
||||
Cli.logInput();
|
||||
}
|
||||
const results = CliFunctionsRepository.GetCliFunctions(Cli.options.mode);
|
||||
CloudRunnerLogger.log(`Entrypoint: ${results.key}`);
|
||||
Cli.options.versioning = 'None';
|
||||
|
||||
const buildParameter = TaskParameterSerializer.readBuildParameterFromEnvironment();
|
||||
CloudRunnerLogger.log(`Build Params:
|
||||
${JSON.stringify(buildParameter, undefined, 4)}
|
||||
`);
|
||||
CloudRunner.buildParameters = buildParameter;
|
||||
CloudRunner.lockedWorkspace = process.env.LOCKED_WORKSPACE;
|
||||
|
||||
return await results.target[results.propertyKey](Cli.options);
|
||||
}
|
||||
|
||||
@CliFunction(`print-input`, `prints all input`)
|
||||
private static logInput() {
|
||||
core.info(`\n`);
|
||||
core.info(`INPUT:`);
|
||||
const properties = CloudRunnerOptionsReader.GetProperties();
|
||||
for (const element of properties) {
|
||||
if (
|
||||
Input[element] !== undefined &&
|
||||
Input[element] !== '' &&
|
||||
typeof Input[element] !== `function` &&
|
||||
element !== 'length' &&
|
||||
element !== 'cliOptions' &&
|
||||
element !== 'prototype'
|
||||
) {
|
||||
core.info(`${element} ${Input[element]}`);
|
||||
}
|
||||
}
|
||||
core.info(`\n`);
|
||||
program.option(
|
||||
'-m, --mode <mode>',
|
||||
GetAllCliModes()
|
||||
.map((x) => `${x.key} (${x.description})`)
|
||||
.join(` | `),
|
||||
);
|
||||
program.parse(process.argv);
|
||||
|
||||
return program.opts();
|
||||
}
|
||||
|
||||
@CliFunction(`cli`, `runs a cloud runner build`)
|
||||
@CliFunction(`cli-build`, `runs a cloud runner build`)
|
||||
public static async CLIBuild(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
return await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
}
|
||||
|
||||
@CliFunction(`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(`async-workflow`, `runs a cloud runner build`)
|
||||
public static async asyncronousWorkflow(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
return await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
}
|
||||
|
||||
@CliFunction(`cach-push`, `push to cache`)
|
||||
static async cachePush() {}
|
||||
@CliFunction(`checks-update`, `runs a cloud runner build`)
|
||||
public static async checksUpdate() {
|
||||
const input = JSON.parse(process.env.CHECKS_UPDATE || ``);
|
||||
core.info(`Checks Update ${process.env.CHECKS_UPDATE}`);
|
||||
if (input.mode === `create`) {
|
||||
throw new Error(`Not supported: only use update`);
|
||||
} else if (input.mode === `update`) {
|
||||
await GitHub.updateGitHubCheckRequest(input.data);
|
||||
}
|
||||
}
|
||||
|
||||
@CliFunction(`cach-pull`, `pull from cache`)
|
||||
static async cachePull() {}
|
||||
@CliFunction(`garbage-collect`, `runs garbage collection`)
|
||||
public static async GarbageCollect(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
@CliFunction(`garbage-collect-aws`, `garbage collect aws`)
|
||||
static async garbageCollectAws() {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new SDK.CloudFormation();
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
const stacks = await CF.listStacks().promise();
|
||||
CloudRunnerLogger.log(JSON.stringify(stacks, undefined, 4));
|
||||
return await CloudRunner.Provider.garbageCollect(``, false, 0, false, false);
|
||||
}
|
||||
|
||||
@CliFunction(`list-resources`, `lists active resources`)
|
||||
public static async ListResources(): Promise<string[]> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
const result = await CloudRunner.Provider.listResources();
|
||||
CloudRunnerLogger.log(JSON.stringify(result, undefined, 4));
|
||||
|
||||
return result.map((x) => x.Name);
|
||||
}
|
||||
|
||||
@CliFunction(`list-worfklow`, `lists running workflows`)
|
||||
public static async ListWorfklow(): Promise<string[]> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
return (await CloudRunner.Provider.listWorkflow()).map((x) => x.Name);
|
||||
}
|
||||
|
||||
@CliFunction(`watch`, `follows logs of a running workflow`)
|
||||
public static async Watch(): Promise<string> {
|
||||
const buildParameter = await BuildParameters.create();
|
||||
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
return await CloudRunner.Provider.watchWorkflow();
|
||||
}
|
||||
|
||||
@CliFunction(`remote-cli-post-build`, `runs a cloud runner build`)
|
||||
public static async PostCLIBuild(): Promise<string> {
|
||||
core.info(`Running POST build tasks`);
|
||||
|
||||
await Caching.PushToCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(`${CloudRunnerFolders.cacheFolderForCacheKeyFull}/Library`),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute),
|
||||
`lib-${CloudRunner.buildParameters.buildGuid}`,
|
||||
);
|
||||
|
||||
await Caching.PushToCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(`${CloudRunnerFolders.cacheFolderForCacheKeyFull}/build`),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectBuildFolderAbsolute),
|
||||
`build-${CloudRunner.buildParameters.buildGuid}`,
|
||||
);
|
||||
|
||||
if (!CloudRunner.buildParameters.retainWorkspace) {
|
||||
await CloudRunnerSystem.Run(
|
||||
`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await RemoteClient.runCustomHookFiles(`after-build`);
|
||||
|
||||
const parameters = await BuildParameters.create();
|
||||
CloudRunner.setup(parameters);
|
||||
if (parameters.constantGarbageCollection) {
|
||||
await CloudRunnerSystem.Run(
|
||||
`find /${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.buildVolumeFolder)}/ -name '*.*' -mmin +${
|
||||
parameters.garbageCollectionMaxAge * 60
|
||||
} -delete`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForAllFull)} -name '*.*' -mmin +${
|
||||
parameters.garbageCollectionMaxAge * 60
|
||||
} -delete`,
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise((result) => result(``));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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<string>((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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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 -b ${CloudRunnerState.branchName} ${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}/..`);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
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<CloudRunnerAWSTaskDef> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
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;
|
||||
290
src/model/cloud-runner/cloud-runner-options.ts
Normal file
290
src/model/cloud-runner/cloud-runner-options.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { Cli } from '../cli/cli';
|
||||
import CloudRunnerQueryOverride from './services/cloud-runner-query-override';
|
||||
import GitHub from '../github';
|
||||
const core = require('@actions/core');
|
||||
|
||||
class CloudRunnerOptions {
|
||||
// ### ### ###
|
||||
// Input Handling
|
||||
// ### ### ###
|
||||
public static getInput(query) {
|
||||
if (GitHub.githubInputEnabled) {
|
||||
const coreInput = core.getInput(query);
|
||||
if (coreInput && coreInput !== '') {
|
||||
return coreInput;
|
||||
}
|
||||
}
|
||||
const alternativeQuery = CloudRunnerOptions.ToEnvVarFormat(query);
|
||||
|
||||
// Query input sources
|
||||
if (Cli.query(query, alternativeQuery)) {
|
||||
return Cli.query(query, alternativeQuery);
|
||||
}
|
||||
|
||||
if (CloudRunnerQueryOverride.query(query, alternativeQuery)) {
|
||||
return CloudRunnerQueryOverride.query(query, alternativeQuery);
|
||||
}
|
||||
|
||||
if (process.env[query] !== undefined) {
|
||||
return process.env[query];
|
||||
}
|
||||
|
||||
if (alternativeQuery !== query && process.env[alternativeQuery] !== undefined) {
|
||||
return process.env[alternativeQuery];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input: string) {
|
||||
if (input.toUpperCase() === input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return input
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/ /g, '_');
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Provider parameters
|
||||
// ### ### ###
|
||||
|
||||
static get region(): string {
|
||||
return CloudRunnerOptions.getInput('region') || 'eu-west-2';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// GitHub parameters
|
||||
// ### ### ###
|
||||
static get githubChecks(): boolean {
|
||||
return CloudRunnerOptions.getInput('githubChecks') || false;
|
||||
}
|
||||
|
||||
static get githubOwner() {
|
||||
return CloudRunnerOptions.getInput('githubOwner') || CloudRunnerOptions.githubRepo.split(`/`)[0] || false;
|
||||
}
|
||||
|
||||
static get githubRepoName() {
|
||||
return CloudRunnerOptions.getInput('githubRepoName') || CloudRunnerOptions.githubRepo.split(`/`)[1] || false;
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Git syncronization parameters
|
||||
// ### ### ###
|
||||
|
||||
static get githubRepo() {
|
||||
return CloudRunnerOptions.getInput('GITHUB_REPOSITORY') || CloudRunnerOptions.getInput('GITHUB_REPO') || undefined;
|
||||
}
|
||||
static get branch() {
|
||||
if (CloudRunnerOptions.getInput(`GITHUB_REF`)) {
|
||||
return CloudRunnerOptions.getInput(`GITHUB_REF`).replace('refs/', '').replace(`head/`, '').replace(`heads/`, '');
|
||||
} else if (CloudRunnerOptions.getInput('branch')) {
|
||||
return CloudRunnerOptions.getInput('branch');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static get gitSha() {
|
||||
if (CloudRunnerOptions.getInput(`GITHUB_SHA`)) {
|
||||
return CloudRunnerOptions.getInput(`GITHUB_SHA`);
|
||||
} else if (CloudRunnerOptions.getInput(`GitSHA`)) {
|
||||
return CloudRunnerOptions.getInput(`GitSHA`);
|
||||
}
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Cloud Runner parameters
|
||||
// ### ### ###
|
||||
|
||||
static get cloudRunnerBuilderPlatform() {
|
||||
const input = CloudRunnerOptions.getInput('cloudRunnerBuilderPlatform');
|
||||
if (input) {
|
||||
return input;
|
||||
}
|
||||
if (CloudRunnerOptions.cloudRunnerCluster !== 'local') {
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static get cloudRunnerBranch() {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerBranch') || 'main';
|
||||
}
|
||||
|
||||
static get cloudRunnerCluster() {
|
||||
if (Cli.isCliMode) {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'aws';
|
||||
}
|
||||
|
||||
return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'local';
|
||||
}
|
||||
|
||||
static get cloudRunnerCpu() {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerCpu');
|
||||
}
|
||||
|
||||
static get cloudRunnerMemory() {
|
||||
return CloudRunnerOptions.getInput('cloudRunnerMemory');
|
||||
}
|
||||
|
||||
static get customJob() {
|
||||
return CloudRunnerOptions.getInput('customJob') || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Custom commands from files parameters
|
||||
// ### ### ###
|
||||
|
||||
static get customStepFiles() {
|
||||
return CloudRunnerOptions.getInput('customStepFiles')?.split(`,`) || [];
|
||||
}
|
||||
|
||||
static get customHookFiles() {
|
||||
return CloudRunnerOptions.getInput('customHookFiles')?.split(`,`) || [];
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Custom commands from yaml parameters
|
||||
// ### ### ###
|
||||
|
||||
static customJobHooks() {
|
||||
return CloudRunnerOptions.getInput('customJobHooks') || '';
|
||||
}
|
||||
|
||||
static get postBuildSteps() {
|
||||
return CloudRunnerOptions.getInput('postBuildSteps') || '';
|
||||
}
|
||||
|
||||
static get preBuildSteps() {
|
||||
return CloudRunnerOptions.getInput('preBuildSteps') || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Input override handling
|
||||
// ### ### ###
|
||||
|
||||
static readInputFromOverrideList() {
|
||||
return CloudRunnerOptions.getInput('readInputFromOverrideList') || '';
|
||||
}
|
||||
|
||||
static readInputOverrideCommand() {
|
||||
const value = CloudRunnerOptions.getInput('readInputOverrideCommand');
|
||||
|
||||
if (value === 'gcp-secret-manager') {
|
||||
return 'gcloud secrets versions access 1 --secret="{0}"';
|
||||
} else if (value === 'aws-secret-manager') {
|
||||
return 'aws secretsmanager get-secret-value --secret-id {0}';
|
||||
}
|
||||
|
||||
return value || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Aws
|
||||
// ### ### ###
|
||||
|
||||
static get awsBaseStackName() {
|
||||
return CloudRunnerOptions.getInput('awsBaseStackName') || 'game-ci';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// K8s
|
||||
// ### ### ###
|
||||
|
||||
static get kubeConfig() {
|
||||
return CloudRunnerOptions.getInput('kubeConfig') || '';
|
||||
}
|
||||
|
||||
static get kubeVolume() {
|
||||
return CloudRunnerOptions.getInput('kubeVolume') || '';
|
||||
}
|
||||
|
||||
static get kubeVolumeSize() {
|
||||
return CloudRunnerOptions.getInput('kubeVolumeSize') || '5Gi';
|
||||
}
|
||||
|
||||
static get kubeStorageClass(): string {
|
||||
return CloudRunnerOptions.getInput('kubeStorageClass') || '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Caching
|
||||
// ### ### ###
|
||||
|
||||
static get cacheKey(): string {
|
||||
return CloudRunnerOptions.getInput('cacheKey') || CloudRunnerOptions.branch;
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Utility Parameters
|
||||
// ### ### ###
|
||||
|
||||
static get cloudRunnerDebug(): boolean {
|
||||
return CloudRunnerOptions.getInput(`cloudRunnerTests`) || CloudRunnerOptions.getInput(`cloudRunnerDebug`) || false;
|
||||
}
|
||||
static get cloudRunnerDebugTree(): boolean {
|
||||
return CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) || false;
|
||||
}
|
||||
static get cloudRunnerDebugEnv(): boolean {
|
||||
return CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) || false;
|
||||
}
|
||||
|
||||
static get watchCloudRunnerToEnd(): boolean {
|
||||
if (CloudRunnerOptions.asyncCloudRunner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CloudRunnerOptions.getInput(`watchToEnd`) || true;
|
||||
}
|
||||
|
||||
static get asyncCloudRunner(): boolean {
|
||||
return (CloudRunnerOptions.getInput('asyncCloudRunner') || `false`) === `true` || false;
|
||||
}
|
||||
|
||||
public static get useSharedLargePackages(): boolean {
|
||||
return (CloudRunnerOptions.getInput(`useSharedLargePackages`) || 'false') === 'true';
|
||||
}
|
||||
|
||||
public static get useSharedBuilder(): boolean {
|
||||
return (CloudRunnerOptions.getInput(`useSharedBuilder`) || 'true') === 'true';
|
||||
}
|
||||
|
||||
public static get useLz4Compression(): boolean {
|
||||
return (CloudRunnerOptions.getInput(`useLz4Compression`) || 'false') === 'true';
|
||||
}
|
||||
|
||||
public static get useCleanupCron(): boolean {
|
||||
return (CloudRunnerOptions.getInput(`useCleanupCron`) || 'true') === 'true';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Retained Workspace
|
||||
// ### ### ###
|
||||
|
||||
public static get retainWorkspaces(): boolean {
|
||||
return CloudRunnerOptions.getInput(`retainWorkspaces`) || false;
|
||||
}
|
||||
|
||||
static get maxRetainedWorkspaces(): number {
|
||||
return Number(CloudRunnerOptions.getInput(`maxRetainedWorkspaces`)) || 3;
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Garbage Collection
|
||||
// ### ### ###
|
||||
|
||||
static get constantGarbageCollection(): boolean {
|
||||
return CloudRunnerOptions.getInput(`constantGarbageCollection`) || true;
|
||||
}
|
||||
|
||||
static get garbageCollectionMaxAge(): number {
|
||||
return Number(CloudRunnerOptions.getInput(`garbageCollectionMaxAge`)) || 24;
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudRunnerOptions;
|
||||
@@ -1,3 +1,3 @@
|
||||
export class CloudRunnerStatics {
|
||||
public static readonly logPrefix = `Cloud-Runner-System`;
|
||||
public static readonly logPrefix = `Cloud-Runner`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from './services/cloud-runner-secret';
|
||||
|
||||
export class CloudRunnerStepState {
|
||||
public image: string;
|
||||
@@ -1,50 +0,0 @@
|
||||
import { BuildParameters, ImageTag } from '..';
|
||||
import CloudRunner from './cloud-runner';
|
||||
import Input from '../input';
|
||||
import { CloudRunnerStatics } from './cloud-runner-statics';
|
||||
import { TaskParameterSerializer } from './services/task-parameter-serializer';
|
||||
import UnityVersioning from '../unity-versioning';
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -1,35 +1,73 @@
|
||||
import AWSBuildPlatform from './aws';
|
||||
import { BuildParameters } from '..';
|
||||
import { CloudRunnerState } from './state/cloud-runner-state';
|
||||
import Kubernetes from './k8s';
|
||||
import AwsBuildPlatform from './providers/aws';
|
||||
import { BuildParameters, Input } from '..';
|
||||
import Kubernetes from './providers/k8s';
|
||||
import CloudRunnerLogger from './services/cloud-runner-logger';
|
||||
import { CloudRunnerStepState } from './state/cloud-runner-step-state';
|
||||
import { CloudRunnerStepState } from './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';
|
||||
import CloudRunnerSecret from './services/cloud-runner-secret';
|
||||
import { ProviderInterface } from './providers/provider-interface';
|
||||
import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable';
|
||||
import TestCloudRunner from './providers/test';
|
||||
import LocalCloudRunner from './providers/local';
|
||||
import LocalDockerCloudRunner from './providers/docker';
|
||||
import GitHub from '../github';
|
||||
import SharedWorkspaceLocking from './services/shared-workspace-locking';
|
||||
|
||||
class CloudRunner {
|
||||
private static setup(buildParameters: BuildParameters) {
|
||||
public static Provider: ProviderInterface;
|
||||
public static buildParameters: BuildParameters;
|
||||
private static defaultSecrets: CloudRunnerSecret[];
|
||||
private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
|
||||
static lockedWorkspace: string | undefined;
|
||||
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
|
||||
public static githubCheckId;
|
||||
public 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);
|
||||
CloudRunnerLogger.log(`Setting up cloud runner`);
|
||||
CloudRunner.buildParameters = buildParameters;
|
||||
CloudRunner.setupSelectedBuildPlatform();
|
||||
CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets();
|
||||
CloudRunner.cloudRunnerEnvironmentVariables =
|
||||
TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters);
|
||||
if (GitHub.githubInputEnabled) {
|
||||
const buildParameterPropertyNames = Object.getOwnPropertyNames(buildParameters);
|
||||
for (const element of CloudRunner.cloudRunnerEnvironmentVariables) {
|
||||
// CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element.name)} = ${element.value}`);
|
||||
core.setOutput(Input.ToEnvVarFormat(element.name), element.value);
|
||||
}
|
||||
for (const element of buildParameterPropertyNames) {
|
||||
// CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`);
|
||||
core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]);
|
||||
}
|
||||
core.setOutput(
|
||||
Input.ToEnvVarFormat(`buildArtifact`),
|
||||
`build-${CloudRunner.buildParameters.buildGuid}.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static setupBuildPlatform() {
|
||||
switch (CloudRunnerState.buildParams.cloudRunnerCluster) {
|
||||
private static setupSelectedBuildPlatform() {
|
||||
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.cloudRunnerCluster}`);
|
||||
switch (CloudRunner.buildParameters.cloudRunnerCluster) {
|
||||
case 'k8s':
|
||||
CloudRunnerLogger.log('Cloud Runner platform selected Kubernetes');
|
||||
CloudRunnerState.CloudRunnerProviderPlatform = new Kubernetes(CloudRunnerState.buildParams);
|
||||
CloudRunner.Provider = new Kubernetes(CloudRunner.buildParameters);
|
||||
break;
|
||||
default:
|
||||
case 'aws':
|
||||
CloudRunnerLogger.log('Cloud Runner platform selected AWS');
|
||||
CloudRunnerState.CloudRunnerProviderPlatform = new AWSBuildPlatform(CloudRunnerState.buildParams);
|
||||
CloudRunner.Provider = new AwsBuildPlatform(CloudRunner.buildParameters);
|
||||
break;
|
||||
case 'test':
|
||||
CloudRunner.Provider = new TestCloudRunner();
|
||||
break;
|
||||
case 'local-docker':
|
||||
CloudRunner.Provider = new LocalDockerCloudRunner();
|
||||
break;
|
||||
case 'local-system':
|
||||
CloudRunner.Provider = new LocalCloudRunner();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -37,34 +75,70 @@ class CloudRunner {
|
||||
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,
|
||||
CloudRunner.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid);
|
||||
|
||||
if (buildParameters.retainWorkspace) {
|
||||
CloudRunner.lockedWorkspace = `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`;
|
||||
|
||||
const result = await SharedWorkspaceLocking.GetOrCreateLockedWorkspace(
|
||||
CloudRunner.lockedWorkspace,
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
CloudRunnerLogger.logLine(`Using retained workspace ${CloudRunner.lockedWorkspace}`);
|
||||
CloudRunner.cloudRunnerEnvironmentVariables = [
|
||||
...CloudRunner.cloudRunnerEnvironmentVariables,
|
||||
{ name: `LOCKED_WORKSPACE`, value: CloudRunner.lockedWorkspace },
|
||||
];
|
||||
} else {
|
||||
CloudRunnerLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`);
|
||||
buildParameters.retainWorkspace = false;
|
||||
CloudRunner.lockedWorkspace = undefined;
|
||||
}
|
||||
}
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Setup shared cloud runner resources');
|
||||
await CloudRunner.Provider.setupWorkflow(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
CloudRunner.buildParameters.branch,
|
||||
CloudRunner.defaultSecrets,
|
||||
);
|
||||
core.endGroup();
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters.buildGuid);
|
||||
const output = await new WorkflowCompositionRoot().run(
|
||||
new CloudRunnerStepState(
|
||||
baseImage,
|
||||
TaskParameterSerializer.readBuildEnvironmentVariables(),
|
||||
CloudRunnerState.defaultSecrets,
|
||||
),
|
||||
new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets),
|
||||
);
|
||||
core.startGroup('Cleanup');
|
||||
await CloudRunnerState.CloudRunnerProviderPlatform.cleanupSharedResources(
|
||||
CloudRunnerState.buildParams.buildGuid,
|
||||
CloudRunnerState.buildParams,
|
||||
CloudRunnerState.branchName,
|
||||
CloudRunnerState.defaultSecrets,
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Cleanup shared cloud runner resources');
|
||||
await CloudRunner.Provider.cleanupWorkflow(
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
CloudRunner.buildParameters.branch,
|
||||
CloudRunner.defaultSecrets,
|
||||
);
|
||||
CloudRunnerLogger.log(`Cleanup complete`);
|
||||
core.endGroup();
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, `success`, `success`, `completed`);
|
||||
|
||||
if (CloudRunner.buildParameters.retainWorkspace) {
|
||||
await SharedWorkspaceLocking.ReleaseWorkspace(
|
||||
CloudRunner.lockedWorkspace || ``,
|
||||
CloudRunner.buildParameters.buildGuid,
|
||||
CloudRunner.buildParameters,
|
||||
);
|
||||
CloudRunner.lockedWorkspace = undefined;
|
||||
}
|
||||
|
||||
if (buildParameters.constantGarbageCollection) {
|
||||
CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageCollectionMaxAge, true, true);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
core.endGroup();
|
||||
await CloudRunnerError.handleException(error);
|
||||
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, error, `failure`, `completed`);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
await CloudRunnerError.handleException(error, CloudRunner.buildParameters, CloudRunner.defaultSecrets);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import { CloudRunnerState } from '../state/cloud-runner-state';
|
||||
import * as core from '@actions/core';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import BuildParameters from '../../build-parameters';
|
||||
|
||||
export class CloudRunnerError {
|
||||
public static async handleException(error: unknown) {
|
||||
public static async handleException(error: unknown, buildParameters: BuildParameters, secrets: CloudRunnerSecret[]) {
|
||||
CloudRunnerLogger.error(JSON.stringify(error, undefined, 4));
|
||||
core.setFailed('Cloud Runner failed');
|
||||
await CloudRunnerState.CloudRunnerProviderPlatform.cleanupSharedResources(
|
||||
CloudRunnerState.buildParams.buildGuid,
|
||||
CloudRunnerState.buildParams,
|
||||
CloudRunnerState.branchName,
|
||||
CloudRunnerState.defaultSecrets,
|
||||
);
|
||||
if (CloudRunner.Provider !== undefined) {
|
||||
await CloudRunner.Provider.cleanupWorkflow(
|
||||
buildParameters.buildGuid,
|
||||
buildParameters,
|
||||
buildParameters.branch,
|
||||
secrets,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
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<string> {
|
||||
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;
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
@@ -1,8 +1,7 @@
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
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';
|
||||
import { BaseStackFormation } from './cloud-formations/base-stack-formation';
|
||||
const crypto = require('crypto');
|
||||
|
||||
export class AWSBaseStack {
|
||||
@@ -14,7 +13,7 @@ export class AWSBaseStack {
|
||||
async setupBaseStack(CF: SDK.CloudFormation) {
|
||||
const baseStackName = this.baseStackName;
|
||||
|
||||
const baseStack = fs.readFileSync(path.join(__dirname, 'cloud-formations', 'base-setup.yml'), 'utf8');
|
||||
const baseStack = BaseStackFormation.formation;
|
||||
|
||||
// Cloud Formation Input
|
||||
const describeStackInput: SDK.CloudFormation.DescribeStacksInput = {
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from 'fs';
|
||||
import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation';
|
||||
|
||||
export class AWSTemplates {
|
||||
export class AWSCloudFormationTemplates {
|
||||
public static getParameterTemplate(p1) {
|
||||
return `
|
||||
${p1}:
|
||||
@@ -29,10 +29,11 @@ export class AWSTemplates {
|
||||
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');
|
||||
return TaskDefinitionFormation.formation;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import * as SDK from 'aws-sdk';
|
||||
import * as core from '@actions/core';
|
||||
import { Input } from '../..';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
|
||||
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) {
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log('Getting events and resources for task stack');
|
||||
const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents;
|
||||
CloudRunnerLogger.log(JSON.stringify(events, undefined, 4));
|
||||
207
src/model/cloud-runner/providers/aws/aws-job-stack.ts
Normal file
207
src/model/cloud-runner/providers/aws/aws-job-stack.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import * as SDK from 'aws-sdk';
|
||||
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { AWSError } from './aws-error';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import { CleanupCronFormation } from './cloud-formations/cleanup-cron-formation';
|
||||
import CloudRunnerOptions from '../../cloud-runner-options';
|
||||
import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation';
|
||||
|
||||
export class AWSJobStack {
|
||||
private baseStackName: string;
|
||||
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<CloudRunnerAWSTaskDef> {
|
||||
const taskDefStackName = `${this.baseStackName}-${buildGuid}`;
|
||||
let taskDefCloudFormation = AWSCloudFormationTemplates.readTaskCloudFormationTemplate();
|
||||
const cpu = CloudRunner.buildParameters.cloudRunnerCpu || '1024';
|
||||
const memory = CloudRunner.buildParameters.cloudRunnerMemory || '3072';
|
||||
taskDefCloudFormation = taskDefCloudFormation.replace(
|
||||
`ContainerCpu:
|
||||
Default: 1024`,
|
||||
`ContainerCpu:
|
||||
Default: ${Number.parseInt(cpu)}`,
|
||||
);
|
||||
taskDefCloudFormation = taskDefCloudFormation.replace(
|
||||
`ContainerMemory:
|
||||
Default: 2048`,
|
||||
`ContainerMemory:
|
||||
Default: ${Number.parseInt(memory)}`,
|
||||
);
|
||||
if (CloudRunnerOptions.watchCloudRunnerToEnd) {
|
||||
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
|
||||
taskDefCloudFormation,
|
||||
'# template resources logstream',
|
||||
TaskDefinitionFormation.streamLogs,
|
||||
);
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
secret.ParameterKey = `${buildGuid.replace(/[^\dA-Za-z]/g, '')}${secret.ParameterKey.replace(
|
||||
/[^\dA-Za-z]/g,
|
||||
'',
|
||||
)}`;
|
||||
if (typeof secret.ParameterValue == 'number') {
|
||||
secret.ParameterValue = `${secret.ParameterValue}`;
|
||||
}
|
||||
if (!secret.ParameterValue || secret.ParameterValue === '') {
|
||||
secrets = secrets.filter((x) => x !== secret);
|
||||
continue;
|
||||
}
|
||||
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
|
||||
taskDefCloudFormation,
|
||||
'p1 - input',
|
||||
AWSCloudFormationTemplates.getParameterTemplate(secret.ParameterKey),
|
||||
);
|
||||
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
|
||||
taskDefCloudFormation,
|
||||
'# template resources secrets',
|
||||
AWSCloudFormationTemplates.getSecretTemplate(`${secret.ParameterKey}`),
|
||||
);
|
||||
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
|
||||
taskDefCloudFormation,
|
||||
'p3 - container def',
|
||||
AWSCloudFormationTemplates.getSecretDefinitionTemplate(secret.EnvironmentVariable, secret.ParameterKey),
|
||||
);
|
||||
}
|
||||
const secretsMappedToCloudFormationParameters = secrets.map((x) => {
|
||||
return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue };
|
||||
});
|
||||
const logGroupName = `${this.baseStackName}/${taskDefStackName}`;
|
||||
const parameters = [
|
||||
{
|
||||
ParameterKey: 'EnvironmentName',
|
||||
ParameterValue: this.baseStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'ImageUrl',
|
||||
ParameterValue: image,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'ServiceName',
|
||||
ParameterValue: taskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'LogGroupName',
|
||||
ParameterValue: logGroupName,
|
||||
},
|
||||
{
|
||||
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,
|
||||
];
|
||||
CloudRunnerLogger.log(
|
||||
`Starting AWS job with memory: ${CloudRunner.buildParameters.cloudRunnerMemory} cpu: ${CloudRunner.buildParameters.cloudRunnerCpu}`,
|
||||
);
|
||||
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)}`);
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
const createStackInput: SDK.CloudFormation.CreateStackInput = {
|
||||
StackName: taskDefStackName,
|
||||
TemplateBody: taskDefCloudFormation,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
Parameters: parameters,
|
||||
};
|
||||
|
||||
try {
|
||||
CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`);
|
||||
await CF.createStack(createStackInput).promise();
|
||||
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise();
|
||||
} catch (error) {
|
||||
await AWSError.handleStackCreationFailure(error, CF, taskDefStackName);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const createCleanupStackInput: SDK.CloudFormation.CreateStackInput = {
|
||||
StackName: `${taskDefStackName}-cleanup`,
|
||||
TemplateBody: CleanupCronFormation.formation,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
Parameters: [
|
||||
{
|
||||
ParameterKey: 'StackName',
|
||||
ParameterValue: taskDefStackName,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'DeleteStackName',
|
||||
ParameterValue: `${taskDefStackName}-cleanup`,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'TTL',
|
||||
ParameterValue: `1080`,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'BUILDGUID',
|
||||
ParameterValue: CloudRunner.buildParameters.buildGuid,
|
||||
},
|
||||
{
|
||||
ParameterKey: 'EnvironmentName',
|
||||
ParameterValue: this.baseStackName,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (CloudRunnerOptions.useCleanupCron) {
|
||||
try {
|
||||
CloudRunnerLogger.log(`Creating job cleanup formation`);
|
||||
CF.createStack(createCleanupStackInput).promise();
|
||||
|
||||
// await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise();
|
||||
} catch (error) {
|
||||
await AWSError.handleStackCreationFailure(error, CF, taskDefStackName);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const taskDefResources = (
|
||||
await CF.describeStackResources({
|
||||
StackName: taskDefStackName,
|
||||
}).promise()
|
||||
).StackResources;
|
||||
|
||||
const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources;
|
||||
|
||||
return {
|
||||
taskDefStackName,
|
||||
taskDefCloudFormation,
|
||||
taskDefResources,
|
||||
baseResources,
|
||||
};
|
||||
}
|
||||
}
|
||||
241
src/model/cloud-runner/providers/aws/aws-task-runner.ts
Normal file
241
src/model/cloud-runner/providers/aws/aws-task-runner.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
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 CloudRunner from '../../cloud-runner';
|
||||
import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks';
|
||||
import { FollowLogStreamService } from '../../services/follow-log-stream-service';
|
||||
import CloudRunnerOptions from '../../cloud-runner-options';
|
||||
import GitHub from '../../../github';
|
||||
|
||||
class AWSTaskRunner {
|
||||
public static ECS: AWS.ECS;
|
||||
public static Kinesis: AWS.Kinesis;
|
||||
private static readonly encodedUnderscore = `$252F`;
|
||||
static async runTask(
|
||||
taskDef: CloudRunnerAWSTaskDef,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
commands: string,
|
||||
): Promise<{ output: string; shouldCleanup: boolean }> {
|
||||
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
|
||||
const taskDefinition =
|
||||
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || '';
|
||||
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 AWSTaskRunner.ECS.runTask({
|
||||
cluster,
|
||||
taskDefinition,
|
||||
platformVersion: '1.4.0',
|
||||
overrides: {
|
||||
containerOverrides: [
|
||||
{
|
||||
name: taskDef.taskDefStackName,
|
||||
environment,
|
||||
command: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(commands, CloudRunner.buildParameters)],
|
||||
},
|
||||
],
|
||||
},
|
||||
launchType: 'FARGATE',
|
||||
networkConfiguration: {
|
||||
awsvpcConfiguration: {
|
||||
subnets: [SubnetOne, SubnetTwo],
|
||||
assignPublicIp: 'ENABLED',
|
||||
securityGroups: [ContainerSecurityGroup],
|
||||
},
|
||||
},
|
||||
}).promise();
|
||||
const taskArn = task.tasks?.[0].taskArn || '';
|
||||
CloudRunnerLogger.log('Cloud runner job is starting');
|
||||
await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster);
|
||||
CloudRunnerLogger.log(
|
||||
`Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus} Watch:${
|
||||
CloudRunnerOptions.watchCloudRunnerToEnd
|
||||
} Async:${CloudRunnerOptions.asyncCloudRunner}`,
|
||||
);
|
||||
if (!CloudRunnerOptions.watchCloudRunnerToEnd) {
|
||||
const shouldCleanup: boolean = false;
|
||||
const output: string = '';
|
||||
CloudRunnerLogger.log(`Watch Cloud Runner To End: false`);
|
||||
|
||||
return { output, shouldCleanup };
|
||||
}
|
||||
|
||||
CloudRunnerLogger.log(`Streaming...`);
|
||||
const { output, shouldCleanup } = await this.streamLogsUntilTaskStops(cluster, taskArn, streamName);
|
||||
await new Promise((resolve) => resolve(5000));
|
||||
const taskData = await AWSTaskRunner.describeTasks(cluster, taskArn);
|
||||
const containerState = taskData.containers?.[0];
|
||||
const exitCode = containerState?.exitCode || undefined;
|
||||
CloudRunnerLogger.log(`Container State: ${JSON.stringify(containerState, undefined, 4)}`);
|
||||
const wasSuccessful = exitCode === 0 || (exitCode === undefined && taskData.lastStatus === 'RUNNING');
|
||||
if (wasSuccessful) {
|
||||
CloudRunnerLogger.log(`Cloud runner job has finished successfully`);
|
||||
|
||||
return { output, shouldCleanup };
|
||||
} else {
|
||||
if (taskData.stoppedReason === 'Essential container in task exited' && exitCode === 1) {
|
||||
throw new Error('Container exited with code 1');
|
||||
}
|
||||
const message = `Cloud runner job exit code ${exitCode}`;
|
||||
taskData.overrides = undefined;
|
||||
taskData.attachments = undefined;
|
||||
CloudRunnerLogger.log(`${message} ${JSON.stringify(taskData, undefined, 4)}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static async waitUntilTaskRunning(taskArn: string, cluster: string) {
|
||||
try {
|
||||
await AWSTaskRunner.ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
|
||||
} catch (error_) {
|
||||
const error = error_ as Error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
CloudRunnerLogger.log(
|
||||
`Cloud runner job has ended ${
|
||||
(await AWSTaskRunner.describeTasks(cluster, taskArn)).containers?.[0].lastStatus
|
||||
}`,
|
||||
);
|
||||
|
||||
core.setFailed(error);
|
||||
core.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async describeTasks(clusterName: string, taskArn: string) {
|
||||
const tasks = await AWSTaskRunner.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(clusterName: string, taskArn: string, kinesisStreamName: string) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
CloudRunnerLogger.log(`Streaming...`);
|
||||
const stream = await AWSTaskRunner.getLogStream(kinesisStreamName);
|
||||
let iterator = await AWSTaskRunner.getLogIterator(stream);
|
||||
|
||||
const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${Input.region}#logsV2:log-groups/log-group/${CloudRunner.buildParameters.awsBaseStackName}${AWSTaskRunner.encodedUnderscore}${CloudRunner.buildParameters.awsBaseStackName}-${CloudRunner.buildParameters.buildGuid}`;
|
||||
CloudRunnerLogger.log(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`);
|
||||
await GitHub.updateGitHubCheck(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`, ``);
|
||||
let shouldReadLogs = true;
|
||||
let shouldCleanup = true;
|
||||
let timestamp: number = 0;
|
||||
let output = '';
|
||||
while (shouldReadLogs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const taskData = await AWSTaskRunner.describeTasks(clusterName, taskArn);
|
||||
({ timestamp, shouldReadLogs } = AWSTaskRunner.checkStreamingShouldContinue(taskData, timestamp, shouldReadLogs));
|
||||
({ iterator, shouldReadLogs, output, shouldCleanup } = await AWSTaskRunner.handleLogStreamIteration(
|
||||
iterator,
|
||||
shouldReadLogs,
|
||||
output,
|
||||
shouldCleanup,
|
||||
));
|
||||
}
|
||||
|
||||
return { output, shouldCleanup };
|
||||
}
|
||||
|
||||
private static async handleLogStreamIteration(
|
||||
iterator: string,
|
||||
shouldReadLogs: boolean,
|
||||
output: string,
|
||||
shouldCleanup: boolean,
|
||||
) {
|
||||
const records = await AWSTaskRunner.Kinesis.getRecords({
|
||||
ShardIterator: iterator,
|
||||
}).promise();
|
||||
iterator = records.NextShardIterator || '';
|
||||
({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords(
|
||||
records,
|
||||
iterator,
|
||||
shouldReadLogs,
|
||||
output,
|
||||
shouldCleanup,
|
||||
));
|
||||
|
||||
return { iterator, shouldReadLogs, output, shouldCleanup };
|
||||
}
|
||||
|
||||
private static checkStreamingShouldContinue(taskData: AWS.ECS.Task, timestamp: number, shouldReadLogs: boolean) {
|
||||
if (taskData?.lastStatus === 'UNKNOWN') {
|
||||
CloudRunnerLogger.log('## Cloud runner job unknwon');
|
||||
}
|
||||
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,
|
||||
shouldReadLogs: boolean,
|
||||
output: string,
|
||||
shouldCleanup: boolean,
|
||||
) {
|
||||
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++) {
|
||||
const message = json.logEvents[logEventsIndex].message;
|
||||
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
|
||||
message,
|
||||
shouldReadLogs,
|
||||
shouldCleanup,
|
||||
output,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { shouldReadLogs, output, shouldCleanup };
|
||||
}
|
||||
|
||||
private static async getLogStream(kinesisStreamName: string) {
|
||||
return await AWSTaskRunner.Kinesis.describeStream({
|
||||
StreamName: kinesisStreamName,
|
||||
}).promise();
|
||||
}
|
||||
|
||||
private static async getLogIterator(stream) {
|
||||
return (
|
||||
(
|
||||
await AWSTaskRunner.Kinesis.getShardIterator({
|
||||
ShardIteratorType: 'TRIM_HORIZON',
|
||||
StreamName: stream.StreamDescription.StreamName,
|
||||
ShardId: stream.StreamDescription.Shards[0].ShardId,
|
||||
}).promise()
|
||||
).ShardIterator || ''
|
||||
);
|
||||
}
|
||||
}
|
||||
export default AWSTaskRunner;
|
||||
@@ -1,7 +1,7 @@
|
||||
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.
|
||||
export class BaseStackFormation {
|
||||
public static readonly baseStackDecription = `Game-CI base stack`;
|
||||
public static readonly formation: string = `AWSTemplateFormatVersion: '2010-09-09'
|
||||
Description: ${BaseStackFormation.baseStackDecription}
|
||||
Parameters:
|
||||
EnvironmentName:
|
||||
Type: String
|
||||
@@ -47,6 +47,11 @@ Resources:
|
||||
EnableDnsHostnames: true
|
||||
CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']
|
||||
|
||||
MainBucket:
|
||||
Type: "AWS::S3::Bucket"
|
||||
Properties:
|
||||
BucketName: !Ref EnvironmentName
|
||||
|
||||
EFSServerSecurityGroup:
|
||||
Type: AWS::EC2::SecurityGroup
|
||||
Properties:
|
||||
@@ -335,57 +340,58 @@ Outputs:
|
||||
Description: 'The connection endpoint for the database.'
|
||||
Value: !Ref EfsFileStorage
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:EfsFileStorageId
|
||||
Name: !Sub ${'${EnvironmentName}'}:EfsFileStorageId
|
||||
ClusterName:
|
||||
Description: The name of the ECS cluster
|
||||
Value: !Ref 'ECSCluster'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:ClusterName
|
||||
Name: !Sub${' ${EnvironmentName}'}:ClusterName
|
||||
AutoscalingRole:
|
||||
Description: The ARN of the role used for autoscaling
|
||||
Value: !GetAtt 'AutoscalingRole.Arn'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:AutoscalingRole
|
||||
Name: !Sub ${'${EnvironmentName}'}:AutoscalingRole
|
||||
ECSRole:
|
||||
Description: The ARN of the ECS role
|
||||
Value: !GetAtt 'ECSRole.Arn'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:ECSRole
|
||||
Name: !Sub ${'${EnvironmentName}'}:ECSRole
|
||||
ECSTaskExecutionRole:
|
||||
Description: The ARN of the ECS role tsk execution role
|
||||
Value: !GetAtt 'ECSTaskExecutionRole.Arn'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:ECSTaskExecutionRole
|
||||
Name: !Sub ${'${EnvironmentName}'}:ECSTaskExecutionRole
|
||||
|
||||
DeleteCFNLambdaExecutionRole:
|
||||
Description: Lambda execution role for cleaning up cloud formations
|
||||
Value: !GetAtt 'DeleteCFNLambdaExecutionRole.Arn'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:DeleteCFNLambdaExecutionRole
|
||||
Name: !Sub ${'${EnvironmentName}'}:DeleteCFNLambdaExecutionRole
|
||||
|
||||
CloudWatchIAMRole:
|
||||
Description: The ARN of the CloudWatch role for subscription filter
|
||||
Value: !GetAtt 'CloudWatchIAMRole.Arn'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:CloudWatchIAMRole
|
||||
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
|
||||
Name: !Sub ${'${EnvironmentName}'}:VpcId
|
||||
PublicSubnetOne:
|
||||
Description: Public subnet one
|
||||
Value: !Ref 'PublicSubnetOne'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:PublicSubnetOne
|
||||
Name: !Sub ${'${EnvironmentName}'}:PublicSubnetOne
|
||||
PublicSubnetTwo:
|
||||
Description: Public subnet two
|
||||
Value: !Ref 'PublicSubnetTwo'
|
||||
Export:
|
||||
Name: !Sub ${EnvironmentName}:PublicSubnetTwo
|
||||
|
||||
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
|
||||
Name: !Sub ${'${EnvironmentName}'}:ContainerSecurityGroup
|
||||
`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
export class CleanupCronFormation {
|
||||
public static readonly formation: string = `AWSTemplateFormatVersion: '2010-09-09'
|
||||
Description: Schedule automatic deletion of CloudFormation stacks
|
||||
Metadata:
|
||||
AWS::CloudFormation::Interface:
|
||||
@@ -64,10 +65,10 @@ Resources:
|
||||
stackName: !Ref 'StackName'
|
||||
deleteStackName: !Ref 'DeleteStackName'
|
||||
Handler: "index.handler"
|
||||
Runtime: "python3.6"
|
||||
Runtime: "python3.9"
|
||||
Timeout: "5"
|
||||
Role:
|
||||
'Fn::ImportValue': !Sub '${EnvironmentName}:DeleteCFNLambdaExecutionRole'
|
||||
'Fn::ImportValue': !Sub '\${EnvironmentName}:DeleteCFNLambdaExecutionRole'
|
||||
DeleteStackEventRule:
|
||||
DependsOn:
|
||||
- DeleteCFNLambda
|
||||
@@ -130,10 +131,10 @@ Resources:
|
||||
status = cfnresponse.FAILED
|
||||
cfnresponse.send(event, context, status, {}, None)
|
||||
Handler: "index.handler"
|
||||
Runtime: "python3.6"
|
||||
Runtime: "python3.9"
|
||||
Timeout: "5"
|
||||
Role:
|
||||
'Fn::ImportValue': !Sub '${EnvironmentName}:DeleteCFNLambdaExecutionRole'
|
||||
'Fn::ImportValue': !Sub '\${EnvironmentName}:DeleteCFNLambdaExecutionRole'
|
||||
GenerateCronExpression:
|
||||
Type: "Custom::GenerateCronExpression"
|
||||
Version: "1.0"
|
||||
@@ -141,3 +142,5 @@ Resources:
|
||||
Name: !Join [ "", [ 'GenerateCronExpression', !Ref BUILDGUID ] ]
|
||||
ServiceToken: !GetAtt GenerateCronExpLambda.Arn
|
||||
ttl: !Ref 'TTL'
|
||||
`;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
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.
|
||||
export class TaskDefinitionFormation {
|
||||
public static readonly description: string = `Game CI Cloud Runner Task Stack`;
|
||||
public static readonly formation: string = `AWSTemplateFormatVersion: 2010-09-09
|
||||
Description: ${TaskDefinitionFormation.description}
|
||||
Parameters:
|
||||
EnvironmentName:
|
||||
Type: String
|
||||
@@ -12,6 +11,10 @@ Parameters:
|
||||
Type: String
|
||||
Default: example
|
||||
Description: A name for the service
|
||||
LogGroupName:
|
||||
Type: String
|
||||
Default: example
|
||||
Description: Name to use for the log group created for this task
|
||||
ImageUrl:
|
||||
Type: String
|
||||
Default: nginx
|
||||
@@ -23,12 +26,12 @@ Parameters:
|
||||
Default: 80
|
||||
Description: What port number the application inside the docker container is binding to
|
||||
ContainerCpu:
|
||||
Type: Number
|
||||
Default: 1024
|
||||
Type: Number
|
||||
Description: How much CPU to give the container. 1024 is 1 CPU
|
||||
ContainerMemory:
|
||||
Type: Number
|
||||
Default: 2048
|
||||
Type: Number
|
||||
Description: How much memory in megabytes to give the container
|
||||
BUILDGUID:
|
||||
Type: String
|
||||
@@ -69,37 +72,13 @@ Resources:
|
||||
LogGroup:
|
||||
Type: 'AWS::Logs::LogGroup'
|
||||
Properties:
|
||||
LogGroupName: !Ref ServiceName
|
||||
LogGroupName: !Ref LogGroupName
|
||||
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 resources secrets
|
||||
|
||||
# template secrets p2 - secret
|
||||
# template resources logstream
|
||||
|
||||
TaskDefinition:
|
||||
Type: 'AWS::ECS::TaskDefinition'
|
||||
@@ -112,12 +91,12 @@ Resources:
|
||||
- Name: efs-data
|
||||
EFSVolumeConfiguration:
|
||||
FilesystemId:
|
||||
'Fn::ImportValue': !Sub '${EnvironmentName}:EfsFileStorageId'
|
||||
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:EfsFileStorageId'
|
||||
TransitEncryption: ENABLED
|
||||
RequiresCompatibilities:
|
||||
- FARGATE
|
||||
ExecutionRoleArn:
|
||||
'Fn::ImportValue': !Sub '${EnvironmentName}:ECSTaskExecutionRole'
|
||||
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:ECSTaskExecutionRole'
|
||||
TaskRoleArn:
|
||||
'Fn::If':
|
||||
- HasCustomRole
|
||||
@@ -150,72 +129,37 @@ Resources:
|
||||
LogConfiguration:
|
||||
LogDriver: awslogs
|
||||
Options:
|
||||
awslogs-group: !Ref ServiceName
|
||||
awslogs-group: !Ref LogGroupName
|
||||
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:
|
||||
`;
|
||||
public static streamLogs = `
|
||||
SubscriptionFilter:
|
||||
Type: 'AWS::Logs::SubscriptionFilter'
|
||||
Properties:
|
||||
FilterPattern: ''
|
||||
RoleArn:
|
||||
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:CloudWatchIAMRole'
|
||||
LogGroupName: !Ref LogGroupName
|
||||
DestinationArn:
|
||||
'Fn::GetAtt':
|
||||
- KinesisStream
|
||||
- Arn
|
||||
Metadata:
|
||||
'AWS::CloudFormation::Designer':
|
||||
id: 7f809e91-9e5d-4678-98c1-c5085956c480
|
||||
target:
|
||||
id: aece53ae-b82d-4267-bc16-ed964b05db27
|
||||
z: 11
|
||||
14eb957b-f094-4653-93c4-77b2f851953c:
|
||||
source:
|
||||
id: 7f809e91-9e5d-4678-98c1-c5085956c480
|
||||
target:
|
||||
DependsOn:
|
||||
- LogGroup
|
||||
- KinesisStream
|
||||
KinesisStream:
|
||||
Type: 'AWS::Kinesis::Stream'
|
||||
Properties:
|
||||
Name: !Ref ServiceName
|
||||
ShardCount: 1
|
||||
Metadata:
|
||||
'AWS::CloudFormation::Designer':
|
||||
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
|
||||
`;
|
||||
}
|
||||
@@ -1,22 +1,62 @@
|
||||
import * as SDK from 'aws-sdk';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable';
|
||||
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 '../..';
|
||||
import AwsTaskRunner from './aws-task-runner';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { AWSJobStack as AwsJobStack } from './aws-job-stack';
|
||||
import { AWSBaseStack as AwsBaseStack } from './aws-base-stack';
|
||||
import { Input } from '../../..';
|
||||
import { GarbageCollectionService } from './services/garbage-collection-service';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import { TaskService } from './services/task-service';
|
||||
import CloudRunnerOptions from '../../cloud-runner-options';
|
||||
|
||||
class AWSBuildEnvironment implements CloudRunnerProviderInterface {
|
||||
class AWSBuildEnvironment implements ProviderInterface {
|
||||
private baseStackName: string;
|
||||
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
this.baseStackName = buildParameters.awsBaseStackName;
|
||||
}
|
||||
async cleanupSharedResources(
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
await TaskService.getCloudFormationJobStacks();
|
||||
await TaskService.getLogGroups();
|
||||
await TaskService.getTasks();
|
||||
|
||||
return [];
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async watchWorkflow(): Promise<string> {
|
||||
return await TaskService.watch();
|
||||
}
|
||||
|
||||
async listOtherResources(): Promise<string> {
|
||||
await TaskService.getLogGroups();
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
filter: string,
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
await GarbageCollectionService.cleanup(!previewOnly);
|
||||
|
||||
return ``;
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -26,7 +66,7 @@ class AWSBuildEnvironment implements CloudRunnerProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
async setupSharedResources(
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -37,7 +77,7 @@ class AWSBuildEnvironment implements CloudRunnerProviderInterface {
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
|
||||
async runTask(
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
@@ -49,12 +89,14 @@ class AWSBuildEnvironment implements CloudRunnerProviderInterface {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ECS = new SDK.ECS();
|
||||
const CF = new SDK.CloudFormation();
|
||||
AwsTaskRunner.ECS = ECS;
|
||||
AwsTaskRunner.Kinesis = new SDK.Kinesis();
|
||||
CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`);
|
||||
const entrypoint = ['/bin/sh'];
|
||||
const startTimeMs = Date.now();
|
||||
|
||||
await new AWSBaseStack(this.baseStackName).setupBaseStack(CF);
|
||||
const taskDef = await new AWSJobStack(this.baseStackName).setupCloudFormations(
|
||||
await new AwsBaseStack(this.baseStackName).setupBaseStack(CF);
|
||||
const taskDef = await new AwsJobStack(this.baseStackName).setupCloudFormations(
|
||||
CF,
|
||||
buildGuid,
|
||||
image,
|
||||
@@ -66,20 +108,25 @@ class AWSBuildEnvironment implements CloudRunnerProviderInterface {
|
||||
);
|
||||
|
||||
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);
|
||||
const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, commands);
|
||||
postRunTaskTimeMs = Date.now();
|
||||
CloudRunnerLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`);
|
||||
} finally {
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
if (shouldCleanup) {
|
||||
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;
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`error running task ${error}`);
|
||||
await this.cleanupResources(CF, taskDef);
|
||||
throw error;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async cleanupResources(CF: SDK.CloudFormation, taskDef: CloudRunnerAWSTaskDef) {
|
||||
@@ -87,6 +134,11 @@ class AWSBuildEnvironment implements CloudRunnerProviderInterface {
|
||||
await CF.deleteStack({
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
if (CloudRunnerOptions.useCleanupCron) {
|
||||
await CF.deleteStack({
|
||||
StackName: `${taskDef.taskDefStackName}-cleanup`,
|
||||
}).promise();
|
||||
}
|
||||
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: taskDef.taskDefStackName,
|
||||
@@ -0,0 +1,66 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
|
||||
import { TaskService } from './task-service';
|
||||
|
||||
export class GarbageCollectionService {
|
||||
static isOlderThan1day(date: any) {
|
||||
const ageDate = new Date(date.getTime() - Date.now());
|
||||
|
||||
return ageDate.getDay() > 0;
|
||||
}
|
||||
|
||||
public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const ecs = new AWS.ECS();
|
||||
const cwl = new AWS.CloudWatchLogs();
|
||||
const taskDefinitionsInUse = new Array();
|
||||
const tasks = await TaskService.getTasks();
|
||||
for (const task of tasks) {
|
||||
const { taskElement, element } = task;
|
||||
taskDefinitionsInUse.push(taskElement.taskDefinitionArn);
|
||||
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.CreatedAt))) {
|
||||
CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
|
||||
await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise();
|
||||
}
|
||||
}
|
||||
const jobStacks = await TaskService.getCloudFormationJobStacks();
|
||||
for (const element of jobStacks) {
|
||||
if (
|
||||
(await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some(
|
||||
(x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId),
|
||||
)
|
||||
) {
|
||||
CloudRunnerLogger.log(`Skipping ${element.StackName} - active task was running not deleting`);
|
||||
|
||||
return;
|
||||
}
|
||||
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) {
|
||||
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
|
||||
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
|
||||
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName };
|
||||
await CF.deleteStack(deleteStackInput).promise();
|
||||
}
|
||||
}
|
||||
const logGroups = await TaskService.getLogGroups();
|
||||
for (const element of logGroups) {
|
||||
if (
|
||||
deleteResources &&
|
||||
(!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.createdAt)))
|
||||
) {
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise();
|
||||
}
|
||||
}
|
||||
|
||||
const locks = await TaskService.getLocks();
|
||||
for (const element of locks) {
|
||||
CloudRunnerLogger.log(`Lock: ${element.Key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/model/cloud-runner/providers/aws/services/task-service.ts
Normal file
168
src/model/cloud-runner/providers/aws/services/task-service.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
|
||||
import { BaseStackFormation } from '../cloud-formations/base-stack-formation';
|
||||
import AwsTaskRunner from '../aws-task-runner';
|
||||
import { ListObjectsRequest } from 'aws-sdk/clients/s3';
|
||||
import CloudRunner from '../../../cloud-runner';
|
||||
|
||||
export class TaskService {
|
||||
static async watch() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { output, shouldCleanup } = await AwsTaskRunner.streamLogsUntilTaskStops(
|
||||
process.env.cluster || ``,
|
||||
process.env.taskArn || ``,
|
||||
process.env.streamName || ``,
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
public static async getCloudFormationJobStacks() {
|
||||
const result: any[] = [];
|
||||
CloudRunnerLogger.log(``);
|
||||
CloudRunnerLogger.log(`List Cloud Formation Stacks`);
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const stacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) =>
|
||||
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription !== BaseStackFormation.baseStackDecription,
|
||||
) || [];
|
||||
CloudRunnerLogger.log(``);
|
||||
CloudRunnerLogger.log(`Cloud Formation Stacks ${stacks.length}`);
|
||||
for (const element of stacks) {
|
||||
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
result.push(element);
|
||||
}
|
||||
const baseStacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(_x) =>
|
||||
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription,
|
||||
) || [];
|
||||
CloudRunnerLogger.log(``);
|
||||
CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`);
|
||||
for (const element of baseStacks) {
|
||||
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
result.push(element);
|
||||
}
|
||||
CloudRunnerLogger.log(``);
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async getTasks() {
|
||||
const result: any[] = [];
|
||||
CloudRunnerLogger.log(``);
|
||||
CloudRunnerLogger.log(`List Tasks`);
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.ECS();
|
||||
const clusters = (await ecs.listClusters().promise()).clusterArns || [];
|
||||
CloudRunnerLogger.log(`Task Clusters ${clusters.length}`);
|
||||
for (const element of clusters) {
|
||||
const input: AWS.ECS.ListTasksRequest = {
|
||||
cluster: element,
|
||||
};
|
||||
|
||||
const list = (await ecs.listTasks(input).promise()).taskArns || [];
|
||||
if (list.length > 0) {
|
||||
const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element };
|
||||
const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || [];
|
||||
if (describeList.length === 0) {
|
||||
CloudRunnerLogger.log(`No Tasks`);
|
||||
continue;
|
||||
}
|
||||
CloudRunnerLogger.log(`Tasks ${describeList.length}`);
|
||||
for (const taskElement of describeList) {
|
||||
if (taskElement === undefined) {
|
||||
continue;
|
||||
}
|
||||
taskElement.overrides = {};
|
||||
taskElement.attachments = [];
|
||||
if (taskElement.createdAt === undefined) {
|
||||
CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`);
|
||||
continue;
|
||||
}
|
||||
result.push({ taskElement, element });
|
||||
}
|
||||
}
|
||||
}
|
||||
CloudRunnerLogger.log(``);
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async awsDescribeJob(job: string) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const stack = (await CF.listStacks().promise()).StackSummaries?.find((_x) => _x.StackName === job) || undefined;
|
||||
const stackInfo = (await CF.describeStackResources({ StackName: job }).promise()) || undefined;
|
||||
const stackInfo2 = (await CF.describeStacks({ StackName: job }).promise()) || undefined;
|
||||
if (stack === undefined) {
|
||||
throw new Error('stack not defined');
|
||||
}
|
||||
const ageDate: Date = new Date(Date.now() - stack.CreationTime.getTime());
|
||||
const message = `
|
||||
Task Stack ${stack.StackName}
|
||||
Age D${Math.floor(ageDate.getHours() / 24)} H${ageDate.getHours()} M${ageDate.getMinutes()}
|
||||
${JSON.stringify(stack, undefined, 4)}
|
||||
${JSON.stringify(stackInfo, undefined, 4)}
|
||||
${JSON.stringify(stackInfo2, undefined, 4)}
|
||||
`;
|
||||
CloudRunnerLogger.log(message);
|
||||
|
||||
return message;
|
||||
}
|
||||
public static async getLogGroups() {
|
||||
const result: any[] = [];
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.CloudWatchLogs();
|
||||
let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = {
|
||||
/* logGroupNamePrefix: 'game-ci' */
|
||||
};
|
||||
let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
const logGroups = logGroupsDescribe.logGroups || [];
|
||||
while (logGroupsDescribe.nextToken) {
|
||||
logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken };
|
||||
logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
logGroups.push(...(logGroupsDescribe?.logGroups || []));
|
||||
}
|
||||
|
||||
CloudRunnerLogger.log(`Log Groups ${logGroups.length}`);
|
||||
for (const element of logGroups) {
|
||||
if (element.creationTime === undefined) {
|
||||
CloudRunnerLogger.log(`Skipping ${element.logGroupName} no createdAt date`);
|
||||
continue;
|
||||
}
|
||||
const ageDate: Date = new Date(Date.now() - element.creationTime);
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.logGroupName} - Age D${Math.floor(
|
||||
ageDate.getHours() / 24,
|
||||
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
|
||||
);
|
||||
result.push(element);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static async getLocks() {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const s3 = new AWS.S3();
|
||||
const listRequest: ListObjectsRequest = {
|
||||
Bucket: CloudRunner.buildParameters.awsBaseStackName,
|
||||
};
|
||||
const results = await s3.listObjects(listRequest).promise();
|
||||
|
||||
return results.Contents || [];
|
||||
}
|
||||
}
|
||||
156
src/model/cloud-runner/providers/docker/index.ts
Normal file
156
src/model/cloud-runner/providers/docker/index.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import Docker from '../../../docker';
|
||||
import { Action } from '../../..';
|
||||
import { writeFileSync } from 'fs';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import { CloudRunnerSystem } from '../../services/cloud-runner-system';
|
||||
import * as fs from 'fs';
|
||||
|
||||
class LocalDockerCloudRunner implements ProviderInterface {
|
||||
public buildParameters: BuildParameters | undefined;
|
||||
|
||||
listResources(): Promise<ProviderResource[]> {
|
||||
return new Promise((resolve) => resolve([]));
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return new Promise((result) => result(``));
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
const { workspace } = Action;
|
||||
if (
|
||||
fs.existsSync(
|
||||
`${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
}`,
|
||||
)
|
||||
) {
|
||||
await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache/cache/build/`);
|
||||
await CloudRunnerSystem.Run(
|
||||
`rm -r ${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
setupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
this.buildParameters = buildParameters;
|
||||
}
|
||||
|
||||
public async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
CloudRunnerLogger.log(buildGuid);
|
||||
CloudRunnerLogger.log(commands);
|
||||
|
||||
const { workspace, actionFolder } = Action;
|
||||
const content: any[] = [];
|
||||
for (const x of secrets) {
|
||||
content.push({ name: x.EnvironmentVariable, value: x.ParameterValue });
|
||||
}
|
||||
for (const x of environment) {
|
||||
content.push({ name: x.name, value: x.value });
|
||||
}
|
||||
|
||||
// if (this.buildParameters?.cloudRunnerIntegrationTests) {
|
||||
// core.info(JSON.stringify(content, undefined, 4));
|
||||
// core.info(JSON.stringify(secrets, undefined, 4));
|
||||
// core.info(JSON.stringify(environment, undefined, 4));
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line unicorn/no-for-loop
|
||||
for (let index = 0; index < content.length; index++) {
|
||||
if (content[index] === undefined) {
|
||||
delete content[index];
|
||||
}
|
||||
}
|
||||
let myOutput = '';
|
||||
const sharedFolder = `/data/`;
|
||||
|
||||
// core.info(JSON.stringify({ workspace, actionFolder, ...this.buildParameters, ...content }, undefined, 4));
|
||||
const entrypointFilePath = `start.sh`;
|
||||
const fileContents = `#!/bin/bash
|
||||
set -e
|
||||
|
||||
mkdir -p /github/workspace/cloud-runner-cache
|
||||
mkdir -p /data/cache
|
||||
cp -a /github/workspace/cloud-runner-cache/. ${sharedFolder}
|
||||
${commands}
|
||||
cp -a ${sharedFolder}. /github/workspace/cloud-runner-cache/
|
||||
`;
|
||||
writeFileSync(`${workspace}/${entrypointFilePath}`, fileContents, {
|
||||
flag: 'w',
|
||||
});
|
||||
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Running local-docker: \n ${fileContents}`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(`${workspace}/cloud-runner-cache`)) {
|
||||
await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache && du -sh ${workspace}/cloud-runner-cache`);
|
||||
}
|
||||
await Docker.run(
|
||||
image,
|
||||
{ workspace, actionFolder, ...this.buildParameters },
|
||||
false,
|
||||
`chmod +x /github/workspace/${entrypointFilePath} && /github/workspace/${entrypointFilePath}`,
|
||||
content,
|
||||
{
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
myOutput += data.toString();
|
||||
},
|
||||
stderr: (data: Buffer) => {
|
||||
myOutput += `[LOCAL-DOCKER-ERROR]${data.toString()}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return myOutput;
|
||||
}
|
||||
}
|
||||
export default LocalDockerCloudRunner;
|
||||
293
src/model/cloud-runner/providers/k8s/index.ts
Normal file
293
src/model/cloud-runner/providers/k8s/index.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import { BuildParameters } from '../../..';
|
||||
import * as core from '@actions/core';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import KubernetesStorage from './kubernetes-storage';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import KubernetesTaskRunner from './kubernetes-task-runner';
|
||||
import KubernetesSecret from './kubernetes-secret';
|
||||
import KubernetesJobSpecFactory from './kubernetes-job-spec-factory';
|
||||
import KubernetesServiceAccount from './kubernetes-service-account';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import KubernetesPods from './kubernetes-pods';
|
||||
|
||||
class Kubernetes implements ProviderInterface {
|
||||
public static Instance: Kubernetes;
|
||||
public kubeConfig!: k8s.KubeConfig;
|
||||
public kubeClient!: k8s.CoreV1Api;
|
||||
public kubeClientBatch!: k8s.BatchV1Api;
|
||||
public buildGuid: string = '';
|
||||
public buildParameters!: BuildParameters;
|
||||
public pvcName: string = '';
|
||||
public secretName: string = '';
|
||||
public jobName: string = '';
|
||||
public namespace!: string;
|
||||
public podName: string = '';
|
||||
public containerName: string = '';
|
||||
public cleanupCronJobName: string = '';
|
||||
public serviceAccountName: string = '';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
Kubernetes.Instance = this;
|
||||
this.kubeConfig = new k8s.KubeConfig();
|
||||
this.kubeConfig.loadFromDefault();
|
||||
this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
|
||||
this.kubeClientBatch = this.kubeConfig.makeApiClient(k8s.BatchV1Api);
|
||||
this.namespace = 'default';
|
||||
CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment');
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
const pods = await this.kubeClient.listNamespacedPod(this.namespace);
|
||||
const serviceAccounts = await this.kubeClient.listNamespacedServiceAccount(this.namespace);
|
||||
const secrets = await this.kubeClient.listNamespacedSecret(this.namespace);
|
||||
const jobs = await this.kubeClientBatch.listNamespacedJob(this.namespace);
|
||||
|
||||
return [
|
||||
...pods.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
...serviceAccounts.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
...secrets.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
...jobs.body.items.map((x) => {
|
||||
return { Name: x.metadata?.name || `` };
|
||||
}),
|
||||
];
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return new Promise((result) => result(``));
|
||||
}
|
||||
public async setupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
try {
|
||||
this.buildParameters = buildParameters;
|
||||
const id = buildParameters.retainWorkspace ? CloudRunner.lockedWorkspace : buildParameters.buildGuid;
|
||||
this.pvcName = `unity-builder-pvc-${id}`;
|
||||
this.cleanupCronJobName = `unity-builder-cronjob-${id}`;
|
||||
this.serviceAccountName = `service-account-${buildParameters.buildGuid}`;
|
||||
await KubernetesStorage.createPersistentVolumeClaim(
|
||||
buildParameters,
|
||||
this.pvcName,
|
||||
this.kubeClient,
|
||||
this.namespace,
|
||||
);
|
||||
|
||||
await KubernetesServiceAccount.createServiceAccount(this.serviceAccountName, this.namespace, this.kubeClient);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
try {
|
||||
CloudRunnerLogger.log('Cloud Runner K8s workflow!');
|
||||
|
||||
// Setup
|
||||
this.buildGuid = buildGuid;
|
||||
this.secretName = `build-credentials-${this.buildGuid}`;
|
||||
this.jobName = `unity-builder-job-${this.buildGuid}`;
|
||||
this.containerName = `main`;
|
||||
await KubernetesSecret.createSecret(secrets, this.secretName, this.namespace, this.kubeClient);
|
||||
await this.createNamespacedJob(commands, image, mountdir, workingdir, environment, secrets);
|
||||
this.setPodNameAndContainerName(await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace));
|
||||
CloudRunnerLogger.log('Watching pod until running');
|
||||
await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace);
|
||||
let output = '';
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
CloudRunnerLogger.log('Pod running, streaming logs');
|
||||
output = await KubernetesTaskRunner.runTask(
|
||||
this.kubeConfig,
|
||||
this.kubeClient,
|
||||
this.jobName,
|
||||
this.podName,
|
||||
'main',
|
||||
this.namespace,
|
||||
);
|
||||
const running = await KubernetesPods.IsPodRunning(this.podName, this.namespace, this.kubeClient);
|
||||
|
||||
if (!running) {
|
||||
CloudRunnerLogger.log(`Pod not found, assumed ended!`);
|
||||
break;
|
||||
} else {
|
||||
CloudRunnerLogger.log('Pod still running, recovering stream...');
|
||||
}
|
||||
await this.cleanupTaskResources();
|
||||
} catch (error: any) {
|
||||
let errorParsed;
|
||||
try {
|
||||
errorParsed = JSON.parse(error);
|
||||
} catch {
|
||||
errorParsed = error;
|
||||
}
|
||||
|
||||
const reason = errorParsed.reason || errorParsed.response?.body?.reason || ``;
|
||||
const errorMessage = errorParsed.message || reason;
|
||||
|
||||
const continueStreaming =
|
||||
errorMessage.includes(`dial timeout, backstop`) ||
|
||||
errorMessage.includes(`HttpError: HTTP request failed`) ||
|
||||
errorMessage.includes(`an error occurred when try to find container`) ||
|
||||
errorMessage.includes(`not found`) ||
|
||||
errorMessage.includes(`Not Found`);
|
||||
if (continueStreaming) {
|
||||
CloudRunnerLogger.log('Log Stream Container Not Found');
|
||||
await new Promise((resolve) => resolve(5000));
|
||||
continue;
|
||||
} else {
|
||||
CloudRunnerLogger.log(`error running k8s workflow ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log('Running job failed');
|
||||
core.error(JSON.stringify(error, undefined, 4));
|
||||
await this.cleanupTaskResources();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createNamespacedJob(
|
||||
commands: string,
|
||||
image: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
secrets: CloudRunnerSecret[],
|
||||
) {
|
||||
for (let index = 0; index < 3; index++) {
|
||||
try {
|
||||
const jobSpec = KubernetesJobSpecFactory.getJobSpec(
|
||||
commands,
|
||||
image,
|
||||
mountdir,
|
||||
workingdir,
|
||||
environment,
|
||||
secrets,
|
||||
this.buildGuid,
|
||||
this.buildParameters,
|
||||
this.secretName,
|
||||
this.pvcName,
|
||||
this.jobName,
|
||||
k8s,
|
||||
);
|
||||
await new Promise((promise) => setTimeout(promise, 15000));
|
||||
await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
|
||||
CloudRunnerLogger.log(`Build job created`);
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
CloudRunnerLogger.log('Job created');
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`Error occured creating job: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPodNameAndContainerName(pod: k8s.V1Pod) {
|
||||
this.podName = pod.metadata?.name || '';
|
||||
this.containerName = pod.status?.containerStatuses?.[0].name || '';
|
||||
}
|
||||
|
||||
async cleanupTaskResources() {
|
||||
CloudRunnerLogger.log('cleaning up');
|
||||
try {
|
||||
await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
|
||||
await this.kubeClient.deleteNamespacedPod(this.podName, this.namespace);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup`);
|
||||
if (error.response.body.reason !== `NotFound`) {
|
||||
CloudRunnerLogger.log(`Wasn't a not found error: ${error.response.body.reason}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup secret`);
|
||||
CloudRunnerLogger.log(error.response.body.reason);
|
||||
}
|
||||
CloudRunnerLogger.log('cleaned up Secret, Job and Pod');
|
||||
CloudRunnerLogger.log('cleaning up finished');
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
if (buildParameters.retainWorkspace) {
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`deleting PVC`);
|
||||
|
||||
try {
|
||||
await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
|
||||
await this.kubeClient.deleteNamespacedServiceAccount(this.serviceAccountName, this.namespace);
|
||||
CloudRunnerLogger.log('cleaned up PVC and Service Account');
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Cleanup failed ${JSON.stringify(error, undefined, 4)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) {
|
||||
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;
|
||||
@@ -1,9 +1,9 @@
|
||||
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';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
|
||||
class KubernetesJobSpecFactory {
|
||||
static getJobSpec(
|
||||
@@ -103,13 +103,13 @@ class KubernetesJobSpecFactory {
|
||||
name: 'main',
|
||||
image,
|
||||
command: ['/bin/sh'],
|
||||
args: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(command, CloudRunnerState.buildParams)],
|
||||
args: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(command, CloudRunner.buildParameters)],
|
||||
|
||||
workingDir: `${workingDirectory}`,
|
||||
resources: {
|
||||
requests: {
|
||||
memory: buildParameters.cloudRunnerMemory,
|
||||
cpu: buildParameters.cloudRunnerCpu,
|
||||
memory: buildParameters.cloudRunnerMemory || '750M',
|
||||
cpu: buildParameters.cloudRunnerCpu || '1',
|
||||
},
|
||||
},
|
||||
env: [
|
||||
@@ -117,6 +117,7 @@ class KubernetesJobSpecFactory {
|
||||
const environmentVariable = new V1EnvVar();
|
||||
environmentVariable.name = x.name;
|
||||
environmentVariable.value = x.value;
|
||||
|
||||
return environmentVariable;
|
||||
}),
|
||||
...secrets.map((x) => {
|
||||
@@ -127,6 +128,7 @@ class KubernetesJobSpecFactory {
|
||||
const environmentVariable = new V1EnvVar();
|
||||
environmentVariable.name = x.EnvironmentVariable;
|
||||
environmentVariable.valueFrom = secret;
|
||||
|
||||
return environmentVariable;
|
||||
}),
|
||||
],
|
||||
@@ -155,6 +157,9 @@ class KubernetesJobSpecFactory {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '5Gi';
|
||||
|
||||
return job;
|
||||
}
|
||||
}
|
||||
17
src/model/cloud-runner/providers/k8s/kubernetes-pods.ts
Normal file
17
src/model/cloud-runner/providers/k8s/kubernetes-pods.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
class KubernetesPods {
|
||||
public static async IsPodRunning(podName: string, namespace: string, kubeClient: CoreV1Api) {
|
||||
const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.filter((x) => podName === x.metadata?.name);
|
||||
const running = pods.length > 0 && (pods[0].status?.phase === `Running` || pods[0].status?.phase === `Pending`);
|
||||
const phase = pods[0]?.status?.phase || 'undefined status';
|
||||
CloudRunnerLogger.log(`Getting pod status: ${phase}`);
|
||||
if (phase === `Failed`) {
|
||||
throw new Error(`K8s pod failed`);
|
||||
}
|
||||
|
||||
return running;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesPods;
|
||||
45
src/model/cloud-runner/providers/k8s/kubernetes-secret.ts
Normal file
45
src/model/cloud-runner/providers/k8s/kubernetes-secret.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { CoreV1Api } from '@kubernetes/client-node';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
const base64 = require('base-64');
|
||||
|
||||
class KubernetesSecret {
|
||||
static async createSecret(
|
||||
secrets: CloudRunnerSecret[],
|
||||
secretName: string,
|
||||
namespace: string,
|
||||
kubeClient: CoreV1Api,
|
||||
) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
CloudRunnerLogger.log(`Creating secret: ${secretName}`);
|
||||
const existingSecrets = await kubeClient.listNamespacedSecret(namespace);
|
||||
const mappedSecrets = existingSecrets.body.items.map((x) => {
|
||||
return x.metadata?.name || `no name`;
|
||||
});
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`ExistsAlready: ${mappedSecrets.includes(secretName)} SecretsCount: ${mappedSecrets.length}`,
|
||||
);
|
||||
await new Promise((promise) => setTimeout(promise, 15000));
|
||||
await kubeClient.createNamespacedSecret(namespace, secret);
|
||||
CloudRunnerLogger.log('Created secret');
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.log(`Created secret failed ${error}`);
|
||||
throw new Error(`Failed to create kubernetes secret`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesSecret;
|
||||
@@ -10,6 +10,7 @@ class KubernetesServiceAccount {
|
||||
name: serviceAccountName,
|
||||
};
|
||||
serviceAccount.automountServiceAccountToken = false;
|
||||
|
||||
return kubeClient.createNamespacedServiceAccount(namespace, serviceAccount);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
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';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { IncomingMessage } from 'http';
|
||||
import GitHub from '../../../github';
|
||||
|
||||
class KubernetesStorage {
|
||||
public static async createPersistentVolumeClaim(
|
||||
@@ -12,19 +13,22 @@ class KubernetesStorage {
|
||||
kubeClient: k8s.CoreV1Api,
|
||||
namespace: string,
|
||||
) {
|
||||
if (buildParameters.kubeVolume) {
|
||||
CloudRunnerLogger.log(buildParameters.kubeVolume);
|
||||
if (buildParameters.kubeVolume !== ``) {
|
||||
CloudRunnerLogger.log(`Kube Volume was input was set ${buildParameters.kubeVolume} overriding ${pvcName}`);
|
||||
pvcName = buildParameters.kubeVolume;
|
||||
|
||||
return;
|
||||
}
|
||||
const pvcList = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items.map(
|
||||
(x) => x.metadata?.name,
|
||||
);
|
||||
const allPvc = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items;
|
||||
const pvcList = allPvc.map((x) => x.metadata?.name);
|
||||
CloudRunnerLogger.log(`Current PVCs in namespace ${namespace}`);
|
||||
CloudRunnerLogger.log(JSON.stringify(pvcList, undefined, 4));
|
||||
if (pvcList.includes(pvcName)) {
|
||||
CloudRunnerLogger.log(`pvc ${pvcName} already exists`);
|
||||
core.setOutput('volume', pvcName);
|
||||
if (GitHub.githubInputEnabled) {
|
||||
core.setOutput('volume', pvcName);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`Creating PVC ${pvcName} (does not exist)`);
|
||||
@@ -48,10 +52,10 @@ class KubernetesStorage {
|
||||
CloudRunnerLogger.log(`${await this.getPVCPhase(kubeClient, name, namespace)}`);
|
||||
await waitUntil(
|
||||
async () => {
|
||||
return (await this.getPVCPhase(kubeClient, name, namespace)) !== 'Pending';
|
||||
return (await this.getPVCPhase(kubeClient, name, namespace)) === 'Pending';
|
||||
},
|
||||
{
|
||||
timeout: 500000,
|
||||
timeout: 750000,
|
||||
intervalBetweenAttempts: 15000,
|
||||
},
|
||||
);
|
||||
@@ -83,22 +87,20 @@ class KubernetesStorage {
|
||||
};
|
||||
pvc.spec = {
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
storageClassName: process.env.K8s_STORAGE_CLASS || 'standard',
|
||||
storageClassName: buildParameters.kubeStorageClass === '' ? 'standard' : buildParameters.kubeStorageClass,
|
||||
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 },
|
||||
result: { response: IncomingMessage; body: k8s.V1PersistentVolumeClaim },
|
||||
kubeClient: k8s.CoreV1Api,
|
||||
namespace: string,
|
||||
pvcName: string,
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CoreV1Api, KubeConfig, Log } from '@kubernetes/client-node';
|
||||
import { Writable } from 'stream';
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import * as core from '@actions/core';
|
||||
import { CloudRunnerStatics } from '../cloud-runner-statics';
|
||||
import { CloudRunnerStatics } from '../../cloud-runner-statics';
|
||||
import waitUntil from 'async-wait-until';
|
||||
import { Input } from '../..';
|
||||
import { FollowLogStreamService } from '../../services/follow-log-stream-service';
|
||||
|
||||
class KubernetesTaskRunner {
|
||||
static async runTask(
|
||||
@@ -14,20 +14,23 @@ class KubernetesTaskRunner {
|
||||
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;
|
||||
let shouldReadLogs = true;
|
||||
let shouldCleanup = true;
|
||||
stream._write = (chunk, encoding, next) => {
|
||||
didStreamAnyLogs = true;
|
||||
let message = chunk.toString().trimRight(`\n`);
|
||||
message = `[${CloudRunnerStatics.logPrefix}] ${message}`;
|
||||
if (Input.cloudRunnerTests) {
|
||||
output += message;
|
||||
}
|
||||
logCallback(message);
|
||||
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
|
||||
message,
|
||||
shouldReadLogs,
|
||||
shouldCleanup,
|
||||
output,
|
||||
));
|
||||
next();
|
||||
};
|
||||
const logOptions = {
|
||||
@@ -73,6 +76,7 @@ class KubernetesTaskRunner {
|
||||
throw error;
|
||||
}
|
||||
CloudRunnerLogger.log('end of log stream');
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -90,6 +94,7 @@ class KubernetesTaskRunner {
|
||||
}`,
|
||||
);
|
||||
if (success || phase !== 'Pending') return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
{
|
||||
@@ -97,6 +102,7 @@ class KubernetesTaskRunner {
|
||||
intervalBetweenAttempts: 15000,
|
||||
},
|
||||
);
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
74
src/model/cloud-runner/providers/local/index.ts
Normal file
74
src/model/cloud-runner/providers/local/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { CloudRunnerSystem } from '../../services/cloud-runner-system';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
class LocalCloudRunner implements ProviderInterface {
|
||||
listResources(): Promise<ProviderResource[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
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<string> {
|
||||
CloudRunnerLogger.log(image);
|
||||
CloudRunnerLogger.log(buildGuid);
|
||||
CloudRunnerLogger.log(commands);
|
||||
|
||||
return await CloudRunnerSystem.Run(commands);
|
||||
}
|
||||
}
|
||||
export default LocalCloudRunner;
|
||||
@@ -1,9 +1,11 @@
|
||||
import BuildParameters from '../../build-parameters';
|
||||
import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from './cloud-runner-secret';
|
||||
import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerSecret from '../services/cloud-runner-secret';
|
||||
import { ProviderResource } from './provider-resource';
|
||||
import { ProviderWorkflow } from './provider-workflow';
|
||||
|
||||
export interface CloudRunnerProviderInterface {
|
||||
cleanupSharedResources(
|
||||
export interface ProviderInterface {
|
||||
cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -13,7 +15,7 @@ export interface CloudRunnerProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
);
|
||||
setupSharedResources(
|
||||
setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -23,7 +25,7 @@ export interface CloudRunnerProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
);
|
||||
runTask(
|
||||
runTaskInWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -39,4 +41,19 @@ export interface CloudRunnerProviderInterface {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string>;
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string>;
|
||||
listResources(): Promise<ProviderResource[]>;
|
||||
listWorkflow(): Promise<ProviderWorkflow[]>;
|
||||
watchWorkflow(): Promise<string>;
|
||||
}
|
||||
3
src/model/cloud-runner/providers/provider-resource.ts
Normal file
3
src/model/cloud-runner/providers/provider-resource.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class ProviderResource {
|
||||
public Name!: string;
|
||||
}
|
||||
3
src/model/cloud-runner/providers/provider-workflow.ts
Normal file
3
src/model/cloud-runner/providers/provider-workflow.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class ProviderWorkflow {
|
||||
public Name!: string;
|
||||
}
|
||||
69
src/model/cloud-runner/providers/test/index.ts
Normal file
69
src/model/cloud-runner/providers/test/index.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
|
||||
import CloudRunnerLogger from '../../services/cloud-runner-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import CloudRunnerSecret from '../../services/cloud-runner-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
class TestCloudRunner implements ProviderInterface {
|
||||
listResources(): Promise<ProviderResource[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
watchWorkflow(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
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 }[],
|
||||
) {}
|
||||
setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {}
|
||||
public async runTaskInWorkflow(
|
||||
commands: string,
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
mountdir: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
workingdir: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
environment: CloudRunnerEnvironmentVariable[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
CloudRunnerLogger.log(image);
|
||||
CloudRunnerLogger.log(buildGuid);
|
||||
CloudRunnerLogger.log(commands);
|
||||
|
||||
return await new Promise((result) => {
|
||||
result(commands);
|
||||
});
|
||||
}
|
||||
}
|
||||
export default TestCloudRunner;
|
||||
180
src/model/cloud-runner/remote-client/caching.ts
Normal file
180
src/model/cloud-runner/remote-client/caching.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { assert } from 'console';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import { CloudRunnerFolders } from '../services/cloud-runner-folders';
|
||||
import { CloudRunnerSystem } from '../services/cloud-runner-system';
|
||||
import { LfsHashing } from '../services/lfs-hashing';
|
||||
import { RemoteClientLogger } from './remote-client-logger';
|
||||
import { Cli } from '../../cli/cli';
|
||||
import { CliFunction } from '../../cli/cli-functions-repository';
|
||||
// eslint-disable-next-line github/no-then
|
||||
const fileExists = async (fpath) => !!(await fs.promises.stat(fpath).catch(() => false));
|
||||
|
||||
export class Caching {
|
||||
@CliFunction(`cache-push`, `push to cache`)
|
||||
static async cachePush() {
|
||||
try {
|
||||
const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}');
|
||||
CloudRunner.buildParameters = buildParameter;
|
||||
await Caching.PushToCache(
|
||||
Cli.options['cachePushTo'],
|
||||
Cli.options['cachePushFrom'],
|
||||
Cli.options['artifactName'] || '',
|
||||
);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@CliFunction(`cache-pull`, `pull from cache`)
|
||||
static async cachePull() {
|
||||
try {
|
||||
const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}');
|
||||
CloudRunner.buildParameters = buildParameter;
|
||||
await Caching.PullFromCache(
|
||||
Cli.options['cachePushFrom'],
|
||||
Cli.options['cachePushTo'],
|
||||
Cli.options['artifactName'] || '',
|
||||
);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheArtifactName: string) {
|
||||
cacheArtifactName = cacheArtifactName.replace(' ', '');
|
||||
const startPath = process.cwd();
|
||||
let compressionSuffix = '';
|
||||
if (CloudRunner.buildParameters.useLz4Compression === true) {
|
||||
compressionSuffix = `.lz4`;
|
||||
}
|
||||
CloudRunnerLogger.log(`Compression: ${CloudRunner.buildParameters.useLz4Compression} ${compressionSuffix}`);
|
||||
try {
|
||||
if (!(await fileExists(cacheFolder))) {
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`);
|
||||
}
|
||||
process.chdir(path.resolve(sourceFolder, '..'));
|
||||
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(
|
||||
`Hashed cache folder ${await LfsHashing.hashAllFiles(sourceFolder)} ${sourceFolder} ${path.basename(
|
||||
sourceFolder,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
const contents = await fs.promises.readdir(path.basename(sourceFolder));
|
||||
CloudRunnerLogger.log(
|
||||
`There is ${contents.length} files/dir in the source folder ${path.basename(sourceFolder)}`,
|
||||
);
|
||||
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
// await CloudRunnerSystem.Run(`tree -L 2 ./..`);
|
||||
// await CloudRunnerSystem.Run(`tree -L 2`);
|
||||
}
|
||||
|
||||
if (contents.length === 0) {
|
||||
CloudRunnerLogger.log(
|
||||
`Did not push source folder to cache because it was empty ${path.basename(sourceFolder)}`,
|
||||
);
|
||||
process.chdir(`${startPath}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await CloudRunnerSystem.Run(
|
||||
`tar -cf ${cacheArtifactName}.tar${compressionSuffix} ${path.basename(sourceFolder)}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`du ${cacheArtifactName}.tar${compressionSuffix}`);
|
||||
assert(await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`), 'cache archive exists');
|
||||
assert(await fileExists(path.basename(sourceFolder)), 'source folder exists');
|
||||
await CloudRunnerSystem.Run(`mv ${cacheArtifactName}.tar${compressionSuffix} ${cacheFolder}`);
|
||||
RemoteClientLogger.log(`moved cache entry ${cacheArtifactName} to ${cacheFolder}`);
|
||||
assert(
|
||||
await fileExists(`${path.join(cacheFolder, cacheArtifactName)}.tar${compressionSuffix}`),
|
||||
'cache archive exists inside cache folder',
|
||||
);
|
||||
} catch (error) {
|
||||
process.chdir(`${startPath}`);
|
||||
throw error;
|
||||
}
|
||||
process.chdir(`${startPath}`);
|
||||
}
|
||||
public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheArtifactName: string = ``) {
|
||||
cacheArtifactName = cacheArtifactName.replace(' ', '');
|
||||
let compressionSuffix = '';
|
||||
if (CloudRunner.buildParameters.useLz4Compression === true) {
|
||||
compressionSuffix = `.lz4`;
|
||||
}
|
||||
const startPath = process.cwd();
|
||||
RemoteClientLogger.log(`Caching for (lz4 ${compressionSuffix}) ${path.basename(destinationFolder)}`);
|
||||
try {
|
||||
if (!(await fileExists(cacheFolder))) {
|
||||
await fs.promises.mkdir(cacheFolder);
|
||||
}
|
||||
|
||||
if (!(await fileExists(destinationFolder))) {
|
||||
await fs.promises.mkdir(destinationFolder);
|
||||
}
|
||||
|
||||
const latestInBranch = await (
|
||||
await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .tar${compressionSuffix}$ | head -1`)
|
||||
)
|
||||
.replace(/\n/g, ``)
|
||||
.replace(`.tar${compressionSuffix}`, '');
|
||||
|
||||
process.chdir(cacheFolder);
|
||||
|
||||
const cacheSelection =
|
||||
cacheArtifactName !== `` && (await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`))
|
||||
? cacheArtifactName
|
||||
: latestInBranch;
|
||||
await CloudRunnerLogger.log(`cache key ${cacheArtifactName} selection ${cacheSelection}`);
|
||||
|
||||
if (await fileExists(`${cacheSelection}.tar${compressionSuffix}`)) {
|
||||
const resultsFolder = `results${CloudRunner.buildParameters.buildGuid}`;
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${resultsFolder}`);
|
||||
RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.tar${compressionSuffix}`);
|
||||
const fullResultsFolder = path.join(cacheFolder, resultsFolder);
|
||||
await CloudRunnerSystem.Run(`tar -xf ${cacheSelection}.tar${compressionSuffix} -C ${fullResultsFolder}`);
|
||||
RemoteClientLogger.log(`cache item extracted to ${fullResultsFolder}`);
|
||||
assert(await fileExists(fullResultsFolder), `cache extraction results folder exists`);
|
||||
const destinationParentFolder = path.resolve(destinationFolder, '..');
|
||||
|
||||
if (await fileExists(destinationFolder)) {
|
||||
await fs.promises.rmdir(destinationFolder, { recursive: true });
|
||||
}
|
||||
await CloudRunnerSystem.Run(
|
||||
`mv "${path.join(fullResultsFolder, path.basename(destinationFolder))}" "${destinationParentFolder}"`,
|
||||
);
|
||||
const contents = await fs.promises.readdir(
|
||||
path.join(destinationParentFolder, path.basename(destinationFolder)),
|
||||
);
|
||||
CloudRunnerLogger.log(
|
||||
`There is ${contents.length} files/dir in the cache pulled contents for ${path.basename(destinationFolder)}`,
|
||||
);
|
||||
} else {
|
||||
RemoteClientLogger.logWarning(`cache item ${cacheArtifactName} doesn't exist ${destinationFolder}`);
|
||||
if (cacheSelection !== ``) {
|
||||
RemoteClientLogger.logWarning(
|
||||
`cache item ${cacheArtifactName}.tar${compressionSuffix} doesn't exist ${destinationFolder}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`tree ${cacheFolder}`);
|
||||
throw new Error(`Failed to get cache item, but cache hit was found: ${cacheSelection}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
process.chdir(startPath);
|
||||
throw error;
|
||||
}
|
||||
process.chdir(startPath);
|
||||
}
|
||||
|
||||
public static async handleCachePurging() {
|
||||
if (process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined) {
|
||||
RemoteClientLogger.log(`purging ${CloudRunnerFolders.purgeRemoteCaching}`);
|
||||
fs.promises.rmdir(CloudRunnerFolders.cacheFolder, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/model/cloud-runner/remote-client/index.ts
Normal file
161
src/model/cloud-runner/remote-client/index.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import fs from 'fs';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import { CloudRunnerFolders } from '../services/cloud-runner-folders';
|
||||
import { Caching } from './caching';
|
||||
import { LfsHashing } from '../services/lfs-hashing';
|
||||
import { RemoteClientLogger } from './remote-client-logger';
|
||||
import path from 'path';
|
||||
import { assert } from 'console';
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
import { CliFunction } from '../../cli/cli-functions-repository';
|
||||
import { CloudRunnerSystem } from '../services/cloud-runner-system';
|
||||
import YAML from 'yaml';
|
||||
|
||||
export class RemoteClient {
|
||||
public static async bootstrapRepository() {
|
||||
try {
|
||||
await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}`);
|
||||
await CloudRunnerSystem.Run(
|
||||
`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForCacheKeyFull)}`,
|
||||
);
|
||||
process.chdir(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute));
|
||||
await RemoteClient.cloneRepoWithoutLFSFiles();
|
||||
RemoteClient.replaceLargePackageReferencesWithSharedReferences();
|
||||
await RemoteClient.sizeOfFolder(
|
||||
'repo before lfs cache pull',
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute),
|
||||
);
|
||||
const lfsHashes = await LfsHashing.createLFSHashFiles();
|
||||
if (fs.existsSync(CloudRunnerFolders.libraryFolderAbsolute)) {
|
||||
RemoteClientLogger.logWarning(`!Warning!: The Unity library was included in the git repository`);
|
||||
}
|
||||
await Caching.PullFromCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute),
|
||||
`${lfsHashes.lfsGuidSum}`,
|
||||
);
|
||||
await RemoteClient.sizeOfFolder('repo after lfs cache pull', CloudRunnerFolders.repoPathAbsolute);
|
||||
await RemoteClient.pullLatestLFS();
|
||||
await RemoteClient.sizeOfFolder('repo before lfs git pull', CloudRunnerFolders.repoPathAbsolute);
|
||||
await Caching.PushToCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute),
|
||||
`${lfsHashes.lfsGuidSum}`,
|
||||
);
|
||||
await Caching.PullFromCache(
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryCacheFolderFull),
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute),
|
||||
);
|
||||
await RemoteClient.sizeOfFolder('repo after library cache pull', CloudRunnerFolders.repoPathAbsolute);
|
||||
await Caching.handleCachePurging();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async sizeOfFolder(message: string, folder: string) {
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Size of ${message}`);
|
||||
await CloudRunnerSystem.Run(`du -sh ${folder}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async cloneRepoWithoutLFSFiles() {
|
||||
process.chdir(`${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
|
||||
if (
|
||||
CloudRunner.buildParameters.retainWorkspace &&
|
||||
fs.existsSync(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`))
|
||||
) {
|
||||
process.chdir(CloudRunnerFolders.repoPathAbsolute);
|
||||
RemoteClientLogger.log(
|
||||
`${CloudRunnerFolders.repoPathAbsolute} repo exists - skipping clone - retained workspace mode ${CloudRunner.buildParameters.retainWorkspace}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`git fetch && git reset --hard ${CloudRunner.buildParameters.gitSha}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(CloudRunnerFolders.repoPathAbsolute)) {
|
||||
RemoteClientLogger.log(`${CloudRunnerFolders.repoPathAbsolute} repo exists cleaning up`);
|
||||
await CloudRunnerSystem.Run(`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
|
||||
await CloudRunnerSystem.Run(`git config --global advice.detachedHead false`);
|
||||
RemoteClientLogger.log(`Cloning the repository being built:`);
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
||||
await CloudRunnerSystem.Run(
|
||||
`git clone -q ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(CloudRunnerFolders.repoPathAbsolute)}`,
|
||||
);
|
||||
process.chdir(CloudRunnerFolders.repoPathAbsolute);
|
||||
await CloudRunnerSystem.Run(`git lfs install`);
|
||||
assert(fs.existsSync(`.git`), 'git folder exists');
|
||||
RemoteClientLogger.log(`${CloudRunner.buildParameters.branch}`);
|
||||
await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.branch}`);
|
||||
await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`);
|
||||
assert(fs.existsSync(path.join(`.git`, `lfs`)), 'LFS folder should not exist before caching');
|
||||
RemoteClientLogger.log(`Checked out ${CloudRunner.buildParameters.branch}`);
|
||||
} catch (error) {
|
||||
await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static replaceLargePackageReferencesWithSharedReferences() {
|
||||
if (CloudRunner.buildParameters.useSharedLargePackages) {
|
||||
const filePath = path.join(CloudRunnerFolders.projectPathAbsolute, `Packages/manifest.json`);
|
||||
let manifest = fs.readFileSync(filePath, 'utf8');
|
||||
manifest = manifest.replace(/LargeContent/g, '../../../LargeContent');
|
||||
fs.writeFileSync(filePath, manifest);
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Package Manifest`);
|
||||
CloudRunnerLogger.log(manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async pullLatestLFS() {
|
||||
process.chdir(CloudRunnerFolders.repoPathAbsolute);
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge -- %f"`);
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process"`);
|
||||
await CloudRunnerSystem.Run(`git lfs pull`);
|
||||
RemoteClientLogger.log(`pulled latest LFS files`);
|
||||
assert(fs.existsSync(CloudRunnerFolders.lfsFolderAbsolute));
|
||||
}
|
||||
|
||||
@CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
|
||||
static async runRemoteClientJob() {
|
||||
// await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
RemoteClient.handleRetainedWorkspace();
|
||||
|
||||
// await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
await RemoteClient.bootstrapRepository();
|
||||
|
||||
// await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
await RemoteClient.runCustomHookFiles(`before-build`);
|
||||
|
||||
// await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
|
||||
}
|
||||
static async runCustomHookFiles(hookLifecycle: string) {
|
||||
RemoteClientLogger.log(`RunCustomHookFiles: ${hookLifecycle}`);
|
||||
const gameCiCustomHooksPath = path.join(CloudRunnerFolders.repoPathAbsolute, `game-ci`, `hooks`);
|
||||
const files = fs.readdirSync(gameCiCustomHooksPath);
|
||||
for (const file of files) {
|
||||
const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`);
|
||||
const fileContentsObject = YAML.parse(fileContents.toString());
|
||||
if (fileContentsObject.hook === hookLifecycle) {
|
||||
RemoteClientLogger.log(`Active Hook File ${file} \n \n file contents: \n ${fileContents}`);
|
||||
await CloudRunnerSystem.Run(fileContentsObject.commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
static handleRetainedWorkspace() {
|
||||
if (!CloudRunner.buildParameters.retainWorkspace) {
|
||||
return;
|
||||
}
|
||||
RemoteClientLogger.log(`Retained Workspace: ${CloudRunner.lockedWorkspace}`);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import CloudRunnerLogger from '../../../cloud-runner/services/cloud-runner-logger';
|
||||
import CloudRunnerLogger from '../services/cloud-runner-logger';
|
||||
|
||||
export class RemoteClientLogger {
|
||||
public static log(message: string) {
|
||||
@@ -1,40 +0,0 @@
|
||||
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<Hook>();
|
||||
if (experimentHooks && experimentHooks !== '') {
|
||||
try {
|
||||
output = YAML.parse(experimentHooks);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return output.filter((x) => x.step !== undefined && x.hook !== undefined && x.hook.length > 0);
|
||||
}
|
||||
}
|
||||
export class Hook {
|
||||
public commands;
|
||||
public secrets: CloudRunnerSecret[] = new Array<CloudRunnerSecret>();
|
||||
public name;
|
||||
public hook!: string[];
|
||||
public step!: string[];
|
||||
}
|
||||
118
src/model/cloud-runner/services/cloud-runner-custom-hooks.ts
Normal file
118
src/model/cloud-runner/services/cloud-runner-custom-hooks.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { BuildParameters, Input } from '../..';
|
||||
import YAML from 'yaml';
|
||||
import CloudRunnerSecret from './cloud-runner-secret';
|
||||
import { RemoteClientLogger } from '../remote-client/remote-client-logger';
|
||||
import path from 'path';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// import CloudRunnerLogger from './cloud-runner-logger';
|
||||
|
||||
export class CloudRunnerCustomHooks {
|
||||
// TODO also accept hooks as yaml files in the repo
|
||||
public static ApplyHooksToCommands(commands: string, buildParameters: BuildParameters): string {
|
||||
const hooks = CloudRunnerCustomHooks.getHooks(buildParameters.customJobHooks).filter((x) => x.step.includes(`all`));
|
||||
|
||||
return `echo "---"
|
||||
echo "start cloud runner init"
|
||||
${CloudRunnerOptions.cloudRunnerDebugEnv ? `printenv` : `#`}
|
||||
echo "start of cloud runner job"
|
||||
${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '}
|
||||
${commands}
|
||||
${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '}
|
||||
echo "end of cloud runner job"
|
||||
echo "---${buildParameters.logId}"`;
|
||||
}
|
||||
|
||||
public static getHooks(customJobHooks): Hook[] {
|
||||
const experimentHooks = customJobHooks;
|
||||
let output = new Array<Hook>();
|
||||
if (experimentHooks && experimentHooks !== '') {
|
||||
try {
|
||||
output = YAML.parse(experimentHooks);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return output.filter((x) => x.step !== undefined && x.hook !== undefined && x.hook.length > 0);
|
||||
}
|
||||
|
||||
static GetCustomHooksFromFiles(hookLifecycle: string): Hook[] {
|
||||
const results: Hook[] = [];
|
||||
RemoteClientLogger.log(`GetCustomStepFiles: ${hookLifecycle}`);
|
||||
try {
|
||||
const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `hooks`);
|
||||
const files = fs.readdirSync(gameCiCustomStepsPath);
|
||||
for (const file of files) {
|
||||
if (!CloudRunnerOptions.customHookFiles.includes(file.replace(`.yaml`, ``))) {
|
||||
continue;
|
||||
}
|
||||
const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`);
|
||||
const fileContentsObject = CloudRunnerCustomHooks.ParseHooks(fileContents)[0];
|
||||
if (fileContentsObject.hook.includes(hookLifecycle)) {
|
||||
results.push(fileContentsObject);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`);
|
||||
}
|
||||
RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ConvertYamlSecrets(object) {
|
||||
if (object.secrets === undefined) {
|
||||
object.secrets = [];
|
||||
|
||||
return;
|
||||
}
|
||||
object.secrets = object.secrets.map((x) => {
|
||||
return {
|
||||
ParameterKey: x.name,
|
||||
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
|
||||
ParameterValue: x.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static ParseHooks(steps: string): Hook[] {
|
||||
if (steps === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) {
|
||||
|
||||
// CloudRunnerLogger.log(`Parsing build hooks: ${steps}`);
|
||||
|
||||
// }
|
||||
const isArray = steps.replace(/\s/g, ``)[0] === `-`;
|
||||
const object: Hook[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)];
|
||||
for (const hook of object) {
|
||||
CloudRunnerCustomHooks.ConvertYamlSecrets(hook);
|
||||
if (hook.secrets === undefined) {
|
||||
hook.secrets = [];
|
||||
}
|
||||
}
|
||||
if (object === undefined) {
|
||||
throw new Error(`Failed to parse ${steps}`);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
public static getSecrets(hooks) {
|
||||
const secrets = hooks.map((x) => x.secrets).filter((x) => x !== undefined && x.length > 0);
|
||||
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
return secrets.length > 0 ? secrets.reduce((x, y) => [...x, ...y]) : [];
|
||||
}
|
||||
}
|
||||
export class Hook {
|
||||
public commands;
|
||||
public secrets: CloudRunnerSecret[] = new Array<CloudRunnerSecret>();
|
||||
public name;
|
||||
public hook!: string[];
|
||||
public step!: string[];
|
||||
}
|
||||
269
src/model/cloud-runner/services/cloud-runner-custom-steps.ts
Normal file
269
src/model/cloud-runner/services/cloud-runner-custom-steps.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import YAML from 'yaml';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import * as core from '@actions/core';
|
||||
import { CustomWorkflow } from '../workflows/custom-workflow';
|
||||
import { RemoteClientLogger } from '../remote-client/remote-client-logger';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import Input from '../../input';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import CloudRunnerLogger from './cloud-runner-logger';
|
||||
import { CustomStep } from './custom-step';
|
||||
|
||||
export class CloudRunnerCustomSteps {
|
||||
static GetCustomStepsFromFiles(hookLifecycle: string): CustomStep[] {
|
||||
const results: CustomStep[] = [];
|
||||
RemoteClientLogger.log(
|
||||
`GetCustomStepFiles: ${hookLifecycle} CustomStepFiles: ${CloudRunnerOptions.customStepFiles}`,
|
||||
);
|
||||
try {
|
||||
const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `steps`);
|
||||
const files = fs.readdirSync(gameCiCustomStepsPath);
|
||||
for (const file of files) {
|
||||
if (!CloudRunnerOptions.customStepFiles.includes(file.replace(`.yaml`, ``))) {
|
||||
RemoteClientLogger.log(`Skipping CustomStepFile: ${file}`);
|
||||
continue;
|
||||
}
|
||||
const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`);
|
||||
const fileContentsObject = CloudRunnerCustomSteps.ParseSteps(fileContents)[0];
|
||||
if (fileContentsObject.hook === hookLifecycle) {
|
||||
results.push(fileContentsObject);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`);
|
||||
}
|
||||
RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`);
|
||||
|
||||
const builtInCustomSteps: CustomStep[] = CloudRunnerCustomSteps.ParseSteps(
|
||||
`- name: aws-s3-upload-build
|
||||
image: amazon/aws-cli
|
||||
hook: after
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 cp /data/cache/$CACHE_KEY/build/build-${CloudRunner.buildParameters.buildGuid}.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
} s3://${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
}
|
||||
rm /data/cache/$CACHE_KEY/build/build-${CloudRunner.buildParameters.buildGuid}.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
}
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
value: ${process.env.AWS_REGION || ``}
|
||||
- name: aws-s3-pull-build
|
||||
image: amazon/aws-cli
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/ || true
|
||||
aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/build || true
|
||||
aws s3 cp s3://${
|
||||
CloudRunner.buildParameters.awsBaseStackName
|
||||
}/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
} /data/cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${
|
||||
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
|
||||
}
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
- name: awsSecretAccessKey
|
||||
- name: awsDefaultRegion
|
||||
- name: BUILD_GUID_TARGET
|
||||
- name: steam-deploy-client
|
||||
image: steamcmd/steamcmd
|
||||
commands: |
|
||||
apt-get update
|
||||
apt-get install -y curl tar coreutils git tree > /dev/null
|
||||
curl -s https://gist.githubusercontent.com/frostebite/1d56f5505b36b403b64193b7a6e54cdc/raw/fa6639ed4ef750c4268ea319d63aa80f52712ffb/deploy-client-steam.sh | bash
|
||||
secrets:
|
||||
- name: STEAM_USERNAME
|
||||
- name: STEAM_PASSWORD
|
||||
- name: STEAM_APPID
|
||||
- name: STEAM_SSFN_FILE_NAME
|
||||
- name: STEAM_SSFN_FILE_CONTENTS
|
||||
- name: STEAM_CONFIG_VDF_1
|
||||
- name: STEAM_CONFIG_VDF_2
|
||||
- name: STEAM_CONFIG_VDF_3
|
||||
- name: STEAM_CONFIG_VDF_4
|
||||
- name: BUILD_GUID_TARGET
|
||||
- name: RELEASE_BRANCH
|
||||
- name: steam-deploy-project
|
||||
image: steamcmd/steamcmd
|
||||
commands: |
|
||||
apt-get update
|
||||
apt-get install -y curl tar coreutils git tree > /dev/null
|
||||
curl -s https://gist.githubusercontent.com/frostebite/969da6a41002a0e901174124b643709f/raw/02403e53fb292026cba81ddcf4ff35fc1eba111d/steam-deploy-project.sh | bash
|
||||
secrets:
|
||||
- name: STEAM_USERNAME
|
||||
- name: STEAM_PASSWORD
|
||||
- name: STEAM_APPID
|
||||
- name: STEAM_SSFN_FILE_NAME
|
||||
- name: STEAM_SSFN_FILE_CONTENTS
|
||||
- name: STEAM_CONFIG_VDF_1
|
||||
- name: STEAM_CONFIG_VDF_2
|
||||
- name: STEAM_CONFIG_VDF_3
|
||||
- name: STEAM_CONFIG_VDF_4
|
||||
- name: BUILD_GUID_2
|
||||
- name: RELEASE_BRANCH
|
||||
- name: aws-s3-upload-cache
|
||||
image: amazon/aws-cli
|
||||
hook: after
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://${
|
||||
CloudRunner.buildParameters.awsBaseStackName
|
||||
}/cloud-runner-cache/$CACHE_KEY/lfs
|
||||
rm -r /data/cache/$CACHE_KEY/lfs
|
||||
aws s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://${
|
||||
CloudRunner.buildParameters.awsBaseStackName
|
||||
}/cloud-runner-cache/$CACHE_KEY/Library
|
||||
rm -r /data/cache/$CACHE_KEY/Library
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
value: ${process.env.AWS_REGION || ``}
|
||||
- name: aws-s3-pull-cache
|
||||
image: amazon/aws-cli
|
||||
hook: before
|
||||
commands: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default
|
||||
aws configure set region $AWS_DEFAULT_REGION --profile default
|
||||
aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/ || true
|
||||
aws s3 ls ${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/ || true
|
||||
BUCKET1="${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/Library/"
|
||||
aws s3 ls $BUCKET1 || true
|
||||
OBJECT1="$(aws s3 ls $BUCKET1 | sort | tail -n 1 | awk '{print $4}' || '')"
|
||||
aws s3 cp s3://$BUCKET1$OBJECT1 /data/cache/$CACHE_KEY/Library/ || true
|
||||
BUCKET2="${CloudRunner.buildParameters.awsBaseStackName}/cloud-runner-cache/$CACHE_KEY/lfs/"
|
||||
aws s3 ls $BUCKET2 || true
|
||||
OBJECT2="$(aws s3 ls $BUCKET2 | sort | tail -n 1 | awk '{print $4}' || '')"
|
||||
aws s3 cp s3://$BUCKET2$OBJECT2 /data/cache/$CACHE_KEY/lfs/ || true
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
value: ${process.env.AWS_REGION || ``}
|
||||
- name: debug-cache
|
||||
image: ubuntu
|
||||
hook: after
|
||||
commands: |
|
||||
apt-get update > /dev/null
|
||||
${CloudRunnerOptions.cloudRunnerDebugTree ? `apt-get install -y tree > /dev/null` : `#`}
|
||||
${CloudRunnerOptions.cloudRunnerDebugTree ? `tree -L 3 /data/cache` : `#`}
|
||||
secrets:
|
||||
- name: awsAccessKeyId
|
||||
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
|
||||
- name: awsSecretAccessKey
|
||||
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
|
||||
- name: awsDefaultRegion
|
||||
value: ${process.env.AWS_REGION || ``}`,
|
||||
).filter((x) => CloudRunnerOptions.customStepFiles.includes(x.name) && x.hook === hookLifecycle);
|
||||
if (builtInCustomSteps.length > 0) {
|
||||
results.push(...builtInCustomSteps);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ConvertYamlSecrets(object) {
|
||||
if (object.secrets === undefined) {
|
||||
object.secrets = [];
|
||||
|
||||
return;
|
||||
}
|
||||
object.secrets = object.secrets.map((x) => {
|
||||
return {
|
||||
ParameterKey: x.name,
|
||||
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
|
||||
ParameterValue: x.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static ParseSteps(steps: string): CustomStep[] {
|
||||
if (steps === '') {
|
||||
return [];
|
||||
}
|
||||
const isArray = steps.replace(/\s/g, ``)[0] === `-`;
|
||||
const object: CustomStep[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)];
|
||||
for (const step of object) {
|
||||
CloudRunnerCustomSteps.ConvertYamlSecrets(step);
|
||||
if (step.secrets === undefined) {
|
||||
step.secrets = [];
|
||||
} else {
|
||||
for (const secret of step.secrets) {
|
||||
if (secret.ParameterValue === undefined && process.env[secret.EnvironmentVariable] !== undefined) {
|
||||
if (CloudRunner.buildParameters?.cloudRunnerDebug) {
|
||||
CloudRunnerLogger.log(`Injecting custom step ${step.name} from env var ${secret.ParameterKey}`);
|
||||
}
|
||||
secret.ParameterValue = process.env[secret.ParameterKey] || ``;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (step.image === undefined) {
|
||||
step.image = `ubuntu`;
|
||||
}
|
||||
}
|
||||
if (object === undefined) {
|
||||
throw new Error(`Failed to parse ${steps}`);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
static async RunPostBuildSteps(cloudRunnerStepState) {
|
||||
let output = ``;
|
||||
const steps: CustomStep[] = [
|
||||
...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.postBuildSteps),
|
||||
...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`after`),
|
||||
];
|
||||
|
||||
if (steps.length > 0) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps');
|
||||
output += await CustomWorkflow.runCustomJob(
|
||||
steps,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
static async RunPreBuildSteps(cloudRunnerStepState) {
|
||||
let output = ``;
|
||||
const steps: CustomStep[] = [
|
||||
...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.preBuildSteps),
|
||||
...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`before`),
|
||||
];
|
||||
|
||||
if (steps.length > 0) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps');
|
||||
output += await CustomWorkflow.runCustomJob(
|
||||
steps,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
89
src/model/cloud-runner/services/cloud-runner-folders.ts
Normal file
89
src/model/cloud-runner/services/cloud-runner-folders.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import path from 'path';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import CloudRunner from './../cloud-runner';
|
||||
|
||||
export class CloudRunnerFolders {
|
||||
public static readonly repositoryFolder = 'repo';
|
||||
|
||||
public static ToLinuxFolder(folder: string) {
|
||||
return folder.replace(/\\/g, `/`);
|
||||
}
|
||||
|
||||
// Only the following paths that do not start a path.join with another "Full" suffixed property need to start with an absolute /
|
||||
|
||||
public static get uniqueCloudRunnerJobFolderAbsolute(): string {
|
||||
return CloudRunner.buildParameters && CloudRunner.buildParameters.retainWorkspace && CloudRunner.lockedWorkspace
|
||||
? path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.lockedWorkspace)
|
||||
: path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid);
|
||||
}
|
||||
|
||||
public static get cacheFolderForAllFull(): string {
|
||||
return path.join('/', CloudRunnerFolders.buildVolumeFolder, CloudRunnerFolders.cacheFolder);
|
||||
}
|
||||
|
||||
public static get cacheFolderForCacheKeyFull(): string {
|
||||
return path.join(
|
||||
'/',
|
||||
CloudRunnerFolders.buildVolumeFolder,
|
||||
CloudRunnerFolders.cacheFolder,
|
||||
CloudRunner.buildParameters.cacheKey,
|
||||
);
|
||||
}
|
||||
|
||||
public static get builderPathAbsolute(): string {
|
||||
return path.join(
|
||||
CloudRunnerOptions.useSharedBuilder
|
||||
? `/${CloudRunnerFolders.buildVolumeFolder}`
|
||||
: CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute,
|
||||
`builder`,
|
||||
);
|
||||
}
|
||||
|
||||
public static get repoPathAbsolute(): string {
|
||||
return path.join(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, CloudRunnerFolders.repositoryFolder);
|
||||
}
|
||||
|
||||
public static get projectPathAbsolute(): string {
|
||||
return path.join(CloudRunnerFolders.repoPathAbsolute, CloudRunner.buildParameters.projectPath);
|
||||
}
|
||||
|
||||
public static get libraryFolderAbsolute(): string {
|
||||
return path.join(CloudRunnerFolders.projectPathAbsolute, `Library`);
|
||||
}
|
||||
|
||||
public static get projectBuildFolderAbsolute(): string {
|
||||
return path.join(CloudRunnerFolders.repoPathAbsolute, CloudRunner.buildParameters.buildPath);
|
||||
}
|
||||
|
||||
public static get lfsFolderAbsolute(): string {
|
||||
return path.join(CloudRunnerFolders.repoPathAbsolute, `.git`, `lfs`);
|
||||
}
|
||||
|
||||
public static get purgeRemoteCaching(): boolean {
|
||||
return process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined;
|
||||
}
|
||||
|
||||
public static get lfsCacheFolderFull() {
|
||||
return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `lfs`);
|
||||
}
|
||||
|
||||
public static get libraryCacheFolderFull() {
|
||||
return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `Library`);
|
||||
}
|
||||
|
||||
public static get unityBuilderRepoUrl(): string {
|
||||
return `https://${CloudRunner.buildParameters.gitPrivateToken}@github.com/game-ci/unity-builder.git`;
|
||||
}
|
||||
|
||||
public static get targetBuildRepoUrl(): string {
|
||||
return `https://${CloudRunner.buildParameters.gitPrivateToken}@github.com/${CloudRunner.buildParameters.githubRepo}.git`;
|
||||
}
|
||||
|
||||
public static get buildVolumeFolder() {
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public static get cacheFolder() {
|
||||
return 'cache';
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import { customAlphabet } from 'nanoid';
|
||||
import CloudRunnerConstants from './cloud-runner-constants';
|
||||
|
||||
class CloudRunnerNamespace {
|
||||
static generateBuildName(runNumber: string | number, platform: string) {
|
||||
static generateGuid(runNumber: string | number, platform: string) {
|
||||
const nanoid = customAlphabet(CloudRunnerConstants.alphabet, 4);
|
||||
|
||||
return `${runNumber}-${platform.toLowerCase().replace('standalone', '')}-${nanoid()}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Input from '../../input';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
|
||||
class CloudRunnerOptionsReader {
|
||||
static GetProperties() {
|
||||
return [...Object.getOwnPropertyNames(Input), ...Object.getOwnPropertyNames(CloudRunnerOptions)];
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudRunnerOptionsReader;
|
||||
@@ -0,0 +1,67 @@
|
||||
import Input from '../../input';
|
||||
import { GenericInputReader } from '../../input-readers/generic-input-reader';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
|
||||
const formatFunction = (value, arguments_) => {
|
||||
for (const element of arguments_) {
|
||||
value = value.replace(`{${element.key}}`, element.value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
class CloudRunnerQueryOverride {
|
||||
static queryOverrides: any;
|
||||
|
||||
// TODO accept premade secret sources or custom secret source definition yamls
|
||||
|
||||
public static query(key, alternativeKey) {
|
||||
if (CloudRunnerQueryOverride.queryOverrides && CloudRunnerQueryOverride.queryOverrides[key] !== undefined) {
|
||||
return CloudRunnerQueryOverride.queryOverrides[key];
|
||||
}
|
||||
if (
|
||||
CloudRunnerQueryOverride.queryOverrides &&
|
||||
alternativeKey &&
|
||||
CloudRunnerQueryOverride.queryOverrides[alternativeKey] !== undefined
|
||||
) {
|
||||
return CloudRunnerQueryOverride.queryOverrides[alternativeKey];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private static shouldUseOverride(query) {
|
||||
if (CloudRunnerOptions.readInputOverrideCommand() !== '') {
|
||||
if (CloudRunnerOptions.readInputFromOverrideList() !== '') {
|
||||
const doesInclude =
|
||||
CloudRunnerOptions.readInputFromOverrideList().split(',').includes(query) ||
|
||||
CloudRunnerOptions.readInputFromOverrideList().split(',').includes(Input.ToEnvVarFormat(query));
|
||||
|
||||
return doesInclude ? true : false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async queryOverride(query) {
|
||||
if (!this.shouldUseOverride(query)) {
|
||||
throw new Error(`Should not be trying to run override query on ${query}`);
|
||||
}
|
||||
|
||||
return await GenericInputReader.Run(
|
||||
formatFunction(CloudRunnerOptions.readInputOverrideCommand(), [{ key: 0, value: query }]),
|
||||
);
|
||||
}
|
||||
|
||||
public static async PopulateQueryOverrideInput() {
|
||||
const queries = CloudRunnerOptions.readInputFromOverrideList().split(',');
|
||||
CloudRunnerQueryOverride.queryOverrides = new Array();
|
||||
for (const element of queries) {
|
||||
if (CloudRunnerQueryOverride.shouldUseOverride(element)) {
|
||||
CloudRunnerQueryOverride.queryOverrides[element] = await CloudRunnerQueryOverride.queryOverride(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export default CloudRunnerQueryOverride;
|
||||
60
src/model/cloud-runner/services/cloud-runner-system.ts
Normal file
60
src/model/cloud-runner/services/cloud-runner-system.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { exec } from 'child_process';
|
||||
import { RemoteClientLogger } from '../remote-client/remote-client-logger';
|
||||
|
||||
export class CloudRunnerSystem {
|
||||
public static async RunAndReadLines(command: string): Promise<string[]> {
|
||||
const result = await CloudRunnerSystem.Run(command, false, true);
|
||||
|
||||
return result
|
||||
.split(`\n`)
|
||||
.map((x) => x.replace(`\r`, ``))
|
||||
.filter((x) => x !== ``)
|
||||
.map((x) => {
|
||||
const lineValues = x.split(` `);
|
||||
|
||||
return lineValues[lineValues.length - 1];
|
||||
});
|
||||
}
|
||||
|
||||
public static async Run(command: string, suppressError = false, suppressLogs = false) {
|
||||
for (const element of command.split(`\n`)) {
|
||||
if (!suppressLogs) {
|
||||
RemoteClientLogger.log(element);
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise<string>((promise, throwError) => {
|
||||
let output = '';
|
||||
const child = exec(command, (error, stdout, stderr) => {
|
||||
if (!suppressError && error) {
|
||||
RemoteClientLogger.log(error.toString());
|
||||
throwError(error);
|
||||
}
|
||||
if (stderr) {
|
||||
const diagnosticOutput = `${stderr.toString()}`;
|
||||
if (!suppressLogs) {
|
||||
RemoteClientLogger.logCliDiagnostic(diagnosticOutput);
|
||||
}
|
||||
output += diagnosticOutput;
|
||||
}
|
||||
const outputChunk = `${stdout}`;
|
||||
output += outputChunk;
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (!suppressLogs) {
|
||||
RemoteClientLogger.log(`[${code}]`);
|
||||
}
|
||||
if (code !== 0 && !suppressError) {
|
||||
throwError(output);
|
||||
}
|
||||
const outputLines = output.split(`\n`);
|
||||
for (const element of outputLines) {
|
||||
if (!suppressLogs) {
|
||||
RemoteClientLogger.log(element);
|
||||
}
|
||||
}
|
||||
promise(output);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
9
src/model/cloud-runner/services/custom-step.ts
Normal file
9
src/model/cloud-runner/services/custom-step.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import CloudRunnerSecret from './cloud-runner-secret';
|
||||
|
||||
export class CustomStep {
|
||||
public commands;
|
||||
public secrets: CloudRunnerSecret[] = new Array<CloudRunnerSecret>();
|
||||
public name;
|
||||
public image: string = `ubuntu`;
|
||||
public hook!: string;
|
||||
}
|
||||
37
src/model/cloud-runner/services/follow-log-stream-service.ts
Normal file
37
src/model/cloud-runner/services/follow-log-stream-service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import CloudRunnerLogger from './cloud-runner-logger';
|
||||
import * as core from '@actions/core';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import { CloudRunnerStatics } from '../cloud-runner-statics';
|
||||
import GitHub from '../../github';
|
||||
|
||||
export class FollowLogStreamService {
|
||||
public static handleIteration(message, shouldReadLogs, shouldCleanup, output) {
|
||||
if (message.includes(`---${CloudRunner.buildParameters.logId}`)) {
|
||||
CloudRunnerLogger.log('End of log transmission received');
|
||||
shouldReadLogs = false;
|
||||
} else if (message.includes('Rebuilding Library because the asset database could not be found!')) {
|
||||
GitHub.updateGitHubCheck(`Library was not found, importing new Library`, ``);
|
||||
core.warning('LIBRARY NOT FOUND!');
|
||||
core.setOutput('library-found', 'false');
|
||||
} else if (message.includes('Build succeeded')) {
|
||||
GitHub.updateGitHubCheck(`Build succeeded`, `Build succeeded`);
|
||||
core.setOutput('build-result', 'success');
|
||||
} else if (message.includes('Build fail')) {
|
||||
GitHub.updateGitHubCheck(`Build failed`, `Build failed`);
|
||||
core.setOutput('build-result', 'failed');
|
||||
core.setFailed('unity build failed');
|
||||
core.error('BUILD FAILED!');
|
||||
} else if (CloudRunner.buildParameters.cloudRunnerDebug && message.includes(': Listening for Jobs')) {
|
||||
core.setOutput('cloud runner stop watching', 'true');
|
||||
shouldReadLogs = false;
|
||||
shouldCleanup = false;
|
||||
core.warning('cloud runner stop watching');
|
||||
}
|
||||
if (CloudRunner.buildParameters.cloudRunnerDebug) {
|
||||
output += `${message}\n`;
|
||||
}
|
||||
CloudRunnerLogger.log(`[${CloudRunnerStatics.logPrefix}] ${message}`);
|
||||
|
||||
return { shouldReadLogs, shouldCleanup, output };
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from 'path';
|
||||
import { CloudRunnerState } from '../../../cloud-runner/state/cloud-runner-state';
|
||||
import { CloudRunnerFolders } from './cloud-runner-folders';
|
||||
import { CloudRunnerSystem } from './cloud-runner-system';
|
||||
import fs from 'fs';
|
||||
import { assert } from 'console';
|
||||
import { Input } from '../../..';
|
||||
import { RemoteClientLogger } from './remote-client-logger';
|
||||
import { Cli } from '../../cli/cli';
|
||||
import { CliFunction } from '../../cli/cli-functions-repository';
|
||||
|
||||
export class LFSHashing {
|
||||
export class LfsHashing {
|
||||
public static async createLFSHashFiles() {
|
||||
try {
|
||||
await CloudRunnerSystem.Run(`git lfs ls-files -l | cut -d ' ' -f1 | sort > .lfs-assets-guid`);
|
||||
@@ -15,16 +15,14 @@ export class LFSHashing {
|
||||
assert(fs.existsSync(`.lfs-assets-guid`));
|
||||
const lfsHashes = {
|
||||
lfsGuid: fs
|
||||
.readFileSync(`${path.join(CloudRunnerState.repoPathFull, `.lfs-assets-guid`)}`, 'utf8')
|
||||
.readFileSync(`${path.join(CloudRunnerFolders.repoPathAbsolute, `.lfs-assets-guid`)}`, 'utf8')
|
||||
.replace(/\n/g, ``),
|
||||
lfsGuidSum: fs
|
||||
.readFileSync(`${path.join(CloudRunnerState.repoPathFull, `.lfs-assets-guid-sum`)}`, 'utf8')
|
||||
.readFileSync(`${path.join(CloudRunnerFolders.repoPathAbsolute, `.lfs-assets-guid-sum`)}`, 'utf8')
|
||||
.replace(' .lfs-assets-guid', '')
|
||||
.replace(/\n/g, ``),
|
||||
};
|
||||
if (Input.cloudRunnerTests) {
|
||||
RemoteClientLogger.log(lfsHashes.lfsGuid);
|
||||
RemoteClientLogger.log(lfsHashes.lfsGuidSum);
|
||||
}
|
||||
|
||||
return lfsHashes;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -37,6 +35,13 @@ export class LFSHashing {
|
||||
.replace(/\n/g, '')
|
||||
.split(` `)[0];
|
||||
process.chdir(startPath);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@CliFunction(`hash`, `hash all folder contents`)
|
||||
static async hash() {
|
||||
const folder = Cli.options['cachePushFrom'];
|
||||
LfsHashing.hashAllFiles(folder);
|
||||
}
|
||||
}
|
||||
287
src/model/cloud-runner/services/shared-workspace-locking.ts
Normal file
287
src/model/cloud-runner/services/shared-workspace-locking.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { CloudRunnerSystem } from './cloud-runner-system';
|
||||
import * as fs from 'fs';
|
||||
import CloudRunnerLogger from './cloud-runner-logger';
|
||||
import CloudRunnerOptions from '../cloud-runner-options';
|
||||
import BuildParameters from '../../build-parameters';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
export class SharedWorkspaceLocking {
|
||||
private static get workspaceBucketRoot() {
|
||||
return `s3://${CloudRunner.buildParameters.awsBaseStackName}/`;
|
||||
}
|
||||
private static get workspaceRoot() {
|
||||
return `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`;
|
||||
}
|
||||
public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`,
|
||||
)
|
||||
).map((x) => x.replace(`/`, ``));
|
||||
}
|
||||
public static async DoesWorkspaceTopLevelExist(buildParametersContext: BuildParameters) {
|
||||
await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`);
|
||||
|
||||
return (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`))
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.includes(buildParametersContext.cacheKey);
|
||||
}
|
||||
public static async GetAllLocks(workspace: string, buildParametersContext: BuildParameters): Promise<string[]> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`,
|
||||
)
|
||||
)
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.filter((x) => x.includes(`_lock`));
|
||||
}
|
||||
public static async GetOrCreateLockedWorkspace(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
) {
|
||||
if (!CloudRunnerOptions.retainWorkspaces) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext)) {
|
||||
const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext);
|
||||
CloudRunnerLogger.log(
|
||||
`run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`,
|
||||
);
|
||||
for (const element of workspaces) {
|
||||
await new Promise((promise) => setTimeout(promise, 1000));
|
||||
const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext);
|
||||
CloudRunnerLogger.log(`run agent: ${runId} try lock workspace: ${element} result: ${lockResult}`);
|
||||
|
||||
if (lockResult) {
|
||||
CloudRunner.lockedWorkspace = element;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext, runId);
|
||||
CloudRunnerLogger.log(
|
||||
`run agent ${runId} didn't find a free workspace so created: ${workspace} createWorkspaceSuccess: ${createResult}`,
|
||||
);
|
||||
|
||||
return createResult;
|
||||
}
|
||||
|
||||
public static async DoesWorkspaceExist(workspace: string, buildParametersContext: BuildParameters) {
|
||||
return (await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext)).includes(workspace);
|
||||
}
|
||||
public static async HasWorkspaceLock(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
return false;
|
||||
}
|
||||
const locks = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext))
|
||||
.map((x) => {
|
||||
return {
|
||||
name: x,
|
||||
timestamp: Number(x.split(`_`)[0]),
|
||||
};
|
||||
})
|
||||
.sort((x) => x.timestamp);
|
||||
const lockMatches = locks.filter((x) => x.name.includes(runId));
|
||||
const includesRunLock = lockMatches.length > 0 && locks.indexOf(lockMatches[0]) === 0;
|
||||
CloudRunnerLogger.log(
|
||||
`Checking has workspace lock, runId: ${runId}, workspace: ${workspace}, success: ${includesRunLock} \n- Num of locks created by Run Agent: ${
|
||||
lockMatches.length
|
||||
} Num of Locks: ${locks.length}, Time ordered index for Run Agent: ${locks.indexOf(lockMatches[0])} \n \n`,
|
||||
);
|
||||
|
||||
return includesRunLock;
|
||||
}
|
||||
|
||||
public static async GetFreeWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
|
||||
for (const element of workspaces) {
|
||||
await new Promise((promise) => setTimeout(promise, 1500));
|
||||
const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(element, buildParametersContext);
|
||||
const isBelowMax = await SharedWorkspaceLocking.IsWorkspaceBelowMax(element, buildParametersContext);
|
||||
if (!isLocked && isBelowMax) {
|
||||
result.push(element);
|
||||
CloudRunnerLogger.log(`workspace ${element} is free`);
|
||||
} else {
|
||||
CloudRunnerLogger.log(`workspace ${element} is NOT free ${!isLocked} ${isBelowMax}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async IsWorkspaceBelowMax(
|
||||
workspace: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
|
||||
if (workspace === ``) {
|
||||
return (
|
||||
workspaces.length < buildParametersContext.maxRetainedWorkspaces ||
|
||||
buildParametersContext.maxRetainedWorkspaces === 0
|
||||
);
|
||||
}
|
||||
const ordered: any[] = [];
|
||||
for (const ws of workspaces) {
|
||||
ordered.push({
|
||||
name: ws,
|
||||
timestamp: await SharedWorkspaceLocking.GetWorkspaceTimestamp(ws, buildParametersContext),
|
||||
});
|
||||
}
|
||||
ordered.sort((x) => x.timestamp);
|
||||
const matches = ordered.filter((x) => x.name.includes(workspace));
|
||||
const isWorkspaceBelowMax =
|
||||
matches.length > 0 &&
|
||||
(ordered.indexOf(matches[0]) < buildParametersContext.maxRetainedWorkspaces ||
|
||||
buildParametersContext.maxRetainedWorkspaces === 0);
|
||||
|
||||
return isWorkspaceBelowMax;
|
||||
}
|
||||
|
||||
public static async GetWorkspaceTimestamp(
|
||||
workspace: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<Number> {
|
||||
if (workspace.split(`_`).length > 0) {
|
||||
return Number(workspace.split(`_`)[1]);
|
||||
}
|
||||
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
throw new Error("Workspace doesn't exist, can't call get all locks");
|
||||
}
|
||||
|
||||
return (
|
||||
await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`,
|
||||
)
|
||||
)
|
||||
.map((x) => x.replace(`/`, ``))
|
||||
.filter((x) => x.includes(`_workspace`))
|
||||
.map((x) => Number(x))[0];
|
||||
}
|
||||
|
||||
public static async IsWorkspaceLocked(workspace: string, buildParametersContext: BuildParameters): Promise<boolean> {
|
||||
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
|
||||
return false;
|
||||
}
|
||||
const files = await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`,
|
||||
);
|
||||
|
||||
const workspaceFileDoesNotExists =
|
||||
files.filter((x) => {
|
||||
return x.includes(`_workspace`);
|
||||
}).length === 0;
|
||||
|
||||
const lockFilesExist =
|
||||
files.filter((x) => {
|
||||
return x.includes(`_lock`);
|
||||
}).length > 0;
|
||||
|
||||
return workspaceFileDoesNotExists || lockFilesExist;
|
||||
}
|
||||
|
||||
public static async CreateWorkspace(
|
||||
workspace: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
lockId: string = ``,
|
||||
): Promise<boolean> {
|
||||
if (lockId !== ``) {
|
||||
await SharedWorkspaceLocking.LockWorkspace(workspace, lockId, buildParametersContext);
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const file = `${timestamp}_workspace`;
|
||||
fs.writeFileSync(file, '');
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
fs.rmSync(file);
|
||||
|
||||
const workspaces = await SharedWorkspaceLocking.ReadLines(
|
||||
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`,
|
||||
);
|
||||
|
||||
CloudRunnerLogger.log(`All workspaces ${workspaces}`);
|
||||
if (!(await SharedWorkspaceLocking.IsWorkspaceBelowMax(workspace, buildParametersContext))) {
|
||||
CloudRunnerLogger.log(`Workspace is below max ${workspaces} ${buildParametersContext.maxRetainedWorkspaces}`);
|
||||
await SharedWorkspaceLocking.CleanupWorkspace(workspace, buildParametersContext);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async LockWorkspace(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
const file = `${Date.now()}_${runId}_lock`;
|
||||
fs.writeFileSync(file, '');
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
fs.rmSync(file);
|
||||
|
||||
return SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext);
|
||||
}
|
||||
|
||||
public static async ReleaseWorkspace(
|
||||
workspace: string,
|
||||
runId: string,
|
||||
buildParametersContext: BuildParameters,
|
||||
): Promise<boolean> {
|
||||
const file = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext)).filter((x) =>
|
||||
x.includes(`_${runId}_lock`),
|
||||
);
|
||||
CloudRunnerLogger.log(`Deleting lock ${workspace}/${file}`);
|
||||
CloudRunnerLogger.log(
|
||||
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
return !SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext);
|
||||
}
|
||||
|
||||
public static async CleanupWorkspace(workspace: string, buildParametersContext: BuildParameters) {
|
||||
await CloudRunnerSystem.Run(
|
||||
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace} --recursive`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private static async ReadLines(command: string): Promise<string[]> {
|
||||
return CloudRunnerSystem.RunAndReadLines(command);
|
||||
}
|
||||
}
|
||||
|
||||
export default SharedWorkspaceLocking;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user