Recently I implemented some scripts to automatically publish new versions of Bok’s Banging Butterflies to several platforms. In order to support this, I added a new step where I needed to update the version in the publish script itself. After my umpteenth near-miss forgetting to update the version in my publish script, and narrowly avoiding releasing a broken mod build, I decided enough was enough.
Introduction
If you’ve been modding for any amount of time, you’ve probably come across mc-publish, a tool that automates mod publishing. While this drastically reduces the amount of work I need to do to publish my builds to Github, CurseForge, and Modrinth, it does add a couple of steps when I’m porting the mods.
First, I have to remember to update the version number in the YAML script before I push any new code to a branch for a specific version. Second, as the versions are slightly different for each branch, they cause conflicts that I need to resolve every time I integrate the script. If I forget either step, especially the first step, I have to manually cancel the script on Github and start again before a bad version of the mod is released.
A major source of tech debt is when you add a small step like this. You can easily fall into the trap of thinking a small step like this is trivial, but when you end up with a dozen small steps like this they start piling up. Forget one step and you’re chasing runaway builds like a tipsy adventurer chasing a butterfly in the dark.
Identifying and automating small steps early on can save time later in development. If you notice yourself making small mistakes during a common task, it’s a good idea to ask yourself if there’s any way to automate it so the mistake can never happen again.
I wanted to get rid of these small steps. The only way to do that would be to have the script automatically extract the version. If you have a mod of your own and you’d like to also be able to skip this step, then read on to see my solution.
Manual Version Updates
In the original YAML scripts I wrote to automatically publish my mod, you’ll see these environment variables at the top:
env: MINECRAFT_VERSION: 1.20.2 JAVA_VERSION: 17 VERSION: 6.5.2
The current version is already defined in gradle.properties
, which stores properties used by the gradle tool to build the mod. You can find it under mod_version
within the Mod Properties section of the file:
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=All Rights Reserved # The mod version. See https://semver.org/ mod_version=6.5.3 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html mod_group_id=com.bokmcdok.butterflies
This is where I update the version manually based on semantic versioning. In this case, I increase the MAJOR version when I add an entirely new aspect to the mod (e.g. moths, villagers), the MINOR version when I add a new feature (a new butterfly, a new block, and so on), and the PATCH version when I fix a bug or for trivial updates that aren’t really a full feature.
What I needed to do was to extract the value of mod_version
from this file, and add it as an environment variable in my publish script.
Automating Version Extraction
The YAML script is set to run on Ubuntu, a Linux distribution, so we can add a step to our YAML file that will extract the value and store it as an environment variable. We have to run this after the checkout step, otherwise gradle.properties
won’t exist on the remote machine yet:
# Extract the mod version dynamically from gradle.properties for consistent versioning - name: Extract version from gradle.properties id: get_version run: | VERSION=$(grep '^mod_version=' ./gradle.properties | cut -d'=' -f2 | tr -d '[:space:]') echo "VERSION=$VERSION" >> $GITHUB_ENV
We use the pipe (|
) to run commands in succession based on the result of the previous command. So in this step, cut
will run on the result of grep
, and tr
will run on the result of cut
.
This uses grep
to search a file called gradle.properties
for lines that begin with “mod_version=
“. The caret (^
) anchors the search to the start of the line, so it won’t match if it finds “new_mod_version=
“, for example.
Next, the cut
command is used to extract the value of the property. The -d
parameter is telling us what delimiter to use when we split the string (in this case “=
“). The -f2
parameter tells the command to return the second field, which in our case would be everything after the delimiter (=
).
Finally, we remove any whitespace from the string using tr
. The delimiter (-d
) in this case specifies what we want to remove, i.e. all whitespace including spaces and tabs.
Wrapping the entire command with $(...)
allows us to take the result of the commands and assign them to a variable, in this case VERSION
.
Finally, we use echo
to add the new variable to $GITHUB_ENV
. This sets it as an environment variable that can be used in later steps.
Logging
Logging is one of the most important tools to a developer, especially with processes you can’t attach a debugger to. With the right logs, you can spot an error and its cause before you even look at a line of code. In this case, we want to make sure the extracted value is correct, and we can do this by adding a step that prints the extracted value:
# Display the extracted mod version for confirmation/debugging - name: Show extracted VERSION run: echo "Version is $VERSION"
Extracting Minecraft Version
While I was here, I figured I could also extract the Minecraft version I was working with. This would make it easier to port to newer versions in the future, as I could just use the same script. Unfortunately, there is a problem.
At some point NeoForge changed mapping_version
to minecraft_version
. Thankfully, the solution to this can be solved with a simple conditional statement:
# Extract the Minecraft version dynamically from gradle.properties for consistent versioning - name: Extract Minecraft version from gradle.properties id: get_mc_version run: | # Try to get mapping_version first MC_VERSION=$(grep '^mapping_version=' ./gradle.properties | cut -d'=' -f2 | tr -d '[:space:]') # If empty, fallback to minecraft_version if [ -z "$MC_VERSION" ]; then MC_VERSION=$(grep '^minecraft_version=' ./gradle.properties | cut -d'=' -f2 | tr -d '[:space:]') fi # Export the value as an environment variable echo "MINECRAFT_VERSION=$MC_VERSION" >> $GITHUB_ENV
This code is essentially the same as above, with the addition of a check to see if we successfully extracted a version already:
if [ -z "$MC_VERSION" ]; then
This line uses -z
to check if MC_VERSION
is of zero length. If mapping_version
wasn’t present in gradle.properties
, then the first grep
would fail, returning an empty string. This check would then pass, and the script would check for minecraft_version
as well.
Testing and Debugging the Script
Before running the scripts for the first time, I commented out the publishing steps so I didn’t accidentally publish an incorrect version. I’m glad I did this, as the scripts didn’t work first time. The code above is the result of testing and making sure all the variables were extracted correctly.
With steps to print out the extracted variables, you can see if the environment variables are set correctly by looking through the action logs in Github:

In this case we can see the VERSION
is 6.5.3
, and the MINECRAFT_VERSION
is 1.21.1
.
Now that this is all working, I no longer have to remember the extra step when publishing the mod, and I no longer have to worry about that extra conflict. I can just focus on the code, and Github will do the rest.
Before & After Automation
Here’s a summary of the steps I needed to go through to publish the mod before I added a publish script, with the old version of the script, and the new script that automatically extracts the versions.
Steps | Before Publish Script | Old Publish Script | New Publish Script |
Update Changelog and gradle properties | ✔️ | ✔️ | ✔️ |
Merge code to each Minecraft version branch | ✔️ | ✔️ | ✔️ |
Fix conflicts in publish script | ❌ | ✔️ | ❌ |
Fix conflicts | ✔️ | ✔️ | ✔️ |
Update code to support each Minecraft version | ✔️ | ✔️ | ✔️ |
Push code to remote branches | ✔️ | ✔️ | ✔️ |
Create a release in Github | ✔️ | ❌ | ❌ |
Update links on website | ✔️ | ✔️ | ✔️ |
Publish each version individually to CurseForge | ✔️ | ❌ | ❌ |
Publish each version individually to Modrinth | ✔️ | ❌ | ❌ |
Publish each version individually to PlanetMinecraft | ✔️ | ✔️ | ✔️ |
As you can see without the script there was a lot of extra work manually creating releases for Github, CurseForge and Modrinth.
The old version of the script saved a lot of time by getting rid of these steps, but added a couple of extra steps near the beginning of the process. Forgetting these steps, which I did a few times, can lead to a bad release. In these cases I cancelled the scripts in time, but if I hadn’t realised I’d have to manually delete releases.
The new version of the script automates these new steps, and means that I no longer risk publishing a wonky release purely because of versioning slip-ups.
What’s Next?
There are still some pitfalls with the script and rooms for improvement that I might work on in the future.
- Malformed Properties
The script assumes thatgradle.properties
is valid with no errors, and that version numbers will always be extracted. If the file is missing, or the text is malformed, this will cause errors later on in the script. While this is unlikely to ever be a problem, the script would be better if it explicitly exited with an error in cases where an empty string is returned when the extraction code is run. - Malformed Versions
The values for the versions themselves could also be malformed. Performing a regex on them to ensure that they are of a valid format could also be implemented to prevent problems that may come up in the future. - Java Version
On the surface there’s no easy way to extract the Java version from the project, so the script still has two different versions for pre- and post-1.20.5 Minecraft. If we can extract the Java version as well, then we can use the same script for all versions of the mod. - Better Parsing Tools
Usinggrep
is fine for this script, but we could use a specific tool for parsing configs in order to extract the values would be a more robust solution.
I plan to look into these improvements in the future, and if I do, I’ll share how I went about implementing them.
Can you can think of any other improvements I could make? Do you know any other automation hacks? Or do you have any comments, good or bad about my solution? If so, please let me know in the comments below!