Skip to content

ci: populate bundler caches at default-branch scope so PRs get cache hits #1674

Description

@jcouball

Problem

continuous_integration.yml triggers only on pull_request. GitHub Actions scopes a cache written during a PR run to that PR's ref (refs/pull/<n>/merge), and only caches at default-branch scope are readable by every branch. Because CI never runs on a push to main, it never writes a cache any other PR can read, so nearly every PR pays a cold bundle install on nearly every job.

The one exception is Ruby 4.0, and only by accident: release.yml uses ruby-version: ruby, which currently resolves to 4.0.6, and it runs on pushes to main. The v5.0.4 release populated that cache.

Evidence

From the first (cold) run on the ci/split-lint-job-from-matrix branch, run 31216830767 attempt 1 — Setup Ruby accounted for 293s of 925s total:

Job Setup Ruby Cache
Ruby 3.2 on windows-latest 109s miss
Ruby jruby-10.0.0.1 on ubuntu-latest 54s miss
Ruby truffleruby-24.2.1 on ubuntu-latest 45s miss
Ruby 3.2 on ubuntu-latest 41s miss
Lint and Docs (Ruby 3.4) 35s miss
Ruby 4.0 on ubuntu-latest 9s hit

Attempt 2 of the same run, with the PR-scoped caches now warm, took 731s total versus 925s.

Expected value

If every job matched the cache-hitting 4.0 job's 9s setup, that is roughly 240s off total job-seconds and 100s off the critical path -- comparable to the savings in PR #1672.

Options

  1. A dedicated cache-warming workflow on push: [main] that runs ruby/setup-ruby with bundler-cache: true for each version in the matrix and nothing else. Cheapest: no specs, no lint, just populates the caches at default-branch scope.
  2. Add push: branches: [main] to the existing CI workflow. Simpler diff, but runs the entire matrix on every merge, which partly spends the savings it creates.

Option 1 is likely preferable.

Implementation note

PR #1672 added a workflow-level concurrency group keyed on ${{ github.workflow }}-${{ github.ref }} with cancel-in-progress: true. A push-triggered run needs either its own concurrency group or cancel-in-progress: false, otherwise consecutive merges to main will cancel each other's cache warming.

Also worth confirming: cache keys include the runner OS, Ruby version, and Gemfile.lock hash, so the warming job must cover every (os, ruby) pair the matrix uses, including windows-latest + 3.2.

Found while verifying PR #1672.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementinternal-changeThe PR includes changes that are NOT user facing and will NOT require a release

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions