At some point you will push a change, forget to test it, and break something. Or you will spend ten minutes manually zipping a build and uploading it somewhere before you can share it with a tester. Or you will be three weeks into a project before realizing that two different people on the team have been building with different dependency versions and their outputs are subtly different in ways that are annoying to track down.
GitHub Actions solves all of that. You define workflows as YAML files that trigger on events like pushes, pull requests, or a schedule, and GitHub runs those steps on its own servers. No external tools, no separate accounts, everything lives in your repository.
I avoided GitHub Actions for longer than I should have because "CI/CD pipeline" sounded like enterprise infrastructure, not something a solo developer needed. Turns out a workflow that runs your tests and uploads a build artifact every time you push to main takes about fifteen minutes to write and saves a disproportionate amount of manual work over the course of a project. This guide covers how to write one from scratch, how to use secrets safely, and how to set up an automated Unity build — tying directly into the Git guide from this series.
How GitHub Actions Works
Every GitHub Actions workflow is a YAML file sitting in .github/workflows/ inside your repository. When a specified event happens — someone pushes code, opens a pull request, or a timer fires — GitHub reads that file and runs the steps it defines on a virtual machine it spins up specifically for that run.
Five terms you need before writing any YAML:
- Workflow — the whole thing. One YAML file, one workflow.
- Event — what triggers the workflow. A push, a pull request, a schedule, a manual button click.
- Job — a group of steps that run together on one virtual machine. Workflows can have multiple jobs running in parallel or in sequence.
- Step — one task inside a job. Either a shell command or a pre-built action from the marketplace.
- Runner — the virtual machine that runs the job. GitHub provides ubuntu-latest, windows-latest, and macos-latest for free.
That is the whole mental model. Everything else is details about how to configure these five things.
Part 1: Your First Workflow
Create this folder structure in your repository: .github/workflows/
Inside it, create a file called ci.yml. Paste this:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Print a message
run: echo "The workflow ran successfully"
Push this file to your repository. Go to the Actions tab on GitHub. You will see a workflow run appear, go yellow (running), then green (succeeded). That is GitHub Actions working. The actual steps do nothing useful yet — that is fine. You have confirmed the plumbing works before writing anything that matters.
Breaking Down the YAML
name: CI sets the display name in the Actions tab. Name it whatever makes sense for what it does.
on: push/pull_request defines the trigger. This workflow runs when code is pushed to main or when a pull request targets main. You can add more events or change these to whatever makes sense for your project.
runs-on: ubuntu-latest tells GitHub which virtual machine to use. Ubuntu is the fastest and cheapest for most workflows. Use windows-latest or macos-latest only when you specifically need those environments.
uses: actions/checkout@v4 is a pre-built action that checks out your repository's code onto the runner. Almost every workflow starts with this step — without it, the runner is a blank machine with none of your files on it.
run: echo "..." runs a shell command directly. Any command you could run in a terminal works here.
Part 2: A Real Workflow — Running Tests
The most common first use of GitHub Actions is running tests automatically on every push, so broken code cannot be merged without someone noticing. Here is a workflow for a Node.js project:
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
uses: actions/setup-node@v4 is another pre-built action, this one installs a specific Node.js version. The with: block passes parameters to it — the version and whether to cache dependencies. Caching is worth enabling on anything that installs packages, because it can cut minutes off your run time by not re-downloading the same packages on every run.
npm ci instead of npm install is intentional. npm ci installs exactly what is in the lockfile, which is what you want on a CI runner. npm install can update the lockfile and introduce version drift between runs.
For a Python project, replace the setup-node step with actions/setup-python@v5 and the npm commands with pip and pytest. The structure is identical — just different setup actions and commands.
Part 3: Using Secrets Safely
Secrets are credentials, API keys, and other sensitive values that your workflow needs but absolutely cannot be hardcoded in the YAML file. A YAML file in a public repository is public. Putting an API key directly in it means that key is visible to everyone on the internet.
GitHub Secrets solve this. They are stored encrypted in your repository settings and injected as environment variables into your workflow at runtime.
Adding a Secret
- Go to your repository on GitHub.
- Click Settings > Secrets and variables > Actions.
- Click New repository secret.
- Name it (all caps, underscores: MY_API_KEY) and paste the value.
- Save it.
Using a Secret in a Workflow
- name: Deploy
env:
API_KEY: ${{ secrets.MY_API_KEY }}
run: ./deploy.sh
${{ secrets.MY_API_KEY }} is the syntax for referencing a secret. GitHub replaces it with the actual value at runtime and masks it in the workflow logs — if the value is accidentally printed to the log, it shows as *** instead of the real key.
Never echo a secret directly or pass it as a command-line argument in a way that appears in the process list. The masking in logs is a safety net, not a license to handle secrets carelessly.
Part 4: Saving Build Outputs as Artifacts
Running tests is useful. Having something to download after the workflow runs is even more useful — a compiled binary, a generated APK, a documentation site. GitHub Actions can save these as artifacts that appear in the workflow run summary and can be downloaded directly.
- name: Build project
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: ./dist/
retention-days: 7
actions/upload-artifact@v4 takes whatever is in the specified path and saves it to GitHub's artifact storage. After the workflow finishes, anyone with access to the repository can go to the workflow run and download the artifact directly from the browser. No separate file hosting, no manual uploads.
retention-days: 7 tells GitHub to delete the artifact after seven days. The default is 90 days. Artifacts count against your GitHub storage limit, so setting a sensible retention period matters if you run builds frequently.
Part 5: A Unity Build Workflow
This is the section specific to this blog's audience. Getting an automated Unity build running on GitHub Actions requires a few extra steps compared to a standard web project, mainly because Unity needs a license activated before it can build anything — even headlessly on a CI runner.
GameCI (game.ci) is the most widely used open-source collection of GitHub Actions for Unity. It handles license activation and deactivation automatically, runs in a Docker container with Unity pre-installed, and supports all major build targets.
Prerequisites
- Your Unity project is in a GitHub repository (covered in the Git guide).
- You have a Unity Personal or Pro license. Free Personal licenses work with GameCI.
- Git LFS is enabled if your project contains large binary files (textures, audio, scenes).
Step 1: Get Your Unity License File
GameCI needs your Unity license as a secret. To get it:
- Add this workflow file to your repository temporarily:
name: Acquire Activation File
on: [push]
jobs:
getManualLicenseFile:
runs-on: ubuntu-latest
steps:
- uses: game-ci/unity-request-activation-file@v2
- uses: actions/upload-artifact@v4
with:
name: Manual Activation File
path: ./*.alf
- After the workflow runs, download the .alf artifact from the run summary.
- Go to license.unity3d.com, upload the .alf file, and download the resulting .ulf license file.
- Add your license file content as a GitHub Secret named UNITY_LICENSE.
- Add your Unity email as UNITY_EMAIL and password as UNITY_PASSWORD.
- Delete the temporary activation workflow file.
Step 2: The Build Workflow
name: Unity Build
on:
push:
branches: [main]
jobs:
build:
name: Build for ${{ matrix.targetPlatform }}
runs-on: ubuntu-latest
strategy:
matrix:
targetPlatform:
- StandaloneWindows64
- Android
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- uses: actions/cache@v4
with:
path: Library
key: Library-${{ matrix.targetPlatform }}-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-${{ matrix.targetPlatform }}-
Library-
- uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
targetPlatform: ${{ matrix.targetPlatform }}
- uses: actions/upload-artifact@v4
with:
name: Build-${{ matrix.targetPlatform }}
path: build/${{ matrix.targetPlatform }}
What This Workflow Does
strategy: matrix: targetPlatform: runs the job once for each platform listed, in parallel. Both StandaloneWindows64 and Android builds run at the same time rather than sequentially, halving the total time compared to running them one after another.
lfs: true on the checkout step pulls Git LFS objects during the checkout. Without this, large binary assets from LFS are missing from the build and Unity throws import errors.
actions/cache@v4 caches Unity's Library folder between runs. Unity regenerates this folder from scratch if it is missing, which takes several minutes on a large project. Caching it cuts build time significantly after the first run. The cache key includes hashes of your Assets, Packages, and ProjectSettings directories, so it invalidates correctly when those change.
game-ci/unity-builder@v4 handles the actual build. It activates the license, runs Unity headlessly with the correct build target, and deactivates the license when done.
Free Tier and Pricing
GitHub Actions is free for public repositories and gives private repositories 2,000 free minutes per month on the Free plan. Minutes are consumed based on the runner type — ubuntu-latest counts at 1x, windows-latest at 2x, and macos-latest at 10x. A Unity build that takes ten minutes on ubuntu-latest costs ten minutes. The same build on macos-latest costs 100 minutes.
For a solo developer with one or two Unity projects, 2,000 minutes per month is usually enough if you are building on Linux. IL2CPP builds targeting Windows or macOS are CPU-intensive, and GitHub-hosted runner minutes can add up fast on large projects — self-hosted runners are worth considering once build frequency scales up. A self-hosted runner is just a machine you own or rent that registers itself with GitHub and runs workflows locally instead of on GitHub's infrastructure.
Paid plans for GitHub Actions exist as part of GitHub's overall plans (Pro, Team, Enterprise) and add more monthly minutes rather than changing the feature set.
Things That Will Trip You Up
The YAML indentation is wrong and the workflow fails to parse.
YAML is whitespace-sensitive and GitHub's error messages for YAML syntax errors are not always pinpointing the exact problem. Use a YAML linter (yamllint.com or the YAML extension in VS Code) to validate your workflow file before pushing. Two-space indentation everywhere is the convention — mixing spaces and tabs causes parse errors that are invisible to the human eye.
The workflow runs but steps fail with "command not found."
The runner is a fresh Ubuntu/Windows/macOS machine with no custom tools pre-installed. Anything beyond the basics needs a setup step. If you need Python, add actions/setup-python. If you need the Unity Editor, use game-ci. Check what is pre-installed on each runner type in GitHub's documentation before assuming a tool is available.
Secrets are showing as empty strings in the workflow.
Secret names are case-sensitive. UNITY_LICENSE and unity_license are different secrets. Double-check the name in Settings matches exactly what you wrote in the YAML. Also confirm the secret was saved to the repository, not to an organization or environment with different access rules.
The Unity build fails with a license activation error.
Either UNITY_EMAIL, UNITY_PASSWORD, or UNITY_LICENSE is wrong, or the license has expired or been revoked. Unity Personal licenses require periodic reactivation. If the build was working and suddenly starts failing, check when the license was last activated and whether it needs renewal.
The cache is not being used between runs.
The cache key needs to match exactly for a cache hit to occur. If your Assets directory changed since the last run, the key changes and the cache misses — this is correct behavior, not a bug. If the cache is never being used even on unchanged code, check whether the cache key expression is evaluating correctly by looking at the "Post cache" step output in the workflow log.
What to Build Next
The workflows in this guide cover the basics. Once they are running reliably, a few natural extensions:
Automatic releases. Add a step using softprops/action-gh-release that creates a GitHub Release and attaches the build artifacts whenever you push a tag. One git tag push produces a versioned release with downloadable builds automatically.
Scheduled workflows. Set a workflow to run on a cron schedule (every night at midnight, for example) to catch any drift or dependency issues that appear over time even without active development.
Multiple environments. Use GitHub Environments (Settings > Environments) to create separate configurations for staging and production, with different secrets and optional approval requirements before a deployment can proceed.
None of those are difficult once the basic workflow structure is familiar. The YAML syntax is the steepest part of the learning curve, and it stops feeling foreign fairly quickly once you have read and modified a few working examples.
Next Topics To Learn
- Git and GitHub for Beginners — the foundation for everything in this guide. If branches, commits, and remotes are still fuzzy, start there.
- VS Code Setup for Unity Developers — write and edit workflow YAML files in the same editor as your Unity scripts, with syntax highlighting and validation.
- Unity Android Build Setup Guide — the Android build target referenced in the Unity matrix workflow needs the Android Player Settings configuration covered there.
No comments:
Post a Comment