Skip to content

Instantly share code, notes, and snippets.

@killthekitten
Last active June 16, 2026 16:50
Show Gist options
  • Select an option

  • Save killthekitten/27f374b7ad723dd9d30f0678cd3f608e to your computer and use it in GitHub Desktop.

Select an option

Save killthekitten/27f374b7ad723dd9d30f0678cd3f608e to your computer and use it in GitHub Desktop.

Unity Build Automation Setup with GameCI + itch.io

This covers the GitHub Actions setup for automating Unity builds with GameCI and deploying them to itch.io with butler. Two workflow files do the work; everything is manually triggered.

Overview

The build and upload steps are separate so you can build without deploying, or re-deploy an old build without rebuilding. The upload workflow is reusable it can be called by the build workflow or triggered manually.

As next steps, I plan connecting this to GitHub Releases so that every push/merge to the main branch generates a draft of release notes (ie. with a release drafter) and a build artifact, and all I'd have to do afterwards is simply edit the release notes and hit a button.

Caveats and workarounds

Build profiles

I use a custom build profile. It generates an .asset file which you need to point to in BUILD_PROFILE_PATH. Not sure whether this code would work without one, but it's easy to create one and it's a good practice.

I had to override the scene list in my build profile, for some reason it wouldn't use the global scene list and fail.

image

First deployment is manual

You would need to create the itch.io page beforehand, publish at least one HTML release and mark it as such. This is a limitation of itch.io API.

More info:

Builds can be slow

First build can take a lot of time (1 hour for me) and fail for various reasons. Be ready that you'll waste some time fixing those failures. After the first successful build, the subsequent builds become a lot faster (mine is currently at 15 minutes) thanks to caching. If all your builds take equally long, check the logs of the caching action – could be that you're using a wrong cache key.

In addition to that, my builds were always failing at first. I suppose that's because of using too much memory, which has something to do with Unity 6 WebGL builds. My investigation brought me to this topic: WebGL build takes a lot of time.

Setting m_CodeOptimization: 2 in the build profile helped me resolve the issue. This is the WasmCodeOptimization enum that corresponds to Disk Size instead of the standard Disk Size with LTO. This can be edited manually in the profile's .asset file, or selected from a dropdown in Unity:

image

Other build targets are work in progress

I haven't tested the build script with the other platforms (mac, windows etc), even though they are present in the GitHub Action dropdown.

If it fails, most likely it would be an easy fix: the "build/${{ inputs.targetPlatform }}/${{ inputs.targetPlatform }}" part of the butler call will have to change to point to the actual build folder that corresponds to the target platform.

Other engines

GameCI is engine agnostic but the happy path exists only for Unity users. Godot and Unreal are supported via GameCI CLI, so theoretically you could roll your own setup.

More info:

Secrets and Variables

Before the workflows run, you need to set up the following Secrets and Variables under repository's Settings → Secrets and variables → Actions:

Name Type Value
UNITY_LICENSE Secret Contents of your Unity .ulf license file
UNITY_EMAIL Secret Unity account email
UNITY_PASSWORD Secret Unity account password
ITCH_API_KEY Secret API key from itch.io → Settings → API keys
ITCH_USERNAME Variable Your itch.io username
ITCH_SLUG Variable The game's URL slug on itch.io (e.g. coiled-kingdoms)
BUILD_PROFILE_PATH Variable A path to the build profile asset, i.e. Assets/Settings/Build Profiles/Web - Desktop - Release.asset

Note on credentials

The Unity credentials are required by GameCI to activate a license on the runner. You get the .ulf file by running the GameCI license activation workflow once. More details you can find in GameCI Activation Guide.

I am always suspicious to third-party software that uses such sensitive credentials, but at the end of the day this is on Unity for not offering a better CI integration on a Free license. A common practice in this case is to use a "robotic" account, i.e. an email/password pair that only exists for the purpose of being used on CI, but it's a legal grey zone until proven otherwise.

Workflow 1: Build the game

File: .github/workflows/build-unity-project.yaml
Trigger: Manual (workflow_dispatch) from the Actions tab.

name: Build the game
run-name: Building the `${{ github.ref }}` branch (${{ inputs.targetPlatform }})
on:
  workflow_dispatch:
    inputs:
      targetPlatform:
        description: Target platform
        required: true
        default: WebGL
        type: choice
        options:
          - WebGL
          - StandaloneWindows64
          - StandaloneOSX
          - StandaloneLinux64
          - Android
          - iOS
      upload_to_itch:
        description: Upload to itch.io after build
        type: boolean
        default: false
      itch_channel:
        description: itch.io channel (only used when uploading)
        default: html5
        required: false
jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub."

      - name: Free disk space
        uses: jlumbroso/free-disk-space@v1.3.1
        with:
          tool-cache: false
          android: true
          dotnet: true
          haskell: true
          large-packages: true
          docker-images: true
          swap-storage: true

      - name: Check out repository code
        uses: actions/checkout@v6
        with:
          ref: ${{ github.ref }}

      - name: Create LFS file list
        run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id

      - name: Restore LFS cache
        uses: actions/cache@v5
        id: lfs-cache
        with:
          path: .git/lfs
          key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}

      - name: Git LFS Pull
        run: |
          git lfs pull
          git add .
          git reset --hard

      - uses: actions/cache@v5
        with:
          path: Library
          key: Library-${{ inputs.targetPlatform }}-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
          restore-keys: |
            Library-${{ inputs.targetPlatform }}-

      ## Uncomment this if you have tests in your project
      # - name: Run tests
      #   uses: game-ci/unity-test-runner@v4
      #   env:
      #     UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
      #     UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
      #     UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
      #   with:
      #     githubToken: ${{ secrets.GITHUB_TOKEN }}

      - name: Build project
        uses: game-ci/unity-builder@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
        with:
          targetPlatform: ${{ inputs.targetPlatform }}
          buildProfile: ${{ vars.BUILD_PROFILE_PATH }}

      - name: Upload the build as artifact
        uses: actions/upload-artifact@v7
        with:
          name: Build-${{ inputs.targetPlatform }}-${{ github.run_id }}
          path: build

  upload-to-itch:
    needs: setup
    if: ${{ inputs.upload_to_itch }}
    permissions:
      actions: read
    uses: ./.github/workflows/upload-build-to-itch.yaml
    with:
      run_id: ${{ github.run_id }}
      targetPlatform: ${{ inputs.targetPlatform }}
      channel: ${{ inputs.itch_channel }}
    secrets: inherit

What each step does

  • Free disk space — GitHub's ubuntu-latest runners only have ~14 GB free. Unity builds (especially WebGL) need more. This action removes Android SDK, .NET SDKs, Haskell, large apt packages, and Docker images to reclaim ~10–12 GB before the build starts. Paranoid disclaimer: it's another third-party action that has too much control over your build.

  • Checkout — pulls code and LFS (if you have it configured) from GitHub.

  • LFS cache — creates a fingerprint of all LFS files, then caches .git/lfs by that hash. On re-runs where assets haven't changed, LFS files are restored from cache instead of re-downloaded from the remote. git lfs pull then checks out the actual file contents, and the git add . && git reset --hard ensures that the newline symbols are correctly normalized (I'm not sure if that step is necessary but theoretically this could resolve cache issues).

  • Library cache — caches Unity's Library/ folder, which is where Unity stores imported asset data. Re-importing from scratch on every run would add significant time. The cache key is per-platform (Library-WebGL-<hash>) because the same asset imports differently for WebGL vs Windows. The restore-keys fallback (Library-WebGL-) means a partial cache hit (e.g. a new asset was added) still reuses most of the existing imported state.

  • Build project — uses GameCI's unity-builder. It runs Unity headlessly in a Docker container with the right Unity version. buildProfile points to a custom Build Profile asset in the repo that configures compression, optimization, and target scenes for the web release.

  • Upload artifact — stores the build output as a GitHub Actions artifact named Build-WebGL-<run_id>. The run ID in the name lets the upload workflow fetch the right artifact later. Artifacts expire after 90 days by default.

  • upload-to-itch (job) — only runs if upload_to_itch was checked. It calls the second workflow as a reusable workflow (workflow_call), passing the run ID, platform, and channel through. secrets: inherit forwards all secrets so the upload workflow can use ITCH_API_KEY.

Workflow 2: Upload build to itch.io

File: .github/workflows/upload-build-to-itch.yaml
Trigger: Called automatically by workflow 1, OR triggered manually with a specific run ID.

The manual trigger is useful when you want to deploy to itch at a convenient time, or re-push an old build.

name: Upload build to itch
run-name: Uploading ${{ inputs.targetPlatform }} build from run `${{ inputs.run_id }}` to itch.io
on:
  workflow_dispatch:
    inputs:
      run_id:
        description: ID of the "Build the game" run that produced the build artifact
        required: true
      targetPlatform:
        description: Target platform (must match the build run)
        required: true
        default: WebGL
        type: choice
        options:
          - WebGL
          - StandaloneWindows64
          - StandaloneOSX
          - StandaloneLinux64
          - Android
          - iOS
      channel:
        description: itch.io channel to push to
        default: html5
        required: true
  workflow_call:
    inputs:
      run_id:
        type: string
        required: true
      targetPlatform:
        type: string
        required: true
      channel:
        type: string
        required: true

jobs:
  itchio-upload:
    runs-on: ubuntu-latest
    permissions:
      actions: read
    steps:
      - name: Setup butler
        uses: remarkablegames/setup-butler@v3

      - name: Download build artifact
        uses: actions/download-artifact@v8
        with:
          name: Build-${{ inputs.targetPlatform }}-${{ inputs.run_id }}
          path: build
          run-id: ${{ inputs.run_id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      # https://itch.io/docs/butler/pushing.html
      - name: Upload to itch.io
        run: butler push "build/${{ inputs.targetPlatform }}/${{ inputs.targetPlatform }}" ${{ vars.ITCH_USERNAME }}/${{ vars.ITCH_SLUG }}:${{ inputs.channel }} --userversion ${{ inputs.run_id }}
        env:
          BUTLER_API_KEY: ${{ secrets.ITCH_API_KEY }}

What each step does

  • permissions: actions: read — required so the workflow can download artifacts from a different workflow run. Without this the download-artifact step gets a 403.

  • Setup butler — installs butler, itch.io's official upload CLI, via remarkablegames/setup-butler. This is simpler and more reliable than downloading butler manually with curl and a bit more secure than using an all-inclusive action like butler-publish-itchio-action (which is totally valid too and works perfectly fine).

  • Download build artifact — downloads the artifact from the build run using its run-id. The artifact name Build-WebGL-<run_id> must exactly match what the build workflow uploaded, which is why both workflows take targetPlatform as an input.

  • Upload to itch.io — calls butler push with:

    • the local build directory (build/WebGL/WebGL)
    • the target: username/game-slug:channel
    • --userversion set to the GitHub run ID (a monotonically increasing number, sufficient as a version string for itch.io)

Triggering a build

  1. Go to Actions → Build the game → Run workflow
  2. Choose the target platform (default: WebGL)
  3. Optionally check "Upload to itch.io after build" and set the channel
  4. Click Run workflow

To upload a previously built artifact to itch.io without rebuilding:

  1. Find the run ID of the build (visible in the URL when you open a past run)
  2. Go to Actions → Upload build to itch → Run workflow
  3. Paste the run ID, match the platform, set the channel
  4. Click Run workflow
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment