diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2c8621f0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Changes to the CI workflows and to the AI self-review check need a maintainer's review, see +# docs/adr/0002-self-review-check-on-pull-request.md. This is enforced once the branch protection +# of main requires a review from code owners. +/.github/workflows/ @b0661 @NormannK +/.github/CODEOWNERS @b0661 @NormannK +/scripts/check_pr_self_review.py @b0661 @NormannK diff --git a/.github/workflows/ai-self-review.yml b/.github/workflows/ai-self-review.yml index c203ceab..93328d71 100644 --- a/.github/workflows/ai-self-review.yml +++ b/.github/workflows/ai-self-review.yml @@ -3,19 +3,21 @@ name: ai-self-review # Checks the self-review declaration of agent-assisted pull requests, see # docs/adr/0001-self-declared-sha-bound-review-gate.md and docs/develop/ai.md. # -# pull_request_target runs the workflow and the check script of the base branch, so a pull -# request cannot weaken its own check. The job never checks out or runs code of the pull -# request; it reads the body and the commit messages through the API only. +# It runs on pull_request, see docs/adr/0002-self-review-check-on-pull-request.md. The workflow +# file therefore comes from the pull request, but the job checks out and runs the check script of +# the base branch; it never checks out or runs other code of the pull request and reads the body +# and the commit messages through the API only. A pull request that changes this workflow can +# weaken its own check, so .github/CODEOWNERS requires a maintainer's review for it. # # It runs only for pull requests into main, never on pushes to main or to other branches, and not # for pull requests into other branches. # # On failure it posts one comment that explains the fix, found again by its marker and updated in -# place; once the check passes, the comment is updated to say so. Commenting needs -# pull-requests: write, which is safe here because no pull request code runs. +# place; once the check passes, the comment is updated to say so. Pull requests from forks get a +# read-only token, so the comment step is skipped for them; the report is in the job summary. on: - pull_request_target: + pull_request: types: [opened, edited, synchronize, reopened] branches: [main] @@ -83,6 +85,8 @@ jobs: echo "status=$status" >> "$GITHUB_OUTPUT" cat "$RUNNER_TEMP/report.md" >> "$GITHUB_STEP_SUMMARY" - name: Comment on the pull request + # The token of a pull request from a fork is read-only and cannot comment. + if: github.event.pull_request.head.repo.full_name == github.repository # A failing comment must not change the result of the check. continue-on-error: true env: @@ -104,8 +108,13 @@ jobs: if: always() env: STATUS: ${{ steps.check.outputs.status }} + FROM_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} run: | if [ "${STATUS:-1}" != "0" ]; then - echo "::error::The self-review check failed; see the step summary or the pull request comment." + if [ "$FROM_FORK" = "true" ]; then + echo "::error::The self-review check failed; see the job summary of this run." + else + echo "::error::The self-review check failed; see the job summary or the pull request comment." + fi fi exit "${STATUS:-1}" diff --git a/docs/adr/0001-self-declared-sha-bound-review-gate.md b/docs/adr/0001-self-declared-sha-bound-review-gate.md index d296c958..cceab7d1 100644 --- a/docs/adr/0001-self-declared-sha-bound-review-gate.md +++ b/docs/adr/0001-self-declared-sha-bound-review-gate.md @@ -27,6 +27,7 @@ the maintainers' review remain the actual controls. - The check only blocks merging once maintainers make it a required status check. - The workflow runs on `pull_request_target` with the base branch's script and never runs pull request code, so a pull request cannot weaken its own check; changes to the check take effect - only after they are merged. + only after they are merged. Superseded by [0002](0002-self-review-check-on-pull-request.md). - The workflow needs `pull-requests: write` to comment how to fix a failing check. That is safe - only as long as it runs no pull request code. + only as long as it runs no pull request code. Superseded by + [0002](0002-self-review-check-on-pull-request.md). diff --git a/docs/adr/0002-self-review-check-on-pull-request.md b/docs/adr/0002-self-review-check-on-pull-request.md new file mode 100644 index 00000000..71f9dff4 --- /dev/null +++ b/docs/adr/0002-self-review-check-on-pull-request.md @@ -0,0 +1,31 @@ +# The self-review check runs on pull_request, guarded by code owners + +GitHub blocks `pull_request_target` by default in public repositories from December 2, 2026. +The `ai-self-review` workflow of [0001](0001-self-declared-sha-bound-review-gate.md) therefore +runs on `pull_request`. It still checks out and runs the check script of the base branch and no +other code of the pull request, but the workflow file itself now comes from the pull request, so +a pull request can weaken its own check by changing the workflow. `.github/CODEOWNERS` makes a +maintainer the code owner of `.github/workflows/`, `.github/CODEOWNERS` and +`scripts/check_pr_self_review.py`, so such a change needs a maintainer's review. This supersedes +the consequence of 0001 that a pull request cannot weaken its own check. + +## Considered Options + +- **Keep `pull_request_target` and allow it in an Actions event policy**: rejected, it depends on + a repository setting outside the code. +- **`pull_request` without code owners**: rejected, nothing would force a review of a change that + weakens the check. + +## Consequences + +- The code owner review is enforced only once the branch protection of `main` requires a review + from code owners. +- A pull request that deletes the workflow gets no check run; if the check is required, the pull + request waits for it and cannot be merged. +- The workflow keeps `pull-requests: write` to comment. The authors of pull requests from the + repository itself can write to it anyway; pull requests from forks get a read-only token, so + the workflow posts no comment for them, and the report is in the job summary only. +- Depending on the approval setting of the repository, runs for pull requests from forks of + first-time contributors wait for a maintainer's approval. +- `pull_request` runs on the merge commit of the pull request, so a pull request with merge + conflicts gets no run; a required check stays pending until the conflicts are resolved. diff --git a/docs/develop/ai.md b/docs/develop/ai.md index fe4c60b1..f81697b8 100644 --- a/docs/develop/ai.md +++ b/docs/develop/ai.md @@ -260,6 +260,8 @@ comment says so. The same report appears in the job summary of the run. Values t pull request body or the commit messages appear in the comment only inside a code block, so they cannot mention anyone or inject Markdown. If the check script itself fails, the comment says that the check could not run and points to the log of the workflow run; the check then fails. +Pull requests from forks get a read-only token, so the workflow does not comment on them; their +report is only in the job summary of the run. Every push changes the head, so the check fails until the body names the new head. Therefore the self-review is rerun and `pr.md` rewritten before the push, and the body is replaced right after @@ -270,13 +272,21 @@ Rebases and squashes before the merge need a fresh declaration too. The check cannot tell a real review from a typed-in SHA, as no AI runs in CI. Like a sign-off, it makes forgetting impossible and a false claim deliberate. Your author review and the maintainers' -review remain the controls. See the decision record -[0001](../adr/0001-self-declared-sha-bound-review-gate.md). +review remain the controls. See the decision records +[0001](../adr/0001-self-declared-sha-bound-review-gate.md) and +[0002](../adr/0002-self-review-check-on-pull-request.md). -The workflow runs on `pull_request_target`: it uses the workflow and the script of the base -branch, never the code of the pull request, so a pull request cannot weaken its own check. It runs -only for pull requests into `main`, never on pushes to `main` or to other branches, and not for -pull requests into other branches. +The workflow runs on `pull_request`, because GitHub blocks `pull_request_target` by default in +public repositories from December 2026. The workflow file therefore comes from the pull request, +while the check script is taken from the base branch, and no other code of the pull request runs. A +pull request that changes the workflow can weaken its own check, so `.github/CODEOWNERS` makes a +maintainer the code owner of `.github/workflows/`, `.github/CODEOWNERS` and +`scripts/check_pr_self_review.py`. This takes effect once the branch protection of `main` requires a +review from code owners. Depending on the approval setting of the repository, runs for pull requests +from forks of first-time contributors wait for a maintainer's approval. A pull request with merge +conflicts gets no run until the conflicts are resolved. The workflow runs only for pull requests +into `main`, never on pushes to `main` or to other branches, and not for pull requests into other +branches. ## The AI development sandbox