How Interesting...
May 15, 2024

Distributing tools with Brew & GitHub Actions - Part II

Posted on May 15, 2024  •  8 minutes  • 1555 words
Table of contents

Over in Part I of this two-part article, we investigated sharing tools/apps/utilities using Brew. Now in Part II, we’ll complete the solution by automating away the chore of creating releases ourselves with GitHub Actions.

Automate releases with GitHub Actions

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform. It allows developers to automate the build, test, and deployment pipeline for their applications directly from their GitHub repositories. We can use GitHub Actions to automate some of the chore, so that we can better spend our time on developing our tools instead.

Fortunately, GitHub Runners come pre-installed with the GitHub CLI and Ruby.

Brew Formula File Template

To automatically create our Brew formula file, we need to convert it to a Ruby Template that we can call as part of the workflow. Below you will find an example for the joke app.

Filename: joke.erb

 1class Joke < Formula
 2  desc "Prints a random joke on the command-line"
 3  homepage "https://github.com/chris-gillatt/homebrew-joke"
 4  url "https://github.com/chris-gillatt/homebrew-joke/releases/download/v0.0.<%= ENV["GITHUB_RUN_NUMBER"] %>/joke-0.0.<%= ENV["GITHUB_RUN_NUMBER"] %>.tar.gz"
 5  sha256 "<%= `sha256sum dist/joke-0.0.#{ENV["GITHUB_RUN_NUMBER"]}.tar.gz | awk '{printf $1}'` %>"
 6  version "0.0.<%= ENV["GITHUB_RUN_NUMBER"] %>"
 7
 8  depends_on "curl"
 9  depends_on "figlet"
10  depends_on "cowsay"
11  depends_on "lolcat"
12  depends_on "coreutils"
13
14  def install
15    bin.install "joke"
16  end
17end

As you can see, most of the file is the same as before, with a few key differences:

NOTE - There is a gotcha here. If you’ve been following this guide throughout, you will already have version 0.0.1 of your app published as a release in GitHub. The value of GITHUB_RUN_NUMBER starts at 1 so the first run of the workflow will fail. You can either run the workflow again until GITHUB_RUN_NUMBER becomes higher than your latest version number of your app, or increment the minor version of the app in the template, such as 0.1.1. You could also write a bit of bash that performs some maths to ensure that the version number is where it needs to be and place it in the workflow.

Identity and Access Management (IAM)

Not so long ago I would have recommended creating a GitHub Personal Access Token (PAT) and placing it into a secret to be read by the CI/CD system of choice. However, I’ve discovered a more elegant solution in the form of simply granting write permissions on our repository to the workflow via the GitHub Runner (the term for a worker agent of GitHub Actions).

This conveniently avoids the need to create and manage a PAT or SSH key to allow the committing of new files back to our repository. GitHub hosted runners are ephemeral only shared with other members of the same organisation. They’re created for the job they are allocated for and are destroyed on completion, so they should be more than secure enough for our personal projects. For the purposes of organisational use, you can always deploy your own self-hosted GitHub Runners and manage them internally, as the runners themselves are Open Source!

Granting Permissions

To grant permissions to the GitHub Runner, we need to change Workflow Settings

  1. In GitHub, navigate to: Settings > Actions / General > Actions permissions / Workflow Permissions

  2. Under Workflow Permissions, ensure the Read and write permissions is selected, and click Save.

That’s it, unless you wish to further fine-tune this permission within the workflow file itself.

Git Config

When working manually, our Git config is set up for our personal user that appears in the git logs. This as a minimum requires a name and email address to identify us. When shifting this responsibility to the GitHub Runner, we could choose instead to have it identify itself so it’s less confusing when looking back at git commits and the git log. This will make things easier to debug too if something goes wrong at a later date.

There’s a special user and email address that’s been created specifically for this purpose. We’ll use this in the workflow shortly.

"github-actions[bot]"
"41898282+github-actions[bot]@users.noreply.github.com"

Creating a Workflow

A workflow in GitHub Actions terms is just a manifest file in yaml format that describes a set of actions. If you’re familiar with CI/CD principles (especially if you’ve used CI/CD platforms like Ansible) you’ll be right at home here. Within this file we tell GitHub Actions where to find our files and what to do with them to produce a result.

Workflows exist within the .github/workflows path, relative to your GitHub repository. You can learn more specifically about GitHub Actions, workflows etc via the official documentation .

Here’s an example workflow file for joke:

Filename/path: .github/workflows/ci.yaml

 1name: ci
 2
 3# Controls when the workflow will run
 4on:
 5  # Triggers the workflow on push or pull request events but only for the master branch
 6  push:
 7    branches: [ main ]
 8  pull_request:
 9    branches: [ main ]
10
11  # Allows you to run this workflow manually from the Actions tab
12  workflow_dispatch:
13
14# A workflow run is made up of one or more jobs that can run sequentially or in parallel
15jobs:
16  # This workflow contains a single job called "build"
17  build:
18    # The type of runner that the job will run on
19    runs-on: ubuntu-latest
20
21    # Steps represent a sequence of tasks that will be executed as part of the job
22    steps:
23      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
24      - uses: actions/checkout@v4
25
26      # Runs a set of commands using the runners shell
27      - name: Create Release 
28        env:
29            GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30        run: |
31          # Use the GitHub Bot user for Git and GitHub IAM.
32          git config user.name "github-actions[bot]"
33          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
34
35          # Specify name of app for packaging & distribution purposes; Compress to a tar.gz.
36          export APP="joke"
37          mkdir dist
38          tar cf dist/"${APP}-0.0.${GITHUB_RUN_NUMBER}.tar.gz" "$APP"
39          
40          # Generate Brew formula file using the template file.
41          erb "${APP}.erb" > "${APP}.rb"
42          
43          # Commit the new complete formula file back to the repository.
44          git add "${APP}.rb"
45          
46          # Commit and push file to repository.
47          git commit -m "$APP release 0.0.${GITHUB_RUN_NUMBER}"
48          git push
49
50          # Create and publish release to repository. 
51          gh release create v0.0.${GITHUB_RUN_NUMBER} ./dist/*.tar.gz --title "0.0.${GITHUB_RUN_NUMBER}" --generate-notes          

As can be seen above, I’ve annotated the workflow and embedded script to explain what’s going on. As the script is only a few lines, I’ve chosen to write it directly in the workflow for repository simplicity. Should there be more work to do, I would place the script into a file of its own and call it separately as part of the run command.

NOTE - We already gave permission to the workflow (via the GitHub Runner) to be able to commit back to the repo as part of Identity and Access Management (IAM) above. However, this does not grant permission to the gh (GitHub CLI) app. Therefore, we can use the existing permission by reusing its credentials as can be seen on lines 28-29 (highlighted). A GitHub runner has its credential stored automatically inside variable GITHUB_TOKEN, and GitHub CLI uses one named GH_TOKEN. Because we already granted elevated permissions, we can use them again.

One last thing…

The last thing to do is to commit and push the workflow file and the Ruby template file. Doing so will automatically kick off the workflow, and if you’ve done everything correctly, a new release will appear.

You will notice the GitHub Bot user now appears as a contributor now. This is because it created the new Brew formula file and automatically pushed it back to your repo!

🚀 Nice work

Troubleshooting

Click to expand for Troubleshooting

Workflow reports error 403

This usually means you’ve forgotten to click save when granting workflow permissions. Check Granting Permissions above.

Workflow reports exit code 1: a release with the same tag name already exists

If you’ve followed along from beginning to end, you will have already published a release containing v0.0.1 and the variable we’re using to automatically increment the version number (GITHUB_RUN_NUMBER) starts at 1. You can just run the workflow again as the second run will become 2. For more information, check the note on Brew Formula File Template .

Wrap Up

Over the course of this two-part series, we’ve covered a whole lot, so well done if you’ve made it this far.

This is just the tip of the iceberg for both Brew and GitHub Actions, which are incredibly cool and powerful tools. For me, it’s been enjoyable and rewarding to experiment with them both, and I highly recommend doing so for yourself.

I hope you enjoyed reading, and have fun playing around with your own projects! As ever, feel free to drop any comments below.


📷 Cover photo by Ionela Mat on Unsplash under the Unsplash license.
Socials / Links

I tweet tech, bad jokes and silly memes