Compare commits

...

108 Commits

Author SHA1 Message Date
marekvalenta
c8202c01e7 Pass buildOptions to the build with customParameters (#190)
* Update Builder.cs

* update of buildOptions

buildOptions should be taken into account for build

* missed name
2021-01-05 19:19:16 +01:00
David Finol
9f510401e2 Update CONTRIBUTING.md 2021-01-04 16:08:15 -06:00
David Finol
32acb22fec Default unityVersion to auto to use ProjectSettings/ProjectVersion.txt (#162) (#188) 2020-12-28 23:36:31 -06:00
Ivan Murashka
602b0b45d7 decode keystore to project folder (#186) 2020-12-11 18:55:53 +01:00
Webber Takken
9c804d98a0 Fix typo 2020-12-06 18:13:32 +01:00
Webber Takken
042fb6ae32 remove anything recognizable as Unity (#184) 2020-12-06 17:57:57 +01:00
David Finol
df7d1e1e25 Update index.js with linux-il2cpp fix (#181)
* Update index.js

* Update index.js with linux-il2cpp fix
2020-11-19 11:05:49 +01:00
David Finol
9707ad7ad5 Convert version major and minor to numbers (#180)
* Convert version major and minor to numbers

* fix linting error

Co-authored-by: Webber Takken <webber@takken.io>
2020-11-18 00:51:10 +01:00
Webber Takken
8eeb848483 add il2cpp support for linux from 2019.3 (#177) 2020-11-14 00:57:44 +01:00
Webber Takken
9e2a1b2d35 Remove duplicate flag batch-mode (#173) 2020-11-09 20:04:56 +01:00
Webber
471bf0bdf0 Merge branch 'BLaZeKiLL-fix/unity-alias' into main 2020-11-01 15:08:06 +01:00
Webber
6c2fbff839 Merge branch 'fix/unity-alias' of https://github.com/BLaZeKiLL/unity-builder into BLaZeKiLL-fix/unity-alias 2020-11-01 15:06:09 +01:00
Webber Takken
17c28995b2 Update action to use main (#168) 2020-10-29 23:21:48 +01:00
BLaZeKiLL
72ab50499d linux platform added 2020-10-24 13:09:01 +05:30
BLaZeKiLL
9269fba307 image tags updated 2020-10-24 12:13:29 +05:30
BLaZeKiLL
dc54ae10a5 Merge branch 'main' of https://github.com/webbertakken/unity-builder into fix/unity-alias 2020-10-22 23:51:43 +05:30
BLaZeKiLL
5417369dba action build 2020-10-22 23:44:36 +05:30
Devashish Lal
21c985b8e9 added -nographics flag to build.sh (#166) 2020-10-22 16:05:41 +02:00
BLaZeKiLL
483b784253 default docker repository updated to new docker repository 2020-10-22 18:24:42 +05:30
Kai Biermeier
e6d3daedbe revert Fix file ownership issues for self-hosted runners. (#141) (#165) 2020-10-22 10:20:12 +02:00
BLaZeKiLL
046ab3a72d -no-graphics flag removed from build script 2020-10-22 12:27:27 +05:30
BLaZeKiLL
ec0baa5d7b removed redundant xvfb calls (part of the alias) 2020-10-20 17:59:57 +05:30
BLaZeKiLL
5b8f3cf342 refactored to unity-editor alias 2020-10-20 17:29:34 +05:30
Webber Takken
ef74241772 Fix recursive path (#160) 2020-10-12 19:37:26 +02:00
dependabot[bot]
3d7e4a8018 Bump @actions/core from 1.2.4 to 1.2.6 (#158)
Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.2.4 to 1.2.6.
- [Release notes](https://github.com/actions/toolkit/releases)
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-01 21:30:09 +02:00
Webber Takken
91da660786 remove artifacts older than 21 days (#157) 2020-09-28 19:19:18 +02:00
Webber Takken
f42f7923ed Add intructions for debugging (#154) 2020-09-27 21:44:14 +02:00
Webber Takken
22bc9a9bad Allow k8s workflow failures (#152) 2020-09-19 02:41:34 +02:00
Kai Biermeier
6a53a9e853 Solution proposal to Issue Add customImage parameter #150 (#151)
* add customImage attribute

* add one more test for input passing && check for customImage == ''
2020-09-18 18:41:31 +02:00
dependabot-preview[bot]
cccd5074ea [Security] Bump node-fetch from 2.6.0 to 2.6.1
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1. **This update includes a security fix.**
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-12 05:30:21 +00:00
Forrest Jones
977683cd5f Add buildVersion as action output (#144) (#145) 2020-08-27 02:24:33 +02:00
Webber Takken
89bdaa5e46 Add issue templates and pull request template (#142) 2020-08-22 22:03:44 +02:00
xanantis
24e9c186fd Fix file ownership issues for self-hosted runners. (#141) 2020-08-22 21:28:57 +02:00
Frostebite
92cfb31622 k8s retry delete during cleanup 2020-08-22 19:03:22 +01:00
Webber Takken
67b76584b8 Cleanup root dir (#133)
* Remove package-lock.json

* Move bootstrap logic into action folder

* Fix relative path

* update index after rebase

* Shell files are executable

* Update yarn.lock

* Update yarn.lock
2020-08-22 19:17:02 +02:00
xanantis
3ee15170fd Use RUNNER_TEMP environment variable instead of a hardcoded path (#138) 2020-08-22 17:59:08 +02:00
Justin Spahr-Summers
3e0842dda0 Rename custom -version flag to -buildVersion (#137)
* Rename custom -version flag to -customProjectVersion

* Rename flag to -buildVersion
2020-08-16 21:09:34 +02:00
Benoit Dion
1f8d196ed0 Fix boolean logic (#129) 2020-08-10 16:30:06 +02:00
dependabot[bot]
e003f9e2ca Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-09 21:46:16 +02:00
Frostebite
21634107c1 K8s Feature (#124)
Adds the ability to use a kubernetes container to run builds that are too large for the local machine running the unity-builder. Logs are streamed back during the build. Build results can then be downloaded separately.
2020-08-09 20:27:47 +01:00
Webber
196fe8fc5b Move docs to unity-ci.com 2020-07-11 15:53:20 +02:00
dogboydog
ec0cde0c85 avoid double logging of git diff 2020-07-09 13:31:51 +02:00
dogboydog
72ff2983a1 test with short max diff lines 2020-07-09 13:31:51 +02:00
dogboydog
fdf71758a9 use System.run for logging Git diff 2020-07-09 13:31:51 +02:00
dogboydog
91ec427695 make logging of git diff unconditional, remove parameter 2020-07-09 13:31:51 +02:00
dogboydog
6fb8550919 increase code coverage with versioning.test.js 2020-07-09 13:31:51 +02:00
dogboydog
cb913cd286 Initial implementation of logDiffIfDirty 2020-07-09 13:31:51 +02:00
Webber
96eeaf940a Remove androidVersionCode from default workflow 2020-07-08 20:07:03 +02:00
David Finol
6ece6447b2 Add Android Build Settings 2020-07-06 11:53:40 +02:00
Benoit Dion
3523c6a934 Document additional action inputs
This helps avoid warnings when using previously undocumented inputs
2020-06-25 18:14:32 +02:00
Webber
bf702784d2 cleanup workflow 2020-06-25 00:57:16 +02:00
Benoit Dion
bdc3a88d22 Add input to set version code
Use action input `androidVersionCode` when provided. Generate the androidVersionCode from the version otherwise.
2020-06-25 00:33:25 +02:00
Benoit Dion
401ddcaae0 Add support for android builds
The emoji in the github action name exposed as a env variable through docker was causing a gradle crash 😢.
2020-06-20 10:08:08 +02:00
Benoit Dion
1245bfefc8 Fix fork PR builds
Remove hardcoded reference to the `origin` remote and instead implictly use the current commit or ref
2020-06-20 00:59:05 +02:00
Nathan Leiby
229b0d02f8 Adding logging before updating permissions 2020-06-13 22:48:49 +02:00
Nathan Leiby
c68bdf8177 make StandaloneOSX app runnable 2020-06-13 22:48:49 +02:00
Webber
938926799f Remove WebGL for faster CI 2020-06-13 17:06:03 +02:00
Webber
5efb4868ad Comment out activation 2020-06-13 17:06:03 +02:00
Webber
7749b8862d Add matrix support for multi license 2020-06-13 17:06:03 +02:00
Webber
98a56c4169 Add 2019.3.15 workflow 2020-06-13 17:06:03 +02:00
Webber
720ee0c896 Acquire activation files 2020-06-13 17:06:03 +02:00
Webber
d42c251af3 Snyk badge really almost never works, bye 2020-05-23 02:22:55 +02:00
Webber
bfe6be7ce2 Update readme for v1.0 🎉 2020-05-23 00:25:26 +02:00
Webber
f15f40d265 Use head for tags 2020-05-22 23:01:58 +02:00
Webber
866f364f64 Use ref instead of tag vs branch 2020-05-22 23:01:58 +02:00
Webber
a245f08e75 rename to throwContextualError 2020-05-22 23:01:58 +02:00
Webber
21c211bbdd rebase on master 2020-05-22 23:01:58 +02:00
Webber
3718e05961 Describe errors in System.run 2020-05-22 23:01:58 +02:00
Webber
0159028bb1 Fix missing await 2020-05-22 23:01:58 +02:00
Webber
054c6bfab3 Catch command for in-shell errors 2020-05-22 23:01:58 +02:00
Webber
8c9ff3249e More info if command gives no output, just the exit code. 2020-05-22 23:01:58 +02:00
Webber
7386c669ad Fix no output from errors 2020-05-22 23:01:58 +02:00
Webber
ce865270c4 Use commit-ish for git description 2020-05-22 23:01:58 +02:00
Webber
7e17091251 Split responsibilities between Input and BuildParameters models 2020-05-22 00:55:26 +02:00
Webber
02ff5bbef2 Add documentation and tests for allowDirtyBuild 2020-05-22 00:55:26 +02:00
Webber
8c177b1bad Add flag for allowing dirty branch 2020-05-22 00:55:26 +02:00
Webber
699621ed21 Run versioning commands in projectPath instead 2020-05-21 14:26:37 +02:00
Webber
44bde7feb9 Base number of commits off of the branch on origin 2020-05-02 16:37:24 +02:00
Webber
5328bda08e Base number of commits off of the branch 2020-05-02 16:37:24 +02:00
Webber
34e4b86924 Fix case where no tags does not trigger false 2020-05-01 20:32:41 +02:00
Webber
e9514b4e37 Stop hammering patches 🔨 2020-05-01 17:55:27 +02:00
Webber
d4d91e75c3 Configure codecov threshold 2020-05-01 17:55:27 +02:00
Webber
2085eff264 Add missing test for ref getters 2020-05-01 17:55:27 +02:00
Webber
98a1b078fc Update styles to latest unicorn 🦄 and prettier 🦋 2020-05-01 17:55:27 +02:00
Webber
afef854ea0 update packages to latest 2020-05-01 17:55:27 +02:00
Webber
a0a5de2a83 Cover all versioning paths 2020-05-01 16:15:56 +02:00
Webber
c146049b33 Mock all output methods from core 2020-05-01 16:15:56 +02:00
Webber
e46399169f Add tests for System model 2020-05-01 16:15:56 +02:00
Webber
b41026b36e Introduce smart fetching, based on type of local repo. 2020-05-01 16:15:56 +02:00
Webber
cd1d215dfa Remove unshallow fetch 2020-05-01 16:15:56 +02:00
Webber
d01e844eea Fix versioning for push event. 2020-05-01 16:15:56 +02:00
Webber
40564afbaf Fix failed test 2020-04-28 02:02:34 +02:00
Webber
b6f8040f4a Add tests for the versioning model 2020-04-28 02:02:34 +02:00
Webber
22c7d0e516 Improve versioning readme 📜 2020-04-26 21:45:10 +02:00
Webber
f85e50e499 Fix bug in hasAnyVersionTags 🐜 2020-04-26 21:45:10 +02:00
Webber
d75d7890d0 Implement versioning strategies in js 🧉 2020-04-26 21:45:10 +02:00
Webber
2e81e61af3 Add additional tests 🧪 2020-04-26 21:45:10 +02:00
Webber
ac76e9d562 Update readme with versioning examples 🧉 2020-04-26 21:45:10 +02:00
Webber
39a160b789 Allow versioning and version parameters. 2020-04-26 21:45:10 +02:00
Webber
e8a2eaad72 Cleanup default-build-script 2020-04-26 21:45:10 +02:00
Webber
6f1d03d8a8 Refactor Builder 2020-04-26 21:45:10 +02:00
Webber
5ee9c59113 Create VersionApplicator 2020-04-26 21:45:10 +02:00
Webber
03510d4a55 Abstract out the stdout reporter 2020-04-26 21:45:10 +02:00
Webber
ad65b0ece4 Abstract out the argument parser 2020-04-26 21:45:10 +02:00
Webber
a513e5b640 Add semantic versioning logic 2020-04-26 21:45:10 +02:00
Webber
328b0d8ac0 Add system extension for Process 2020-04-26 21:45:10 +02:00
Webber
e53dcf13e3 Add .editorconfig for the default-build-script project 2020-04-26 21:45:10 +02:00
dependabot-preview[bot]
3338b392a0 [Security] Bump acorn from 6.4.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1. **This update includes a security fix.**
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-13 22:32:17 +00:00
83 changed files with 4576 additions and 2499 deletions

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.-->

View 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
View File

@@ -0,0 +1,7 @@
---
name: Other
about: Everything else
title: ''
labels: ''
assignees: ''
---

11
.github/pull_request_template.md vendored Normal file
View 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)

13
.github/workflows/cleanup.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Delete old artifacts
on:
schedule:
- cron: '30 10 * * *' # every day at 10:30
jobs:
delete-artifacts:
runs-on: ubuntu-latest
steps:
- uses: kolpav/purge-artifacts-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
expire-in: 21 days

View File

@@ -1,12 +1,15 @@
name: Actions 😎
name: Actions
on:
pull_request: {}
push: { branches: [master] }
push: { branches: [main] }
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() }}

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea
node_modules
coverage/

View File

@@ -19,7 +19,7 @@ When fixing a bug it is fine to submit a pull request right away.
Steps to be performed to submit a pull request:
1. Fork the repository and create your branch from `master`.
1. Fork the repository and create your branch from `main`.
2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Fill out the description, link any related issues and submit your pull request.

289
README.md
View File

@@ -1,284 +1,45 @@
# Unity - Builder
[![Actions status](https://github.com/webbertakken/unity-builder/workflows/Actions%20%F0%9F%98%8E/badge.svg?event=push&branch=master)](https://github.com/webbertakken/unity-builder/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22Actions+%F0%9F%98%8E%22)
[![snyk - known vulnerabilities](https://snyk.io/test/github/webbertakken/unity-builder/badge.svg)](https://snyk.io/test/github/webbertakken/unity-builder)
[![lgtm - code quality](https://img.shields.io/lgtm/grade/javascript/g/webbertakken/unity-builder.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/webbertakken/unity-builder/context:javascript)
[![codecov - test coverage](https://codecov.io/gh/webbertakken/unity-builder/branch/master/graph/badge.svg)](https://codecov.io/gh/webbertakken/unity-builder)
---
(Not affiliated with Unity Technologies)
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://game.ci">GameCI</a> open source project.
<br />
<br />
---
[![Actions status](https://github.com/webbertakken/unity-builder/workflows/Actions/badge.svg?event=push&branch=master)](https://github.com/webbertakken/unity-builder/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22Actions)
[![lgtm - code quality](https://img.shields.io/lgtm/grade/javascript/g/webbertakken/unity-builder.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/webbertakken/unity-builder/context:javascript)
[![codecov - test coverage](https://codecov.io/gh/webbertakken/unity-builder/branch/master/graph/badge.svg)](https://codecov.io/gh/webbertakken/unity-builder)
<br />
<br />
[Github Action](https://github.com/features/actions)
to build Unity projects for different platforms.
## How to use
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.
Find the
[docs](https://game.ci/docs)
on the GameCI
[website](https://game.ci/).
## Documentation
## Related actions
See the
[Unity Actions](https://github.com/webbertakken/unity-actions)
collection repository for workflow documentation and reference implementation.
Visit the
GameCI <a href="https://github.com/game-ci/unity-actions">Unity Actions</a>
status repository for related Actions.
## Usage
## Community
#### Setup builder
Feel free to join us on
<a href="http://game.ci/discord"><img height="30" src="media/Discord-Logo.svg" alt="Discord" /></a>
and engage with the community.
By default the enabled scenes from the project's settings will be built.
## Contributing
Create or edit the file called `.github/workflows/main.yml` and add a job to it.
To help improve the documentation, please find the docs [repository](https://github.com/game-ci/documentation).
##### Personal License
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@v0.11
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@v0.11
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@v0.11
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@master
with:
buildMethod: EditorNamespace.BuilderClassName.StaticBulidMethod
```
_**required:** `false`_
_**default:** Built-in script that will run a build out of the box._
#### 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@master
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

View File

@@ -3,9 +3,13 @@ author: Webber Takken <webber@takken.io>
description: 'Build Unity projects for different platforms.'
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'
customImage:
required: false
default: ''
description: 'Version of unity to use for building the project.'
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'

View 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

View 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

View File

@@ -0,0 +1,4 @@
root = true
[*.cs]
resharper_check_namespace_highlighting = do_not_show

View File

@@ -57,6 +57,13 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Assets\Editor\Builder.cs" />
<Compile Include="Assets\Editor\Input\ArgumentsParser.cs" />
<Compile Include="Assets\Editor\Reporting\StdOutReporter.cs" />
<Compile Include="Assets\Editor\System\ProcessExtensions.cs" />
<Compile Include="Assets\Editor\Versioning\VersionApplicator.cs" />
<Compile Include="Assets\Editor\Versioning\Git.cs" />
<Compile Include="Assets\Editor\Versioning\VersionGenerator.cs" />
<Compile Include="Assets\Editor\Versioning\GitException.cs" />
<Reference Include="UnityEditor.TestRunner">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEditor.TestRunner.dll</HintPath>
</Reference>
@@ -688,6 +695,9 @@
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/unityscript/Boo.Lang.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include=".editorconfig" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -1,147 +1,59 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityBuilderAction.Input;
using UnityBuilderAction.Reporting;
using UnityBuilderAction.Versioning;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
namespace UnityBuilderAction
{
static class Builder
{
private static string EOL = Environment.NewLine;
private static void ParseCommandLineArguments(out Dictionary<string, string> providedArguments)
{
providedArguments = new Dictionary<string, string>();
string[] args = Environment.GetCommandLineArgs();
Console.WriteLine(
$"{EOL}" +
$"###########################{EOL}" +
$"# Parsing settings #{EOL}" +
$"###########################{EOL}" +
$"{EOL}"
);
// Extract flags with optional values
for (int current = 0, next = 1; current < args.Length; current++, next++) {
// Parse flag
bool isFlag = args[current].StartsWith("-");
if (!isFlag) continue;
string flag = args[current].TrimStart('-');
// Parse optional value
bool flagHasValue = next < args.Length && !args[next].StartsWith("-");
string value = flagHasValue ? args[next].TrimStart('-') : "";
// Assign
Console.WriteLine($"Found flag \"{flag}\" with value \"{value}\".");
providedArguments.Add(flag, value);
}
}
private static Dictionary<string, string> GetValidatedOptions()
{
ParseCommandLineArguments(out var validatedOptions);
if (!validatedOptions.TryGetValue("projectPath", out var projectPath)) {
Console.WriteLine("Missing argument -projectPath");
EditorApplication.Exit(110);
}
if (!validatedOptions.TryGetValue("buildTarget", out var buildTarget)) {
Console.WriteLine("Missing argument -buildTarget");
EditorApplication.Exit(120);
}
if (!Enum.IsDefined(typeof(BuildTarget), buildTarget)) {
EditorApplication.Exit(121);
}
if (!validatedOptions.TryGetValue("customBuildPath", out var customBuildPath)) {
Console.WriteLine("Missing argument -customBuildPath");
EditorApplication.Exit(130);
}
string defaultCustomBuildName = "TestBuild";
if (!validatedOptions.TryGetValue("customBuildName", out var customBuildName)) {
Console.WriteLine($"Missing argument -customBuildName, defaulting to {defaultCustomBuildName}.");
validatedOptions.Add("customBuildName", defaultCustomBuildName);
}
else if (customBuildName == "") {
Console.WriteLine($"Invalid argument -customBuildName, defaulting to {defaultCustomBuildName}.");
validatedOptions.Add("customBuildName", defaultCustomBuildName);
}
return validatedOptions;
}
public static void BuildProject()
{
// Gather values from args
var options = GetValidatedOptions();
var options = ArgumentsParser.GetValidatedOptions();
// Gather values from project
var scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(s => s.path).ToArray();
// Get all buildOptions from options
BuildOptions buildOptions = BuildOptions.None;
foreach (string buildOptionString in Enum.GetNames(typeof(BuildOptions))) {
if (options.ContainsKey(buildOptionString)) {
BuildOptions buildOptionEnum = (BuildOptions) Enum.Parse(typeof(BuildOptions), buildOptionString);
buildOptions |= buildOptionEnum;
}
}
// Define BuildPlayer Options
var buildOptions = new BuildPlayerOptions {
var buildPlayerOptions = new BuildPlayerOptions {
scenes = scenes,
locationPathName = options["customBuildPath"],
target = (BuildTarget) Enum.Parse(typeof(BuildTarget), options["buildTarget"]),
options = buildOptions
};
// Set version for this build
VersionApplicator.SetVersion(options["buildVersion"]);
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
// Apply Android settings
if (buildPlayerOptions.target == BuildTarget.Android)
AndroidSettings.Apply(options);
// Perform build
BuildReport buildReport = BuildPipeline.BuildPlayer(buildOptions);
BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions);
// Summary
BuildSummary summary = buildReport.summary;
ReportSummary(summary);
StdOutReporter.ReportSummary(summary);
// Result
BuildResult result = summary.result;
ExitWithResult(result);
}
private static void ReportSummary(BuildSummary summary)
{
Console.WriteLine(
$"{EOL}" +
$"###########################{EOL}" +
$"# Build results #{EOL}" +
$"###########################{EOL}" +
$"{EOL}" +
$"Duration: {summary.totalTime.ToString()}{EOL}" +
$"Warnings: {summary.totalWarnings.ToString()}{EOL}" +
$"Errors: {summary.totalErrors.ToString()}{EOL}" +
$"Size: {summary.totalSize.ToString()} bytes{EOL}" +
$"{EOL}"
);
}
private static void ExitWithResult(BuildResult result)
{
if (result == BuildResult.Succeeded) {
Console.WriteLine("Build succeeded!");
EditorApplication.Exit(0);
}
if (result == BuildResult.Failed) {
Console.WriteLine("Build failed!");
EditorApplication.Exit(101);
}
if (result == BuildResult.Cancelled) {
Console.WriteLine("Build cancelled!");
EditorApplication.Exit(102);
}
if (result == BuildResult.Unknown) {
Console.WriteLine("Build result is unknown!");
EditorApplication.Exit(103);
}
StdOutReporter.ExitWithResult(result);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b6e5ef18d769419d887b56665969442b
timeCreated: 1587503329

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d51cf8acfff8c941bb753e82750b60a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
namespace UnityBuilderAction.Input
{
public class ArgumentsParser
{
static string EOL = Environment.NewLine;
static readonly string[] Secrets = { "androidKeystorePass", "androidKeyaliasName", "androidKeyaliasPass" };
public static Dictionary<string, string> GetValidatedOptions()
{
ParseCommandLineArguments(out var validatedOptions);
if (!validatedOptions.TryGetValue("projectPath", out var projectPath)) {
Console.WriteLine("Missing argument -projectPath");
EditorApplication.Exit(110);
}
if (!validatedOptions.TryGetValue("buildTarget", out var buildTarget)) {
Console.WriteLine("Missing argument -buildTarget");
EditorApplication.Exit(120);
}
if (!Enum.IsDefined(typeof(BuildTarget), buildTarget)) {
EditorApplication.Exit(121);
}
if (!validatedOptions.TryGetValue("customBuildPath", out var customBuildPath)) {
Console.WriteLine("Missing argument -customBuildPath");
EditorApplication.Exit(130);
}
const string defaultCustomBuildName = "TestBuild";
if (!validatedOptions.TryGetValue("customBuildName", out var customBuildName)) {
Console.WriteLine($"Missing argument -customBuildName, defaulting to {defaultCustomBuildName}.");
validatedOptions.Add("customBuildName", defaultCustomBuildName);
} else if (customBuildName == "") {
Console.WriteLine($"Invalid argument -customBuildName, defaulting to {defaultCustomBuildName}.");
validatedOptions.Add("customBuildName", defaultCustomBuildName);
}
return validatedOptions;
}
static void ParseCommandLineArguments(out Dictionary<string, string> providedArguments)
{
providedArguments = new Dictionary<string, string>();
string[] args = Environment.GetCommandLineArgs();
Console.WriteLine(
$"{EOL}" +
$"###########################{EOL}" +
$"# Parsing settings #{EOL}" +
$"###########################{EOL}" +
$"{EOL}"
);
// Extract flags with optional values
for (int current = 0, next = 1; current < args.Length; current++, next++) {
// Parse flag
bool isFlag = args[current].StartsWith("-");
if (!isFlag) continue;
string flag = args[current].TrimStart('-');
// 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 {displayValue}.");
providedArguments.Add(flag, value);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 46d2ec4a86604575be2b2d02b0df7b74
timeCreated: 1587503354

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 166f919334c44e7a80ae916667974e7d
timeCreated: 1587503566

View File

@@ -0,0 +1,50 @@
using System;
using UnityEditor;
using UnityEditor.Build.Reporting;
namespace UnityBuilderAction.Reporting
{
public class StdOutReporter
{
static string EOL = Environment.NewLine;
public static void ReportSummary(BuildSummary summary)
{
Console.WriteLine(
$"{EOL}" +
$"###########################{EOL}" +
$"# Build results #{EOL}" +
$"###########################{EOL}" +
$"{EOL}" +
$"Duration: {summary.totalTime.ToString()}{EOL}" +
$"Warnings: {summary.totalWarnings.ToString()}{EOL}" +
$"Errors: {summary.totalErrors.ToString()}{EOL}" +
$"Size: {summary.totalSize.ToString()} bytes{EOL}" +
$"{EOL}"
);
}
public static void ExitWithResult(BuildResult result)
{
if (result == BuildResult.Succeeded) {
Console.WriteLine("Build succeeded!");
EditorApplication.Exit(0);
}
if (result == BuildResult.Failed) {
Console.WriteLine("Build failed!");
EditorApplication.Exit(101);
}
if (result == BuildResult.Cancelled) {
Console.WriteLine("Build cancelled!");
EditorApplication.Exit(102);
}
if (result == BuildResult.Unknown) {
Console.WriteLine("Build result is unknown!");
EditorApplication.Exit(103);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e59b491a4124442ea7277f76761cdc8a
timeCreated: 1587503545

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b5da3bd7e18c43d79243410166c8dc9a
timeCreated: 1587493708

View File

@@ -0,0 +1,42 @@
using System.Diagnostics;
using System.Text;
public static class ProcessExtensions
{
// Execute an application or binary with given arguments
//
// See: https://stackoverflow.com/questions/4291912/process-start-how-to-get-the-output
public static int Run(this Process process, string application,
string arguments, string workingDirectory, out string output,
out string errors)
{
// Configure how to run the application
process.StartInfo = new ProcessStartInfo {
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
FileName = application,
Arguments = arguments,
WorkingDirectory = workingDirectory
};
// Read the output
var outputBuilder = new StringBuilder();
var errorsBuilder = new StringBuilder();
process.OutputDataReceived += (_, args) => outputBuilder.AppendLine(args.Data);
process.ErrorDataReceived += (_, args) => errorsBuilder.AppendLine(args.Data);
// Run the application and wait for it to complete
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
// Format the output
output = outputBuilder.ToString().TrimEnd();
errors = errorsBuilder.ToString().TrimEnd();
return process.ExitCode;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 29c1880a390c4af7be821b7877602815
timeCreated: 1587494270

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1c3bddf6d8984cde9208e3f0fe584879
timeCreated: 1587490700

View File

@@ -0,0 +1,116 @@
using System;
using UnityEngine;
namespace UnityBuilderAction.Versioning
{
public static class Git
{
const string application = @"git";
/// <summary>
/// Generate a version based on the latest tag and the amount of commits.
/// Format: 0.1.2 (where 2 is the amount of commits).
///
/// If no tag is present in the repository then v0.0 is assumed.
/// This would result in 0.0.# where # is the amount of commits.
/// </summary>
public static string GenerateSemanticCommitVersion()
{
string version;
if (HasAnyVersionTags()) {
version = GetSemanticCommitVersion();
Console.WriteLine("Repository has a valid version tag.");
} else {
version = $"0.0.{GetTotalNumberOfCommits()}";
Console.WriteLine("Repository does not have tags to base the version on.");
}
Console.WriteLine($"Version is {version}");
return version;
}
/// <summary>
/// Get the version of the current tag.
///
/// The tag must point at HEAD for this method to work.
///
/// Output Format:
/// #.* (where # is the major version and * can be any number of any type of character)
/// </summary>
public static string GetTagVersion()
{
string version = Run(@"tag --points-at HEAD | grep v[0-9]*");
version = version.Substring(1);
return version;
}
/// <summary>
/// Get the total number of commits.
/// </summary>
static int GetTotalNumberOfCommits()
{
string numberOfCommitsAsString = Run(@"git rev-list --count HEAD");
return int.Parse(numberOfCommitsAsString);
}
/// <summary>
/// Whether or not the repository has any version tags yet.
/// </summary>
static bool HasAnyVersionTags()
{
return "0" != Run(@"tag --list --merged HEAD | grep v[0-9]* | wc -l");
}
/// <summary>
/// Retrieves the build version from git based on the most recent matching tag and
/// commit history. This returns the version as: {major.minor.build} where 'build'
/// represents the nth commit after the tagged commit.
/// Note: The initial 'v' and the commit hash are removed.
/// </summary>
static string GetSemanticCommitVersion()
{
// v0.1-2-g12345678 (where 2 is the amount of commits, g stands for git)
string version = GetVersionString();
// 0.1-2
version = version.Substring(1, version.LastIndexOf('-') - 1);
// 0.1.2
version = version.Replace('-', '.');
return version;
}
/// <summary>
/// Get version string.
///
/// Format: `v0.1-2-g12345678` (where 2 is the amount of commits since the last tag)
///
/// See: https://softwareengineering.stackexchange.com/questions/141973/how-do-you-achieve-a-numeric-versioning-scheme-with-git
/// </summary>
static string GetVersionString()
{
return Run(@"describe --tags --long --match ""v[0-9]*""");
// Todo - implement split function based on this more complete query
// return Run(@"describe --long --tags --dirty --always");
}
/// <summary>
/// Runs git binary with any given arguments and returns the output.
/// </summary>
static string Run(string arguments)
{
using (var process = new System.Diagnostics.Process()) {
string workingDirectory = Application.dataPath;
int exitCode = process.Run(application, arguments, workingDirectory, out string output, out string errors);
if (exitCode != 0) { throw new GitException(exitCode, errors); }
return output;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cdec7fa0f5bb44958fdf74d4658a4601
timeCreated: 1587495075

View File

@@ -0,0 +1,14 @@
using System;
namespace UnityBuilderAction.Versioning
{
public class GitException : InvalidOperationException
{
public readonly int code;
public GitException(int code, string errors) : base(errors)
{
this.code = code;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d375e209fd14fc5bc2f3dc3c78ac574
timeCreated: 1587490750

View File

@@ -0,0 +1,27 @@
using System;
using UnityEditor;
namespace UnityBuilderAction.Versioning
{
public class VersionApplicator
{
public static void SetVersion(string version)
{
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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 30483367ddc84699a0da377ccb93769a
timeCreated: 1587504315

View File

@@ -0,0 +1,10 @@
namespace UnityBuilderAction.Versioning
{
public static class VersionGenerator
{
public static string Generate()
{
return Git.GenerateSemanticCommitVersion();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9892e03ae8314b7eacd793c8002de007
timeCreated: 1587490842

View File

@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Untracked/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Versioning/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

17
action/entrypoint.sh Normal file → Executable file
View File

@@ -8,6 +8,23 @@ source /steps/activate.sh
source /steps/build.sh
source /steps/return_license.sh
#
# Instructions for debugging
#
if [[ $BUILD_EXIT_CODE -gt 0 ]]; then
echo ""
echo "###########################"
echo "# Failure #"
echo "###########################"
echo ""
echo "Please note that the exit code is not very descriptive."
echo "Most likely it will not help you solve the issue."
echo ""
echo "To find the reason for failure: please search for errors in the log above."
echo ""
fi;
#
# Exit with code from the build step.
#

39
action/exec-child.js Normal file
View 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

21
action/steps/activate.sh Normal file → Executable file
View File

@@ -24,9 +24,7 @@ if [[ -n "$UNITY_LICENSE" ]] || [[ -n "$UNITY_LICENSE_FILE" ]]; then
fi
# Activate license
ACTIVATION_OUTPUT=$(xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-batchmode \
ACTIVATION_OUTPUT=$(unity-editor \
-nographics \
-logFile /dev/stdout \
-quit \
@@ -63,15 +61,14 @@ elif [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
echo "Requesting activation (professional license)"
# Activate license
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-batchmode \
-nographics \
-logFile /dev/stdout \
-quit \
-serial "$UNITY_SERIAL" \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD"
unity-editor \
-batchmode \
-nographics \
-logFile /dev/stdout \
-quit \
-serial "$UNITY_SERIAL" \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD"
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?

48
action/steps/build.sh Normal file → Executable file
View File

@@ -48,7 +48,7 @@ if [ -z "$BUILD_METHOD" ]; then
# Create Editor directory if it does not exist
mkdir -p "$UNITY_PROJECT_PATH/Assets/Editor/"
# Copy the build script of Unity Builder action
cp -r "/UnityBuilderAction/Assets/Editor" "$UNITY_PROJECT_PATH/Assets/Editor/"
cp -R "/UnityBuilderAction/Assets/Editor/" "$UNITY_PROJECT_PATH/Assets/Editor/"
# Set the Build method to that of UnityBuilder Action
BUILD_METHOD="UnityBuilderAction.Builder.BuildProject"
# Verify recursive paths
@@ -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 > "$UNITY_PROJECT_PATH/$ANDROID_KEYSTORE_NAME"
echo "Created Android keystore."
fi
#
# Display custom parameters
#
@@ -98,18 +108,23 @@ echo "# Building platform #"
echo "###########################"
echo ""
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-batchmode \
-logfile /dev/stdout \
-quit \
-customBuildName "$BUILD_NAME" \
-projectPath "$UNITY_PROJECT_PATH" \
-buildTarget "$BUILD_TARGET" \
-customBuildTarget "$BUILD_TARGET" \
-customBuildPath "$CUSTOM_BUILD_PATH" \
-executeMethod "$BUILD_METHOD" \
$CUSTOM_PARAMETERS
unity-editor \
-nographics \
-logfile /dev/stdout \
-quit \
-customBuildName "$BUILD_NAME" \
-projectPath "$UNITY_PROJECT_PATH" \
-buildTarget "$BUILD_TARGET" \
-customBuildTarget "$BUILD_TARGET" \
-customBuildPath "$CUSTOM_BUILD_PATH" \
-executeMethod "$BUILD_METHOD" \
-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
BUILD_EXIT_CODE=$?
@@ -121,6 +136,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
#

12
action/steps/return_license.sh Normal file → Executable file
View File

@@ -6,11 +6,9 @@ if [[ -n "$UNITY_SERIAL" ]]; then
#
# This will return the license that is currently in use.
#
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-batchmode \
-nographics \
-logFile /dev/stdout \
-quit \
-returnlicense
unity-editor \
-nographics \
-logFile /dev/stdout \
-quit \
-returnlicense
fi

39
codecov.yml Normal file
View File

@@ -0,0 +1,39 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: '70...100'
status:
patch:
default:
# basic
target: auto
threshold: 100% # don't require patch to have coverage per se
base: auto
project:
default:
# basic
target: auto
threshold: 5%
base: auto
flags:
- unit
paths:
- 'src'
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: 'reach,diff,flags,tree'
behavior: default
require_changes: no

View File

@@ -5,4 +5,5 @@ module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: { '^.+\\.(js|jsx)?$': 'babel-jest' },
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
setupFilesAfterEnv: ['./src/jest.setup.js'],
};

1
media/Discord-Logo.svg Normal file
View 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

View File

@@ -10,33 +10,37 @@
"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.2",
"@actions/exec": "1.0.3",
"@actions/github": "^2.1.1"
"@actions/core": "^1.2.6",
"@actions/exec": "1.0.4",
"@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",
"@babel/core": "7.8.7",
"@babel/preset-env": "7.8.7",
"@zeit/ncc": "0.21.1",
"@babel/core": "7.9.6",
"@babel/preset-env": "7.9.6",
"@zeit/ncc": "0.22.1",
"babel-eslint": "10.1.0",
"eslint": "6.8.0",
"eslint-config-airbnb": "18.0.1",
"eslint-config-prettier": "6.10.0",
"eslint-plugin-flowtype": "4.6.0",
"eslint-plugin-import": "2.20.1",
"eslint-config-airbnb": "18.1.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-flowtype": "4.7.0",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-prettier": "3.1.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-unicorn": "17.1.0",
"husky": "4.2.3",
"jest": "25.1.0",
"lint-staged": "10.0.8",
"eslint-plugin-unicorn": "19.0.1",
"husky": "4.2.5",
"jest": "25.5.3",
"lint-staged": "10.2.2",
"lodash-es": "4.17.15",
"prettier": "1.19.1"
"prettier": "2.0.5"
},
"husky": {
"hooks": {
@@ -53,6 +57,9 @@
"*.{json,md,yaml,yml}": [
"prettier --write",
"git add"
],
"*.sh": [
"git update-index --chmod=+x"
]
}
}

View File

@@ -1,4 +1,4 @@
import { Action, BuildParameters, Cache, Docker, Input, ImageTag } from './model';
import { Action, BuildParameters, Cache, Docker, ImageTag, Kubernetes, Output } from './model';
const core = require('@actions/core');
@@ -7,16 +7,23 @@ async function action() {
Cache.verify();
const { dockerfile, workspace, actionFolder } = Action;
const buildParameters = BuildParameters.create(Input.getFromUser());
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 });
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 => {
action().catch((error) => {
core.setFailed(error.message);
});

49
src/jest.setup.js Normal file
View File

@@ -0,0 +1,49 @@
expect.extend({
toBeOfType(received, expectedType) {
const type = typeof received;
const pass = type === expectedType;
const message = () => `
Expected value to be of type ${this.utils.printExpected(expectedType)},
but received ${this.utils.printReceived(type)}`;
return {
message,
pass,
};
},
toBeEitherAFunctionOrAnObject(received) {
const type = typeof received;
const pass = ['object', 'function'].includes(type);
const message = () => `Expected a ${this.utils.printExpected('function')}
or an ${this.utils.printExpected('object')},
but received ${type}`;
return {
message,
pass,
};
},
toBeParsableToANumber(received) {
let pass = false;
let errorMessage = '';
try {
Number.parseInt(received, 10);
pass = true;
} catch (error) {
errorMessage = error;
}
const message = () => `Expected ${this.utils.printExpected(received)} to be parsable as a number
, but received error: ${this.utils.printReceived(errorMessage)}.`;
return {
message,
pass,
};
},
});

View File

@@ -0,0 +1,17 @@
// Import this named export into your test file:
import Platform from '../platform';
export const mockGetFromUser = jest.fn().mockResolvedValue({
version: '',
targetPlatform: Platform.types.Test,
projectPath: '.',
buildName: Platform.types.Test,
buildsPath: 'build',
buildMethod: undefined,
buildVersion: '1.3.37',
customParameters: '',
});
export default {
getFromUser: mockGetFromUser,
};

View File

@@ -0,0 +1,38 @@
/* eslint-disable unicorn/prevent-abbreviations */
// Import these named export into your test file:
export const mockProjectPath = jest.fn().mockResolvedValue('mockProjectPath');
export const mockIsDirtyAllowed = jest.fn().mockResolvedValue(false);
export const mockBranch = jest.fn().mockResolvedValue('mockBranch');
export const mockHeadRef = jest.fn().mockResolvedValue('mockHeadRef');
export const mockRef = jest.fn().mockResolvedValue('mockRef');
export const mockDetermineVersion = jest.fn().mockResolvedValue('1.2.3');
export const mockGenerateSemanticVersion = jest.fn().mockResolvedValue('2.3.4');
export const mockGenerateTagVersion = jest.fn().mockResolvedValue('1.0');
export const mockParseSemanticVersion = jest.fn().mockResolvedValue({});
export const mockFetch = jest.fn().mockImplementation(() => {});
export const mockGetVersionDescription = jest.fn().mockResolvedValue('1.2-3-g12345678-dirty');
export const mockIsDirty = jest.fn().mockResolvedValue(false);
export const mockGetTag = jest.fn().mockResolvedValue('v1.0');
export const mockHasAnyVersionTags = jest.fn().mockResolvedValue(true);
export const mockGetTotalNumberOfCommits = jest.fn().mockResolvedValue(3);
export const mockGit = jest.fn().mockImplementation(() => {});
export default {
projectPath: mockProjectPath,
isDirtyAllowed: mockIsDirtyAllowed,
branch: mockBranch,
headRef: mockHeadRef,
ref: mockRef,
determineVersion: mockDetermineVersion,
generateSemanticVersion: mockGenerateSemanticVersion,
generateTagVersion: mockGenerateTagVersion,
parseSemanticVersion: mockParseSemanticVersion,
fetch: mockFetch,
getVersionDescription: mockGetVersionDescription,
isDirty: mockIsDirty,
getTag: mockGetTag,
hasAnyVersionTags: mockHasAnyVersionTags,
getTotalNumberOfCommits: mockGetTotalNumberOfCommits,
git: mockGit,
};

View File

@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Versioning determineVersion throws for invalid strategy 0 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;
exports[`Versioning determineVersion throws for invalid strategy somethingRandom 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;
exports[`Versioning determineVersion throws for invalid strategy undefined 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;

View 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;
}
}

View 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);
});
});
});

View File

@@ -1,36 +1,67 @@
import AndroidVersioning from './android-versioning';
import Input from './input';
import Platform from './platform';
import UnityVersioning from './unity-versioning';
import Versioning from './versioning';
class BuildParameters {
static create(parameters) {
const {
unityVersion,
targetPlatform,
projectPath,
buildName,
buildsPath,
buildMethod,
customParameters,
} = parameters;
static async create() {
const buildFile = this.parseBuildFile(
Input.buildName,
Input.targetPlatform,
Input.androidAppBundle,
);
const unityVersion = UnityVersioning.determineUnityVersion(
Input.projectPath,
Input.unityVersion,
);
const buildVersion = await Versioning.determineVersion(
Input.versioningStrategy,
Input.specifiedVersion,
);
const androidVersionCode = AndroidVersioning.determineVersionCode(
buildVersion,
Input.androidVersionCode,
);
return {
version: unityVersion,
platform: targetPlatform,
projectPath,
buildName,
buildPath: `${buildsPath}/${targetPlatform}`,
buildFile: this.parseBuildFile(buildName, targetPlatform),
buildMethod,
customParameters,
customImage: Input.customImage,
runnerTempPath: process.env.RUNNER_TEMP,
platform: Input.targetPlatform,
projectPath: Input.projectPath,
buildName: Input.buildName,
buildPath: `${Input.buildsPath}/${Input.targetPlatform}`,
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;

View File

@@ -1,84 +1,178 @@
import Versioning from './versioning';
import UnityVersioning from './unity-versioning';
import BuildParameters from './build-parameters';
import Input from './input';
import Platform from './platform';
const determineVersion = jest
.spyOn(Versioning, 'determineVersion')
.mockImplementation(() => '1.3.37');
const determineUnityVersion = jest
.spyOn(UnityVersioning, 'determineUnityVersion')
.mockImplementation(() => '2019.2.11f1');
afterEach(() => {
jest.clearAllMocks();
});
describe('BuildParameters', () => {
describe('create', () => {
const someParameters = {
unityVersion: 'someVersion',
targetPlatform: 'somePlatform',
projectPath: 'path/to/project',
buildName: 'someBuildName',
buildsPath: 'someBuildsPath',
buildMethod: 'Namespace.Class.Method',
customParameters: '-someParam someValue',
};
it('does not throw', () => {
expect(() => BuildParameters.create(someParameters)).not.toThrow();
it('does not throw', async () => {
await expect(BuildParameters.create()).resolves.not.toThrow();
});
it('returns the version', () => {
expect(BuildParameters.create(someParameters).version).toStrictEqual(
someParameters.unityVersion,
it('determines the version only once', async () => {
await BuildParameters.create();
expect(determineVersion).toHaveBeenCalledTimes(1);
});
it('determines the unity version only once', async () => {
await BuildParameters.create();
expect(determineUnityVersion).toHaveBeenCalledTimes(1);
});
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 platform', () => {
expect(BuildParameters.create(someParameters).platform).toStrictEqual(
someParameters.targetPlatform,
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 project path', () => {
expect(BuildParameters.create(someParameters).projectPath).toStrictEqual(
someParameters.projectPath,
it('returns the platform', async () => {
const mockValue = 'somePlatform';
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ platform: mockValue }),
);
});
it('returns the build name', () => {
expect(BuildParameters.create(someParameters).buildName).toStrictEqual(
someParameters.buildName,
it('returns the project path', async () => {
const mockValue = 'path/to/project';
jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ projectPath: mockValue }),
);
});
it('returns the build path', () => {
expect(BuildParameters.create(someParameters).buildPath).toStrictEqual(
`${someParameters.buildsPath}/${someParameters.targetPlatform}`,
it('returns the build name', async () => {
const mockValue = 'someBuildName';
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildName: mockValue }),
);
});
describe('build file', () => {
it('returns the build file', () => {
expect(BuildParameters.create(someParameters).buildFile).toStrictEqual(
someParameters.buildName,
it('returns the build path', async () => {
const mockPath = 'somePath';
const mockPlatform = 'somePlatform';
const expectedBuildPath = `${mockPath}/${mockPlatform}`;
jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildPath: expectedBuildPath }),
);
});
it('returns the build file', async () => {
const mockValue = 'someBuildName';
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: mockValue }),
);
});
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])(
'appends exe for %s',
async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}.exe` }),
);
});
},
);
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])(
'appends exe for %s',
targetPlatform => {
expect(
BuildParameters.create({ ...someParameters, targetPlatform }).buildFile,
).toStrictEqual(`${someParameters.buildName}.exe`);
},
);
test.each([Platform.types.Android])('appends apk for %s', targetPlatform => {
expect(
BuildParameters.create({ ...someParameters, targetPlatform }).buildFile,
).toStrictEqual(`${someParameters.buildName}.apk`);
});
});
it('returns the build method', () => {
expect(BuildParameters.create(someParameters).buildMethod).toStrictEqual(
someParameters.buildMethod,
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` }),
);
});
it('returns the custom parameters', () => {
expect(BuildParameters.create(someParameters).customParameters).toStrictEqual(
someParameters.customParameters,
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);
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);
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);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ customParameters: mockValue }),
);
});
});

View File

@@ -1,3 +1,4 @@
import * as core from '@actions/core';
import fs from 'fs';
import Action from './action';
import Project from './project';
@@ -14,11 +15,11 @@ class Cache {
return;
}
// eslint-disable-next-line no-console
console.log(`
core.warning(`
Library folder does not exist.
Consider setting up caching to speed up your workflow
If this is not your first build.`);
Consider setting up caching to speed up your workflow,
if this is not your first build.
`);
}
}

View File

@@ -1,5 +1,7 @@
import Cache from './cache';
jest.mock('./input');
describe('Cache', () => {
describe('Verification', () => {
it('does not throw', () => {

View File

@@ -12,7 +12,7 @@ class Docker {
--build-arg IMAGE=${baseImage} \
--tag ${tag}`;
await exec(command, null, { silent });
await exec(command, undefined, { silent });
return tag;
}
@@ -21,12 +21,20 @@ class Docker {
const {
version,
workspace,
runnerTempPath,
platform,
projectPath,
buildName,
buildPath,
buildFile,
buildMethod,
buildVersion,
androidVersionCode,
androidKeystoreName,
androidKeystoreBase64,
androidKeystorePass,
androidKeyaliasName,
androidKeyaliasPass,
customParameters,
} = parameters;
@@ -45,6 +53,13 @@ class Docker {
--env BUILD_PATH="${buildPath}" \
--env BUILD_FILE="${buildFile}" \
--env BUILD_METHOD="${buildMethod}" \
--env VERSION="${buildVersion}" \
--env ANDROID_VERSION_CODE="${androidVersionCode}" \
--env ANDROID_KEYSTORE_NAME="${androidKeystoreName}" \
--env ANDROID_KEYSTORE_BASE64="${androidKeystoreBase64}" \
--env ANDROID_KEYSTORE_PASS="${androidKeystorePass}" \
--env ANDROID_KEYALIAS_NAME="${androidKeyaliasName}" \
--env ANDROID_KEYALIAS_PASS="${androidKeyaliasPass}" \
--env CUSTOM_PARAMETERS="${customParameters}" \
--env HOME=/github/home \
--env GITHUB_REF \
@@ -63,12 +78,12 @@ 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}`;
await exec(command, null, { silent });
await exec(command, undefined, { silent });
}
}

View File

@@ -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);
});
});

View File

@@ -0,0 +1,8 @@
class CommandExecutionError extends Error {
constructor(message) {
super(message);
this.name = 'CommandExecutionError';
}
}
export default CommandExecutionError;

View File

@@ -0,0 +1,14 @@
import CommandExecutionError from './command-execution-error';
describe('CommandExecutionError', () => {
it('instantiates', () => {
expect(() => new CommandExecutionError()).not.toThrow();
});
test.each([1, 'one', { name: '!' }])('Displays title %s', (message) => {
const error = new CommandExecutionError(message);
expect(error.name).toStrictEqual('CommandExecutionError');
expect(error.message).toStrictEqual(message.toString());
});
});

View File

@@ -0,0 +1,8 @@
class NotImplementedException extends Error {
constructor(message) {
super(message);
this.name = 'NotImplementedException';
}
}
export default NotImplementedException;

View File

@@ -0,0 +1,14 @@
import NotImplementedException from './not-implemented-exception';
describe('NotImplementedException', () => {
it('instantiates', () => {
expect(() => new NotImplementedException()).not.toThrow();
});
test.each([1, 'one', { name: '!' }])('Displays title %s', (message) => {
const error = new NotImplementedException(message);
expect(error.name).toStrictEqual('NotImplementedException');
expect(error.message).toStrictEqual(message.toString());
});
});

View File

@@ -0,0 +1,8 @@
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
export default ValidationError;

View File

@@ -0,0 +1,14 @@
import ValidationError from './validation-error';
describe('ValidationError', () => {
it('instantiates', () => {
expect(() => new ValidationError()).not.toThrow();
});
test.each([1, 'one', { name: '!' }])('Displays title %s', (message) => {
const error = new ValidationError(message);
expect(error.name).toStrictEqual('ValidationError');
expect(error.message).toStrictEqual(message.toString());
});
});

View File

@@ -1,30 +1,23 @@
import { has, get, trimEnd, trimStart } from 'lodash-es';
import { trimEnd, trimStart } from 'lodash-es';
import Platform from './platform';
class ImageTag {
constructor(imageProperties) {
const {
repository = 'gableroux',
name = 'unity3d',
repository = 'unityci',
name = 'editor',
version = '2019.2.11f1',
platform,
customImage,
} = imageProperties;
if (!ImageTag.versionPattern.test(version)) {
throw new Error(`Invalid version "${version}".`);
}
if (!has(ImageTag.targetPlatformToImageSuffixMap, platform)) {
throw new Error(`Platform "${platform}" is currently not supported.`);
}
const builderPlatform = ImageTag.getTargetPlatformToImageSuffixMap(platform, version);
const builderPlatform = get(
ImageTag.targetPlatformToImageSuffixMap,
platform,
ImageTag.imageSuffixes.generic,
);
Object.assign(this, { repository, name, version, platform, builderPlatform });
Object.assign(this, { repository, name, version, platform, builderPlatform, customImage });
}
static get versionPattern() {
@@ -35,40 +28,81 @@ class ImageTag {
return {
generic: '',
webgl: 'webgl',
mac: 'mac',
windows: 'windows',
mac: 'mac-mono',
windows: 'windows-mono',
linux: 'base',
linuxIl2cpp: 'linux-il2cpp',
android: 'android',
ios: 'ios',
facebook: 'facebook',
};
}
static get targetPlatformToImageSuffixMap() {
const { generic, webgl, mac, windows, android, ios, facebook } = ImageTag.imageSuffixes;
static getTargetPlatformToImageSuffixMap(platform, version) {
const {
generic,
webgl,
mac,
windows,
linux,
linuxIl2cpp,
android,
ios,
facebook,
} = ImageTag.imageSuffixes;
const [major, minor] = version.split('.').map((digit) => Number(digit));
// @see: https://docs.unity3d.com/ScriptReference/BuildTarget.html
return {
[Platform.types.StandaloneOSX]: mac,
[Platform.types.StandaloneWindows]: windows,
[Platform.types.StandaloneWindows64]: windows,
[Platform.types.StandaloneLinux64]: windows,
[Platform.types.iOS]: ios,
[Platform.types.Android]: android,
[Platform.types.WebGL]: webgl,
[Platform.types.WSAPlayer]: windows,
[Platform.types.PS4]: windows,
[Platform.types.XboxOne]: windows,
[Platform.types.tvOS]: windows,
[Platform.types.Switch]: windows,
switch (platform) {
case Platform.types.StandaloneOSX:
return mac;
case Platform.types.StandaloneWindows:
return windows;
case Platform.types.StandaloneWindows64:
return windows;
case Platform.types.StandaloneLinux64: {
// Unity versions before 2019.3 do not support il2cpp
if (major >= 2020 || (major === 2019 && minor >= 3)) {
return linuxIl2cpp;
}
return linux;
}
case Platform.types.iOS:
return ios;
case Platform.types.Android:
return android;
case Platform.types.WebGL:
return webgl;
case Platform.types.WSAPlayer:
return windows;
case Platform.types.PS4:
return windows;
case Platform.types.XboxOne:
return windows;
case Platform.types.tvOS:
return windows;
case Platform.types.Switch:
return windows;
// Unsupported
[Platform.types.Lumin]: windows,
[Platform.types.BJM]: windows,
[Platform.types.Stadia]: windows,
[Platform.types.Facebook]: facebook,
[Platform.types.NoTarget]: generic,
case Platform.types.Lumin:
return windows;
case Platform.types.BJM:
return windows;
case Platform.types.Stadia:
return windows;
case Platform.types.Facebook:
return facebook;
case Platform.types.NoTarget:
return generic;
// Test specific
[Platform.types.Test]: generic,
};
case Platform.types.Test:
return generic;
default:
throw new Error(`
Platform must be one of the ones described in the documentation.
"${platform}" is currently not supported.`);
}
}
get tag() {
@@ -82,7 +116,11 @@ class ImageTag {
toString() {
const { image, tag } = this;
return `${image}:${tag}`;
if (this.customImage && this.customImage !== '') {
return this.customImage;
}
return `${image}:${tag}-0`; // '0' here represents the docker repo version
}
}

View File

@@ -1,6 +1,6 @@
import ImageTag from './image-tag';
describe('UnityImageVersion', () => {
describe('ImageTag', () => {
const some = {
repository: 'test1',
name: 'test2',
@@ -10,9 +10,9 @@ describe('UnityImageVersion', () => {
};
const defaults = {
repository: 'gableroux',
name: 'unity3d',
image: 'gableroux/unity3d',
repository: 'unityci',
name: 'editor',
image: 'unityci/editor',
};
describe('constructor', () => {
@@ -31,16 +31,16 @@ describe('UnityImageVersion', () => {
expect(image.builderPlatform).toStrictEqual(some.builderPlatform);
});
test.each(['2000.0.0f0', '2011.1.11f1'])('accepts %p version format', version => {
test.each(['2000.0.0f0', '2011.1.11f1'])('accepts %p version format', (version) => {
expect(() => new ImageTag({ version, platform: some.platform })).not.toThrow();
});
test.each(['some version', '', 1, null])('throws for incorrect versions %p', version => {
test.each(['some version', '', 1])('throws for incorrect versions %p', (version) => {
const { platform } = some;
expect(() => new ImageTag({ version, platform })).toThrow();
});
test.each([undefined, 'nonExisting'])('throws for unsupported target %p', platform => {
test.each([undefined, 'nonExisting'])('throws for unsupported target %p', (platform) => {
expect(() => new ImageTag({ platform })).toThrow();
});
});
@@ -49,19 +49,28 @@ describe('UnityImageVersion', () => {
it('returns the correct version', () => {
const image = new ImageTag({ version: '2099.1.1111', platform: some.platform });
expect(image.toString()).toStrictEqual(`${defaults.image}:2099.1.1111`);
expect(image.toString()).toStrictEqual(`${defaults.image}:2099.1.1111-0`);
});
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' });
expect(image.toString()).toStrictEqual(`${defaults.image}:2019.2.11f1-webgl`);
expect(image.toString()).toStrictEqual(`${defaults.image}:2019.2.11f1-webgl-0`);
});
it('returns no specific build platform for generic targetPlatforms', () => {
const image = new ImageTag({ platform: 'NoTarget' });
expect(image.toString()).toStrictEqual(`${defaults.image}:2019.2.11f1`);
expect(image.toString()).toStrictEqual(`${defaults.image}:2019.2.11f1-0`);
});
});
});

View File

@@ -4,8 +4,24 @@ 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, BuildParameters, Cache, Docker, Input, ImageTag, Platform, Project, Unity };
export {
Action,
BuildParameters,
Cache,
Docker,
Input,
ImageTag,
Output,
Platform,
Project,
Unity,
Versioning,
Kubernetes,
};

View File

@@ -6,12 +6,12 @@ describe('Index', () => {
'BuildParameters',
'Cache',
'Docker',
'Input',
'ImageTag',
'Input',
'Platform',
'Project',
'Unity',
])('exports %s', exportedModule => {
expect(typeof Index[exportedModule]).toStrictEqual('function');
])('exports %s', (exportedModule) => {
expect(Index[exportedModule]).toBeEitherAFunctionOrAnObject();
});
});

View File

@@ -2,30 +2,111 @@ import Platform from './platform';
const core = require('@actions/core');
/**
* Input variables specified in workflows using "with" prop.
*
* Note that input is always passed as a string, even booleans.
*/
class Input {
static getFromUser() {
// Input variables specified in workflows using "with" prop.
const unityVersion = core.getInput('unityVersion');
const targetPlatform = core.getInput('targetPlatform') || Platform.default;
static get unityVersion() {
return core.getInput('unityVersion') || 'auto';
}
static get customImage() {
return core.getInput('customImage');
}
static get targetPlatform() {
return core.getInput('targetPlatform') || Platform.default;
}
static get projectPath() {
const rawProjectPath = core.getInput('projectPath') || '.';
const buildName = core.getInput('buildName') || targetPlatform;
const buildsPath = core.getInput('buildsPath') || 'build';
const buildMethod = core.getInput('buildMethod'); // processed in docker file
const customParameters = core.getInput('customParameters') || '';
return rawProjectPath.replace(/\/$/, '');
}
// Sanitise input
const projectPath = rawProjectPath.replace(/\/$/, '');
static get buildName() {
return core.getInput('buildName') || this.targetPlatform;
}
// Return sanitised input
return {
unityVersion,
targetPlatform,
projectPath,
buildName,
buildsPath,
buildMethod,
customParameters,
};
static get buildsPath() {
return core.getInput('buildsPath') || 'build';
}
static get buildMethod() {
return core.getInput('buildMethod'); // processed in docker file
}
static get versioningStrategy() {
return core.getInput('versioning') || 'Semantic';
}
static get specifiedVersion() {
return core.getInput('version') || '';
}
static get androidVersionCode() {
return core.getInput('androidVersionCode');
}
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') || '';
}
}

View File

@@ -1,13 +1,259 @@
import * as core from '@actions/core';
import Input from './input';
import Platform from './platform';
afterEach(() => {
jest.restoreAllMocks();
});
describe('Input', () => {
describe('getFromUser', () => {
it('does not throw', () => {
expect(() => Input.getFromUser()).not.toThrow();
describe('unityVersion', () => {
it('returns the default value', () => {
expect(Input.unityVersion).toStrictEqual('auto');
});
it('returns an object', () => {
expect(typeof Input.getFromUser()).toStrictEqual('object');
it('takes input from the users workflow', () => {
const mockValue = '2020.4.99f9';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.unityVersion).toStrictEqual(mockValue);
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', () => {
expect(Input.targetPlatform).toStrictEqual(Platform.default);
});
it('takes input from the users workflow', () => {
const mockValue = 'Android';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.targetPlatform).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('projectPath', () => {
it('returns the default value', () => {
expect(Input.projectPath).toStrictEqual('.');
});
it('takes input from the users workflow', () => {
const mockValue = 'customProjectPath';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.projectPath).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('buildName', () => {
it('returns the default value', () => {
expect(Input.buildName).toStrictEqual(Input.targetPlatform);
});
it('takes input from the users workflow', () => {
const mockValue = 'Build';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildName).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
it('takes special characters as input', () => {
const mockValue = '1ßúëld2';
jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildName).toStrictEqual(mockValue);
});
});
describe('buildsPath', () => {
it('returns the default value', () => {
expect(Input.buildsPath).toStrictEqual('build');
});
it('takes input from the users workflow', () => {
const mockValue = 'customBuildsPath';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildsPath).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('buildMethod', () => {
it('returns the default value', () => {
expect(Input.buildMethod).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'Namespace.ClassName.Method';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildMethod).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('versioningStrategy', () => {
it('returns the default value', () => {
expect(Input.versioningStrategy).toStrictEqual('Semantic');
});
it('takes input from the users workflow', () => {
const mockValue = 'Anything';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.versioningStrategy).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('specifiedVersion', () => {
it('returns the default value', () => {
expect(Input.specifiedVersion).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = '1.33.7';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.specifiedVersion).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
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.androidAppBundle).toStrictEqual(false);
});
it('returns true when string true is passed', () => {
const spy = jest.spyOn(core, 'getInput').mockReturnValue('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.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);
});
});
describe('customParameters', () => {
it('returns the default value', () => {
expect(Input.customParameters).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = '-imAFlag';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.customParameters).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});

355
src/model/kubernetes.js Normal file
View 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
View 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
View 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();
});
});
});

View File

@@ -1,10 +1,10 @@
import Unity from './unity';
import Input from './input';
import Unity from './unity';
import Action from './action';
class Project {
static get relativePath() {
const { projectPath } = Input.getFromUser();
const { projectPath } = Input;
return `${projectPath}`;
}

View File

@@ -1,5 +1,7 @@
import Project from './project';
jest.mock('./input');
describe('Platform', () => {
describe('relativePath', () => {
it('does not throw', () => {

62
src/model/system.js Normal file
View File

@@ -0,0 +1,62 @@
import * as core from '@actions/core';
import { exec } from '@actions/exec';
class System {
static async run(command, arguments_, options) {
let result = '';
let error = '';
let debug = '';
const listeners = {
stdout: (dataBuffer) => {
result += dataBuffer.toString();
},
stderr: (dataBuffer) => {
error += dataBuffer.toString();
},
debug: (dataString) => {
debug += dataString.toString();
},
};
const showOutput = () => {
if (debug !== '') {
core.debug(debug);
}
if (result !== '') {
core.info(result);
}
if (error !== '') {
core.warning(error);
}
};
const throwContextualError = (message) => {
let commandAsString = command;
if (Array.isArray(arguments_)) {
commandAsString += ` ${arguments_.join(' ')}`;
} else if (typeof arguments_ === 'string') {
commandAsString += ` ${arguments_}`;
}
throw new Error(`Failed to run "${commandAsString}".\n ${message}`);
};
try {
const exitCode = await exec(command, arguments_, { silent: true, listeners, ...options });
showOutput();
if (exitCode !== 0) {
throwContextualError(`Command returned non-zero exit code.\nError: ${error}`);
}
} catch (inCommandError) {
showOutput();
throwContextualError(`In-command error caught: ${inCommandError}`);
}
return result;
}
}
export default System;

56
src/model/system.test.js Normal file
View File

@@ -0,0 +1,56 @@
import * as core from '@actions/core';
import System from './system';
jest.spyOn(core, 'debug').mockImplementation(() => {});
const info = jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(core, 'warning').mockImplementation(() => {});
jest.spyOn(core, 'error').mockImplementation(() => {});
afterEach(() => {
jest.clearAllMocks();
});
describe('System', () => {
describe('run', () => {
it('runs a command successfully', async () => {
await expect(System.run('true')).resolves.not.toBeNull();
});
it('outputs results', async () => {
await expect(System.run('echo test')).resolves.toStrictEqual('test\n');
});
it('throws on when error code is not 0', async () => {
await expect(System.run('false')).rejects.toThrowError();
});
it('throws when no arguments are given', async () => {
await expect(System.run()).rejects.toThrowError();
});
it('outputs info', async () => {
await expect(System.run('echo test')).resolves.not.toBeNull();
expect(info).toHaveBeenLastCalledWith('test\n');
});
it('outputs info only once', async () => {
await expect(System.run('echo 1')).resolves.not.toBeNull();
expect(info).toHaveBeenCalledTimes(1);
expect(info).toHaveBeenLastCalledWith('1\n');
info.mockClear();
await expect(System.run('echo 2')).resolves.not.toBeNull();
await expect(System.run('echo 3')).resolves.not.toBeNull();
expect(info).toHaveBeenCalledTimes(2);
expect(info).toHaveBeenLastCalledWith('3\n');
});
it('allows pipes using buffer', async () => {
await expect(
System.run('sh', undefined, {
input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
}),
).resolves.toBeParsableToANumber();
});
});
});

View File

@@ -0,0 +1,33 @@
import * as core from '@actions/core';
import * as fs from 'fs';
import * as path from 'path';
export default class UnityVersioning {
static get versionPattern() {
return /20\d{2}\.\d\.\w{3,4}|3/;
}
static determineUnityVersion(projectPath, unityVersion) {
if (unityVersion === 'auto') {
return UnityVersioning.read(projectPath);
}
return unityVersion;
}
static read(projectPath) {
const filePath = path.join(projectPath, 'ProjectSettings', 'ProjectVersion.txt');
if (!fs.existsSync(filePath)) {
core.warning(`Could not find "${filePath}", keeping unityVersion as "auto"`);
return 'auto';
}
return UnityVersioning.parse(fs.readFileSync(filePath, 'utf8'));
}
static parse(projectVersionTxt) {
const matches = projectVersionTxt.match(UnityVersioning.versionPattern);
if (!matches || matches.length === 0) {
throw new Error(`Failed to parse version from "${projectVersionTxt}".`);
}
return matches[0];
}
}

View File

@@ -0,0 +1,35 @@
import UnityVersioning from './unity-versioning';
describe('Unity Versioning', () => {
describe('parse', () => {
it('throws for empty string', () => {
expect(() => UnityVersioning.parse('')).toThrow(Error);
});
it('parses from ProjectVersion.txt', () => {
const projectVersionContents = `m_EditorVersion: 2019.2.11f1
m_EditorVersionWithRevision: 2019.2.11f1 (5f859a4cfee5)`;
expect(UnityVersioning.parse(projectVersionContents)).toBe('2019.2.11f1');
});
});
describe('read', () => {
it('does not throw', () => {
expect(() => UnityVersioning.read('')).not.toThrow();
});
it('reads from test-project', () => {
expect(UnityVersioning.read('./test-project')).toBe('2019.2.11f1');
});
});
describe('determineUnityVersion', () => {
it('defaults to parsed version', () => {
expect(UnityVersioning.determineUnityVersion('./test-project', 'auto')).toBe('2019.2.11f1');
});
it('use specified unityVersion', () => {
expect(UnityVersioning.determineUnityVersion('./test-project', '1.2.3')).toBe('1.2.3');
});
});
});

242
src/model/versioning.js Normal file
View File

@@ -0,0 +1,242 @@
import * as core from '@actions/core';
import NotImplementedException from './error/not-implemented-exception';
import ValidationError from './error/validation-error';
import Input from './input';
import System from './system';
export default class Versioning {
static get projectPath() {
return Input.projectPath;
}
static get isDirtyAllowed() {
return Input.allowDirtyBuild;
}
static get strategies() {
return { None: 'None', Semantic: 'Semantic', Tag: 'Tag', Custom: 'Custom' };
}
/**
* Get the branch name of the (related) branch
*/
static get branch() {
// Todo - use optional chaining (https://github.com/zeit/ncc/issues/534)
return this.headRef || (this.ref && this.ref.slice(11));
}
/**
* For pull requests we can reliably use GITHUB_HEAD_REF
*/
static get headRef() {
return process.env.GITHUB_HEAD_REF;
}
/**
* For branches GITHUB_REF will have format `refs/heads/feature-branch-1`
*/
static get ref() {
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
*/
static get descriptionRegex() {
return /^v([\d.]+)-(\d+)-g(\w+)-?(\w+)*/g;
}
static async determineVersion(strategy, inputVersion) {
// Validate input
if (!Object.hasOwnProperty.call(this.strategies, strategy)) {
throw new ValidationError(
`Versioning strategy should be one of ${Object.values(this.strategies).join(', ')}.`,
);
}
let version;
switch (strategy) {
case this.strategies.None:
version = 'none';
break;
case this.strategies.Custom:
version = inputVersion;
break;
case this.strategies.Semantic:
version = await this.generateSemanticVersion();
break;
case this.strategies.Tag:
version = await this.generateTagVersion();
break;
default:
throw new NotImplementedException(`Strategy ${strategy} is not implemented.`);
}
return version;
}
/**
* Automatically generates a version based on SemVer out of the box.
*
* The version works as follows: `<major>.<minor>.<patch>` for example `0.1.2`.
*
* The latest tag dictates `<major>.<minor>`
* The number of commits since that tag dictates`<patch>`.
*
* @See: https://semver.org/
*/
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');
}
if (!(await this.hasAnyVersionTags())) {
const version = `0.0.${await this.getTotalNumberOfCommits()}`;
core.info(`Generated version ${version} (no version tags found).`);
return version;
}
const { tag, commits, hash } = await this.parseSemanticVersion();
core.info(`Found semantic version ${tag}.${commits} for ${this.branch}@${hash}`);
return `${tag}.${commits}`;
}
/**
* Generate the proper version for unity based on an existing tag.
*/
static async generateTagVersion() {
let tag = await this.getTag();
if (tag.charAt(0) === 'v') {
tag = tag.slice(1);
}
return tag;
}
/**
* Parses the versionDescription into their named parts.
*/
static async parseSemanticVersion() {
const description = await this.getVersionDescription();
try {
const [match, tag, commits, hash] = this.descriptionRegex.exec(description);
return {
match,
tag,
commits,
hash,
};
} catch (error) {
throw new Error(`Failed to parse git describe output: "${description}".`);
}
}
/**
* Retrieves refs from the configured remote.
*
* Fetch unshallow for incomplete repository, but fall back to normal fetch.
*
* Note: `--all` should not be used, and would break fetching for push event.
*/
static async fetch() {
try {
await this.git(['fetch', '--unshallow']);
} catch (error) {
core.warning(`Fetch --unshallow caught: ${error}`);
await this.git(['fetch']);
}
}
/**
* Retrieves information about the branch.
*
* Format: `v0.12-24-gd2198ab`
*
* In this format v0.12 is the latest tag, 24 are the number of commits since, and gd2198ab
* identifies the current commit.
*/
static async getVersionDescription() {
return this.git(['describe', '--long', '--tags', '--always', '--debug', this.sha]);
}
/**
* Returns whether there are uncommitted changes that are not ignored.
*/
static async isDirty() {
const output = await this.git(['status', '--porcelain']);
return output !== '';
}
/**
* Get the tag if there is one pointing at HEAD
*/
static async getTag() {
return this.git(['tag', '--points-at', 'HEAD']);
}
/**
* Whether or not the repository has any version tags yet.
*/
static async hasAnyVersionTags() {
const numberOfCommitsAsString = await System.run('sh', undefined, {
input: Buffer.from('git tag --list --merged HEAD | grep v[0-9]* | wc -l'),
silent: false,
});
const numberOfCommits = Number.parseInt(numberOfCommitsAsString, 10);
return numberOfCommits !== 0;
}
/**
* Get the total number of commits on head.
*
* 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', this.sha]);
return Number.parseInt(numberOfCommitsAsString, 10);
}
/**
* Run git in the specified project path
*/
static async git(arguments_, options = {}) {
return System.run('git', arguments_, { cwd: this.projectPath, ...options });
}
}

View File

@@ -0,0 +1,339 @@
import * as core from '@actions/core';
import NotImplementedException from './error/not-implemented-exception';
import System from './system';
import Versioning from './versioning';
afterEach(() => {
jest.restoreAllMocks();
});
describe('Versioning', () => {
describe('strategies', () => {
it('returns an object', () => {
expect(typeof Versioning.strategies).toStrictEqual('object');
});
it('has items', () => {
expect(Object.values(Versioning.strategies).length).toBeGreaterThan(2);
});
it('has an opt out option', () => {
expect(Versioning.strategies).toHaveProperty('None');
});
it('has the semantic option', () => {
expect(Versioning.strategies).toHaveProperty('Semantic');
});
it('has a strategy for tags', () => {
expect(Versioning.strategies).toHaveProperty('Tag');
});
it('has an option that allows custom input', () => {
expect(Versioning.strategies).toHaveProperty('Custom');
});
});
describe('branch', () => {
it('returns headRef when set', () => {
const headReference = jest
.spyOn(Versioning, 'headRef', 'get')
.mockReturnValue('feature-branch-1');
expect(Versioning.branch).toStrictEqual('feature-branch-1');
expect(headReference).toHaveBeenCalledTimes(1);
});
it('returns part of Ref when set', () => {
jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue(undefined);
const reference = jest
.spyOn(Versioning, 'ref', 'get')
.mockReturnValue('refs/heads/feature-branch-2');
expect(Versioning.branch).toStrictEqual('feature-branch-2');
expect(reference).toHaveBeenCalledTimes(2);
});
it('prefers headRef over ref when set', () => {
const headReference = jest
.spyOn(Versioning, 'headRef', 'get')
.mockReturnValue('feature-branch-1');
const reference = jest
.spyOn(Versioning, 'ref', 'get')
.mockReturnValue('refs/heads/feature-2');
expect(Versioning.branch).toStrictEqual('feature-branch-1');
expect(headReference).toHaveBeenCalledTimes(1);
expect(reference).toHaveBeenCalledTimes(0);
});
it('returns undefined when headRef and ref are not set', () => {
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue(undefined);
const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue(undefined);
expect(Versioning.branch).not.toBeDefined();
expect(headReference).toHaveBeenCalledTimes(1);
expect(reference).toHaveBeenCalledTimes(1);
});
});
describe('headRef', () => {
it('does not throw', () => {
expect(() => Versioning.headRef).not.toThrow();
});
});
describe('ref', () => {
it('does not throw', () => {
expect(() => Versioning.ref).not.toThrow();
});
});
describe('isDirtyAllowed', () => {
it('does not throw', () => {
expect(() => Versioning.isDirtyAllowed).not.toThrow();
});
it('returns false by default', () => {
expect(Versioning.isDirtyAllowed).toStrictEqual(false);
});
});
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);
});
test.each(['v1.1-1-g12345678', 'v0.1-2-g12345678', 'v0.0-500-gA9B6C3D0-dirty'])(
'is happy with valid %s',
(description) => {
expect(Versioning.descriptionRegex.test(description)).toBeTruthy();
},
);
test.each([undefined, 'v0', 'v0.1', 'v0.1.2', 'v0.1-2', 'v0.1-2-g'])(
'does not like %s',
(description) => {
expect(Versioning.descriptionRegex.test(description)).toBeFalsy();
// Also never expect without the v to work for any of these cases.
expect(Versioning.descriptionRegex.test(description?.substr(1))).toBeFalsy();
},
);
});
describe('determineVersion', () => {
test.each([undefined, 0, 'somethingRandom'])(
'throws for invalid strategy %s',
async (strategy) => {
await expect(Versioning.determineVersion(strategy)).rejects.toThrowErrorMatchingSnapshot();
},
);
describe('opt out strategy', () => {
it("returns 'none'", async () => {
await expect(Versioning.determineVersion('None', 'v1.0')).resolves.toMatchInlineSnapshot(
`"none"`,
);
});
});
describe('custom strategy', () => {
test.each([undefined, 0, 'v0.1', '1', 'CamelCase', 'dashed-version'])(
'returns the inputVersion for %s',
async (inputVersion) => {
await expect(Versioning.determineVersion('Custom', inputVersion)).resolves.toStrictEqual(
inputVersion,
);
},
);
});
describe('semantic strategy', () => {
it('refers to generateSemanticVersion', async () => {
const generateSemanticVersion = jest
.spyOn(Versioning, 'generateSemanticVersion')
.mockResolvedValue('1.3.37');
await expect(Versioning.determineVersion('Semantic')).resolves.toStrictEqual('1.3.37');
expect(generateSemanticVersion).toHaveBeenCalledTimes(1);
});
});
describe('tag strategy', () => {
it('refers to generateTagVersion', async () => {
const generateTagVersion = jest
.spyOn(Versioning, 'generateTagVersion')
.mockResolvedValue('0.1');
await expect(Versioning.determineVersion('Tag')).resolves.toStrictEqual('0.1');
expect(generateTagVersion).toHaveBeenCalledTimes(1);
});
});
describe('not implemented strategy', () => {
it('throws a not implemented exception', async () => {
const strategy = 'Test';
jest.spyOn(Versioning, 'strategies', 'get').mockReturnValue({ [strategy]: strategy });
await expect(Versioning.determineVersion(strategy)).rejects.toThrowError(
NotImplementedException,
);
});
});
});
describe('generateTagVersion', () => {
it('removes the v', async () => {
jest.spyOn(Versioning, 'getTag').mockResolvedValue('v1.3.37');
await expect(Versioning.generateTagVersion()).resolves.toStrictEqual('1.3.37');
});
});
describe('parseSemanticVersion', () => {
it('returns the named parts', async () => {
jest.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('v0.1-2-g12345678');
await expect(Versioning.parseSemanticVersion()).resolves.toMatchObject({
tag: '0.1',
commits: '2',
hash: '12345678',
});
});
it('throws when no match could be made', async () => {
jest.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('no-match-can-be-made');
await expect(Versioning.parseSemanticVersion()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to parse git describe output: \\"no-match-can-be-made\\"."`,
);
});
});
describe('getVersionDescription', () => {
it('returns the commands output', async () => {
const runOutput = 'someValue';
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
await expect(Versioning.getVersionDescription()).resolves.toStrictEqual(runOutput);
});
});
describe('fetch', () => {
it('awaits the command', async () => {
jest.spyOn(core, 'warning').mockImplementation(() => {});
jest.spyOn(System, 'run').mockResolvedValue(undefined);
await expect(Versioning.fetch()).resolves.not.toThrow();
});
it('falls back to the second strategy when the first fails', async () => {
jest.spyOn(core, 'warning').mockImplementation(() => {});
const gitFetch = jest
.spyOn(System, 'run')
.mockResolvedValue(undefined)
.mockRejectedValueOnce(undefined);
await expect(Versioning.fetch()).resolves.not.toThrow();
expect(gitFetch).toHaveBeenCalledTimes(2);
});
});
describe('generateSemanticVersion', () => {
it('returns a proper version from description', async () => {
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
jest.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(2);
jest.spyOn(Versioning, 'parseSemanticVersion').mockResolvedValue({
match: '0.1-2-g1b345678',
tag: '0.1',
commits: '2',
hash: '1b345678',
});
await expect(Versioning.generateSemanticVersion()).resolves.toStrictEqual('0.1.2');
});
it('throws when dirty', async () => {
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(true);
await expect(Versioning.generateSemanticVersion()).rejects.toThrowError();
});
it('falls back to commits only, when no tags are present', async () => {
const commits = Math.round(Math.random() * 10);
jest.spyOn(System, 'run').mockResolvedValue(undefined);
jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(false);
jest.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(commits);
await expect(Versioning.generateSemanticVersion()).resolves.toStrictEqual(`0.0.${commits}`);
});
});
describe('isDirty', () => {
it('returns true when there are files listed', async () => {
const runOutput = 'file.ext\nfile2.ext';
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
await expect(Versioning.isDirty()).resolves.toStrictEqual(true);
});
it('returns false when there is no output', async () => {
const runOutput = '';
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
await expect(Versioning.isDirty()).resolves.toStrictEqual(false);
});
});
describe('getTag', () => {
it('returns the commands output', async () => {
const runOutput = 'v1.0';
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
await expect(Versioning.getTag()).resolves.toStrictEqual(runOutput);
});
});
describe('hasAnyVersionTags', () => {
it('returns false when the command returns 0', async () => {
const runOutput = '0';
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
await expect(Versioning.hasAnyVersionTags()).resolves.toStrictEqual(false);
});
it('returns true when the command returns >= 0', async () => {
const runOutput = '9';
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
await expect(Versioning.hasAnyVersionTags()).resolves.toStrictEqual(true);
});
});
describe('getTotalNumberOfCommits', () => {
it('returns a number from the command', async () => {
jest.spyOn(System, 'run').mockResolvedValue('9');
await expect(Versioning.getTotalNumberOfCommits()).resolves.toStrictEqual(9);
});
});
});

3407
yarn.lock

File diff suppressed because it is too large Load Diff