mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-01-29 12:19:06 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22bc9a9bad | ||
|
|
6a53a9e853 | ||
|
|
cccd5074ea | ||
|
|
977683cd5f | ||
|
|
89bdaa5e46 | ||
|
|
24e9c186fd | ||
|
|
92cfb31622 | ||
|
|
67b76584b8 | ||
|
|
3ee15170fd | ||
|
|
3e0842dda0 | ||
|
|
1f8d196ed0 | ||
|
|
e003f9e2ca | ||
|
|
21634107c1 | ||
|
|
196fe8fc5b | ||
|
|
ec0cde0c85 | ||
|
|
72ff2983a1 | ||
|
|
fdf71758a9 | ||
|
|
91ec427695 | ||
|
|
6fb8550919 | ||
|
|
cb913cd286 | ||
|
|
96eeaf940a | ||
|
|
6ece6447b2 | ||
|
|
3523c6a934 | ||
|
|
bf702784d2 | ||
|
|
bdc3a88d22 | ||
|
|
401ddcaae0 | ||
|
|
1245bfefc8 | ||
|
|
229b0d02f8 | ||
|
|
c68bdf8177 | ||
|
|
938926799f | ||
|
|
5efb4868ad | ||
|
|
7749b8862d | ||
|
|
98a56c4169 | ||
|
|
720ee0c896 | ||
|
|
d42c251af3 |
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Bug description**
|
||||
|
||||
<!--A clear and concise description of what the bug is.-->
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
<!--Steps to reproduce the behavior:-->
|
||||
|
||||
-
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
<!--A clear and concise description of what you expected to happen.-->
|
||||
|
||||
**Additional details**
|
||||
|
||||
<!--Please add context, links, reasons, screenshots, etc.-->
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an improvement or now feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Context**
|
||||
|
||||
<!--Please describe a proper context-->
|
||||
|
||||
**Suggested solution**
|
||||
|
||||
<!--Tell us what you would suggest-->
|
||||
|
||||
**Considered alternatives**
|
||||
|
||||
<!--Please add any alternative solutions that you have considered-->
|
||||
|
||||
**Additional details**
|
||||
|
||||
<!--Please add context, links, reasons, screenshots, etc.-->
|
||||
7
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Other
|
||||
about: Everything else
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
11
.github/pull_request_template.md
vendored
Normal file
11
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
#### Changes
|
||||
|
||||
- ...
|
||||
|
||||
#### Checklist
|
||||
|
||||
<!-- please check all items and add your own -->
|
||||
|
||||
- [x] Read the contribution [guide](../CONTRIBUTING.md) and accept the [code](../CODE_OF_CONDUCT.md) of conduct
|
||||
- [ ] Readme (updated or not needed)
|
||||
- [ ] Tests (added, updated or not needed)
|
||||
78
.github/workflows/main.yml
vendored
78
.github/workflows/main.yml
vendored
@@ -1,12 +1,15 @@
|
||||
name: Actions 😎
|
||||
name: Actions
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
push: { branches: [master] }
|
||||
|
||||
env:
|
||||
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"d39b8e2f4d364b2e98b06afa0c6e08c5\"/>\n <Binding Key=\"2\" Value=\"d39b8e2f4d364b2e98b06afa0c6e08c5\"/>\n </MachineBindings>\n <MachineID Value=\"Xxo1ZKbdPu/IATrc0mPBYANJFF0=\"/>\n <SerialHash Value=\"1efd68fa935192b6090ac03c77d289a9f588c55a\"/>\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=\"AQAAAEY0LUg2WFMtUE00NS1SM0M4LUUyWlotWkdWOA==\"/>\n <SerialMasked Value=\"F4-H6XS-PM45-R3C8-E2ZZ-XXXX\"/>\n <StartDate Value=\"2018-05-02T00:00:00\"/>\n <UpdateDate Value=\"2019-11-25T18:23:38\"/>\n <InitialActivationDate Value=\"2018-05-02T14:21:28\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2019.2.11f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" 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>JHdOBFmBNq2H8BrGFzir/StLoYo=</DigestValue></Reference></SignedInfo><SignatureValue>aENLHd37a51RtP2/g7YU0Pexf5mx0/ENXYGtrPzqwZ8NQt2AsSdxGnl0CUB45/GuNXfJVDt2HWot\ncNYZB2OylVBn1WHQbKZlPmm8gEAMz0MYbr4Isb5i5buryBrZlmbEOjnRI+pEg1CBwlgMo6xdtjjE\n/d7cC293QIUO91kdzRXftYou1dNaUyuPL9ZH65vdB2pDXGRNxgUVD+GnnqZA7b5L2HXqNQclcWAK\n5Yd1BeF3VzR1iLw9G/SmH5oOhnpXSmqbL4qk7LVP2/mgXpFk5kP4X8VC3z47obNhBIGq40dwWyEe\nUYk5/nRAOkZawDT+tcu96e06gPC9Cxk5PdbRbA==</SignatureValue></Signature></root>"
|
||||
CODECOV_TOKEN: '2f2eb890-30e2-4724-83eb-7633832cf0de'
|
||||
GKE_ZONE: 'us-central1-c'
|
||||
GKE_REGION: 'us-central1'
|
||||
GKE_PROJECT: 'unitykubernetesbuilder'
|
||||
GKE_CLUSTER: 'unity-builder-cluster'
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -22,8 +25,7 @@ jobs:
|
||||
- run: yarn test --coverage
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: yarn build || { echo "build command should always succeed" ; exit 61; }
|
||||
- run: yarn build --quiet && git diff --quiet action || { echo "action should be auto generated" ; git diff action ; exit 62; }
|
||||
|
||||
# - run: yarn build --quiet && git diff --quiet action || { echo "action should be auto generated" ; git diff action ; exit 62; }
|
||||
buildForAllPlatforms:
|
||||
name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -34,21 +36,28 @@ jobs:
|
||||
- test-project
|
||||
unityVersion:
|
||||
- 2019.2.11f1
|
||||
# - 2019.3.0f1 # requires different license file/method
|
||||
- 2019.3.15f1
|
||||
include:
|
||||
# Please be polite; don't copy my personal licenses.
|
||||
# These are here because they are needed to allowing pull requests from forks to unity-builder.
|
||||
# You should be using ${{ secrets.UNITY_LICENSE_2019_3_15 }} here.
|
||||
- unityVersion: 2019.2.11f1
|
||||
license: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"d39b8e2f4d364b2e98b06afa0c6e08c5\"/>\n <Binding Key=\"2\" Value=\"d39b8e2f4d364b2e98b06afa0c6e08c5\"/>\n </MachineBindings>\n <MachineID Value=\"Xxo1ZKbdPu/IATrc0mPBYANJFF0=\"/>\n <SerialHash Value=\"1efd68fa935192b6090ac03c77d289a9f588c55a\"/>\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=\"AQAAAEY0LUg2WFMtUE00NS1SM0M4LUUyWlotWkdWOA==\"/>\n <SerialMasked Value=\"F4-H6XS-PM45-R3C8-E2ZZ-XXXX\"/>\n <StartDate Value=\"2018-05-02T00:00:00\"/>\n <UpdateDate Value=\"2019-11-25T18:23:38\"/>\n <InitialActivationDate Value=\"2018-05-02T14:21:28\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2019.2.11f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" 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>JHdOBFmBNq2H8BrGFzir/StLoYo=</DigestValue></Reference></SignedInfo><SignatureValue>aENLHd37a51RtP2/g7YU0Pexf5mx0/ENXYGtrPzqwZ8NQt2AsSdxGnl0CUB45/GuNXfJVDt2HWot\ncNYZB2OylVBn1WHQbKZlPmm8gEAMz0MYbr4Isb5i5buryBrZlmbEOjnRI+pEg1CBwlgMo6xdtjjE\n/d7cC293QIUO91kdzRXftYou1dNaUyuPL9ZH65vdB2pDXGRNxgUVD+GnnqZA7b5L2HXqNQclcWAK\n5Yd1BeF3VzR1iLw9G/SmH5oOhnpXSmqbL4qk7LVP2/mgXpFk5kP4X8VC3z47obNhBIGq40dwWyEe\nUYk5/nRAOkZawDT+tcu96e06gPC9Cxk5PdbRbA==</SignatureValue></Signature></root>"
|
||||
- unityVersion: 2019.3.15f1
|
||||
license: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"33bf639e81e54693a8f9bf57c8900e5a\"/>\n <Binding Key=\"2\" Value=\"33bf639e81e54693a8f9bf57c8900e5a\"/>\n </MachineBindings>\n <MachineID Value=\"xWka2iXdDJejhZdi/zU2RUeXUi4=\"/>\n <SerialHash Value=\"1efd68fa935192b6090ac03c77d289a9f588c55a\"/>\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=\"AQAAAEY0LUg2WFMtUE00NS1SM0M4LUUyWlotWkdWOA==\"/>\n <SerialMasked Value=\"F4-H6XS-PM45-R3C8-E2ZZ-XXXX\"/>\n <StartDate Value=\"2018-05-02T00:00:00\"/>\n <UpdateDate Value=\"2020-06-14T13:49:47\"/>\n <InitialActivationDate Value=\"2018-05-02T14:21:28\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2019.3.15f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" 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>bpzWx3PZ0lqWDo1m9aLQuZ4cweo=</DigestValue></Reference></SignedInfo><SignatureValue>QcDm4/qAXZuUMQbUVk63vO6u66Bp8PnqqWQcZZOcym/rGUZLj1sr66EquF3X3w1L7aqiwMGtbY2b\nkPttcalFeaBkc5NsJMrexWjuBCxQvhbmVFQnTjvC6vNS+k1wrkz7If1oPkz/XaDtCfUs8oxc9iPe\nPzzUJIVYLZoDtpPq2XbgVn9/TiVb3Zu6ldKgvtNRYUjrB3KywtvL9OcIFll3htRcBZPG43kxryJc\nDD2TL5Nw1JuX6MejBBuYTZsZNpGX9Pjop9+uFUZ4GI9h8a5g6wJUfXzsGw7j4gkvDkC9MvyWiksi\n2hNXw1QNeB6JfQsd4sAuhYh/CqTm2gCz9i9ZpA==</SignatureValue></Signature></root>"
|
||||
targetPlatform:
|
||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
- StandaloneWindows # Build a Windows standalone.
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- iOS # Build an iOS player.
|
||||
# - Android # Build an Android .apk standalone app.
|
||||
- WebGL # WebGL.
|
||||
# - Android # Build an Android .apk.
|
||||
# - StandaloneWindows # Build a Windows standalone.
|
||||
# - WebGL # WebGL.
|
||||
# - 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.
|
||||
|
||||
# - Switch # Build a Nintendo Switch player
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -61,6 +70,8 @@ jobs:
|
||||
Library-${{ matrix.projectPath }}-
|
||||
Library-
|
||||
- uses: ./
|
||||
env:
|
||||
UNITY_LICENSE: ${{ matrix.license }}
|
||||
with:
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
@@ -68,5 +79,50 @@ jobs:
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: Build
|
||||
name: Build (${{ matrix.unityVersion }})
|
||||
path: build
|
||||
k8sBuilds:
|
||||
name: K8s build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
targetPlatform:
|
||||
- StandaloneLinux64
|
||||
- StandaloneWindows64
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
- uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
|
||||
with:
|
||||
version: '288.0.0'
|
||||
service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}
|
||||
service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||
- run: ./action/bootstrapper/ApplyClusterAndAcquireLock.sh ${{ env.GKE_PROJECT }} ${{ env.GKE_CLUSTER }} ${{ env.GKE_ZONE }}
|
||||
- uses: frostebite/File-To-Base64@master
|
||||
id: read-base64
|
||||
with:
|
||||
filePath: ~/.kube/config
|
||||
- uses: ./
|
||||
id: k8s-unity-build
|
||||
env:
|
||||
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"33bf639e81e54693a8f9bf57c8900e5a\"/>\n <Binding Key=\"2\" Value=\"33bf639e81e54693a8f9bf57c8900e5a\"/>\n </MachineBindings>\n <MachineID Value=\"xWka2iXdDJejhZdi/zU2RUeXUi4=\"/>\n <SerialHash Value=\"1efd68fa935192b6090ac03c77d289a9f588c55a\"/>\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=\"AQAAAEY0LUg2WFMtUE00NS1SM0M4LUUyWlotWkdWOA==\"/>\n <SerialMasked Value=\"F4-H6XS-PM45-R3C8-E2ZZ-XXXX\"/>\n <StartDate Value=\"2018-05-02T00:00:00\"/>\n <UpdateDate Value=\"2020-06-14T13:49:47\"/>\n <InitialActivationDate Value=\"2018-05-02T14:21:28\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2019.3.15f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" 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>bpzWx3PZ0lqWDo1m9aLQuZ4cweo=</DigestValue></Reference></SignedInfo><SignatureValue>QcDm4/qAXZuUMQbUVk63vO6u66Bp8PnqqWQcZZOcym/rGUZLj1sr66EquF3X3w1L7aqiwMGtbY2b\nkPttcalFeaBkc5NsJMrexWjuBCxQvhbmVFQnTjvC6vNS+k1wrkz7If1oPkz/XaDtCfUs8oxc9iPe\nPzzUJIVYLZoDtpPq2XbgVn9/TiVb3Zu6ldKgvtNRYUjrB3KywtvL9OcIFll3htRcBZPG43kxryJc\nDD2TL5Nw1JuX6MejBBuYTZsZNpGX9Pjop9+uFUZ4GI9h8a5g6wJUfXzsGw7j4gkvDkC9MvyWiksi\n2hNXw1QNeB6JfQsd4sAuhYh/CqTm2gCz9i9ZpA==</SignatureValue></Signature></root>"
|
||||
with:
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
kubeConfig: ${{ steps.read-base64.outputs.base64 }}
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
projectPath: test-project
|
||||
unityVersion: 2019.3.15f1
|
||||
- uses: frostebite/K8s-Download-Volume@master
|
||||
with:
|
||||
kubeConfig: ${{ steps.read-base64.outputs.base64 }}
|
||||
volume: ${{ steps.k8s-unity-build.outputs.volume }}
|
||||
sourcePath: repo/build/
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: Kubernetes Build (${{ matrix.targetPlatform }})
|
||||
path: k8s-volume-download
|
||||
- run: ./action/bootstrapper/ReleaseLockAndAttemptShutdown.sh ${{ env.GKE_PROJECT }} ${{ env.GKE_CLUSTER }} ${{ env.GKE_ZONE }}
|
||||
if: ${{ always() }}
|
||||
|
||||
353
README.md
353
README.md
@@ -1,341 +1,50 @@
|
||||
# Unity - Builder
|
||||
|
||||
[](https://github.com/webbertakken/unity-builder/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22Actions+%F0%9F%98%8E%22)
|
||||
[](https://snyk.io/test/github/webbertakken/unity-builder)
|
||||
[](https://lgtm.com/projects/g/webbertakken/unity-builder/context:javascript)
|
||||
[](https://codecov.io/gh/webbertakken/unity-builder)
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
<a href="https://github.com/marketplace/actions/unity-builder">
|
||||
<img width="800" src="media/UnityBuilder-Logo.png" alt="Unity Builder">
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
GitHub Action to
|
||||
[build Unity projects](https://github.com/marketplace/actions/unity-builder)
|
||||
for different platforms.
|
||||
|
||||
Part of the
|
||||
[Unity Actions](https://github.com/webbertakken/unity-actions)
|
||||
collection.
|
||||
Part of the <a href="https://unity-ci.com"><img height="30" src="media/UnityCI-ReferenceLogo.png" alt="Unity CI"></a> open source project.
|
||||
<br />
|
||||
<br />
|
||||
|
||||
---
|
||||
[](https://github.com/webbertakken/unity-builder/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22Actions)
|
||||
[](https://lgtm.com/projects/g/webbertakken/unity-builder/context:javascript)
|
||||
[](https://codecov.io/gh/webbertakken/unity-builder)
|
||||
<br />
|
||||
<br />
|
||||
|
||||
[Github Action](https://github.com/features/actions)
|
||||
to build Unity projects for different platforms.
|
||||
</div>
|
||||
|
||||
It is recommended to run the
|
||||
[Test](https://github.com/webbertakken/unity-actions#test)
|
||||
action from the
|
||||
[Unity Actions](https://github.com/webbertakken/unity-actions)
|
||||
collection before running this action. This action also requires the [Activation](https://github.com/marketplace/actions/unity-activate) step.
|
||||
## How to use
|
||||
|
||||
## Documentation
|
||||
Find the
|
||||
[docs](https://unity-ci.com/docs/github)
|
||||
on the Unity CI
|
||||
[website](https://unity-ci.com/).
|
||||
|
||||
See the
|
||||
[Unity Actions](https://github.com/webbertakken/unity-actions)
|
||||
collection repository for workflow documentation and reference implementation.
|
||||
## Related actions
|
||||
|
||||
## Usage
|
||||
Visit the
|
||||
<a href="https://github.com/webbertakken/unity-actions"><img height="30" src="media/UnityActions-ReferenceLogo.png" alt="Unity Actions"></a>
|
||||
status repository for related Actions.
|
||||
|
||||
#### Setup builder
|
||||
## Community
|
||||
|
||||
By default the enabled scenes from the project's settings will be built.
|
||||
Feel free to join us on
|
||||
<a href="http://unity-ci.com/discord"><img height="30" src="media/Discord-Logo.svg" alt="Discord" /></a>
|
||||
and engage with the community.
|
||||
|
||||
Create or edit the file called `.github/workflows/main.yml` and add a job to it.
|
||||
## Contributing
|
||||
|
||||
##### Personal License
|
||||
To help out sharpen the documentation, please find the docs [repository](https://github.com/Unity-CI/Website).
|
||||
|
||||
Personal licenses require a one-time manual activation step (per unity version).
|
||||
|
||||
Make sure you
|
||||
[acquire and activate](https://github.com/marketplace/actions/unity-request-activation-file)
|
||||
your license file and add it as a secret.
|
||||
|
||||
Then, define the build step as follows:
|
||||
|
||||
```yaml
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
projectPath: path/to/your/project
|
||||
unityVersion: 2020.X.XXXX
|
||||
targetPlatform: WebGL
|
||||
```
|
||||
|
||||
##### Professional license
|
||||
|
||||
Professional licenses do not need any manual steps.
|
||||
|
||||
Instead, three variables will need to be set.
|
||||
|
||||
- `UNITY_EMAIL` (should contain the email address for your Unity account)
|
||||
- `UNITY_PASSWORD` (the password that you use to login to Unity)
|
||||
- `UNITY_SERIAL` (the serial provided by Unity)
|
||||
|
||||
Define the build step as follows:
|
||||
|
||||
```yaml
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
with:
|
||||
projectPath: path/to/your/project
|
||||
unityVersion: 2020.X.XXXX
|
||||
targetPlatform: WebGL
|
||||
```
|
||||
|
||||
That is all you need to build your project.
|
||||
|
||||
#### Storing the build
|
||||
|
||||
To be able to access your built files,
|
||||
they need to be uploaded as artifacts.
|
||||
To do this it is recommended to use Github Actions official
|
||||
[upload artifact action](https://github.com/marketplace/actions/upload-artifact)
|
||||
after any build action.
|
||||
|
||||
By default, Builder outputs it's builds to a folder named `build`.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: Build
|
||||
path: build
|
||||
```
|
||||
|
||||
Builds can now be downloaded as Artifacts in the Actions tab.
|
||||
|
||||
#### Caching
|
||||
|
||||
In order to make builds run faster, you can cache Library files from previous
|
||||
builds. To do so simply add Github Actions official
|
||||
[cache action](https://github.com/marketplace/actions/cache) before any unity steps.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1.1.0
|
||||
with:
|
||||
path: path/to/your/project/Library
|
||||
key: Library-MyProjectName-TargetPlatform
|
||||
restore-keys: |
|
||||
Library-MyProjectName-
|
||||
Library-
|
||||
```
|
||||
|
||||
This simple addition could speed up your build by more than 50%.
|
||||
|
||||
## Complete example
|
||||
|
||||
A complete workflow that builds every available platform could look like this:
|
||||
|
||||
```yaml
|
||||
name: Build project
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
push: { branches: [master] }
|
||||
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
|
||||
jobs:
|
||||
buildForSomePlatforms:
|
||||
name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
projectPath:
|
||||
- path/to/your/project
|
||||
unityVersion:
|
||||
- 2019.2.11f1
|
||||
- 2019.3.0f1
|
||||
targetPlatform:
|
||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||
- StandaloneWindows # Build a Windows standalone.
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- iOS # Build an iOS player.
|
||||
- Android # Build an Android .apk standalone app.
|
||||
- WebGL # WebGL.
|
||||
- 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:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
- uses: actions/cache@v1.1.0
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-${{ matrix.targetPlatform }}
|
||||
restore-keys: |
|
||||
Library-${{ matrix.projectPath }}-
|
||||
Library-
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
with:
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
targetPlatform: ${{ matrix.targetPlatform }}
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: Build
|
||||
path: build
|
||||
```
|
||||
|
||||
> **Note:** _Environment variables are set for all jobs in the workflow like this._
|
||||
|
||||
## Configuration options
|
||||
|
||||
Below options can be specified under `with:` for the `unity-builder` action.
|
||||
|
||||
#### projectPath
|
||||
|
||||
Specify the path to your Unity project to be built.
|
||||
The path should be relative to the root of your project.
|
||||
|
||||
_**required:** `false`_
|
||||
_**default:** `<your project root>`_
|
||||
|
||||
#### unityVersion
|
||||
|
||||
Version of Unity to use for building the project.
|
||||
|
||||
_**required:** `false`_
|
||||
_**default:** `2019.2.1f11`_
|
||||
|
||||
#### targetPlatform
|
||||
|
||||
Platform that the build should target.
|
||||
|
||||
Must be one of the [allowed values](https://docs.unity3d.com/ScriptReference/BuildTarget.html) listed in the Unity scripting manual.
|
||||
|
||||
_**required:** `true`_
|
||||
|
||||
#### buildName
|
||||
|
||||
Name of the build. Also the folder in which the build will be stored within `buildsPath`.
|
||||
|
||||
_**required:** `false`_
|
||||
_**default:** `<build_target>`_
|
||||
|
||||
#### buildsPath
|
||||
|
||||
Path where the builds should be stored.
|
||||
|
||||
In this folder a folder will be created for every targetPlatform.
|
||||
|
||||
_**required:** `false`_
|
||||
_**default:** `build`_
|
||||
|
||||
#### buildMethod
|
||||
|
||||
Custom command to run your build.
|
||||
|
||||
There are two conditions for a custom buildCommand:
|
||||
|
||||
- Must reference a valid path to a `static` method.
|
||||
- The class must reside in the `Assets/Editor` directory.
|
||||
|
||||
_**example:**_
|
||||
|
||||
```yaml
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
with:
|
||||
buildMethod: EditorNamespace.BuilderClassName.StaticBulidMethod
|
||||
```
|
||||
|
||||
_**required:** `false`_
|
||||
_**default:** Built-in script that will run a build out of the box._
|
||||
|
||||
#### versioning
|
||||
|
||||
Configure a specific versioning strategy
|
||||
|
||||
```yaml
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
with:
|
||||
versioning: Semantic
|
||||
```
|
||||
|
||||
Find the available strategies below:
|
||||
|
||||
##### Semantic
|
||||
|
||||
Versioning out of the box! **(recommended)**
|
||||
|
||||
> Compatible with **all platforms**.
|
||||
> Does **not** modify your repository.
|
||||
> Requires **zero configuration**.
|
||||
|
||||
How it works:
|
||||
|
||||
> Generates a version based on [semantic versioning](https://semver.org/).
|
||||
> Follows `<major>.<minor>.<patch>` for example `0.17.2`.
|
||||
> The latest tag dictates `<major>.<minor>` (defaults to 0.0 for no tag).
|
||||
> The number of commits (since the last tag, if any) is used for `<patch>`.
|
||||
|
||||
No configuration required.
|
||||
|
||||
##### Custom
|
||||
|
||||
Allows specifying a custom version in the `version` field. **(advanced users)**
|
||||
|
||||
> This strategy is useful when your project or pipeline has some kind of orchestration
|
||||
> that determines the versions.
|
||||
|
||||
##### None
|
||||
|
||||
No version will be set by Builder. **(not recommended)**
|
||||
|
||||
> Not recommended unless you generate a new version in a pre-commit hook. Manually
|
||||
> setting versions is error-prone.
|
||||
|
||||
#### allowDirtyBuild
|
||||
|
||||
Allows the branch of the build to be dirty, and still generate the build.
|
||||
|
||||
```yaml
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
with:
|
||||
allowDirtyBuild: true
|
||||
```
|
||||
|
||||
Note that it is generally bad practice to modify your branch
|
||||
in a CI Pipeline. However there are exceptions where this might
|
||||
be needed. (use with care).
|
||||
|
||||
#### customParameters
|
||||
|
||||
Custom parameters to configure the build.
|
||||
|
||||
Parameters must start with a hyphen (`-`) and may be followed by a value (without hyphen).
|
||||
|
||||
Parameters without a value will be considered booleans (with a value of true).
|
||||
|
||||
_**example:**_
|
||||
|
||||
```yaml
|
||||
- uses: webbertakken/unity-builder@<version>
|
||||
with:
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
|
||||
```
|
||||
|
||||
_**required:** `false`_
|
||||
_**default:** ""_
|
||||
|
||||
## More actions
|
||||
|
||||
Visit
|
||||
[Unity Actions](https://github.com/webbertakken/unity-actions)
|
||||
to find related actions for Unity.
|
||||
|
||||
Feel free to contribute.
|
||||
To contribute to Unity Builder, kindly read the [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
87
action.yml
87
action.yml
@@ -6,6 +6,10 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Version of unity to use for building the project.'
|
||||
customImage:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Specific docker image that should be used for building the project'
|
||||
targetPlatform:
|
||||
required: false
|
||||
default: ''
|
||||
@@ -26,7 +30,88 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Path to a Namespace.Class.StaticMethod to run to perform the build.'
|
||||
outputs: {}
|
||||
kubeConfig:
|
||||
default: ''
|
||||
required: false
|
||||
description: '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.'
|
||||
kubeContainerMemory:
|
||||
default: '800M'
|
||||
required: false
|
||||
description: 'Amount of memory to assign the build container in Kubernetes (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes)'
|
||||
kubeContainerCPU:
|
||||
default: '0.25'
|
||||
required: false
|
||||
description: 'Amount of CPU time to assign the build container in Kubernetes (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes)'
|
||||
kubeVolumeSize:
|
||||
default: '5Gi'
|
||||
required: false
|
||||
description: 'Amount of disc space to assign the Kubernetes Persistent Volume (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes)'
|
||||
githubToken:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'GitHub token for cloning, only needed when kubeconfig is used.'
|
||||
versioning:
|
||||
required: false
|
||||
default: 'Semantic'
|
||||
description: 'The versioning scheme to use when building the project'
|
||||
version:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The version, when used with the "Custom" versioning scheme'
|
||||
androidVersionCode:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The android versionCode'
|
||||
androidAppBundle:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Whether to build .aab instead of .apk'
|
||||
androidKeystoreName:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The android keystoreName'
|
||||
androidKeystoreBase64:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The base64 contents of the android keystore file'
|
||||
androidKeystorePass:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The android keystorePass'
|
||||
androidKeyaliasName:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The android keyaliasName'
|
||||
androidKeyaliasPass:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'The android keyaliasPass'
|
||||
customParameters:
|
||||
required: false
|
||||
default: ''
|
||||
description: >
|
||||
Custom parameters to configure the build.
|
||||
|
||||
Parameters must start with a hyphen (-) and may be followed by a value (without hyphen).
|
||||
Parameters without a value will be considered booleans (with a value of true).
|
||||
allowDirtyBuild:
|
||||
required: false
|
||||
default: ''
|
||||
description: >
|
||||
Allows the branch of the build to be dirty, and still generate the build.
|
||||
|
||||
Note that it is generally bad practice to modify your branch
|
||||
in a CI Pipeline. However there are exceptions where this might
|
||||
be needed. (use with care).
|
||||
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'
|
||||
branding:
|
||||
icon: 'box'
|
||||
color: 'gray-dark'
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
ARG IMAGE
|
||||
FROM $IMAGE
|
||||
|
||||
ARG UNAME=runner
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
|
||||
LABEL "com.github.actions.name"="Unity - Builder"
|
||||
LABEL "com.github.actions.description"="Build Unity projects for different platforms."
|
||||
LABEL "com.github.actions.icon"="box"
|
||||
@@ -10,9 +14,15 @@ LABEL "repository"="http://github.com/webbertakken/unity-actions"
|
||||
LABEL "homepage"="http://github.com/webbertakken/unity-actions"
|
||||
LABEL "maintainer"="Webber Takken <webber@takken.io>"
|
||||
|
||||
RUN bash -c 'mkdir -p /github/{home,workflow,workspace}' && chown $UID:$GID -R /github/
|
||||
RUN getent group $GID || groupadd -g $GID $UNAME
|
||||
RUN id -u $UID &>/dev/null || useradd -m -u $UID -g $GID -s /bin/bash -d /github/home $UNAME
|
||||
|
||||
ADD default-build-script /UnityBuilderAction
|
||||
ADD steps /steps
|
||||
RUN chmod -R +x /steps
|
||||
ADD entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
USER $UID:$GID
|
||||
|
||||
69
action/bootstrapper/ApplyClusterAndAcquireLock.sh
Executable file
69
action/bootstrapper/ApplyClusterAndAcquireLock.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This creates a GKE Cluster
|
||||
# - Will wait for any deletion to complete on a cluster with the same name before creating
|
||||
# - Will wait for completion before continuing
|
||||
# - If the script is run concurrently multiple times, only one cluster will be created, all instances will wait for availability
|
||||
# Requires GCP Cloud SDK
|
||||
# Installs retry https://github.com/kadwanev/retry
|
||||
|
||||
GKE_PROJECT=$1
|
||||
GKE_CLUSTER=$2
|
||||
GKE_ZONE=$3
|
||||
|
||||
# may update this to avoid repeated install, drop me a comment if needed
|
||||
sudo sh -c "curl https://raw.githubusercontent.com/kadwanev/retry/master/retry -o /usr/local/bin/retry && chmod +x /usr/local/bin/retry"
|
||||
|
||||
attempts=0
|
||||
while [ $attempts -le 1 ]
|
||||
do
|
||||
retry -s 15 -t 20 -v '
|
||||
STATUS=$(gcloud container clusters list --format="json" --project $GKE_PROJECT |
|
||||
jq "
|
||||
.[] |
|
||||
{name: .name, status: .status} |
|
||||
select(.name == \"$GKE_CLUSTER\")
|
||||
" |
|
||||
jq ".status")
|
||||
if [ "$STATUS" == "\"STOPPING\"" ]; then echo "Cluster stopping waiting for completion" && exit 1; fi
|
||||
exit 0
|
||||
'
|
||||
cluster=$(gcloud container clusters list --project $GKE_PROJECT --format="json" | jq '.[] | select(.name == "${GKE_CLUSTER}")')
|
||||
|
||||
if [ -z "$cluster" ];
|
||||
then
|
||||
echo "No clusters found for \"$GKE_CLUSTER\" in project \"$GKE_CLUSTER\" in zone \"$GKE_ZONE\""
|
||||
# you may not need this, it installs GCP beta for additional command line options
|
||||
gcloud components install beta -q
|
||||
# replace this line with whatever type of cluster you want to create
|
||||
gcloud beta container --project $GKE_PROJECT clusters create $GKE_CLUSTER --zone $GKE_ZONE --no-enable-basic-auth --cluster-version "1.15.12-gke.2" --machine-type "custom-1-3072" --image-type "COS" --disk-type "pd-standard" --disk-size "15" --metadata disable-legacy-endpoints=true --scopes "https://www.googleapis.com/auth/devstorage.read_only","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/monitoring","https://www.googleapis.com/auth/servicecontrol","https://www.googleapis.com/auth/service.management.readonly","https://www.googleapis.com/auth/trace.append" --num-nodes "1" --enable-stackdriver-kubernetes --enable-ip-alias --default-max-pods-per-node "110" --enable-autoscaling --min-nodes "0" --max-nodes "3" --no-enable-master-authorized-networks --addons HorizontalPodAutoscaling,HttpLoadBalancing --enable-autoupgrade --enable-autorepair --max-surge-upgrade 1 --max-unavailable-upgrade 0
|
||||
fi;
|
||||
retry -s 15 -t 20 -v '
|
||||
STATUS=$(gcloud container clusters list --format="json" --project $GKE_PROJECT |
|
||||
jq "
|
||||
.[] |
|
||||
{name: .name, status: .status} |
|
||||
select(.name == \"$GKE_CLUSTER\")
|
||||
" |
|
||||
jq ".status")
|
||||
if [ "$STATUS" == "\"PROVISIONING\"" ]; then echo "Cluster provisioning waiting for available" && exit 1; fi
|
||||
exit 0
|
||||
'
|
||||
echo "Cluster is available"
|
||||
gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT
|
||||
kubectl version
|
||||
NSID=$(cat /proc/sys/kernel/random/uuid)
|
||||
echo "::set-env name=NSID::"$NSID
|
||||
{
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ns-unity-builder-$NSID
|
||||
labels:
|
||||
app: unity-builder
|
||||
EOF
|
||||
} && exit 0
|
||||
|
||||
attempts=$(($attempts+1))
|
||||
done
|
||||
13
action/bootstrapper/ReleaseLockAndAttemptShutdown.sh
Executable file
13
action/bootstrapper/ReleaseLockAndAttemptShutdown.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
kubectl delete ns ns-unity-builder-$NSID
|
||||
|
||||
# do any unity-builder namespaces remain?
|
||||
namespaceCount=$(kubectl get ns --output json | jq ".items | .[] | select(.metadata.labels.app == \"unity-builder\") | select(.status.phase != \"TERMINATING\")" | jq -s "length")
|
||||
echo $namespaceCount
|
||||
if [ "$namespaceCount" != "0" ]
|
||||
then
|
||||
echo "let next cluster delete"
|
||||
exit 0
|
||||
else
|
||||
echo "delete cluster"
|
||||
retry -s 15 -t 5 -v 'gcloud container clusters delete $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT --quiet'
|
||||
fi
|
||||
@@ -27,7 +27,12 @@ namespace UnityBuilderAction
|
||||
};
|
||||
|
||||
// Set version for this build
|
||||
VersionApplicator.SetVersion(options["version"]);
|
||||
VersionApplicator.SetVersion(options["buildVersion"]);
|
||||
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
|
||||
|
||||
// Apply Android settings
|
||||
if (buildOptions.target == BuildTarget.Android)
|
||||
AndroidSettings.Apply(options);
|
||||
|
||||
// Perform build
|
||||
BuildReport buildReport = BuildPipeline.BuildPlayer(buildOptions);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityBuilderAction.Input
|
||||
{
|
||||
public class AndroidSettings
|
||||
{
|
||||
public static void Apply(Dictionary<string, string> options)
|
||||
{
|
||||
EditorUserBuildSettings.buildAppBundle = options["customBuildPath"].EndsWith(".aab");
|
||||
if (options.TryGetValue("androidKeystoreName", out string keystoreName) && !string.IsNullOrEmpty(keystoreName))
|
||||
PlayerSettings.Android.keystoreName = keystoreName;
|
||||
if (options.TryGetValue("androidKeystorePass", out string keystorePass) && !string.IsNullOrEmpty(keystorePass))
|
||||
PlayerSettings.Android.keystorePass = keystorePass;
|
||||
if (options.TryGetValue("androidKeyaliasName", out string keyaliasName) && !string.IsNullOrEmpty(keyaliasName))
|
||||
PlayerSettings.Android.keyaliasName = keyaliasName;
|
||||
if (options.TryGetValue("androidKeyaliasPass", out string keyaliasPass) && !string.IsNullOrEmpty(keyaliasPass))
|
||||
PlayerSettings.Android.keyaliasPass = keyaliasPass;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d51cf8acfff8c941bb753e82750b60a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityBuilderAction.Input
|
||||
@@ -7,6 +8,7 @@ namespace UnityBuilderAction.Input
|
||||
public class ArgumentsParser
|
||||
{
|
||||
static string EOL = Environment.NewLine;
|
||||
static readonly string[] Secrets = { "androidKeystorePass", "androidKeyaliasName", "androidKeyaliasPass" };
|
||||
|
||||
public static Dictionary<string, string> GetValidatedOptions()
|
||||
{
|
||||
@@ -66,9 +68,11 @@ namespace UnityBuilderAction.Input
|
||||
// Parse optional value
|
||||
bool flagHasValue = next < args.Length && !args[next].StartsWith("-");
|
||||
string value = flagHasValue ? args[next].TrimStart('-') : "";
|
||||
bool secret = Secrets.Contains(flag);
|
||||
string displayValue = secret ? "*HIDDEN*" : "\"" + value + "\"";
|
||||
|
||||
// Assign
|
||||
Console.WriteLine($"Found flag \"{flag}\" with value \"{value}\".");
|
||||
Console.WriteLine($"Found flag \"{flag}\" with value {displayValue}.");
|
||||
providedArguments.Add(flag, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityBuilderAction.Versioning
|
||||
@@ -11,13 +10,18 @@ namespace UnityBuilderAction.Versioning
|
||||
if (version == "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Apply(version);
|
||||
}
|
||||
|
||||
public static void SetAndroidVersionCode(string androidVersionCode) {
|
||||
PlayerSettings.Android.bundleVersionCode = Int32.Parse(androidVersionCode);
|
||||
}
|
||||
|
||||
static void Apply(string version)
|
||||
{
|
||||
PlayerSettings.bundleVersion = version;
|
||||
PlayerSettings.macOS.buildNumber = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
action/exec-child.js
Normal file
39
action/exec-child.js
Normal file
@@ -0,0 +1,39 @@
|
||||
if (require.main !== module) {
|
||||
throw new Error('This file should not be required');
|
||||
}
|
||||
|
||||
var childProcess = require('child_process');
|
||||
var fs = require('fs');
|
||||
|
||||
var paramFilePath = process.argv[2];
|
||||
|
||||
var serializedParams = fs.readFileSync(paramFilePath, 'utf8');
|
||||
var params = JSON.parse(serializedParams);
|
||||
|
||||
var cmd = params.command;
|
||||
var execOptions = params.execOptions;
|
||||
var pipe = params.pipe;
|
||||
var stdoutFile = params.stdoutFile;
|
||||
var stderrFile = params.stderrFile;
|
||||
|
||||
var c = childProcess.exec(cmd, execOptions, function (err) {
|
||||
if (!err) {
|
||||
process.exitCode = 0;
|
||||
} else if (err.code === undefined) {
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.exitCode = err.code;
|
||||
}
|
||||
});
|
||||
|
||||
var stdoutStream = fs.createWriteStream(stdoutFile);
|
||||
var stderrStream = fs.createWriteStream(stderrFile);
|
||||
|
||||
c.stdout.pipe(stdoutStream);
|
||||
c.stderr.pipe(stderrStream);
|
||||
c.stdout.pipe(process.stdout);
|
||||
c.stderr.pipe(process.stderr);
|
||||
|
||||
if (pipe) {
|
||||
c.stdin.end(pipe);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -62,6 +62,16 @@ else
|
||||
#
|
||||
fi
|
||||
|
||||
#
|
||||
# Create Android keystore, if needed
|
||||
#
|
||||
if [[ -z $ANDROID_KEYSTORE_NAME || -z $ANDROID_KEYSTORE_BASE64 ]]; then
|
||||
echo "Not creating Android keystore."
|
||||
else
|
||||
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$ANDROID_KEYSTORE_NAME"
|
||||
echo "Created Android keystore."
|
||||
fi
|
||||
|
||||
#
|
||||
# Display custom parameters
|
||||
#
|
||||
@@ -109,8 +119,12 @@ xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
|
||||
-customBuildTarget "$BUILD_TARGET" \
|
||||
-customBuildPath "$CUSTOM_BUILD_PATH" \
|
||||
-executeMethod "$BUILD_METHOD" \
|
||||
-versioning "$VERSIONING" \
|
||||
-version "$VERSION" \
|
||||
-buildVersion "$VERSION" \
|
||||
-androidVersionCode "$ANDROID_VERSION_CODE" \
|
||||
-androidKeystoreName "$ANDROID_KEYSTORE_NAME" \
|
||||
-androidKeystorePass "$ANDROID_KEYSTORE_PASS" \
|
||||
-androidKeyaliasName "$ANDROID_KEYALIAS_NAME" \
|
||||
-androidKeyaliasPass "$ANDROID_KEYALIAS_PASS" \
|
||||
$CUSTOM_PARAMETERS
|
||||
|
||||
# Catch exit code
|
||||
@@ -123,6 +137,13 @@ else
|
||||
echo "Build failed, with exit code $BUILD_EXIT_CODE";
|
||||
fi
|
||||
|
||||
# Add permissions to make app runnable
|
||||
if [[ "$BUILD_TARGET" == "StandaloneOSX" ]]; then
|
||||
ADD_PERMISSIONS_PATH=$BUILD_PATH_FULL/StandaloneOSX.app/Contents/MacOS/*
|
||||
echo "Making the following path executable: $ADD_PERMISSIONS_PATH"
|
||||
chmod +x $ADD_PERMISSIONS_PATH
|
||||
fi
|
||||
|
||||
#
|
||||
# Results
|
||||
#
|
||||
|
||||
1
media/Discord-Logo.svg
Normal file
1
media/Discord-Logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 272.1"><style>.st0{fill:#7289DA;}</style><path class="st0" d="M142.8 120.1c-5.7 0-10.2 4.9-10.2 11s4.6 11 10.2 11c5.7 0 10.2-4.9 10.2-11s-4.6-11-10.2-11zM106.3 120.1c-5.7 0-10.2 4.9-10.2 11s4.6 11 10.2 11c5.7 0 10.2-4.9 10.2-11 .1-6.1-4.5-11-10.2-11z"/><path class="st0" d="M191.4 36.9h-134c-11.3 0-20.5 9.2-20.5 20.5v134c0 11.3 9.2 20.5 20.5 20.5h113.4l-5.3-18.3 12.8 11.8 12.1 11.1 21.6 18.7V57.4c-.1-11.3-9.3-20.5-20.6-20.5zm-38.6 129.5s-3.6-4.3-6.6-8c13.1-3.7 18.1-11.8 18.1-11.8-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.4-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.6-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.2-1.8-1-2.8-1.7-2.8-1.7s4.8 7.9 17.5 11.7c-3 3.8-6.7 8.2-6.7 8.2-22.1-.7-30.5-15.1-30.5-15.1 0-31.9 14.4-57.8 14.4-57.8 14.4-10.7 28-10.4 28-10.4l1 1.2c-18 5.1-26.2 13-26.2 13s2.2-1.2 5.9-2.8c10.7-4.7 19.2-5.9 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.5 0 0-7.9-7.5-24.9-12.6l1.4-1.6s13.7-.3 28 10.4c0 0 14.4 25.9 14.4 57.8 0-.1-8.4 14.3-30.5 15zM303.8 79.7h-33.2V117l22.1 19.9v-36.2h11.8c7.5 0 11.2 3.6 11.2 9.4v27.7c0 5.8-3.5 9.7-11.2 9.7h-34v21.1h33.2c17.8.1 34.5-8.8 34.5-29.2v-29.8c.1-20.8-16.6-29.9-34.4-29.9zm174 59.7v-30.6c0-11 19.8-13.5 25.8-2.5l18.3-7.4c-7.2-15.8-20.3-20.4-31.2-20.4-17.8 0-35.4 10.3-35.4 30.3v30.6c0 20.2 17.6 30.3 35 30.3 11.2 0 24.6-5.5 32-19.9l-19.6-9c-4.8 12.3-24.9 9.3-24.9-1.4zM417.3 113c-6.9-1.5-11.5-4-11.8-8.3.4-10.3 16.3-10.7 25.6-.8l14.7-11.3c-9.2-11.2-19.6-14.2-30.3-14.2-16.3 0-32.1 9.2-32.1 26.6 0 16.9 13 26 27.3 28.2 7.3 1 15.4 3.9 15.2 8.9-.6 9.5-20.2 9-29.1-1.8l-14.2 13.3c8.3 10.7 19.6 16.1 30.2 16.1 16.3 0 34.4-9.4 35.1-26.6 1-21.7-14.8-27.2-30.6-30.1zm-67 55.5h22.4V79.7h-22.4v88.8zM728 79.7h-33.2V117l22.1 19.9v-36.2h11.8c7.5 0 11.2 3.6 11.2 9.4v27.7c0 5.8-3.5 9.7-11.2 9.7h-34v21.1H728c17.8.1 34.5-8.8 34.5-29.2v-29.8c0-20.8-16.7-29.9-34.5-29.9zm-162.9-1.2c-18.4 0-36.7 10-36.7 30.5v30.3c0 20.3 18.4 30.5 36.9 30.5 18.4 0 36.7-10.2 36.7-30.5V109c0-20.4-18.5-30.5-36.9-30.5zm14.4 60.8c0 6.4-7.2 9.7-14.3 9.7-7.2 0-14.4-3.1-14.4-9.7V109c0-6.5 7-10 14-10 7.3 0 14.7 3.1 14.7 10v30.3zM682.4 109c-.5-20.8-14.7-29.2-33-29.2h-35.5v88.8h22.7v-28.2h4l20.6 28.2h28L665 138.1c10.7-3.4 17.4-12.7 17.4-29.1zm-32.6 12h-13.2v-20.3h13.2c14.1 0 14.1 20.3 0 20.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
media/UnityActions-ReferenceLogo.png
Normal file
BIN
media/UnityActions-ReferenceLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
media/UnityBuilder-Logo.png
Normal file
BIN
media/UnityBuilder-Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
media/UnityCI-ReferenceLogo.png
Normal file
BIN
media/UnityCI-ReferenceLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -10,12 +10,16 @@
|
||||
"prebuild": "yarn",
|
||||
"build": "ncc build src --out action --minify",
|
||||
"lint": "prettier --check \"src/**/*.js\" && eslint src",
|
||||
"format": "prettier --write \"src/**/*.js\"",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.4",
|
||||
"@actions/exec": "1.0.4",
|
||||
"@actions/github": "^2.1.1"
|
||||
"@actions/github": "^2.1.1",
|
||||
"base-64": "^0.1.0",
|
||||
"kubernetes-client": "^9.0.0",
|
||||
"semver": "^7.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
@@ -53,6 +57,9 @@
|
||||
"*.{json,md,yaml,yml}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.sh": [
|
||||
"git update-index --chmod=+x"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
24
src/index.js
24
src/index.js
@@ -1,4 +1,4 @@
|
||||
import { Action, BuildParameters, Cache, Docker, ImageTag } from './model';
|
||||
import { Action, BuildParameters, Cache, Docker, ImageTag, Kubernetes, Output } from './model';
|
||||
|
||||
const core = require('@actions/core');
|
||||
|
||||
@@ -10,12 +10,24 @@ async function action() {
|
||||
|
||||
const buildParameters = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameters);
|
||||
if (buildParameters.kubeConfig) {
|
||||
core.info('Building with Kubernetes');
|
||||
await Kubernetes.runBuildJob(buildParameters, baseImage);
|
||||
} else {
|
||||
// Build docker image
|
||||
// TODO: No image required (instead use a version published to dockerhub for the action, supply credentials for github cloning)
|
||||
const builtImage = await Docker.build({
|
||||
path: actionFolder,
|
||||
dockerfile,
|
||||
baseImage,
|
||||
uid: buildParameters.uid,
|
||||
gid: buildParameters.gid,
|
||||
});
|
||||
await Docker.run(builtImage, { workspace, ...buildParameters });
|
||||
}
|
||||
|
||||
// Build docker image
|
||||
const builtImage = await Docker.build({ path: actionFolder, dockerfile, baseImage });
|
||||
|
||||
// Run docker image
|
||||
await Docker.run(builtImage, { workspace, ...buildParameters });
|
||||
// Set output
|
||||
await Output.setBuildVersion(buildParameters.buildVersion);
|
||||
}
|
||||
|
||||
action().catch((error) => {
|
||||
|
||||
33
src/model/android-versioning.js
Normal file
33
src/model/android-versioning.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as semver from 'semver';
|
||||
|
||||
export default class AndroidVersioning {
|
||||
static determineVersionCode(version, inputVersionCode) {
|
||||
if (!inputVersionCode) {
|
||||
return AndroidVersioning.versionToVersionCode(version);
|
||||
}
|
||||
return inputVersionCode;
|
||||
}
|
||||
|
||||
static versionToVersionCode(version) {
|
||||
const parsedVersion = semver.parse(version);
|
||||
|
||||
if (!parsedVersion) {
|
||||
core.warning(`Could not parse "${version}" to semver, defaulting android version code to 1`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The greatest value Google Plays allows is 2100000000.
|
||||
// Allow for 3 patch digits, 3 minor digits and 3 major digits.
|
||||
const versionCode =
|
||||
parsedVersion.major * 1000000 + parsedVersion.minor * 1000 + parsedVersion.patch;
|
||||
|
||||
if (versionCode >= 1000000000) {
|
||||
throw new Error(
|
||||
`Generated versionCode ${versionCode} is dangerously close to the maximum allowed number 2100000000. Consider a different versioning scheme to be able to continue updating your application.`,
|
||||
);
|
||||
}
|
||||
core.info(`Using android versionCode ${versionCode}`);
|
||||
return versionCode;
|
||||
}
|
||||
}
|
||||
27
src/model/android-versioning.test.js
Normal file
27
src/model/android-versioning.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import AndroidVersioning from './android-versioning';
|
||||
|
||||
describe('Android Versioning', () => {
|
||||
describe('versionToVersionCode', () => {
|
||||
it('defaults to 1 when version is not a valid semver', () => {
|
||||
expect(AndroidVersioning.versionToVersionCode('abcd')).toBe(1);
|
||||
});
|
||||
|
||||
it('returns a number', () => {
|
||||
expect(AndroidVersioning.versionToVersionCode('123.456.789')).toBe(123456789);
|
||||
});
|
||||
|
||||
it('throw when generated version code is too large', () => {
|
||||
expect(() => AndroidVersioning.versionToVersionCode('1234.0.0')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineVersionCode', () => {
|
||||
it('defaults to parsed version', () => {
|
||||
expect(AndroidVersioning.determineVersionCode('1.2.3', '')).toBe(1002003);
|
||||
});
|
||||
|
||||
it('use specified code', () => {
|
||||
expect(AndroidVersioning.determineVersionCode('1.2.3', 2)).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,34 @@
|
||||
import os from 'os';
|
||||
import AndroidVersioning from './android-versioning';
|
||||
import Input from './input';
|
||||
import Platform from './platform';
|
||||
import Versioning from './versioning';
|
||||
|
||||
class BuildParameters {
|
||||
static async create() {
|
||||
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform);
|
||||
const buildFile = this.parseBuildFile(
|
||||
Input.buildName,
|
||||
Input.targetPlatform,
|
||||
Input.androidAppBundle,
|
||||
);
|
||||
const buildVersion = await Versioning.determineVersion(
|
||||
Input.versioningStrategy,
|
||||
Input.specifiedVersion,
|
||||
);
|
||||
|
||||
const androidVersionCode = AndroidVersioning.determineVersionCode(
|
||||
buildVersion,
|
||||
Input.androidVersionCode,
|
||||
);
|
||||
|
||||
const { uid, gid } = os.userInfo();
|
||||
|
||||
return {
|
||||
version: Input.unityVersion,
|
||||
customImage: Input.customImage,
|
||||
uid,
|
||||
gid,
|
||||
runnerTempPath: process.env.RUNNER_TEMP,
|
||||
platform: Input.targetPlatform,
|
||||
projectPath: Input.projectPath,
|
||||
buildName: Input.buildName,
|
||||
@@ -19,17 +36,29 @@ class BuildParameters {
|
||||
buildFile,
|
||||
buildMethod: Input.buildMethod,
|
||||
buildVersion,
|
||||
androidVersionCode,
|
||||
androidKeystoreName: Input.androidKeystoreName,
|
||||
androidKeystoreBase64: Input.androidKeystoreBase64,
|
||||
androidKeystorePass: Input.androidKeystorePass,
|
||||
androidKeyaliasName: Input.androidKeyaliasName,
|
||||
androidKeyaliasPass: Input.androidKeyaliasPass,
|
||||
customParameters: Input.customParameters,
|
||||
kubeConfig: Input.kubeConfig,
|
||||
githubToken: Input.githubToken,
|
||||
kubeContainerMemory: Input.kubeContainerMemory,
|
||||
kubeContainerCPU: Input.kubeContainerCPU,
|
||||
kubeVolumeSize: Input.kubeVolumeSize,
|
||||
kubeVolume: Input.kubeVolume,
|
||||
};
|
||||
}
|
||||
|
||||
static parseBuildFile(filename, platform) {
|
||||
static parseBuildFile(filename, platform, androidAppBundle) {
|
||||
if (Platform.isWindows(platform)) {
|
||||
return `${filename}.exe`;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid(platform)) {
|
||||
return `${filename}.apk`;
|
||||
return androidAppBundle ? `${filename}.aab` : `${filename}.apk`;
|
||||
}
|
||||
|
||||
return filename;
|
||||
|
||||
@@ -30,6 +30,22 @@ describe('BuildParameters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android version code with provided input', async () => {
|
||||
const mockValue = '42';
|
||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidVersionCode: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android version code from version by default', async () => {
|
||||
const mockValue = '';
|
||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidVersionCode: 1003037 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the platform', async () => {
|
||||
const mockValue = 'somePlatform';
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
||||
@@ -87,11 +103,21 @@ describe('BuildParameters', () => {
|
||||
test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: `${targetPlatform}.apk` }),
|
||||
);
|
||||
});
|
||||
|
||||
test.each([Platform.types.Android])('appends aab for %s', async (targetPlatform) => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: `${targetPlatform}.aab` }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the build method', async () => {
|
||||
const mockValue = 'Namespace.ClassName.BuildMethod';
|
||||
jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
||||
@@ -100,6 +126,46 @@ describe('BuildParameters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android keystore name', async () => {
|
||||
const mockValue = 'keystore.keystore';
|
||||
jest.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeystoreName: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android keystore base64-encoded content', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeystoreBase64: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android keystore pass', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeystorePass: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android keyalias name', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeyaliasName: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android keyalias pass', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeyaliasPass: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the custom parameters', async () => {
|
||||
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
|
||||
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import fs from 'fs';
|
||||
import { exec } from '@actions/exec';
|
||||
import ImageTag from './image-tag';
|
||||
|
||||
class Docker {
|
||||
static async build(buildParameters, silent = false) {
|
||||
const { path, dockerfile, baseImage } = buildParameters;
|
||||
const { path, dockerfile, baseImage, uid, gid } = buildParameters;
|
||||
const { version, platform } = baseImage;
|
||||
|
||||
const tag = new ImageTag({ repository: '', name: 'unity-builder', version, platform });
|
||||
const command = `docker build ${path} \
|
||||
--file ${dockerfile} \
|
||||
--build-arg IMAGE=${baseImage} \
|
||||
--build-arg UID=${uid} \
|
||||
--build-arg GID=${gid} \
|
||||
--tag ${tag}`;
|
||||
|
||||
await exec(command, undefined, { silent });
|
||||
@@ -21,6 +24,7 @@ class Docker {
|
||||
const {
|
||||
version,
|
||||
workspace,
|
||||
runnerTempPath,
|
||||
platform,
|
||||
projectPath,
|
||||
buildName,
|
||||
@@ -28,6 +32,12 @@ class Docker {
|
||||
buildFile,
|
||||
buildMethod,
|
||||
buildVersion,
|
||||
androidVersionCode,
|
||||
androidKeystoreName,
|
||||
androidKeystoreBase64,
|
||||
androidKeystorePass,
|
||||
androidKeyaliasName,
|
||||
androidKeyaliasPass,
|
||||
customParameters,
|
||||
} = parameters;
|
||||
|
||||
@@ -47,6 +57,12 @@ class Docker {
|
||||
--env BUILD_FILE="${buildFile}" \
|
||||
--env BUILD_METHOD="${buildMethod}" \
|
||||
--env VERSION="${buildVersion}" \
|
||||
--env ANDROID_VERSION_CODE="${androidVersionCode}" \
|
||||
--env ANDROID_KEYSTORE_NAME="${androidKeystoreName}" \
|
||||
--env ANDROID_KEYSTORE_BASE64="${androidKeystoreBase64}" \
|
||||
--env ANDROID_KEYSTORE_PASS="${androidKeystorePass}" \
|
||||
--env ANDROID_KEYALIAS_NAME="${androidKeyaliasName}" \
|
||||
--env ANDROID_KEYALIAS_PASS="${androidKeyaliasPass}" \
|
||||
--env CUSTOM_PARAMETERS="${customParameters}" \
|
||||
--env HOME=/github/home \
|
||||
--env GITHUB_REF \
|
||||
@@ -65,11 +81,14 @@ class Docker {
|
||||
--env RUNNER_TEMP \
|
||||
--env RUNNER_WORKSPACE \
|
||||
--volume "/var/run/docker.sock":"/var/run/docker.sock" \
|
||||
--volume "/home/runner/work/_temp/_github_home":"/github/home" \
|
||||
--volume "/home/runner/work/_temp/_github_workflow":"/github/workflow" \
|
||||
--volume "${runnerTempPath}/_github_home":"/github/home" \
|
||||
--volume "${runnerTempPath}/_github_workflow":"/github/workflow" \
|
||||
--volume "${workspace}":"/github/workspace" \
|
||||
${image}`;
|
||||
|
||||
fs.mkdirSync(`${runnerTempPath}/_github_home`, { recursive: true });
|
||||
fs.mkdirSync(`${runnerTempPath}/_github_workflow`, { recursive: true });
|
||||
|
||||
await exec(command, undefined, { silent });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Docker from './docker';
|
||||
import ImageTag from './image-tag';
|
||||
|
||||
describe('Docker', () => {
|
||||
it('builds', async () => {
|
||||
it.skip('builds', async () => {
|
||||
const path = Action.actionFolder;
|
||||
const dockerfile = `${path}/Dockerfile`;
|
||||
const baseImage = new ImageTag({
|
||||
@@ -12,24 +12,19 @@ describe('Docker', () => {
|
||||
version: '3',
|
||||
platform: 'Test',
|
||||
});
|
||||
|
||||
const tag = await Docker.build({ path, dockerfile, baseImage }, true);
|
||||
|
||||
expect(tag).toBeInstanceOf(ImageTag);
|
||||
expect(tag.toString()).toStrictEqual('unity-builder:3');
|
||||
}, 240000);
|
||||
|
||||
it.skip('runs', async () => {
|
||||
const image = 'unity-builder:2019.2.11f1-webgl';
|
||||
|
||||
const parameters = {
|
||||
workspace: Action.rootFolder,
|
||||
projectPath: `${Action.rootFolder}/test-project`,
|
||||
buildName: 'someBulidName',
|
||||
buildName: 'someBuildName',
|
||||
buildsPath: 'build',
|
||||
method: '',
|
||||
};
|
||||
|
||||
await Docker.run(image, parameters);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ class ImageTag {
|
||||
name = 'unity3d',
|
||||
version = '2019.2.11f1',
|
||||
platform,
|
||||
customImage,
|
||||
} = imageProperties;
|
||||
|
||||
if (!ImageTag.versionPattern.test(version)) {
|
||||
@@ -24,7 +25,7 @@ class ImageTag {
|
||||
ImageTag.imageSuffixes.generic,
|
||||
);
|
||||
|
||||
Object.assign(this, { repository, name, version, platform, builderPlatform });
|
||||
Object.assign(this, { repository, name, version, platform, builderPlatform, customImage });
|
||||
}
|
||||
|
||||
static get versionPattern() {
|
||||
@@ -82,6 +83,10 @@ class ImageTag {
|
||||
toString() {
|
||||
const { image, tag } = this;
|
||||
|
||||
if (this.customImage && this.customImage !== '') {
|
||||
return this.customImage;
|
||||
}
|
||||
|
||||
return `${image}:${tag}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ describe('UnityImageVersion', () => {
|
||||
|
||||
expect(image.toString()).toStrictEqual(`${defaults.image}:2099.1.1111`);
|
||||
});
|
||||
it('returns customImage if given', () => {
|
||||
const image = new ImageTag({
|
||||
version: '2099.1.1111',
|
||||
platform: some.platform,
|
||||
customImage: `${defaults.image}:2099.1.1111@347598437689743986`,
|
||||
});
|
||||
|
||||
expect(image.toString()).toStrictEqual(image.customImage);
|
||||
});
|
||||
|
||||
it('returns the specific build platform', () => {
|
||||
const image = new ImageTag({ version: '2019.2.11f1', platform: 'WebGL' });
|
||||
|
||||
@@ -4,10 +4,12 @@ import Cache from './cache';
|
||||
import Docker from './docker';
|
||||
import Input from './input';
|
||||
import ImageTag from './image-tag';
|
||||
import Output from './output';
|
||||
import Platform from './platform';
|
||||
import Project from './project';
|
||||
import Unity from './unity';
|
||||
import Versioning from './versioning';
|
||||
import Kubernetes from './kubernetes';
|
||||
|
||||
export {
|
||||
Action,
|
||||
@@ -16,8 +18,10 @@ export {
|
||||
Docker,
|
||||
Input,
|
||||
ImageTag,
|
||||
Output,
|
||||
Platform,
|
||||
Project,
|
||||
Unity,
|
||||
Versioning,
|
||||
Kubernetes,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,10 @@ class Input {
|
||||
return core.getInput('unityVersion');
|
||||
}
|
||||
|
||||
static get customImage() {
|
||||
return core.getInput('customImage');
|
||||
}
|
||||
|
||||
static get targetPlatform() {
|
||||
return core.getInput('targetPlatform') || Platform.default;
|
||||
}
|
||||
@@ -41,15 +45,69 @@ class Input {
|
||||
return core.getInput('version') || '';
|
||||
}
|
||||
|
||||
static get allowDirtyBuild() {
|
||||
const input = core.getInput('allowDirtyBuild') || 'false';
|
||||
static get androidVersionCode() {
|
||||
return core.getInput('androidVersionCode');
|
||||
}
|
||||
|
||||
return input === 'true' ? 'true' : 'false';
|
||||
static get androidAppBundle() {
|
||||
const input = core.getInput('androidAppBundle') || false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get androidKeystoreName() {
|
||||
return core.getInput('androidKeystoreName') || '';
|
||||
}
|
||||
|
||||
static get androidKeystoreBase64() {
|
||||
return core.getInput('androidKeystoreBase64') || '';
|
||||
}
|
||||
|
||||
static get androidKeystorePass() {
|
||||
return core.getInput('androidKeystorePass') || '';
|
||||
}
|
||||
|
||||
static get androidKeyaliasName() {
|
||||
return core.getInput('androidKeyaliasName') || '';
|
||||
}
|
||||
|
||||
static get androidKeyaliasPass() {
|
||||
return core.getInput('androidKeyaliasPass') || '';
|
||||
}
|
||||
|
||||
static get allowDirtyBuild() {
|
||||
const input = core.getInput('allowDirtyBuild') || false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get customParameters() {
|
||||
return core.getInput('customParameters') || '';
|
||||
}
|
||||
|
||||
static get kubeConfig() {
|
||||
return core.getInput('kubeConfig') || '';
|
||||
}
|
||||
|
||||
static get githubToken() {
|
||||
return core.getInput('githubToken') || '';
|
||||
}
|
||||
|
||||
static get kubeContainerMemory() {
|
||||
return core.getInput('kubeContainerMemory') || '800M';
|
||||
}
|
||||
|
||||
static get kubeContainerCPU() {
|
||||
return core.getInput('kubeContainerCPU') || '0.25';
|
||||
}
|
||||
|
||||
static get kubeVolumeSize() {
|
||||
return core.getInput('kubeVolumeSize') || '5Gi';
|
||||
}
|
||||
|
||||
static get kubeVolume() {
|
||||
return core.getInput('kubeVolume') || '';
|
||||
}
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
||||
@@ -20,6 +20,18 @@ describe('Input', () => {
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe('customImage', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.customImage).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = '2020.4.99f9';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.customImage).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('targetPlatform', () => {
|
||||
it('returns the default value', () => {
|
||||
@@ -118,20 +130,116 @@ describe('Input', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowDirtyBuild', () => {
|
||||
describe('androidVersionCode', () => {
|
||||
it('defaults to null', () => {
|
||||
expect(Input.androidVersionCode).toBeFalsy();
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = '42';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.androidVersionCode).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('androidAppBundle', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.allowDirtyBuild).toStrictEqual('false');
|
||||
expect(Input.androidAppBundle).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when string true is passed', () => {
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.allowDirtyBuild).toStrictEqual('true');
|
||||
expect(Input.androidAppBundle).toStrictEqual(true);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns false when string false is passed', () => {
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.allowDirtyBuild).toStrictEqual('false');
|
||||
expect(Input.androidAppBundle).toStrictEqual(false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('androidKeystoreName', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.androidKeystoreName).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = 'keystore.keystore';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.androidKeystoreName).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('androidKeystoreBase64', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.androidKeystoreBase64).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = 'secret';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.androidKeystoreBase64).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('androidKeystorePass', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.androidKeystorePass).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = 'secret';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.androidKeystorePass).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('androidKeyaliasName', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.androidKeyaliasName).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = 'secret';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.androidKeyaliasName).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('androidKeyaliasPass', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.androidKeyaliasPass).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = 'secret';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.androidKeyaliasPass).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowDirtyBuild', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.allowDirtyBuild).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when string true is passed', () => {
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.allowDirtyBuild).toStrictEqual(true);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns false when string false is passed', () => {
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.allowDirtyBuild).toStrictEqual(false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
355
src/model/kubernetes.js
Normal file
355
src/model/kubernetes.js
Normal file
@@ -0,0 +1,355 @@
|
||||
import { Client, KubeConfig } from 'kubernetes-client';
|
||||
import Request from 'kubernetes-client/backends/request';
|
||||
|
||||
const core = require('@actions/core');
|
||||
const base64 = require('base-64');
|
||||
|
||||
const pollInterval = 10000;
|
||||
|
||||
class Kubernetes {
|
||||
static async runBuildJob(buildParameters, baseImage) {
|
||||
const kubeconfig = new KubeConfig();
|
||||
kubeconfig.loadFromString(base64.decode(buildParameters.kubeConfig));
|
||||
const backend = new Request({ kubeconfig });
|
||||
const kubeClient = new Client(backend);
|
||||
await kubeClient.loadSpec();
|
||||
|
||||
const buildId = Kubernetes.uuidv4();
|
||||
const pvcName = `unity-builder-pvc-${buildId}`;
|
||||
const secretName = `build-credentials-${buildId}`;
|
||||
const jobName = `unity-builder-job-${buildId}`;
|
||||
const namespace = 'default';
|
||||
|
||||
Object.assign(this, {
|
||||
kubeClient,
|
||||
buildId,
|
||||
buildParameters,
|
||||
baseImage,
|
||||
pvcName,
|
||||
secretName,
|
||||
jobName,
|
||||
namespace,
|
||||
});
|
||||
|
||||
await Kubernetes.createSecret();
|
||||
await Kubernetes.createPersistentVolumeClaim();
|
||||
await Kubernetes.scheduleBuildJob();
|
||||
await Kubernetes.watchBuildJobUntilFinished();
|
||||
await Kubernetes.cleanup();
|
||||
|
||||
core.setOutput('volume', pvcName);
|
||||
}
|
||||
|
||||
static async createSecret() {
|
||||
const secretManifest = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name: this.secretName,
|
||||
},
|
||||
type: 'Opaque',
|
||||
data: {
|
||||
GITHUB_TOKEN: base64.encode(this.buildParameters.githubToken),
|
||||
UNITY_LICENSE: base64.encode(process.env.UNITY_LICENSE),
|
||||
ANDROID_KEYSTORE_BASE64: base64.encode(this.buildParameters.androidKeystoreBase64),
|
||||
ANDROID_KEYSTORE_PASS: base64.encode(this.buildParameters.androidKeystorePass),
|
||||
ANDROID_KEYALIAS_PASS: base64.encode(this.buildParameters.androidKeyaliasPass),
|
||||
},
|
||||
};
|
||||
await this.kubeClient.api.v1.namespaces(this.namespace).secrets.post({ body: secretManifest });
|
||||
}
|
||||
|
||||
static async createPersistentVolumeClaim() {
|
||||
if (this.buildParameters.kubeVolume) {
|
||||
core.info(this.buildParameters.kubeVolume);
|
||||
this.pvcName = this.buildParameters.kubeVolume;
|
||||
return;
|
||||
}
|
||||
const pvcManifest = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'PersistentVolumeClaim',
|
||||
metadata: {
|
||||
name: this.pvcName,
|
||||
},
|
||||
spec: {
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
volumeMode: 'Filesystem',
|
||||
resources: {
|
||||
requests: {
|
||||
storage: this.buildParameters.kubeVolumeSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await this.kubeClient.api.v1
|
||||
.namespaces(this.namespace)
|
||||
.persistentvolumeclaims.post({ body: pvcManifest });
|
||||
core.info('Persistent Volume created, waiting for ready state...');
|
||||
await Kubernetes.watchPersistentVolumeClaimUntilReady();
|
||||
core.info('Persistent Volume ready for claims');
|
||||
}
|
||||
|
||||
static async watchPersistentVolumeClaimUntilReady() {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
const queryResult = await this.kubeClient.api.v1
|
||||
.namespaces(this.namespace)
|
||||
.persistentvolumeclaims(this.pvcName)
|
||||
.get();
|
||||
if (queryResult.body.status.phase === 'Pending') {
|
||||
await Kubernetes.watchPersistentVolumeClaimUntilReady();
|
||||
}
|
||||
}
|
||||
|
||||
static async scheduleBuildJob() {
|
||||
core.info('Creating build job');
|
||||
const jobManifest = {
|
||||
apiVersion: 'batch/v1',
|
||||
kind: 'Job',
|
||||
metadata: {
|
||||
name: this.jobName,
|
||||
labels: {
|
||||
app: 'unity-builder',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
template: {
|
||||
backoffLimit: 1,
|
||||
spec: {
|
||||
volumes: [
|
||||
{
|
||||
name: 'data',
|
||||
persistentVolumeClaim: {
|
||||
claimName: this.pvcName,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
secret: {
|
||||
secretName: this.secretName,
|
||||
},
|
||||
},
|
||||
],
|
||||
initContainers: [
|
||||
{
|
||||
name: 'clone',
|
||||
image: 'alpine/git',
|
||||
command: [
|
||||
'/bin/sh',
|
||||
'-c',
|
||||
`apk update;
|
||||
apk add git-lfs;
|
||||
export GITHUB_TOKEN=$(cat /credentials/GITHUB_TOKEN);
|
||||
cd /data;
|
||||
git clone https://github.com/${process.env.GITHUB_REPOSITORY}.git repo;
|
||||
git clone https://github.com/webbertakken/unity-builder.git builder;
|
||||
cd repo;
|
||||
git checkout $GITHUB_SHA;
|
||||
ls`,
|
||||
],
|
||||
volumeMounts: [
|
||||
{
|
||||
name: 'data',
|
||||
mountPath: '/data',
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
mountPath: '/credentials',
|
||||
readOnly: true,
|
||||
},
|
||||
],
|
||||
env: [
|
||||
{
|
||||
name: 'GITHUB_SHA',
|
||||
value: this.buildId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
containers: [
|
||||
{
|
||||
name: 'main',
|
||||
image: `${this.baseImage.toString()}`,
|
||||
command: [
|
||||
'bin/bash',
|
||||
'-c',
|
||||
`for f in ./credentials/*; do export $(basename $f)="$(cat $f)"; done
|
||||
cp -r /data/builder/action/default-build-script /UnityBuilderAction
|
||||
cp -r /data/builder/action/entrypoint.sh /entrypoint.sh
|
||||
cp -r /data/builder/action/steps /steps
|
||||
chmod -R +x /entrypoint.sh;
|
||||
chmod -R +x /steps;
|
||||
/entrypoint.sh;
|
||||
`,
|
||||
],
|
||||
resources: {
|
||||
requests: {
|
||||
memory: this.buildParameters.kubeContainerMemory,
|
||||
cpu: this.buildParameters.kubeContainerCPU,
|
||||
},
|
||||
},
|
||||
env: [
|
||||
{
|
||||
name: 'GITHUB_WORKSPACE',
|
||||
value: '/data/repo',
|
||||
},
|
||||
{
|
||||
name: 'PROJECT_PATH',
|
||||
value: this.buildParameters.projectPath,
|
||||
},
|
||||
{
|
||||
name: 'BUILD_PATH',
|
||||
value: this.buildParameters.buildPath,
|
||||
},
|
||||
{
|
||||
name: 'BUILD_FILE',
|
||||
value: this.buildParameters.buildFile,
|
||||
},
|
||||
{
|
||||
name: 'BUILD_NAME',
|
||||
value: this.buildParameters.buildName,
|
||||
},
|
||||
{
|
||||
name: 'BUILD_METHOD',
|
||||
value: this.buildParameters.buildMethod,
|
||||
},
|
||||
{
|
||||
name: 'CUSTOM_PARAMETERS',
|
||||
value: this.buildParameters.customParameters,
|
||||
},
|
||||
{
|
||||
name: 'BUILD_TARGET',
|
||||
value: this.buildParameters.platform,
|
||||
},
|
||||
{
|
||||
name: 'ANDROID_VERSION_CODE',
|
||||
value: this.buildParameters.androidVersionCode.toString(),
|
||||
},
|
||||
{
|
||||
name: 'ANDROID_KEYSTORE_NAME',
|
||||
value: this.buildParameters.androidKeystoreName,
|
||||
},
|
||||
{
|
||||
name: 'ANDROID_KEYALIAS_NAME',
|
||||
value: this.buildParameters.androidKeyaliasName,
|
||||
},
|
||||
],
|
||||
volumeMounts: [
|
||||
{
|
||||
name: 'data',
|
||||
mountPath: '/data',
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
mountPath: '/credentials',
|
||||
readOnly: true,
|
||||
},
|
||||
],
|
||||
lifeCycle: {
|
||||
preStop: {
|
||||
exec: {
|
||||
command: [
|
||||
'bin/bash',
|
||||
'-c',
|
||||
`cd /data/builder/action/steps;
|
||||
chmod +x /return_license.sh;
|
||||
/return_license.sh;`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
restartPolicy: 'Never',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await this.kubeClient.apis.batch.v1.namespaces(this.namespace).jobs.post({ body: jobManifest });
|
||||
core.info('Job created');
|
||||
}
|
||||
|
||||
static async watchBuildJobUntilFinished() {
|
||||
let podname;
|
||||
let ready = false;
|
||||
while (!ready) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pods = await this.kubeClient.api.v1.namespaces(this.namespace).pods.get();
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let index = 0; index < pods.body.items.length; index++) {
|
||||
const element = pods.body.items[index];
|
||||
if (element.metadata.labels['job-name'] === this.jobName) {
|
||||
if (element.status.phase !== 'Pending') {
|
||||
core.info('Pod no longer pending');
|
||||
if (element.status.phase === 'Failure') {
|
||||
core.error('Kubernetes job failed');
|
||||
} else {
|
||||
ready = true;
|
||||
podname = element.metadata.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`Watching build job ${podname}`);
|
||||
let logQueryTime;
|
||||
let complete = false;
|
||||
while (!complete) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const podStatus = await this.kubeClient.api.v1.namespaces(this.namespace).pod(podname).get();
|
||||
if (podStatus.body.status.phase !== 'Running') {
|
||||
complete = true;
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const logs = await this.kubeClient.api.v1
|
||||
.namespaces(this.namespace)
|
||||
.pod(podname)
|
||||
.log.get({
|
||||
qs: {
|
||||
sinceTime: logQueryTime,
|
||||
timestamps: true,
|
||||
},
|
||||
});
|
||||
if (logs.body !== undefined) {
|
||||
const arrayOfLines = logs.body.match(/[^\n\r]+/g).reverse();
|
||||
// eslint-disable-next-line unicorn/no-for-loop
|
||||
for (let index = 0; index < arrayOfLines.length; index += 1) {
|
||||
const element = arrayOfLines[index];
|
||||
const [time, ...line] = element.split(' ');
|
||||
if (time !== logQueryTime) {
|
||||
core.info(line.join(' '));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (podStatus.body.status.phase === 'Failed') {
|
||||
throw new Error('Kubernetes job failed');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
logQueryTime = arrayOfLines[0].split(' ')[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async cleanup() {
|
||||
await this.kubeClient.apis.batch.v1.namespaces(this.namespace).jobs(this.jobName).delete();
|
||||
await this.kubeClient.api.v1.namespaces(this.namespace).secrets(this.secretName).delete();
|
||||
}
|
||||
|
||||
static uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const r = (Math.random() * 16) | 0;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
export default Kubernetes;
|
||||
9
src/model/output.js
Normal file
9
src/model/output.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const core = require('@actions/core');
|
||||
|
||||
class Output {
|
||||
static async setBuildVersion(buildVersion) {
|
||||
await core.setOutput('buildVersion', buildVersion);
|
||||
}
|
||||
}
|
||||
|
||||
export default Output;
|
||||
9
src/model/output.test.js
Normal file
9
src/model/output.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Output from './output';
|
||||
|
||||
describe('Output', () => {
|
||||
describe('setBuildVersion', () => {
|
||||
it('does not throw', async () => {
|
||||
await expect(Output.setBuildVersion('1.0.0')).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ export default class Versioning {
|
||||
}
|
||||
|
||||
static get isDirtyAllowed() {
|
||||
return Input.allowDirtyBuild === 'true';
|
||||
return Input.allowDirtyBuild;
|
||||
}
|
||||
|
||||
static get strategies() {
|
||||
@@ -39,6 +39,31 @@ export default class Versioning {
|
||||
return process.env.GITHUB_REF;
|
||||
}
|
||||
|
||||
/**
|
||||
* The commit SHA that triggered the workflow run.
|
||||
*/
|
||||
static get sha() {
|
||||
return process.env.GITHUB_SHA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of lines to print when logging the git diff
|
||||
*/
|
||||
static get maxDiffLines() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log up to maxDiffLines of the git diff.
|
||||
*/
|
||||
static async logDiff() {
|
||||
const diffCommand = `git --no-pager diff | head -n ${this.maxDiffLines.toString()}`;
|
||||
await System.run('sh', undefined, {
|
||||
input: Buffer.from(diffCommand),
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex to parse version description into separate fields
|
||||
*/
|
||||
@@ -88,6 +113,8 @@ export default class Versioning {
|
||||
static async generateSemanticVersion() {
|
||||
await this.fetch();
|
||||
|
||||
await this.logDiff();
|
||||
|
||||
if ((await this.isDirty()) && !this.isDirtyAllowed) {
|
||||
throw new Error('Branch is dirty. Refusing to base semantic version on uncommitted changes');
|
||||
}
|
||||
@@ -162,8 +189,7 @@ export default class Versioning {
|
||||
* identifies the current commit.
|
||||
*/
|
||||
static async getVersionDescription() {
|
||||
const commitIsh = (await this.getTag()) ? 'HEAD' : `origin/${this.branch}`;
|
||||
return this.git(['describe', '--long', '--tags', '--always', '--debug', commitIsh]);
|
||||
return this.git(['describe', '--long', '--tags', '--always', '--debug', this.sha]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,11 +228,7 @@ export default class Versioning {
|
||||
* Note: HEAD should not be used, as it may be detached, resulting in an additional count.
|
||||
*/
|
||||
static async getTotalNumberOfCommits() {
|
||||
const numberOfCommitsAsString = await this.git([
|
||||
'rev-list',
|
||||
'--count',
|
||||
`origin/${this.branch}`,
|
||||
]);
|
||||
const numberOfCommitsAsString = await this.git(['rev-list', '--count', this.sha]);
|
||||
|
||||
return Number.parseInt(numberOfCommitsAsString, 10);
|
||||
}
|
||||
|
||||
@@ -100,6 +100,28 @@ describe('Versioning', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging git diff', () => {
|
||||
it('calls git diff', async () => {
|
||||
// allowDirtyBuild: true
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
jest.spyOn(Versioning, 'fetch').mockResolvedValue(undefined);
|
||||
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
|
||||
jest
|
||||
.spyOn(Versioning, 'parseSemanticVersion')
|
||||
.mockResolvedValue({ tag: 'mocktag', commits: 'abcdef', hash: '75822BCAF' });
|
||||
const logDiffSpy = jest.spyOn(Versioning, 'logDiff');
|
||||
const gitSpy = jest.spyOn(System, 'run').mockResolvedValue({});
|
||||
|
||||
await Versioning.generateSemanticVersion();
|
||||
|
||||
expect(logDiffSpy).toHaveBeenCalledTimes(1);
|
||||
expect(gitSpy).toHaveBeenCalledTimes(1);
|
||||
const issuedCommand = System.run.mock.calls[0][2].input.toString();
|
||||
expect(issuedCommand.indexOf('diff')).toBeGreaterThan(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('descriptionRegex', () => {
|
||||
it('is a valid regex', () => {
|
||||
expect(Versioning.descriptionRegex).toBeInstanceOf(RegExp);
|
||||
|
||||
Reference in New Issue
Block a user