Managing a game dev community with GitHub Actions
A Little Game Called Mario is an open source, collectively developed hell project. Anyone and everyone is welcome to contribute their unique talents to make both the player and developer experience more enjoyable. Find out how the collective leverages GitHub Actions to manage this wonderful little community.
This is a guest post from @lazerwalker (Emilia Lazer-Walker). Emilia is a Toronto-based artist/engineer and Senior Cloud Advocate at Microsoft. Most of her work focuses on using nontraditional interfaces to reframe everyday objects and spaces as playful experiences and to inspire people to become self-motivated learners.
When Izzy first told me about her idea for the project A Little Game Called Mario, I was immediately sold on it.
game idea: open source collective hell game called "A Little Game Called Mario"
anyone can commit changes to the repo (which auto-builds and releases each time) so any and all contributors are considered to have done "work" on the game
— izzy kestrel (@iznaut) March 27, 2022
The concept was simple: she made a barebones 2D platformer using the open-source game engine Godot, released it as open source on GitHub, and announced publicly that any reasonable contributions would be accepted. As soon as a pull request is merged, a new version of the game is built and uploaded to the indie game platform itch.io.
Intimidated by calling yourself a “game dev”? No worries! Make the smallest change you can think of, and you are now a published game dev on a large project. Want to add a small little visual effect, add physics to Mario’s mustache, or change the game from a platformer into a Dance Dance Revolution-style rhythm game or a block-pushing puzzle game? All contributions are welcome!
As I started to get involved, we quickly faced a hard problem. We knew that “success” for this project would mean attracting a large number of contributions from people who weren’t familiar with git or GitHub, many of whom are non-coders (such as artists, designers, musicians and sound designers). At the same time, a high volume of contributions also meant that every merged pull request would potentially lead to merge conflicts with people actively working on changes. Since the people whose branches would now be in conflict were, again, likely not familiar enough with git to know how to fetch new changes and merge or rebase them. This would lead to pull requests stacking up as people required technical assistance to get their changes merge-ready again. We knew early on that we would need to lean heavily on automating as much as possible to try to make contributions painless for newcomers, even more than most open-source projects. In this post, I’ll walk through some of the ways we use GitHub Actions to help make our project more accessible for newcomers to make contributions.
Automatically build and publish the game
The first piece of infrastructure that Izzy built was a GitHub Actions workflow that ensured the copy of the game on itch.io always matched the codebase on GitHub, fulfilling part of the core promise of the project.
She used two different GitHub Actions for this: one to build the Godot game and one to upload to itch.io. This workflow is now a bit more complicated, but the original version was fairly straight-forward. Whenever new code hits the main branch, the workflow checks out the repository, builds the game, and uploads the game to itch.io. Both the build step and the upload step were easy to configure. Building the Godot project in the current directory just requires linking to the version of the Godot engine you want to use, and uploading to itch.io just requires a few config options and an API key.
View the workflow file for this
Credits and contributors
Early on, it was important to make sure we were crediting everyone as visibly as possible. Seeing how many people have contributed is a strong social signal that this is a fun, vibrant community that you should join, and seeing your name added to the game as soon as your changes get merged is a key part of feeling like you’re now part of that community.
We added a badge to the README from https://shields.io/ that showed the number of contributors the project had at any given moment, and we sketched out a build step to our GitHub Actions workflow that would programmatically generate a text file containing every user in git commit history, to be shown as part of an in-game credits screen.
This was an early victim of the chaos of such a community-driven project. Someone edited our GitHub Actions build workflow so that it pulled contributor data from git and stored it in a text file, but didn’t hook that up in-engine. At the same time, someone else built a credits screen that contacted the GitHub API at runtime to fetch credits. Eventually, we consolidated all of these into a single system. Before building the game, GitHub Actions uses the GitHub contributions API to write a list of contributors to a file on disk that the game loads when you open the Credits screen.
View the workflow file for this
An important note on inclusivity: although both git commit history and the GitHub API contain contributor information, we very consciously chose to use the GitHub contributors API to use GitHub usernames rather than git commit names. If someone wants to change their name in the credits (for example, they’ve transitioned, and previous contributions were made under their deadname), updating a git-based credits screen would require rewriting git history, which can be disruptive for an open source project. Conversely, pulling real-time data from the GitHub API helped us ensure that we were always referring to people how they wanted to be.
This crediting solution also had ramifications for how we designed our contribution flow for non-coders. We wanted artists and musicians to be able to drop assets into the project without having to wire them up in-game themselves, but figuring out how to do a sort of “drop box” ended up being difficult. We decided that encouraging these contributors to use a proper GitHub workflow (creating a fork, uploading their files, and opening a pull request) was the best strategy, even if a lot of these contributors had no experience with either git or GitHub. There were other reasons for this decision as well, but a big part of it was being able to point to the GitHub contributions API as the sole source of credit truth, rather than having to maintain alternate credit lists for art contributions.
Preview builds
Probably my favorite feature we’ve added to our workflow is our preview builds. Any time anyone opens a pull request, we automatically build the game with their changes, upload it to a Netlify deploy preview, and drop a link to that public website containing the new copy of the game as a pull request comment.
That link is also automatically posted to Twitter and a channel in our community Discord. This both broadcasts new changes to the community and makes it easy for a code reviewer to test out changes and confirm they actually do what the author says! This has been key in helping us not merge in code that breaks the game or doesn’t do what the author intends.
Building this required dealing with a particularly tricky part of GitHub Actions: the security model of the pull_request
trigger.
The simplest version of this workflow would trigger whenever a pull request was opened or pushed to, and it would just build the game and then upload that build. However, the workflow environment has access to sensitive environment variables, such as our Netlify deploy keys and the automatically-provided GitHub auth token that has write access to the our repo.
For a public repository like ours, where anybody can fork the project, open a pull request, and trigger a GitHub Actions workflow run within the context of our project, that can be a huge security vulnerability. All it takes is one bad actor to write some Godot script that happens to access the GITHUB_TOKEN
environment variable, and suddenly they have the ability to push arbitrary code to our repository without our permission and without us having merged their changes. That’s not great!
GitHub naturally has a solution for this. By default, workflows triggered via the pull_request
trigger don’t have access to your repository secrets or ENV variables. If you are confident your workflow is safe from this sort of attack, simply switching to the pull_request_target
trigger will grant you full environment access. But that wasn’t our situation, as we weren’t confident someone couldn’t exploit our build step. Instead, we implemented the solution recommended in this GitHub blog post. The pull_request
workflow was responsible for performing the “unsafe” game build and uploading the result (a bundle of HTML and JS) as a build artifact. Then, another workflow would start whenever that first workflow completed. This second workflow had full access to our repository secrets, at which point it would pull down the generated code, build and upload it to Netlify, and add the Netlify link to the pull request as a comment. This feels a bit clunky, especially given we eventually had to add various text files to that uploaded artifact to pass metadata to the second build (such as the pulll request number, title, and author). But it works! As far as I know, as long as you’re running unsafe code in this way, this is really the only viable way to deal with this potential security vulnerability.
View the workflow file for this
Even one of the core Godot maintainers called out this preview build workflow as a cool feature he hopes more games will adopt.
The CI that lets you test PRs right away is amazing, well done!
I foresee many open source Godot games interested in replicating this, it's really cool.
— Rémi Verschelde (@Akien) April 13, 2022
Linting GDScript
An increasingly common pattern in open source web development projects is programmatically enforcing a consistent style guide for your code. This is a great way to make sure your code reviews don’t get too deep in the weeds arguing about stylistic choices. Whatever the computer thinks is “correct” looking code, that’s what should get merged in. We thought it would be a great fit for our project if we could enforce a style guide on GDScript, the Python-like programming language Godot uses.
You typically see style checks like this implemented one of two ways: either the project is configured to use git pre-commit hooks (usually using a tool like husky) to automatically run every time a contributor makes a git commit (and fail to commit if the linter fails), or there is a GitHub Actions step that will fail the build if you don’t match the code style. Regrettably, neither of these were particularly good fits for A Little Game Called Mario.
The pre-commit hook option would have required all contributors to do a non-trivial amount of local developer tooling setup. The only GDScript linter I could find requires a functioning Python environment, which isn’t otherwise needed by Godot. Asking contributors to set up Python felt like a big ask.
Similarly, while a linter step in GitHub Actions would have worked, we wanted to streamline the process as much as possible for newer programmers less familiar with coding collaboratively. Asking people to make stylistic changes themselves in response to code review, while perfectly fine for more professional code projects, wasn’t an ideal workflow for us. We were also running into a Godot-specific issue: GDScript is a language where whitespace is significant, and the editor allows you to automatically convert tabs to spaces (or vice versa) on save, but there’s no way to store whitespace settings within the project itself. So people who had no familiarity with these problems would blithely open pull requests that modified the entire project’s whitespace. Before adding in a code workflow that auto-formatted code, we spent a lot of time explaining the intricacies of Godot’s whitespace system and editor defaults to new contributors who didn’t necessarily want to have to deal with that to just get their changes merged.
This all led us to wanting an auto-linter that proactively made changes, instead of simply telling people what changes to make themselves, was the correct choice for us.
We used this GDScript auto-formatter, and specifically the gdformat
command it offers. It’s a simple tool — most notably, it doesn’t let us set any style preferences ourselves, and whatever the authors of this tool think is “proper” GDScript is what we get. My philosophy is, even if I might disagree with individual stylistic choices it enforces, the fact that we have a consistent enforced style is far more important to me than what the specific style guide might be.
We initially hoped that a GitHub Actions workflow could run whenever a pull request was opened or updated and directly make an auto-formatter commit to that pull request branch. We ran into issues with that, likely related to being a public repository and the permissions of a bot account pushing new commits to other users’ forks. We landed on running our auto-formatter script every time code was merged into the main branch. It’s unfortunate that we don’t get to test out preview builds with auto-formatted code before merging them, but this seems like an acceptable compromise, and we’ve yet to run into an issue of the auto-formatter causing errors that broke our build.
The explosion of community-generated GitHub Actions workflows
Our initial GDScript formatter was such a success, it led to community members contributing other similar GitHub Actions workflows that provided some level of compile-time safety about our code.
One of the systems that has been added to A Little Game Called Mario is an interactive narrative system, which stores dialogue data in a JSON format. Someone built a JSON Schema specifying our dialogue format, and added a GitHub Actions workflow that validates that schema, using a premade schema validator action that automatically fails the build and posts a comment if the JSON doesn’t validate.
Another system we have is one that runs some checks to make sure that collision layers are set up properly for all objects added to our Godot scenes. I think this is a particularly great example of our community coming up with its own solution to solve a unique game development problem. Managing collision layers is an important “housekeeping” task for a game engine like Godot, and the workflow logic itself is written directly in GDScript.
…and whatever comes next!
Seeing so many of our recent workflows and process improvements come from our community brings me so much joy. It’s one thing to design a collaboration workflow for others to use; it’s another to see that same community take collective ownership over the way they work. Having worked with many CI/CD systems before, I love how accessible GitHub Actions is. Having our workflows be simple YAML scripts that live directly inside our git repository, and being able to easily call out to composable actions that others in the community have built, makes it easy for our contributors to feel empowered to tweak our workflows and contribute their own.
If you’re interested in checking out any of the build steps I talk about here, or want to contribute something yourself, checkout A Little Game Called Mario’s GitHub repo! Pull requests are happily accepted 😉
Tags:
Written by
Related posts
Highlights from Git 2.47
Git 2.47 is here, with features like incremental multi-pack indexes and more. Check out our coverage of some of the highlights here.
Leading the way: 10 projects in the Open Source Zone at GitHub Universe 2024
Let’s take a closer look at some of the stars of the Open Source Zone at GitHub Universe 2024 🔎
The 10 best tools to green your software
Looking for ways to code in a more sustainable way? We’ve got you covered with our top list of tools to help lower your carbon footprint.