# G20 — CI/CD & Export Pipelines

> **Category:** Guide · **Engine:** Godot 4.x · **Language:** GDScript / C#  
> **Related:** [G18 Performance Profiling](./G18_performance_profiling.md) · [G16 GDExtension](./G16_gdextension_native_code.md) · [E1 Architecture Overview](../architecture/E1_architecture_overview.md)

---

## What This Guide Covers

Shipping a game means more than pressing "Export" in the editor. A reliable CI/CD pipeline catches regressions before players do, builds reproducible exports for every platform, and can auto-deploy to Itch.io, Steam, or the Google Play Store on every tagged commit.

This guide covers Godot's export system (templates, presets, CLI), automated testing with GdUnit4, CI/CD pipelines for GitHub Actions and GitLab CI using the `godot-ci` Docker image, platform-specific export gotchas (Web, Android, iOS, desktop), and deployment to distribution platforms.

**This guide assumes:** you use Git for version control and have a basic understanding of CI/CD concepts. All examples target Godot 4.4+ with GDScript.

---

## Table of Contents

1. [Export Fundamentals](#1-export-fundamentals)
2. [Export Presets and the CLI](#2-export-presets-and-the-cli)
3. [Platform-Specific Considerations](#3-platform-specific-considerations)
4. [Automated Testing with GdUnit4](#4-automated-testing-with-gdunit4)
5. [GitHub Actions Pipeline](#5-github-actions-pipeline)
6. [GitLab CI Pipeline](#6-gitlab-ci-pipeline)
7. [Deploying to Itch.io](#7-deploying-to-itchio)
8. [Deploying to Steam](#8-deploying-to-steam)
9. [Mobile Store Deployment](#9-mobile-store-deployment)
10. [Versioning Strategy](#10-versioning-strategy)
11. [Common Pipeline Failures](#11-common-pipeline-failures)

---

## 1. Export Fundamentals

### Export Templates

Export templates are precompiled Godot binaries for each target platform. They must **match your Godot version exactly** — Godot 4.4.1 requires 4.4.1 export templates.

**Installing templates:**
- **Editor:** Editor → Manage Export Templates → Download
- **CLI:** Download from `https://github.com/godotengine/godot/releases` and place in `~/.local/share/godot/export_templates/4.4.1.stable/` (Linux) or equivalent

### Export Presets

Export presets are stored in `export_presets.cfg` at the project root. **Commit this file to version control** — it defines every platform target, included/excluded files, and platform-specific settings.

```ini
# export_presets.cfg (auto-generated by editor, excerpt)
[preset.0]
name="Linux"
platform="Linux"
export_filter="all_resources"
include_filter=""
exclude_filter="*.import,addons/gut/*"
```

### Rendering Methods and Platform Compatibility

| Renderer | Desktop | Mobile | Web |
|----------|---------|--------|-----|
| Forward+ | ✓ | ✗ | ✗ |
| Mobile | ✓ | ✓ | ✗ |
| Compatibility (WebGL 2) | ✓ | ✓ | ✓ |

> **Key constraint:** Web exports require the **Compatibility** renderer. Forward+ and Mobile do not work on the web. Plan your rendering pipeline early if you target web.

---

## 2. Export Presets and the CLI

### Command-Line Export

Godot supports headless export for CI environments:

```bash
# Export to a named preset (defined in export_presets.cfg)
godot --headless --export-release "Linux" ./build/linux/mygame.x86_64

# Debug export (includes debugger, larger binary)
godot --headless --export-debug "Linux" ./build/linux/mygame.x86_64

# Export PCK only (no executable — useful for patching)
godot --headless --export-pack "Linux" ./build/linux/mygame.pck
```

### Export Flags

| Flag | Purpose |
|------|---------|
| `--headless` | No window — required for CI servers |
| `--export-release "Preset"` | Release build |
| `--export-debug "Preset"` | Debug build |
| `--export-pack "Preset"` | PCK/ZIP only |
| `--path /project/dir` | Project directory |
| `--quit-after 1` | Exit after one frame (useful for validation) |

---

## 3. Platform-Specific Considerations

### Web (HTML5 / WebAssembly)

- **Renderer:** Must use Compatibility (WebGL 2.0)
- **C# limitation:** Godot 4 C# projects **cannot** export to web. Use GDScript or GDExtension.
- **File naming:** Name main file `index.html` for clean URL serving
- **SharedArrayBuffer:** Multithreaded exports require `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` HTTP headers. Itch.io handles this automatically.
- **Audio:** Browsers require user interaction before playing audio. Godot handles this with an automatic click-to-start overlay.
- **File size:** Minimize by excluding unused assets. The `export_filter` in presets can use `customized` mode to only include files you reference.

### Android

- **Requirements:** Android SDK, JDK 17+, Gradle
- **Keystore:** Create a release keystore for signing. **Never commit keystores to version control.**
- **Permissions:** Declare only permissions you use in export settings (camera, microphone, internet, etc.)
- **Min SDK:** Godot 4.4 defaults to API level 24 (Android 7.0)
- **AAB vs APK:** Google Play requires AAB (Android App Bundle). Use APK for sideloading and Itch.io.

```bash
# Generate a release keystore (one-time)
keytool -genkey -v -keystore release.keystore \
  -alias my_game -keyalg RSA -keysize 2048 -validity 10000
```

### iOS

- **Requirements:** macOS with Xcode, Apple Developer account ($99/year)
- **Export produces an Xcode project**, not a final IPA. You must open and archive in Xcode.
- **Provisioning profiles:** Create in Apple Developer portal. Use development for testing, distribution for App Store.
- **CI builds:** Possible on macOS runners using `xcodebuild` after Godot exports the Xcode project.

### Desktop (Windows, macOS, Linux)

- **Windows:** Code signing recommended for distribution. Unsigned executables trigger SmartScreen warnings.
- **macOS:** Requires notarization for distribution outside the App Store. Use `codesign` and `xcrun notarytool`.
- **Linux:** Export produces a self-contained binary. Consider AppImage or Flatpak for broader compatibility.

---

## 4. Automated Testing with GdUnit4

[GdUnit4](https://github.com/MikeSchulze/gdUnit4) is the most mature testing framework for Godot 4.x, supporting both GDScript and C#.

### Installation

1. Download from AssetLib → search "GdUnit4"
2. Enable in **Project → Project Settings → Plugins**

### Writing Tests

```gdscript
# tests/test_player.gd
extends GdUnitTestSuite

var player: Player

func before_test() -> void:
    player = auto_free(Player.new())

func test_initial_health() -> void:
    assert_int(player.health).is_equal(100)

func test_take_damage_reduces_health() -> void:
    player.take_damage(25)
    assert_int(player.health).is_equal(75)

func test_take_damage_does_not_go_below_zero() -> void:
    player.take_damage(999)
    assert_int(player.health).is_greater_equal(0)

func test_heal_does_not_exceed_max() -> void:
    player.take_damage(50)
    player.heal(999)
    assert_int(player.health).is_equal(player.max_health)
```

### Running Tests in CI

GdUnit4 provides a GitHub Action for automated test execution:

```yaml
# In your GitHub Actions workflow
- name: Run GdUnit4 Tests
  uses: godot-gdunit-labs/gdUnit4-action@v1
  with:
    godot-version: '4.4.1'
    test-path: 'tests/'
    report-name: 'test-results'
```

---

## 5. GitHub Actions Pipeline

### Full Pipeline with godot-ci

The `abarichello/godot-ci` Docker image bundles Godot with export templates, ready for headless use:

```yaml
# .github/workflows/build.yml
name: Build & Deploy

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  GODOT_VERSION: "4.4.1"

jobs:
  # Job 1: Run tests on every push and PR
  test:
    runs-on: ubuntu-latest
    container:
      image: barichello/godot-ci:4.4.1
    steps:
      - uses: actions/checkout@v4

      - name: Import project
        run: godot --headless --import --quit-after 1 || true

      - name: Run GdUnit4 tests
        uses: godot-gdunit-labs/gdUnit4-action@v1
        with:
          godot-version: ${{ env.GODOT_VERSION }}
          test-path: 'tests/'

  # Job 2: Build exports on tag push
  export:
    needs: test
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    container:
      image: barichello/godot-ci:4.4.1
    strategy:
      matrix:
        include:
          - preset: "Linux"
            path: build/linux/mygame.x86_64
          - preset: "Windows"
            path: build/windows/mygame.exe
          - preset: "Web"
            path: build/web/index.html
    steps:
      - uses: actions/checkout@v4

      - name: Import project
        run: godot --headless --import --quit-after 1 || true

      - name: Create build directory
        run: mkdir -p $(dirname ${{ matrix.path }})

      - name: Export ${{ matrix.preset }}
        run: godot --headless --export-release "${{ matrix.preset }}" ${{ matrix.path }}

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.preset }}-build
          path: build/${{ matrix.preset | lower }}/

  # Job 3: Deploy web build to GitHub Pages
  deploy-web:
    needs: export
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: Web-build
          path: build/web/

      - uses: actions/upload-pages-artifact@v3
        with:
          path: build/web/

      - uses: actions/deploy-pages@v4
```

### Import Step Explained

The `godot --headless --import` step forces Godot to scan and import all assets (textures, audio, etc.). Without it, exports may fail with missing `.import` files. The `|| true` prevents pipeline failure on non-critical import warnings.

---

## 6. GitLab CI Pipeline

```yaml
# .gitlab-ci.yml
image: barichello/godot-ci:4.4.1

stages:
  - test
  - export
  - deploy

variables:
  EXPORT_NAME: my_game

before_script:
  - godot --headless --import --quit-after 1 || true

test:
  stage: test
  script:
    - godot --headless --run-tests --quit-after 1

export-linux:
  stage: export
  only:
    - tags
  script:
    - mkdir -p build/linux
    - godot --headless --export-release "Linux" build/linux/${EXPORT_NAME}.x86_64
  artifacts:
    paths:
      - build/linux/

export-windows:
  stage: export
  only:
    - tags
  script:
    - mkdir -p build/windows
    - godot --headless --export-release "Windows" build/windows/${EXPORT_NAME}.exe
  artifacts:
    paths:
      - build/windows/

export-web:
  stage: export
  only:
    - tags
  script:
    - mkdir -p build/web
    - godot --headless --export-release "Web" build/web/index.html
  artifacts:
    paths:
      - build/web/

deploy-itch:
  stage: deploy
  only:
    - tags
  dependencies:
    - export-linux
    - export-windows
    - export-web
  script:
    - apt-get update && apt-get install -y curl unzip
    - curl -L https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default -o butler.zip
    - unzip butler.zip && chmod +x butler
    - ./butler push build/linux/ $ITCH_USER/$ITCH_GAME:linux --userversion $CI_COMMIT_TAG
    - ./butler push build/windows/ $ITCH_USER/$ITCH_GAME:windows --userversion $CI_COMMIT_TAG
    - ./butler push build/web/ $ITCH_USER/$ITCH_GAME:web --userversion $CI_COMMIT_TAG
```

---

## 7. Deploying to Itch.io

### Using Butler (Itch.io CLI)

[Butler](https://itch.io/docs/butler/) is Itch.io's official deployment tool. It uploads incremental diffs, making updates fast.

```bash
# Install butler
curl -L https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default -o butler.zip
unzip butler.zip && chmod +x butler

# Authenticate (one-time)
./butler login

# Push a build
./butler push build/linux/ yourname/yourgame:linux --userversion 1.0.0
./butler push build/windows/ yourname/yourgame:windows --userversion 1.0.0
./butler push build/web/ yourname/yourgame:web --userversion 1.0.0
```

### CI Authentication

Set `BUTLER_API_KEY` as a repository secret (GitHub) or CI/CD variable (GitLab). Butler reads it automatically — no `butler login` needed.

---

## 8. Deploying to Steam

Steam deployment uses [SteamCMD](https://developer.valvesoftware.com/wiki/SteamCMD) and depot configuration:

```vdf
# app_build.vdf
"AppBuild"
{
    "AppID" "YOUR_APP_ID"
    "Desc" "Automated CI build"
    "BuildOutput" "./output/"
    "Depots"
    {
        "YOUR_DEPOT_ID"
        {
            "FileMapping"
            {
                "LocalPath" "./build/windows/*"
                "DepotPath" "."
                "recursive" "1"
            }
        }
    }
}
```

```bash
# Upload via SteamCMD
steamcmd +login "$STEAM_USER" "$STEAM_PASS" \
  +run_app_build app_build.vdf +quit
```

> **Security:** Use Steam's app-specific password and store credentials as CI secrets. Enable Steam Guard with a shared secret for headless auth.

---

## 9. Mobile Store Deployment

### Google Play (Android)

1. Export as AAB from Godot (`--export-release "Android" game.aab`)
2. Sign with your release keystore
3. Upload via Google Play Console or use [Fastlane](https://fastlane.tools/) for automation:

```ruby
# fastlane/Fastfile
lane :deploy do
  upload_to_play_store(
    aab: "build/android/game.aab",
    track: "internal",  # internal → alpha → beta → production
    skip_upload_metadata: true
  )
end
```

### Apple App Store (iOS)

1. Export from Godot → produces Xcode project
2. Build and archive with `xcodebuild` on a macOS CI runner
3. Upload with `xcrun altool` or Fastlane

> **macOS CI runner required.** GitHub Actions provides macOS runners (`runs-on: macos-latest`). GitLab requires a self-hosted macOS runner.

---

## 10. Versioning Strategy

### Semantic Versioning for Games

```
MAJOR.MINOR.PATCH
  │     │     └── Bug fixes, balance tweaks
  │     └──────── New content, features
  └────────────── Breaking changes, major milestones (Early Access → 1.0)
```

### Embedding Version in the Build

```gdscript
# version.gd — autoload
extends Node

const VERSION := "1.2.3"
const BUILD_NUMBER := "auto"  # Replaced by CI

func get_version_string() -> String:
    if BUILD_NUMBER == "auto":
        return VERSION + "-dev"
    return VERSION + "+" + BUILD_NUMBER
```

In CI, use `sed` to inject the build number before export:

```bash
sed -i "s/const BUILD_NUMBER := \"auto\"/const BUILD_NUMBER := \"$CI_COMMIT_SHORT_SHA\"/" version.gd
```

---

## 11. Common Pipeline Failures

| Symptom | Cause | Fix |
|---------|-------|-----|
| `Export template not found` | Template version mismatch | Match `godot-ci` image version to your project |
| `No export presets found` | Missing `export_presets.cfg` | Commit `export_presets.cfg` to version control |
| `.import` errors during export | Skipped import step | Run `godot --headless --import` before export |
| Android export fails | Missing keystore or SDK | Configure keystore path in export preset; install SDK in Docker |
| Web export blank screen | Wrong renderer | Switch to Compatibility renderer for web |
| Tests pass locally, fail in CI | Resource path differences | Use `res://` paths, ensure case-sensitive filenames |
| Massive build artifacts | Debug symbols included | Use `--export-release`, not `--export-debug` for dist builds |
| `godot-ci` image too old | Docker tag doesn't match | Pin to exact version: `barichello/godot-ci:4.4.1` |

---

## Cheat Sheet

```bash
# Local export (CLI)
godot --headless --export-release "Linux" ./build/mygame.x86_64

# Run project headless (smoke test)
godot --headless --quit-after 60

# Import all assets (required before CI export)
godot --headless --import --quit-after 1

# Butler push to Itch.io
butler push ./build/linux/ user/game:linux --userversion 1.0.0

# Generate export_presets.cfg
# → Use the Godot editor: Project → Export → Add preset → save
```

---

## .gitignore for Godot Projects

```gitignore
# Godot import cache (regenerated on import)
.godot/

# Build output
build/

# OS files
.DS_Store
Thumbs.db

# IDE
.vscode/
*.code-workspace

# Do NOT ignore these:
# export_presets.cfg  ← needed for CI export
# project.godot      ← project definition
# *.import           ← tells Godot how to process each asset
```
