Skip to content

Adopt the ADL-32 standard Dependabot posture (+ github-actions lane, + brakeman canary line) - #2033

Merged
jbirdjavi merged 3 commits into
masterfrom
adl32-wave3-dependabot-posture
Aug 19, 2026
Merged

Adopt the ADL-32 standard Dependabot posture (+ github-actions lane, + brakeman canary line)#2033
jbirdjavi merged 3 commits into
masterfrom
adl32-wave3-dependabot-posture

Conversation

@jbirdjavi

@jbirdjavi jbirdjavi commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

ADL-32 wave 3, repo 5 of 8. Adopts the ratified fleet Dependabot posture, adds the github-actions ecosystem this repo has never had, and moves the Brakeman lint step to the fleet canary line.

This PR touches no dependency: .github/dependabot.yml (new content), .github/workflows/ruby.yml (one step), CLAUDE.md (one doc line kept truthful). Gemfile.lock is untouched — see "Deviation from the plan" for why the planned ride-alongs are gone.


What this file changes

  • New github-actions ecosystem. This repo has only ever had a bundler stanza (6 lines, no limit, no groups, no registries). The gha lane arrives with the ratified actions-patch / actions-minor groups and the CruGlobal/.github* ignore already in place, so the 1→2 noise pair other repos are still closing never materialises here.
  • Bundler open-pull-requests-limit: 10. The old config set none, so the implicit default of 5 applied — and that is the version lane's cause of death (below).
  • Contribsys registry attachedGemfile.lock:20 sources sidekiq-pro (8.0.3) from gems.contribsys.com. The token is the org-level Dependabot secret, auto-scoped to every org:CruGlobal language:ruby repo by cru-terraform github/CruGlobal/secrets/rails_variables.tf; nothing to provision, no DevOps ask. insecure-external-code-execution: allow is required once a registry is attached. Until now Dependabot could not update sidekiq-pro at all.
  • Split groupsbundler-security (security-updates), bundler-patch, bundler-minor (version-updates). Majors stay ungrouped and individual by design.
  • Ignores:
    • rails minor + major — Rails upgrades are deliberate, hand-run work. Security patches inside 8.0.x still flow. Specimen of what this blocks: mpd_tool Bump ddtrace from 0.7.2 to 0.25.1 #186 silently rewrote the Gemfile pin ~> 8.0.5.1~> 8.1.3.1.
    • minitest major — minitest 6 drops minitest/mock (ararat Bump dotenv-rails from 2.2.1 to 2.7.5 #210). This is an RSpec app and the lock is at minitest (5.27.0), so this is the fleet guard, not a live wound.
    • connection_pool major — Gemfile:77 pins < 3.0 # Conflict with redis_cache_store in Rails < 8.1.2, and we are on Rails 8.0.5.1. Specimens of the pin-rewrite class this blocks: global_registry Bump rspec-rails from 4.0.0 to 4.0.1 #394 (rewrote < 3.0< 4.0 and silently downgraded sidekiq 7.3.10 → 7.3.9) and infobase GT-941 Fallback content spec #511.
    • CruGlobal/.github*blanket (every update type, not majors-only). Those reusable workflows are consumed at the floating @v1 tag, which the org moves by hand as a coordinated rollout; a bot proposal against a human-managed tag is noise in every direction.
  • Brakeman lint step → the fleet canary line: bundle exec brakeman -A -q --no-pager -w2 -x UnscopedFind.
    • --ensure-latest dropped. It reds the required lint on every repo the day a brakeman release ships — it bit three wave-2 PRs — and under the groups added here Dependabot now proposes those bumps itself, which is what the flag was standing in for.
    • -w2 blocks only on medium+ confidence findings, which clears today's EOLRails nudge without an ignore fingerprint. Donor: rails-infrastructure-canary ruby.yml; knutsenm endorsed the shape on mpd_tool Bump active_model_serializers from 0.10.5 to 0.10.10 #192.
    • Correction to this body's first draft: -w2 does not structurally retire the EOLRails class — it defers it, and the deferral is dated. -w2 sets min_confidence = 3 − 2 = 1, so it filters only Weak (confidence 2); Medium (1) and High (0) still report and still fail the job under brakeman's default exit_on_warn: true. Brakeman's EOL ladder (checks/eol_check.rb) escalates the Rails-EOL warning to :medium at eol_date − 30 days, and the EOL data moved between releases: RAILS_EOL_DATES['8.0.0'..'8.0.99'] is 2026-10-07 in brakeman 8.0.5 and 2026-11-07 in 8.0.6. Master is on brakeman 8.0.6 (via Bump json from 2.21.1 to 2.21.2 in the bundler group across 1 directory #2032), so this repo's fuse is 2026-10-08, when the warning turns :medium and reds the required lint again. Repos still on 8.0.5 have a much nearer fuse of 2026-09-07. The §2d CI-split is the real answer; -w2 buys the runway.
    • Second undersold effect: -w2 raises the floor for every check, not just EOLRails — weak-confidence SQL/XSS/redirect findings stop failing lint too. Non-regressive today (master run 32184999428 is green under the old default level, so nothing live is being newly hidden), but it is a forward-looking sensitivity reduction and should be read as one.
    • Repo-specific adaptation, disclosed — rationale corrected in review: -x UnscopedFind is kept, but not because dropping it would change behaviour. It would not. check_unscoped_find.rb has exactly one warn call and it hardcodes :confidence => :weak; warning.rb maps :weak to 2; -w2 sets min_confidence to 1; checks.rb:78 drops anything above that at add_warning time. So under -w2 the warning can never be emitted, and removing -x UnscopedFind would produce byte-identical output and exit code — the flag is inert on this line. It was load-bearing under the old default-confidence line; -w2 retired it in the same commit. An earlier revision of this body claimed dropping it "would be a behaviour change, not a cleanup"; that was backwards and is struck rather than quietly deleted. The flag is kept solely as a guard, so that dropping -w2 later does not silently re-red lint. Note the fleet canary line this PR otherwise matches carries no such flag. The change remains provably non-regressive: master's lint was green under default confidence on run 32184999428, and raising the threshold to -w2 can only remove findings.
    • CLAUDE.md's documented brakeman invocation was updated to match, so the doc does not immediately go stale.
    • The inline ruby.yml comment was corrected in review too. It read that weak nudges "shouldn't wedge merges devs can't individually fix", which invites the reading that EOL warnings cannot red lint. They can, and this one will on 2026-10-08. The body above was right; the file comment — which is what the next dev actually reads — was the optimistic version. It now states that -w2 filters Weak only, that the EOLRails warning returns as Medium at eol − 30d and reds the job from then on, and that the durable fix is the Rails 8.1 upgrade rather than a brakeman flag. It also carries the corrected -x UnscopedFind guard rationale. Comment-only: both revisions parse and the loaded YAML structures compare equal.

Repair diagnosis (live, re-verified 2026-08-19 ~02:00Z — not copied from the plan)

Two independent lane deaths, and the third symptom has already been cleared by someone else.

1. Version lane: dead since 2026-08-10 — saturation on top of pre-existing dormancy, not saturation alone.
Saturation is verified exactly at the claimed moment: the 2026-08-10T02:12:53Z scheduled run created #2029 at 02:14:13Z, taking the open bundler PR count to 5/5 against the implicit default ceiling of 5 (the old config set no open-pull-requests-limit). Nothing has run in the 9 days since. Per-PR rebase jobs kept firing throughout (four at 20:46:5xZ on 08-18 when master moved), which is why every status surface reads "healthy".

But the lane was already A4-dormant before it saturated, and this body's first draft omitted that. Full pagination of workflow 106794586 (329 runs retained, oldest 2025-07-03) shows plain scheduled bundler in /. runs firing daily through 2025-11-07 and then only three times ever since: 2026-06-08T02:11:43Z, 2026-08-07T02:11:44Z, 2026-08-10T02:12:53Z. That is a 7-month silence (2025-11-07 → 2026-06-08) and then a 60-day silence (06-08 → 08-07), both while the open bundler PR count was nowhere near the limit — only #1894 was open, itself created 2025-11-07, the same day the daily cadence stopped.

So the ceiling is a real and current wall, but it is the second fault, not the only one. This PR raises the ceiling to 10 and collapses proposals into grouped PRs and re-registers the update configs, which addresses both — but it lowers the prior that the two-night acceptance passes on the first try, and it is the reason the acceptance criterion below is two nights rather than one.

2. Security lane: 9 days of total silence on a live alert, then a human poke.
Alert #152 (json, LOW, GHSA-9hj4-r449-hfvc / CVE-2026-71847, vulnerable >= 2.20.0, <= 2.21.1, patched 2.21.2) was created 2026-08-09T08:41:27Z. Between then and 08-18 the security lane produced no job at all — not a filtered success, literally nothing — with automated-security-fixes reporting {"enabled":true,"paused":false} and vulnerability alerts enabled (204). The lane then fired at 2026-08-18T19:16:29Z and opened #2032 56 seconds later. That is the A4 shape exactly: the bundler ecosystem's scheduler moves on a human poke and does not re-latch on its own. It dates the dormancy rather than disproving it.

3. The wedge loop is already broken — by #2032, not by this PR.
bundle audit check --update runs inside the required lint job, so the open json CVE reddened every PR and master (Ruby red on ecf27c4f 08-13 and 013b8a5c 08-10, failing step "Bundle audit", test green). #2032 landed json 2.21.1 → 2.21.2 and brakeman 8.0.5 → 8.0.6 in one commit (0cb36448, merged 2026-08-18T20:45:33Z by frett), and master's Ruby run 32184999428 on 60fb812e is green. Master lock now reads json (2.21.2), brakeman (8.0.6), mail (2.9.1) — every ride-along the plan budgeted for this repo is already on master.

4. RETRACTED — "the alert store is not reconciling" was a lag, not a fault.
This body's first draft reported alert #152 as frozen at state: open / fixed_at: null 5h17m after the fix merged, and proposed it as a second specimen for the cortex_gateway GitHub support ticket. That finding is dead. #152 flipped one minute after that census was taken:

gh api repos/CruGlobal/mobile-content-api/dependabot/alerts/152
→ state=fixed   fixed_at=2026-08-19T02:11:09Z   updated_at=2026-08-19T02:11:09Z

Total reconciliation lag was 5h26m from #2032's merge (2026-08-18T20:45:33Z) — slow, worth knowing, but a lag rather than a reconciliation failure. No escalation is warranted and none should be attached to the cortex ticket on this basis. Open alerts on this repo are now 0. The one durable lesson is the measurement rule the draft got right by accident: re-census immediately before acting on any alert-state finding, because these surfaces settle on the order of hours.


Deviation from the plan (disclosed)

The wave-3 plan's §A2 row for this repo required two ride-along lock bumps — json 2.21.1 → 2.21.2 and brakeman 8.0.5 → 8.0.6 — because a config-only diff would have been unmergeable against a red required lint.

Both were re-verified as unnecessary at authoring time and are therefore not carried. #2032 merged them to master at 20:45:33Z on 08-18, roughly two hours after the plan was written, and master's lint is green on the result. Carrying them would have produced an empty or conflicting lock diff. mail was never in scope here (2.9.1 since the 07-29 Rails 8.0.5.1 bump).

Consequently this PR has zero Gemfile.lock changes and is not a runtime deploy in the dependency sense — though merging it still triggers build-deploy-ecs.yml (see Sequencing).

Two smaller deltas from the plan text, for the record: #2027 is now oj 3.17.3 → 3.17.6 (was 3.17.5 when the plan was written — the 08-18 rebase moved it), and closing #1894 is no longer a precondition for this PR, because raising the limit 5 → 10 frees the slot the close was meant to free.

Outcome, observed rather than predicted: this PR's own lint is greenBundle audit ✅, Brakeman (the new -w2 line) ✅, StandardRB ✅ — and all 10 required contexts pass, codecov included, on a diff that touches no Ruby and no lockfile. Wave 3 budgeted every config PR as a potential unmasking event; on this repo there was nothing left to unmask.


Stale-PR disposition

Principle: grouped proposals supersede one-off version PRs, so nothing group-foldable is merged ahead of alignment; leftovers get closed after this config lands and the groups re-propose. Only true majors stay individual.

PR Title Semver Disposition Reason
#2029 web-console 4.2.1 → 4.3.0 minor auto-closes on config merge (superseded by the fleet pivot below) Version lane goes to limit: 0; nothing re-proposes it
#2027 oj 3.17.3 → 3.17.6 patch auto-closes on config merge (superseded) Version lane at 0
#2026 ougai 2.0.0 → 2.1.0 minor auto-closes on config merge (superseded) Version lane at 0
#2028 rubyzip 2.4.1 → 3.4.1 major auto-closes on config merge (superseded) Under limit: 0 it auto-closes (it was red on three contexts anyway); if the rubyzip 3 major is wanted it becomes a deliberate hand-run bump — Justin's call, unchanged in substance
#1894 sidekiq 7.3.9 → 8.0.9 (2025-11-07) major auto-closes on config merge (superseded) Already moot — master is at sidekiq (8.0.10), CONFLICTING for months; the limit goes to 0 here
#2032 json 2.21.2 (security group) already merged 2026-08-18T20:45:33Z Record only — it is what makes this PR config-only
#2031, #1998, #1944, #577 human PRs (frett, andrewroth) not ours Listed so the census is complete; #577 dates to 2021

No PR is closed by this change; the closes above are post-merge chores for Justin.


Button-flush census (live, mine)

  • Open alerts: 0. #152 (json, LOW, GHSA-9hj4-r449-hfvc, born 2026-08-09T08:41:27Z) flipped to fixed at 2026-08-19T02:11:09Z — see the retraction in finding 4. Nothing to flush.
  • Fixed: 132. Dismissed: 0. Auto-dismissed: 0. (The draft's "100" was a pagination artifact — per_page=100 caps at one page; --paginate gives 132. An exactly-round 100 is the tell.)
  • Census methodology, fleet-wide: ?state=all is not a valid filter on /dependabot/alerts and silently returns [] on every repo. Query per-state, or with no state parameter at all.
  • mail GHSA-mvxr-6m87-mv2q is still 404 in GitHub's Advisory Database (re-verified at authoring). Zero mail alerts exist in any state on any repo. When it is ingested, this repo is not exposed — master is mail (2.9.1). Nothing to flush now or later.
  • Standing rule that applies at merge: alerts born before a config merge never self-trigger and need the "Create Dependabot security update" button; alerts born after it do. Re-census at merge.

Sequencing, blockers, acceptance

Sequencing

  • Merging this deploys to production. build-deploy-ecs.yml triggers on push: branches: [master, staging] with no path filter, so even a config-only diff builds an image and triggers the ECS deploy. Standing rules: one leg at a time per app, stagger wave-3 merges ≥ 5 minutes apart (four-at-once is what tripped AWS ECS Rate exceeded in the Rails 8 wave).
  • Disclosure — the staging leg is already spent. The On Staging label is an action trigger on this repo, not a status marker. Applying it caused stage-branch-merger[bot] to merge this branch into staging at 2026-08-19T02:10:17Z (staging HEAD a032093a), which fired Build & Deploy ECS run 32207595443 — Build Docker Container success 02:10:24Z, Deploy success 02:12:52Z. That is a real staging deploy of this branch ~3 minutes after the PR opened and before any review. So the master merge is this app's second leg tonight, not its first. Five wave-3 repos fired staging deploys inside 5m46s tonight; all succeeded, but §A5's stagger rule is written for master merges only and the label path has the identical deploy consequence with no review gate.
  • No PR-ordering dependency: the ride-along precondition is discharged, and Bump sidekiq from 7.3.9 to 8.0.9 #1894 no longer gates anything.
  • Post-merge: no choresBump web-console from 4.2.1 to 4.3.0 #2029 / Bump rubyzip from 2.4.1 to 3.4.1 #2028 / Bump oj from 3.17.3 to 3.17.6 #2027 / Bump ougai from 2.0.0 to 2.1.0 #2026 / Bump sidekiq from 7.3.9 to 8.0.9 #1894 all auto-close when the security-only config lands (limit: 0, wave-2 chore precedent). The rubyzip 3 major, if wanted, becomes a deliberate hand-run bump.

Blockers / preconditions

  • Mechanical: 10 required contexts on master — 6 XML schema jobs (Manifest, Content, Choose Your Own Adventure, Lesson, Tract, Training / Validate XML Schema), lint, test, codecov/project, codecov/patch; strict: true, 1 approving review, allow_auto_merge: false. The XML workflow triggers on every pull_request → master with no path filter, so all six report on a config-only diff. Live trap worth repeating: codecov contexts are absent, not failed, when test fails (Bump rubyzip from 2.4.1 to 3.4.1 #2028 shows exactly 8 of 10) — a red test blocks on three contexts, not one.
  • Default branch is master, not main.
  • Not a blocker: the bundle audit step living inside a required context is the mechanism behind the wedge loop diagnosed above. The fleet answer is the §2d CI-split (audit → a separate non-required audit job, canary ruby.yml as donor); it changes required contexts and belongs in its own PR, filed not fixed here.

Lane acceptance — two nights, per repo, starting at merge

  1. Bundler version lane: two consecutive scheduled bundler in /. runs on the two nights after merge (the plain form, no for <gem> suffix — per-PR rebase jobs and security jobs do not count).
  2. github-actions lane: at least one github-actions in / run, since this ecosystem is new here.
  3. Alert Update sinatra to fix security vulnerability #152: verify it flips to fixedalready satisfied, flipped 2026-08-19T02:11:09Z. Gate discharged before merge.

If the bundler lane shows zero scheduled runs on both nights, config re-registration did not revive it and this repo joins cortex_gateway as a GitHub support-ticket candidate. Wave 3 is not done at merge time — the acceptance window closes ~2 days later.


Fleet pivot: security-only (ratified 2026-08-19)

This PR was approved on the config above (open-pull-requests-limit: 10, three bundler groups: bundler-security, bundler-patch, bundler-minor). This revision supersedes that with the fleet's settled posture. It does not touch the github-actions lane, the registries block, or insecure-external-code-execution — Mark's review of those stands.

What changed in this commit

  • open-pull-requests-limit: 100 on the bundler entry, with a comment explaining why.
  • groups: bundler-patch and bundler-minor deleted. bundler-security kept byte-identical.
  • ignore: all three stanzas (rails, minitest, connection_pool) kept, with their original per-stanza reasons untouched. Added one NOTE at the top of the block: with the limit at 0 these are now inert forward guards — the version-update lane never runs, and (per the proof below) update-types-scoped ignores never gated security updates in the first place. Kept so a future limit > 0 flip inherits the pin guards without anyone having to reconstruct them.
  • github-actions lane: untouched, exactly as this PR originally added it.

Rationale

Justin ratified 2026-08-19: the ADL-32 fleet goes security-only for gems. This restores the original ADL-32 intent — cut developer notification fatigue from version-update PRs — from before a bundler-minor group got conflated in from Brian's ararat work.

Two pieces of evidence drove it, both live specimens rather than theory:

  • Individual majors refill the ceiling daily. Grouping only folds patches and minors; majors stay ungrouped by design (this PR's own body notes that above), so the PR count refills on its own. global_registry: 8 bundler PRs within 10m23s of its grouped config merging.
  • Grouped minors rewrite Gemfile pins straight past the ignore stanzas. infobase#519 — opened 15 minutes after infobase#517 merged, with all three of this repo's same-shaped ignores in force — rewrote rails "~> 8.0.5""~> 8.1.3" and connection_pool "< 3.0""< 4.0" as collateral inside a joint group resolution. ignore blocks Dependabot proposing an update for the named dependency; it does not stop the grouped resolver widening a neighboring gem's constraint to make the set resolvable. This is exactly the class of specimen Mark's review below (#394/#511/#186) already flagged as live — infobase#519 is the same failure mode arriving through the grouped path the ignores don't cover.

Zero security cost

Both exemptions are proven from source, not assumed:

  • open-pull-requests-limit docs: security-update PRs are exempt and do not count toward the limit. Setting it to 0 stops the version-update lane; it does not touch security.
  • The three ignore stanzas are update-types-scoped (version-update:semver-minor / semver-major). dependabot-core's IgnoreCondition#ignored_versions returns only versions:-keyed ranges when a job is security_updates_only, so update-types conditions are never consulted on a security job — confirmed reading dependabot-core source 2026-08-19. The rails/minitest/connection_pool ignores could not have gated a security PR even before this change; nothing about security coverage is being traded away.

What still flows

  • bundler-security group: unchanged, still catches every security-updates match (patterns: ["*"]).
  • registries: [contribsys] and insecure-external-code-execution: allow: unchanged — the security lane still needs contribsys resolution for sidekiq-pro, exactly as Mark verified.
  • github-actions lane: fully unchanged from what this PR already added and Mark already reviewed.

Carve-out note

A native allow-based carve-out (scoping the bundler entry to just brakeman + bundler-audit via dependency-name) was evaluated and rejected. Verified against GitHub's current options-reference page and dependabot-core's allowed_update? (job.rb): allow/dependency-name is not on the documented security-exempt surface — only update-types carries that carve-out — and allowed_update? name-matches unconditionally regardless of security_updates_only. An allow list naming only those two gems would silence security PRs for every other vulnerable gem in the fleet, the opposite of intent. So this PR uses a pure limit: 0, and the brakeman/bundler-audit currency carve-out is handled outside Dependabot: the nightly digest watches both tools' versions today, and a nightly proposer for exactly those two gems is planned.

Tool-currency plan

With the version lane at 0, brakeman and bundler-audit no longer get their own version-update PRs from Dependabot. A tool-floor watch in the nightly digest is planned (today its floor collector watches only the ruby-vips boot floor); a small nightly proposer scoped to just those two gems is the planned mechanism to keep them current without reopening the general version-update noise this pivot exists to close.

Effect on open Dependabot PRs: the five open bundler version PRs — #2029, #2028, #2027, #2026, #1894 — auto-close when this merges; the disposition table above predates the pivot and is superseded on those rows (marked inline).

Answers Mark's "worth deciding fleet-wide" ask

Mark's review above flagged the infobase#519 pin-rewrite as a fleet question, not a per-repo one — "six merged wave-2 repos are generating these right now." This is that decision: security-only, fleet-wide, waves 2 and 3 (rails-infrastructure-canary and Brian's gate-armed ararat/cutmeoff excluded). It also moots his adjacent finding that the groups don't bound majors — with the version lane at 0, majors don't get proposed either, grouped or not.

🤖 Generated with Claude Code

https://claude.ai/code/session_017iJRsigMLohAnL6fW2ynHB

Adds the missing github-actions ecosystem (this repo has never had one),
raises the bundler open-pull-requests-limit from the implicit default of 5
to 10, attaches the contribsys registry (Gemfile.lock:20, sidekiq-pro), and
splits proposals into bundler-security / bundler-patch / bundler-minor
groups. Ignores: rails minor+major, minitest major, connection_pool major
(Gemfile:77 pins < 3.0), and a blanket ignore on CruGlobal/.github*.

Also moves the Brakeman lint step to the fleet canary line: drop
--ensure-latest, add -w2. --ensure-latest reds every repo's required lint on
brakeman release day, and under the new groups Dependabot proposes those
bumps itself; -w2 blocks only on medium+ confidence findings, which
structurally retires the EOLRails weak warning. -x UnscopedFind is preserved
(this repo runs -A, which enables optional checks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iJRsigMLohAnL6fW2ynHB
@stage-branch-merger

Copy link
Copy Markdown

I see you added the "On Staging" label, I'll get this merged to the staging branch!

@knutsenm knutsenm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. CI green on all 10 required contexts, and the change is provably non-regressive from the before/after brakeman logs rather than by assertion. Two corrections below, neither blocking.

Verified

  • contribsys registry correct end-to-end. Gemfile opens the contribsys source for sidekiq-pro, lock confirms sidekiq-pro (8.0.3) from that remote, the block is byte-identical to the canary's, and insecure-external-code-execution: allow is genuinely load-bearing here — you also carry two git-sourced gems (raddocs, rspec_api_documentation) that need gemspec eval. Repo reports language Ruby, so the org secret's scoping reaches it.
  • The github-actions lane is genuinely new and its blast radius is one PR. master's config really was six lines, bundler-only. Of 6 distinct action dependencies, only actions/checkout@v6 is behind (v7 available); ruby/setup-ruby@v1 and codecov-action@v7 are current, and the two CruGlobal/.github* refs are ignored. Everything else is a floating major tag, so patch/minor never surface.
  • The CLAUDE.md line is a genuine improvement, not a smuggled change — the old line already omitted -x UnscopedFind that CI has carried, so this both tracks the flag change and repairs pre-existing doc drift. "(matches CI)" is now literally true.
  • connection_pool is the best-evidenced ignore — your comment cites Gemfile:77 and the line number is exact on master, with the stated reason reproducing the Gemfile's own trailing comment.
  • Nothing actionable is newly suppressed: master's brakeman run at the default confidence (weak included) already reported Security Warnings: 0 / No warnings found, and config/brakeman.ignore holds zero entries.

Correction 1 — the -x UnscopedFind rationale is backwards

The body says dropping the exclusion "would be a behaviour change, not a cleanup." It wouldn't. check_unscoped_find.rb has exactly one warn call and it hardcodes :confidence => :weak; warning.rb maps :weak to 2; -w2 sets min_confidence to 1; checks.rb:78 drops anything above that at add_warning time. So under -w2 the warning can never be emitted, and removing -x UnscopedFind would produce byte-identical output and exit code. It was load-bearing under the old line — -w2 retired it in the same commit. Keeping it as a guard against someone later dropping -w2 is fine; the stated reason shouldn't survive into the record. Note the canary line you're matching carries no such flag.

Correction 2 — the inline -w2 comment is the optimistic version

The comment says weak nudges "shouldn't wedge merges devs can't individually fix," which reads as though EOL warnings can't red lint. They can — see the fuse arithmetic below. Your body states this correctly, including both dates; the file comment, which is what the next dev reads, doesn't. Worth aligning them.

Fleet finding: the ignore stanzas don't survive a grouped bundler-minor resolution

This wave installs rails / minitest / connection_pool ignores as the guard against Dependabot rewriting Gemfile constraints — the class your #394 / #511 / #186 specimens demonstrate. They do guard the individual-PR path. They do not guard the grouped path, and the fleet already has a live counter-specimen one day old:

infobase#519, opened 2026-08-18T21:21:53Z — 15 minutes after infobase#517 merged at 21:06:14Z, based on that merged config, with all three ignores in force — rewrote six pins:

-gem "rails", "~> 8.0.5"                                +gem "rails", "~> 8.1.3"
-gem "activerecord-oracle_enhanced-adapter", "~> 8.0.0"  +... "~> 8.1.4"
-gem "connection_pool", "< 3.0"                          +gem "connection_pool", "< 4.0"
-gem "sidekiq", "~> 8.0"                                 +gem "sidekiq", "~> 8.1"
-gem "active_model_serializers", "~> 0.8.0"              +... "~> 0.10.16"

A Rails minor and a connection_pool major — the exact two things the ignores exist to block. Both lint and test fail on it. global_registry#401 shows the same class (active_model_serializers ~> 0.8.0~> 0.10.16, pg ~> 1.5.9~> 1.6.3), also red.

The mechanism: inside a group Dependabot resolves jointly and widens other gems' requirements to make the set resolvable. ignore prevents it proposing an update for a dependency; it does not prevent collateral rewrites of neighbouring constraints. So a grouped-minor PR has to be reviewed line-by-line against the Gemfile, not by its group title.

Worth deciding fleet-wide rather than per-repo, since six merged wave-2 repos are generating these right now.

Also: the groups don't bound majors

Several bodies in this wave argue grouping collapses the lane to "at most 3 PRs a day." bundler-patch and bundler-minor carry update-types: [patch] / [minor], so nothing matches a major — every outstanding major still gets its own PR. Measured on the merged siblings: global_registry got 8 bundler PRs within 15 minutes of #399 landing (#400 patch group, #401 minor group, #402#407 six individual majors); infobase the same shape. open-pull-requests-limit: 10 is Dependabot's default, not a tightening. Expect the ceiling, refilling daily.

And -w2 defers the EOLRails red rather than retiring it

Verified in the installed brakeman 8.0.6 source. eol_check.rb walks a confidence ladder — :low (Weak) at eol−60, :medium at eol−30, :high at EOL. options.rb:386 makes -w2 a min_confidence of 1 and checks.rb:78 drops only warnings above it, i.e. Weak alone. check_eol_rails.rb has ['8.0.0', '8.0.99'] => Date.new(2026, 11, 7), so the Medium rung fires 2026-10-08 and reds the required lint again. Repos still on brakeman 8.0.5 carry the older 2026-10-07 date and fuse on 2026-09-07.

Dropping --ensure-latest and moving to -w2 is still the right call and it's the canary's line — just don't bank on it as the EOL answer. The Rails 8.1 upgrade is.

Minor, for the record

The minitest ignore is inert here — minitest isn't in the Gemfile or DEPENDENCIES, only transitive at 5.27.0 — so it can't raise the direct-major PR it names. Your comment is technically honest about that and the body discloses it; flagging only so it isn't mistaken for live protection later. config/brakeman.ignore also still stamps brakeman_version: 8.0.5, which isn't this PR's doing.

🤖 Review drafted with Claude Code

Review follow-up (knutsenm on #2033), both non-blocking corrections. The PR
body stated the fuse correctly; the file comment -- which is what the next
dev actually reads -- did not. Comments only, no behaviour change.

1. The -w2 comment was the optimistic version. It said weak nudges "shouldn't
   wedge merges devs can't individually fix", which reads as though EOL
   warnings cannot red lint. They can, and this one will. -w2 filters Weak
   ONLY (min_confidence 1); Medium and High still report and still fail the
   job. Brakeman's EOL ladder escalates the Rails-EOL warning to :medium at
   eol_date - 30d, and brakeman 8.0.6 -- the version in our lock -- dates
   Rails 8.0.x EOL at 2026-11-07, so the warning returns as Medium on
   2026-10-08 and reds this job on every PR from then on. The comment now
   says so, and names the durable fix: the Rails 8.1 upgrade, not a flag.

2. The `-x UnscopedFind` rationale was backwards and is corrected in place.
   check_unscoped_find.rb has one warn call and it hardcodes
   :confidence => :weak; warning.rb maps :weak to 2; -w2 sets min_confidence
   to 1; checks.rb drops anything above that at add_warning time. So under
   -w2 the warning can never be emitted and removing the flag would produce
   byte-identical output and exit code -- it is inert, not a behaviour
   change. It WAS load-bearing under the old default-confidence line; -w2
   retired it in the same commit.

   The flag is KEPT deliberately, per review ("keeping it as a guard is
   fine"), so that dropping -w2 later does not silently re-red this job. The
   comment now states that guard rationale instead of the false
   behaviour-change claim, and notes the fleet canary line carries no such
   flag.

Verified comment-only: both revisions parse and the loaded YAML structures
compare equal, and no non-comment line appears in the diff. The invocation
`brakeman -A -q --no-pager -w2 -x UnscopedFind` is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iJRsigMLohAnL6fW2ynHB
@jbirdjavi

Copy link
Copy Markdown
Contributor Author

Thanks Mark — both corrections taken, one in the file and one in the body.

Correction 1: -x UnscopedFind rationale was backwards — body fixed

You're right, and I'd reasoned it from "-A enables optional checks" without following it through to what -w2 does to them. Confirmed the chain: check_unscoped_find.rb has one warn call hardcoding :confidence => :weakwarning.rb maps :weak to 2 → -w2 sets min_confidence to 1 → checks.rb:78 drops anything above that at add_warning time. So the warning can never be emitted under this line and removing the flag would produce byte-identical output and exit code. It's inert, not a behaviour change — and it was load-bearing under the old default-confidence line, which -w2 retired in the same commit.

Body passage rewritten to say that, with the false claim struck rather than silently deleted. The flag is kept, per your "keeping it as a guard is fine" — the stated reason is now the guard rationale only: it exists so dropping -w2 later doesn't silently re-red lint. Also noted that the canary line this PR otherwise matches carries no such flag, since that's the thing that makes the deviation worth disclosing at all.

Correction 2: the inline -w2 comment was the optimistic version — file fixed

Agreed, and this was the more important of the two: the body had the fuse right, but the file comment is what the next dev actually reads, and "weak nudges shouldn't wedge merges devs can't individually fix" invites exactly the wrong inference.

.github/workflows/ruby.yml now says, above the Brakeman step:

  • -w2 filters Weak only (min_confidence 1); Medium and High still report and still red the job.
  • Explicitly: do not read it as "EOL warnings can't fail lint" — they can, and this one will.
  • The EOL ladder escalates Rails-EOL to :medium at eol − 30d, and brakeman 8.0.6 (our lock) dates Rails 8.0.x EOL at 2026-11-07, so it returns as Medium on 2026-10-08 and reds every PR from then on.
  • -w2 buys runway, it does not retire the class — the durable fix is the Rails 8.1 upgrade, not a brakeman flag.

The corrected -x UnscopedFind guard rationale is in the same comment block, so the two live together where they're read.

Comment-only change, verified rather than asserted: both revisions parse and the loaded YAML structures compare equal, and the diff contains no non-comment lines. The invocation brakeman -A -q --no-pager -w2 -x UnscopedFind is untouched.

Your other notes

  • minitest ignore is inert here — accepted; minitest is transitive-only at 5.27.0, so it can't raise the direct-major PR it names. The body already discloses it and the comment is honest about it; leaving it as the fleet-consistent guard rather than dropping it from one repo's config.
  • config/brakeman.ignore still stamps brakeman_version: 8.0.5 — confirmed, and confirmed it isn't this PR's doing (the file holds zero entries, so the stamp is cosmetic). Leaving it out of this PR rather than smuggling in an unrelated regeneration.
  • The grouped-bundler-minor collateral-rewrite finding (infobase Bump codecov from 0.2.11 to 0.2.12 #519, global_registry Various test fixes #401) — treating as a fleet-wide decision rather than a per-repo one. Unlike autonag this repo does run a bundler-minor group, so it is exposed to the class; the operational answer until there's a fleet decision is your line — review a grouped-minor PR line-by-line against the Gemfile, not by its group title.

Justin ratified 2026-08-19: the ADL-32 fleet goes security-only for
gems, restoring the original project intent (developer notification
fatigue) before a bundler-minor group got conflated in from Brian's
ararat work.

Evidence: individual majors refill the 10-PR ceiling daily
(global_registry: 8 PRs in 10m23s after config merge); grouped minors
rewrite Gemfile pins past ignores (infobase#519 rewrote rails
"~> 8.0.5"->"~> 8.1.3" and connection_pool "< 3.0"->"< 4.0"). Zero
security cost: security PRs are exempt from open-pull-requests-limit,
and update-types-scoped ignores never gate security updates
(dependabot-core IgnoreCondition#ignored_versions returns only
`versions:` ranges when security_updates_only — proven from source
2026-08-19).

- bundler: open-pull-requests-limit -> 0, bundler-patch/bundler-minor
  groups deleted, bundler-security kept byte-identical.
- registries + insecure-external-code-execution: unchanged (security
  lane still needs contribsys resolution).
- ignore block: every stanza and its per-stanza reason kept; added a
  NOTE that with limit 0 these are now inert forward guards (the
  version-update lane never runs) kept so a future limit>0 flip
  inherits the pin guards.
- github-actions lane: untouched, per repo-specific review.

Scope: waves 2+3 repos. rails-infrastructure-canary and
ararat/cutmeoff are explicitly excluded from this ruling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iJRsigMLohAnL6fW2ynHB
@jbirdjavi

Copy link
Copy Markdown
Contributor Author

@knutsenm The fleet ruling landed: gems go security-only, fleet-wide (Justin, ratified 2026-08-19). This is the answer to your "worth deciding fleet-wide rather than per-repo" note on the infobase#519 pin-rewrite — it's not per-repo, and this PR now carries the fleet's answer.

What changed since your review, in .github/dependabot.yml:

  • open-pull-requests-limit: 100 on the bundler entry.
  • groups: bundler-patch and bundler-minor deleted. bundler-security is byte-identical to what you reviewed.
  • ignore: all three stanzas you checked (rails, minitest, connection_pool) are untouched, same reasons. Added one NOTE at the top of the block: they're now inert forward guards at limit 0, kept so a future limit>0 flip inherits them.
  • Everything else you verified is untouched: registries: [contribsys], insecure-external-code-execution: allow, the github-actions lane (new ecosystem, groups, CruGlobal/.github* ignore), the brakeman -w2 line and its guard rationale, CLAUDE.md.

Why: individual majors already weren't bounded by the groups (your "groups don't bound majors" finding) — they refill the ceiling on their own. And grouped minors don't respect the ignore stanzas at all, which is exactly what infobase#519 showed live against ignores shaped like this repo's. Limit 0 removes both failure modes at once instead of trying to out-guard the joint-resolution behavior.

Zero security cost, not just asserted: open-pull-requests-limit exempts security PRs by design, and dependabot-core's IgnoreCondition#ignored_versions only consults update-types conditions on version-update jobs, not security jobs — so these ignores were never gating security updates in the first place, before or after this change.

On the allow-based carve-out for brakeman/bundler-audit specifically: evaluated and rejected. allow/dependency-name is not on the documented or code-confirmed security-exempt surface (only update-types is) — a dependency-name allow list would silence security PRs for every other gem, not just scope version PRs to two. So this uses a pure limit: 0; brakeman/bundler-audit currency is handled outside Dependabot — nightly digest watches both today, a scoped nightly proposer for just those two is planned.

Ready for re-review.

@knutsenm knutsenm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at 44050242f. My earlier approval was at d84a6c5ec; this repo doesn't dismiss stale reviews on push, so it was standing over two unreviewed commits — this replaces it. All ten required contexts plus the config check are green.

What landed since: 0b7b13ee3 rewrites the brakeman comment and 44050242f applies the security-only pivot.

Correction — I got the ignore/security-updates interaction wrong, and your new NOTE is right

I told you on the wave-2 batch and again here that ignore gates security updates as well as version updates, so a Rails CVE fixed only in a later minor would be silently suppressed. That is wrong for the form these configs use, and the NOTE this PR adds is correct. From dependabot-core, common/lib/dependabot/config/ignore_condition.rb:

def ignored_versions(dependency, security_updates_only)
  return versions if security_updates_only
  return [ALL_VERSIONS] if versions.empty? && transformed_update_types.empty?

  versions_by_type(dependency) + versions
end

Under security_updates_only it returns versions and never reaches versions_by_type, which is the only place update_types are translated into ranges. Every ignore stanza in these files is update-types-scoped with no versions: key, so on the security lane they contribute nothing — the discard is real.

Two consequences worth stating plainly:

  • The "sharp edge" I flagged does not exist. A Rails CVE whose fix ships only in 8.1.x is not suppressed by the rails stanza. The security-only pivot therefore does not create a CVE blind spot, which was the main risk I would otherwise have raised against it.
  • The bare - dependency-name: "CruGlobal/.github*" stanza follows the same rule: it returns [ALL_VERSIONS] for version updates (genuinely blanket, as your comments now say) but contributes nothing on the security lane.

I'd rather flag this loudly than let it sit in the audit record — I asserted it in six wave-2 reviews.

The pivot fixes the defect I raised, structurally

Dropping bundler-patch/bundler-minor and setting open-pull-requests-limit: 0 removes the grouped-minor pin-rewrite problem at the root rather than patching around it: no version groups means no joint resolution, so there is no path for Dependabot to widen a neighbouring constraint. That's a better answer than adding activerecord-oracle_enhanced-adapter to the ignores, which is what I'd suggested as the cheap fix.

But it hasn't reached wave 2. infobase, global_registry, mpdx_api, mpd_tool, okta-profile-editor and familylife-gift-cards all still carry open-pull-requests-limit: 10 with both version groups, and the two specimens are still open and red — infobase#519 (rewrites rails to a minor and connection_pool to a major, both explicitly ignored there) and global_registry#401. Those six need the same pivot, or they keep minting this class nightly.

Standing note: the version lane going quiet has a cost

With limit: 0 there is no proposer for routine gem movement anywhere on bundler — gems now advance only on a security advisory or by hand. mobile-content-api#2033 names the sharpest instance of this in its own workflow comment (dropping --ensure-latest was justified by Dependabot keeping brakeman current, and under limit: 0 nothing does). The same reasoning applies to standard and bundler-audit, and it lands right as the Rails 8.1 upgrade becomes the durable fix for the 2026-10-08 EOLRails fuse. Worth an explicit owner rather than leaving it implied.

Both corrections landed accurately

The new comment gets the mechanism right on both counts: -w2 filters Weak only, so Medium and High still red the job; the EOL ladder escalates at eol−30 and brakeman 8.0.6 dates Rails 8.0.x EOL 2026-11-07, so the warning returns 2026-10-08; and the durable fix is the Rails 8.1 upgrade rather than a flag. -x UnscopedFind is now correctly described as inert under -w2 and kept as a guard against a future -w2 removal — which was my point, and keeping it on those terms is the right call.

You also caught the consequence I was about to raise: --ensure-latest was dropped on the rationale that Dependabot keeps brakeman current under the version groups, and limit: 0 removes exactly that. Naming it in the comment rather than leaving it implied is the right treatment; it just needs an owner fleet-wide, per the standing note above.

🤖 Review drafted with Claude Code

@jbirdjavi
jbirdjavi merged commit 12fe240 into master Aug 19, 2026
11 checks passed
@jbirdjavi
jbirdjavi deleted the adl32-wave3-dependabot-posture branch August 19, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants