Skip to content

feat: wait for the checks another app reports after CI, before reading it - #25

Merged
MarketDataDev02 merged 6 commits into
mainfrom
feat/wait-for-listed-checks
Oct 1, 2026
Merged

MarketDataDev02 merged 6 commits into
mainfrom
feat/wait-for-listed-checks

Conversation

@MarketDataDev02

@MarketDataDev02 MarketDataDev02 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #24

The problem

A review is triggered when the Tests or Lint workflow finishes, and should_skip skips it while any check run on the head commit is not completed ("CI has not finished on the head commit"). Codecov, with its default wait_for_ci, posts codecov/patch and codecov/project only after every other check has finished: it creates and completes them within one or two seconds, 35 to 50 seconds after Tests. Nothing triggers a review when they finish.

Measured on MarketDataApp/sdk-py #159, comparing when the Tests-triggered run started reviewbot run with when Codecov's checks completed:

head run margin outcome
7759248 36462367405 -24 s reviewed, Codecov not yet reported
9f65a39 36445268644 -15 s reviewed, Codecov not yet reported
b8035b2 36436007907 -3 s reviewed, Codecov not yet reported
bbeea3c 36452710753 +2 s reviewed
7f929dc 36460028984 +8 s reviewed
21efa02 36465597062 -5 s reviewbot: skipped, CI has not finished on the head commit

So a review usually reads CI before Codecov exists, and require_ci_green never sees coverage; and when it reads during the second Codecov is in progress (21efa02: checks created 18:30:28Z, completed 18:30:30Z, read about 18:30:26Z), it skips and nothing brings it back. That head was reviewed only after a manual @marketdata-code-review re-review. Codecov reports on sdk-py, sdk-csharp and sdk-php.

The change

  • wait_for_checks (list of check-run names, default []) and wait_for_checks_seconds (default 300, from 0 to 300) in the policy, validated in config.py. A policy may not list its own check_name, which the wait never sees.
  • GitHub.check_runs(sha, exclude_check_name): the name, status and conclusion of every check run but the bot's own, across every page of the listing. ci_state reads the same pages through the same helper, so the wait and the review judge the same runs.
  • reviewbot wait-for-checks --pr N: reads the policy from the base branch; does nothing when forced, when require_ci_green is off or when nothing is listed; otherwise polls every 10 seconds, the last sleep ending at the deadline, until every listed check exists and no check run on the head is still running, from any app. It stops at once when CI is already red, by the same FAILED_CONCLUSIONS that ci_state uses (failure, timed_out, action_required), since run skips that anyway. cancelled and stale are not red for ci_state, so the review still runs on them and the wait keeps waiting for the checks it will read. At the deadline it logs whether the review reads CI without a check that never appeared, or skips because one is still running. Any error, a raw transport error included, is logged and the command exits 0.
  • The wait writes the commit it polled as the step output head, and reviewbot run gets it as REVIEWBOT_WAITED_HEAD: when the pull request has moved past that commit, the run skips, and the new head's own CI triggers the review that waits for its checks. Nothing is written when the policy asks for no wait, so those reviews run as before.
  • review.yml: a "Wait for the checks the policy lists" step (id: wait) after the gate and before the pull request checkout and "Borrow the Codex credential", so the wait holds the runner but never the shared lease. It ends in || true, so it cannot skip the review.
  • docs/setup.md: the fourth choice under "Add a policy", including that the wait needs require_ci_green, that a failing listed check skips the review, and that the names are check-run names.

A repository that does not set the key behaves as before: the step reads the pull request and the policy, and returns.

With two trigger workflows (sdk-py's Tests and Lint) both runs wait. The repositories that list Codecov borrow the Codex lease, so the second run then finds the head reviewed and skips; a repository without the lease may review twice.

Budget

The wait is at most 300 seconds and happens before the lease, so it adds to the job's wall time, not to the lease. _lease_minutes subtracts it from the lease wait's room when checks are listed and require_ci_green is on, so overhead, the wait, the lease wait and both review attempts fit the 90-minute job for every timeout_minutes from 1 to 30.

Rollout order

Merge this first. Then each repository adds, on its default branch:

wait_for_checks: ["codecov/patch", "codecov/project"]

The policy loader rejects unknown keys, so a repository that sets it before this is merged fails every review with "policy.yml is malformed". Once a repository sets it, a red Codecov check makes the review skip with "CI is red on the head commit", as require_ci_green intends; list only codecov/project to gate on the project status alone.

Tests

  • tests/test_wait_for_checks.py (24): waits until both Codecov checks exist and finish, reading the base branch's policy and the head's check runs; a listed check still running, and an unrelated Actions workflow still running, keep the wait going; each of the three red conclusions ends it at once; it gives up after exactly the cap, the last sleep ending at the deadline, and says whether a check never appeared or is still running; it names the polled head, and names none when forced, unlisted, not gating on CI, malformed or failing; no listed check, a forced review, or require_ci_green: false read nothing; a malformed policy, an API error and a dropped connection leave the decision to the review; the real client over a fake transport requests the base policy and the head's check runs; the CLI passes the pull request, repository, token and force.
  • tests/test_cli.py (3): a head that moved after the wait is not reviewed; an unmoved head goes on to should_skip; the command forwards REVIEWBOT_WAITED_HEAD.
  • tests/test_config.py (7), tests/test_github.py (3), tests/test_workflow.py (3), tests/test_checkout_command.py (32): the defaults, a repository's list, the cap, the names and the own-check refusal; the status and conclusion per check run, every page read, and a failure on a later page making ci_state red; the step order, id, token, force, || true and the pinned head reaching the review; the lease wait leaving room for the wait at every timeout_minutes, and keeping its whole room when nothing waits.

uv run pytest: 755 passed on Windows. The other 54 fail on an unmodified checkout of main on Windows too, because their fake claude and codex executables are POSIX scripts; ubuntu CI runs them. ruff check . and ruff format --check . are clean.

Also: a test that expired

tests/test_lease_report.py::test_the_command_prints_a_report_for_the_store failed this pull request's first CI run and fails on main as well: it dated its only hold 2026-09-21T10:00Z, while lease_report read its seven-day window from the clock, so from 2026-09-28T10:00Z the hold fell outside the window. lease_report now takes now, and the test passes a fixed one with its fixed dates.

…g it

A review is triggered when the Tests or Lint workflow finishes, and it skips
while any check on the head commit is still running. Codecov, with its default
wait_for_ci, reports only after every other check has finished, so its checks
land 35 to 50 seconds after Tests, and nothing triggers a review when they do.
Measured on sdk-py: the review read CI from 24 seconds before Codecov finished
to 8 seconds after, so it usually reviewed without Codecov, and on 21efa02 it
read while Codecov was in progress, skipped, and never came back.

`wait_for_checks` lists such checks by name, and `wait_for_checks_seconds`
caps the wait (default 300, at most 300). The new `reviewbot wait-for-checks`
step, which runs before the Codex lease is borrowed, returns at once while a
GitHub Actions job is still running, since its workflow triggers the review
again; otherwise it polls until every listed check exists and has finished,
or the cap passes. It never fails the job. The default lists nothing, so a
repository that does not set it behaves as before.
`lease_report` reads its window, the last seven days, from the clock. The
test fixed its only hold at 2026-09-21T10:00Z, so from 2026-09-28T10:00Z
that hold fell outside the window and the test failed on every branch,
main included. The hold now starts a day before the test runs.
@marketdata-code-review

marketdata-code-review Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

🟡 Needs changes — Code review

This change adds policy-driven polling for delayed check runs, pins the subsequent review to the polled head, and accounts for that wait in the credential-lease budget. The polling deadline does not actually bound elapsed runtime because another API read starts after the final sleep reaches the deadline.

Rating patch 4/6 (works with reservations) · proof 5/6 (reproducible) · overall 4/6

Findings

Should fix

  • Wait can exceed its configured deadline (correctness, 6f0f217d)
    The loop checks the deadline only after fetching every page of check runs, so the final sleep reaches the deadline and then starts another potentially slow or retried API request. This breaks the documented maximum and can invalidate the job-budget calculation. Check the clock before polling again after a sleep, or otherwise include request time in the enforced timeout.

    reviewbot/cli.py: runs = api.check_runs(sha, policy["check_name"]) occurs before left = deadline - clock() on every loop iteration.

Since last review

  • Resolved: 5da929df
  • 0 still open, 1 new

Reviewed e6244c9 · revision 5 · codex (gpt-5.6-sol) · claude unavailable · rules: default

The shortcut that ended the wait while any GitHub Actions check run was
unfinished assumed that job's workflow would trigger the review again. Only
the caller's `workflow_run.workflows` do, and only for a pull_request run: a
CodeQL or Copilot job, a push-event run of Tests or a reporter workflow ended
the wait, `run` skipped on the pending check, and nothing brought the review
back (review finding 55a3117e). The wait now ends once every listed check
exists and no check run is still running, from any app. With two trigger
workflows both runs wait; the repositories that list Codecov borrow the Codex
lease, so the second run then finds the head reviewed.

Also:
- a red conclusion ends the wait at once, since `run` skips it anyway;
- at the deadline the log says whether the review reads CI without a check
  that never appeared, or skips because one is still running;
- the last sleep ends at the deadline instead of overrunning it (7941a128);
- `check_runs` reads every page and returns the conclusion (48de260e);
- a transport error that is not a GitHubError no longer escapes the wait,
  and the step ends in `|| true`, so the wait can never skip the review;
- the step runs before the pull request checkout, so a push during the wait
  is fetched instead of the new head being reviewed against the old files;
- `_lease_minutes` leaves room for the wait when checks are listed and
  require_ci_green is on: from timeout_minutes 17 the worst case was 93 to
  95 minutes against the job's 90;
- a policy may not list the review's own check, which the wait never sees;
- `lease_report` takes `now`, so its test uses fixed dates again (5e548d48);
- setup.md says the wait needs require_ci_green, that a failing listed check
  skips the review, and that the names are check-run names.
@MarketDataDev02
MarketDataDev02 force-pushed the feat/wait-for-listed-checks branch from 1e28a55 to 0412566 Compare September 29, 2026 23:48
A push during the wait could move the pull request to a head whose listed
checks were never polled: the checkout fetched it, and `run` reviewed it as
soon as its own CI was green, before Codecov reported. That head's own
trigger then found it already reviewed and skipped (review finding
04a30a84).

The wait now writes the commit it polled as the step output `head`, and
`run` skips when the pull request has moved past it; the new head's own CI
triggers the review that waits for it. Nothing is written when the policy
asks for no wait, so those reviews run as before.

The red conclusions the wait stops on are now the constant `ci_state` uses,
so the two cannot drift (0c04032b). `cancelled` and `stale` stay out of it:
`ci_state` does not count them as red, so the review still runs on them, and
the wait keeps waiting for the checks that review will read.
The wait paginated the check runs but `ci_state` read only the first 100,
so on a commit with more runs a failure on a later page stopped the wait as
red while `run` saw green and reviewed anyway (review finding 5da929df).
Both now read the listing through one helper that follows every page.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A check another app reports after CI (Codecov) is missed by the gate, or stalls the review

1 participant