Why I am using conda instead of rbenv to install Ruby inside of the CI job that builds this static site.

The problem

I wanted to make the GitHub Action that builds this static site more portable by replacing the parade of third-party GitHub Actions with simple shell scripts.

With a self-contained build process, I can match my local build workflow to the one used to deploy the site from GitHub without going to (IMO) extremes like running the GitHub Actions Docker image locally. It also makes it easier to migrate my hosting away from GitHub Pages if I ever should desire to.

However, in my previous attempts to make a portable build script, I kept having difficulty with what should be the easiest step: Installing a Ruby version of my choice (currently v3.2) and pulling in all of Jekyll’s dependencies.

The normal solution

Most of the guides I consulted recommend rbenv, which is a Ruby version manager that lets you specify your desired Ruby version in a .ruby-version file and install it with rbenv install.

Unfortunately, installing rbenv itself is not that simple: On Debian stable and Ubuntu 22.04 (which is currently ubuntu-latest in GitHub Actions), the version of rbenv in the repos is too far out of date to install the version of Ruby I want. The rbenv developers recommend using the rbenv-installer script to install the latest rbenv from Git instead, but then the rbenv-installer README advises against usage in a CI pipeline, which is exactly what I want to do:

For automating installation across machines it’s better to avoid using this script in favor of fine-tuning rbenv & ruby-build installation manually.

This gets at another difficulty with rbenv, which is that it builds Ruby from source when you rbenv install a version that isn’t in your local cache. This means installing ruby-build in addition to a bunch of dynamically linked development libraries. On some of my workstations, I struggled and failed to put together a functional ruby-build toolchain; on my laptop, I finally got it working, only to have the build fail because I ran out of memory.

For those who have the time and computational resources to compile from source, rbenv is a great tool, but for me, what I really needed was a precompiled binary. (Thus, I also ruled out RVM, another popular recommendation.)

The conda solution

Enter conda-forge::ruby. Conda is a cross-platform package management and virtual environment system that is generally associated with the Python world (because it is written in Python) but supports generic binary packages. I used to be skeptical of conda because the most popular distributions (Anaconda and Miniconda) are proprietary, but Miniforge is an open-source alternative that covers all my use cases.

After installing conda (and mamba because why not), I created build scripts configure.sh, build.sh, and so on that simply wrap mamba create and mamba run. The GitHub Action that builds the site is now basically just this:

./configure.sh  # Create the conda environment and run bundle install
./info.sh       # Log package versions
./build.sh      # Build the site
./check.sh      # Lint scripts with ShellCheck; might add more checks

This workflow takes an average of two minutes to run on the GitHub cloud runners, which makes it faster, on average, than the parade of GitHub Actions I had before, which took about three minutes (except when running the workflow several times in a row, in which case caching would bring the runtime down to 20 or 30 seconds).

Alas

Alas, my GitHub Action workflow still has one third-party dependency: The Setup Miniconda action. Despite the name, it can set up either Miniconda or Miniforge, and it is a little easier to work with than Miniforge’s interactive installer script.

I think this is a sustainable solution, although those are probably bold words from the guy who just set all this up last week.