Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions .github/workflows/pr-policy-here.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Pull request policy for this repository. The reusable workflow itself is .github/workflows/pr-policy.yml.

name: PR policy

on:
pull_request_target:
types: [opened, reopened, review_requested]

permissions:
pull-requests: write

jobs:
policy:
uses: redrob-labs/.github/.github/workflows/pr-policy.yml@main
148 changes: 148 additions & 0 deletions .github/workflows/pr-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Pull request policy shared by every redrob-labs repository.
#
# Two rules, both enforced by closing or un-requesting rather than by failing a check, so they
# cannot be ignored:
#
# 1. Open-PR cap. An author keeps at most `cap` open pull requests per repository. The oldest
# `cap` stay; any newer one is closed with a comment the moment it is opened or reopened.
# 2. Review-request gate. A review may be requested from a maintainer in `gated_reviewers` only
# after the author has reviewed their own pull request (one review of any state on their own
# PR) and has no unchecked box left in the description. Otherwise the request is removed and
# one comment, updated in place, says what is missing.
#
# Logins in `exempt` are never limited or gated.
#
# Called from a repository through `pull_request_target`, so it runs the base branch's copy and
# the token can write to the base repository even for forks. Callers must grant
# `pull-requests: write`.

name: PR policy

on:
workflow_call:
inputs:
cap:
description: Maximum open pull requests per author in this repository
type: number
default: 3
exempt:
description: Space-separated logins never limited or gated
type: string
default: "its-janghoon dependabot[bot]"
gated_reviewers:
description: Space-separated logins whose review may only be requested after a self-review
type: string
default: "its-janghoon"

permissions:
pull-requests: write

jobs:
policy:
name: Enforce
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target'
steps:
- name: Enforce the open-PR cap and the review-request gate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
ACTION: ${{ github.event.action }}
PR: ${{ github.event.pull_request.number }}
AUTHOR: ${{ github.event.pull_request.user.login }}
REQUESTED: ${{ github.event.requested_reviewer.login }}
CAP: ${{ inputs.cap }}
EXEMPT: ${{ inputs.exempt }}
GATED: ${{ inputs.gated_reviewers }}
run: |
set -euo pipefail

has() { local x; for x in $2; do [ "$x" = "$1" ] && return 0; done; return 1; }

if has "$AUTHOR" "$EXEMPT"; then
echo "$AUTHOR is exempt; nothing to enforce."
exit 0
fi

case "$ACTION" in
opened|reopened)
# Rank this PR among the author's open PRs by number. The oldest $CAP keep their
# place; anything ranked at or past the cap is closed. Ranking (rather than a plain
# count) keeps a burst of simultaneous openings from closing all of them.
numbers=$(gh pr list -R "$REPO" --author "$AUTHOR" --state open --limit 200 \
--json number --jq 'sort_by(.number)[].number')
mapfile -t open < <(printf '%s\n' "$numbers")
rank=-1; i=0
for n in "${open[@]}"; do
[ "$n" = "$PR" ] && rank=$i
i=$((i + 1))
done
echo "$AUTHOR has ${#open[@]} open PR(s); #$PR ranks $rank, cap $CAP."
if [ "$rank" -lt 0 ]; then
echo "::error::#$PR is not in the author's open list; refusing to guess." >&2
exit 1
fi
if [ "$rank" -lt "$CAP" ]; then
echo "Within the cap."
exit 0
fi
keep=$(printf '#%s, ' "${open[@]:0:$CAP}"); keep=${keep%, }
msg="<!-- pr-policy:cap -->
Closed by the pull request policy: an author keeps at most **$CAP** open pull requests in this repository, and you already have $keep open.

Land or close one of those, then reopen this one. Folding related branches into one of the open pull requests is usually the faster route."
gh pr close -R "$REPO" "$PR" --comment "$msg"
;;

review_requested)
if [ -z "$REQUESTED" ]; then
echo "A team was requested, not a person; nothing to gate."
exit 0
fi
if ! has "$REQUESTED" "$GATED"; then
echo "$REQUESTED is not a gated reviewer."
exit 0
fi

self_reviews=$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \
--jq ".[] | select(.user.login == \"$AUTHOR\") | .id" | wc -l)
body=$(gh pr view -R "$REPO" "$PR" --json body --jq .body)
unchecked=$(printf '%s\n' "$body" | grep -c -E '^[[:space:]]*[-*] \[ \]' || true)
echo "self reviews: $self_reviews, unchecked boxes: $unchecked"

missing=""
if [ "$self_reviews" -eq 0 ]; then
missing="$missing
- a review of your own pull request (**Files changed → Review changes → Comment**). Read the diff the way a reviewer would, and leave the notes you would want answered;"
fi
if [ "$unchecked" -gt 0 ]; then
missing="$missing
- $unchecked unchecked box(es) in the description. Tick each one you have actually done; delete the line if it does not apply."
fi
if [ -z "$missing" ]; then
echo "Self-review done; the request to $REQUESTED stands."
exit 0
fi

gh api -X DELETE "repos/$REPO/pulls/$PR/requested_reviewers" -f "reviewers[]=$REQUESTED" >/dev/null
echo "Removed the review request to $REQUESTED."

marker='<!-- pr-policy:review-gate -->'
msg="$marker
Review request to @$REQUESTED removed by the pull request policy. Before asking a maintainer to review, finish your own review. Still missing:
$missing

Request the review again once both are done."
existing=$(gh api --paginate "repos/$REPO/issues/$PR/comments" \
--jq ".[] | select(.user.type == \"Bot\") | select(.body | startswith(\"$marker\")) | .id" | head -n1)
if [ -n "$existing" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$existing" -f body="$msg" >/dev/null
else
gh api -X POST "repos/$REPO/issues/$PR/comments" -f body="$msg" >/dev/null
fi
;;

*)
echo "No rule for action '$ACTION'."
;;
esac
10 changes: 10 additions & 0 deletions CONTRIBUTING.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ diff가 아니라 빌드된 화면을 보고 검증합니다. 초록불 빌드

## 리뷰와 머지

다음 두 규칙은 모든 저장소의 워크플로가 강제합니다. 빨간불로 두고 넘어갈 수 있는 검사가 아닙니다.

- **한 사람이 한 저장소에 동시에 열 수 있는 풀 리퀘스트는 3개까지입니다.** 오래된 3개가 남고, 그
뒤에 연 것은 여는 즉시 댓글과 함께 닫힙니다. 하나를 머지하거나 닫은 뒤 다시 여세요. 여러
브랜치가 한 덩어리라면 풀 리퀘스트도 하나입니다.
- **메인테이너에게 리뷰를 요청하기 전에 자기 풀 리퀘스트를 먼저 리뷰합니다.** 자기 풀 리퀘스트에
리뷰를 남기고(Files changed → Review changes → Comment) 설명의 체크박스를 모두 처리하기 전에는
메인테이너에게 보낸 리뷰 요청이 제거됩니다. 한 것은 체크하고, 해당 없는 줄은 지우세요. 그 다음
리뷰를 다시 요청합니다.

풀 리퀘스트는 검사가 초록불이고 메인테이너가 머지해야 합니다. 머지 방식은 취향이 아닙니다. 다음
머지가 무엇을 할 수 있는지를 바꿉니다.

Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ has shipped an invisible label and a control that did nothing.

## Reviews and merging

Two rules are enforced by a workflow in every repository, not by a check you can leave red:

- **At most three open pull requests per author, per repository.** The oldest three stay; a
newer one is closed on opening with a comment. Land or close one, then reopen. If several
branches belong together, they are one pull request.
- **Review your own pull request before asking a maintainer to.** A review request to a
maintainer is removed until you have left a review on your own pull request (Files changed →
Review changes → Comment) and no box in the description is left unchecked. Tick what you did;
delete a line that does not apply. Then request the review again.

A pull request needs its checks green and a maintainer merge. Merge style is not a preference,
because it changes what the next merge can do:

Expand Down
6 changes: 6 additions & 0 deletions workflow-templates/pr-policy.properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "PR policy (open-PR cap and self-review gate)",
"description": "Closes a pull request beyond the author's cap of 3 open ones, and removes a review request to a maintainer until the author has reviewed their own pull request and ticked every box in the description.",
"iconName": "octicon shield-check",
"categories": ["Continuous integration"]
}
17 changes: 17 additions & 0 deletions workflow-templates/pr-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Pull request policy: at most 3 open pull requests per author, and a maintainer review may be
# requested only after the author has reviewed their own pull request. Rules and defaults live in
# redrob-labs/.github/.github/workflows/pr-policy.yml; pass `with:` to change them for this
# repository.

name: PR policy

on:
pull_request_target:
types: [opened, reopened, review_requested]

permissions:
pull-requests: write

jobs:
policy:
uses: redrob-labs/.github/.github/workflows/pr-policy.yml@main
Loading