release: also publish an image on Ruby HEAD with ZJIT on
Each release now pushes a second image, built from the same Dockerfile
on rubylang/ruby:master-noble with RUBYOPT=--zjit, tagged
<release>-ruby-head-zjit and ruby-head-zjit. It runs in a job of its own
so a HEAD that breaks holds up neither the release nor the deploy.
The Dockerfile takes the base as RUBY_IMAGE, restates the official
image's gem layout for a base that lacks it, and installs what the
HEAD bundle needs to compile nokogiri, sqlite3 and google-protobuf
from source.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHtenXohDhpqANgMby5weB
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 19d97fa..8bbaec0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,6 +86,52 @@ jobs:
# false, so the tag cannot ride the leftover git credentials.
run: gh release create "${STEPS_META_OUTPUTS_VERSION}" --target "$GITHUB_SHA" --generate-notes
+ # The release again, on Ruby HEAD with ZJIT on, tagged
+ # <release>-ruby-head-zjit and ruby-head-zjit. A job of its own so a
+ # HEAD that will not build or install the bundle holds up neither the
+ # release nor the deploy.
+ build-head:
+ name: Build on Ruby HEAD with ZJIT
+ needs: build
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read # checkout
+ packages: write # push container image to GHCR
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+ - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
+ - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
+ id: meta
+ with:
+ images: ghcr.io/${{ github.repository }}
+ flavor: latest=false
+ tags: |
+ type=raw,value=${{ needs.build.outputs.version }}-ruby-head-zjit
+ type=raw,value=ruby-head-zjit
+ - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
+ with:
+ context: .
+ # Pulled fresh each time: HEAD is the point, so a cached base
+ # would be yesterday's Ruby.
+ pull: true
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ RUBY_IMAGE=rubylang/ruby:master-noble
+ RUBYOPT=--zjit
+ VERSION=${{ needs.build.outputs.version }}-ruby-head-zjit
+ cache-from: type=gha,scope=ruby-head-zjit
+ cache-to: type=gha,scope=ruby-head-zjit,mode=max
+
deploy:
name: Deploy main preview
needs: build
diff --git a/Dockerfile b/Dockerfile
index d52f9eb..4376cd3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,19 @@
-FROM ruby:4.0-slim AS build
+# The Ruby both stages build on. A release also builds this file on
+# rubylang/ruby:master-noble, Ruby HEAD as the core team builds it every
+# few hours, configured with --enable-zjit (see ci.yml's build-head).
+ARG RUBY_IMAGE=ruby:4.0-slim
-RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
+FROM ${RUBY_IMAGE} AS build
+
+# The official image's own gem layout, restated for a base that has
+# none: rubylang/ruby installs gems beside Ruby and keeps bundle config
+# in ~/.bundle, neither of which the runtime stage copies.
+ENV GEM_HOME=/usr/local/bundle BUNDLE_APP_CONFIG=/usr/local/bundle BUNDLE_SILENCE_ROOT_WARNING=1
+
+# pkg-config, patch and zlib, for the HEAD build: no precompiled
+# nokogiri, sqlite3 or google-protobuf is cut for an unreleased Ruby, so
+# its bundle compiles them from source.
+RUN apt-get update && apt-get install -y build-essential pkg-config patch zlib1g-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -9,7 +22,10 @@ RUN bundle config set without development && bundle install
COPY . .
-FROM ruby:4.0-slim
+FROM ${RUBY_IMAGE}
+
+ENV GEM_HOME=/usr/local/bundle BUNDLE_APP_CONFIG=/usr/local/bundle BUNDLE_SILENCE_ROOT_WARNING=1
+ENV PATH=/usr/local/bundle/bin:$PATH
# The image's own tag, and the release's name (see ci.yml) — the one
# build fact the app reads back out, in the admin footer. It carries the
@@ -20,6 +36,10 @@ ENV VERSION=${VERSION}
# rackup defaults to development, which wraps the app in Rack::Lint and
# ShowExceptions.
ENV RACK_ENV=production
+# Empty but for the HEAD build, which passes --zjit so the app runs
+# under ZJIT without anyone having to ask for it.
+ARG RUBYOPT
+ENV RUBYOPT=${RUBYOPT}
# Set TZ at runtime — Date.today drives the birthday list's rollover.
# git, for the commit `rake db:dump` makes of what it wrote
@@ -32,6 +52,10 @@ WORKDIR /app
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /app /app
+# An image tagged for ZJIT checks it really has it, rather than trusting
+# the base was configured with it.
+RUN case "$RUBYOPT" in *--zjit*) ruby -e 'abort "ZJIT is off" unless defined?(RubyVM::ZJIT) && RubyVM::ZJIT.enabled?' ;; esac
+
EXPOSE 9292
# 0.0.0.0 reaches only the container's own network namespace. Exposure to