Skip to content

Commit e5093df

Browse files
committed
fix(review): match the other reviewer by workflow too
1 parent d7f8a87 commit e5093df

4 files changed

Lines changed: 63 additions & 9 deletions

File tree

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ one, and after the trial that is the only shape a real drift takes, so it is the
9292
pin.
9393

9494
The CI block is the third channel and the one with teeth, so `CHECKS_JQ` and `FAILING_JOBS_JQ`
95-
drop the same reviewer's checks by name — matched against the app slug, since a GitHub App's login
96-
is its slug plus `[bot]`, so `$skip` stays the single constant. Pullfrog's verdict is a check,
95+
drop the same reviewer's checks through one shared owner test, `CHECK_OWNER_JQ`, composed into both
96+
the way the workflow composes `CMD_JQ` into `TOOL_USAGE_JQ` — two copies of that rule were free to
97+
disagree about what "theirs" means, and did. It matches the app slug against both the entry's own
98+
name and its `workflowName`, downcased, since a GitHub App's login is its slug plus `[bot]` (so
99+
`$skip` stays the single constant) while an Actions check run carries the *job* name in `.name`
100+
matching that alone would make the exclusion depend on a job key in another repository. Pullfrog's verdict is a check,
97101
`pullfrog-approval`, failing when it requested changes, and this prompt tells the reviewer that a
98102
failing check is a blocking issue to name and cite. Unfiltered, the rollup does not merely leak
99103
the other arm's conclusion; it converts it into a request-changes this reviewer cannot
@@ -113,7 +117,11 @@ relabelled rather than renumbered or dropped.
113117

114118
Pullfrog cannot submit an approving review and `pullfrog-approval` is not required in the org
115119
ruleset, so this reviewer remains the only automated approval in the org for the duration of the
116-
trial. The constant, the four exclusions, and this section come out when the trial ends.
120+
trial. The constant, the four exclusions, the drift predicate's exception and this section come out
121+
when the trial ends — the drift predicate by name, because it is the one place an incomplete
122+
removal is silent. Delete `OTHER_REVIEW_BOT` and leave `$skip` in `DRIFT_JQ` and its `jq` call
123+
fails to compile, which the `elif` guarding it reads as "no drift": the backstop against a
124+
reviewer-login change would be gone with no warning in either direction.
117125

118126
### Tool usage artifact
119127

scripts/gather-review-context.sh

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -568,14 +568,30 @@ cap_file_escaped "$FILES_FILE" $((CTX_MAX_BYTES / 8)) \
568568
# Silent while other checks remain, and stated when the exclusion empties the block: a list
569569
# claims nothing about being every check, but "No checks reported." on a PR that has some is
570570
# a false claim, and this is the block whose emptiness the README warns gets read as green.
571-
CHECKS_JQ='def slug: $skip | sub("\\[bot\\]$"; ""); def theirs: ((if .__typename == "CheckRun" then (.name // "") else (.context // "") end) | . == slug or startswith(slug + "-")); def render: if .__typename == "CheckRun" then "\(.conclusion // .status // "UNKNOWN") \(.workflowName // "") / \(.name // "(unnamed check)")" else "\(.state // "UNKNOWN") \(.context // "status")" end; (.statusCheckRollup // []) as $all | ($all | map(select(theirs | not))) as $kept | (($all | length) - ($kept | length)) as $dropped | if ($kept | length) == 0 then (if $dropped > 0 then "No checks reported. (\($dropped) check(s) from \(slug) are excluded from this block; they carry a verdict from another reviewer, not a CI result.)" else "No checks reported." end) else ($kept | map(render) | sort | join("\n")) end'
571+
# One definition of "belongs to the other reviewer", composed into both programs below rather
572+
# than written into each: they are two programs, and a second copy of the rule is a copy free to
573+
# disagree with the first about what it matches -- which it already did, the failing-job scan
574+
# testing only .name while the list tested .context as well. Composed the way the workflow
575+
# composes CMD_JQ into TOOL_USAGE_JQ.
576+
#
577+
# It reads both names on the entry, not one. For a check the app posts itself, .name is the check
578+
# ("pullfrog", "pullfrog-approval") and there is no workflow behind it; for one that reached the
579+
# rollup from an Actions run, .name is the *job* name and .workflowName is the workflow's `name:`.
580+
# So a name-only test makes this exclusion depend on a job key in another repository staying
581+
# `pullfrog`: `name: Pullfrog` over a job called `review` arrives as
582+
# {name: "review", workflowName: "Pullfrog"} and slips through both programs whole. Dropping a
583+
# check belonging to a workflow named for the other reviewer is the intent in either shape.
584+
# Compared downcased for the same reason -- the slug is lowercase by construction, and a job name
585+
# is whatever someone typed.
586+
CHECK_OWNER_JQ='def slug: $skip | sub("\\[bot\\]$"; "") | ascii_downcase; def theirs: [(if .__typename == "CheckRun" then (.name // "") else (.context // "") end), (.workflowName // "")] | any(. != "" and (ascii_downcase | . == slug or startswith(slug + "-")));'
587+
CHECKS_JQ='def render: if .__typename == "CheckRun" then "\(.conclusion // .status // "UNKNOWN") \(.workflowName // "") / \(.name // "(unnamed check)")" else "\(.state // "UNKNOWN") \(.context // "status")" end; (.statusCheckRollup // []) as $all | ($all | map(select(theirs | not))) as $kept | (($all | length) - ($kept | length)) as $dropped | if ($kept | length) == 0 then (if $dropped > 0 then "No checks reported. (\($dropped) check(s) from \(slug) are excluded from this block; they carry a verdict from another reviewer, not a CI result.)" else "No checks reported." end) else ($kept | map(render) | sort | join("\n")) end'
572588
# Actions check runs carry the job id in detailsUrl; scan rather than capture so a
573589
# non-Actions check with no job id drops out instead of erroring.
574-
FAILING_JOBS_JQ='def slug: $skip | sub("\\[bot\\]$"; ""); [(.statusCheckRollup // [])[] | select(.__typename == "CheckRun") | select(((.name // "") | . == slug or startswith(slug + "-")) | not) | select((.conclusion // "") | test("FAILURE|TIMED_OUT|ACTION_REQUIRED")) | (.detailsUrl // "") | [scan("/job/([0-9]+)")] | flatten | .[0] // empty] | unique | .[0:3] | join(" ")'
590+
FAILING_JOBS_JQ='[(.statusCheckRollup // [])[] | select(.__typename == "CheckRun") | select(theirs | not) | select((.conclusion // "") | test("FAILURE|TIMED_OUT|ACTION_REQUIRED")) | (.detailsUrl // "") | [scan("/job/([0-9]+)")] | flatten | .[0] // empty] | unique | .[0:3] | join(" ")'
575591
if ROLLUP=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json statusCheckRollup); then
576-
CHECKS=$(printf '%s' "$ROLLUP" | jq --arg skip "$OTHER_REVIEW_BOT" -r "$CHECKS_JQ" 2>/dev/null) \
592+
CHECKS=$(printf '%s' "$ROLLUP" | jq --arg skip "$OTHER_REVIEW_BOT" -r "$CHECK_OWNER_JQ $CHECKS_JQ" 2>/dev/null) \
577593
|| CHECKS="Could not parse checks."
578-
JOB_IDS=$(printf '%s' "$ROLLUP" | jq --arg skip "$OTHER_REVIEW_BOT" -r "$FAILING_JOBS_JQ" 2>/dev/null) || JOB_IDS=''
594+
JOB_IDS=$(printf '%s' "$ROLLUP" | jq --arg skip "$OTHER_REVIEW_BOT" -r "$CHECK_OWNER_JQ $FAILING_JOBS_JQ" 2>/dev/null) || JOB_IDS=''
579595
else
580596
echo "::warning::Could not read check status."
581597
CHECKS="Could not read check status."

tests/fixtures/rollup-with-review-bot.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@
3030
"startedAt": "2026-08-28T17:48:02Z",
3131
"completedAt": "2026-08-28T17:52:11Z"
3232
},
33+
{
34+
"__typename": "CheckRun",
35+
"name": "review",
36+
"workflowName": "Pullfrog",
37+
"status": "COMPLETED",
38+
"conclusion": "FAILURE",
39+
"detailsUrl": "https://github.com/hotdata-dev/github-workflows/actions/runs/33190500002/job/98914300003",
40+
"startedAt": "2026-08-28T17:48:02Z",
41+
"completedAt": "2026-08-28T17:53:40Z"
42+
},
43+
{
44+
"__typename": "StatusContext",
45+
"context": "Pullfrog-Approval",
46+
"state": "FAILURE"
47+
},
3348
{
3449
"__typename": "StatusContext",
3550
"context": "aikido/code-scan",

tests/pr-context-test.sh

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ cd "$(dirname "$0")/.."
2121

2222
COMMITS_JQ=$(extract_jq COMMITS_JQ)
2323
FILES_JQ=$(extract_jq FILES_JQ)
24-
CHECKS_JQ=$(extract_jq CHECKS_JQ)
25-
FAILING_JOBS_JQ=$(extract_jq FAILING_JOBS_JQ)
24+
# Both check programs are shipped composed with the shared owner test, the way the workflow
25+
# ships CMD_JQ composed into TOOL_USAGE_JQ -- so they are exercised composed, and a change that
26+
# only holds in one of the two halves has nowhere to hide.
27+
CHECK_OWNER_JQ=$(extract_jq CHECK_OWNER_JQ)
28+
CHECKS_JQ="$CHECK_OWNER_JQ $(extract_jq CHECKS_JQ)"
29+
FAILING_JOBS_JQ="$CHECK_OWNER_JQ $(extract_jq FAILING_JOBS_JQ)"
2630
LAST_REVIEW_JQ=$(extract_jq LAST_REVIEW_JQ)
2731
COMPARE_STATUS_JQ=$(extract_jq COMPARE_STATUS_JQ)
2832
ISSUE_COMMENTS_JQ=$(extract_jq ISSUE_COMMENTS_JQ)
@@ -163,6 +167,17 @@ SUCCESS aikido/code-scan" \
163167
expect "$(plain rollup-with-review-bot.json "$FAILING_JOBS_JQ")" "98914277093" \
164168
"the other reviewer's failing check contributes no job log"
165169

170+
# The entry name is not the only place the other reviewer's identity shows up, and on an Actions
171+
# check run it is the *job* name -- so an exclusion testing only that depends on a job key in
172+
# another repository staying `pullfrog`, which nothing here pins. `{name: "review", workflowName:
173+
# "Pullfrog"}` is the ordinary shape of `name: Pullfrog` with a job called review, and the fixture
174+
# carries it alongside a `Pullfrog-Approval` status: both leaks come back whole under a
175+
# name-only, case-sensitive match.
176+
expect "$(plain rollup-with-review-bot.json "$CHECKS_JQ" | grep -ci pullfrog)" "0" \
177+
"a workflow named for the other reviewer is excluded whatever its job is called"
178+
expect "$(plain rollup-with-review-bot.json "$FAILING_JOBS_JQ" | grep -c '98914300003')" "0" \
179+
"that job's log is not fetched either"
180+
166181
# Both rollup shapes, because which one an app posts is the app's choice. The slug comes from
167182
# $skip -- a GitHub App's login is its slug plus "[bot]" -- so there is no second constant to
168183
# keep in step.

0 commit comments

Comments
 (0)