Godot Game Builds on GitHub Actions

Godot exports on GitHub Actions run on WarpBuild Linux, macOS, and Windows runners with a cached export template directory and rates from $0.004 per minute.

Last verified:

A Godot export on GitHub Actions needs three things on the runner before it produces anything: the editor binary, the export templates that match that editor version exactly, and an export_presets.cfg committed to the repository. WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, so one workflow can fan a Godot release out across all three desktop targets by changing only the warp- label on runs-on, at $0.008 per minute for the Linux export job, $0.032 for the Windows job, and $0.08 for the macOS job.

This page covers the export template cache strategy, the per-platform export configuration, a matrix workflow mapped onto Linux, macOS, and Windows labels, a worked cost model with each platform rate applied, and the four things that dominate wall clock time on export-heavy jobs.

Overview

Godot's headless export is a two-command sequence. godot --headless --import walks the project and writes the imported representation of every asset into .godot/imported, and godot --headless --export-release "<preset>" <output> reads that imported output plus the matching export template and writes the shipping build. Both commands fail loudly when the export templates for the running editor version are absent, which is the most common first failure on a fresh runner.

Which target needs which operating system comes down to what happens after the export, not to the export itself. The Linux and Web presets produce their output with no platform tooling beyond the editor, so they belong on a Linux runner. The Windows preset needs Windows when the build embeds icon and version metadata through rcedit or gets signed with signtool. The macOS preset needs macOS when Godot hands the bundle to codesign and when the workflow submits the result through xcrun notarytool, both of which ship with Xcode on the macOS images.

That split is the whole reason a Godot pipeline goes multi-platform. WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners with the same tooling as the GitHub-hosted images, so the three jobs live in one workflow file and differ by label. The full size and price matrix is in the cloud runner documentation, and the Windows label families are listed on the Windows runner hub.

Teams that also ship a second engine hit the same sizing questions from a different angle; the Unity builds page covers that pipeline. Wiring the export matrix into a tag-triggered release is covered in the release build workflow guide.

Configuration

Two caches carry a Godot pipeline, and they have opposite key strategies.

Export templates: key on the Godot version and nothing else. The template archive published for a given Godot release is immutable, so a cache entry keyed on 4.4.1 stays valid until the project upgrades the engine. Every job in the matrix downloads the same archive, and the unpacked directory differs only by platform: ~/.local/share/godot/export_templates/4.4.1.stable on Linux, ~/Library/Application Support/Godot/export_templates/4.4.1.stable on macOS, and %APPDATA%\Godot\export_templates\4.4.1.stable on Windows. Restore the platform-appropriate path and skip the download step on a hit.

Imported assets: key on the asset tree. .godot/imported holds textures recompressed for the target, audio transcoded, and meshes converted. Hash the source assets and the import configuration so the entry turns over exactly when the imported output stops being valid, and keep a prefix in restore-keys so a run that touches one texture starts from the previous entry instead of importing the project cold.

name: godot-export

on:
  push:
    tags: ["v*"]
  workflow_dispatch:

env:
  GODOT_VERSION: "4.4.1"

jobs:
  export:
    name: export ${{ matrix.target }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: linux
            runner: warp-ubuntu-latest-x64-4x
            preset: "Linux"
            output: build/linux/game.x86_64
          - target: web
            runner: warp-ubuntu-latest-x64-4x
            preset: "Web"
            output: build/web/index.html
          - target: windows
            runner: warp-windows-latest-x64-8x
            preset: "Windows Desktop"
            output: build/windows/game.exe
          - target: macos
            runner: warp-macos-latest-arm64-6x
            preset: "macOS"
            output: build/macos/game.zip
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - name: Resolve the export template directory
        shell: bash
        run: |
          case "${{ runner.os }}" in
            Linux)   dir="$HOME/.local/share/godot/export_templates" ;;
            macOS)   dir="$HOME/Library/Application Support/Godot/export_templates" ;;
            Windows) dir="$APPDATA/Godot/export_templates" ;;
          esac
          echo "TEMPLATES_DIR=$dir" >> "$GITHUB_ENV"

      - name: Install the Godot editor
        shell: bash
        run: ./ci/install-godot.sh "$GODOT_VERSION"

      - id: templates
        uses: actions/cache@v4
        with:
          path: ${{ env.TEMPLATES_DIR }}
          key: godot-templates-${{ env.GODOT_VERSION }}

      - name: Download the export templates
        if: steps.templates.outputs.cache-hit != 'true'
        shell: bash
        run: ./ci/install-export-templates.sh "$GODOT_VERSION" "$TEMPLATES_DIR"

      - uses: actions/cache@v4
        with:
          path: .godot/imported
          key: godot-import-${{ matrix.target }}-${{ env.GODOT_VERSION }}-${{ hashFiles('assets/**', '**/*.import', 'project.godot') }}
          restore-keys: |
            godot-import-${{ matrix.target }}-${{ env.GODOT_VERSION }}-

      - name: Import project assets
        shell: bash
        run: godot --headless --import --path .

      - name: Export ${{ matrix.target }}
        shell: bash
        run: |
          mkdir -p "$(dirname "${{ matrix.output }}")"
          godot --headless --export-release "${{ matrix.preset }}" "${{ matrix.output }}"

      - name: Sign the Windows build
        if: matrix.target == 'windows'
        shell: pwsh
        run: ./ci/sign-windows.ps1 -Path "${{ matrix.output }}"

      - name: Notarize the macOS bundle
        if: matrix.target == 'macos'
        run: |
          xcrun notarytool submit "${{ matrix.output }}" \
            --keychain-profile godot-release --wait

      - uses: actions/upload-artifact@v4
        with:
          name: godot-${{ matrix.target }}
          path: ${{ matrix.output }}
          retention-days: 14
          compression-level: 0

Four details in that file matter more than the rest.

shell: bash keeps one script across all three operating systems, because bash is present on the Windows images as well. Without it the Windows leg of the matrix needs a PowerShell copy of every step, and the two copies drift.

The import cache key carries matrix.target. Texture compression and audio format differ per export target, so a .godot/imported tree built for Web is wrong for macOS, and a shared key gives every job a reimport it did not ask for.

Per-platform preset options live in export_presets.cfg, which belongs in the repository. Windows metadata embedding reads the rcedit path from the editor settings file, so a job that wants icon and version fields either provisions that settings file or drops the metadata step. The macOS preset decides whether Godot invokes codesign itself or leaves an unsigned bundle for a later step.

On Linux labels, actions/cache@v4 can be swapped for WarpBuilds/cache@v1 as a drop-in replacement, which moves the entries onto the WarpBuild cache and off the 10 GB per-repository limit of GitHub's cache service. Details are in the caching documentation. Windows and macOS jobs stay on actions/cache.

Sizing

The three labels this workflow uses, with per-minute rates from the pricing page:

Runner labelvCPUMemoryStoragePer minuteJob it runs
warp-ubuntu-latest-x64-4x416GB150GB SSD$0.008Linux and Web exports
warp-windows-latest-x64-8x832GB256GB SSD$0.032Windows export plus signing
warp-macos-latest-arm64-6x622GB120GB SSD$0.08macOS bundle plus notarization

Two of those rows have a same-shape GitHub-hosted equivalent. warp-windows-latest-x64-8x costs $0.032 per minute against $0.042 per minute for the 8-core Windows larger runner at the same 8 vCPU and 32 GB shape: ($0.042 - $0.032) / $0.042 = 24 percent lower list price. warp-macos-latest-arm64-6x costs $0.08 per minute against $0.102 per minute for the largest GitHub-hosted macOS ARM64 runner: ($0.102 - $0.08) / $0.102 = 22 percent lower list price, and the WarpBuild shape carries one more vCPU and 8 GB more memory. GitHub rates come from the GitHub Actions minute multipliers reference, checked on 2026-08-13.

Worked cost model

Take a studio cutting 40 tagged releases a month, each one running the four-entry matrix above: two Linux-label jobs at 9 minutes each, a Windows job at 11 minutes, and a macOS job at 15 minutes including the notarization wait.

JobMinutes per monthRateMonthly cost
Linux export (4 vCPU)360$0.008/min$2.88
Web export (4 vCPU)360$0.008/min$2.88
Windows export (8 vCPU)440$0.032/min$14.08
macOS export (6 vCPU)600$0.08/min$48.00
Total1,760$67.84

On the two rows with a same-shape GitHub-hosted equivalent, the same minutes cost $18.48 for Windows and $61.20 for macOS, a total of $79.68 against $62.08 here, so those two jobs land $17.60 lower a month at equal minutes. macOS dominates the bill at every size, which is why the macOS job is the one worth trimming first.

Per-minute billing also settles the matrix shape. Four jobs of 9 to 15 minutes running in parallel cost the same as one 44 minute job that exports every target in sequence, and the release lands three times sooner.

Bottlenecks

1. Cold asset import. On a runner with no .godot/imported entry, the editor recompresses every texture and transcodes every audio file before the export step starts. On an asset-heavy project this phase outweighs the export itself, which makes the import cache key the highest-value tuning decision on this page. Watch the cache hit rate in the job log; a key that turns over on every commit hands you a cold import every run.

2. Export template downloads. The template archive covers every platform in one file, and each of the four matrix jobs pulls it. Keyed on the version alone it is fetched once and restored afterward, so the download stops appearing in the job timeline until the next engine upgrade.

3. Notarization wait. xcrun notarytool submit --wait blocks on Apple's service, and the runner bills for the wait while using almost no CPU. A larger macOS label does nothing here. The levers are submitting one bundle per release instead of one per artifact, and moving notarization into a separate job so the export job releases its runner while the submission is pending.

4. Artifact upload. A desktop build plus a Web bundle runs to several gigabytes across the matrix, and the upload sits at the end of the job where the release is waiting on it. Upload only what the release job consumes, and set compression-level: 0 for payloads that are already compressed so the step stops burning CPU for no size reduction.

Telling these apart is a measurement problem. CI observability streams system metrics from the runner agent and correlates them with GitHub Actions job logs, so a job stuck on single-threaded import looks visibly different from one blocked on a network wait. The Action Debugger pauses a workflow and opens a session on the live runner, which is the quickest way to see what actually landed in the export template directory when an export fails on a missing template.

Proof

The migration cost for an existing Godot workflow is the label. Runner images carry the same tooling as GitHub-hosted runners, so the editor install script, the export template layout, signtool, codesign, and every marketplace action behave the same way after the runs-on line changes.

Every cost statement above carries its number, its source, and its checked-on date: WarpBuild rates from the pricing page, GitHub rates from the minute multipliers reference checked on 2026-08-13, and the arithmetic shown inline rather than rounded into a slogan. To confirm the numbers on your own project, run one tagged release on the labels above and compare the job durations and the billed minutes against the same release on GitHub-hosted runners.

FAQ

Which runner sizes should a Godot export matrix use?

Run the Linux and Web exports on warp-ubuntu-latest-x64-4x at $0.008 per minute, the signed Windows export on warp-windows-latest-x64-8x at $0.032 per minute, and the macOS bundle with its notarization step on warp-macos-latest-arm64-6x at $0.08 per minute. Sizing up past those points buys little, because Godot's export step spends most of its time on asset import and on packing the PCK rather than on saturating cores.

Should export templates be downloaded on every run or cached?

Cache them, keyed on the Godot version alone. The template archive for a given Godot release never changes, so a version-keyed cache entry stays valid until you upgrade the engine. The directory differs per platform: ~/.local/share/godot/export_templates on Linux, ~/Library/Application Support/Godot/export_templates on macOS, and %APPDATA%\Godot\export_templates on Windows.

What do the Windows and macOS export jobs cost against GitHub-hosted runners?

warp-windows-latest-x64-8x costs $0.032 per minute against $0.042 per minute for the 8-core Windows larger runner at the same 8 vCPU and 32 GB shape, which is 24 percent lower list price. warp-macos-latest-arm64-6x costs $0.08 per minute against $0.102 per minute for the largest GitHub-hosted macOS ARM64 runner, which is 22 percent lower list price (GitHub pricing, checked on 2026-08-13).

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.