The team’s repositories include a number of services that still build and
ship on Debian Jessie and Stretch. The new CI on GitHub Actions had to keep
building them, even though the underlying Node.js compatibility between
GitHub’s actions and the old glibc on those distributions was the central
problem. This post is about the stack of CI images we built, and the
workarounds we use to keep actions/checkout running on systems that
GitHub no longer expects you to use.
Why Old Debian Was Still There
Several production services are pinned to Jessie or Stretch because the business has not yet funded the rebuild against a newer Debian release. Rebuilding the package on Bookworm is straightforward; what is not straightforward is the regression testing that follows, especially for the Kamailio and SEMS components that talk to the carrier network. Until the product side gives the budget for that work, the build pipelines have to keep producing Jessie and Stretch packages from the same source as the modern ones.
The Image Stack
The CI images are organised in three layers per Debian distribution. A small base image installs the distribution itself with the right sources.list and a stable apt configuration. A generic image on top of that adds the tools the workflows need: build-essential, devscripts, debhelper, lintian, javahelper, maven, the JFrog CLI, the GitHub CLI, and a usable Node.js. Per-service variants on top of the generic image add service-specific dependencies for components like Kamailio or SEMS.
The full set covers Jessie, Stretch, Buster, Bullseye and Bookworm. There
is also a separate “runner image” used by the workflow shell itself,
based on node:lts-alpine, which carries bash, git, gh, the JFrog
CLI, jq, yq, xmllint, docker and docker-cli-compose. That image
is what the workflow steps execute in when they are not building a
package; the per-distribution images are what the package build steps run
in.
The Node.js Problem
Most of GitHub’s first-party actions are JavaScript actions: GitHub ships
a Node binary inside the runner and the action’s index.js runs against
it. When actions/checkout@v3 was the current version it shipped Node 16.
When actions/checkout@v4 released, it jumped to Node 20. Node 20’s
binary requires a newer glibc than Debian Jessie has, so @v4 simply
fails to start on a Jessie container. The workaround is to pin
actions/checkout to @v3 in every workflow that targets Jessie or
Stretch, with a comment explaining why so the next person to bump
dependencies does not break the build.
Inside the per-distribution image, the system Node is also too old to
run modern actions. The Jessie image installs Node 16 through NVM at
build time, because Node 16 is the latest line that still runs against
the glibc shipped with Jessie. Node 18 and Node 20 do not. The image is
careful to install Node into a path that the workflow PATH picks up,
because the action runner shells out to node and expects to find a
working interpreter.
Smaller Workarounds
A handful of small fixes accumulated over the year:
- The
ghCLI is installed from the upstream apt repository, not from Debian, because Jessie’s archive stops well beforeghexisted. libxml2-utilsis added to the per-distribution images because the workflows parsepom.xmlwithxmllint.default-mysql-clientis added to every distribution because some workflow steps need to talk to a MySQL server.- The slim runner image carries GNU
grep. The Alpine busyboxgrepdoes not support regular expressions in a way the workflows expect. git config --global --add safe.directory .runs early in every workflow, because modern git refuses to operate on a working tree that is not owned by the current user, and the container runs as root.
Added Later
The Bookworm distribution was added to the stack in November, so the
modern services now have a current Debian image to build against. The
Jessie and Stretch images stay where they are and continue to use
actions/checkout@v3. When upstream releases @v5 against Node 22 (or
newer), the same pattern will apply: the modern services move forward,
the legacy services stay pinned to whichever version still runs against
their glibc, and the comment in the workflow file explains why.