Skip to main content
Version: 3.2.0

CI workflows

The GitHub Actions workflows in .github/workflows/*.yml are generated. The source of truth is a single Ruby script, support/ci/generate, which builds every workflow and writes the YAML.

Do not hand-edit the generated YAML. Any manual change is overwritten on the next regeneration, and CI rejects the drift. Change the script instead.

Making a change​

# 1. edit support/ci/generate
# 2. regenerate the YAML in place
make generate-workflows
# 3. commit the script AND the regenerated .github/workflows/*.yml together

Drift guard​

make verify-workflows regenerates the workflows into a temporary directory and diffs them against the committed .github/workflows/. A non-empty diff means the YAML has drifted from the script, and the command exits non-zero.

The same check runs in CI (verify_workflows.yml) on every pull request that touches support/ci/** or .github/workflows/**, so a hand-edited workflow fails before it can be merged. verify_workflows.yml is itself hand-written — the generator does not produce it — and is excluded from the diff.

What the script produces​

Three job flavours per gem, plus a standalone devenv smoke test:

BuilderJobRuns onPurpose
release_test / contrib_testtestubuntuunit tests across the matrix
release_mutate / contrib_mutatemutatemacosincremental mutation testing
release_coverage / contrib_coveragecoveragemacosfull mutation coverage

release_* builders are for the gems released from the repository root; contrib_* are for the gems under contrib/. The full list lives in the workflows method at the top of the script.

The matrix DSL​

Each job's strategy.matrix.include is built from small combinators (backed by the szczupac gem):

  • axis(name, *values) — one dimension, e.g. ruby_version("ruby-4.0", "ruby-3.4"). Named helpers wrap it: ruby_version, bundle_gemfile, database_url, data_type.

  • sparse(*axes) — a sparse matrix. It varies one axis at a time against the others' first (baseline) value. This is not a full cartesian product:

    sparse(ruby_version("4.0", "3.4", "3.3"), bundle_gemfile("G", "R1", "R2"))
    # => 5 rows, not 9:
    # {ruby 4.0, G} {ruby 3.4, G} {ruby 3.3, G} # ruby varies at the baseline gemfile
    # {ruby 4.0, R1} {ruby 4.0, R2} # gemfile varies at the baseline ruby

    It keeps the matrix small while exercising every value at least once. sparse also composes: passing already-built row lists as "axes" sparsely combines whole groups (see the ruby_event_store-active_record workflow).

  • union(*groups) — set union (with dedup) of row lists, for stitching several sparse blocks into one matrix.

Need the full cartesian product instead? Use Szczupac.permutation in place of sparse.

Output directory override​

By default the script writes to .github/workflows/. Set WORKFLOWS_ROOT to redirect it (this is how make verify-workflows generates into a temporary directory):

WORKFLOWS_ROOT=/tmp/wf ruby support/ci/generate