Skip to content

Commit 72223ad

Browse files
authored
Update PR visual recap workflow with cost controls (#268)
Recaps now auto-run only for large changes (>= 20 changed files or >= 600 changed lines by default; override via VISUAL_RECAP_MIN_FILES / VISUAL_RECAP_MIN_LINES repository variables). Small PRs are skipped silently before any model invocation. The synchronize and closed triggers are removed, so recaps are not regenerated on every push or on merge. Adding the visual-recap label forces a run at any size; remove and re-add the label to regenerate.
1 parent b66aa1c commit 72223ad

1 file changed

Lines changed: 47 additions & 7 deletions

File tree

.github/workflows/pr-visual-recap.yml

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ name: PR Visual Recap
33
# Visual code review: a coding agent runs the repo's visual-recap skill over the
44
# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.
55
# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.
6+
#
7+
# Cost policy: recaps auto-run only for LARGE changes — at least
8+
# VISUAL_RECAP_MIN_FILES changed files OR VISUAL_RECAP_MIN_LINES changed lines
9+
# (defaults 20 / 600, overridable via repo variables). Smaller PRs skip
10+
# silently in the gate job, BEFORE any model invocation or spend. Adding the
11+
# `visual-recap` label forces a run at any size; remove and re-add the label
12+
# to regenerate. `synchronize` is intentionally NOT a trigger (no regeneration
13+
# on every push), and `closed` was removed with it (no regeneration on merge).
614

715
on:
816
pull_request:
9-
types: [opened, synchronize, reopened, ready_for_review, closed]
17+
types: [opened, reopened, ready_for_review, labeled]
1018

1119
permissions:
1220
contents: read
@@ -27,14 +35,17 @@ jobs:
2735
# Hard gate, evaluated before any step runs: only same-repository branches
2836
# from trusted authors (OWNER/MEMBER/COLLABORATOR) start this workflow.
2937
# Fork PRs, bot PRs (Dependabot etc.), and drafts are skipped entirely —
30-
# no secrets, no comments, no red checks. The gate step below additionally
38+
# no secrets, no comments, no red checks. For `labeled` events, only the
39+
# `visual-recap` label starts the job — every other label addition skips
40+
# the whole workflow with zero noise. The gate step below additionally
3141
# no-ops gracefully while the recap secrets are not yet configured, so this
3242
# file can merge before any API key exists.
3343
if: >-
3444
github.event.pull_request.head.repo.full_name == github.repository &&
3545
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) &&
3646
github.event.pull_request.user.type != 'Bot' &&
37-
github.event.pull_request.draft == false
47+
github.event.pull_request.draft == false &&
48+
(github.event.action != 'labeled' || github.event.label.name == 'visual-recap')
3849
# A custom plain-label runner is allowed only for trusted same-repo authors.
3950
# Fork and untrusted PRs are forced onto GitHub-hosted ubuntu-latest before
4051
# any step starts. The only fromJSON input is this static association list.
@@ -62,6 +73,9 @@ jobs:
6273
VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}
6374
VISUAL_RECAP_RUNS_ON: ${{ vars.VISUAL_RECAP_RUNS_ON || '"ubuntu-latest"' }}
6475
VISUAL_RECAP_SKILL_SOURCE: ${{ env.VISUAL_RECAP_SKILL_SOURCE }}
76+
# Auto-run size thresholds (cost gate). Overridable via repo variables.
77+
VISUAL_RECAP_MIN_FILES: ${{ vars.VISUAL_RECAP_MIN_FILES || '20' }}
78+
VISUAL_RECAP_MIN_LINES: ${{ vars.VISUAL_RECAP_MIN_LINES || '600' }}
6579
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
6680
with:
6781
script: |
@@ -70,8 +84,31 @@ jobs:
7084
7185
if (!pr) reasons.push('no pull_request payload');
7286
if (pr && pr.draft) reasons.push('draft PR');
73-
if (pr && context.payload.action === 'closed' && !pr.merged) {
74-
reasons.push('closed without merge');
87+
88+
// Cost gate: auto-run only for large changes. The `visual-recap`
89+
// label forces a run at any size (labeled events reach this job
90+
// only for that exact label, per the job-level if — re-adding the
91+
// label after removing it re-fires this workflow and regenerates
92+
// the recap). Thresholds come from repo variables
93+
// VISUAL_RECAP_MIN_FILES / VISUAL_RECAP_MIN_LINES (defaults
94+
// 20 files / 600 changed lines). Size stats are the raw GitHub PR
95+
// numbers from the event payload (lockfiles included) — no API
96+
// call, no checkout, and the recap job (and every model
97+
// invocation) is skipped entirely when the gate says no.
98+
const parseThreshold = (raw, fallback) => {
99+
const value = Number.parseInt(String(raw == null ? '' : raw).trim(), 10);
100+
return Number.isInteger(value) && value >= 0 ? value : fallback;
101+
};
102+
const minFiles = parseThreshold(process.env.VISUAL_RECAP_MIN_FILES, 20);
103+
const minLines = parseThreshold(process.env.VISUAL_RECAP_MIN_LINES, 600);
104+
const labelNames = ((pr && pr.labels) || []).map((l) => ((l && l.name) || '').toLowerCase());
105+
const hasRecapLabel = labelNames.includes('visual-recap');
106+
const changedFiles = (pr && pr.changed_files) || 0;
107+
const changedLines = ((pr && pr.additions) || 0) + ((pr && pr.deletions) || 0);
108+
let skippedBySize = false;
109+
if (pr && !hasRecapLabel && changedFiles < minFiles && changedLines < minLines) {
110+
skippedBySize = true;
111+
reasons.push(`below auto-run size threshold (${changedFiles} files / ${changedLines} changed lines; auto-runs at >= ${minFiles} files or >= ${minLines} lines) — add the "visual-recap" label to generate a recap anyway`);
75112
}
76113
77114
// Fork PRs only receive repo secrets when the org/repo opts into
@@ -195,8 +232,11 @@ jobs:
195232
}
196233
197234
// When skipping, upsert a sticky recap comment with a short skip
198-
// line so the PR always explains why the recap job did not run.
199-
if (!run && pr) {
235+
// line so the PR explains why the recap job did not run — EXCEPT
236+
// when the size gate skipped it: size skips are the expected
237+
// outcome for most PRs, so they stay silent (notice annotation
238+
// only, no comment spam on every small PR).
239+
if (!run && pr && !skippedBySize) {
200240
try {
201241
const MARKER = '<!-- pr-visual-recap -->';
202242
const { data: comments } = await github.rest.issues.listComments({

0 commit comments

Comments
 (0)