Skip to content
Merged
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
82 changes: 56 additions & 26 deletions .github/workflows/review-debt-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,43 +52,73 @@ jobs:
*,review-debt-exempt,*) echo "review-debt-exempt 라벨 — 건너뜀"; exit 0 ;;
esac

# 작성자에게 걸린 리뷰 요청. 자기가 만든 PR 은 자신이 리뷰어가 될 수
# 없으므로 이 검색에 잡히지 않는다. --paginate 없이는 30건에서 잘린다.
if ! requested=$(gh api -X GET search/issues \
-f q="is:open is:pr repo:$REPO review-requested:$AUTHOR" --paginate \
--jq '.items[] | "\(.number)\t\(.created_at[0:10])\t\(.title)"'); then
echo "::error::리뷰 요청 조회에 실패했다 — 근거 없이 PR 을 닫지 않는다. 재실행할 것"
# 빚의 기준은 «나에게 요청이 걸렸는가» 가 아니라 «아무도 응하지 않은 PR 이
# 남아 있는가» 다. 리뷰 요청은 알림 수단이지 담당 배정이 아니다 —
# 누구에게 걸렸든 팀에서 한 사람이 보면 그 PR 의 리뷰는 끝난 것이다.
# 그래서 요청 목록이 아니라 열린 PR 전체를 훑는다.
#
# 자기 PR 은 자신이 리뷰할 수 없으므로 뺀다. draft 는 리뷰를 받을 상태가
# 아니므로 뺀다(review-request-all 도 draft 에는 요청을 걸지 않는다).
if ! open_prs=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --paginate \
--jq ".[] | select(.draft == false) | select(.user.login != \"$AUTHOR\") | \"\(.number)\t\(.created_at[0:10])\t\(.title)\""); then
echo "::error::열린 PR 조회에 실패했다 — 근거 없이 PR 을 닫지 않는다. 재실행할 것"
exit 1
fi

# 요청이 걸렸다고 다 빚은 아니다. 두 가지를 걸러낸다.
# 남은 것에서 «리뷰어의 공이 아닌» PR 을 걸러낸다. «누가» 봤는지는 따지지
# 않는다 — 팀에서 한 사람이 보면 그 PR 의 리뷰는 채워진 것이다.
#
# 승인과 변경요청은 무게가 다르다.
# APPROVED 머지 가능한 상태다. 그 뒤에 작성자가 손보는 것은
# 작성자 몫이고, 다시 봐 줄 의무는 누구에게도 없다.
# CHANGES_REQUESTED 아직 끝난 판정이 아니다. 작성자가 지적을 반영하면
# 공이 리뷰어에게 돌아온다. 재리뷰가 없으면 그 PR 은
# 영영 머지되지 못한다.
#
# 1. «누가» 봤는지는 따지지 않는다. PR 하나에 필요한 리뷰는 한 건이므로,
# 다른 사람이 이미 판정을 냈으면 내 몫은 끝난 것으로 본다. 요청을
# 전원에게 거는 것은 «누구든 먼저 보라» 는 뜻이지 «둘 다 보라» 가 아니다.
# 2. 병합 커밋은 «새 코드» 로 치지 않는다. develop 전파만 쌓인 PR 이
# 미리뷰로 잡히면, 이미 승인해 둔 스택 PR 이 통째로 빚이 된다.
# parents 가 둘 이상인 커밋을 빼고 마지막 실질 변경 시각을 잡는다.
# 그래서 빚은 두 종류다. (1) 아무도 판정을 내지 않은 PR,
# (2) 미해소 변경요청이 있는데 작성자가 이미 반영 커밋을 올린 PR.
# 변경요청 뒤로 새 커밋이 없으면 공은 아직 작성자에게 있으므로 빚이 아니다.
# 반영 여부는 병합 커밋(parents 2개 이상)을 뺀 실질 커밋으로만 센다.
debt_file=$(mktemp)
reviews_file=$(mktemp)
while IFS=$'\t' read -r pn created title; do
[ -n "$pn" ] || continue

changed_at=$(gh api "repos/$REPO/pulls/$pn/commits?per_page=100" --paginate \
--jq '[.[] | select((.parents | length) < 2)] | last | .commit.committer.date' 2>/dev/null || true)
[ -n "$changed_at" ] && [ "$changed_at" != "null" ] || changed_at="1970-01-01T00:00:00Z"
gh api "repos/$REPO/pulls/$pn/reviews" --paginate \
--jq '.[] | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED")
| {u: .user.login, s: .state, t: .submitted_at}' \
> "$reviews_file" 2>/dev/null || true

if [ ! -s "$reviews_file" ]; then
echo "#$pn — 아무도 판정하지 않음"
printf '%s\t%s\t%s\n' "$pn" "$created" "$title" >> "$debt_file"
continue
fi

# 리뷰어별 최신 판정이 CHANGES_REQUESTED 인 것 중 가장 늦은 시각.
blocked_at=$(jq -rs 'group_by(.u) | map(sort_by(.t) | last)
| map(select(.s == "CHANGES_REQUESTED"))
| sort_by(.t) | last | .t // empty' "$reviews_file" 2>/dev/null || true)

if [ -z "$blocked_at" ]; then
echo "#$pn — 승인됨, 빚 아님"
continue
fi

judged=$(gh api "repos/$REPO/pulls/$pn/reviews" --paginate \
--jq "[.[] | select(.submitted_at > \"$changed_at\") | select(.state == \"APPROVED\" or .state == \"CHANGES_REQUESTED\")] | length" 2>/dev/null || true)
fixed=$(gh api "repos/$REPO/pulls/$pn/commits?per_page=100" --paginate \
--jq "[.[] | select((.parents | length) < 2)
| select(.commit.committer.date > \"$blocked_at\")] | length" 2>/dev/null || true)

if [ "${judged:-0}" -eq 0 ]; then
if [ "${fixed:-0}" -gt 0 ]; then
echo "#$pn — 변경요청 반영(${fixed}커밋) 뒤 재리뷰 없음"
printf '%s\t%s\t%s\n' "$pn" "$created" "$title" >> "$debt_file"
else
echo "#$pn — 최신 코드에 이미 판정이 있음(${judged}건), 빚 아님"
echo "#$pn — 변경요청 미반영, 공은 작성자에게"
fi
done <<< "$requested"
done <<< "$open_prs"

if [ ! -s "$debt_file" ]; then
echo "미처리 리뷰 요청 없음 — 통과"
echo "응답을 기다리는 PR 없음 — 통과"
exit 0
fi

Expand All @@ -99,7 +129,7 @@ jobs:
{
echo "## 🚧 리뷰 적체 가드 — 이 PR 을 닫습니다"
echo
echo "@$AUTHOR 님이 아직 처리하지 않은 리뷰 요청이 **${count}건** 있습니다."
echo "**아무도 리뷰하지 않은** PR 이 **${count}건** 남아 있습니다."
echo
echo "| PR | 열린 날 | 경과 | 제목 |"
echo "|---|---|---|---|"
Expand All @@ -110,9 +140,9 @@ jobs:
echo "| #$n | $created | ${days}일 | $title |"
done <<< "$debt"
echo
echo "이 저장소는 미처리 리뷰 요청이 1건이라도 있으면 새 PR 을 열 수 없습니다 (#1136)."
echo "이 저장소는 아무도 응답하지 않은 PR 이 1건이라도 남아 있으면 새 PR 을 열 수 없습니다 (#1136)."
echo
echo "각 PR 에 필요한 리뷰는 **한 건**입니다 — 다른 사람이 먼저 판정을 내면 그 PR 은 목록에서 빠집니다."
echo "각 PR 에 필요한 리뷰는 **한 건**입니다 — 누구든 먼저 판정을 내면 그 PR 은 모두의 목록에서 빠집니다. 승인 뒤에 올라온 변경을 다시 봐 줄 의무는 없지만, **변경요청은 작성자가 반영한 시점에 다시 리뷰어의 몫**이 됩니다."
echo
echo "### 되살리는 법"
echo
Expand All @@ -125,7 +155,7 @@ jobs:
echo "리뷰보다 이 PR 이 급하면 \`review-debt-exempt\` 라벨을 붙이고 다시 열면 통과합니다."
} > /tmp/debt-comment.md

echo "미처리 ${count}건 — PR #$PR_NUMBER 를 닫는다"
echo "미응답 ${count}건 — PR #$PR_NUMBER 를 닫는다"
gh pr close "$PR_NUMBER" --repo "$REPO" --comment "$(cat /tmp/debt-comment.md)"

bypass-audit:
Expand Down
Loading