Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 60 additions & 21 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
name: Ruby

on: [push, pull_request]
on:
push:
branches: [master]
pull_request:
schedule:
# Weekly, matching .github/dependabot.yml: a scheduled run is the only way
# bundler-audit sees an advisory published against a lockfile nobody has
# touched. Only the audit leg runs on this trigger, via the `if` on `test`
# and the matrix in `checks`. The minute is off the hour on purpose: :00 is
# GitHub's busiest slot and runs scheduled there are routinely delayed.
- cron: "17 6 * * 1"
# GitHub disables the cron above after 60 days without repository activity,
# which is exactly the dormant repository the audit is meant to cover. This
# is the manual path, and running it also re-arms the schedule.
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
# Only PR pushes are worth cancelling. Cancelling a master run would leave
# that commit recorded as cancelled instead of green or red, and would kill
# the run that populates the cache PR runs restore from.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
# The scheduled run is for the advisory check; the compatibility matrix has
# nothing new to say about a tree nobody has touched.
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
# Generous next to the 10-20s a warm run takes, because a dependabot
# lockfile bump misses the bundler cache and installs a full Rails tree
# from source. Still bounds a hang well under the 6-hour default.
timeout-minutes: 30
name: "rspec - Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }}"
strategy:
fail-fast: false
Expand All @@ -25,7 +53,16 @@ jobs:
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
steps:
- uses: actions/checkout@v5
# actions/checkout is pinned by commit SHA; dependabot bumps the SHA and
# the trailing version comment together. ruby/setup-ruby stays on the
# `v1` tag upstream documents, because a pinned release only knows the
# Ruby builds it shipped with: an added matrix entry (ROADMAP 5.2 plans
# Ruby 4.0) would fail with an unhelpful unknown-version error.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# No step needs git credentials after checkout, and bundler runs
# gemspec and native-extension code from the branch under test.
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
Expand All @@ -34,31 +71,33 @@ jobs:
- name: Run rspec
run: bundle exec rspec

lint:
checks:
runs-on: ubuntu-latest
name: rubocop
timeout-minutes: 30
name: ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
# Each leg's `command` is executed by the single step at the end of
# this job, so the `bundle exec ...` lines live here rather than in a
# `run:`. rubocop lints the tree; bundler-audit checks the lockfile
# against the advisory database. A scheduled run drops the rubocop leg
# for the reason given on `test` above, and repeats the audit entry
# because an expression can only pick between whole lists.
include: >-
${{ github.event_name == 'schedule'
&& fromJSON('[{"name": "bundler-audit", "command": "bundle exec bundle-audit check --update"}]')
|| fromJSON('[{"name": "rubocop", "command": "bundle exec rubocop"}, {"name": "bundler-audit", "command": "bundle exec bundle-audit check --update"}]') }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
# The root lockfile pins dev tooling that needs Ruby >= 3.3; the
# gem's own 3.2 floor is covered by the test matrix.
ruby-version: "3.4"
bundler-cache: true
- name: Run rubocop
run: bundle exec rubocop

audit:
runs-on: ubuntu-latest
name: bundler-audit
steps:
- uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
# As in the lint job: the root lockfile needs Ruby >= 3.3.
ruby-version: "3.4"
bundler-cache: true
- name: Check the lockfile against the advisory database
run: bundle exec bundle-audit check --update
- name: Run ${{ matrix.name }}
run: ${{ matrix.command }}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## [Unreleased]

### Changed
- CI pins `actions/checkout` by commit SHA (v7.0.1) instead of the mutable `v7` tag, so a repointed upstream tag cannot change what runs without a diff here. `ruby/setup-ruby` deliberately stays on `v1`, the reference upstream documents, because a pinned release only knows the Ruby builds it shipped with. Checkout also runs with `persist-credentials: false`: no step needs git credentials, and bundler evaluates gemspec and native-extension code from the branch under test.
- The workflow runs once per change rather than twice. `push` is scoped to `master`, so a branch with an open pull request no longer builds under both events, and a `concurrency` group cancels superseded pull request runs. Runs on `master` are left alone, so every commit there keeps a result.
- `rubocop` and `bundler-audit` share one job definition instead of two byte-identical ones, and every job carries `timeout-minutes: 30` in place of the six-hour default.

### Added
- A weekly `schedule` trigger, so `bundler-audit` reports an advisory published against a lockfile nobody has touched. Only the audit runs on that trigger. `workflow_dispatch` runs it on demand and is also how the cron is re-armed, since GitHub disables scheduled workflows after 60 days without repository activity.

## [1.6.0] - 2026-09-03

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,11 +450,11 @@ Priority: High. Category: Test debt. Effort: M. Status: Confirmed.

Priority: Medium. Category: CI. Effort: M. Status: Confirmed.

**Evidence.** `.github/workflows/main.yml:15-21` runs six Ruby/Rails pairs, but `spec/spec_helper.rb` requires only `rspec/json_api`, which in turn requires a single ActiveSupport file (`json_api.rb:7`). Nothing requires `rails` or `rspec-rails`, and the generators are never invoked. Each Rails job therefore tests `Object#blank?` against that Rails version. The 1.5.0 CHANGELOG describes the matrix as making "the advertised version support actually tested".
**Evidence.** `.github/workflows/main.yml:47-52` runs six Ruby/Rails pairs, but `spec/spec_helper.rb` requires only `rspec/json_api`, which in turn requires a single ActiveSupport file (`json_api.rb:7`). Nothing requires `rails` or `rspec-rails`, and the generators are never invoked. Each Rails job therefore tests `Object#blank?` against that Rails version. The 1.5.0 CHANGELOG describes the matrix as making "the advertised version support actually tested".

**Problem and impact.** Six jobs of CI time for one line of coverage, and a false sense that Rails 6.1 through 8.1 compatibility is verified. Ruby 4.0 is also absent even though 1.5.0 shipped a Ruby 4.0 load fix (commit `8f7318a`), and the local lockfile was resolved on Ruby 4.0.

**Recommended solution.** After 2.1, the runtime matrix only needs Ruby versions (3.2, 3.3, 3.4, 4.0) with the plain Gemfile. Keep one or two Rails appraisals that actually load `rspec-rails` and run the generator specs from 5.1 against a minimal dummy app. Add `permissions: contents: read` and a `concurrency` group while touching the file.
**Recommended solution.** After 2.1, the runtime matrix only needs Ruby versions (3.2, 3.3, 3.4, 4.0) with the plain Gemfile. Keep one or two Rails appraisals that actually load `rspec-rails` and run the generator specs from 5.1 against a minimal dummy app. The `permissions: contents: read` and `concurrency` parts of this item are done; only the matrix rework is outstanding.

### 5.3 Supply-chain checks are not automated (done in 1.6.0)

Expand Down