Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
41a3d01
Add review checklists workflow and action
LittleHuba Aug 6, 2026
9078660
Split review-checklists workflow into unprivileged trigger + privileg…
LittleHuba Aug 10, 2026
cb8202a
Minimize stage1->stage2 data transfer in review-checklists split
LittleHuba Aug 10, 2026
816c9dd
Fix PR-number resolution: gh api --jq does not accept --arg
LittleHuba Aug 10, 2026
0572ce5
Drop stage1->stage2 action transfer; dismiss_sync self-determines no-op
LittleHuba Aug 10, 2026
3218931
Resolve PR number by head branch, not head SHA, to avoid a fast-itera…
LittleHuba Aug 10, 2026
cb82f95
Disable the example checklist
LittleHuba Aug 10, 2026
714236b
Move review-checklists action to tools/review-checklists; add README
LittleHuba Aug 10, 2026
f8281cb
Post checklist findings as file-level comments to support binary files
LittleHuba Aug 10, 2026
881c163
Restrict compile_pip_requirements to linux
LittleHuba Aug 10, 2026
155b003
Validate checklist evidence in merge queue instead of assuming it
LittleHuba Aug 10, 2026
8339f6e
Transfer PR number from trigger to apply stage via artifact
LittleHuba Aug 10, 2026
c901cb5
Fix merge_group evidence validation never actually running
LittleHuba Aug 11, 2026
fc5ab66
Switch checklist path matching from fnmatch to pathspec (gitignore-st…
LittleHuba Aug 11, 2026
eb7fa1d
Reduce visibility
LittleHuba Aug 11, 2026
e0039ac
Fix stale copyright-checker srcs entry after actions/ -> tools/ move
LittleHuba Aug 13, 2026
6834410
Add cancel-in-progress to review-checklists apply workflow concurrency
LittleHuba Aug 13, 2026
0c12cba
Document why review-checklists action installs deps with pip, not Bazel
LittleHuba Aug 13, 2026
1865c83
Clarify Otterdog status-check syntax for the review-checklists status
LittleHuba Aug 13, 2026
bbe339e
Deduplicate OK-reply lookup across review-checklists scripts
LittleHuba Aug 13, 2026
05b053e
Use Bzlmod module name instead of hardcoded '_main' for Rlocation
LittleHuba Aug 13, 2026
075c29f
Document why get_approving_reviewers' discard call is not redundant
LittleHuba Aug 13, 2026
3d3caa7
Name the commit-status description length magic number
LittleHuba Aug 13, 2026
b66b6b2
Document why is_pr_in_merge_queue fails closed to False on error
LittleHuba Aug 13, 2026
5f11541
Stay pending until every relevant checklist has been posted
LittleHuba Aug 13, 2026
1908c66
Clarify why the merge-queue notice check is separate from merge_group…
LittleHuba Aug 13, 2026
b329e1d
Avoid single/two-letter abbreviations for checklist variables
LittleHuba Aug 13, 2026
61b1e38
Improve remaining unclear variable names
LittleHuba Aug 13, 2026
ea54a07
Rename relevant_ids/affected_ids to posted_relevant_ids/posted_affect…
LittleHuba Aug 13, 2026
1822060
Rename relevant/affected checklist-list variables for clarity
LittleHuba Aug 13, 2026
15c6f1d
Add review checklists concept to CI.md
LittleHuba Aug 19, 2026
1315cfe
Fix sync_skills.check false failure by removing .github/BUILD package…
LittleHuba Aug 19, 2026
2a79786
Reformat review-checklists scripts with ruff
LittleHuba Aug 19, 2026
4bd3483
Fix ruff F401 unused-import findings in helpers_test.py
LittleHuba Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/review_checklists.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

# Review checklists configuration.
# Each checklist has:
# id: Unique identifier (used in markers and tracking)
# name: Human-readable name shown in the PR conversation
# include: List of glob patterns; a file must match at least one to be considered
# exclude: (optional) List of glob patterns; a file matching any of these is excluded
# checklist: Markdown checklist body shown to reviewers

checklists: []
# - id: example-review
# name: "Example checklist"
# include:
# - "**"
# exclude:
# - ".github/**"
# checklist: |
# - This is an example checklist item
# - Avoid checkmarks in the items since this makes it easy for users to accidentally modify the checklist
# - Modifying the checklist will reset all previous acknowledgments
178 changes: 178 additions & 0 deletions .github/workflows/review_checklists_apply.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

# Privileged apply stage for the review-checklists workflow.
#
# Triggered via `workflow_run` once "Review Checklists (Trigger)" completes.
# `workflow_run` always executes using the base repository's workflow file
# and permissions, regardless of which repository (including forks) raised
# the original event — this is what lets this job safely hold a
# pull-requests/statuses write token even though some of the original
# events (pull_request_review, pull_request_review_comment on fork PRs)
# would otherwise force a read-only GITHUB_TOKEN.
#
# This workflow does all the actual work: it derives the original event
# name/head SHA entirely from the `workflow_run` context and the GitHub
# API, and re-runs the checklist logic that the previous single-stage
# workflow used to run directly on the untrusted events. The only value
# carried over from the trigger stage is the PR number, downloaded from
# the artifact the trigger stage uploaded (see review_checklists_trigger.yml
# for why that value is safe to trust as-is). Nothing else is transferred
# — not even the original event's `action` (opened/reopened/synchronize/
# edited): `dismiss_sync` is simply run on every pull_request_target event
# and self-determines whether anything actually changed (see below), so
# there is nothing else stage 2 needs stage 1 to hand it.
#
# Keep review-checklists up to date on pull requests and report whether all
# checklists have been acknowledged by every approving reviewer.
#
# Original events handled:
# pull_request_target (opened/reopened/synchronize/edited)
# -> Post or update checklist findings on the PR.
# -> Invalidate OKs for checklists affected by commits since the
# previous trigger run (dismiss_sync resolves the "before" SHA
# itself via run history and diffs against the current head; if
# the head SHA hasn't moved — e.g. on "edited"/"reopened" without
# a new push — the diff is empty and this is a no-op).
# -> On edited: refresh checklist evidence/notices after PR description changes.
# -> Re-check acknowledgements.
# pull_request_review_comment (created/edited/deleted)
# -> Restore checklist findings if they were tampered with or deleted.
# -> Re-check acknowledgements (this naturally also handles OK-comment
# edits/deletions — see check_acknowledgements.py).
# pull_request_review (submitted/dismissed)
# -> Re-check acknowledgements (approver set may have changed).
# merge_group (checks_requested)
# -> Resolve the originating PR (its number transferred from the
# trigger stage, parsed there from the merge-queue's synthetic head
# ref) and re-validate its checklist evidence from scratch against
# its current live state — not merely assumed OK because a required
# status check passed at some earlier point (see
# check_acknowledgements.py).
#
# When a PR is in merge queue, the "Check acknowledgements" step also
# ensures:
# - a persistent PR comment notice, and
# - a standalone PR-description notice,
# explaining that post-queue changes do not alter evidence recorded in git
# history by the merge commit evidence flow.

name: Review Checklists (Apply)

on:
workflow_run:
workflows: ["Review Checklists (Trigger)"]
types: [completed]

permissions:
pull-requests: write
statuses: write
actions: read

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).

# Multiple trigger-stage runs for the same head SHA can complete in quick
# succession (e.g. several review comments posted back-to-back), so let a
# newer run cancel an older, now-superseded one. Safe because this job only
# recomputes idempotent PR state (comments/statuses) from scratch each time.
cancel-in-progress: true

jobs:
review-checklists:
name: Review Checklists
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v6.0.2

# Downloads the PR number from the artifact the trigger stage
# uploaded (see review_checklists_trigger.yml for why this value is
# safe to trust as-is). Requires actions: read to fetch artifacts
# from another workflow run via run-id.
#
# A missing/empty artifact (e.g. the trigger job failed before
# uploading, or ran before this artifact step existed) resolves to
# an empty pr_number, and downstream steps already tolerate that
# (they simply have nothing to act on).
- name: Download PR number
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: pr-number
path: pr-number
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Read PR number
id: resolve_pr
run: |
pr_number="$(cat pr-number/pr-number.txt 2>/dev/null || true)"
echo "Resolved PR number: ${pr_number}"
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"

# Step 1: Post or update checklist findings.
# Runs on PR open/reopen/sync, on PR description edits, and on
# review comment events so tampered checklist findings and notices
# are restored.
- name: Post / update checklist findings
if: >-
github.event.workflow_run.event == 'pull_request_target' ||
github.event.workflow_run.event == 'pull_request_review_comment'
uses: ./tools/review-checklists
with:
action: post
github-token: ${{ secrets.GITHUB_TOKEN }}
pr-number: ${{ steps.resolve_pr.outputs.pr_number }}

# Step 2: Invalidate stale OKs for commits since the previous push.
# dismiss_sync resolves the "before" SHA itself (via the previous
# Trigger-workflow run for this branch) and diffs it against the
# current head, so it doesn't need to know which pull_request_target
# sub-action fired: if the head SHA hasn't moved since the previous
# trigger run (e.g. "edited"/"reopened" without a new push), the
# diff is empty and this step is a no-op.
- name: Invalidate stale acknowledgements
if: github.event.workflow_run.event == 'pull_request_target'
uses: ./tools/review-checklists
with:
action: dismiss_sync
github-token: ${{ secrets.GITHUB_TOKEN }}
pr-number: ${{ steps.resolve_pr.outputs.pr_number }}
run-id: ${{ github.event.workflow_run.id }}
head-branch: ${{ github.event.workflow_run.head_branch }}

# Step 3: Always re-check acknowledgement status.
# The script sets the commit status context "review-checklists"
# to "pending"/"success"/"failure", and (as of this stateless
# redesign) also refreshes the merge-queue notice. It always
# re-scans the live comment state, so it naturally also picks up
# OK-comment edits/deletions (there's no dedicated "dismiss_edit"
# action anymore). For merge_group it resolves the originating PR
# (via pr-number, transferred from the trigger stage above) and
# re-validates that PR's checklist evidence from scratch — needing
# only the original event name, head SHA, and PR number. Commit statuses
# with the same context on the same SHA are deduplicated, so the
# latest result from any trigger always wins. This job always has
# write access (it only ever runs via workflow_run) — branch
# protection must require the commit status context (not the
# workflow check run) to gate merges.
- name: Check acknowledgements
uses: ./tools/review-checklists
with:
action: check
github-token: ${{ secrets.GITHUB_TOKEN }}
pr-number: ${{ steps.resolve_pr.outputs.pr_number }}
head-sha: ${{ github.event.workflow_run.head_sha }}
event-name: ${{ github.event.workflow_run.event }}
98 changes: 98 additions & 0 deletions .github/workflows/review_checklists_trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

# Unprivileged trigger stage for the review-checklists workflow.
#
# This workflow reacts to every event that can affect checklist state,
# including events raised for pull requests from forked repositories
# (pull_request_review, pull_request_review_comment). For those events
# GitHub always issues a read-only GITHUB_TOKEN, no matter what the
# `permissions:` block requests, so this workflow must not (and does not)
# attempt to write anything.
#
# It performs no checkout and executes no repository/PR-supplied code. The
# only thing handed across the trust boundary to the privileged
# `review_checklists_apply.yml` workflow is the PR number, uploaded as a
# build artifact for the apply stage to download by run ID. This is safe
# to trust as-is: it comes straight from the trusted event payload
# (github.event.pull_request.number, or parsed from
# github.event.merge_group.head_ref for merge_group), is never
# attacker-influenced code or a value that steers which logic runs, and at
# worst a wrong number would just point the apply stage at the wrong
# already-public, same-repository PR. Everything else the apply stage
# needs (event name, head SHA, and even whether anything actually needs
# invalidating) is still derived natively from the `workflow_run` context /
# the GitHub API / a live diff against run history. This job's sole
# purpose beyond that is to exist and complete, so that `workflow_run`
# fires the apply stage, which always executes with the base repository's
# permissions regardless of where the triggering event originated.
#
# See "Preventing pwn requests" for background on this pattern:
# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/

name: Review Checklists (Trigger)

on:
pull_request_target:
types: [opened, reopened, synchronize, edited]
branches:
- main
pull_request_review_comment:
types: [created, edited, deleted]
pull_request_review:
types: [submitted, dismissed]
merge_group:
types: [checks_requested]

permissions: {}

jobs:
trigger-apply:
name: Trigger apply stage
runs-on: ubuntu-24.04
steps:
# Resolve the PR number directly from the trusted event payload.
#
# For merge_group there is no github.event.pull_request; the number
# is instead parsed out of the synthetic queue ref name, which
# GitHub generates as `gh-readonly-queue/<base-branch>/pr-<number>-<sha>`.
- name: Resolve PR number
id: resolve_pr
env:
EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
MERGE_GROUP_REF: ${{ github.event.merge_group.head_ref }}
run: |
if [ "${EVENT_NAME}" = "merge_group" ]; then
pr_number="$(echo "${MERGE_GROUP_REF}" | grep -oP '(?<=/pr-)[0-9]+' || true)"
else
pr_number="${PR_NUMBER}"
fi
echo "Resolved PR number: ${pr_number}"
mkdir -p pr-number
echo -n "${pr_number}" > pr-number/pr-number.txt

# Upload it for the privileged apply stage to pick up by run ID.
# This is the only piece of data transferred across the trust
# boundary; see the file-level comment above for why it's safe.
- name: Upload PR number
uses: actions/upload-artifact@v4
with:
name: pr-number
path: pr-number/pr-number.txt
retention-days: 1

# No-op beyond the above: this job's completion is what fires the
# `workflow_run`-triggered apply stage.
- name: Signal apply stage
run: echo "Triggering privileged apply stage via workflow_run."
12 changes: 12 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ exports_files(["MODULE.bazel"])

sync_skills()

# Kept in the root package (rather than a .github/BUILD file) so that this
# package doesn't become its own Bazel package: a BUILD file under .github
# would create a package boundary there, causing the sync_skills.check
# glob(".github/skills/score-*/**") above to silently stop matching any
# files and always report the score_tooling skills as missing.
filegroup(
name = "review_checklists_config",
srcs = [".github/review_checklists.yml"],
visibility = ["//tools/review-checklists:__subpackages__"],
)

sphinx_docs_library(
name = "contributing_md",
srcs = ["CONTRIBUTING.md"],
Expand All @@ -47,6 +58,7 @@ copyright_checker(
"quality",
"score",
"third_party",
"tools",
"//:BUILD",
"//:MODULE.bazel",
],
Expand Down
7 changes: 7 additions & 0 deletions CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ but with the addition of the flag `--runs_per_test=20`.
To be able to have fast feedback, all our CI job use Bazel cache. To avoid permanent cache poisoning and cache size exploding, the cache is recreated nightly.
More details can be found in the [cache strategy design document](./.github/cache-strategy.md).

#### Review checklists

Ensures that specific manual actions are performed by the reviewers of a pull request.
Reviewers must acknowledge that they have performed the actions in the checklist before a pull request can be merged.
During merge to the base branch, the acknowledgements are stored in the git history as evidence.
The checklists are defined in the [review checklist configuration document](./.github/review_checklists.yml).

## Post-mortem analysis and follow-up actions

It is acknowledged that this design is not perfect, and experience will teach us that this design will have to be adapted.
Expand Down
7 changes: 7 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,13 @@ pip.parse(
)
use_repo(pip, "codeql_coding_standards_pip_hub")

pip.parse(
hub_name = "review_checklists_dependencies",
python_version = "3.12",
requirements_lock = "//tools/review-checklists:requirements.txt",
)
use_repo(pip, "review_checklists_dependencies")

# TRLC dependency for requirements traceability
bazel_dep(name = "trlc", version = "3.0.1", dev_dependency = True)
bazel_dep(name = "rules_oci", version = "2.2.7", dev_dependency = True)
Expand Down
21 changes: 21 additions & 0 deletions tools/review-checklists/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
load("@rules_python//python:pip.bzl", "compile_pip_requirements")

compile_pip_requirements(
name = "requirements",
src = "requirements.txt.in",
exec_compatible_with = ["@platforms//os:linux"],
requirements_txt = "requirements.txt",
target_compatible_with = ["@platforms//os:linux"],
)
Loading
Loading