0

I am building a workflow using GitHub actions to automate test runs for my current Godot project. I have written two independent actions and one workflow which references them. Of these two actions - install and unit-test - only the latter is causing an issue.

In my current set-up I am also using act to simulate the GitHub Action behaviour locally so to avoid pushing every single change to my remote branch.

I will try to summarize what the workflow is supposed to do before posting some code snippets. My goal for the workflow is to have it install a clean version of Godot - given a specific version or the latest available one otherwise - before running the tests. The artifact is uploaded using actions/upload-artifact@v4. The unit-test action is then called, which downloads the artifact, checks for the addon I am using for the testing suite - GUT - Godot Unit Testing - and installs it if missing. It then runs the command to execute tests.

As mentioned, this exact setup, with the exact same workflow yaml files works locally when run through act, but fails due to some parsing error on GitHub. What could be causing this? Perhaps some permission problems?

-- Follows code snippets --

Main workflow - the one under .github/workflows:

name: Build & Deploy pipeline

# Controls when the workflow will run
on:
  push: {}
  pull_request: {}

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:

  install-godot:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Current Branch
        uses: actions/checkout@v4
        with:
          lfs: true
      
      - name: Install Godot Binary
        uses: ./src/test/resources/actions/godot-install
        with:
          install-path: '/home/runner/godot-linux'
        id: install-binary
  
  unit-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Current Branch
        uses: actions/checkout@v4
        with:
          lfs: true
      
      - name: Run Unit Tests
        uses: ./src/test/resources/actions/godot-test
        with:
          install-path: '/home/runner/godot-linux'
          gut-params: '-gconfig=res://src/test/resources/.gutconfigs.json'
    needs: install-godot

GUT Testing action:

name: 'GUT Testing Godot'
description: 'Runs the Godot Unit Testing (GUT) suite'

# Specifies when to run the workflow
on:
  workflow_dispatch:

# Lists all the workflow available arguments/settings
inputs:
  install-path:
    description: 'The path to Godot Bin/Executable'
    type: string
    required: true
  project-dir:
    description: 'The project directory in which the action is to be executed. Must end with a path-separator. e.g. ./MyGame/'
    type: string
    required: false
    default: './'
  gut-version:
    description: 'The target GUT addon version to install - if not available'
    type: string
    required: false
    default: 'latest'
  gut-params:
    description: 'A custom set of GUT parameters (see GUT doc for more info)'
    type: string
    required: false

# Lists all the jobs for the current workflow
runs:
  using: "composite"
  steps:
    - name: Download Godot artifact
#      if: ${{ !env.ACT }}
      uses: actions/download-artifact@v4
      with:
        name: godot-artifact
        path: ${{ inputs.install-path }}

    - name: Install GUT addon
      shell: bash
      run: |
        echo "🔍 Checking for GUT addon install..."
        if [[ -n ${{ inputs.project-dir }} ]]; then
          cd ${{ inputs.project-dir }}
        fi
        
        GUT_PATH="./addons/gut/"
        if [[ ! -d "${GUT_PATH}" ]]; then
          echo "⚠️ WARNING: GUT addon not found - Installing version ${{ inputs.gut-version }}"

          mkdir -p "${GUT_PATH}"
          GUT_VERSION=${{ inputs.gut-version }}
          if [ $GUT_VERSION == 'latest' ]; then
            GUT_VERSION=$(git ls-remote --refs --tags https://github.com/bitwes/Gut v* | sort -t '/' -k 3 -V | tail -n 1 | cut -d '/' -f 3)
          fi

          echo "🔍 Downloading GUT version: ${GUT_VERSION}"
          git clone --quiet --depth 1 --branch ${GUT_VERSION} --single-branch https://github.com/bitwes/Gut ${GUT_PATH}
        else
          echo "✅ GUT addon already installed - skipping job"
          exit 0
        fi

    - name: Execute Project Re-Import
      shell: bash
      run: |
        echo "⚙️ Executing Godot to Import dependencies"
        chmod u+x ${{ inputs.install-path }}/godot
        ${{ inputs.install-path }}/godot --headless -d --import --path "$PWD"
    
    - name: Run Unit Tests
      id: unit-test
      shell: bash
      run: |
        TEMP_FILE=/tmp/gut.log
        chmod u+x ${{ inputs.install-path }}/godot
        ${{ inputs.install-path }}/godot --headless -s ${{ inputs.project-dir }}/addons/gut/gut_cmdln.gd -d --path "$PWD" ${{inputs.gut-params}} -gexit 2>&1 | tee $TEMP_FILE
        
        if grep -q "No tests ran" "$TEMP_FILE" || grep -qE "Asserts\s+none" "$TEMP_FILE"; then
          echo "⚠️ WARNING: No tests ran! Please check test directory and parameters"
          exit 1
        fi

        if ! grep -q "All tests passed" "$TEMP_FILE"; then
          echo "❌ ERROR: One or more tests failed!"
          exit 1
        fi

        echo "✅ All Unit Tests passed successfully"
        exit 0

Here is - one of - the Error(s) I am getting when the job Run Unit Tests is run on GitHub - I am omitting the rest as they are mostly the same - :

Debugger Break, Reason: 'Invalid call. Nonexistent function 'new' in base 'GDScript'.'
*Frame 0 - res://src/test/unit/entity/player/PlayerTest.gd:10 in function 'before_all'
Enter "help" for assistance.
debug> * test_ready_call_should_correctly_set_all_properties

Debugger Break, Reason: 'Parser Error: Expected superclass name after "extends".'
*Frame 0 - res://addons/gut/not_a_real_file/gut_dynamic_script_8.gd:1 in function ''
Enter "help" for assistance.
debug> SCRIPT ERROR: Parse Error: Expected superclass name after "extends".
          at: GDScript::reload (res://addons/gut/not_a_real_file/gut_dynamic_script_8.gd:1)
    [ERROR]:  Could not create script from source.  Error:  43
      at line -1

This error does not come unexpectedly. Soon after triggering the --headless --import commmand I can see a series of errors in the logs, like:

SCRIPT ERROR: Parse Error: Could not find type "CustomMultiplayerSpawner" in the current scope.
          at: GDScript::reload (res://src/mygame/level/Level.gd:8)
Debugger Break, Reason: 'Parser Error: Could not find type "CustomMultiplayerSpawner" in the current scope.'
SCRIPT ERROR: Parse Error: Cannot use simple "@export" annotation because the type of the initialized value can't be inferred.
*Frame 0 - res://src/mygame/level/Level.gd:8 in function ''
          at: GDScript::reload (res://src/mygame/level/Level.gd:8)
Enter "help" for assistance.
debug> 
Debugger Break, Reason: 'Parser Error: Could not resolve external class member "main_hand_spawner".'
*Frame 0 - res://src/mygame/entity/player/Player.gd:138 in function ''
Enter "help" for assistance.
SCRIPT ERROR: Parse Error: Could not resolve external class member "main_hand_spawner".
          at: GDScript::reload (res://src/mygame/entity/player/Player.gd:138)
ERROR: Failed to load script "res://src/mygame/entity/player/Player.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript.cpp:3022)
debug> 

I can't stress enough that this does not happen localy when using act.

For the sake of completeness I will include this PlayerTest.gd script as an example, eventhough I doubt that is the culprit as it works fine both in-engine and when using act.

class_name PlayerTest
extends GutTest

var pl_res: Resource = load("res://src/mygame/entity/player/Player.gd")
var pl_vis_res: Resource = load("res://src/mygame/entity/player/PlayerVisual.gd")
var cc_res: Resource = load("res://src/mygame/entity/components/CameraComponent.gd")
var pl: Player

func before_all() -> void:
    pl = pl_res.new()

func test_ready_call_should_correctly_set_all_properties() -> void:
    var d_pl: Player = autofree(partial_double(pl_res).new())
    var d_pl_vis: PlayerVisual = autofree(partial_double(pl_vis_res).new())
    var d_cc: CameraComponent = autofree(partial_double(cc_res).new())
    stub(d_pl.reset_to_player_camera).to_do_nothing()
    stub(d_cc._init_camera).to_do_nothing()
    var test_id: int = autofree(1)
    var test_username: String = autofree("Test-Username")
    SteamManager.steam_user_id = test_id
    SteamManager.steam_username = test_username
    d_pl.name = autofree(str("123"))
    d_pl.third_person_model = d_pl_vis
    d_pl.camera_component = d_cc
    d_pl_vis.main_hand_container = autofree(Node3D.new())
    
    d_pl._ready()
    
    assert_eq(d_pl.user_peer_id, 123)
    assert_eq(d_pl.user_steam_id, test_id)
    assert_eq(d_pl.username_steam, test_username)
    assert_called_count(d_pl.reset_to_player_camera, 1)

func test_should_set_head_pivot_position(p = use_parameters([
    autoqfree(Vector3.UP), autoqfree(Vector3.DOWN), autoqfree(Vector3(-3.25, 1.05, 0.99))
])) -> void:
    pl.head_pivot = autofree(Node3D.new())
    
    pl.set_head_pivot_pos(p)
    
    assert_eq(pl.head_pivot.position, p)

func test_should_reset_head_pivot_position_and_rotation_to_default() -> void:
    var hp: Node3D = autofree(Node3D.new())
    hp.position = autofree(Vector3(99.0, -99.0, 99.0))
    hp.rotation_degrees = autofree(Vector3(-180.11, -45.25, 91.3))
    pl.head_pivot = hp
    
    pl.reset_head_pivot_pos()
    
    assert_eq(pl.head_pivot.position, autofree(Vector3(0.0, 1.45, 0.0)))
    assert_eq(pl.head_pivot.rotation_degrees, autofree(Vector3(0.0, 180.0, 0.0)))

func after_all() -> void:
    pl.free()

Extra notes

From what I have gathered online, some issues could be related to the missing .godot folder which is included in the .gitignore by default. I have that folder included in my ignore list, however I am forcefully triggering the re-import (see --import) to avoid the issue. Doing this indeed fixed it locally, but there still seems to be an issue during the import as the parsing problem seems to be closely related to that (imho).

I have tried double-checking all the parameters with no success. Also, just for reference this is the command I run when starting the workflow with act:

act --rm --artifact-server-path /home/runner/godot-linux
2
  • Without diving too deep: what you install in one job isn't persisted elsewhere, so running godot-install has no effect on the unit-test job, because they're separate jobs. Commented Jul 22 at 0:35
  • @BenjaminW. that I am aware of. As I mentioned in the post, the installed artifact is uploaded at the end of the godot-install job, and downloaded at the start of the next one Commented Jul 22 at 6:48

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.