Skip to content

Verify both Rails legs, and correct the example count #24

Verify both Rails legs, and correct the example count

Verify both Rails legs, and correct the example count #24

Workflow file for this run

# Copyright (c) 2026 Web Ascender. All rights reserved. Internal use only.
#
# WHY THIS EXISTS, beyond habit: this library's entire thesis is a set of forcing
# functions -- coverage_spec, completeness_spec, job_correlation_spec,
# pagination_spec, archive_spec, redaction_spec -- and CLAUDE.md says outright that
# "a change that makes any of those pass more easily is a regression". They force
# nothing if they only run when someone remembers. A library whose cardinal rule is
# "nothing goes missing without saying so" needs something saying so about itself.
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
spec:
name: rspec (Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}, PostgreSQL ${{ matrix.postgres }})
runs-on: ubuntu-latest
strategy:
# Every leg always runs: which combination broke is the whole point, so
# stopping at the first failure would throw away most of the answer.
fail-fast: false
matrix:
# 3.3 is the FLOOR (required_ruby_version), and it is here because testing
# only the development version leaves that claim unverified -- an adopting
# app would be the one to discover it was wrong.
#
# Which is exactly what happened: this leg was added as "3.2", matching the
# gemspec, and failed on SecureRandom.uuid_v7 being 3.3+. DESIGN §2.1 had
# said 3.3 was the hard floor all along; the gemspec was simply wrong. If
# this leg fails again, fix the code or raise the floor -- never drop it.
#
# Unpinned patch on purpose: 3.3.0 exactly cannot run Rails 8.1 (actionview
# uses `yield(*, **)` inside a block, which its parser rejects), so pinning
# would test a configuration nobody can use.
#
# 4.0.6 is what the library is developed on, and what .ruby-version and the
# reference application pin.
ruby: ["3.3", "4.0.6"]
# 16 is the FLOOR DESIGN §20 claims, and it is here for the same reason
# the 3.3 Ruby leg is: a floor nothing runs against is a guess. §20 says
# the design "targets PG 16 and requires nothing newer", and a code audit
# agrees -- the newest thing used anywhere is gen_random_uuid() (PG 13) in
# the benchmark task, and the partitioning is all PG 11-era. But agreeing
# with a grep is not the same as passing, which is the whole argument for
# the Ruby floor leg.
#
# If this leg fails, fix the code or raise the floor in DESIGN §20, the
# README and CLAUDE.md together. Dropping it is not one of the options.
#
# 18 is what the library is developed on, and what §20.2 records real wins
# from -- eager page freezing on insert-only partitions above all.
postgres: ["16", "18"]
# 8.0 is the FLOOR `rails ~> 8.0` claims, and it is here for the third
# time on the same argument: an unrun floor is a guess. This one had
# already been WRITTEN DOWN as a guess -- CLAUDE.md carried the standing
# note "Unverified claim, deliberately left standing: `rails ~> 8.0`
# admits 8.0, but CI tests 8.1.3.1 only", and the 0.1.0 CHANGELOG listed
# it under Known gaps. It was not a hypothetical gap either: Rails 8.0
# has no `Rails.event`, so AuditLog.notify takes a DIFFERENT BRANCH there
# (falling back to calling the subscriber directly), and that branch was
# the one every adopter on the floor would use and the only one nothing
# ran. event_transport_spec asserts which branch is taken, in both
# directions, so this leg proves the fallback works rather than merely
# that nothing raised.
#
# Adding it found one real breakage immediately: spec/dummy pinned
# `config.load_defaults 8.1`, which raises `Unknown version "8.1"` on 8.0
# before a single example loads. It now tracks the resolved Rails.
#
# "latest" is UNPINNED on purpose and is not a synonym for 8.1: it is the
# newest Rails the gemspec admits, so when 8.2 ships this leg tests it
# without anybody editing this file. Pinning both ends would quietly stop
# testing the ceiling the day it moved.
rails: ["8.0", "latest"]
# Six legs, not eight. Rails 8.0 runs on the two combinations worth
# having -- every floor at once (3.3 / PG 16), and the floor Rails on the
# development Ruby and Postgres -- while `latest` keeps the full 2x2 it
# has always had. The excluded pair would only re-test a middle.
exclude:
- ruby: "3.3"
postgres: "18"
rails: "8.0"
- ruby: "4.0.6"
postgres: "16"
rails: "8.0"
services:
# Both ends of the supported range, per DESIGN §20: 16 because that is the
# floor the design claims, 18 because that is what it is developed on.
#
# The gem itself uses nothing newer than PG 13. uuidv7() is PG 18 and is
# used by the REFERENCE APP's seeds, not by this library -- spec/dummy has
# no seeds at all. §20.3 records that ALTER TABLE ... MERGE PARTITIONS does
# not exist in any version, which is why rollup_year! is a hand-rolled
# copy-and-swap; that is a constraint, not a dependency.
postgres:
image: postgres:${{ matrix.postgres }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 10
env:
RAILS_ENV: test
# Read by the Gemfile. Empty means "resolve normally", which is the
# `latest` leg -- see the note there and the one in the Gemfile.
RAILS_VERSION: ${{ matrix.rails == '8.0' && '~> 8.0.0' || '' }}
# spec/dummy/config/database.yml reads all four, defaulting to the local
# non-standard port 5438. CI is an ordinary 5432.
AUDIT_LOG_DB_HOST: localhost
AUDIT_LOG_DB_PORT: 5432
AUDIT_LOG_DB_USER: postgres
AUDIT_LOG_DB_PASSWORD: postgres
steps:
- uses: actions/checkout@v5
# A PostgreSQL 18 CLIENT on every leg, including the 16 one. pg_dump refuses
# to dump a server NEWER than itself but is happy with an older one, so the
# 18 client covers both matrix legs while the runner's stock client-16 would
# fail the 18 leg. The runner image ships
# postgresql-client-16, and pg_dump refuses to dump a server newer than
# itself -- "aborting because of server version mismatch". That is a real
# hazard for this library rather than a CI detail: schema_format = :sql is
# mandatory here, so pg_dump is on the normal migration path, and the
# engine's PGTZ initializer exists precisely to control what it renders.
- name: Install the PostgreSQL 18 client
run: |
sudo install -d /usr/share/keyrings
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/pgdg.gpg
echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update
sudo apt-get install -y postgresql-client-18 libpq-dev
echo "/usr/lib/postgresql/18/bin" >> "$GITHUB_PATH"
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
# No Gemfile.lock is committed -- correct for a gem, whose dependencies
# must resolve against whatever the host app pins -- so there is nothing
# to key a bundler cache on.
bundler-cache: false
- name: Install dependencies
run: bundle install --jobs 4 --retry 3
# Fail loudly and early on the wrong server version rather than letting a
# partition or planner spec fail obscurely twenty minutes later.
# Asserted separately from the server version, because they fail differently
# and a mismatched CLIENT is the one that cost a build.
- name: Assert the pg_dump client is 18
run: |
echo "pg_dump: $(pg_dump --version)"
pg_dump --version | grep -qE " 18\." \
|| { echo "::error::pg_dump is not 18; schema dumps will refuse to run."; exit 1; }
- name: Assert the PostgreSQL version under test
run: |
version=$(PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -tAc "SHOW server_version")
echo "server_version = $version"
case "$version" in
${{ matrix.postgres }}*) ;;
*) echo "::error::Expected PostgreSQL ${{ matrix.postgres }}, got $version. See DESIGN.md §20."; exit 1 ;;
esac
# Same discipline as the PostgreSQL version assertion above: a matrix leg
# that silently resolved the wrong Rails would report a floor as tested
# when it was not -- the exact failure this leg exists to close.
- name: Assert the Rails version under test
run: |
version=$(bundle exec ruby -e 'require "rails"; print Rails::VERSION::STRING')
echo "Rails $version"
case "${{ matrix.rails }}" in
latest) ;;
*) case "$version" in
${{ matrix.rails }}.*) ;;
*) echo "::error::Expected Rails ${{ matrix.rails }}.x, got $version."; exit 1 ;;
esac ;;
esac
- name: Prepare the dummy app's database
working-directory: spec/dummy
# db:create + db:migrate, NOT db:prepare. structure.sql is deliberately
# git-ignored for this disposable app (db:migrate loads it in preference to
# re-running migrations, so a schema edit silently does nothing), which
# means the migrations are the only source of truth here.
run: bundle exec bin/rails db:create db:migrate
- name: rspec
run: bundle exec rspec --format progress
# The forcing function, through the rake path rather than the spec path.
# Both go through AuditLog::Coverage, and running each proves the wiring.
- name: audit_log:coverage
working-directory: spec/dummy
run: bundle exec bin/rails audit_log:coverage
# A gemspec warning is how a gem ships with an empty licence field or an
# unbuildable file list. This gem is proprietary and must never be pushed, so
# `gem build` is the only packaging step that ever runs -- keep it clean.
#
# DELIBERATELY STRICT, and it has already earned its keep: it caught the
# open-ended `rails >= 8.0` dependency, and only on the older Ruby leg,
# because that rubygems is stricter than the one 4.0.6 ships. If a future
# rubygems adds an advisory warning, the response is to fix the gemspec or
# consciously narrow this check -- not to delete it.
- name: gem build must be warning-free
run: |
output=$(gem build audit_log.gemspec 2>&1)
echo "$output"
if echo "$output" | grep -qi "WARNING"; then
echo "::error::gem build emitted warnings."
exit 1
fi
# LICENSE.txt must travel inside the built gem: it is the only thing stating
# the terms to whoever ends up holding the .gem file.
- name: Packaged gem carries the licence
run: |
tar -xOf audit_log-*.gem data.tar.gz | tar -tzf - | grep -qx "LICENSE.txt" \
|| { echo "::error::LICENSE.txt is missing from the packaged gem."; exit 1; }