Every time I make a new release of Bok’s Banging Butterflies, I have to manually upload it to multiple sites. While this doesn’t take too long to do, it is tedious and repetitive, especially now I’m updating the mod more frequently. This week, I discovered a way to automate the process, so from now on I can focus more time on development.
A New Shortcode
Every time I release a new mod, I need to update the links on the mod page with new download links. It’s awkward and fiddly since I have to edit 7 links in a row. I finally got annoyed enough that I decided to automate the process.
To do this, I need to implement my own shortcode. Then, I can just use a shortcode on the page and specify a version and it will generate the links for me. The first thing I did was look at the HTML that was already there by right-clicking and inspecting the element. This is what it looks like with some formatting applied:
<ul class="wp-block-list"> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.21.4.jar">NeoForge 1.21.4</a></li> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.21.1.jar">NeoForge 1.21.1</a></li> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.20.4.jar">NeoForge 1.20.4</a></li> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.20.2.jar">Forge 1.20.2</a></li> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.20.1.jar">Forge 1.20.1</a></li> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.19.2.jar">Forge 1.19.2</a></li> <li><a href="https://github.com/doc-bok/Butterflies/releases/download/6.4.4/butterflies-6.4.4-for-1.18.2.jar">Forge 1.18.2</a></li> </ul>
Simple enough. My next step was to write a PHP function that would generate a single list item (<li>
). In my functions.php I add the following function:
function generate_bok_butterfly_release_link($mod_version, $mod_loader, $minecraft_version) { return '<li><a href="https://github.com/doc-bok/Butterflies/releases/download/'.esc_html($mod_version).'/butterflies-'.esc_html($mod_version).'-for-'.esc_html($minecraft_version).'.jar">'.esc_html($mod_loader).' '.esc_html($minecraft_version).'</a></li>'; }
This looks a little messy, but all it does is take in the mod version, the mod loader being used (i.e Forge/NeoForge), and the version of minecraft the mod is for. Even though all calls to this method will come from my own scripts, I still use esc_html
to ensure any data it uses is clean.
The function to generate the links is a lot easier on the eyes:
function generate_bok_butterfly_release_links( $attributes ) { extract(shortcode_atts(array( 'version' => '0.0.0', ), $attributes)); $output = '<ul class="wp-block-list">'; $output .= generate_bok_butterfly_release_link($version, 'NeoForge', '1.21.4'); $output .= generate_bok_butterfly_release_link($version, 'NeoForge', '1.21.1'); $output .= generate_bok_butterfly_release_link($version, 'NeoForge', '1.20.4'); $output .= generate_bok_butterfly_release_link($version, 'Forge', '1.20.2'); $output .= generate_bok_butterfly_release_link($version, 'Forge', '1.20.1'); $output .= generate_bok_butterfly_release_link($version, 'Forge', '1.19.2'); $output .= generate_bok_butterfly_release_link($version, 'Forge', '1.18.2'); $output .= '</ul>'; return $output; }
Since I intend to use this as a shortcode, it takes a single parameter, $attributes
, that holds all the attributes the shortcode can apply. For my purposes I only need the mod version, so I extract
that attribute at the start of the function.
Then I use the function above to generate a link for each version of Minecraft. The last thing I need to do is register this function so it can be used as a shortcode. I decide to use bok_butterfly_release_links
as my shortcode name:
add_shortcode( 'bok_butterfly_release_links', 'generate_bok_butterfly_release_links' );
Now, instead of modifying seven links every time, I can simply use a shortcode block and enter in the following:
Now all the links will be generated for me. With this new method, each new release is as simple as changing this one value. It also makes it easier to add support for a new version of Minecraft. I can just add another call to generate_bok_butterfly_release_link
to the shortcode function, and it will appear automatically!
This makes my life a little bit easier, but I wanted to go further. I wanted to automate releasing the mod across all sites, not just my own.
mc-publish
I already knew that CurseForge and Modrinth had APIs. I just hadn’t gotten around to investigating how to use them to automatically upload mods. I knew it would be a time investment, and I hadn’t found the time to invest into it yet. But since I was getting bored of uploading 7 files one after the other to each of these platforms, I decided it was finally time to do the research.
The first thing I discovered was that someone else had already done the work for me (or rather, for everyone). There is a tool called mc-publish that will automatically build and upload your projects to GitHub, CurseForge, and Modrinth for you.
The tool can be added to any GitHub project as an Action, which are operations that GitHub runs after certain triggers. A trigger can be anything from pushing to a branch, accepting a pull request, or just letting the user trigger them manually.
Once added to your project they will run automatically, and you won’t need to worry about them unless they fail to run for some reason. If they fail, you’ll get an email, and then you can look at fixing them.
YAML
The way to add an action is to create a YAML script under your .github/workflows/
folder. YAML is essentially a configuration file that tells a service to run specific actions with specific parameters. If you’re using NeoForge you’ll already have a build.yml
that tells GitHub to build every time you accept a pull request.
I don’t want to trigger a publish during development, so I don’t add any scripts to my development
or main
branches. I only add them to the version specific branches. You can see the script I use for 1.20.2 here, but I’ll go through it line by line.
The first thing we do is give the script a name. This name is what will be displayed in GitHub when the action runs. It’s there purely for human reference, so make it different to any other scripts you may have in your workflow.
name: Publish on GitHub, CurseForge & Modrinth #The name of your GitHub Action on github.com
Next we tell GitHub when we want the action to execute. Based on my way of working, I want the action to be performed when I push to a branch, but for most projects you’ll only want pull_request
and workflow_dispatch
.
on: [ push, pull_request, workflow_dispatch ] #When your GitHub Action will be executed ('pull_request' -> on every Merge(/Pull) Request; 'workflow_dispatch' -> allows manual execution through github.com
Next we set up the env
variables. These are essentially globals that will be used by the rest of the script. Here we also reference several tokens. These are passwords and should never be published to your GitHub repository. Instead, you can store them as secrets on a per-project basis. Look under Settings->Secrets and Variables->Actions and you can add them here, then reference them under secrets
.
env: #Environment variables that can later be referenced using ${{ env.MINECRAFT_VERSION }}. These are useful for repeating information and allow for quick changes for new mod updates MINECRAFT_VERSION: 1.20.2 JAVA_VERSION: 17 VERSION: 6.4.4+1.20.2 RELEASE_NAME: Bok's Banging Butterflies v6.4.4 MODRINTH_TOKEN: ${{ secrets.PUBLISH_MODRINTH_TOKEN }} CURSEFORGE_TOKEN: ${{ secrets.PUBLISH_CURSEFORGE_TOKEN }} GITHUB_TOKEN: ${{ secrets.PUBLISH_GITHUB_TOKEN }}
Next we tell GitHub what permissions we want to allow the script to have. In this case, we need to be able to write as we will be publishing to GitHub.
permissions: contents: write
Finally we get to the jobs
. Here we tell GitHub what actions we want to perform. The first action is usually to check the environment variables.
jobs: #The place where you actually tell the GitHub server what to do. build: #To publish your mod you only need one 'job', here it is called 'build'. runs-on: ubuntu-latest #'runs-on' specifies the operating system (linux). steps: #Under 'steps' you will list all the individual commands, such as MC-Publish by Kir-Antipov. - name: Check Environment Variables run: env
The next part of the script tells GitHub to build the project. I won’t go into detail here, but this would use the same actions as your build.yml
if you have one.
- name: Checkout Repository uses: actions/checkout@v4 with: submodules: true - name: Setup Java uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "${{env.JAVA_VERSION}}" - name: Make Gradle Wrapper Executable if: ${{ runner.os != 'Windows' }} run: chmod +x ./gradlew - name: Build run: ./gradlew clean build
And now we get to the actual tool. Under uses
we specify Kir-Antipov/mc-publish@v3.3
, which tells GitHub we want to use the tool from the mc-publish
repository, as well as the version we want to use. At the time of writing, 3.3 was the latest version so we use that one.
The rest of the configuration just tells mc-publish
the project IDs and the tokens needed to publish each project. This is the minimum code needed, as the tool automatically figures out what type of project you are running, what to call it, where the changelog is, and a lot of other things. Honestly, it’s damned impressive how powerful this tool is.
- name: Publish (CurseForge/Modrinth/GitHub) uses: Kir-Antipov/mc-publish@v3.3 #The specified MC-Publish GitHub Action in the version 3.2 with: curseforge-id: 929419 #The id of your CurseForge project curseforge-token: "${{env.CURSEFORGE_TOKEN}}" modrinth-id: hUw80ZZs #The id of your modrinth project modrinth-token: "${{env.MODRINTH_TOKEN}}" github-tag: "v${{env.VERSION}}" github-token: "${{env.GITHUB_TOKEN}}"
I include some extra configuration at the bottom of my YAML script, but you most likely won’t need this if you want to add it to your own projects. I’m just a stickler for details.
name: "${{env.RELEASE_NAME}}" version: "${{env.VERSION}}" version-type: release changelog-file: CHANGELOG.md #The file in which the changes of your new update are specified (the path starts at your project root) loaders: forge game-versions: "${{env.MINECRAFT_VERSION}}" java: "${{env.JAVA_VERSION}}"
Planet Minecraft
So that just leaves one more site that I like to publish my releases to. Unfortunately there is no way to automate publishing as Planet Minecraft has no API. I’ll have to keep doing it the old fashioned way. Still, all the other work I’ve done this week will save me a ton of time. At least now I only have to manually publish to one site instead of five.
But the most important thing is that from now on I can mostly focus on just writing code.