thoughts, stories and ideas

Using continuous integration in our everyday workflow can help us a lot with faster and iterative development, and having CI do checks every time we change our codebase helps us with dealing with fear of modifying code.

Deploying game builds manually takes time and leaves us idle while we could be developing new and exciting features instead.

In this read we will go through a sample YAML configuration for Unity projects and describe the main YAML building blocks with common Unity CI jobs.

YAML intro

The YAML file defines a set of jobs with constraints stating when they should be run. The jobs are defined as top-level elements with a name and always have to contain at least the script clause:

helloworld_job:
  script: "echo Hello World!"

YAML syntax allows for more complex job definitions than in the above example:

before_script:
  - bundle install
  
after_script:
  - rm secrets
  
stages:
  - build
  - test
  - deploy
  
helloworld_job:
  stage: build
  script:
    - echo Hello World
  only:
    - master
  tags:
    - unity

before_script - commands that run before each jobs script
after_script - commands that run after each jobs script
stages - used to define build stages
only - defines the names of branches and tags for which the job will run
tags - used to select specific Runners from the list of all Runners that are allowed to run this project.

Initial setup for Unity
First step is to create a YAML file called .gitlab-ci.yml in the root directory of your Unity project and add the following code:

image: "gableroux/unity3d:latest"

stages:
- test
- build

variables:
  BUILD_NAME: ExampleProjectName

.unity_before_script: &unity_before_script
  before_script:
  - chmod +x ./ci/before_script.sh && ./ci/before_script.sh

In unity_before_script we execute these two commands:
- chmod +x ./ci/before_script.sh - makes before script executable by Gitlab CI runner
- ./ci/before_script.sh - executes before script

In the stages section we define 4 build stages:
- test- for test jobs that include unit and instrumentation tests
- build- for build jobs
- deploy- for deployment jobs

Build stage
This job (build) is used to create a build artifact that can be used to test the game manually.

.build: &build
  stage: build
  <<: *unity_before_script
  script:
  - chmod +x ./ci/build.sh && ./ci/build.sh

  artifacts:
    paths:
    - ./Builds/

build-StandaloneLinux64:
  <<: *build
  variables:
    BUILD_TARGET: StandaloneLinux64

build-StandaloneOSX:
  <<: *build
  variables:
    BUILD_TARGET: StandaloneOSX

build:- name of the CI job
stage: build- it gets executed in the build stage
./ci/build.sh- executes shell script to create a build
artifacts:- job section that defines list of files and directories that are attached to a job after completion.
paths:- output file paths
./Builds- directory path of our build
BUILD_TARGET- environment variable used by build.sh to define for what platform its build your game

Tests
This job (test) runs our tests in a test stage. Every time the tests fail, a report artifact will be created.

.test: &test
  stage: test
  <<: *unity_before_script
  script:
  - chmod +x ./ci/test.sh && ./ci/test.sh
  artifacts:
    paths:
    - $(pwd)/$TEST_PLATFORM-results.xml

test-editmode:
  <<: *test
  variables:
    TEST_PLATFORM: editmode

test-playmode:
  <<: *test
  variables:
    TEST_PLATFORM: playmode

./ci/tesh.sh- runs shell script that triggers our tests
TEST_PLATFORM- environment variable used by test.sh to run unit or instrumental tests

Shell scripts
Here is a list of the shell scripts used in this blog, simply copy/paste them in CI folder of your Unity project.

before_script.sh
build.sh
test.sh

How to activate your Unity license
You'll first need to run this locally. All you need is docker installed on your machine.

  1. Add all of the files above (.gitlab-ci.yml, ci/before_script.sh, ci/build.sh, ci/test.sh)
  2. Pull the docker image and run bash inside, passing Unity username and password
UNITY_VERSION=2018.3.0f2
docker run -it --rm \
-e "UNITY_USERNAME=username@example.com" \
-e "UNITY_PASSWORD=example_password" \
-e "TEST_PLATFORM=linux" \
-e "WORKDIR=/root/project" \
-v "$(pwd):/root/project" \
gableroux/unity3d:$UNITY_VERSION \
bash

3. In Unity docker container's bash, run once like this, and it will try to activate:

xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-logFile \
-batchmode \
-username "$UNITY_USERNAME" -password "$UNITY_PASSWORD"

4. Wait for the output that looks like this:

LICENSE SYSTEM [2017723 8:6:38] Posting <?xml version="1.0" encoding="UTF-8"?><root><SystemInfo><IsoCode>en</IsoCode><UserName>[...]

If you get the following error:

Can't activate unity: No sufficient permissions while processing request HTTP error code 401

Make sure your credentials are valid.

You may try to disable 2FA in your account and try again. Once done, you should enable 2FA again for security reasons. See #11 for more details.

5. Copy xml content and save as `unity3d.alf`

6. Open https://license.unity3d.com/manual and answer questions

7. Upload unity3d.alf for manual activation

8. Download Unity_v2018.x.ulf

9. Copy the content of Unity_v2018.x.ulf license file to your CI's environment variable UNITY_LICENSE_CONTENT. Note: if you are doing this on windows, chances are the line endings will be wrong as explained here. Luckily for you, .gitlab-ci.yml solves this by removing \r character from the env variable so you'll be alright gitlab-ci.yml will then place the UNITY_LICENSE_CONTENT to the right place before running tests or creating the builds.

Test file location
- editmode tests need to be in Assets/Scripts/Editor/EditModeTests
- playmode tests need to be in Assets/Tests/

There’s plenty more
Setting up Unity continuous integration with GitLab CI is awesome and supports plenty of cool features, a lot more than we talked about.

Hopefully this short introduction was helpful and is going to motivate you to discover more features on your own.

Check out the complete .gitlab-ci.yml:

image: "gableroux/unity3d:latest"

stages:
- test
- build

variables:
  BUILD_NAME: ExampleProjectName

.unity_before_script: &unity_before_script
  before_script:
  - chmod +x ./ci/before_script.sh && ./ci/before_script.sh

.test: &test
  stage: test
  <<: *unity_before_script
  script:
  - chmod +x ./ci/test.sh && ./ci/test.sh
  artifacts:
    paths:
    - $(pwd)/$TEST_PLATFORM-results.xml

test-editmode:
  <<: *test
  variables:
    TEST_PLATFORM: editmode

test-playmode:
  <<: *test
  variables:
    TEST_PLATFORM: playmode

.build: &build
  stage: build
  <<: *unity_before_script
  script:
  - chmod +x ./ci/build.sh && ./ci/build.sh
  artifacts:
    paths:
    - ./Builds/

build-StandaloneLinux64:
  <<: *build
  variables:
    BUILD_TARGET: StandaloneLinux64

build-StandaloneOSX:
  <<: *build
  variables:
    BUILD_TARGET: StandaloneOSX

build-StandaloneWindows64:
  <<: *build
  variables:
    BUILD_TARGET: StandaloneWindows64