Skip to content
Go back

Leveling Up Our Workflow: Automated Godot Deployments to Itch.io!

Published:  at  19:18

The Upload Grind

Let’s be honest, after spending hours coding, debugging, and polishing a game update or a game jam entry, the last thing you often feel like doing is manually:

  1. Exporting the game for Windows.
  2. Exporting for Linux.
  3. Exporting for Web.
  4. Zipping the web build.
  5. Opening the Itch.io dashboard.
  6. Uploading each build to the correct channel.
  7. Remembering what version you just uploaded.

It’s tedious, repetitive, and surprisingly easy to make a mistake (like uploading the wrong file or forgetting a platform). As 9to5-Grind, we’re all about optimizing workflows where possible, especially coming from the webdev world where automation is king. So, we decided it was time to automate our Godot game deployments to Itch.io!

Enter: GitLab CI/CD

We host our code on GitLab, which comes with a powerful built-in automation tool called CI/CD (Continuous Integration / Continuous Deployment). The basic idea is simple: you define a set of instructions (a “pipeline”) in a special file (.gitlab-ci.yml), and GitLab automatically runs those instructions whenever specific events happen, like pushing code to a particular branch.

Our goal: push code to our main branch (or create a release tag), and have GitLab automatically build the game for our target platforms and upload them straight to Itch.io.

The Tools for the Job

To make this magic happen, we needed a few key ingredients:

  1. Godot Engine (Headless): The command-line version of Godot, which can run exports without needing the graphical editor.
  2. Godot Export Templates: The necessary files Godot uses to package the game for different platforms (Windows, Linux, Web).
  3. Butler: Itch.io’s official command-line tool specifically designed for uploading builds.
  4. GitLab CI/CD Runner: The environment provided by GitLab where our automation script runs.
  5. A Butler API Key (BUTLER_API_KEY): This is the crucial piece for authentication. Important Note: This isn’t the standard API key found in your Itch.io settings page! It’s a special key generated specifically for Butler automation.

Getting the Right Key: BUTLER_API_KEY

This tripped us up initially! To securely allow our GitLab CI job to upload via Butler, we needed to perform a one-time setup locally:

  1. Download Butler onto our local machine.
  2. Run the command butler login in the terminal. This opens a browser window to authorize Butler with our Itch.io account.
  3. Once authorized, Butler saves credentials locally. Crucially, it also provides instructions (or saves the key to a specific file) on how to get the BUTLER_API_KEY value needed for CI environments. This key is what proves to Itch.io that our automated script is allowed to upload on our behalf. (More details on this process can be found in the official Itch.io Butler Login Documentation).

We then took this generated BUTLER_API_KEY value and stored it securely as a masked and protected variable in our GitLab project’s Settings > CI/CD > Variables. It should never be written directly into the .gitlab-ci.yml file!

Our Automated Pipeline

With the correct key safely stored, we configured our .gitlab-ci.yml file to perform the following steps whenever we push to our main branch:

  1. Build Stage:
    • Download the correct Godot headless engine and export templates (GitLab caches these to speed things up!).
    • Run the Godot command-line export process for each platform (Windows, Linux, Web), using the presets defined in our export_presets.cfg file.
    • Zip up the web build, as Itch.io prefers web games uploaded as a single .zip file.
    • ✨ Smart Versioning: Automatically generate a VERSION.txt file.
    • Package all the builds and the VERSION.txt file as “artifacts” to be used in the next stage.
Expand build step
# .gitlab-ci.yml
build_game:
  stage: build
  image: debian:stable-slim
  cache:
    key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
    paths:
      - godot.zip
      - templates.zip
      - godot/
      - ~/.cache/godot/
      - ~/.config/godot/
      - ~/.local/share/godot/export_templates/
  before_script:
    - apt-get update -qq && apt-get install -y -qq unzip wget zip
    - GODOT_VERSION="4.4"
    - GODOT_RELEASE_NAME="stable"
    - GODOT_BASE_URL="https://github.com/godotengine/godot-builds/releases/download/${GODOT_VERSION}-${GODOT_RELEASE_NAME}"
    - GODOT_HEADLESS_FILENAME="Godot_v${GODOT_VERSION}-${GODOT_RELEASE_NAME}_linux.x86_64"
    - GODOT_TEMPLATES_FILENAME="Godot_v${GODOT_VERSION}-${GODOT_RELEASE_NAME}_export_templates.tpz"
    - TEMPLATES_PATH="$HOME/.local/share/godot/export_templates/${GODOT_VERSION}.${GODOT_RELEASE_NAME}"
    - |
      if [ ! -f godot.zip ]; then
        echo "Downloading Godot headless..."
        wget -q -O godot.zip "${GODOT_BASE_URL}/${GODOT_HEADLESS_FILENAME}.zip"
        unzip -q -o godot.zip -d godot/
        chmod +x "godot/${GODOT_HEADLESS_FILENAME}"
      else
        echo "Using cached Godot headless."
      fi
    - |
      if [ ! -f templates.zip ]; then
        echo "Downloading Godot export templates..."
        wget -q -O templates.zip "${GODOT_BASE_URL}/${GODOT_TEMPLATES_FILENAME}"
        mkdir -p "${TEMPLATES_PATH}"
        unzip -q -o templates.zip -d "${TEMPLATES_PATH}"
        mv "${TEMPLATES_PATH}/templates/"* "${TEMPLATES_PATH}/" # Correct structure
        rm -rf "${TEMPLATES_PATH}/templates"
      else
        echo "Using cached Godot export templates."
      fi
  script:
    - VERSION_FILE_PATH="$CI_PROJECT_DIR/VERSION.txt"
    - GODOT_EXECUTABLE="$CI_PROJECT_DIR/godot/${GODOT_HEADLESS_FILENAME}"
    - PROJECT_PATH="$CI_PROJECT_DIR/src"
    - BUILD_DIR="$CI_PROJECT_DIR/build"
    - echo "Generating VERSION.txt..."
    - |
      if [ -n "$CI_COMMIT_TAG" ]; then
        # If triggered by a tag (e.g., v1.0.0), use the tag name
        VERSION_STRING="$CI_COMMIT_TAG (commit: $CI_COMMIT_SHORT_SHA)"
        echo "Using tag for version: $VERSION_STRING"
      else
        # Otherwise, use date, branch, and commit SHA for dev builds
        BUILD_TIMESTAMP=$(date +'%Y-%m-%d_%H-%M-%S') # Format: YYYY-MM-DD_HH-MM-SS
        VERSION_STRING="DevBuild-${BUILD_TIMESTAMP}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}"
        echo "Using dev build format for version: $VERSION_STRING"
      fi
      echo "$VERSION_STRING" > "$VERSION_FILE_PATH"
    - echo "VERSION.txt content:"
    - cat "$VERSION_FILE_PATH"
    - mkdir -p "$BUILD_DIR/web"
    - echo "Exporting for Web..."
    - $GODOT_EXECUTABLE --headless --path "$PROJECT_PATH" --export-release "Horror-Manison-Thief" "$BUILD_DIR/web/index.html" # <-- CHANGE Preset Name
    - echo "Zipping Web build..."
    - cd "$BUILD_DIR/web" && zip -q -r ../web.zip . && cd "$CI_PROJECT_DIR"
  artifacts:
    paths:
      - build/
      - VERSION.txt
    expire_in: 1 week
  1. Deploy Stage:
    • Download Butler within the CI environment.
    • Use Butler and our secure BUTLER_API_KEY (pulled from the GitLab variable) to push each exported build to the correct channel on our Itch.io game page.
    • Tell Butler to use the content of VERSION.txt to label the build on Itch.io.
Expand deploy step
# .gitlab-ci.yml
deploy_to_itch:
  stage: deploy
  image: debian:stable-slim
  needs: [build_game]
  before_script:
    - apt-get update -qq && apt-get install -y -qq unzip wget
    - BUTLER_URL="https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default"
    - echo "Downloading Butler..."
    - wget -q -O butler.zip $BUTLER_URL
    - unzip -q -o butler.zip
    - chmod +x butler
  script:
    - ITCHIO_USER="CHANGEME"
    - ITCHIO_GAME="CHANGEME"
    - BUTLER_EXE="$CI_PROJECT_DIR/butler"
    - BUILD_DIR="$CI_PROJECT_DIR/build"
    - VERSION_FILE="$CI_PROJECT_DIR/VERSION.txt"
    - VERSION_ARG=""
    - if [ -f "$VERSION_FILE" ]; then VERSION_ARG="--userversion-file $VERSION_FILE"; fi
    - echo "Uploading Web build..."
    - $BUTLER_EXE push "$BUILD_DIR/web.zip" "${ITCHIO_USER}/${ITCHIO_GAME}:html5" $VERSION_ARG
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

✨ The Smart Versioning Trick

Manually naming builds is another pain point. Did I call the last one v1.1 or v1.1.0? Was that the build from Tuesday or Wednesday?

Our pipeline now automatically creates that VERSION.txt file with human-readable info:

Butler uses this file to label the uploads on Itch.io, so we always know exactly what build is live!

The Result? Sweet Relief!

Now, our deployment process is as simple as:

git push origin main


Previous Post
Bringing Mixamo Animations to Life in Godot: Our Manual Import Workflow
Next Post
Hello, Devlog! Our Journey Begins