perf(ci): halve CI wall clock by parallelizing jobs and caching gems - #380
Open
matthv wants to merge 1 commit into
Open
perf(ci): halve CI wall clock by parallelizing jobs and caching gems#380matthv wants to merge 1 commit into
matthv wants to merge 1 commit into
Conversation
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on 🛟 Help
|
The lint matrix ran the same command 26 times: a single root .rubocop.yml governs the whole monorepo, so one `bundle exec rubocop` already lints all 840 files across the 13 packages. The `packages` dimension only changed which package bundle got installed, and `bundle exec` at the root restricts activation to the root Gemfile, so rubocop never saw those gems in the first place. TargetRubyVersion pins the parser, so the `ruby-version` dimension couldn't change the result either. Collapse it to a single job and drop the package install and unixODBC steps. Verified against a pristine BUNDLE_PATH holding the root Gemfile alone: rails, sqlite3, mongoid and activerecord are absent from the load path, and rubocop still exits 0 over all 840 files. Test no longer needs lint: the two are independent, and chaining them made the critical path the sum of both rather than the max. Deploy now lists lint explicitly, since coverage alone would let a release ship with RuboCop red. Cache the shared BUNDLE_PATH between runs, keyed on the Gemfiles and gemspecs rather than the gitignored lockfiles. The test key names only its own package's Gemfile-test, so a one-package PR doesn't miss on all 24 cells; it keeps every gemspec, since path deps mean a sibling's gemspec still affects resolution. Update the CLAUDE.md scaffolding checklist, which pointed at the lint matrix this commit deletes. 50 jobs -> 27. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
matthv
force-pushed
the
perf/ci-parallel-jobs-and-gem-cache
branch
from
August 27, 2026 12:49
2723c42 to
1c78eb1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Why
CI grew slow as datasources were added: 50 jobs, ~52 min of runner time, ~3 min 10 wall clock. Per-job breakdown from run
33058967442showed no slow job — the cost was job count plus uncachedbundle installeating ~60% of every job.What changed
Lint: 26 jobs → 1. A single root
.rubocop.ymlgoverns the monorepo, so onebundle exec rubocopalready lints all 840 files across the 13 packages — thepackagesmatrix ran the identical command 13× per Ruby version. The per-packagebundle installwas dead weight on top:bundle execat the root restricts activation to the root Gemfile, so rubocop never saw those gems anyway.TargetRubyVersion: 3.0pins the parser, so theruby-versiondimension couldn't change the result either.Verified against a pristine
BUNDLE_PATHholding the root Gemfile alone —rails,sqlite3,mongoid,activerecordall absent from the load path, rubocop still exits 0 over all 840 files.The unixODBC step goes with it (it only existed to build
ruby-odbcfor the snowflake bundle); it is retained in thetestjob, which still needs it.Lint and test now run in parallel. They are independent; chaining them made the critical path the sum rather than the max.
deploynow listslintexplicitly, since it previously depended on lint only transitively throughtest— without that, a release could ship with RuboCop red.Gem caching on the shared
BUNDLE_PATH, keyed on Gemfiles/gemspecs rather than the gitignored lockfiles. The test key names only its own package'sGemfile-testso a one-package PR doesn't miss on all 24 cells, while keeping every gemspec, sincepath:deps mean a sibling's gemspec still affects resolution.CLAUDE.md— the scaffolding checklist pointed at the lint matrix this PR deletes.Review notes
Verified and cleared:
hashFilesdegenerating to an empty key (all patterns match real files); a restored cache serving stale gems (impossible — no committed lockfiles, so bundler re-resolves every run and/tmp/bundleis only an artifact store);deploypublishing without lint/test green (theif:carries no status function, so implicitneedsgating holds); the qlty/ruby-versioncoupling (untouched);notify-ci-failure.yml(keys off the workflow name). Branch protection onmainhas no required status checks, so collapsing the 26Lint (x, y)contexts into one cannot wedge PRs.One accepted regression: no job resolves
packages/*/Gemfileany more — the test job installsGemfile-test, a different file in 11 of 13 packages. An unresolvable package Gemfile now goes green. Accepted because the signal covered 3 of 13 packages by accident of an inconsistency, tested a dependency edge absent from the published gemspecs (none of those three declares aforest_adminruntime dep), and never exercised PR code. A deliberate release-time install smoke test is the right home for that question.Follow-ups (not blocking, both pre-existing)
zendesk,mambu_paymentsandgraphql_hasuradeclare siblings withoutpath:in theirGemfile, unlike the other 10 — sobundle installin those packages resolves published siblings rather than the working tree.forest_admin_test_toolkitis published to RubyGems but has no specs, noGemfile-test, and now no CI entry at all.🤖 Generated with Claude Code
Note
Parallelize CI jobs and cache gems to halve wall clock time
/tmp/bundle": "- Adds gem caching to the test job keyed by OS, Ruby version, package, and Gemfile hashes, restoring to/tmp/bundleneeds: [lint], and addslintas a required dependency for deploydeploynow blocks onlintcompleting — if lint fails, deploy will not run even if coverage passesMacroscope summarized 1c78eb1.