Skip to content

Instantly share code, notes, and snippets.

@shichen85
Last active July 22, 2024 14:24
Show Gist options
  • Save shichen85/887d237cdc4338fa3f4e4749a14990db to your computer and use it in GitHub Desktop.
Save shichen85/887d237cdc4338fa3f4e4749a14990db to your computer and use it in GitHub Desktop.
Buttery Smooth Tech: Automate Your Builds in GitHub Actions

Buttery Smooth Tech: Automate Your Builds in GitHub Actions

Hello, Game Makers! This is Shi Chen from Butterscotch Shenanigans. I work on tools and pipelines to smoothly ship our games to all the platforms we publish on. And we publish on a lot of platforms! Our most cross-platform title, Levelhead, required 7 different builds to be made at least once per day. Without a way to automate those builds, our lives would have been very difficult.

Today, I want to share how we use GitHub Actions as build automation servers to make builds for our upcoming game, Crashlands 2. Build automation servers, or more fancily named, continuous integration and continuous deployment (abbreviated as “CI/CD”) servers, can liberate you from the tedious button-clicking and waiting when you build your project. Plus, they can run tests and upload builds to stores seamlessly, saving you countless hours and reducing human errors.

If your project is already stored on GitHub, like ours, GitHub Actions is easy to integrate and inexpensive to start (it's free for all public repositories!). We have open-sourced the tools we use, so you can build your GameMaker projects too. If you are already familiar with GitHub Actions, check out the demo to try it out yourself. Otherwise, I will walk you through how to set it up to automatically make Windows builds in the first part of the article. Later, I will discuss how to make builds for Android and iOS, automatically test the builds, and upload them to stores. Lastly, I will discuss the cost and limitations of using GitHub Actions.

Our tools bscotch/igor-setup and  bscotch/igor-build only support building for Windows, Android, and iOS exports. However, they are fully open-source, so more export support can be added via pull request or forking.

Setting Up to Make Windows Builds

Prerequisites

Before we get started, you'll need the following:

If your project uses the GMEXT-Steamworks extension, you need to delete the extensions\steamworks\post_build_step.bat file and use the Extension Editor to add the Steam SDK's sdk\redistributable_bin\win64\steam_api64.dll file to the extension. This allows the project to build without having to set up the Steam SDK in the build automation server.

steam

Setting Up the Github Actions Workflow

If an automation server is like a chef in a restaurant, then the GitHub Actions Workflow (which I will simply refer as “workflow” in the remainder of this article) is the recipe the chef follows to create a delicious dish. For our dish, the Windows build, we need to do the following to set up the workflow:

  1. Generate a GameMaker Access Key: Go to GameMaker’s Access Key page to generate a GameMaker Access Key, which allows us to set up Igor – the GameMaker Command Line build executable – later in the build automation server. 

  2. Create a GitHub Repository: If you haven't already, create a new GitHub repository for your GameMaker project, and upload your GameMaker project files to this repository. Check out the official GitHub guide if you need some help!

  3. Set Up Secrets: In your GitHub repository, navigate to Settings > Secrets and variables > Actions. Add a new secret named ACCESS_KEY with the GameMaker Access Key you just generated.

  4. Add the workflow File: In your repository, create a new directory called .github/workflows. Inside this directory, create a file named ci.yml, which is the abbreviation for “Continuous Integration”. Again, check out this other official GitHub guide if you need some help.

  5. Edit the workflow File: In this file, we will code up a Job to achieve the following goals:

    1. Checkout The Repository: We will use the actions/checkout action to check out your repository to access the GameMake project.

    2. Find the yyp File: We will use a PowerShell script to locate the GameMaker project file (*.yyp) and save its path.

    3. Set Up Igor: We will use the bscotch/igor-setup action to set up the GameMaker Command Line build executable Igor.

    4. Build with Igor: We will use the bscotch/igor-build action to build your GameMaker project for the Windows export.

    5. Upload Build Artifact: We will use the actions/upload-artifact action to upload the Windows build so you can download it later. 

A sample workflow that does all of that is shown below, though you'll need to make edits for the specifics of your project. I have indicated them with the 📝 emoji:

name: Continuous Integration for Windows export

on: 
  # push: #Uncomment this line to run the workflow on push
  workflow_dispatch: #This line allows you to run the workflow manually from the GitHub Actions page
  workflow_call: #This line allows you to run the workflow from another workflow

jobs:
  build:
    runs-on: windows-2022
    steps:
      # Check out the repository with the GameMaker project
      - uses: actions/checkout@v4
        with:
          lfs: true
      # This step finds the yyp file in the repository and saves the path to an output
      - id: find_yyp
        name: Find the yyp file
        run: |
          $yyp = Get-ChildItem -Path ${{ github.workspace }} -Recurse -Filter *.yyp
          Write-Output "YYP file found at: $yyp"
          "yyp-path=$yyp" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
      # This step sets up the GameMaker build CLI tool Igor https://github.com/bscotch/igor-setup
      - name: use Igor Setup
        uses: bscotch/igor-setup@v1
        id: igor
        with:
          target-yyp: ${{ steps.find_yyp.outputs.yyp-path }}
          access-key: ${{ secrets.ACCESS_KEY }} # 📝 To generate your Access Key, check out the Access Key section of the GameMaker Manual's Building via Command Line page: https://manual.gamemaker.io/monthly/en/#t=Settings%2FBuilding_via_Command_Line.htm
      # This step uses Igor to build the GameMaker project https://github.com/bscotch/igor-build
      - name: use Igor build
        uses: bscotch/igor-build@v1
        id: build
        with:
          yyp-path: ${{ steps.find_yyp.outputs.yyp-path }}
          user-dir: ${{ steps.igor.outputs.user-dir }}
      # This step uploads the build to the artifacts, so you can download it from the GitHub Actions page or use it in another workflow
      - name: upload-build
        uses: actions/upload-artifact@v4
        with:
          name: igor-build-windows
          path: ${{ steps.build.outputs.out-dir }}
          retention-days: 1 # Longer retention days can incur additional charges. See https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts

Running the Workflow

To run the workflow manually:

  1. Go to the Actions tab in your GitHub repository.

  2. Select the Continuous Integration for Windows export workflow.

  3. Click the Run workflow button.

Manual Trigger

To run the workflow automatically when you push any commits to this repository, uncomment the line # push: in the ci.yml file.

Using the Build Artifacts

Once the workflow completes, you'll find the build artifacts in the Actions tab. Click on the completed workflow run, and you'll see an option to download the build artifact named igor-build-windows.

Artifact

Mobile Builds, Testing, and Uploading

While the above workflow works great for Windows builds, you can also take it further to make mobile builds, test them, and upload them to stores like Steam, Google Play, and Apple App Store. These tasks can get pretty complicated, so I can't fully explain everything here. However, the following sections provide tips, tricks, references for accomplishing these tasks, and you can also find examples in our GameMaker runtime testing repo Ganary.

Building for iOS 

When you make iOS builds in the GameMaker IDE, you must have a MacOS device and set up XCode with your Apple Developer account’s certificate and provisioning profile. Then, the GameMaker IDE essentially converts your GameMaker project into an XCode project and builds it on the MacOS device.

To make iOS builds in GitHub Actions, you need to import your certificate and provisioning profile to the build environment. There are a lot of solutions such as Fastlane, Apple-Actions/import-codesign-certs, and yukiarrr/ios-build-action. We chose Fastlane as it is already installed in the default GitHub Actions environment, streamlines the setup process, and also has other useful features such as building the XCode project and uploading to Testflight.

On a high level, our implementation contains the following steps: 

  1. On your local machine, follow Fastlane’s guide to set up the Match action. This allows you to import the certificate and provisioning profile later in the GitHub Actions environment.

  2. Store the Match setup’s MATCH_PASSWORD and MATCH_GIT_BASIC_AUTHORIZATION in your GitHub repo’s secrets.

  3. The GitHub Actions workflow will need to:

    1. Use the MacOS runner.

    2. Mostly follow the same steps as the Windows workflow, but this one will create the XCode project and expose its location.

    3. Create a fastlane/Fastfile which instructs Fastlane to import the certificate and provisioning profiles and build the project.

    4. Run Fastlane with the secrets MATCH_PASSWORD and MATCH_GIT_BASIC_AUTHORIZATION as environment variables to build the export *.ipa file.

Here is what the workflow file should look like with the 📝 emoji indicating where you should edit:

name: Continuous Integration for iOS export

on: 
  # push: #Uncomment this line to run the workflow on push
  workflow_dispatch: #This line allows you to run the workflow manually from the GitHub Actions page
  workflow_call: #This line allows you to run the workflow from another workflow

jobs:
  Build:
    runs-on: macOS-latest
    steps:
      # Check out the repository with the GameMaker project
      - uses: actions/checkout@v4
        with:
          lfs: true
      # This step finds the yyp file in the repository and saves the path to an output
      - id: find_yyp
        name: Find the yyp file
        run: |
          $yyp = Get-ChildItem -Path ${{ github.workspace }} -Recurse -Filter *.yyp
          Write-Output "YYP file found at: $yyp"
          "yyp-path=$yyp" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
      # This step sets up the GameMaker build CLI tool Igor https://github.com/bscotch/igor-setup
      - name: use Igor Setup
        uses: bscotch/igor-setup@v1
        id: igor
        with:
          target-yyp: ${{ steps.find_yyp.outputs.yyp-path }}
          access-key: ${{ secrets.ACCESS_KEY }} # 📝 To generate your Access Key, check out the Access Key section of the GameMaker Manual's Building via Command Line page: https://manual.gamemaker.io/monthly/en/#t=Settings%2FBuilding_via_Command_Line.htm
      # This step uses Igor to build the GameMaker project https://github.com/bscotch/igor-build
      - name: use Igor build
        uses: bscotch/igor-build@v1
        id: build
        with:
          yyp-path: ${{ steps.find_yyp.outputs.yyp-path }}
          user-dir: ${{ steps.igor.outputs.user-dir }}
      # This step sets up the Fastlane fast file
      - name: Set up the Fastlane fast file
        working-directory: ${{ steps.build.outputs.out-dir }}
        run: |
          # Create the fastlane/Fastfile with the following content
          mkdir -p fastlane
          cat << EOF > fastlane/Fastfile
          lane :beta do
            setup_ci
            matchType = "appstore"
            bundleId = "YOUR BUNDLE NAME" #📝Use your iOS Game Options’s bundle name
            match(
              type: matchType,
              git_url:"YOUR MATCH SETUP'S GIT REPO URL", #📝Use your Match setup’s Git repo url
              storage_mode: "git",
              shallow_clone: "true",
              app_identifier: bundleId,
              readonly: "true",
            )
            update_code_signing_settings(
              use_automatic_signing: false,
              team_id: ENV["sigh_#{bundleId}_#{matchType}_team-id"],
              profile_name: ENV["sigh_#{bundleId}_#{matchType}_profile-name"],
              code_sign_identity: "iPhone Distribution"
            )
            build_app
          end
          EOF
      # This step runs Fastlane to build the XCode project
      - name: Run Fastlane to build the XCode project
        working-directory: ${{ steps.build.outputs.out-dir }}
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} # 📝 This is the password for the match repo, see https://docs.fastlane.tools/actions/match/#:~:text=for%20each%20app.-,Passphrase,-Git%20Repo%20storage
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} # 📝 This is the basic authorization for the match repo, see https://docs.fastlane.tools/actions/match/#:~:text=MATCH_GIT_BASIC_AUTHORIZATION
        run: |
          fastlane beta
      - name: upload build as artifact
        uses: actions/upload-artifact@v4
        with:
          name: igor-build-macOS
          path: ${{ steps.build.outputs.out-dir }}
          retention-days: 1

After running the workflow, both the *.ipa file and the XCode project will be uploaded as the artifact for you to download.

In later parts of the article, we will discuss how to run automated test on this *.ipa file and upload it to Testflight.

Building for Android

Similar to building iOS, building for Android requires importing your code-signing secret, the Keystore File, to the GitHub Actions environment. In addition, we need to expose the Keystore File’s location, alias, and password to Igor (GameMaker's Command Line build tool).

On a high level, our implementation contains the following steps: 

  1. On your local machine, create the Keystore File, its alias, and password as mentioned in the GameMaker guide Setting Up for Android.

  2. Locate the Keystore file and follow this guide to make it available in the GitHub environment as a secret.

  3. Locate the local_settings.json file, which stores your IDE preferences and the Android SDK configuration. Copy the entries "machine.Platform Settings.Android.Keystore.keystore_password" and "machine.Platform Settings.Android.Keystore.alias"'s values and store them as GitHub secrets.

  1. The GitHub Actions workflow will need to:

    1. Use the Ubuntu runner.

    2. Add a step to recreate the Keystore file and expose its location.

    3. Add a step to create a local_settings.json file that includes the path to the recreated keystore file, its alias, and password by populating the entries:

      • "machine.Platform Settings.Android.Keystore.filename": the path to the recreated keystore file

      • "machine.Platform Settings.Android.Keystore.keystore_password": the value you previously stored in secret

      • "machine.Platform Settings.Android.Keystore.keystore_alias_password": same value as above

      • "machine.Platform Settings.Android.Keystore.alias": the value you previously stored in secret

    4. In the step that uses bscotch/igor-setup@v1, add the local-settings-override-file option to point to the local_settings.json file.

    5. Set up ffmpeg and Android SDK's platform tools, as these two are not included in the default GitHub Actions environment or GameMaker runtime.

Here is what the workflow file should look like with the 📝 emoji indicating where you should edit:

name: Continuous Integration for Android export

on: 
  # push: #Uncomment this line to run the workflow on push
  workflow_dispatch: #This line allows you to run the workflow manually from the GitHub Actions page
  workflow_call: #This line allows you to run the workflow from another workflow

jobs:
  Build:
    runs-on: ubuntu-latest
    steps:
      # Check out the repository with the GameMaker project
      - uses: actions/checkout@v4
        with:
          lfs: true
      # This step finds the yyp file in the repository and saves the path to an output
      - id: find_yyp
        name: Find the yyp file
        run: |
          yyp=$(find ${{ github.workspace }} -name "*.yyp")
          echo "YYP file found at: $yyp"
          echo "yyp-path=$yyp" >> $GITHUB_OUTPUT
      #region Android setup
      - name: Create the keystore file from secrets
        id: write_file
        uses: timheuer/base64-to-file@v1.2
        with:
          fileName: 'myTemporaryFile.keystore'
          encodedString: ${{ secrets.KEYSTORE_BASE64 }} # 📝 Your keystore file encoded
      - name: Create the local-settings-override-file for igor-setup
        run: |
          echo '{
          "machine.Platform Settings.Android.Keystore.filename": "${{ steps.write_file.outputs.filePath }}"
          "machine.Platform Settings.Android.Keystore.keystore_password": "${{ secrets.KEYSTORE_PASSWORD }}", # 📝 Your keystore password
          "machine.Platform Settings.Android.Keystore.keystore_alias_password": "${{ secrets.KEYSTORE_PASSWORD }}", # 📝 Your keystore password
          "machine.Platform Settings.Android.Keystore.alias": "${{ secrets.KEYSTORE_USERNAME }}" # 📝 Your keystore alias
          }' \
          > local_settings.json
      - name: Set up ffmpeg # This step may be removed when https://github.com/YoYoGames/GameMaker-Bugs/issues/4977 is fixed
        uses: FedericoCarboni/setup-ffmpeg@v3
        with:
          ffmpeg-version: '6.1.0'
      - name: Set Up Android SDK's platform-tools # The default Android SDK does not include platform-tools and its component `adb`, which is required by Igor for the Android build
        run: |
          ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager \
          --sdk_root=$ANDROID_SDK_ROOT \
          "platform-tools"
      #endregion
      # This step sets up the GameMaker build CLI tool Igor https://github.com/bscotch/igor-setup
      - name: use Igor Setup
        uses: bscotch/igor-setup@v1
        id: igor
        with:
          local-settings-override-file: ${{ github.workspace }}/local_settings.json
          target-yyp: ${{ steps.find_yyp.outputs.yyp-path }}
          access-key: ${{ secrets.ACCESS_KEY }} # 📝 To generate your Access Key, check out the Access Key section of the GameMaker Manual's Building via Command Line page: https://manual.gamemaker.io/monthly/en/#t=Settings%2FBuilding_via_Command_Line.htm
      # This step uses Igor to build the GameMaker project https://github.com/bscotch/igor-build
      - name: use Igor build
        uses: bscotch/igor-build@v1
        id: build
        with:
          yyp-path: ${{ steps.find_yyp.outputs.yyp-path }}
          user-dir: ${{ steps.igor.outputs.user-dir }}
      - name: upload build as artifact
        uses: actions/upload-artifact@v4
        with:
          name: igor-build-ubuntu
          path: ${{ steps.build.outputs.out-dir }}
          retention-days: 1

After running the workflow, the *.aab file will be uploaded as the artifact for you to download.

Automatically Test the Builds

Another important advantage of using build automation servers is that you can set up automated tests for your builds before you even ship them to your team members or players. The automated tests can be as simple or as complicated as you want, but they should launch without requiring any keyboard/mouse/touchscreen input, and the test results should be accessible for your workflow to decide whether to fail the build or ship it. In our GameMaker runtime testing workflow, we run automated tests on the Windows, Android, and iOS builds using Olympus, our GameMaker test framework:

  1. For Windows: Once the build is made, we launch the *.exe file with Command Line Parameters, which Olympus parses to launch the game into the testing loop. Once the loop completes, the result is output to a file for the workflow to evaluate.

  2. For iOS and Android: We deploy the builds to Firebase Test Lab. Similar to parsing the Command Line Parameters on Windows, Olympus uses an extension to detect whether the game is launched in Firebase Test Lab and should go to the testing loop. Once the loop completes, the game will either exit normally to flag the test result as passed, or it will throw an error to fail the test. The test result is then evaluated by the workflow.

It is possible to deploy and test mobile builds on simulators, which the GitHub hosted runners already include. We chose Firebase Test Lab as it has a free tier, offers both simulator and physical devices for running tests, and the android test can also be used for Google Play’s pre-launch report.

Uploading Builds to Steam, Google Play, and iOS TestFlight

Leveraging the vibrant GitHub Marketplace and the broader open-source community, we can use off-the-shelf solutions to upload builds to distribution platforms like Steam, Google Play, and iOS TestFlight:

Cost and Caveats

GitHub Actions is a powerful tool for automating builds, testing, and deployment, but there are still costs and limitations to consider.

Cost

GitHub Actions offers a good amount of free minutes for every user, so you won't have to worry about the cost when you are just getting started. However, if you have a sizable project that builds frequently, you will want to be more conscious about your workflows.

As an example, our current project, Crashlands 2, is a 5GB (and growing!) project. Our workflow makes two Windows builds (one for the full game version and another for the demo version), two Android builds (one for QA and one for publishing partner), and one iOS build. These builds are then automatically tested, uploaded to various distribution platforms, and notify team members for further testing. For such a workflow, here is the rough cost breakdown:

Billable Minutes Per-minute rate (USD) Total
Windows full 31 $0.016 $0.50
Windows demo 28 $0.016 $0.45
Android QA 19 $0.008 $0.15
Android publisher 20 $0.008 $0.16
iOS 11 $0.08 $0.88
Sum $2.14

Compared to buying and managing your own hardware, or the downtime cost of waiting for your game to compile, spending $2.14 to get all of that (5 builds, automated testing, automated deployment) is a good deal!

But the costs can definitely add up if you're making a lot of builds. Fortunately, there are plenty of ways to optimize the workflow to cut costs, and you can self-host the runners or make your project public, as either of these two options exempts you from GitHub Actions billing.

Caveats

  • Using GitHub Actions involves a lot of secrets and running third party actions, so you should familiarize yourself with good safety practices to secure your workflows.

  • Over time, third-party actions that you depend on can change and break your workflow. Lock in a version by targeting the action's SHA value. You can also fork the action's repository, modify it to fit your project, and use your forked version instead.

  • When you are actively developing a workflow for a private repository, consider setting up a local development environment so you can iterate faster and cheaper. nektos/act is a wonderful tool to run your workflow locally instead of having to upload it to your private repository, which will incur costs.

  • GitHub hosted runners have their own physical limitations in terms of RAM and Storage. If your workflow takes a long time to run and fails, it might be that the project requires more resources than the runners provide. You will then need to use larger runners or self-hosted runners.

  • Lastly, GitHub Actions can be unavailable due to service outage from time to time. Since we started using GitHub Actions in 2021, we have only encountered a handful of times where service outages happened during our working hours, but they were quick to resolve. You can subscribe to the service status update at https://www.githubstatus.com/.

Conclusion

Whether you're new to continuous integration or already automating your builds, I hope you found something useful in this article. I've cross-posted this article on GitHub Gist, where you can leave comments and feedback. I'd love to hear if the countless hours I spent figuring all of this out saved you some time! Any other comments/questions are also welcome. And if you have your own solutions to automating GameMaker builds, I'd love to hear about those as well!

Thanks for reading. You can check out our 100% GitHub-Actions-grown Crashlands 2 demo builds on Steam. Wishlisting it is the best way to support us, and we hope you enjoy the game just as much as the tech behind it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment