SSC377QE - 30fps only, no cropping, majestic unstable #847
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Merge gate for the Qodo code-review agent. | |
| # | |
| # Mirrors OpenIPC/devourer's qodo-gate.yml, which has already been through the | |
| # failure modes documented below. Keep the two in sync when either is fixed. | |
| # | |
| # Why: branch protection's "require conversation resolution" only blocks | |
| # unresolved threads that EXIST at merge time. Qodo posts its review a minute | |
| # or two after the PR opens, so a "merge when CI is green" flow can race past | |
| # it. This required check stays red until (a) Qodo has reviewed the PR at least | |
| # once — the first, whole-diff pass is the valuable one — and (b) every review | |
| # thread Qodo opened is resolved. | |
| # | |
| # Deliberately NOT pinned to the current head: requiring a review of every | |
| # follow-up commit turns each review-response push into a fresh summon, and | |
| # each re-review re-scans the diff and opens a new batch of ever-smaller | |
| # findings — an unbounded fix/re-review treadmill. The first review catches | |
| # the substance; thread resolution keeps each finding accountable (address it | |
| # or dismiss it with rationale, in the thread, before resolving); follow-up | |
| # commits are maintainer judgment, exactly as with a human reviewer who does | |
| # not re-review every fixup. This repo also leaves handle_push_trigger off in | |
| # .pr_agent.toml for the same reason — re-review on demand with /review. | |
| # | |
| # Event-driven: re-evaluates when the PR updates, when a review is submitted, | |
| # and when someone replies in a review thread. GitHub's workflow parser | |
| # rejects the documented `pull_request_review_thread` trigger ("Unexpected | |
| # value" — verified empirically on devourer, zero-job "workflow file issue" | |
| # run), so plain thread resolution does not auto-retrigger — after resolving | |
| # the last thread, leave a reply (retriggers); the check reads the live | |
| # resolution state each run. Escape hatch for a Qodo outage: the | |
| # `skip-qodo-gate` label passes the check (label changes re-trigger it). | |
| # | |
| # A passing run also re-runs this workflow's earlier FAILED runs on the same | |
| # head commit. Each trigger event creates its own workflow run, and branch | |
| # protection's rollup counts every run of a required check on the commit — a | |
| # fresh green run sits beside the stale red ones rather than superseding | |
| # them, so the PR stays BLOCKED until each red run is re-run by hand. Only a | |
| # passing run re-runs others and a re-run that passes finds nothing red left, | |
| # so it converges; if threads are genuinely unresolved the re-runs go red | |
| # again and the gate still holds. | |
| name: qodo-gate | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled] | |
| pull_request_review: | |
| types: [submitted] | |
| pull_request_review_comment: | |
| types: [created, deleted] | |
| # A clean PR's only proof of review is an ISSUE comment, and without this | |
| # trigger nothing re-evaluates the gate when it arrives: the run fired at | |
| # `opened` fails (Qodo has not answered yet), the comment lands a minute or | |
| # two later, and the check stays red until an unrelated push happens to | |
| # re-trigger it. That is precisely how #2269 went green — a follow-up commit, | |
| # not the review — which hid the gap on the PR that introduced it. | |
| issue_comment: | |
| types: [created, edited] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| actions: write | |
| jobs: | |
| qodo-gate: | |
| runs-on: ubuntu-latest | |
| # Narrow the issue_comment trigger to the single comment that can change | |
| # this gate's verdict: Qodo's review. Without all three clauses the job is | |
| # an API-heavy workflow holding `actions: write` that any commenter could | |
| # run at will, on any PR, as often as they liked — CI noise, and needless | |
| # rate-limit exposure on a REQUIRED check. | |
| # | |
| # issue.pull_request — issue_comment fires for plain issues too, and | |
| # those have no PR to gate. | |
| # comment author — only the bot's own comment is evidence of review. | |
| # A human typing /review still works: Qodo answers, | |
| # and that answer is what triggers this. | |
| # comment body — the bot also posts "Qodo is busy working" and | |
| # "PR Summary by Qodo", neither of which means the | |
| # diff was reviewed. Matching the review header | |
| # keeps this to one run per review. | |
| # | |
| # Every other trigger here is PR-only and needs no guard. | |
| if: >- | |
| github.event_name != 'issue_comment' || | |
| (github.event.issue.pull_request && | |
| startsWith(github.event.comment.user.login, 'qodo-free-for-open-source-projects') && | |
| contains(github.event.comment.body, 'Code Review by Qodo')) | |
| steps: | |
| - name: Require a Qodo review with all its threads resolved | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| # issue_comment carries `issue`, not `pull_request` — for a PR | |
| # comment the issue number IS the PR number. | |
| PR: ${{ github.event.pull_request.number || github.event.issue.number }} | |
| REPO_OWNER: ${{ github.repository_owner }} | |
| REPO_NAME: ${{ github.event.repository.name }} | |
| run: | | |
| set -euo pipefail | |
| BOT='qodo-free-for-open-source-projects' | |
| json=$(gh api graphql \ | |
| -F owner="$REPO_OWNER" -F name="$REPO_NAME" -F pr="$PR" \ | |
| -f query=' | |
| query($owner: String!, $name: String!, $pr: Int!) { | |
| repository(owner: $owner, name: $name) { | |
| pullRequest(number: $pr) { | |
| labels(first: 100) { nodes { name } } | |
| } | |
| } | |
| }') | |
| if echo "$json" | jq -e --arg l skip-qodo-gate \ | |
| '.data.repository.pullRequest.labels.nodes[] | select(.name == $l)' \ | |
| >/dev/null; then | |
| echo "PASS: skip-qodo-gate label set (Qodo outage escape hatch)" | |
| exit 0 | |
| fi | |
| # Qodo signals a finished review in one of two ways, and a clean PR | |
| # only ever produces the second: | |
| # | |
| # 1. a review OBJECT — created when it has inline comments to hang | |
| # on the diff. Its later in-place updates edit the same object, | |
| # so one existing == the PR has been reviewed. | |
| # 2. an issue COMMENT headed "Code Review by Qodo" — which is ALL | |
| # a zero-finding PR gets. Verified on #2269: "Bugs (0), Rule | |
| # violations (0), Requirement gaps (0)", and no review object | |
| # at all. | |
| # | |
| # Counting only (1) therefore blocks every clean PR forever — the | |
| # gate can never go green precisely when there is nothing to fix. | |
| # | |
| # Paginated: a busy PR accumulates well over 100 review objects | |
| # (every inline reply wraps itself in one), and the bot's first | |
| # review is the OLDEST — exactly what a last-100 window loses | |
| # first. --paginate applies --jq per page, so emit ids and count | |
| # lines. REST spells the bot login with a [bot] suffix, unlike | |
| # GraphQL, so match on the prefix. | |
| reviewed=$(gh api "repos/$REPO_OWNER/$REPO_NAME/pulls/$PR/reviews" \ | |
| --paginate --jq ".[] | |
| | select(.user.login | startswith(\"$BOT\")) | |
| | .id" | wc -l) | |
| # Match the review header specifically, NOT merely "a comment by the | |
| # bot". Qodo also posts "Qodo is busy working" (a placeholder before | |
| # it has read anything), "PR Summary by Qodo" (/describe output, not | |
| # a review), and "failed to apply 'local' repo settings" (an error). | |
| # Treating any of those as a review would let the gate pass on a PR | |
| # the agent never actually reviewed. | |
| commented=$(gh api "repos/$REPO_OWNER/$REPO_NAME/issues/$PR/comments" \ | |
| --paginate --jq ".[] | |
| | select(.user.login | startswith(\"$BOT\")) | |
| | select(.body | test(\"Code Review by Qodo\"; \"i\")) | |
| | .id" | wc -l) | |
| # Every PR hits this branch once: this run fires at `opened`, before | |
| # Qodo has answered. That is not an error state, and the wording | |
| # must not read like one — when Qodo's review lands, its comment | |
| # re-triggers this gate and the passing run re-runs this failed one, | |
| # so the check turns green with no action from anyone (observed | |
| # 2-10 minutes on #2273/#2274/#2275, every one via re-run attempt 2). | |
| # Only a much longer red deserves a human's attention. | |
| if [ "$reviewed" -eq 0 ] && [ "$commented" -eq 0 ]; then | |
| echo "EXPECTED RED: Qodo has not posted its review yet." | |
| echo "" | |
| echo "This is the normal state right after a PR is opened or updated." | |
| echo "No action is needed: when Qodo's review lands (usually within" | |
| echo "2-10 minutes), this check re-evaluates and turns green by" | |
| echo "itself. You do not need to re-run it." | |
| echo "" | |
| echo "Still red after ~20 minutes? Then intervene:" | |
| echo " - comment '/review' on the PR to summon Qodo again" | |
| echo " - if Qodo is down, a maintainer can apply the" | |
| echo " 'skip-qodo-gate' label, which passes this check" | |
| exit 1 | |
| fi | |
| echo "review found: $reviewed review object(s), $commented review comment(s)" | |
| # Unresolved Qodo threads, paginated (a long-lived PR can exceed one | |
| # 100-thread page; a truncated read must never produce a false pass). | |
| unresolved=0 | |
| cursor="" | |
| while :; do | |
| args=( -F owner="$REPO_OWNER" -F name="$REPO_NAME" -F pr="$PR" ) | |
| [ -n "$cursor" ] && args+=( -F cursor="$cursor" ) | |
| page=$(gh api graphql "${args[@]}" \ | |
| -f query=' | |
| query($owner: String!, $name: String!, $pr: Int!, $cursor: String) { | |
| repository(owner: $owner, name: $name) { | |
| pullRequest(number: $pr) { | |
| reviewThreads(first: 100, after: $cursor) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { | |
| isResolved | |
| comments(first: 10) { nodes { author { login } } } | |
| } | |
| } | |
| } | |
| } | |
| }') | |
| # A thread is Qodo's if ANY of its first comments is by the bot — | |
| # first-comment-only attribution loses the thread when the bot's | |
| # opening comment is deleted while replies remain (and deletion | |
| # re-triggers this check, so that would be a false pass). | |
| n=$(echo "$page" | jq --arg b "$BOT" \ | |
| '[.data.repository.pullRequest.reviewThreads.nodes[] | |
| | select(.isResolved | not) | |
| | select([.comments.nodes[].author.login] | index($b))] | length') | |
| unresolved=$((unresolved + n)) | |
| more=$(echo "$page" | jq -r \ | |
| '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage') | |
| [ "$more" = "true" ] || break | |
| cursor=$(echo "$page" | jq -r \ | |
| '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor') | |
| done | |
| if [ "$unresolved" -gt 0 ]; then | |
| echo "FAIL: $unresolved unresolved Qodo review thread(s) — address" | |
| echo "or explicitly dismiss each finding in its thread, then mark" | |
| echo "it resolved. Plain resolution does not auto-retrigger this" | |
| echo "check: leave a reply in a thread (retriggers) — the passing" | |
| echo "run then sweeps this red run off the commit itself." | |
| exit 1 | |
| fi | |
| echo "PASS: Qodo review present, all its threads resolved" | |
| # Sweep stale red runs of this gate off the head commit, so the pass | |
| # above is the one the branch-protection rollup sees. Best-effort: a | |
| # failed re-run request must not turn a PASS into a FAIL. | |
| - name: Re-run this gate's earlier failed runs on this commit | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| # Empty on issue_comment, which carries no head sha — resolved from | |
| # the PR below. This is the trigger that matters most for the sweep: | |
| # it is the one that turns a clean PR's earlier red run green. | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR: ${{ github.event.pull_request.number || github.event.issue.number }} | |
| THIS_RUN: ${{ github.run_id }} | |
| run: | | |
| set -u | |
| if [ -z "$HEAD_SHA" ]; then | |
| if ! HEAD_SHA=$(gh pr view "$PR" --repo "$REPO" --json headRefOid \ | |
| --jq .headRefOid); then | |
| echo "sweep skipped: could not resolve head sha for PR $PR" | |
| exit 0 | |
| fi | |
| fi | |
| # The listing is guarded too, not just the reruns: with an unguarded | |
| # pipeline a transient list failure would be the step's exit code — | |
| # exactly the PASS-into-FAIL this step promises not to produce. | |
| if ! ids=$(gh run list --repo "$REPO" --workflow qodo-gate \ | |
| --commit "$HEAD_SHA" --json databaseId,conclusion \ | |
| --jq '.[] | select(.conclusion == "failure") | .databaseId'); then | |
| echo "sweep skipped: could not list this workflow's runs" | |
| exit 0 | |
| fi | |
| for id in $ids; do | |
| [ "$id" = "$THIS_RUN" ] && continue | |
| echo "re-running failed qodo-gate run $id" | |
| gh run rerun "$id" --repo "$REPO" --failed || true | |
| done | |
| exit 0 |