CI/CD for Godot Projects: Automated Testing with GitHub Actions
Godot 4 runs headlessly with --headless — no display required in CI. Combined with GUT's CLI runner, you can run your full test suite in GitHub Actions on every push. This guide covers setting up the CI pipeline, caching the Godot binary, running GUT tests, and reporting results.
Key Takeaways
Godot 4 supports --headless for CI. No Xvfb or virtual display needed. Run godot --headless and the engine runs without a window.
GUT has a dedicated CLI script. addons/gut/gut_cmdln.gd accepts test directories, prefixes, and verbosity flags. Pass -gexit_on_complete to return a non-zero exit code on failure.
Cache the Godot binary. Downloading Godot (100MB+) on every CI run adds 30-60 seconds. Cache it by version hash for fast pipelines.
Import your project before running tests. Godot must import resources on first run. Use a cache action or run godot --headless --import as a separate step.
Fail the build on test failure. GUT returns exit code 1 on test failure. GitHub Actions treats non-zero exit codes as failures — no extra configuration needed.
CI/CD for Godot: What's Required
Running Godot tests in CI requires:
- The Godot binary (headless version or standard with
--headlessflag) - Your GUT plugin installed in the project
- A Godot project that's been imported (
.godot/directory) - The GUT CLI command to run tests
Godot 4 includes headless mode in the standard binary via --headless. You don't need a separate headless build.
Basic GitHub Actions Workflow
# .github/workflows/test.yml
name: Godot Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
GODOT_VERSION: "4.2.2"
jobs:
test:
name: Run GUT Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive # If GUT is a submodule
- name: Cache Godot binary
id: cache-godot
uses: actions/cache@v4
with:
path: ~/.local/share/godot/
key: godot-${{ env.GODOT_VERSION }}
- name: Download Godot
if: steps.cache-godot.outputs.cache-hit != 'true'
run: |
mkdir -p ~/.local/share/godot/
cd ~/.local/share/godot/
wget -q "https://github.com/godotengine/godot/releases/download/${{ env.GODOT_VERSION }}-stable/Godot_v${{ env.GODOT_VERSION }}-stable_linux.x86_64.zip"
unzip -q "Godot_v${{ env.GODOT_VERSION }}-stable_linux.x86_64.zip"
chmod +x "Godot_v${{ env.GODOT_VERSION }}-stable_linux.x86_64"
ln -s "Godot_v${{ env.GODOT_VERSION }}-stable_linux.x86_64" godot
- name: Add Godot to PATH
run: echo "$HOME/.local/share/godot" >> $GITHUB_PATH
- name: Cache Godot import cache
uses: actions/cache@v4
with:
path: .godot/
key: godot-import-${{ hashFiles('project.godot', 'addons/**', 'scripts/**', 'scenes/**') }}
restore-keys: |
godot-import-
- name: Import Godot project
run: |
godot --headless --import --quit-after 100
continue-on-error: true # Import step may exit with non-zero
- name: Run GUT tests
run: |
godot --headless \
-s addons/gut/gut_cmdln.gd \
-gdir=res://test \
-gprefix=test_ \
-gsuffix=.gd \
-glog=2 \
-gexit_on_completeUnderstanding the GUT CLI Flags
| Flag | Description |
|---|---|
-gdir=res://test |
Directory containing test scripts |
-gprefix=test_ |
File prefix for test scripts |
-gsuffix=.gd |
File extension for test scripts |
-glog=2 |
Log level: 0=errors only, 1=summary, 2=all, 3=verbose |
-gexit_on_complete |
Exit Godot when tests finish (required for CI) |
-gselect=test_player.gd |
Run only specific test file |
-gunit_test_name=test_player_health |
Run only specific test method |
-gconfig=res://test/.gutconfig.json |
Load configuration from file |
GUT Configuration File
Instead of long CLI flags, use a .gutconfig.json file:
// test/.gutconfig.json
{
"dirs": ["res://test"],
"prefix": "test_",
"suffix": ".gd",
"log_level": 2,
"exit_on_complete": true,
"include_subdirs": true
}Then the CLI becomes:
godot --headless -s addons/gut/gut_cmdln.gd -gconfig=res://test/.gutconfig.jsonCaching Strategy
Godot import caches speed up CI significantly. Two caches matter:
Godot binary cache — by version:
- uses: actions/cache@v4
with:
path: ~/.local/share/godot/
key: godot-${{ env.GODOT_VERSION }}Project import cache — by source file hash:
- uses: actions/cache@v4
with:
path: .godot/
key: godot-import-${{ hashFiles('project.godot', '**/*.gd', '**/*.tscn') }}
restore-keys: godot-import-The import cache stores Godot's compiled .res files. Without it, the import step runs on every push even if nothing changed.
Handling Import Failures
Godot's import step is notoriously flaky in CI. It may exit with a non-zero code even on success. Use continue-on-error: true for the import step but fail on the test step:
- name: Import project
run: godot --headless --import --quit-after 100
continue-on-error: true
- name: Run tests
run: |
godot --headless -s addons/gut/gut_cmdln.gd \
-gconfig=res://test/.gutconfig.json
# No continue-on-error here — test failures MUST fail the buildAdding a Test Status Badge
After your workflow runs, add a badge to your README:
Running Tests on Multiple Platforms
For games targeting multiple platforms, test on multiple OS:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Download Godot (Ubuntu)
if: runner.os == 'Linux'
run: |
wget -q "https://github.com/godotengine/godot/releases/download/${{ env.GODOT_VERSION }}-stable/Godot_v${{ env.GODOT_VERSION }}-stable_linux.x86_64.zip"
# ... unzip and install
- name: Download Godot (Windows)
if: runner.os == 'Windows'
run: |
Invoke-WebRequest -Uri "https://github.com/godotengine/godot/releases/download/${{ env.GODOT_VERSION }}-stable/Godot_v${{ env.GODOT_VERSION }}-stable_win64.exe.zip" -OutFile godot.zip
# ... unzip and install
- name: Run tests
run: godot --headless -s addons/gut/gut_cmdln.gd -gconfig=res://test/.gutconfig.jsonSeparating Unit and Integration Tests
Use subdirectories to run fast tests in pre-commit and full tests in CI:
test/
├── unit/ # Fast: no scene instantiation
│ ├── test_player_stats.gd
│ └── test_inventory.gd
└── integration/ # Slower: instantiates scenes
├── test_combat_system.gd
└── test_level_loading.gd# Run only unit tests on PR
- name: Run unit tests
run: |
godot --headless -s addons/gut/gut_cmdln.gd \
-gdir=res://test/unit \
-gexit_on_complete
# Run all tests on main branch
- name: Run all tests
if: github.ref == 'refs/heads/main'
run: |
godot --headless -s addons/gut/gut_cmdln.gd \
-gdir=res://test \
-gexit_on_completeDebugging CI Failures
When tests fail in CI but pass locally:
Check the import cache. A stale import cache can cause resource load failures. Clear it by changing the cache key.
Increase log level. Add -glog=3 to see verbose output including which test is running when the failure occurs.
Check for scene-dependent tests. Tests that instantiate scenes are more likely to fail headlessly if they rely on visual nodes (Viewport, Window) that behave differently without a display.
Use -gselect to isolate. Run only the failing test file to reduce noise:
godot --headless -s addons/gut/gut_cmdln.gd \
-gdir=res://test \
-gselect=test_combat_system.gd \
-gexit_on_completeSummary
A Godot CI pipeline with GUT requires four things:
- The Godot binary cached by version
- The project imported (with caching)
godot --headless -s addons/gut/gut_cmdln.gdwith-gexit_on_complete- Non-zero exit code treated as build failure (automatic in GitHub Actions)
Set this up before you write your first test — having CI from day one means you'll never merge code that breaks existing tests.