feat(seidroid-review): publish the review check under both names - #92
Conversation
PR SummaryMedium Risk Overview Publish the review check run now posts the same conclusion, title, and summary twice when enabled: Reviewed by Cursor Bugbot for commit c4a006f. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a909f42. Configure here.
There was a problem hiding this comment.
The dual publish itself is implemented cleanly (single artifact read, review first and authoritative, copy best-effort), but the failure fallback further down the workflow still supersedes only review, so the AI Review copy can remain green when the review failed to post — defeating the branch-protection rule this PR is meant to satisfy.
Findings: 1 blocking | 1 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
- 1 blocking issue(s) flagged inline on specific lines.
Non-blocking
- The copy can mask a genuine
AI Reviewfailure during the transition. On a repo still runningai-review.ymlunder the same App, if seidroid concludessuccessand completes after a realAI Reviewfailure,filter=latestshows green — the race the step's own comment describes, but in the direction that weakens a gate rather than just reordering it. Consider gating the copy behind a workflow input (default off, or off for repos still runningai-review.yml) so the transition is opt-in per caller rather than unconditional.
Comments that couldn't be anchored to the diff
.github/workflows/seidloid-review.yml:1137-- [nit]|| trueswallows a jq failure and leavestitleempty, which the check-runs API rejects (output.titleis required), so a malformed check file now fails with a 422 fromghinstead of jq's own error. A default keeps the publish working:title="${title:-review}"after the assignment.
| # A repository that also runs ai-review.yml under this App gets two checks | ||
| # called `AI Review` on the commit. GitHub keeps both and lists one: the one | ||
| # that completed last. The two publishes therefore race for the name. | ||
| if publish "AI Review"; then |
There was a problem hiding this comment.
[blocker] The AI Review copy is never superseded by the failure fallback, so it can stay green on the exact path the fallback exists to red-flag.
The "State the review's position on the pull request" step (~line 1751) publishes a second check run when the verdict comment could not be posted:
gh api -X POST "repos/$REPO/check-runs" -f name=review ... -f conclusion=failure \
-f output[title]="review produced but not published"
Its comment says it posts "under the same name as the check published above, so it supersedes that conclusion on this commit rather than sitting beside it." That reasoning held when this step published exactly one name. It now publishes two, and the fallback only supersedes review.
Result on that path: review = failure / "review produced but not published", while AI Review still carries the original conclusion — success when the review was clean. A branch-protection rule that requires AI Review (the whole premise of this PR) is then satisfied by a review that never reached the pull request. The same gap applies to the no-verdict path, where this step forces conclusion=failure before publishing but the fallback path can't correct the copy.
Fix: make the fallback publish both names too, e.g. reuse the same list of published names (write them to $GITHUB_OUTPUT here and loop over them there), so any name this step published is superseded by any name the fallback publishes.
There was a problem hiding this comment.
Fixed in a4f0cc3. Post the verdict publishes the fallback under both names now, review first and both best-effort. I did not pass the names through $GITHUB_OUTPUT. Both sites read the same publish-ai-review-check input instead, so one switch decides the set of names. That set cannot drift when the publish step dies before it writes an output.
Stub run, comment POST failing: copy on posts review and AI Review, both failure, both titled review produced but not published; copy off posts review alone; base d477b7d posts review alone in both cases.
a909f42 to
a4f0cc3
Compare
|
Both taken, one with a deliberate difference. a4f0cc3. The input, with the default the other way up. Added Your finding is the reason the switch exists. The measurement behind it is in the PR body, and it is what makes the race weaken a gate rather than just reorder it. The nit, applied with one correction. What I did measure is the loss of the intended default. On base |
There was a problem hiding this comment.
Publishing the review check under both review and AI Review is implemented correctly at both publish sites, with sound error posture (review stays the only publish that can fail the step) and a useful incidental fix to the check title default. One documentation inconsistency in the new input's description; no correctness or security problems found.
Findings: 0 blocking | 5 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Both publish sites are covered —
grepforcheck-runsfinds exactly the twogh api -X POSTcalls this PR touches, so there is no third site left publishingreviewalone. Ordering is also right:Publish the review check runruns beforePost the verdict, so the fallback'sfailuresupersedes both earlier names rather than sitting beside them. publish-ai-review-checkdefaults totrue, and the PR body identifiessei-internal-skillsas a repository that runs both tools under the App slugseidroid— i.e. a caller where the default is the wrong setting on day one. The caller lives outside this repo so it cannot be fixed here, but worth landingpublish-ai-review-check: falsein that caller alongside (or before) this merge rather than after, otherwise the first seidroid run there can overwriteai-review.yml'sAI Reviewverdict.- Nice incidental hardening:
titleis now computed once into a variable with a${title:-review}fallback, so the two publishes cannot disagree and a failingjqno longer yields an empty check title the way the base branch does.summarycorrectly needs no equivalent guard. - The
AI Reviewcopy failing is best-effort (warning only), which is the right posture — failing the step would not publish the check either. Worth being aware that in the one scenario the copy exists for (a rule requiringAI Review), a failed copy leaves that rule pending with only a workflow annotation to explain it. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
| `AI Review` waits forever on a check nothing publishes. That rule reads as | ||
| pending, not failed. | ||
|
|
||
| Set this false where the repository still runs `ai-review.yml` AND a rule |
There was a problem hiding this comment.
[suggestion] The guidance here is narrower than the actual collision. It says to set this false only where the repository runs ai-review.yml AND a rule names AI Review, but the harm the next sentence describes — "GitHub lists only the check run that completed last, and this copy can hide the incumbent's verdict" — happens whenever both tools publish the name under this App, rule or no rule. With no rule, nothing blocks the merge, but the checks list still shows seidroid's conclusion in place of ai-review.yml's; a green copy landing after a red incumbent silently replaces the visible signal a human reads.
The step comment at the publish site ("A repository that also runs ai-review.yml under this App gets two checks called AI Review on the commit") states it correctly without the rule qualifier. Suggest dropping the AND a rule names \AI Review`` condition here so the input's own documentation matches it.
Superseded: latest AI review found no blocking issues.
Every review run now publishes its check run twice: once as `review` and once as `AI Review`. Both carry the conclusion, title and summary this step decides, so a branch-protection rule that requires either name passes. `review` stays the check of record. It publishes first, and a failure there exits the step under `set -e`, where `continue-on-error` decides what it costs the job. The `AI Review` copy publishes second and warns on failure, so it can never stop `review` from reaching the pull request. The missing-`head_sha` path still fails the job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment on the `AI Review` copy claimed that the merge gate reads the check run that completed last. The measurement behind that covers the list endpoint, which returns one check run per name per check suite. How a branch-protection rule resolves two check runs of one name is untested here, so the comment now states the listing and the race, and claims nothing about the gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…et a caller stand the copy down The `Post the verdict` fallback publishes a `failure` check run when the verdict comment cannot be posted. It named only `review`, so the `AI Review` copy kept the review's earlier conclusion. A rule that names `AI Review` then read green on a verdict that never reached the pull request, which is the gate this copy exists to hold. The fallback now publishes every name the publish step writes, `review` first and both best-effort. The new `publish-ai-review-check` input turns the copy off. It defaults to true, so a repository under a ruleset nobody can read stays covered. A repository that still runs `ai-review.yml` and knows a rule names `AI Review` sets it false: both tools publish that name, GitHub lists the check run that completed last, and the copy can hide the incumbent's verdict. `title` falls back to `review` after the read. The `//` in the jq program covers a field that is absent, and the `|| true` beside it turns a jq that fails into an empty title. The summary needs no such line, because its default is the empty string either way. The file header and the `checks: write` comment now say check runs, plural. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`publish-ai-review-check` lets a caller stand the copy down. It does not remove the race between two publishers of one check name. A caller that leaves it on where the repository still runs `ai-review.yml` accepts that race, and the description now says so, so the caller reads it as a decision they are making. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a4f0cc3 to
c4a006f
Compare
There was a problem hiding this comment.
A focused, unusually well-documented change that publishes the review check run under both review and AI Review, gated by a new publish-ai-review-check input, at both the success and verdict-failure publish sites. The shell is correct and the two sites are consistently gated; my two notes are about the true default trading a fail-safe outcome (merge blocked) for an unsafe one (one tool's verdict masking the other's), and about a failed copy publish being reported only as a warning.
Findings: 0 blocking | 7 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Hoisting
title/summaryinto variables before the two publishes is a genuine improvement over the previous code, which re-ranjqfor the trailingechoand so could have printed a title different from the one published. The${title:-review}guard also correctly covers the empty-string case that jq's//does not. - The two publish sites are consistently gated on the same
inputs.mode == 'review'condition and read the sameAI_REVIEW_COPYenv, so the set of names published on the success path and on the verdict-failure path does match, as the comment at line 2100 claims. - No test or dry-run coverage exists for either publish path, so the only verification of the new branch is the workflow running for real. That is the pre-existing posture for this file, not something this PR changed, but the new conditional doubles the number of untested paths through the publish step.
- 2 suggestion(s)/nit(s) flagged inline on specific lines.
- 2 non-blocking pre-existing issue(s) listed below under pre-existing issues.
Pre-existing issues
- [suggestion]
.github/workflows/seidroid-review.yml:1427—summaryis read from the driver's check artifact with no length bound before being sent asoutput[summary]. The GitHub checks API capsoutput.summaryat 65535 characters; an oversized summary makes the publish 422 and, on thereviewpublish, fails the step. The verdict comment path a few hundred lines below does bound its body (see theMaxBodyBytescomment at line ~1896); the check publish does not. - [suggestion]
.github/workflows/seidroid-review.yml:1394and:1863—GH_TOKENfalls back togithub.tokenwhen the App identity step produces nothing. A check run created bygithub-actionslands in a different check suite than one created by theseidroidApp, so on a fallback run thefilter=latestsupersession reasoning in the new comments (and in the PR description's measurement) does not hold: the copy would sit beside the App's check rather than replacing it.
| choice to accept it or to stand the copy down. | ||
| required: false | ||
| type: boolean | ||
| default: true |
There was a problem hiding this comment.
[suggestion] Defaulting the copy to true swaps a fail-safe failure mode for an unsafe one.
The two error states are not symmetric:
- Copy off, rule requires
AI Review, nothing publishes it → check stays pending → merge is blocked. Noisy, but safe. - Copy on, repo still runs
ai-review.yml, seidroid completes last → seidroid's conclusion is the onefilter=latestreturns. Ifai-review.ymlconcludedfailureand seidroid concludessuccess, the required check reads green and the merge that the incumbent blocked goes through.
The step comment and the input description both name the race, but the default picks the side where the failure mode is a bad merge rather than a stuck one. Since a caller that has already retired ai-review.yml is the only one that needs the copy, and that caller is by definition making a deliberate migration change, default: false with an explicit opt-in gets the same coverage without ever letting one tool's verdict mask the other's. If the owner has decided on true (PLT-1152), consider at least scoping it to a documented removal date so no repo silently carries the race indefinitely.
| if publish "AI Review"; then | ||
| names="check runs review and AI Review" | ||
| else | ||
| echo "::warning::the AI Review copy of the review check could not be published on $REPO#$PR; review carries the verdict" |
There was a problem hiding this comment.
[suggestion] A failed AI Review publish reintroduces exactly the state this input exists to prevent, and does it silently.
If publish "AI Review" fails (transient 5xx, secondary rate limit, token scope), this warns and the step succeeds. A caller that retired ai-review.yml under a rule naming AI Review is then back to the pending-forever case from the input description — a green job, a warning annotation nobody reads, and a merge blocked on a check that will never arrive.
Keeping the copy from failing the review is right; making it indistinguishable from success is not. ::error:: instead of ::warning:: would at least surface it as an annotation on the run without changing the step's exit, and it costs nothing given continue-on-error already governs this step.

Every review run publishes its check run twice, as
reviewand asAI Review. Both publish sites carry both names. A caller can turn the copy off.What changed
Three sites in
.github/workflows/seidroid-review.yml.Publish the review check runsends the same conclusion, title and summary under both names. It reads the check artifact once, so the two cannot differ.reviewis unchanged: same name, same conclusion rule, same error posture.Post the verdict, on the path where the verdict comment fails to post, publishes itsfailurecheck run under both names as well. It named onlyreviewbefore, so the copy kept the review's earlier conclusion and a rule namingAI Reviewread green on a verdict that never arrived.publish-ai-review-check, a new boolean input, defaulttrue.The decision
The repository owner decided to publish both names (PLT-1152). A branch-protection rule matches a check by its name, so only a check called
AI Reviewsatisfies a rule that requiresAI Review. A repository that retiresai-review.ymlunder such a rule waits forever on a check nothing publishes, and the rule reads as pending, not failed. The rules on the calling repositories are org-level, and no token in this session can read them. Publishing both names is the safe answer under a rule nobody can see, so the input defaults to on. Delete the copy once someone who can read those rules confirms that none namesAI Review.publish-ai-review-check: falseis for a repository that runs both tools and knows its own ruleset. Both tools then publishAI Review, GitHub lists only the check run that completed last, and this copy can hide the incumbent's verdict. The switch does not remove that race. It makes the race a choice, and the description says so.Error posture
reviewAI ReviewPublish the review check runset -e, andcontinue-on-errordecides the job, as todayPost the verdictfallbackreviewpublishes first at both sites. The copy can never be the reason the primary fails to post. The missing-head_shapaths are unchanged: the publish step fails the job, the fallback warns.The two-publisher collision
The ticket asked me to confirm this rather than assume it. Half of the premise holds.
Two check runs of one name on one commit are legal. GitHub does not show both.
GET /repos/{owner}/{repo}/commits/{ref}/check-runsdefaults tofilter=latest, which returns one check run per name per check suite. The checks list on the pull request reads that default view.Measured on
sei-protocol/sei-chaincommit1a086bc, appcodecov, check suite91901537092:One name plus one app plus one commit therefore means that the later publish supersedes the earlier one in the view that matters.
sei-internal-skillsposts bothai-review.ymland seidroid under the App slugseidroid: itsAI Reviewcheck run on commit530588ccarries"app": {"slug": "seidroid"}. On that repository the twoAI Reviewpublishes race for the name, and the checks list shows whichever completed last. That measurement is why the input exists.Duplicates do sit side by side across check suites. Commit
530588ccarriesai-review / Claudenine times, one per workflow run.Verification
I extracted both steps with a YAML parser and ran them under
bashwith aghstub onPATH.Publish the review check run:review,AI Reviewpublished check runs review and AI Review: success — review: 0 blockersreviewpublished check run review: success — ...reviewfailshead_sha::error::the reviewed commit was not recorded on ...review,AI Reviewfailure, and the file sayssuccessno check run to publishreviewpublished check run review: ...review,AI Reviewreview; on the base branch it is emptyPost the verdict, the failure fallback:posted the verdict on ...review,AI Reviewfailure, both titledreview produced but not published; the base postsreviewalonereviewfailure, as the base doeshead_sha::warning::no reviewed commit was recorded ...The base branch's scripts exit the same way on all twelve cases.
The harness reads the workflow-level
env:out of the file rather than restating it.#86hoistedVERDICT_MARKERfrom thePost the verdictstep env to a workflow-levelenv:, and a harness that named the old key modelled a step that no longer exists. All fourfb-cases run through the line that reads it.actionlintover.github/workflows/*.yml, base5f5fd78and this branch: 6action, 30shellcheck(1x SC1102, 25x SC2086, 4x SC2102), 1syntax-check. Identical. Both newgh apicalls sit inside shell functions, so theoutput[title]andoutput[summary]literals stay at four.Not verified
AI Reviewis still unknown.🤖 Generated with Claude Code