Skip to content

ci: hold a PR while automatic-review findings are unanswered (#4299 option B) - #4575

Merged
rbuergi merged 3 commits into
mainfrom
feat/4299-review-answered-check
Sep 17, 2026
Merged

rbuergi merged 3 commits into
mainfrom
feat/4299-review-answered-check

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Part of #4299 — builds option B (the maintainer's decision of 2026-09-17). It lands non-required; making it required is the maintainer's step after observing it.

What it does

A new workflow, Review answered, publishes the check Automatic review answered on every pull request and every merge-queue entry. It is RED when any of these hold, and it never skips:

  1. The automatic review has not landed. A review from the reviewer account counts only if its body is recognisably a review (Pull request overview). A refusal (Copilot was unable to review …, **Files reviewed:** 0/…) or an unrecognised body is not a review.
  2. A thread the reviewer opened has no reply from a person (user.type == "User", following in_reply_to_id to the root).
  3. The check cannot read its input completely — a failed read, or a comment listing shorter than the review_comments count read before it.

Events: pull_request (opened, synchronize, reopened, ready_for_review, labeled, unlabeled), pull_request_review (submitted, dismissed), pull_request_review_comment (created, deleted), merge_group (the PR number comes from gh-readonly-queue/main/pr-<N>-<sha>). REST only. No job-level if:, no continue-on-error, no path filter; timeout-minutes: 5; one evaluation per PR at a time, never cancelled.

  • Predicate + self-test: .github/scripts/check-review-answered.py. The self-test also runs in dotnet-test.yml's CI-shell lane.
  • Design doc: Doc/Architecture/ReviewFindingsAnswered, plus a line in the topic map.
  • What's New entry (Category: Feature).
  • /pullrequest skill step 4: reply to every thread; never apply the waiver label yourself.

The waiver (design)

  • Label review-waived (created on the repo for this PR). Whether it is present comes from the PR. Who applied it comes from REST issues/{n}/events, using the latest labeled/unlabeled event for that label.
  • It is honoured only if that account is a person whose role_name from collaborators/{login}/permission is admin or maintain. A label applied by a writer or a bot, or one no event can be tied to, is refused, and the refusal names the account.
  • It releases condition 1 only. Threads the reviewer did open still need replies. It is never automatic, and the log names who waived and when.
  • Limitation, stated in the doc: agent sessions run under the maintainer's account, so the check cannot tell a maintainer's waiver from an agent's. The timeline event is the audit, and the skill forbids agents from applying the label.
  • The waiver's permission read runs on every run for the PR author, so a token that cannot verify a waiver shows up on an ordinary PR, not in an emergency.

Measured before writing

Controls (the real REST adapter, --as-of)

PR As of Verdict
#4310 merge 2026-09-14T14:04:21Z RED: 14 of 14 unanswered
#4366 merge RED: 14 of 14 unanswered
#4343 merge RED: 4 of 4 unanswered (replies came 13 min later)
#4343 now GREEN: 4 of 4 answered
#4556 merge 2026-09-17T05:59:08Z GREEN: 5 of 5 answered before merge
#4568 now GREEN: review landed, no threads
#645 now RED: the review is the quota refusal

Backtest over 60 merges (2026-09-16T08:48Z → 09-17T07:41Z), each as of its merged_at: 32 of 60 would have been RED, all because of unanswered threads (126 findings, 86 unanswered at merge).

Negative controls, watched failing. Each self-test case names the exact reasons it must be red for. I broke the predicate twelve ways, one at a time: a bot reply counted as an answer; a refusal counted as a review; an unrecognised body counted as a review; any bot treated as the reviewer; the completeness check removed; a waiver releasing threads; a writer allowed to waive; a bot allowed to waive; --as-of ignored for comments; only direct replies followed; a PENDING review counted; a short sha accepted as a queue ref. Each mutation turned the self-test red (13 of 13, including "wait on any red" after the wait was added).

Local gates: check-workflow-timeouts (0 violations), check-workflow-yaml-keys, check-pr-secret-preflight, check-workflow-permission-pairing, check-workflow-shell (0 live findings). MeshWeaver.Documentation.Test was built -c Release -warnaserror (0 warnings) and run: 508 passed, 0 failed, including link integrity, the topic map and What's New integrity.

Measured on this PR — and it changed the design

  • A pull_request run publishes its check-run on the PR head sha (c496a0bffb), beside Consolidate test results; the first evaluation was RED for the right reason ("the automatic review has not landed — no review by the automatic reviewer"), and GITHUB_TOKEN read collaborators/rbuergi/permission → admin, so the waiver path is readable with the job's own token.
  • 🚨 The reviewer's own event cannot start a run here. Its pull_request_review event arrived and GitHub created run 35197843933 — conclusion action_required, zero jobs, triggering actor Copilot, because actions/permissions/fork-pr-contributor-approval is first_time_contributors. Both pull_request_review_comment runs its comments raised were action_required too. So a PR whose review raises no findings would have kept the red from its opened evaluation until somebody pushed again.
  • Fix in bf37e92a09: --wait-for-review 15 — while the only thing missing is the review, the step re-reads every 30 s for up to 15 minutes, then answers RED (job cap 20 min). Nothing else is waited for; an unanswered thread needs a person and an incomplete listing needs another read. The reviewer took 4m38s on this PR. A person's reply is not gated, so the reply path re-runs in seconds.
  • The cheaper fix is a repository setting (don't require approval for Copilot-triggered runs), which is now Rollout step 1 in the doc — with it, --wait-for-review goes to 0.
  • Copilot's two findings on this PR were both valid and both answered in bf37e92a09: the concurrency comment and the doc paragraph claimed per-PR serialisation for merge_group, which the key does not do (the queue head ref carries the entry sha). Both now state the real granularity — per PR for pull-request events, per queue entry for merge_group — and why that is the right one.

To make it required (not done here)

Add the context Automatic review answered to ruleset 2128472 next to Consolidate test results. First, per the doc's Rollout section:

  1. Watch it on live PRs.
  2. Teach merge-queue-steward.py about this check. Today it reads only failed MeshWeaver Build and Test merge_group runs, so an ejection caused by this check would be rejected as unclassifiable, or matched to an older failed build of the same PR.
  3. Accept the cost: 32 of the last 60 merges would have waited for replies.

Pairs-with: none — adds a workflow, a script and docs; removes no public surface.

🤖 Generated with Claude Code

…ption B)

New workflow `Review answered` publishes the check `Automatic review answered`
on every pull request and merge-queue entry. It is RED while the automatic
review has not landed (a quota refusal or unrecognised body is not a review),
while any thread the reviewer opened has no reply from a person, or while the
check cannot read its input completely. It never skips. A maintainer waiver
(`review-waived`, attributed via REST issue events, admin/maintain only)
releases only the missing-review condition.

Measured: the reviewer posts reviews as copilot-pull-request-reviewer[bot]
and comments as Copilot, one account id (175728472); its July quota refusal
was posted as a review. Controls: RED on #4310/#4366/#4343 at merge, GREEN
on #4556 at merge and #4343 now; 12 of 12 predicate mutations caught by the
self-test. Lands non-required.

Part of #4299.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 17, 2026 08:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

New documentation/comments assert per-PR concurrency semantics that don’t match the merge-queue concurrency key as implemented, which could mislead rollout/ops expectations.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds an “observed” (non-required) GitHub Actions check that turns red until the repository’s automatic code review has landed and every inline finding thread from that reviewer has a human reply, plus documentation and guardrails to explain and self-test the behavior.

Changes:

  • Introduces the Review answered workflow publishing the Automatic review answered check for PR and merge-queue events.
  • Adds the predicate + self-test script (.github/scripts/check-review-answered.py) and wires its self-test into dotnet-test.yml.
  • Documents the design/rollout and updates the PR workflow skill + What’s New entry.
File summaries
File Description
.github/workflows/review-answered.yml New workflow that runs the check across PR + merge queue events and publishes the Automatic review answered context.
.github/scripts/check-review-answered.py Predicate implementation and fixture-based self-test for “review landed + all threads answered + reads complete”.
.github/workflows/dotnet-test.yml Adds a CI-shell self-test invocation to ensure predicate breakage fails on the PR that introduced it.
src/MeshWeaver.Documentation/Data/Architecture/ReviewFindingsAnswered.md Design doc describing the motivation, rule, waiver model, controls, and rollout steps.
src/MeshWeaver.Documentation/Data/Architecture.md Adds the new design doc to the Architecture topic map.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-17-a-pull-request-now-shows-when-a-review-finding-is-unanswered.md What’s New entry describing the new check and its observed-only rollout.
.claude/skills/pullrequest/SKILL.md Updates the PR flow instructions to explicitly reply to all automatic-review threads and avoid self-applying the waiver label.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/review-answered.yml Outdated
Comment on lines +57 to +61
# One evaluation per pull request at a time, NEVER cancelled mid-read: a review lands as one
# `submitted` event plus one `created` event per inline comment, and the evaluation that runs LAST
# must be the one that read LAST. GitHub keeps one pending run per group, so a burst of fourteen
# comments collapses to the running evaluation plus the newest pending one.
concurrency:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it was the comment that was wrong rather than the key. Fixed in bf37e92.

The group is per pull request for the pull-request events and per queue entry for merge_group — the queue head ref carries the entry's sha — and that is the granularity that matters: what must never race is two evaluations publishing the same check-run name on the same commit, and two queue entries of one pull request are two different commits with two separate check-runs. So a re-queue evaluating alongside an older entry is by design, and the comment now says so. It also now says why nothing is cancelled: cancelling a merge_group run ejects the entry rather than retrying it.

Comment on lines +118 to +121
Evaluations of one pull request share a concurrency group and are never cancelled mid-read. A review
arrives as one `submitted` event and one `created` event per inline comment; GitHub keeps one running
and one pending run per group, so a burst of fourteen collapses to two evaluations, and the one that
runs last read last.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bf37e92 — the paragraph now states the real behaviour: serialised per pull request for pull-request events, per queue entry for merge_group (the queue head ref carries the entry's sha), with the reason that two entries of one pull request are two commits carrying two separate check-runs, so there is nothing to serialise between them.

The same commit adds the measurement your review made visible: the reviewer's own pull_request_review event created run 35197843933 with conclusion action_required and zero jobs (triggering actor Copilot, repository policy first_time_contributors), so that event cannot evaluate the check at all — hence the bounded --wait-for-review 15, and the approval policy as Rollout step 1.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

264 tests  ±0   264 ✅ ±0   2m 43s ⏱️ +7s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

533 tests   - 1 005   533 ✅  - 1 005   1m 8s ⏱️ - 1m 57s
  1 suites  -     1     0 💤 ±    0 
  1 files    -     1     0 ❌ ±    0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files   -   1      3 suites   - 1   5m 51s ⏱️ +57s
2 285 tests +268  2 285 ✅ +460  0 💤  - 192  0 ❌ ±0 
2 286 runs  +268  2 286 ✅ +460  0 💤  - 192  0 ❌ ±0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ±  0      6 suites  ±0   11m 26s ⏱️ + 6m 28s
3 172 tests +913  2 980 ✅ +721  192 💤 +192  0 ❌ ±0 
3 176 runs  +913  2 984 ✅ +721  192 💤 +192  0 ❌ ±0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 491 tests  +522   1 489 ✅ +520   7m 41s ⏱️ + 1m 47s
    4 suites +  2       2 💤 +  2 
    4 files   +  2       0 ❌ ±  0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

425 tests   - 785   425 ✅  - 783   57s ⏱️ - 5m 43s
  2 suites  -   1     0 💤  -   2 
  2 files    -   1     0 ❌ ±  0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   17 files   -  1     17 suites   - 1   29m 48s ⏱️ + 1m 37s
8 170 tests  - 87  7 976 ✅  - 87  194 💤 ±0  0 ❌ ±0 
8 175 runs   - 87  7 981 ✅  - 87  194 💤 ±0  0 ❌ ±0 

Results for commit 4f31146. ± Comparison against base commit 3c88439.

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits September 17, 2026 13:08
…ait for it

Measured on this pull request (#4575, run 35197843933): the reviewer's
`pull_request_review` event arrives and GitHub creates a run for it with
conclusion `action_required` and ZERO jobs — the triggering actor is `Copilot`
and this repository requires approval for a first-time contributor's runs
(`actions/permissions/fork-pr-contributor-approval` = first_time_contributors).
Its two `pull_request_review_comment` runs were `action_required` too.

So the event that says "the review has landed" cannot evaluate anything, and a
pull request whose review raises no findings would keep the red from its
`opened` evaluation until somebody pushed again. `--wait-for-review 15` closes
that: while the ONLY thing missing is the review, the step re-reads every 30 s
for up to 15 minutes, then answers RED (job cap 20 min). Nothing else is ever
waited for — an unanswered thread needs a person, an incomplete listing needs
another read. A person's reply is not gated and re-runs the check in seconds.
The cheaper fix is the approval policy, and it is now Rollout step 1.

Also addresses Copilot's review of this PR: the concurrency comment and the
doc paragraph claimed per-pull-request serialisation for `merge_group` too,
which the key (the queue head ref carries the entry sha) does not do. Both now
state what it is — per pull request for pull-request events, per queue entry
for `merge_group` — and why that is the right granularity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The order that matters, with the run that did the work: the opened evaluation
red for the right reason; the reviewer's own event action_required with zero
jobs; the push and the two replies both evaluating and reading 2 of 2 answered;
three sibling runs cancelled by the concurrency group. Plus the two facts a
reader would otherwise have to rediscover — the review that counts was
submitted on the previous head, and the 15-minute wait was exercised against
#645 (three polls, then RED naming the quota refusal).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants