Implementing the Workflow
Setting up Continuous Integration with Unity and GitHub Actions — Part 3
Last time, we looked at getting our first workflow into GitHub and using it to obtain a Unity license file, which we stored in our GitHub secrets. Today we’re going to go over the details for implementing our Build workflow.
Setting up the build Workflow
There are a few steps to the process of setting up our build workflow.
- Create the main.yml file in our .github/workflows folder
- Add the name, on, and jobs workflow steps
- Define the platforms and the Unity version we are going to run our operations against.
- Add the checkout, cache, builder, and upload steps to our job
- Check in the file to our master branch (Depending on your repositories workflow, this may require checking it into a feature branch and then requesting the appropriate pull requests and approvals to get the file to the master branch before it will show up in the actions tab).
Note: make sure that you have included your games scenes into your build settings in Unity otherwise nothing will be built and you will be very confused like I was.
Creating our file and the top-level attributes
First we create our file, in this case we’re simply going to call it main.yml. Once it’s created, let’s add the following code to the file. Remember that tabs and spacing are important in YAML files.
The name property is self-explanatory and it will be displayed in the interface on GitHub. The on property is set up so that this workflow will trigger any time code is pushed into the master branch or pull requests are completed against the master branch. Lastly we add an empty workflow_dispatch node so that we can trigger this work flow manually if we need to. Then we start the list of jobs with the jobs keyword. We’re going to go over each of the jobs in turn next.
Note: I am still using the “master” branch as my primary branch. The default in GitHub is now “Main” you may need to adjust the branch name above to suit your setup. Also, checkout the GitHub documentation at https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions on other options and ways to customize when workflow files are triggered.
Define the platforms and the Unity version
At the start of our build definition, under the jobs tag, we need to define some job-level properties. Here is a look at what we need to define:
Our job definition starts with an identifier for the job, in our case build. Below that, the name property does what you would expect and provides a readable version of the job name for display in the GitHub interface. Next we have the runs-on keyword. This defines the platform for the virtual machine that
the job will be run on, in our case we will be running the job on a Linux based Ubuntu virtual machine with the latest version of the image that has been built in GitHub's provided VM’s.
The next section we have is our strategy section. This section provides details about how we want our job to run. The first option in our strategy section is fail-fast. This option tells our runner, that’s what the virtual machine and automation software is called, to stop running the entire job if any of the steps fail. By default, this option is on but here we turn it off because we are going to configure this job to run multiple times, once for each operating system and unity build version we want to build for. We don’t want a single failed job to stop all the remaining jobs.
The last section in our strategy section is the matrix section. In this section we can define arrays of options that our runner will loop over and create separate runs for. In our case, the runner will loop over each of our selected unity versions, indicated by the list under the unityVersion tag, and run a build for each of the listed in the array of options under the targetPlatform tag. For our purposes we are only using one Unity version but I specified it in the matrix because the default Unity version that our build step uses is different from the version I want to build against. I have also commented out some build platforms that I don’t currently want to build for but I did not remove them because I will want to build against them in the future. We comment out lines in YAML using the ‘#’ symbol.
With our matrix configures and our basic job info set up, we now need to be gin defining our steps. The first step we need to perform is to checkout a copy of our repository on the virtual machine for us to work with. GitHub provides an action for us to use for this.
We begin with our steps keyword followed by our first step definition. Again, remember that tabs and spaces matter as does the ‘-’ symbol next to the name keyword.
Like other sections in this file, our steps contains a name property for display in the GitHub interface. this step is then followed by the uses keyword which has the location of the Action from another repo that we want to use. In this case it’s the GitHub provided action for checking out our repository. lastly, the with section provides some configuration options for the checkout action. fetch-depth tells the action how many commits to fetch, in this case ‘0’ means all history for all branches and tags. The lfs option defines whether to download Git-lfs files. Since building games often requires large asset files I set this to true and make sure my repos are set to use LFS when I create them.
Because we are running multiple builds, one for each platform per Unity version, we will want to cache our Library folder for reuse between builds. The Library folder can often be very large and doesn’t tend to change from platform to platform. Caching the folder per Unity version can help save us a load of valuable build time.
The structure of this step, and indeed most of the steps, looks similar to the checkout step. We provide a human-readable name, we use the uses keyword to define an action from another repo to run, and we use with to provide the parameters for that action. This action is another one provided by GitHub. We provide the path to the folder we want to cache, the key we want to reference the cached version by, and the restore-keys property which is an ordered list of keys to fall back on if the action doesn’t find a copy of the requested folder in the cache.
This is where the meat of our job is going to happen. This job uses a job runner setup by Game.CI that installs a headless copy of Unity and performs a build based on the parameters setup in the with section.
This action is where we are putting our GitHub Secrets and matrix variable to work. We provide our Unity License that we built and stored in a GitHub Secret in our last article so that the installation of Unity on the runner is licensed and will run properly. Then we use our matrix variables to provide the Unity version, target platform, and construct a build name for each of the times the runner is executed and based on the parameters of the current execution.
Note: I am using an alpha build of the Unity Build action provided by Game.CI because at the time of writing the MacOS build requires features from the alpha version that have not yet been merged into the main repository for the Builder action. This is only necessary for MacOS builds that are targeted to be run on Big Sur, the current version of MacOS. More information on the Builder actions check Game.CI’s official documentation located at https://game.ci/docs/github/builder
Now that we have a build executed, we want to be able to access the files that were generated from each of the builds. To do this we need to upload the files from each iteration of the matrix as what GitHub refers to as Artifacts. These generated artifacts are stored with the build and are accessible from the screen that houses the results of each run of the action on the actions tab. By default, the Builder action in the previous step outputs its builds to a folder named build, and then we provided a buildName that aligns with the targetPlatform from the strategy matrix. As such, we are providing the location to the upload step based on those parameters.
The Artifacts are listed below the Action run results on the GitHub Actions tab (once the action is in the default branch for the repository and operating).
The completed file
Here are the contents of the completed file for reference
Checking in the file and triggering a build
Now that our file is completed for the Continuous Integration piece of our pipeline is complete we need to check it into our repository and make sure it ends up in our master branch (which may require a series of pull requests, approvals, and merges if you are working on a team or following that pattern in your repository). Once it’s on the master branch any pushes or pull requests to the master branch will trigger this workflow.
That’s it for the Continuous Integration portion of our build pipeline! While it is possible, and in fact recommended, to run Unit Tests prior to our build as part of the Integration pipeline, Unit Testing is an entire subject to itself. Keep watching this space for a write-up on unit testing in the future but it is outside the scope of this series of articles. Next time we will begin to look at adding the Continuous Delivery portion of our CI/CD pipeline in GitHub actions! If you enjoyed this article, or want to come along with me as I progress on my journey, follow me at gamedevchris.medium.com.