Constantly Delivering my Unity Project builds with Jenkins
Tags: jenkins, unity3d, cicd, programming
Published: 2024-02-05 23:00:42

Hi!

In this document, I will explain how I managed to create my own continuous integration and delivery pipeline using **Jenkins** for my projects in Unity. All the code mentioned here will be available on my **GitHub** account for free use. There may be various ways to implement what I am presenting here, so feel free to share any thoughts on how I can improve in the future. I would love to hear your feedback.

One of the many factors that influenced me to learn more about how I can build this Continuous Integration and Continuous Delivery (CICD) process was my old spare notebook that I have at home. To prevent it from collecting dust in my closet, I decided to turn it into a building machine. This way, I could easily connect to it using any of those remote desktop apps and build my applications. Within a matter of minutes, I would have it ready to be tested on any shared devices.

At this moment, the Pipeline is running in four phases: **checkout**, **setup**, **build**, and **publish**. I will document each one of these phases and explain how it was implemented.

## Using Jenkins

For the Pipelines, I decided to use **Jenkins**, an open-source automation server. It assists in automating various aspects of software development, including builds, testing, and deployment, thereby simplifying the process of continuous integration and delivery.

For the sake of simplicity, I have opted to run **Jenkins** locally on my machine without utilizing any Jenkins image within a separate Docker container. Since it was my first time doing this, I went through various tests and process definitions, involving constant "add" and "remove" actions on files and codes. Additionally, given that my knowledge of Docker is not that advanced, using it would have caused a delay in achieving what I was attempting to do.

During my research, videos from [DevOps Journey](https://www.youtube.com/watch?v=6YZvp2GwT0A&t=3393s&ab_channel=DevOpsJourney) and [CloudBeesTV](https://www.youtube.com/@CloudBeesTV) helped me a lot to understand a little bit more about **Jenkins**. It didn't take me too long to get comfortable using this awesome tool.

I have created a repository on **GitLab** containing all the code needed to run the pipeline. Once **Jenkins** starts the build, it downloads this repository and runs the **main.groovy** script. This file is the beginning of the pipeline, running all the steps, from build to uploading to Google Drive. As I said before, this pipeline is split into 4 phases: **checkout**, **setup**, **build**, and **publish**. These phases run essential commands for generating the build. The diagram of these phases is represented below for a summary.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/glu71jgcxm1cg8j97upm.jpg)

```groovy
node()
{
    stage('checkout')
    {
	    // ...
    }
    stage('Setup')
    {
	    // ...
    }
    stage('Build')
    {
	    // ...
    }
    stage('Publish')
    {
	    // ...
	}
}
```

## A closer look at the phases

### Checkout

First phase, it is required to update the Pipeline to the most recent version. It must stay updated before any builds in case it has changed.

### Setup

The Unity project has its repository. When starting the pipeline, **Jenkins** updates this repository to the most recent version and then starts the build.

In this phase, I encountered an issue that started occurring after **git** had its vulnerability correction, beginning with version [Git v2.35.2](https://github.blog/2022-04-12-git-security-vulnerability-announced/#). In short, when **git** attempts to run a command inside another repository, it begins respecting any configurations in that **git** directory. This could lead to a security problem, especially in cases where sensitive configurations are defined.

As my pipeline already runs inside its repository, when attempting to execute `git checkout <branch>` from within Unity's repository, it triggers the fatal error `unsafe repository`. The error itself suggests a solution that would likely resolve this issue, which is to add Unity's repository to a list of trusted folders. This should allow me to run the command again. However, the fatal error persists.

After numerous attempts, I successfully resolved it by creating a [simple C# console](https://github.com/lourenco-pedro/git_updater) directly within Unity's repository. This program then executes the **git** command, specifying the target branch to checkout. With this workaround, I could update the project, leaving it ready for building in the next phase.

Here is an example of how this program runs the Git command, wrapping every Git functionality that might be necessary for this phase:

```CS
public class Program
{
    public static int Main(string[] args)
    {
        try
        {
            string branchName = args[0];

			Git git = new Git(branchName);
            git.FetchAll();
            git.Checkout();

            return 0;
        }
        catch(IndexOutOfRangeException e)
        {
            Console.WriteLine("**ERROR:** There are no arguments provided specifying the desired branch to checkout!");
            return 1;
        }
    }
}
```

In the end, the **Jenkins** commands for this phase ended up like this, as shown below:

```groovy
    stage('Setup')
    {
        def cmd = Get('cmd');
        echo "Updating unity project by using git solution"
        dir("${env.UNITY_PROJECT_PATH}/git_solution/")
        {
            cmd.Do("dotnet run ${env.GIT_BRANCH_TO_CHECKOUT}");
        }
    }
```

### Build

As the name already suggests, this phase generates the Unity project's build. Unity offers us a way to interact with its editor by providing arguments from the command line. This simplifies the process when working to achieve continuous integration for your game. All the information about how you can interact with the Unity Editor using only command lines is available in [Unity's official documentation](https://docs.unity3d.com/Manual/EditorCommandLineArguments.html).

Building a Unity project via the command line looks like this:
``` shell
"C:\Program Files\Unity\Hub\Editor\<version>\Editor\Unity.exe" -quit -batchmode -projectPath "C:\path\for\Unity\Project" -executeMethod <Namespace.Class.MethodName>
```

Taking a look at the shell command above, the argument `-batchmode` implies that Unity will run in batch mode, and every interaction will be through command lines, ignoring any human action in the Editor. This also suppresses any pop-ups that might appear during the build. Following the next command, I informed the `projectPath` to open. In the end, when the project finishes loading, it executes the method passed in the `executeMethod` argument.

Before **Jenkins** starts the build, I would like to have control over more aspects of how the build will be generated. It would be great to increase the build version, specify whether it will be an **.apk** or **.aab**, indicate if it needs to split application binaries and configure other settings as my project grows. To encapsulate all these settings before the build, I have created [Unity Builder](https://github.com/lourenco-pedro/UnityBuilder), a package that streamlines the Unity build generation process by executing all necessary steps beforehand.

[Unity Builder](https://github.com/lourenco-pedro/UnityBuilder) has the method `UnityBuilder.BuildCmd.Build` that is passed in the `-executeMethod` argument in the shell command above. When running this method, Unity loads the `ScriptableObject` `BuildCmd`, which holds every configuration defined by the user on how it needs to be generated. It also increases the version as programmed by the user.

Every build from [Unity Builder](https://github.com/lourenco-pedro/UnityBuilder) is saved in the path specified in the environment variable `UNITY_BUILDER_ROOT`. It is necessary to have this environment variable defined; otherwise, the build process will end with the result as `INVALID_ENVIRONMENTS`. `UNITY_BUILDER_ROOT` also has the **.json** file `versionSettings`:

```json
versionSettings
{
    "isRelease": false,
    "path": "Path from the last generated build"
}
```

The `isRelease` parameter informs whether `UnityBuilder` needs to increase the main version number or not (`v.MainVersionNumber.BuildNumber`, `v.1.1`). The `path` parameter indicates where the last build by [Unity Builder](https://github.com/lourenco-pedro/UnityBuilder) was stored. This field will later be used by **Jenkins** when publishing to Google Drive.

At the end, the **Jenkins** commands for this phase ended up like this, as shown below:
```groovy
stage('Build')
{
    echo "Building Unity project..."
    def cmd = Get('cmd');
    def unity = Get('unity');
    cmd.Do(unity.Build())
}
```

### Publish

In the final phase of the pipeline, at this moment, **Jenkins** sends the new fresh build to Google Drive. While I plan to use this phase in the future for uploading builds to stores, currently, for internal tests and since my project is in its early stages, I have opted to use only Google Drive. This way, it will be easier to share my builds with other people for testing purposes.

In this phase, I have created two more C# programs that make the process easy when uploading to Google Drive: the `drive_uploader` and `upload_build_to_drive`. The `drive_uploader` is a service that implements the Google Drive API. It works by simply running the following command: `dotnet run -file <path> -mime <type>`, where `<path>` is the path of the file that will be sent to Drive, and `<type>` is the type of file that will be sent. [This service is also available on my GitHub for free use](https://github.com/lourenco-pedro/driver_uploader).

Now, the `upload_build_to_drive` uses this service by loading the `versionSettings.json` mentioned above and passing the path of the recently generated build in the `<path>` argument. As for the `-mime` parameter, there are two values that can be passed:

- For **.aab** files, `<type>` will be `application/x-authorware-bin`
- For **.apk** files, `<type>` will be `application/vnd.android.package-archive`

At the end, the **Jenkins** commands for this phase ended up like this, as shown below:
```groovy
stage('Publish')
{
	def cmd = Get('cmd');
    echo "Publishing app to drive"
    dir(env.DRIVE_UPLOADER_PATH)
    {
	    cmd.Do("dotnet run");
	}
 }
```

## In conclusion

Several improvements can be applied during the presented process. But as a first attempt, I am happy with the result I managed to achieve. Now, for any change I make in my project that requires mobile testing, I can obtain it with just one click. This document was not written to teach how to create your own Continuous Integration and Continuous Delivery Pipeline, but rather to document the processes I performed to achieve this result. Feel free to provide any constructive feedback for improvements in what has been presented.

Thanks!

You can also find me in:
[LinkedIn](https://www.linkedin.com/in/pedro-pereira-7694b7166/)
[GitHub](https://github.com/lourenco-pedro)
[Youtube](https://www.youtube.com/channel/UC7YMkk46fdSzIUVpgcI9mWw)
[Lecture about OOP Applied in Games](https://www.youtube.com/watch?v=-Wg6S9pZUeQ&t=459s)