From 052efdff3ac180dbcb67b14b250285973bdcfcdf Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 21 Jul 2026 12:15:57 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(groom):=20pilot=20ci-groom.yml=20calle?= =?UTF-8?q?r=20=E2=80=94=20groomer-only,=20dry-run=20parity=20dispatch=20(?= =?UTF-8?q?BE-3873)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enroll this repo as the first groom pilot (Groom Phase 3). A thin caller pins the merged reusable groom.yml by full commit SHA, runs weekly on schedule (never on-PR) in groomer-only mode (finds + files `groom`-labeled GitHub issues; no commits/PRs/builder), and adds a manual dispatch with a dry_run toggle for spot-checking parity against a studio groom run before trusting live filing. Files issues as cloud-code-bot (App identity already provisioned here for cursor-review), so the caller needs no issues:write of its own. --- .github/workflows/ci-groom.yml | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/ci-groom.yml diff --git a/.github/workflows/ci-groom.yml b/.github/workflows/ci-groom.yml new file mode 100644 index 0000000..36e4d8d --- /dev/null +++ b/.github/workflows/ci-groom.yml @@ -0,0 +1,72 @@ +name: CI - Groom + +# Thin caller enrolling THIS repo as the FIRST groom pilot (BE-3873 / Groom +# Phase 3). The groom logic (two-phase finder -> independent verifier audit, the +# durable dedup/rejection ledger, the GitHub-issue sink) lives in groom.yml in +# this same repo; the caller pins it by full commit SHA like every external +# consumer — a local `uses: ./...` path would run the PR's OWN copy of the +# auditor, letting a PR weaken the reviewer judging it. Named ci-groom.yml +# because the canonical caller path (groom.yml) is occupied here by the reusable +# itself (same reason as ci-cursor-review.yml / ci-detect-unreviewed-merge.yml). +# +# GROOMER-ONLY mode: groom only FINDS and FILES `groom`-labeled GitHub issues — +# no commits, no PRs, no auto-builder, never merges. It runs weekly on a schedule +# (never on-PR), plus a manual dispatch carrying a `dry_run` toggle so the audit + +# dedup can be spot-checked against a studio groom run on the same commit BEFORE +# trusting live filing (the Phase 3 dry-run parity gate). +# +# PREREQUISITE (operator): `secrets.ANTHROPIC_API_KEY` must be available to this +# repo (org- or repo-level) — the finder + verifier agents bill through it. Until +# it exists the run fails loudly at the agent step, deliberately, so the pilot is +# never silently inert. Filing uses the cloud-code-bot App identity (vars.APP_ID +# + secrets.CLOUD_CODE_BOT_PRIVATE_KEY — already provisioned here for +# cursor-review), so groom's issues are a distinct, queryable actor rather than +# github-actions[bot], and the caller needs no issues:write scope of its own. + +on: + schedule: + # Weekly, Monday 09:17 UTC. Off-peak + a non-round minute to dodge the + # top-of-hour GitHub scheduler congestion. Never fires on a PR. + - cron: '17 9 * * 1' + workflow_dispatch: + inputs: + dry_run: + description: >- + Run the full audit + dedup but do NOT open issues — print what WOULD be + filed to the job summary. Use it to spot-check parity against a studio + groom run on the same repo/commit before trusting live filing. + type: boolean + default: false + +# Serialize runs per target repo — the dedup ledger reads live issue state and +# THEN files, so two overlapping runs could both classify a signature as new and +# file duplicates (a TOCTOU race). Belt to the reusable's own `concurrency:` +# suspenders; never cancel a half-filed run. +concurrency: + group: groom-${{ github.repository }} + cancel-in-progress: false + +jobs: + groom: + permissions: + # Least privilege: the App token (bot_app_id) does the issue I/O, so the + # caller's own github.token stays read-only. (Grant issues:write here ONLY + # if you drop bot_app_id and fall back to filing as github-actions[bot].) + contents: read + uses: Comfy-Org/github-workflows/.github/workflows/groom.yml@07154fb5d0d979017d79b822873cfbe86483bfb3 # main (groom.yml lands here — #49 / BE-3872) + with: + # File groom's issues as cloud-code-bot (App token), not github-actions[bot]. + bot_app_id: ${{ vars.APP_ID }} + # Keep the assets ref (finder/verifier briefs + dedup ledger) in lock-step + # with the `uses:` ref above, so a run always loads the briefs that match + # the workflow filing them. + workflows_ref: 07154fb5d0d979017d79b822873cfbe86483bfb3 + # Manual dispatch can dry-run; the weekly schedule always files live + # (github.event.inputs is null on a schedule event -> '' != 'true' -> false). + dry_run: ${{ github.event.inputs.dry_run == 'true' }} + # Match the volume-gate window to this caller's ~weekly cron, so a week + # with nothing merged skips the (expensive) audit. + cadence: 7 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLOUD_CODE_BOT_PRIVATE_KEY }} From 1d9737460b081c206f73bcd1c0ec74b08e0249be Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 21 Jul 2026 12:32:15 -0700 Subject: [PATCH 2/2] fix(groom): drop deadlocking caller concurrency + bypass volume_gate on dry-run (BE-3873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address cursor-review panel findings on the ci-groom.yml pilot caller: - High: the caller declared `concurrency: groom-${{ github.repository }}` identical to the reusable groom.yml's own top-level group with cancel-in-progress:false. The caller run holds that group while its reusable `uses:` job waits to acquire the same group -> self-deadlock, run hangs to timeout, nothing filed. The reusable already serializes per-repo across ALL callers, so the caller block was redundant; remove it and document why the caller must stay out of the group. - Medium: a manual dry-run dispatch inherited the reusable's default volume_gate:true + cadence:7, so a quiet week silently skipped the audit and produced no parity summary — defeating the Phase 3 dry-run parity gate. Pass volume_gate: ${{ github.event.inputs.dry_run != 'true' }} so a dry-run always runs the full audit while live/scheduled runs keep the gate. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci-groom.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-groom.yml b/.github/workflows/ci-groom.yml index 36e4d8d..fd4408e 100644 --- a/.github/workflows/ci-groom.yml +++ b/.github/workflows/ci-groom.yml @@ -38,13 +38,15 @@ on: type: boolean default: false -# Serialize runs per target repo — the dedup ledger reads live issue state and -# THEN files, so two overlapping runs could both classify a signature as new and -# file duplicates (a TOCTOU race). Belt to the reusable's own `concurrency:` -# suspenders; never cancel a half-filed run. -concurrency: - group: groom-${{ github.repository }} - cancel-in-progress: false +# NOTE: intentionally NO caller-level `concurrency:` here. The reusable groom.yml +# already declares `concurrency: groom-${{ github.repository }}` (cancel-in- +# progress: false), which serializes runs per target repo across ALL callers — +# that is the real TOCTOU guard for the read-then-file dedup ledger. Duplicating +# the identical group in this caller would DEADLOCK the run: the caller holds the +# group while its reusable `uses:` job waits to acquire the SAME group with +# cancel-in-progress:false, so the reusable never gets the slot and the run hangs +# until timeout (GitHub Actions caller/callee same-group deadlock). Serialization +# lives in the reusable; the caller deliberately stays out of the group. jobs: groom: @@ -67,6 +69,14 @@ jobs: # Match the volume-gate window to this caller's ~weekly cron, so a week # with nothing merged skips the (expensive) audit. cadence: 7 + # A dry-run is the Phase 3 parity spot-check, so it must ALWAYS exercise the + # full audit + dedup — bypass the volume gate when dispatched with + # dry_run:true. Otherwise a quiet week (nothing merged in `cadence` days) + # would gate the dry-run to nothing and print no parity summary, defeating + # the dispatch's whole purpose. Live runs (the weekly schedule, where inputs + # is null -> '' != 'true' -> true, or a dry_run:false dispatch) keep the gate + # on so a quiescent repo still skips the spend. + volume_gate: ${{ github.event.inputs.dry_run != 'true' }} secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BOT_APP_PRIVATE_KEY: ${{ secrets.CLOUD_CODE_BOT_PRIVATE_KEY }}