Designing Our Unified Release Process

I'm building Caffeine, a compiler written in Gleam that compiles service expectations to reliability artifacts. This post documents how we release new versions of Caffeine across our various distribution channels.


In the Beginning: A Single CICD Pipeline

The initial use case for Caffeine was to plug in to a CICD-esque Github Action at my day job to maintain our SLOs. In the "Origin Story" post I detail how we went from Terraform to a Ruby Terraform generator, to an early Caffeine prototype in Ruby, to Caffeine itself. The workflow for developers was a bit like this:

(1) Modify some simplified representation of SLO state in a Github Repo.

Making Some Changes

I didn't use the cafe to write Caffeine here... but nothing is stopping you ๐Ÿค”

(2) Put up a PR.

Putting up a PR

Merging the PR to then kick off CICD.

(3) CICD executes the compiler against this input, generates Terraform, and applies it (with Terraform state stored locally in the repo).

Github Actions View

Successful CICD executions.

(4) Users then navigate and see the changes to SLOs in Datadog.

Datadog SLO View

An overview of SLOs generated by Caffeine.

To more easily facilitate this effort, I created a Github Action that enables folks to leverage this workflow and I detail how it came to be in my "Packaging Caffeine" blog post.

For better or worse, this Github Action just fetches the latest release, which for Caffeine, is automated via a Github Action whenever I push a new release like so:

git tag v0.2.10
git push origin v0.2.10

So we were set! All use cases were covered by some automation. No thinking required.

Very nice gif


Onward and Upward (or rather Outward)

Homebrew ๐Ÿ โ˜•๏ธ

As I began to roll out tooling to more coworkers, I needed to use Caffeine in different environments. First off, the whole go install a binary process wasn't as easy as I'd have liked. "Oh yeah dude just go to my release page, unzip, etc. etc." Not great. Not a pit of success. To solve this we just needed to leverage existing distribution mediums, a.k.a. OS package managers. Now I'm, at times, a "coolness-driven-development" guy. Who doesn't like to feel ๐Ÿ˜Ž about the things they build. However, this is (not yet) my main line of work so I needed to stay pragmatic with a YAGNI mindset. Thus, since where I work is a MacBook shop, I targeted Homebrew. Actually ended up being super easy: see repo and a brief note on how I did it here.

So, if you're on a Mac or Linux (x64), you're in luck!

Initial install:

brew tap brickell-research/caffeine
brew install caffeine_lang

Updating for a new version:

brew update
brew upgrade caffeine_lang

But I Don't Wanna Install Your Bits

Time is always of the essence (and if that theme of business hits home, I'd recommend one of Oliver Burkeman's books). So while I have a decent reputation at my place of work and even have the homebrew setup above, expecting people to install tools before even demonstrating any value is an obstacle to adoption. Thus, I set up a playground (named the cafe) on the Caffeine website. This enabled me to say "hey, you have five minutes?" and very quickly demonstrate what exactly Caffeine was and how it worked.

Check it out here: the cafe. Pairs well with the docs.

I do have plans to make this a full tour at some point but since the language is very much in its 0.X.Y phase of rapid development, this feels like a more useful post 1.0.0 launch when things are stable.

Ok So What's the Problem?

We now have four different distribution channels:

  • Caffeine compiler releases (main repo) - automated by Github Action โœ…

  • Official Github Action - automatically just picks up the latest version โœ…

  • Homebrew - script to download SHA checksums and copy-paste โŒ

  • The Cafe - script to build and copy over from local copy of Caffeine โŒ

Only two of the four are automated. While not the most pressing issue right now, it's something I'm interested in solving as it's become annoying to do a bunch of copy-pasta and script wrangling on every release.

Note: This has since been solved! See the update below for how we now handle all five distribution channels automatically.


Brainstorming

Idea #1: Scripted Locally

While it may be attractive to find a fancy automatic solution, we could also just leverage what we already have and script a complete local solution that pulls all the right bits to then push up to the various distribution channels we have.

The main issues here are:

1. accidentally using different versions (i.e., building the cafe from a stale branch instead of the latest)

2. forgetting to run the scripts

As one might expect, the tradeoff here is simplicity vs. consistency and correctness. For at least a dozen releases, this solution was completely acceptable.

Idea #2: Cross Repo Insanity?

Ok bear with me. Some context to my initial approach requires mentioning the "C" word.... cryptocurrency.

Stick with me

A previous project of mine was a visual programming language for smart contracts. The way this worked was we had a common intermediate representation (IR) of smart contracts that we could compile smart contracts to, manipulate via a limited frontend interface, then compile back to smart contracts from the modified IR.

Digicus LLVM Diagram

The LLVM of Smart Contracts?

In a different life I may have taken this project all the way to the point of it being the LLVM of smart contract land (that was the vision...), however, adoption was exceptionally limited as smart contracts, unlike Scratch require serious security and correctness and thus a visual programming language abstraction that peeled out these bits and made it deceivingly easy for beginners to get started wasn't as effective as I initially hoped.

Yet, from this work, I figured out a way to automatically distribute new versions of various plugins that powered this programming environment.

1. From a single repository (for some reason called bean stock), I would add each plugin as a submodule.

2. Each plugin repository had a Github Action to remotely trigger bean stock's module update action, pulling the latest versions of each submodule.

3. Finally, whenever I rebuilt the main application, it would clone the bean stock plugin directory and have the latest versions.

The fairly complex architecture all together looked a bit like this:

Digicus Architecture Diagram

Bean Stock in the middle there.

An obvious issue here is that we may not necessarily get the latest version of a plugin if the bean stock repo didn't also kick off a new build of the main application.

Anyway, back to Caffeine...

The lesson of this story is you can find ways, via Github Actions, to coordinate releases between repositories.


The Initial Naive Solution (for now)

Turns out if you combine idea #1 and idea #2, we can land on a pretty reasonable approach: essentially, by baking in the logic of idea #1 into the already existent release.yml Github Action within the main caffeine compiler repo, we can then remotely execute direct branch pushes with the appropriate changes to both the homebrew repo and the website repo.

What I landed on here is functional, but it feels like there is almost certainly room for improvement. For example, the homebrew release process could probably leverage the Homebrew Releaser Github Action or possibly somehow the more mature GoReleaser, however, no matter how naive, my solution here is simple and doesn't require any third party dependencies ๐Ÿคทโ€โ™‚๏ธ.

Briefly, the two automated release processes work like this:

# Homebrew
1. calculate the SHAs
2. clone existing `homebrew-caffeine` repo
3. generate the ruby formula
4. configure git and then push to the main branch

# Caffeine Lang Website
1. build browser bundle with appropriate shims and such
2. clone the website repo
3. copy over our new bundle
4. update the version file
5. rebuild the website (gleam build)
6. configure git and then push to the main branch

At time of writing, this is our solution. It does its job, it's easy to grok, and it's only ~200 lines of code... feels like a win!


Update: Hex Publishing (December 2025)

With Caffeine reaching 1.0, it felt like time to add another distribution channel: Hex, the package manager for the Erlang ecosystem. While Caffeine is primarily distributed as a CLI binary, publishing to Hex enables Gleam developers to use Caffeine as a library dependency directly.

Adding Hex to the Pipeline

Hex publishing turned out to be the simplest addition to our release workflow. Since gleam.toml already contains all the metadata Hex needs (name, version, description, license, repository), publishing is just:

gleam publish --yes

The HEXPM_API_KEY environment variable (sourced from a GitHub secret) handles authentication. We added this step right after creating the GitHub release but before updating Homebrew and the website.

Making It Idempotent

One thing I wanted to ensure was that the entire release pipeline could be re-run safely. If a release partially failed (say, Homebrew updated but the website didn't), I wanted to be able to re-trigger the workflow without it failing on already-completed steps.

For Hex, this meant handling the case where a version is already published:

- name: Publish to Hex
  env:
    HEXPM_API_KEY: ${{ secrets.HEX_RELEASE_KEY }}
  run: |
    VERSION=${{ steps.version.outputs.version }}
    echo "Publishing caffeine_lang v${VERSION} to Hex..."
    if gleam publish --yes 2>&1; then
      echo "Published to Hex successfully"
    else
      echo "Hex publish failed (version may already exist) - continuing"
    fi

We applied similar idempotency patterns across the pipeline:

  • GitHub Release: Check if release exists before creating

  • Hex: Gracefully handle "already published" errors

  • Homebrew: Only commit if the formula actually changed

  • Website (The Cafe): Only commit if files actually changed

Now our five distribution channels all update atomically on git push origin vX.Y.Z, and re-running is always safe.

Current Distribution Channels

  • โœ… Caffeine compiler releases (main repo) - automated

  • โœ… Official Github Action - automatically picks up latest

  • โœ… Hex - automated

  • โœ… Homebrew - automated

  • โœ… The Cafe - automated

All five channels. All automated. One git tag and git push ๐Ÿค˜.