Skip to content

feat(groom): configurable run cadence via GROOM_INTERVAL_DAYS interval gate (BE-4004)#56

Open
mattmillerai wants to merge 5 commits into
mainfrom
matt/be-4004-groom-interval-days
Open

feat(groom): configurable run cadence via GROOM_INTERVAL_DAYS interval gate (BE-4004)#56
mattmillerai wants to merge 5 commits into
mainfrom
matt/be-4004-groom-interval-days

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

Groom used to run on a hardcoded weekly cron — to change how often it runs you had to edit the workflow file. GitHub Actions crons can't be "every N days" at runtime, so this switches groom to the standard trick: fire a cheap tick every day, but only actually do work every N days, where N is a live knob you set with a repo variable (GROOM_INTERVAL_DAYS, default 7 = today's weekly). A tick that's too soon exits in a few seconds before the expensive finder ever starts. Change the variable, cadence changes — no PR, no YAML edit.


⚠️ REQUIRED MERGE STEP — bump the caller pin

The interval gate lives in the new groom.yml, so ci-groom.yml pins a placeholder (@REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA) that fails loud (unresolvable ref / failed assets checkout) until bumped. This is the same reusable-lands-then-bump-caller two-step the pilot used (#49#53), compressed into one PR. After squash-merge, bump both uses: and workflows_ref in .github/workflows/ci-groom.yml to this PR's merge SHA. groom is schedule/dispatch-only (never on-PR), so nothing breaks on the PR itself; the placeholder just guarantees the daily cron can't silently run against the old (gate-less) reusable.

(No bump-groom-callers.yml automation exists yet — groom has a single caller. A dedicated bumper is a reasonable follow-up if more repos enroll.)

What changed

  • ci-groom.yml (caller): weekly cron → daily 17 9 * * *; grants actions: read; wires interval_days + cadence to ${{ vars.GROOM_INTERVAL_DAYS || '7' }}.
  • groom.yml (reusable): new interval_days input (default 7); the gate job now checks out the assets, runs the interval gate before the finder, then the (unchanged) volume gate, composing both into should_run. Gate job gains actions: read.
  • .github/groom/interval.py + tests: the gate. Derives the last real groom run from this caller's Actions run history — a run counts only if its Audit — finder job actually ran, so interval-skip ticks never reset the clock — and early-exits unless interval_days elapsed. Pure decision logic split from the gh I/O shell, mirroring ledger.py. Fail-open on any history-read error.
  • Docs: README table, .github/groom/README.md (new section), AGENTS.md.

Acceptance criteria

  • Cadence set by GROOM_INTERVAL_DAYS (default 7); changing it changes frequency with no workflow-file edit.
  • Base cron fires daily; a within-interval tick no-ops cheaply (exits in the gate job, before the finder).
  • workflow_dispatch always runs regardless of the interval.
  • Last-run state durable across CI runs (GitHub run history — no net-new secret, only actions: read).
  • Default (unset variable) stays weekly.

Judgment calls (worth a reviewer's eye)

  1. Last-run state = run history, not a GROOM_LAST_RUN repo variable. The ticket sanctioned either. A repo variable needs a Variables: write credential the run doesn't otherwise carry — and a missing grant would fail silently into a daily over-spend. Run history is durable, needs only actions: read, and has no silent-failure trap.
  2. Interval delivered as a reusable input wired to the var (rather than the reusable reading vars.GROOM_INTERVAL_DAYS directly). The caller evaluates the var in its own context — unambiguous — matching how cadence/dry_run/volume_gate are already wired. Same "no file edit" ergonomics.
  3. workflow_dispatch bypasses the interval gate but not the pre-existing volume gate for a non-dry-run dispatch. That volume-gate-on-dispatch behavior is a deliberate spend guard the pilot chose (volume_gate: dry_run != 'true'); the ticket's "always runs regardless of the interval" is about the interval gate, so I left the volume gate alone (Chesterton's fence). A dry-run dispatch bypasses both, as before.
  4. github.workflow_ref resolves to the caller (ci-groom.yml), not the reusable — the OIDC workflow_ref (caller) vs job_workflow_ref (reusable) distinction. If that assumption were ever wrong, the run-history query returns nothing → the gate fails open (runs), never crashes.
  5. A finder job that failed still counts as a real run (not just success) — so a run that spent the billed audit but died at a later step doesn't re-spend on the next daily tick. Trade-off documented in interval.py.

Testing

  • python3 -m unittest discover -s .github/groom/tests54 passed (16 new for the interval gate: within/at/after-interval, skip-ticks-don't-reset, dispatch-always-runs, no-history/API-error fail-open, job-name matching).
  • actionlint (with shellcheck on the embedded run: blocks) clean on both workflows.

…l gate (BE-4004)

GitHub Actions `schedule:` cron is static in the workflow file — there is no
native runtime "every N days". Replace groom's hardcoded weekly cron with the
standard frequent-base-cron + runtime-gate pattern so cadence is a live knob:

- ci-groom.yml now fires DAILY; the effective cadence is a new runtime gate.
- New reusable input `interval_days` (default 7 = today's weekly behavior),
  wired in the caller to repo Actions variable GROOM_INTERVAL_DAYS. Changing the
  variable retunes cadence (weekly -> every-3-days -> daily) with no file edit.
- `.github/groom/interval.py`: the gate. At run start it derives the last REAL
  groom run from this caller's Actions run history (a run counts only if its
  finder job actually ran, so interval-skip ticks never reset the clock) and
  early-exits cheaply — before the finder — unless interval_days have elapsed.
  Durable across stateless CI runs, no net-new secret, only `actions: read`.
  Pure decision logic split from the gh I/O shell + a unittest suite, mirroring
  ledger.py.
- `workflow_dispatch` always bypasses the interval gate (manual override).
- Fail-open on any history-read error (mirrors the volume gate).

The gate runs in the existing `gate` job before the finder; the volume gate is
kept as a second throttle with its window matched to the effective cadence.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 43d86942-2bcb-4344-b729-ae128ec60b05

📥 Commits

Reviewing files that changed from the base of the PR and between acf6445 and 160fdea.

📒 Files selected for processing (5)
  • .github/groom/README.md
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/groom.yml
  • README.md
📝 Walkthrough

Walkthrough

The groom workflows now run on a daily base schedule and apply a configurable runtime interval gate. The gate inspects audited workflow history, supports manual-dispatch bypasses, fails open on errors, and integrates with the existing volume gate.

Changes

Groom cadence control

Layer / File(s) Summary
Interval decision logic
.github/groom/interval.py, .github/groom/tests/test_interval.py
Adds interval parsing, timestamp handling, audited-run detection, and pure run-or-skip decisions with unit coverage.
Run-history evaluation and CLI
.github/groom/interval.py, .github/groom/tests/test_interval.py
Queries workflow runs and jobs, identifies the last audited run, applies dispatch and fail-open behavior, and emits JSON decisions.
Workflow cadence and gate integration
.github/workflows/ci-groom.yml, .github/workflows/groom.yml
Changes schedules to daily base cron, wires GROOM_INTERVAL_DAYS, adds actions: read, and separates interval, volume, and final decision gates.
Cadence documentation
.github/groom/README.md, AGENTS.md, README.md
Documents runtime cadence, history-based gating, dispatch behavior, permissions, and configuration.

Sequence Diagram(s)

sequenceDiagram
  participant GroomWorkflow
  participant interval.py
  participant GitHubActions
  participant VolumeGate
  GroomWorkflow->>interval.py: Evaluate interval gate
  interval.py->>GitHubActions: Read workflow runs and finder jobs
  GitHubActions-->>interval.py: Return run history
  interval.py-->>GroomWorkflow: Return interval result
  GroomWorkflow->>VolumeGate: Evaluate merged-PR volume when interval allows
  VolumeGate-->>GroomWorkflow: Return volume result
  GroomWorkflow-->>GroomWorkflow: Set should_run from both gate results
Loading
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4004-groom-interval-days
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4004-groom-interval-days

Comment @coderabbitai help to get the list of available commands.

@mattmillerai mattmillerai added agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review labels Jul 22, 2026
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 55-59: Update the interval.py description in AGENTS.md to state
that the cadence gate derives its state from GitHub Actions run history, rather
than GitHub issue and PR state. Keep the issue/PR storage description associated
only with the ledger behavior and preserve the existing GROOM_INTERVAL_DAYS
context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 679677d0-a97e-48ad-837f-ec1ed95ed544

📥 Commits

Reviewing files that changed from the base of the PR and between cc325ca and bfb6a5e.

📒 Files selected for processing (7)
  • .github/groom/README.md
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/ci-groom.yml
  • .github/workflows/groom.yml
  • AGENTS.md
  • README.md

Comment thread AGENTS.md Outdated
mattmillerai and others added 2 commits July 21, 2026 20:48
The cadence gate derives state from Actions run history, not GitHub
issue+PR state (that's the ledger's store); drop the misplaced sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…terval-days

# Conflicts:
#	.github/workflows/ci-groom.yml
#	.github/workflows/groom.yml
#	README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci-groom.yml (2)

82-94: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Replace both placeholders before merge. uses: and workflows_ref need the same real squash-merge SHA, and uses: should keep the trailing # v1 comment. As-is, the reusable pin won’t resolve cleanly once this lands, and the groomer will go bonkers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-groom.yml around lines 82 - 94, Replace both
REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA placeholders in the groom workflow
configuration with the actual squash-merge commit SHA, using the identical SHA
for uses and workflows_ref. Preserve the trailing # v1 comment on the uses
reference.

Source: Coding guidelines


98-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the cadence once before both gates. interval.py already turns blank, negative, and bad values into 7 while honoring 0, but the volume gate still feeds inputs.cadence straight into date -d. That means the two gates can drift apart, and 0 won’t mean the same thing in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-groom.yml around lines 98 - 104, Normalize
GROOM_INTERVAL_DAYS once using the existing interval.py behavior, including
blank, negative, invalid, and zero values, then reuse that normalized value for
both interval_days and cadence. Update the workflow’s volume-gate input so it no
longer passes the raw inputs.cadence directly to date -d, keeping both gates
aligned on the same effective cadence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 18: The README workflow description incorrectly says the audit job is
contents-read-only. Update the groom.yml description in the table entry to state
that the audit agent jobs have no repository write permissions, while preserving
the existing explanation that finder/verifier jobs require id-token: write for
OIDC.

---

Outside diff comments:
In @.github/workflows/ci-groom.yml:
- Around line 82-94: Replace both REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA
placeholders in the groom workflow configuration with the actual squash-merge
commit SHA, using the identical SHA for uses and workflows_ref. Preserve the
trailing # v1 comment on the uses reference.
- Around line 98-104: Normalize GROOM_INTERVAL_DAYS once using the existing
interval.py behavior, including blank, negative, invalid, and zero values, then
reuse that normalized value for both interval_days and cadence. Update the
workflow’s volume-gate input so it no longer passes the raw inputs.cadence
directly to date -d, keeping both gates aligned on the same effective cadence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35764ef5-53ef-4885-9882-f0855d3d0063

📥 Commits

Reviewing files that changed from the base of the PR and between bfb6a5e and acf6445.

📒 Files selected for processing (4)
  • .github/workflows/ci-groom.yml
  • .github/workflows/groom.yml
  • AGENTS.md
  • README.md

Comment thread README.md Outdated
…4004)

Both gates are wired to the same GROOM_INTERVAL_DAYS knob in the caller, but
only the interval gate normalized it — the volume gate fed inputs.cadence
straight to `date -d`, so the two drifted on reachable values:

  -3  interval.py -> 7 (safe weekly default), but `date -d '-3 days ago'` is a
      cutoff 3 days in the FUTURE, matching no merged PR -> the volume gate
      skips EVERY run, silently disabling groom.
  0   a legitimate "no throttle" for the interval gate, but a 0-day merge
      window is today-only, so most ticks are judged quiescent.

Add normalize_cadence_days() next to parse_interval_days() (same
blank/garbage/negative -> 7 degradation, then floored at 1 whole day) plus a
flag-only `interval.py --normalize-cadence <raw>` mode the volume gate shells
out to, so one parser backs both gates. The floor removes no capability:
`volume_gate: false` is the input that expresses "no merge-activity throttle".

Also correct the README's groom permission claim — the audit jobs are not
`contents: read` only; they also hold the `id-token: write` claude-code-action
needs to mint its OIDC token. Reworded to "no repository write credentials",
which is the property that actually matters.

Both per CodeRabbit review on #56.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review in 160fdea. Both out-of-diff findings, plus the inline README one:

ci-groom.yml 98-104 — normalize the cadence once before both gates: fixed, and it was a real bug.

Fixed in the reusable (groom.yml) rather than this caller, so every consumer gets it — a caller can't sanitize a vars.* value in a GitHub expression anyway.

normalize_cadence_days() now sits next to parse_interval_days() in interval.py (same blank/garbage/negative → 7 degradation, then floored at 1 whole day), and the volume gate shells out to a new flag-only interval.py --normalize-cadence "$CADENCE" mode instead of feeding the raw value to date -d. One parser now backs both gates.

Confirmed the drift empirically against a GNU-date harness (today = 2026-07-23):

GROOM_INTERVAL_DAYS interval gate volume gate before volume gate after
-3 7 (safe default) cutoff 2026-07-263 days in the future, matches no merged PR → skips every run 7 → 2026-07-16
0 0 = no throttle 2026-07-23 — today-only window 1 → 2026-07-22
abc 7 date errors → fail-open (gate effectively off) 7 → 2026-07-16

The -3 row is the one that bites: a misconfigured negative value degraded the interval gate safely to weekly while silently disabling groom entirely through the volume gate. Note the floor removes no capability — volume_gate: false is the input that expresses "no merge-activity throttle", and it stays available (this caller already uses it for dry-runs).

Covered by 5 new cases in test_interval.py (70 pass); actionlint clean on both workflows.

ci-groom.yml 82-94 — replace the placeholders: working as intended, no change.

REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA can't be resolved before merge — the squash SHA doesn't exist yet, and this caller deliberately pins by SHA rather than a local ./ path so a PR can't run its own copy of the auditor that judges it (the AGENTS.md "scripts are the single source of truth" rule). This is the established reusable-lands-then-bump-caller two-step used for the pilot (#49#53), and the placeholder is chosen to fail loud (unresolvable ref) rather than silently run a stale groom — the header comment at lines 82-86 documents the post-merge bump. The trailing # v1 convention applies to third-party action pins; this self-referential pin carries a # BE-4004 interval gate — bump to merge SHA marker instead, matching the existing ci-*.yml callers here.

README.md 18 — audit permission description: fixed (replied on the thread).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant