Skip to content

Add review checklists workflow - #899

Open
LittleHuba wants to merge 30 commits into
mainfrom
add-review-checklists-workflow
Open

Add review checklists workflow#899
LittleHuba wants to merge 30 commits into
mainfrom
add-review-checklists-workflow

Conversation

@LittleHuba

Copy link
Copy Markdown
Contributor

Adds a workflow to require acknowledgement of checklists by reviewers.

Currently no checklist is enforced.


from __future__ import annotations

import os
import pytest
import yaml

import helpers
import pytest
import yaml

import helpers
candidate = r.Rlocation(f"_main/{config_relpath}")
if candidate and os.path.isfile(candidate):
return candidate
except (ImportError, Exception):
monkeypatch.setenv("GITHUB_REPOSITORY", "org/repo")
monkeypatch.setenv("PR_NUMBER", "42")
gh = MagicMock()
repo, pr = get_repo_and_pr(gh)
monkeypatch.setenv("GITHUB_REPOSITORY", "org/repo")
monkeypatch.setenv("PR_NUMBER", "42")
gh = MagicMock()
repo, pr = get_repo_and_pr(gh)
@LittleHuba
LittleHuba force-pushed the add-review-checklists-workflow branch from e6fb9d8 to 6f2b4c9 Compare August 11, 2026 12:39
@LittleHuba
LittleHuba marked this pull request as ready for review August 11, 2026 13:27

@castler castler 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.

We need to extend the CI design also to explain why and for what we have these workflows.

Due to the size, let's discuss f2f which of the findings we tackle, tackle now or in a follow up.

Comment thread BUILD Outdated
name = "copyright",
srcs = [
".github",
"actions",

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.

Why this change?

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, code was moved and this was not updated.


concurrency:
group: >-
review-checklists-apply-${{ github.event.workflow_run.head_sha || github.event.workflow_run.id }}

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.

I think we are missing here a "cancel-in-progress" option? Or do we say this is so short running, that it does not make sense?

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.

Adding the cancel-in-progress does not hurt. So I added it. But in general this is quite fast (given GitHub schedules the runs fast).

GITHUB_TOKEN: ${{ inputs.github-token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr-number }}
run: python3 "$GITHUB_ACTION_PATH/scripts/post_checklists.py" --config-path "${{ inputs.config-path }}"

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.

We should make a comment here that we we do not on purpose start this with Bazel, and what was our reasoning for it.

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.

Added comment.

@@ -0,0 +1,109 @@
# *******************************************************************************

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.

Wondering if we do not want to place it also under actions/00_infrastructure or similar?

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.

Not quite sure about that. Separating the code over the repo is not perfect, since this file contains logic that is closely related. GitHub actually has no convention where actions must be placed. So keeping it here is perfectly fine.

ruleset to require the commit status context **`review-checklists`**
(the one this action sets via `set_commit_status`). Requiring the
workflow job itself will not gate merges correctly.
5. **Require at least one approving review** in branch protection. The

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.

Are we sure this is possible with Otterdog? As per https://otterdog.readthedocs.io/en/latest/reference/organization/repository/status-check/ I do not believe so?

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.

Commit status checks are viable with any: prefix. I extended the documentation. Of course this is subject to trials.

return is_in_queue

print("GraphQL merge-queue lookup returned no boolean state")
return False

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.

Should we not throw here and rather catch that. Are we sure that IF we would be in the merge queue but the lookup fails for what ever reason (e.g. API error on the graphQL call) that FALSE would not bring us problems?

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.

This branch is only used to provide the warning comment if a PR is modified after entering the merge queue. The worst happening here would be that the comment is wrongly emitted.
Still this omission would not modify the evidence.


repo, pr = get_repo_and_pr(gh)

if is_pr_in_merge_queue(pr):

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.

This I do not get, why do we not execute this in the if statement above?

if event_name == "merge_group":

then the PR needs to be in the merge_queue, no?

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.

We want to add this warning comment when the PR is modified after it enters the merge queue. This means we react to events like pull_request_comment, pull_request_review.
Hence, we do not enter the first if condition at all but must still verify whether the PR is in the merge queue or not.

from typing import Any

from helpers import (
_collect_acknowledgement_details,

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.

I personally think its weird to "import" private functions.

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.

return "success", "No checklists applicable"

existing = find_existing_checklist_comments(pr)
relevant_ids = [cl["id"] for cl in relevant if cl["id"] in existing]

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.

As per my current understanding, this would filter out only checklists where a comment has been made. Thus, if for a checklist no comment has been made (yet), we would skip it here.

Thus we would set success later, if for another checklist is succesful. Do we not rather have to "pend" on that all comments have been posted?

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

Comment on lines +93 to +105
for comment in pr.get_review_comments():
reply_to = getattr(comment, "in_reply_to_id", None)
if reply_to is None or reply_to not in cl_comment_ids:
continue

cid = cl_comment_ids[reply_to] # type: ignore[index]
body = (comment.body or "").strip()
user = comment.user.login

if body.upper() == OK_KEYWORD:
acks[cid].add(user)

return acks

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.

This functionality is duplicated.

See for example: def _find_ok_comments_for_checklist

I think we can extract it into an helper. (I am aware that we store different information, but I think we can make a super set of information returned?

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

LittleHuba added a commit that referenced this pull request Aug 13, 2026
The composite action was later moved from actions/review_checklists to
tools/review-checklists, but the root BUILD file's copyright_checker
srcs list still referenced the now-nonexistent "actions" directory
(a no-op glob) and never gained a "tools" entry, so the new
tools/review-checklists sources were not covered by the copyright
check at all.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Multiple non-merge_group events (e.g. several review comments posted in
quick succession) can each trigger a completed 'Review Checklists
(Trigger)' run for the same head SHA, spawning overlapping apply-stage
runs in the same concurrency group. Let a newer run cancel an older,
now-superseded one instead of leaving it queued/running: the job only
recomputes idempotent PR state (comments/statuses) from scratch each
time, so cancelling a stale run is safe and matches this repo's existing
convention of setting cancel-in-progress explicitly.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Otterdog's status-check reference distinguishes plain commit statuses
(which need the 'any:' prefix) from GitHub Actions workflow-job or
GitHub App statuses. The 'review-checklists' status this action sets
via create_status is a plain commit status, so an Otterdog-managed
branch protection rule/ruleset must reference it as
'any:review-checklists' — the bare name would not be recognized.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
helpers.find_ok_replies (unused in production), helpers.collect_
acknowledgement_details, check_acknowledgements._collect_ok_
acknowledgements, and dismiss_and_invalidate._find_ok_comments_for_
checklist each independently re-implemented the same scan for OK-reply
comments to a checklist finding. Extract a single shared
helpers.find_ok_replies_for_checklists(pr, existing_comments,
checklist_ids) that returns the 'superset' (full comment objects, in
one pass over pr.get_review_comments()) and have each caller project it
down to the narrower shape it needs. This also removes an O(n*k) re-scan
in dismiss_and_invalidate.handle_synchronize (one scan per affected
checklist) in favor of a single O(n) pass.

Also renamed the now-shared collect_acknowledgement_details parameter
'relevant_ids' to 'checklist_ids' for clarity, since it isn't obvious
from the name alone what the ids are relevant to.

Addresses review comments:
#899 (comment)
#899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
'_main' is the current Bzlmod-internal runfiles-directory name for the
root module.
Rlocation() accepts the module name directly and resolves it through the
runfiles repo mapping to whichever directory the root module is actually
materialized under. Use 'score_communication' (this repo's declared
module name in MODULE.bazel) instead — this is the portable, correct
API usage, and keeps working even if Bazel's internal main-repo
directory naming convention changes, instead of depending on that
implementation detail directly.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
get_reviews() returns every review a user has ever submitted on the PR
(in submission order), not just their latest state, so a user can
approve and later request changes (or have an approval dismissed) in a
subsequent review. The approvers.discard() call is what removes such a
since-superseded approval recorded earlier in the same loop — it is not
a no-op, even though 'approvers' starts as an empty set.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Fail-closed to False on any lookup error is intentional here: the only
callers use the result to decide whether to (re-)post an advisory
merge-queue notice on the PR, so a false negative merely delays that
notice — it never affects merge gating. The actual gating decision for
merge_group events is made independently in check_acknowledgements.py,
which resolves and validates the underlying PR itself and fails the
commit status (rather than defaulting to success) if that resolution
or validation fails.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
_validate_checklist_evidence (used for merge_group evidence validation)
and main()'s live-PR flow both derived 'relevant_ids' as only the
relevant checklists that already have a posted finding comment, and
then treated an empty relevant_ids as the only 'not all posted yet'
case. If some but not all relevant checklists had been posted (e.g. a
partial post_checklists run, or a checklist added to the config after
others were already posted), the unposted ones were silently dropped
from consideration entirely, and the status could go to 'success' once
the posted subset was fully acknowledged — without a checklist that was
never even posted ever being acknowledged. Fixed to require every
relevant checklist to have a posted comment before proceeding past
'pending'.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
… handling

Explain that this is_pr_in_merge_queue() check in main()'s regular
(non-merge_group) flow is intentionally distinct from the merge_group
handling above it: it covers events fired for a PR that is still
enqueued but is not itself a merge_group event (e.g. a review comment
posted while the PR is queued), so the advisory merge-queue notice can
be (re-)posted for it.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Rename 'cl' -> 'checklist' and 'cid' -> 'checklist_id' (and
'cl_copy' -> 'checklist_copy', 'comment_id_to_cid' ->
'comment_id_to_checklist_id') throughout helpers.py,
check_acknowledgements.py, dismiss_and_invalidate.py,
post_checklists.py, and helpers_test.py. These abbreviations made call
sites and comprehensions harder to scan at a glance, especially where
'cl' (checklist) could be misread as a different kind of list/class
abbreviation. Wrapped the handful of lines that exceeded 88 columns
after the rename.

Addresses review comments (previously deferred by the reviewer to a
future PR):
#899 (comment)
#899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Following up on discussion_r3772590463 ('relevant_ids is a bad naming
... what is it relevant for?'): the same ambiguity existed one level up
in the variables holding the matched checklist dicts themselves.
'relevant' and 'affected' read as bare adjectives with no object, which
invited exactly this kind of question at every read site. Rename to
'relevant_checklists' (helpers.match_checklists/build_evidence_block,
check_acknowledgements.py, post_checklists.py) and
'affected_checklists' (dismiss_and_invalidate.py) to make explicit what
they are relevant/affected checklists of. Wrapped two comprehensions
that exceeded 88 columns after the rename.

No behavior change; verified with bazel test //tools/review-checklists/...

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
The composite action was later moved from actions/review_checklists to
tools/review-checklists, but the root BUILD file's copyright_checker
srcs list still referenced the now-nonexistent "actions" directory
(a no-op glob) and never gained a "tools" entry, so the new
tools/review-checklists sources were not covered by the copyright
check at all.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Multiple non-merge_group events (e.g. several review comments posted in
quick succession) can each trigger a completed 'Review Checklists
(Trigger)' run for the same head SHA, spawning overlapping apply-stage
runs in the same concurrency group. Let a newer run cancel an older,
now-superseded one instead of leaving it queued/running: the job only
recomputes idempotent PR state (comments/statuses) from scratch each
time, so cancelling a stale run is safe and matches this repo's existing
convention of setting cancel-in-progress explicitly.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Otterdog's status-check reference distinguishes plain commit statuses
(which need the 'any:' prefix) from GitHub Actions workflow-job or
GitHub App statuses. The 'review-checklists' status this action sets
via create_status is a plain commit status, so an Otterdog-managed
branch protection rule/ruleset must reference it as
'any:review-checklists' — the bare name would not be recognized.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
helpers.find_ok_replies (unused in production), helpers.collect_
acknowledgement_details, check_acknowledgements._collect_ok_
acknowledgements, and dismiss_and_invalidate._find_ok_comments_for_
checklist each independently re-implemented the same scan for OK-reply
comments to a checklist finding. Extract a single shared
helpers.find_ok_replies_for_checklists(pr, existing_comments,
checklist_ids) that returns the 'superset' (full comment objects, in
one pass over pr.get_review_comments()) and have each caller project it
down to the narrower shape it needs. This also removes an O(n*k) re-scan
in dismiss_and_invalidate.handle_synchronize (one scan per affected
checklist) in favor of a single O(n) pass.

Also renamed the now-shared collect_acknowledgement_details parameter
'relevant_ids' to 'checklist_ids' for clarity, since it isn't obvious
from the name alone what the ids are relevant to.

Addresses review comments:
#899 (comment)
#899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
'_main' is the current Bzlmod-internal runfiles-directory name for the
root module.
Rlocation() accepts the module name directly and resolves it through the
runfiles repo mapping to whichever directory the root module is actually
materialized under. Use 'score_communication' (this repo's declared
module name in MODULE.bazel) instead — this is the portable, correct
API usage, and keeps working even if Bazel's internal main-repo
directory naming convention changes, instead of depending on that
implementation detail directly.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Rename 'cl' -> 'checklist' and 'cid' -> 'checklist_id' (and
'cl_copy' -> 'checklist_copy', 'comment_id_to_cid' ->
'comment_id_to_checklist_id') throughout helpers.py,
check_acknowledgements.py, dismiss_and_invalidate.py,
post_checklists.py, and helpers_test.py. These abbreviations made call
sites and comprehensions harder to scan at a glance, especially where
'cl' (checklist) could be misread as a different kind of list/class
abbreviation. Wrapped the handful of lines that exceeded 88 columns
after the rename.

Addresses review comments (previously deferred by the reviewer to a
future PR):
#899 (comment)
#899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba added a commit that referenced this pull request Aug 13, 2026
Following up on discussion_r3772590463 ('relevant_ids is a bad naming
... what is it relevant for?'): the same ambiguity existed one level up
in the variables holding the matched checklist dicts themselves.
'relevant' and 'affected' read as bare adjectives with no object, which
invited exactly this kind of question at every read site. Rename to
'relevant_checklists' (helpers.match_checklists/build_evidence_block,
check_acknowledgements.py, post_checklists.py) and
'affected_checklists' (dismiss_and_invalidate.py) to make explicit what
they are relevant/affected checklists of. Wrapped two comprehensions
that exceeded 88 columns after the rename.

No behavior change; verified with bazel test //tools/review-checklists/...

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LittleHuba and others added 3 commits August 13, 2026 18:03
…tion race

The 'Resolve PR number' step previously matched open PRs by
head.sha == workflow_run.head_sha. If the PR receives another push
while the apply run is queued or executing, no open PR will still have
that exact SHA as its head by the time this step runs, so pr_number
resolves empty and the whole apply run silently no-ops for that
(now-superseded) trigger.

Match on the head branch reference instead
(workflow_run.head_repository.owner.login + head_branch via
'pulls?head=owner:branch'), which stays valid across such races as
long as the PR is still open — the exact commit no longer matters for
identifying which PR this run belongs to.

Downstream steps already re-fetch the PR object and use its live
pr.head.sha for everything except the (SHA-independent) merge_group
status-setting path, so no script changes were needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Disable the example checklist to not spam users when
the infrastructure is merged.
Actual production checklists to be merged later.
Relocates the composite action and its scripts from
actions/review_checklists/ to tools/review-checklists/, updating all
path references: MODULE.bazel's pip.parse requirements_lock, the Bazel
BUILD files' visibility/label, requirements.txt(.in)'s embedded paths,
and the apply workflow's three 'uses: ./...' steps.

Adds tools/review-checklists/README.md documenting:
- What the action/workflows do and the acknowledgement model (threaded
  'OK' replies to file-level review comment findings).
- Why the logic is split into an unprivileged trigger workflow and a
  privileged workflow_run-triggered apply workflow (pwn-request
  prevention), and that the apply workflow always reads
  .github/review_checklists.yml from the base branch, never the PR
  branch.
- A step-by-step list of what a consuming repository must additionally
  configure: vendoring/referencing the action, creating the checklist
  config, adjusting the base-branch filter, requiring the
  'review-checklists' commit status (not the workflow job) plus an
  approving-review rule in branch protection, the merge-queue notice's
  assumption that the queue's merge method is a real merge commit, the
  'reply OK in-thread' acknowledgement UX, the diff-position-1 anchor
  constraint on checklist include patterns, runner/network
  requirements, and keeping (or updating) the trigger workflow's
  filename since it's looked up by name for run-history diffing.
- The checklist YAML schema and the composite action's inputs.

All 5 existing Bazel tests (including the requirements lock check)
pass unchanged at the new location.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@LittleHuba
LittleHuba force-pushed the add-review-checklists-workflow branch from 1332063 to 997ef3e Compare August 13, 2026 16:03
LittleHuba and others added 22 commits August 13, 2026 18:03
Switch from create_review(comments=[{path, position: 1, body}]) to the
single-comment create_review_comment(..., subject_type="file") API.
File-level review comments are anchored to a file as a whole rather
than a specific diff position, so checklist include patterns can now
match binary files (images, archives, etc.) without failing to post.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This makes glob builds more reliable on different platforms.
Previously, the merge_group handler in check_acknowledgements.py just
set the commit status to success unconditionally ("checklists assumed
OK"), trusting that the required status check had already validated
the evidence before the PR entered the queue. This left a window where
stale evidence (e.g. dismissed reviews, invalidated acks after the
initial check went green) would not be caught at merge-queue time.

Now the merge_group handler resolves the originating PR (parsed from
the merge-queue's synthetic gh-readonly-queue/<base>/pr-<n>-<sha> head
ref) and re-runs the same acknowledgement validation against its
current live state (changed files, checklist findings, OK replies,
approving reviewers), setting success/pending/failure on the merge
commit SHA accordingly. It performs no PR writes (no evidence/notice
updates), only a read-only re-validation.

Refactored the shared decision logic (approvers vs. acks -> state)
into _acknowledgement_status() so both the live-PR flow and the
merge_group validation path use identical logic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the apply stage's own PR-number resolution (matching the
workflow_run head branch, or a fragile commit-SHA search for
pull_request_review*/fork events) with a value read directly from the
trusted event payload in the trigger stage and forwarded as a build
artifact.

This fixes a real bug: for pull_request_review and
pull_request_review_comment events, github.event.workflow_run's
head_repository/head_branch always reflect the base repository, never
the fork the review was actually submitted on, so the previous
owner:branch lookup could never resolve a PR number for fork PRs on
those events. The PR number is not sensitive/attacker-steerable data
(worst case a wrong number just points at the wrong already-public,
same-repo PR), so carrying it across the trust boundary this way does
not reintroduce the pwn-request risk the two-stage split exists to
avoid.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GITHUB_EVENT_NAME is a GitHub Actions reserved variable name: setting it
via a step's env: block is silently ignored for the subprocess, which
always sees the runner's real ambient value instead (here,
"workflow_run", since that's what actually triggers
review_checklists_apply.yml). This meant check_acknowledgements.py's
`if event_name == "merge_group"` branch could never be reached when
invoked from the apply workflow — confirmed live: for merge_group runs
the script instead executed the normal PR-head-sha flow, posting the
merge-queue notice and setting the commit status on pr.head.sha instead
of the merge queue's synthetic HEAD_SHA, and never actually re-running
_validate_checklist_evidence().

Fix: use a non-reserved variable name (CHECKLISTS_EVENT_NAME) to pass
the original triggering event name into the script, so a step's own
env: can actually override it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…yle)

Python's fnmatch treats '*' as matching across '/' with no distinction
between recursive and single-segment wildcards, so patterns that look
like standard glob syntax behave unintuitively:
  - "**/*.md" requires a literal '/' in the matched path, so it silently
    fails to match root-level files (discovered while live-testing a
    multi-checklist merge-queue scenario).
  - There was no way to anchor a pattern to the repo root at all.

Switch include/exclude matching to the pathspec library's gitwildmatch
style, i.e. the same pattern semantics as .gitignore:
  - Unanchored patterns ("*.md") match at any depth.
  - A leading '/' anchors a pattern to the repo root ("/*.md").
  - "**" explicitly matches zero or more path segments, and correctly
    matches at the root too ("**/BUILD" now matches a root-level BUILD
    file, unlike the previous fnmatch-based implementation).

Added pathspec as a new dependency (requirements.txt.in/BUILD), and
added tests covering root-anchored and unanchored matching. Documented
the new gitignore-style syntax in the README. All 5/5 existing tests
continue to pass unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Public visibility is actually not required for these tools.
Recude it to necessary levels.
The composite action was later moved from actions/review_checklists to
tools/review-checklists, but the root BUILD file's copyright_checker
srcs list still referenced the now-nonexistent "actions" directory
(a no-op glob) and never gained a "tools" entry, so the new
tools/review-checklists sources were not covered by the copyright
check at all.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Multiple non-merge_group events (e.g. several review comments posted in
quick succession) can each trigger a completed 'Review Checklists
(Trigger)' run for the same head SHA, spawning overlapping apply-stage
runs in the same concurrency group. Let a newer run cancel an older,
now-superseded one instead of leaving it queued/running: the job only
recomputes idempotent PR state (comments/statuses) from scratch each
time, so cancelling a stale run is safe and matches this repo's existing
convention of setting cancel-in-progress explicitly.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Otterdog's status-check reference distinguishes plain commit statuses
(which need the 'any:' prefix) from GitHub Actions workflow-job or
GitHub App statuses. The 'review-checklists' status this action sets
via create_status is a plain commit status, so an Otterdog-managed
branch protection rule/ruleset must reference it as
'any:review-checklists' — the bare name would not be recognized.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
helpers.find_ok_replies (unused in production), helpers.collect_
acknowledgement_details, check_acknowledgements._collect_ok_
acknowledgements, and dismiss_and_invalidate._find_ok_comments_for_
checklist each independently re-implemented the same scan for OK-reply
comments to a checklist finding. Extract a single shared
helpers.find_ok_replies_for_checklists(pr, existing_comments,
checklist_ids) that returns the 'superset' (full comment objects, in
one pass over pr.get_review_comments()) and have each caller project it
down to the narrower shape it needs. This also removes an O(n*k) re-scan
in dismiss_and_invalidate.handle_synchronize (one scan per affected
checklist) in favor of a single O(n) pass.

Also renamed the now-shared collect_acknowledgement_details parameter
'relevant_ids' to 'checklist_ids' for clarity, since it isn't obvious
from the name alone what the ids are relevant to.

Addresses review comments:
#899 (comment)
#899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'_main' is the current Bzlmod-internal runfiles-directory name for the
root module.
Rlocation() accepts the module name directly and resolves it through the
runfiles repo mapping to whichever directory the root module is actually
materialized under. Use 'score_communication' (this repo's declared
module name in MODULE.bazel) instead — this is the portable, correct
API usage, and keeps working even if Bazel's internal main-repo
directory naming convention changes, instead of depending on that
implementation detail directly.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
get_reviews() returns every review a user has ever submitted on the PR
(in submission order), not just their latest state, so a user can
approve and later request changes (or have an approval dismissed) in a
subsequent review. The approvers.discard() call is what removes such a
since-superseded approval recorded earlier in the same loop — it is not
a no-op, even though 'approvers' starts as an empty set.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fail-closed to False on any lookup error is intentional here: the only
callers use the result to decide whether to (re-)post an advisory
merge-queue notice on the PR, so a false negative merely delays that
notice — it never affects merge gating. The actual gating decision for
merge_group events is made independently in check_acknowledgements.py,
which resolves and validates the underlying PR itself and fails the
commit status (rather than defaulting to success) if that resolution
or validation fails.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
_validate_checklist_evidence (used for merge_group evidence validation)
and main()'s live-PR flow both derived 'relevant_ids' as only the
relevant checklists that already have a posted finding comment, and
then treated an empty relevant_ids as the only 'not all posted yet'
case. If some but not all relevant checklists had been posted (e.g. a
partial post_checklists run, or a checklist added to the config after
others were already posted), the unposted ones were silently dropped
from consideration entirely, and the status could go to 'success' once
the posted subset was fully acknowledged — without a checklist that was
never even posted ever being acknowledged. Fixed to require every
relevant checklist to have a posted comment before proceeding past
'pending'.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… handling

Explain that this is_pr_in_merge_queue() check in main()'s regular
(non-merge_group) flow is intentionally distinct from the merge_group
handling above it: it covers events fired for a PR that is still
enqueued but is not itself a merge_group event (e.g. a review comment
posted while the PR is queued), so the advisory merge-queue notice can
be (re-)posted for it.

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename 'cl' -> 'checklist' and 'cid' -> 'checklist_id' (and
'cl_copy' -> 'checklist_copy', 'comment_id_to_cid' ->
'comment_id_to_checklist_id') throughout helpers.py,
check_acknowledgements.py, dismiss_and_invalidate.py,
post_checklists.py, and helpers_test.py. These abbreviations made call
sites and comprehensions harder to scan at a glance, especially where
'cl' (checklist) could be misread as a different kind of list/class
abbreviation. Wrapped the handful of lines that exceeded 88 columns
after the rename.

Addresses review comments (previously deferred by the reviewer to a
future PR):
#899 (comment)
#899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename a few names left unclear by a broader review pass:
- helpers.py: rename '_find_checklists_config's local 'r' -> 'runfiles'
  (matches the imported Runfiles type it holds an instance of).
- dismiss_and_invalidate.py: rename '_get_files_in_latest_push's list
  comprehension variable 'r' -> 'run' (workflow run objects, not
  something needing single-letter treatment); rename
  'handle_synchronize's 'review' -> 'finding_comment' — it actually
  holds the bot-posted checklist finding comment looked up from
  'existing', not a PR review object, so the old name was actively
  misleading, not just terse.
- check_acknowledgements.py: rename '_acknowledgement_status's
  single-letter comprehension variable 'u' -> 'username' for clarity.

No behavior change; verified with bazel test //tools/review-checklists/...

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed_ids

These names looked like they held every relevant/affected checklist id,
but they actually only held the subset that already has a posted
finding comment (filtered by 'if checklist["id"] in existing'). That
ambiguity was directly responsible for a prior bug in this same code
(commit 631b5ff): a checklist missing from this filtered list was
silently treated as 'no relevant checklists remain to check' instead
of 'a relevant checklist has not been posted yet'. Renaming to make the
'posted' filtering explicit at every read site, not just at the point
of construction.

No behavior change; verified with bazel test //tools/review-checklists/...

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Following up on discussion_r3772590463 ('relevant_ids is a bad naming
... what is it relevant for?'): the same ambiguity existed one level up
in the variables holding the matched checklist dicts themselves.
'relevant' and 'affected' read as bare adjectives with no object, which
invited exactly this kind of question at every read site. Rename to
'relevant_checklists' (helpers.match_checklists/build_evidence_block,
check_acknowledgements.py, post_checklists.py) and
'affected_checklists' (dismiss_and_invalidate.py) to make explicit what
they are relevant/affected checklists of. Wrapped two comprehensions
that exceeded 88 columns after the rename.

No behavior change; verified with bazel test //tools/review-checklists/...

Addresses review comment: #899 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@LittleHuba
LittleHuba force-pushed the add-review-checklists-workflow branch from 997ef3e to 71a0974 Compare August 13, 2026 16:04
@LittleHuba

Copy link
Copy Markdown
Contributor Author

I'll update the CI design tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants