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 and 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.
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. 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 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:
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:
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.
Building a Unity project via the command line looks like this:
"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, a package that streamlines the Unity build generation process by executing all necessary steps beforehand.
Unity Builder 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 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
:
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 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:
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.
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 beapplication/x-authorware-bin
- For .apk files,
<type>
will beapplication/vnd.android.package-archive
At the end, the Jenkins commands for this phase ended up like this, as shown below:
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
GitHub
Youtube
Lecture about OOP Applied in Games
Top comments (0)