From a8c041067c7a120ada81b218cff580bc0c3c0e35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Horv=C3=A1th=20Istv=C3=A1n?= <dzctir@inf.elte.hu>
Date: Wed, 2 Mar 2022 13:08:54 +0100
Subject: [PATCH] Add CI to the project (see #18)

---
 .gitlab-ci.yml                             | 254 ++++++++++++++++++++
 Assets/Scripts.meta                        |   8 +
 Assets/Scripts/Editor.meta                 |   8 +
 Assets/Scripts/Editor/BuildCommand.cs      | 267 +++++++++++++++++++++
 Assets/Scripts/Editor/BuildCommand.cs.meta |  11 +
 ci/before_script.sh                        |  32 +++
 ci/build.sh                                |  36 +++
 ci/docker_build.sh                         |  14 ++
 ci/docker_test.sh                          |  13 +
 ci/get_activation_file.sh                  |  49 ++++
 ci/nunit-transforms/LICENSE.txt            |  19 ++
 ci/nunit-transforms/nunit3-junit.xslt      |  69 ++++++
 ci/test.sh                                 |  56 +++++
 13 files changed, 836 insertions(+)
 create mode 100644 .gitlab-ci.yml
 create mode 100644 Assets/Scripts.meta
 create mode 100644 Assets/Scripts/Editor.meta
 create mode 100644 Assets/Scripts/Editor/BuildCommand.cs
 create mode 100644 Assets/Scripts/Editor/BuildCommand.cs.meta
 create mode 100644 ci/before_script.sh
 create mode 100644 ci/build.sh
 create mode 100644 ci/docker_build.sh
 create mode 100644 ci/docker_test.sh
 create mode 100644 ci/get_activation_file.sh
 create mode 100644 ci/nunit-transforms/LICENSE.txt
 create mode 100644 ci/nunit-transforms/nunit3-junit.xslt
 create mode 100644 ci/test.sh

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..6197a34
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,254 @@
+stages:
+  - prepare
+  - build_and_test
+  - deploy
+
+# If you are looking for a place where to add 'UNITY_LICENSE_FILE' and other secrets, please visit your project's gitlab page:
+# settings > CI/CD > Variables instead
+variables:
+  BUILD_NAME: Tree-o
+  UNITY_ACTIVATION_FILE: ./unity3d.alf
+  IMAGE: unityci/editor # https://hub.docker.com/r/unityci/editor
+  IMAGE_VERSION: "0.15" # https://github.com/game-ci/docker/releases
+  UNITY_DIR: $CI_PROJECT_DIR # this needs to be an absolute path. Defaults to the root of your tree.
+  # You can expose this in Unity via Application.version
+  VERSION_NUMBER_VAR: $CI_COMMIT_REF_SLUG-$CI_PIPELINE_ID-$CI_JOB_ID
+  VERSION_BUILD_VAR: $CI_PIPELINE_IID
+
+image: $IMAGE:$UNITY_VERSION-base-$IMAGE_VERSION
+
+get-unity-version:
+  image: alpine
+  stage: prepare
+  variables:
+    GIT_DEPTH: 1
+  script:
+    - echo UNITY_VERSION=$(cat $UNITY_DIR/ProjectSettings/ProjectVersion.txt | grep "m_EditorVersion:.*" | awk '{ print $2}') | tee prepare.env
+  artifacts:
+    reports:
+      dotenv: prepare.env
+
+.unity_before_script: &unity_before_script
+  before_script:
+    - chmod +x ./ci/before_script.sh && ./ci/before_script.sh
+  needs:
+    - job: get-unity-version
+      artifacts: true
+
+.cache: &cache
+  cache:
+    key: "$CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-$CI_COMMIT_REF_SLUG-$TEST_PLATFORM"
+    paths:
+      - $UNITY_DIR/Library/
+
+.license: &license
+  rules:
+    - if: '$UNITY_LICENSE != null'
+      when: always
+
+.unity_defaults: &unity_defaults
+  <<:
+    - *unity_before_script
+    - *cache
+    - *license
+
+# run this job when you need to request a license
+# you may need to follow activation steps from documentation
+get-activation-file:
+  <<: *unity_before_script
+  rules:
+    - if: '$UNITY_LICENSE == null'
+      when: manual
+  stage: prepare
+  script:
+    - chmod +x ./ci/get_activation_file.sh && ./ci/get_activation_file.sh
+  artifacts:
+    paths:
+      - $UNITY_ACTIVATION_FILE
+    expire_in: 10 min # Expiring this as artifacts may contain sensitive data and should not be kept public
+
+.test: &test
+  stage: build_and_test
+  <<: *unity_defaults
+  script:
+    - chmod +x ./ci/test.sh && ./ci/test.sh
+  artifacts:
+    when: always
+    expire_in: 2 weeks
+  # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
+  # you may need to remove or replace these to fit your need if you are using your own runners
+  tags:
+    - gitlab-org
+  coverage: /<Linecoverage>(.*?)</Linecoverage>/
+
+# Tests without junit reporting results in GitLab
+# test-playmode:
+#   <<: *test
+#   variables:
+#     TEST_PLATFORM: playmode
+#     TESTING_TYPE: NUNIT
+
+# test-editmode:
+#   <<: *test
+#   variables:
+#     TEST_PLATFORM: editmode
+#     TESTING_TYPE: NUNIT
+
+# uncomment the following blocks if you'd like to have junit reporting unity test results in gitlab
+# We currently have the following issue which prevents it from working right now, but you can give
+# a hand if you're interested in this feature:
+# https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/151
+
+#.test-with-junit-reports: &test-with-junit-reports
+#  stage: build_and_test
+#  <<: *unity_defaults
+#  script:
+#    # This could be made faster by adding these packages to base image or running in a separate job (and step)
+#    # We could use an image with these two depencencies only and only do the saxonb-xslt command on
+#    # previous job's artifacts
+#    - apt-get update && apt-get install -y default-jre libsaxonb-java
+#    - chmod +x ./ci/test.sh && ./ci/test.sh
+#    - saxonb-xslt -s $UNITY_DIR/$TEST_PLATFORM-results.xml -xsl $CI_PROJECT_DIR/ci/nunit-transforms/nunit3-junit.xslt >$UNITY_DIR/$TEST_PLATFORM-junit-results.xml
+#  artifacts:
+#    when: always
+#    paths:
+#    # This is exported to allow viewing the Coverage Report in detail if needed
+#    - $UNITY_DIR/$TEST_PLATFORM-coverage/
+#    reports:
+#      junit:
+#        - $UNITY_DIR/$TEST_PLATFORM-junit-results.xml
+#        - "$UNITY_DIR/$TEST_PLATFORM-coverage/coverage.xml"
+#    expire_in: 2 weeks
+#  # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
+#  # you may need to remove or replace these to fit your need if you are using your own runners
+#  tags:
+#    - gitlab-org
+#  coverage: /<Linecoverage>(.*?)</Linecoverage>/
+#
+#test-playmode-with-junit-reports:
+#  <<: *test-with-junit-reports
+#  variables:
+#    TEST_PLATFORM: playmode
+#    TESTING_TYPE: JUNIT
+#
+#test-editmode-with-junit-reports:
+#  <<: *test-with-junit-reports
+#  variables:
+#    TEST_PLATFORM: editmode
+#    TESTING_TYPE: JUNIT
+
+.build: &build
+  stage: build_and_test
+  <<: *unity_defaults
+  script:
+    - chmod +x ./ci/build.sh && ./ci/build.sh
+  artifacts:
+    paths:
+      - $UNITY_DIR/Builds/
+  # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
+  # you may need to remove or replace these to fit your need if you are using your own runners
+  tags:
+    - gitlab-org
+
+build-StandaloneLinux64:
+  <<: *build
+  variables:
+    BUILD_TARGET: StandaloneLinux64
+
+build-StandaloneLinux64-il2cpp:
+  <<: *build
+  image: $IMAGE:$UNITY_VERSION-linux-il2cpp-$IMAGE_VERSION
+  variables:
+    BUILD_TARGET: StandaloneLinux64
+    SCRIPTING_BACKEND: IL2CPP
+
+#build-StandaloneOSX:
+#  <<: *build
+#  image: $IMAGE:$UNITY_VERSION-mac-mono-$IMAGE_VERSION
+#  variables:
+#    BUILD_TARGET: StandaloneOSX
+
+#Note: build target names changed in recent versions, use this for versions < 2017.2:
+# build-StandaloneOSXUniversal:
+#   <<: *build
+#   variables:
+#     BUILD_TARGET: StandaloneOSXUniversal
+
+build-StandaloneWindows64:
+  <<: *build
+  image: $IMAGE:$UNITY_VERSION-windows-mono-$IMAGE_VERSION
+  variables:
+    BUILD_TARGET: StandaloneWindows64
+
+# For webgl support, you need to set Compression Format to Disabled for v0.9. See https://github.com/game-ci/docker/issues/75
+build-WebGL:
+  <<: *build
+  image: $IMAGE:$UNITY_VERSION-webgl-$IMAGE_VERSION
+  # Temporary workaround for https://github.com/game-ci/docker/releases/tag/v0.9 and webgl support in current project to prevent errors with missing ffmpeg
+  before_script:
+    - chmod +x ./ci/before_script.sh && ./ci/before_script.sh
+    - apt-get update && apt-get install ffmpeg -y
+  variables:
+    BUILD_TARGET: WebGL
+
+#build-android:
+#  <<: *build
+#  image: $IMAGE:$UNITY_VERSION-android-$IMAGE_VERSION
+#  variables:
+#    BUILD_TARGET: Android
+#    BUILD_APP_BUNDLE: "false"
+
+#build-android-il2cpp:
+#  <<: *build
+#  image: $IMAGE:$UNITY_VERSION-android-$IMAGE_VERSION
+#  variables:
+#    BUILD_TARGET: Android
+#    BUILD_APP_BUNDLE: "false"
+#    SCRIPTING_BACKEND: IL2CPP
+
+#deploy-android:
+#  stage: deploy
+#  image: ruby
+#  script:
+#    - cd $UNITY_DIR/Builds/Android
+#    - echo $GPC_TOKEN > gpc_token.json
+#    - gem install bundler
+#    - bundle install
+#    - fastlane supply --aab "${BUILD_NAME}.aab" --track internal --package_name com.youcompany.yourgame --json_key ./gpc_token.json
+#  needs: ["build-android"]
+
+#build-ios-xcode:
+#  <<: *build
+#  image: $IMAGE:$UNITY_VERSION-ios-$IMAGE_VERSION
+#  variables:
+#    BUILD_TARGET: iOS
+
+#build-and-deploy-ios:
+#  stage: deploy
+#  script:
+#    - cd $UNITY_DIR/Builds/iOS/$BUILD_NAME
+#    - pod install
+#    - fastlane ios beta
+#  tags:
+#    - ios
+#    - mac
+#  needs: ["build-ios-xcode"]
+
+pages:
+  image: alpine:latest
+  stage: deploy
+  script:
+    - mv "$UNITY_DIR/Builds/WebGL/${BUILD_NAME}" public
+  artifacts:
+    paths:
+      - public
+  only:
+    - main
+
+workflow:
+  rules:
+    - if: $CI_MERGE_REQUEST_ID
+      when: never
+    - if: $CI_COMMIT_TAG
+      when: never
+    - when: always
diff --git a/Assets/Scripts.meta b/Assets/Scripts.meta
new file mode 100644
index 0000000..a789076
--- /dev/null
+++ b/Assets/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 441a5be2d4157cd419d9457727681552
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Editor.meta b/Assets/Scripts/Editor.meta
new file mode 100644
index 0000000..4b672b9
--- /dev/null
+++ b/Assets/Scripts/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e73cc4c83019b134db945ba52f915a02
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Editor/BuildCommand.cs b/Assets/Scripts/Editor/BuildCommand.cs
new file mode 100644
index 0000000..d5cc771
--- /dev/null
+++ b/Assets/Scripts/Editor/BuildCommand.cs
@@ -0,0 +1,267 @@
+using UnityEditor;
+using System.Linq;
+using System;
+using System.IO;
+
+static class BuildCommand {
+	private const string KEYSTORE_PASS = "KEYSTORE_PASS";
+	private const string KEY_ALIAS_PASS = "KEY_ALIAS_PASS";
+	private const string KEY_ALIAS_NAME = "KEY_ALIAS_NAME";
+	private const string KEYSTORE = "keystore.keystore";
+	private const string BUILD_OPTIONS_ENV_VAR = "BuildOptions";
+	private const string ANDROID_BUNDLE_VERSION_CODE = "VERSION_BUILD_VAR";
+	private const string ANDROID_APP_BUNDLE = "BUILD_APP_BUNDLE";
+	private const string SCRIPTING_BACKEND_ENV_VAR = "SCRIPTING_BACKEND";
+	private const string VERSION_NUMBER_VAR = "VERSION_NUMBER_VAR";
+	private const string VERSION_iOS = "VERSION_BUILD_VAR";
+
+	static string GetArgument(string name) {
+		string[] args = Environment.GetCommandLineArgs();
+		for (int i = 0; i < args.Length; i++) {
+			if (args[i].Contains(name)) {
+				return args[i + 1];
+			}
+		}
+
+		return null;
+	}
+
+	static string[] GetEnabledScenes() {
+		return (
+			from scene in EditorBuildSettings.scenes
+			where scene.enabled
+			where !string.IsNullOrEmpty(scene.path)
+			select scene.path
+		).ToArray();
+	}
+
+	static BuildTarget GetBuildTarget() {
+		string buildTargetName = GetArgument("customBuildTarget");
+		Console.WriteLine(":: Received customBuildTarget " + buildTargetName);
+
+		if (buildTargetName.ToLower() == "android") {
+#if !UNITY_5_6_OR_NEWER
+			// https://issuetracker.unity3d.com/issues/buildoptions-dot-acceptexternalmodificationstoplayer-causes-unityexception-unknown-project-type-0
+			// Fixed in Unity 5.6.0
+			// side effect to fix android build system:
+			EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Internal;
+#endif
+		}
+
+		if (buildTargetName.TryConvertToEnum(out BuildTarget target)) return target;
+
+		Console.WriteLine(
+			$":: {nameof(buildTargetName)} \"{buildTargetName}\" not defined on enum {nameof(BuildTarget)}, using {nameof(BuildTarget.NoTarget)} enum to build");
+
+		return BuildTarget.NoTarget;
+	}
+
+	static string GetBuildPath() {
+		string buildPath = GetArgument("customBuildPath");
+		Console.WriteLine(":: Received customBuildPath " + buildPath);
+		if (buildPath == "") {
+			throw new Exception("customBuildPath argument is missing");
+		}
+
+		return buildPath;
+	}
+
+	static string GetBuildName() {
+		string buildName = GetArgument("customBuildName");
+		Console.WriteLine(":: Received customBuildName " + buildName);
+		if (buildName == "") {
+			throw new Exception("customBuildName argument is missing");
+		}
+
+		return buildName;
+	}
+
+	static string GetFixedBuildPath(BuildTarget buildTarget, string buildPath, string buildName) {
+		if (buildTarget.ToString().ToLower().Contains("windows")) {
+			buildName += ".exe";
+		} else if (buildTarget == BuildTarget.Android) {
+#if UNITY_2018_3_OR_NEWER
+			buildName += EditorUserBuildSettings.buildAppBundle ? ".aab" : ".apk";
+#else
+            buildName += ".apk";
+#endif
+		}
+
+		return buildPath + buildName;
+	}
+
+	static BuildOptions GetBuildOptions() {
+		if (TryGetEnv(BUILD_OPTIONS_ENV_VAR, out string envVar)) {
+			string[] allOptionVars = envVar.Split(',');
+			BuildOptions allOptions = BuildOptions.None;
+			BuildOptions option;
+			string optionVar;
+			int length = allOptionVars.Length;
+
+			Console.WriteLine($":: Detecting {BUILD_OPTIONS_ENV_VAR} env var with {length} elements ({envVar})");
+
+			for (int i = 0; i < length; i++) {
+				optionVar = allOptionVars[i];
+
+				if (optionVar.TryConvertToEnum(out option)) {
+					allOptions |= option;
+				} else {
+					Console.WriteLine($":: Cannot convert {optionVar} to {nameof(BuildOptions)} enum, skipping it.");
+				}
+			}
+
+			return allOptions;
+		}
+
+		return BuildOptions.None;
+	}
+
+	// https://stackoverflow.com/questions/1082532/how-to-tryparse-for-enum-value
+	static bool TryConvertToEnum<TEnum>(this string strEnumValue, out TEnum value) {
+		if (!Enum.IsDefined(typeof(TEnum), strEnumValue)) {
+			value = default;
+			return false;
+		}
+
+		value = (TEnum) Enum.Parse(typeof(TEnum), strEnumValue);
+		return true;
+	}
+
+	static bool TryGetEnv(string key, out string value) {
+		value = Environment.GetEnvironmentVariable(key);
+		return !string.IsNullOrEmpty(value);
+	}
+
+	static void SetScriptingBackendFromEnv(BuildTarget platform) {
+		var targetGroup = BuildPipeline.GetBuildTargetGroup(platform);
+		if (TryGetEnv(SCRIPTING_BACKEND_ENV_VAR, out string scriptingBackend)) {
+			if (scriptingBackend.TryConvertToEnum(out ScriptingImplementation backend)) {
+				Console.WriteLine($":: Setting ScriptingBackend to {backend}");
+				PlayerSettings.SetScriptingBackend(targetGroup, backend);
+			} else {
+				string possibleValues = string.Join(", ",
+					Enum.GetValues(typeof(ScriptingImplementation)).Cast<ScriptingImplementation>());
+				throw new Exception(
+					$"Could not find '{scriptingBackend}' in ScriptingImplementation enum. Possible values are: {possibleValues}");
+			}
+		} else {
+			var defaultBackend = PlayerSettings.GetDefaultScriptingBackend(targetGroup);
+			Console.WriteLine(
+				$":: Using project's configured ScriptingBackend (should be {defaultBackend} for targetGroup {targetGroup}");
+		}
+	}
+
+	static void PerformBuild() {
+		var buildTarget = GetBuildTarget();
+
+		Console.WriteLine(":: Performing build");
+		if (TryGetEnv(VERSION_NUMBER_VAR, out var bundleVersionNumber)) {
+			if (buildTarget == BuildTarget.iOS) {
+				// bundleVersionNumber = GetIosVersion(); // NOTE: we don't build for iOS
+			}
+
+			Console.WriteLine(
+				$":: Setting bundleVersionNumber to '{bundleVersionNumber}' (Length: {bundleVersionNumber.Length})");
+			PlayerSettings.bundleVersion = bundleVersionNumber;
+		}
+
+		if (buildTarget == BuildTarget.Android) {
+			// HandleAndroidAppBundle(); // NOTE: we don't build for Android
+			// HandleAndroidBundleVersionCode();
+			// HandleAndroidKeystore();
+		}
+
+		var buildPath = GetBuildPath();
+		var buildName = GetBuildName();
+		var buildOptions = GetBuildOptions();
+		var fixedBuildPath = GetFixedBuildPath(buildTarget, buildPath, buildName);
+
+		SetScriptingBackendFromEnv(buildTarget);
+
+		var buildReport = BuildPipeline.BuildPlayer(GetEnabledScenes(), fixedBuildPath, buildTarget, buildOptions);
+
+		if (buildReport.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
+			throw new Exception($"Build ended with {buildReport.summary.result} status");
+
+		Console.WriteLine(":: Done with build");
+	}
+//
+// 	private static void HandleAndroidAppBundle() {
+// 		if (TryGetEnv(ANDROID_APP_BUNDLE, out string value)) {
+// #if UNITY_2018_3_OR_NEWER
+// 			if (bool.TryParse(value, out bool buildAppBundle)) {
+// 				EditorUserBuildSettings.buildAppBundle = buildAppBundle;
+// 				Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected, set buildAppBundle to {value}.");
+// 			} else {
+// 				Console.WriteLine(
+// 					$":: {ANDROID_APP_BUNDLE} env var detected but the value \"{value}\" is not a boolean.");
+// 			}
+// #else
+//             Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected but does not work with lower Unity version than 2018.3");
+// #endif
+// 		}
+// 	}
+//
+// 	private static void HandleAndroidBundleVersionCode() {
+// 		if (TryGetEnv(ANDROID_BUNDLE_VERSION_CODE, out string value)) {
+// 			if (int.TryParse(value, out int version)) {
+// 				PlayerSettings.Android.bundleVersionCode = version;
+// 				Console.WriteLine(
+// 					$":: {ANDROID_BUNDLE_VERSION_CODE} env var detected, set the bundle version code to {value}.");
+// 			} else
+// 				Console.WriteLine(
+// 					$":: {ANDROID_BUNDLE_VERSION_CODE} env var detected but the version value \"{value}\" is not an integer.");
+// 		}
+// 	}
+//
+// 	private static string GetIosVersion() {
+// 		if (TryGetEnv(VERSION_iOS, out string value)) {
+// 			if (int.TryParse(value, out int version)) {
+// 				Console.WriteLine($":: {VERSION_iOS} env var detected, set the version to {value}.");
+// 				return version.ToString();
+// 			} else
+// 				Console.WriteLine(
+// 					$":: {VERSION_iOS} env var detected but the version value \"{value}\" is not an integer.");
+// 		}
+//
+// 		throw new ArgumentNullException(nameof(value), $":: Error finding {VERSION_iOS} env var");
+// 	}
+//
+// 	private static void HandleAndroidKeystore() {
+// #if UNITY_2019_1_OR_NEWER
+// 		PlayerSettings.Android.useCustomKeystore = false;
+// #endif
+//
+// 		if (!File.Exists(KEYSTORE)) {
+// 			Console.WriteLine($":: {KEYSTORE} not found, skipping setup, using Unity's default keystore");
+// 			return;
+// 		}
+//
+// 		PlayerSettings.Android.keystoreName = KEYSTORE;
+//
+// 		string keystorePass;
+// 		string keystoreAliasPass;
+//
+// 		if (TryGetEnv(KEY_ALIAS_NAME, out string keyaliasName)) {
+// 			PlayerSettings.Android.keyaliasName = keyaliasName;
+// 			Console.WriteLine($":: using ${KEY_ALIAS_NAME} env var on PlayerSettings");
+// 		} else {
+// 			Console.WriteLine($":: ${KEY_ALIAS_NAME} env var not set, using Project's PlayerSettings");
+// 		}
+//
+// 		if (!TryGetEnv(KEYSTORE_PASS, out keystorePass)) {
+// 			Console.WriteLine($":: ${KEYSTORE_PASS} env var not set, skipping setup, using Unity's default keystore");
+// 			return;
+// 		}
+//
+// 		if (!TryGetEnv(KEY_ALIAS_PASS, out keystoreAliasPass)) {
+// 			Console.WriteLine($":: ${KEY_ALIAS_PASS} env var not set, skipping setup, using Unity's default keystore");
+// 			return;
+// 		}
+// #if UNITY_2019_1_OR_NEWER
+// 		PlayerSettings.Android.useCustomKeystore = true;
+// #endif
+// 		PlayerSettings.Android.keystorePass = keystorePass;
+// 		PlayerSettings.Android.keyaliasPass = keystoreAliasPass;
+// 	}
+}
diff --git a/Assets/Scripts/Editor/BuildCommand.cs.meta b/Assets/Scripts/Editor/BuildCommand.cs.meta
new file mode 100644
index 0000000..9f1ca5f
--- /dev/null
+++ b/Assets/Scripts/Editor/BuildCommand.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 500342c176fc01e42bcd63303276b79a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/ci/before_script.sh b/ci/before_script.sh
new file mode 100644
index 0000000..1e1effa
--- /dev/null
+++ b/ci/before_script.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+set -e
+set -x
+mkdir -p /root/.cache/unity3d
+mkdir -p /root/.local/share/unity3d/Unity/
+set +x
+
+unity_license_destination=/root/.local/share/unity3d/Unity/Unity_lic.ulf
+android_keystore_destination=keystore.keystore
+
+
+upper_case_build_target=${BUILD_TARGET^^};
+
+if [ "$upper_case_build_target" = "ANDROID" ]
+then
+    if [ -n $ANDROID_KEYSTORE_BASE64 ]
+    then
+        echo "'\$ANDROID_KEYSTORE_BASE64' found, decoding content into ${android_keystore_destination}"
+        echo $ANDROID_KEYSTORE_BASE64 | base64 --decode > ${android_keystore_destination}
+    else
+        echo '$ANDROID_KEYSTORE_BASE64'" env var not found, building with Unity's default debug keystore"
+    fi
+fi
+
+if [ -n "$UNITY_LICENSE" ]
+then
+    echo "Writing '\$UNITY_LICENSE' to license file ${unity_license_destination}"
+    echo "${UNITY_LICENSE}" | tr -d '\r' > ${unity_license_destination}
+else
+    echo "'\$UNITY_LICENSE' env var not found"
+fi
diff --git a/ci/build.sh b/ci/build.sh
new file mode 100644
index 0000000..d92a8c5
--- /dev/null
+++ b/ci/build.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -e
+set -x
+
+echo "Building for $BUILD_TARGET"
+
+export BUILD_PATH=$UNITY_DIR/Builds/$BUILD_TARGET/
+mkdir -p $BUILD_PATH
+
+${UNITY_EXECUTABLE:-xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unity-editor} \
+  -projectPath $UNITY_DIR \
+  -quit \
+  -batchmode \
+  -nographics \
+  -buildTarget $BUILD_TARGET \
+  -customBuildTarget $BUILD_TARGET \
+  -customBuildName $BUILD_NAME \
+  -customBuildPath $BUILD_PATH \
+  -executeMethod BuildCommand.PerformBuild \
+  -logFile /dev/stdout
+
+UNITY_EXIT_CODE=$?
+
+if [ $UNITY_EXIT_CODE -eq 0 ]; then
+  echo "Run succeeded, no failures occurred";
+elif [ $UNITY_EXIT_CODE -eq 2 ]; then
+  echo "Run succeeded, some tests failed";
+elif [ $UNITY_EXIT_CODE -eq 3 ]; then
+  echo "Run failure (other failure)";
+else
+  echo "Unexpected exit code $UNITY_EXIT_CODE";
+fi
+
+ls -la $BUILD_PATH
+[ -n "$(ls -A $BUILD_PATH)" ] # fail job if build folder is empty
diff --git a/ci/docker_build.sh b/ci/docker_build.sh
new file mode 100644
index 0000000..3088dfa
--- /dev/null
+++ b/ci/docker_build.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -e
+
+docker run \
+  -e BUILD_NAME \
+  -e UNITY_LICENSE \
+  -e BUILD_TARGET \
+  -e UNITY_USERNAME \
+  -e UNITY_PASSWORD \
+  -w /project/ \
+  -v $UNITY_DIR:/project/ \
+  $IMAGE_NAME \
+  /bin/bash -c "/project/ci/before_script.sh && /project/ci/build.sh"
diff --git a/ci/docker_test.sh b/ci/docker_test.sh
new file mode 100644
index 0000000..1b846af
--- /dev/null
+++ b/ci/docker_test.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -e
+
+docker run \
+  -e UNITY_LICENSE \
+  -e TEST_PLATFORM \
+  -e UNITY_USERNAME \
+  -e UNITY_PASSWORD \
+  -w /project/ \
+  -v $UNITY_DIR:/project/ \
+  $IMAGE_NAME \
+  /bin/bash -c "/project/ci/before_script.sh && /project/ci/test.sh"
diff --git a/ci/get_activation_file.sh b/ci/get_activation_file.sh
new file mode 100644
index 0000000..2ace508
--- /dev/null
+++ b/ci/get_activation_file.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+activation_file=${UNITY_ACTIVATION_FILE:-./unity3d.alf}
+
+if [[ -z "${UNITY_USERNAME}" ]] || [[ -z "${UNITY_PASSWORD}" ]]; then
+  echo "UNITY_USERNAME or UNITY_PASSWORD environment variables are not set, please refer to instructions in the readme and add these to your secret environment variables."
+  exit 1
+fi
+
+xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
+  unity-editor \
+    -logFile /dev/stdout \
+    -batchmode \
+    -nographics \
+    -username "$UNITY_USERNAME" -password "$UNITY_PASSWORD" |
+      tee ./unity-output.log
+
+cat ./unity-output.log |
+  grep 'LICENSE SYSTEM .* Posting *' |
+  sed 's/.*Posting *//' > "${activation_file}"
+
+# Fail job if unity.alf is empty
+ls "${UNITY_ACTIVATION_FILE:-./unity3d.alf}"
+exit_code=$?
+
+if [[ ${exit_code} -eq 0 ]]; then
+  echo ""
+  echo ""
+  echo "### Congratulations! ###"
+  echo "${activation_file} was generated successfully!"
+  echo ""
+  echo "### Next steps ###"
+  echo ""
+  echo "Complete the activation process manually"
+  echo ""
+  echo "   1. Download the artifact which should contain ${activation_file}"
+  echo "   2. Visit https://license.unity3d.com/manual"
+  echo "   3. Upload ${activation_file} in the form"
+  echo "   4. Answer questions (unity pro vs personal edition, both will work, just pick the one you use)"
+  echo "   5. Download 'Unity_v2019.x.ulf' file (year should match your unity version here, 'Unity_v2018.x.ulf' for 2018, etc.)"
+  echo "   6. Copy the content of 'Unity_v2019.x.ulf' license file to your CI's environment variable 'UNITY_LICENSE'. (Open your project's parameters > CI/CD > Variables and add 'UNITY_LICENSE' as the key and paste the content of the license file into the value)"
+  echo ""
+  echo "Once you're done, hit retry on the pipeline where other jobs failed, or just push another commit. Things should be green"
+  echo ""
+  echo "(optional) For more details on why this is not fully automated, visit https://gitlab.com/gableroux/unity3d-gitlab-ci-example/issues/73"
+else
+  echo "License file could not be found at ${UNITY_ACTIVATION_FILE:-./unity3d.alf}"
+fi
+exit $exit_code
diff --git a/ci/nunit-transforms/LICENSE.txt b/ci/nunit-transforms/LICENSE.txt
new file mode 100644
index 0000000..5aa1d8f
--- /dev/null
+++ b/ci/nunit-transforms/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2016 Paul Hicks
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/ci/nunit-transforms/nunit3-junit.xslt b/ci/nunit-transforms/nunit3-junit.xslt
new file mode 100644
index 0000000..08e046a
--- /dev/null
+++ b/ci/nunit-transforms/nunit3-junit.xslt
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:output method="xml" indent="yes"/>
+
+  <xsl:template match="/test-run">
+    <testsuites tests="{@testcasecount}" failures="{@failed}" disabled="{@skipped}" time="{@duration}">
+      <xsl:apply-templates/>
+    </testsuites>
+  </xsl:template>
+
+  <xsl:template match="test-suite">
+    <xsl:if test="test-case">
+      <testsuite tests="{@testcasecount}" time="{@duration}" errors="{@testcasecount - @passed - @skipped - @failed}" failures="{@failed}" skipped="{@skipped}" timestamp="{@start-time}">
+        <xsl:attribute name="name">
+          <xsl:for-each select="ancestor-or-self::test-suite/@name">
+            <xsl:value-of select="concat(., '.')"/>
+          </xsl:for-each>
+        </xsl:attribute>
+        <xsl:apply-templates select="test-case"/>
+      </testsuite>
+      <xsl:apply-templates select="test-suite"/>
+    </xsl:if>
+    <xsl:if test="not(test-case)">
+      <xsl:apply-templates/>
+    </xsl:if>
+  </xsl:template>
+
+  <xsl:template match="test-case">
+    <testcase name="{@name}" assertions="{@asserts}" time="{@duration}" status="{@result}" classname="{@classname}">
+      <xsl:if test="@runstate = 'Skipped' or @runstate = 'Ignored'">
+        <skipped/>
+      </xsl:if>
+      
+      <xsl:apply-templates/>
+    </testcase>
+  </xsl:template>
+
+  <xsl:template match="command-line"/>
+  <xsl:template match="settings"/>
+
+  <xsl:template match="output">
+    <system-out>
+      <xsl:value-of select="."/>
+    </system-out>
+  </xsl:template>
+
+  <xsl:template match="stack-trace">
+  </xsl:template>
+
+  <xsl:template match="test-case/failure">
+    <failure message="{./message}">
+      <xsl:value-of select="./stack-trace"/>
+    </failure>
+  </xsl:template>
+
+  <xsl:template match="test-suite/failure"/>
+
+  <xsl:template match="test-case/reason">
+    <skipped message="{./message}"/>
+  </xsl:template>
+  
+  <xsl:template match="test-case/assertions">
+  </xsl:template>
+
+  <xsl:template match="test-suite/reason"/>
+
+  <xsl:template match="properties"/>
+</xsl:stylesheet>
+
diff --git a/ci/test.sh b/ci/test.sh
new file mode 100644
index 0000000..e45251c
--- /dev/null
+++ b/ci/test.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+set -x
+
+echo "Testing for $TEST_PLATFORM, Unit Type: $TESTING_TYPE"
+
+CODE_COVERAGE_PACKAGE="com.unity.testtools.codecoverage"
+PACKAGE_MANIFEST_PATH="Packages/manifest.json"
+
+${UNITY_EXECUTABLE:-xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unity-editor} \
+  -projectPath $UNITY_DIR \
+  -runTests \
+  -testPlatform $TEST_PLATFORM \
+  -testResults $UNITY_DIR/$TEST_PLATFORM-results.xml \
+  -logFile /dev/stdout \
+  -batchmode \
+  -nographics \
+  -enableCodeCoverage \
+  -coverageResultsPath $UNITY_DIR/$TEST_PLATFORM-coverage \
+  -coverageOptions "generateAdditionalMetrics;generateHtmlReport;generateHtmlReportHistory;generateBadgeReport;" \
+  -debugCodeOptimization
+
+UNITY_EXIT_CODE=$?
+
+if [ $UNITY_EXIT_CODE -eq 0 ]; then
+  echo "Run succeeded, no failures occurred";
+elif [ $UNITY_EXIT_CODE -eq 2 ]; then
+  echo "Run succeeded, some tests failed";
+  if [ $TESTING_TYPE == 'JUNIT' ]; then
+    echo "Converting results to JUNit for analysis";
+    saxonb-xslt -s $UNITY_DIR/$TEST_PLATFORM-results.xml -xsl $CI_PROJECT_DIR/ci/nunit-transforms/nunit3-junit.xslt >$UNITY_DIR/$TEST_PLATFORM-junit-results.xml
+  fi
+elif [ $UNITY_EXIT_CODE -eq 3 ]; then
+  echo "Run failure (other failure)";
+  if [ $TESTING_TYPE == 'JUNIT' ]; then
+    echo "Not converting results to JUNit";
+  fi
+else
+  echo "Unexpected exit code $UNITY_EXIT_CODE";
+  if [ $TESTING_TYPE == 'JUNIT' ]; then
+    echo "Not converting results to JUNit";
+  fi
+fi
+
+if grep $CODE_COVERAGE_PACKAGE $PACKAGE_MANIFEST_PATH; then
+  cat $UNITY_DIR/$TEST_PLATFORM-coverage/Report/Summary.xml | grep Linecoverage
+  mv $UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov/*Mode/TestCoverageResults_*.xml $UNITY_DIR/$TEST_PLATFORM-coverage/coverage.xml
+  rm -r $UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov/
+else
+  {
+    echo -e "\033[33mCode Coverage package not found in $PACKAGE_MANIFEST_PATH. Please install the package \"Code Coverage\" through Unity's Package Manager to enable coverage reports.\033[0m"
+  } 2> /dev/null
+fi
+
+cat $UNITY_DIR/$TEST_PLATFORM-results.xml | grep test-run | grep Passed
+exit $UNITY_EXIT_CODE
-- 
GitLab