Modernizing a Godot testing tool from a legacy bash script to a test-covered Node.js GitHub Action, improving maintainability and community contribution.
Raul G
October 16, 2025
In any software ecosystem, automated testing is the bedrock of quality. For the Godot game engine, the community often uses the Godot Unit Test (GUT) library. To integrate this unit test library into modern development workflows, the godot-tester action was created, allowing developers to automatically run their unit tests as part of a CI/CD pipeline.
In late 2022, I encountered an issue when leveraging the godot-tester action on a pet project of mine. I noticed that the github issue covering the problem had been open for 5 months. To summarize, the GUT testing library had updated its test result format to JUnit XML, but the godot-tester action hadn't been updated to parse it. At this time, I learned that the github action was implemented as a single bash script. While I submitted a patch to the bash code to fix the immediate issue, I saw the deeper challenge: the project's complexity was outgrowing its bash foundation, making it difficult to maintain and adapt.
This became critical when a request for Godot 4 support was opened in November 2022. For months, the issue saw no activity. The technical hurdles were significant enough that modifying the monolithic script was a daunting task for potential contributors.
Seeing the community's need, I decided to take ownership of the modernization effort. Instead of a single "big bang" rewrite, I approached it as a pragmatic, three-stage evolution.
My first step wasn't to start adding new features, but to make the existing codebase manageable. I first tried to empower the community by detailing the required changes in the open issue, providing a clear path forward. After a month passed with no activity, I took ownership of the changes. I refactored the monolithic bash script, extracting logic into named functions and creating a lib/ directory. This made the code more readable and, crucially, lowered the barrier for any future contributions.
With the refactored script in place, I implemented support for Godot 4. This was done carefully, creating switch branches based on the user's specified Godot version (3.x or 4.x) which adjusted the script's behavior accordingly. This ensured that we could introduce a major new feature without breaking the workflow for the existing user base. This version was merged, successfully resolving a months-old community request.
Feeling like I was on a hotstreak after merging the Godot 4 support, I decided to address the project's remaining technical debt. While the bash script now worked, it was still a technical dead-end. It was difficult to test, prone to platform-specific issues, and not an ideal foundation for future features. A week after the Godot 4 support was merged, I proposed and executed a complete rewrite of the action in Node.js.
The new architecture was designed for maintainability, reliability, and extensibility:
graph LR
A((Start))--> B[Read Params/Args];
B --> DownloadGodot;
DownloadGodot --> RunGodotImport;
RunGodotImport --> ExecuteGUTTests;
ExecuteGUTTests --> AnalyzeTestResults;
subgraph DownloadGodot[Download Godot]
C1[Generate Download URL] --> C2[Perform Download];
C2 --> C3[Decompress ZIP];
C3 --> C4[Find Executable];
end
subgraph RunGodotImport[Run Godot Asset Import]
D1[Add Rebuilder Scene] --> D2[Run Godot in Headless Editor Mode];
D2 --> D3[Remove Rebuilder Scene];
end
subgraph ExecuteGUTTests[Execute GUT Tests]
E1[Construct GUT CLI Args] --> E2[Run Godot with GUT Script];
end
subgraph AnalyzeTestResults[Analyze Test Results]
F1[Read XML File] --> F2[Parse XML Data];
F2 --> F3[Calculate Pass Rate];
F3 --> F4[Compare with Minimum Pass/Max Fails];
end
AnalyzeTestResults --> CheckPassRate{Failure
threshold
met?}
CheckPassRate -->|Too Many Failures| Fail;
CheckPassRate -->|All passed| Success;
The migration from a legacy bash script to a modern Node.js application transformed the godot-tester action from a maintenance challenge into a stable and extensible platform.
This modernization effort shows how an iterative approach can breathe new life into a valuable open-source project. By breaking down the problem—first improving the existing script, then adding features, and finally rewriting for long-term health—the tool was updated to meet the community's current and future needs. The project is now in a much better position for continued community contribution and evolution.