How Github Actions can help you win a Hackathon

Ezequiel Cura
Capchase Tech
Published in
8 min readJun 21, 2023

--

by Lucas Crijns Bruno Ploumhans Oisín Morrison, based on their winner project for SQLillo Royale during #hackupc.

Last May we travelled to Barcelona from Switzerland to participate in HackUPC, Spain’s first student hackathon, as team OutLauz. At this hackathon Capchase presented an unusual challenge which immediately piqued our interest, standing in stark contrast to the other challenges involving supply chain optimisation, financial trading or developing games. Instead, they proposed a Fortnite like battle royale game with one simple goal: be the last one standing! Coming from mathematical backgrounds, this immediately sparked our interest as we felt we could use our maths skills to outwit the other teams.

The game had relatively simple rules that we had to learn to take advantage of. In this game, all players start at a random position in a 500×500 2D world. Players can move around in any direction in the world, using special abilities such as shooting bullets, melee attacks and dashing to kill and avoid other players. These abilities had cooldowns, so we had to learn how to make the most of these powers!

To play the game, Capchase presented an online platform where one could upload their playercode in Lua, which was then used to play subsequent games. We were faced with two problems from start:

  1. For every minor change we made, we would have to manually reupload our code.
  2. We would have to watch the newly played game to see if our code had actually improved our player performance!

Argh! these manual tasks would make it annoying to develop our agent. Fortunately, we are also engineers that know a thing or two about how web development works. Wouldn’t it be great if we simply reverse engineered the APIs underlying the platform? We can automate all of it!

Okay sure, we automate uploading and aggregating results, but how do we do that? Simple! We use Github actions to automate this for us: At every push we upload our code to Capchase’s platform and aggregate results, then displaying them in comments on the last commit. This way we could promptly see if a middle-of-the-night commit included a devastating or winning change.

Reverse engineering the API

It was the first night of the hackathon, and the rules of the game had not crystallised yet, as Capchase were fighting with their monolithic C codebase. Of course, this was the perfect time to reverse engineer an unstable API!

To do this, we can open the devtools of any browser and watch requests for saving code and loading recent matches. Let’s start with code uploads. There is a big blue save button for code:

Monitoring network requests when we press it, we see:

Aha! A HTTP POST action when we press the save button. That must be when the code gets uploaded. Indeed, when we check the JSON of the request, it contains the code we just typed in the browser.

Okay, so how do we do it now from, say, a Python script?

We can simply use the requests library to upload it. Unfortunately, there is a catch: We need to be authenticated to upload code. In the POST request we notice there is a specific authorisation header sent along:

Alas! We need an authorisation token. Where do we get that?

Probably during the login we obtained such a token. And yes, we find it upon login, straight after posting our credentials:

Now we have all the ingredients to upload our code:

  1. A way to get a token upon login, and
  2. a way to use that token to upload our code.

We are almost there. The only thing that remains is to get the last played match.

Going to the matches tab and monitoring requests, we find:

Right, it loads all available matches and we see that the JSON includes a timestamp for each match. Perfect, now we only need to download the results of the match to check who won. Monitoring again the requests, we find that:

A huge JSON file is being loaded to preview a match. This JSON file contains the moves of every player. This is what we need to determine who won. Using the IDs in the match overview, we can load the JSON file of the match at a specific URL.

With the API known, we can create Python scripts that do this for us. To see how we did that, look at our repository under ‘scripts’: https://github.com/lucashc/Voyager.

Github actions for the win

Once we have our Python scripts — `post.py` to upload our code, and `summary.py` + `get_ranking.py` to generate a markdown summary of the game and a computed ranking… the next step is to run them automatically with GitHub Actions!

As a first step, we start with the first script — `post.py`, and we just want to run it for each push to the `master` branch. The action workflow definition must be placed in the right directory, here ours is at `.github/workflows/push.yml`:

The first line gives a name to our workflow, which will be displayed in the action panel when the workflow runs:

Then, we define the “trigger” for the workflow, i.e. which conditions need to be met to start the workflow. In our case, we want to run the workflow for every push to the `master` branch. We chose to only trigger on the `master` branch to avoid prematurely uploading code that we were developing on separate branches. This is just one example, but GitHub offers many other triggers, so make sure to check out the official documentation.

Once our workflow has a trigger, we can define the steps of the workflow. Workflows are split into `jobs`. For now, we only need a single job to run our Python script. We name the job `upload`. Then we need to choose an Operating System for the job, here we pick `ubuntu-latest`. Under `steps`, we list the steps or “actions” that the job must execute. Here we have two steps:

  • First, we run the `actions/checkout@v2` action with the `uses` keyword. This is an action provided by GitHub (see https://github.com/actions/checkout) that will checkout the repository. This is required to access any file in the repository, and should probably be the first action of every job.
  • Second, we use the `run` keyword to directly execute a command — in our case we run the Python script. We also give the command a `name` that we will see in the Actions tab.

And there we have our first job:

Now, we want to run our summary scripts, and generate a comment containing the Markdown output. Here is what the end result will look like (truncated):

We know how to run code, but we do not know how to add a comment on a commit. Fortunately for us, someone else already published a reusable action that does exactly this: https://github.com/peter-evans/commit-comment. Passing a generated comment to it is a bit tricky, so let’s go through the steps.

First, we define a new job, `add-feedback-comment`. We add a `needs` clause to make sure that it runs after our previous `upload` job. Once again, it runs on the latest Ubuntu release, and the first step is to checkout the repository.

In principle, the two other steps are quite simple:

  • Run the Python scripts to produce the comment message body.
  • Give that to the `peter-evans/commit-comment@v2` action to create the comment.

However, passing a multiline string to a subsequent step is not as easy as one would expect. We can save task outputs by writing to the `$GITHUB_OUTPUT` file, in the `key=value` format. However, this doesn’t work for multiline strings. For that, we need to use a heredoc. We first output `key<<SOME_UNIQUE_IDENTIFIER` to start the heredoc, and then everything until the next occurrence of `SOME_UNIQUE_IDENTIFIER` is considered part of the value for the `key`.

Now we understand the content of the `run`. Note that each line corresponds to a different shell command:

  • Generate a random unique identifier for the heredoc. Here it is stored in the shell variable `$EOF`. For security, it is better to generate a unique identifier, otherwise a malicious user knowing the used identifier could end the heredoc prematurely and then write to any other variable.
  • Start writing to the `feedback` value by opening a heredoc.
  • Write the title of the commit message to `feedback`.
  • Run `summary.py` and add its output to `feedback`.
  • Run `get_ranking.py` and add its output to `feedback`.
  • Close the heredoc.

Additionally, we give a unique `id` to this step. This allows us to refer to the output using `${{ steps.<step identifier>.outputs.<key> }}`. In our example, we can access the output using `${{ steps.generate-commit-message.outputs.feedback }}`. This is then used for the comment action.

And that’s it!. This is just scratching the surface of what can be done using the very powerful GitHub Actions workflows. No excuse to not use them for your projects now!

Wait — we forgot the most important question! Did we win? Well, kind of, we won something for the use of GitHub actions, but were second in the Capchase challenge. Might be something to do with randomly hand-picking hyperparameters iteratively… Turns out, randomly hand-picking hyperparameters is worse than running a grid search algorithm on the parameter space. Who knew?!

--

--