Ever since I started working on Ren’Py projects and automating their build and deployment, I have had issues with the actions available to push files to Itch.io using Butler. Those issues are many-fold:
To illustrate this, we can consider the last release of Colors at the time of writing. The total action runtime is 1m50s, the total upload-related runtime is 43s, more than a third of the total runtime. Additionally, you can see under this block of text the part of the script used for the release. It’s a wall and a nightmare to maintain, with a lot of argument redundancy between each step. It’s also very fickle as the file’s path has to match the actual distributed file, which changes with each game version number in RenPy1.
- name: Push windows version to itch.io
uses: manleydev/butler-publish-itchio-action@v1.0.3
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
ITCH_GAME: colors
ITCH_USER: kiminako
CHANNEL: win
PACKAGE: target/Colors-${{ inputs.version_number }}-win.zip
VERSION: ${{ inputs.version_number }}
- name: Push linux version to itch.io
uses: manleydev/butler-publish-itchio-action@v1.0.3
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
ITCH_GAME: colors
ITCH_USER: kiminako
CHANNEL: linux
PACKAGE: target/Colors-${{ inputs.version_number }}-linux.tar.bz2
VERSION: ${{ inputs.version_number }}
- name: Push mac version to itch.io
uses: manleydev/butler-publish-itchio-action@v1.0.3
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
ITCH_GAME: colors
ITCH_USER: kiminako
CHANNEL: mac
PACKAGE: target/Colors-${{ inputs.version_number }}-mac.zip
VERSION: ${{ inputs.version_number }}
All existing actions on the marketplace that push to Itch use the same strategy: Download butler into a small container and add a thin env-based wrapper script to exploit it. This strategy is systematically implemented in a way that does not allow to upload multiple files in a single step and adds an initialisation step at the start of each release run.
Honorable mentions:
As all actions that push rely on the exact same mecanisms to perform the action, using one instead of the other can’t provide significant improvements. Here is the detail of the runtime of the release of Colors mentionned in the introduction:
Label | Duration |
---|---|
Set up job | 2s |
Build yeslayla/butler-publish-itchio-action@v1.0.3 | 22s |
Run actions/checkout@v3 | 10s |
Get Ren’Py from cache2 | 1s |
Build distribution | 50s |
Push windows version to itch.io | 9s |
Push linux version to itch.io | 6s |
Push mac version to itch.io | 6s |
Other steps | 4s |
Total | 110s |
No available action felt satisfactory, so I considered building my own. It needed to provide at least one of the following without worsening the other:
There are three options to implement GitHub actions: as a containerized script, as a Javascript project, or as a composite action. Composite actions might help with the first requirement but would forgo the second as the implementation of a loop would become particularly complicated. Containers guaranty a better compatibility accross platforms but add a set-up overhead, while Javascript usually has a smaller overhead but forces to handle platform discrepancies in code.
In this case, most of the overhead is in the set-up, and moving the download to a step handled by a Javascript project would allow to cache the downloaded Butler executable’s file for quick retrieval in later releases3 while removing the cost of the creation of a Docker image and making the implementation of more advanced file handling easier.
I wanted to have the ability to independantly install butler if needed in a specific script4 and avoid needlessly re-downloading it, should the action be used more than once5. The downloaded Butler executable had to change depending on the current platform and the desired Butler version6, and its installation directory could later be added to the PATH
for later commands. Though it is not implemented in the current release, Itch also provides signature files that could be used to verify the downloaded Butler archive in future versions.
Regarding the files pushed to Itch, the main issue that caused redundancy in the configuration was the need to associate channels to pushed files to ensure the proper upstream files were updated.
The following is the state diagram I had when I started writing the implementation:
My next step was to determine what my user requirements were:
PATH
.GitHub inputs have to be strings. This limitation is not a big issue when the code expects a boolean or a number as the conversion is pretty straightforward, however more complex structures like arrays need specific conventions (actions sometimes use the JSON format to support specific input data). In this case, considering that channels can only be mapped to one file at most and have a limited character set, I decided that users would declare them in the same parameter with the following rule:
This resulted in a multiline string of channel and file patterns that could be mapped to an array of channel+file tuples.
Hence, the configuration requirements could transparently be translated into option classes:
The implementation was uneventful thanks to the NPM packages in GitHub’s action toolkit:
The only implementation detail of note is the generation of the file/channel association, as the user might not provide one and the script attempts to build one in such a case and raises an error if more that one file is going to be pushed on a single channel:
This new action allowed me to drastically reduce the configuration required to push multiple files to itch. The twenty-seven lines required to push three files have now become height and does not need to change for most projects or when adding release targets. At the same time, the overall time it takes to initialize the step and push all files is now 17 seconds (from 43 seconds) for the same action, mostly thanks to the fact that the action does not waste 20+ seconds at the start of the job to create its docker image.
- name: Push to itch.io
uses: Ayowel/butler-to-itch@v1.0.0
with:
butler_key: ${{ secrets.BUTLER_CREDENTIALS }}
itch_game: colors
itch_user: kiminako
files: target/*
version: ${{ inputs.version_number }}
Not setting config.version
or setting it to an empty string is an efficient way to make the name of the file generated by Ren’Py more stable. ↩
The same cache is used in an action that lints the code each time a commit is pushed, so the cache is guaranteed to exist at release-time. ↩
The Butler archive weighs 10Mo, so the actual advantage of retrieving the file from cache compared to a direct download is more theoretical that practical. ↩
Beyond the command that allows to push files to Itch, Butler provides several commands to handle and verify files - most of which probably wouldn’t be relevant in an action. ↩
Implementing this differentiation also meant that the script could later evolve to handle more Butler commands if needed without breaking existing scripts. ↩
Pratically, there is pretty much no reason that I’m aware of to ever use an outdated Butler version, but this was an option in some other actions and was close to free to implement as a feature. ↩