Automating Swift command line tool releases with GitHub Actions

Sponsored

Helm logo
Helm for App Store Connect

A native app for App Store Connect to make managing your apps faster, simpler and more fun. Get early access now!

I have recently open-sourced a Swift command line tool called chatty that allows you to use Open AI’s ChatGPT directly from the terminal.

Along with the opportunity to release an open-source project, I spent some time looking into how I could automate the release process of my new command line tool using GitHub Actions.

The release process

I wanted to make chatty’s release process as simple and automated as possible while still giving users an established and popular way of installing the executable.

For this reason, I decided to distribute my command line tool via two different channels:

  1. Homebrew, a popular macOS package manager widely used to install command line applications.
  2. GitHub releases, where users can download artifacts directly. Homebrew uses this method to install executable tools.

Note that chatty is only available on macOS (Intel and Apple Silicon) but I am planning on adding support for Linux and Windows in a future release 👀.

As the project is open-source and GitHub offers unlimited free minutes for public repositories, I decided to use GitHub Actions as my CI/CD provider, which in turn allowed me to make use of its wide ecosystem of community-driven actions to simplify the process.

Creating a release workflow

The first step to setting up a GitHub Actions workflow is to create a new file in the .github/workflows directory of your repository. This file can have any name you want, I decided to call mine release.yml.

Trigger on every tag push

Once I had the workflow file in the correct directory, I added a push event with a filter to only trigger the workflow when a release tag (v*.*.*) is pushed to the repository:

release.yml
name: Release

on:
  push:
    tags:
      - v*.*.*

Building the executable for release

To distribute the application, I first needed to build it for the architectures I wanted to support (macOS arm64 and x86_64).

To do this, I needed to tell the workflow to run on the latest macOS version, check out the repository and build the executable product with two architecture slices using swift build:

release.yml
name: Release

on:
  push:
    tags:
      - v*.*.*

jobs:
  release:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - name: Build executable for release
        run: swift build -c release --arch arm64 --arch x86_64 --product chatty

Creating a GitHub release

I then needed to create a GitHub release with the tag ref as its name and a compressed archive of the previous step’s artefact attached:

name: Release

on:
  push:
    tags:
      - v*.*.*

jobs:
  release:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - name: Build executable for release
        run: swift build -c release --arch arm64 --arch x86_64 --product chatty
      - name: Compress archive
        run: tar -czf ${{ github.ref_name }}.tar.gz -C .build/apple/Products/Release chatty
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          files: ${{ github.ref_name }}.tar.gz
          token: ${{ secrets.GITHUB_TOKEN }}

The naming of the tarball archive is important as it is used to determine the URL of the Homebrew formula’s executable in the next section.

As shown in the workflow above, I found that using the softprops’ action-gh-release action was the easiest way to create and customise a GitHub release.

Creating a new version of the Homebrew formula

As I mentioned earlier in the article, I wanted to make chatty available on Homebrew as it is the most popular way of installing command line tools on macOS.

To distribute my tool on Homebrew, I first had to publish a formula. I reused an existing Homebrew tap that I had created for another project and added a brand new formula pointing to an existing executable of chatty:

Formula/chatty-cli.rb
class ChattyCli < Formula
  desc "A command line application to interact with ChatGPT directly from the terminal"
  homepage ""
  url "https://github.com/polpielladev/chatty-cli/archive/v1.0.2.tar.gz"
  sha256 "a258e0d6d96488bbcb01b51a97e2370185c0207aea7a31565f48716060eabf56"
  license ""
  version "1.0.2"

  def install
    bin.install "chatty"
  end
end

The formula has a description and a URL to the release archive with the corresponding SHA256 checksum. It also has an install method which tells Homebrew to install an executable called ‘chatty’ (found at the root of the tarball) to the user’s Homebrew binaries directory.

While this worked fine, I wanted to automate this process and streamline it as much as possible. In my research to find a good way to do this, I came across this great GitHub action by Mislav Marohnić which, among others, is used by Fastlane to automatically update their Homebrew formula on every release.

I then executed this action directly after creating the GitHub release in my workflow:

release.yml
name: Release

on:
  push:
    tags:
      - v*.*.*

jobs:
  release:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - name: Build executable for release
        run: swift build -c release --arch arm64 --arch x86_64 --product chatty
      - name: Compress archive
        run: tar -czf ${{ github.ref_name }}.tar.gz -C .build/apple/Products/Release chatty
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          files: ${{ github.ref_name }}.tar.gz
          token: ${{ secrets.GITHUB_TOKEN }}
      - uses: mislav/bump-homebrew-formula-action@v2
        with:
          formula-name: chatty-cli
          homebrew-tap: polpielladev/homebrew-tap
          base-branch: main
          download-url: https://github.com/polpielladev/chatty-cli/releases/download/${{ github.ref_name }}/${{ github.ref_name }}.tar.gz
        env:
          COMMITTER_TOKEN: ${{ secrets.CHATTY_COMMITTER_TOKEN }}

I had to give the homebrew action the name of the formula (formula-name), the name of my personal Homebrew tap (polpielladev/homebrew-tap) as it otherwise defaults to Homebrew/homebrew-core, the name of the branch to commit the new formula to in the tap repository (main) and the download URL for the executable archive.

As the action needs to commit to a separate repository, I had to create a new personal access token with the public_repo scope (you might need to create it with the repo scope if your tap is hosted in a private repository), add it to the repository secrets as CHATTY_COMMITTER_TOKEN and give it to the action through an environment variable called COMMITTER_TOKEN.

Now, every time I push a new tag to the repository, the workflow will create a GitHub release, build the executable for macOS arm64 and x86_64, compress it into a tarball archive, upload it to the release and create a new version of the Homebrew formula.

Where to go from here

As I mentioned earlier, chatty is completely open source, so you can check out the full workflow in the GitHub repository.

Likewise, if you have any suggestions on how to improve this release process, please feel free to open an issue or a pull request or reach out to me on Twitter.