-
Notifications
You must be signed in to change notification settings - Fork 0
252 lines (214 loc) · 11.1 KB
/
Copy pathpr-reminder.yml
File metadata and controls
252 lines (214 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# =============================================================================
# PR Reminder (Organization-wide)
# =============================================================================
#
# 개요:
# Organization 내 모든 레포의 Open PR을 조회하여, 일정 시간 이상
# 실제 활동(커밋/리뷰/코멘트)이 없는 PR에 remind-needed 라벨을 부착하고,
# PR 코멘트로 리마인드하며, 활동이 재개된 PR에서는 라벨을 제거한다.
# 리마인드 대상 PR이 있으면 하나의 Slack 메시지로 통합 발송한다.
#
# 비활성 판단 기준:
# updated_at이 아닌 마지막 커밋/리뷰/코멘트 시각 중 최신값을 사용.
# Bot 타입 사용자의 코멘트/리뷰는 활동 판단에서 제외하여 피드백 루프를 방지함.
#
# 흐름:
# Schedule (매 2시간) 또는 수동 트리거
# → GitHub App Token 발급
# → GitHub Search API로 조직 내 모든 Open PR 조회
# → PR별 마지막 활동 시각 조회 (커밋/리뷰/코멘트)
# → 4시간 이상 비활성 PR에 remind-needed 라벨 부착 + PR 코멘트
# → 활성화된 PR에서 remind-needed 라벨 제거
# → 리마인드 대상 PR을 레포별로 그룹핑하여 Slack 발송
#
# -----------------------------------------------------------------------------
# Required Secrets:
# APP_ID - GitHub App ID
# PRIVATE_KEY - GitHub App Private Key
#
# Required Variables:
# ORG_NAME - GitHub Organization 이름 (예: da-bom)
#
# Optional Secrets:
# SLACK_WEBHOOK_URL - Slack Incoming Webhook URL (미설정 시 알림 스킵)
#
# Optional Variables:
# QUIET_HOURS - Slack 알림 차단 시간대 (예: "00-06", 미설정 시 항상 발송)
# QUIET_TIMEZONE - Quiet Hours 기준 타임존 (예: "Asia/Seoul", 기본값: UTC)
#
# GitHub App 추가 권한 필요:
# Pull Requests: Read & Write (라벨 부착/제거)
# =============================================================================
name: PR Reminder
on:
schedule:
# 매 2시간마다 실행 (UTC 기준)
- cron: '0 */2 * * *'
workflow_dispatch:
jobs:
pr-reminder:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ vars.ORG_NAME }}
- name: Check org-wide PRs and manage remind labels
id: check-prs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
ORG_NAME: ${{ vars.ORG_NAME }}
STALE_HOURS: 4
REMIND_LABEL: remind-needed
EXEMPT_LABELS: wip,blocked,do-not-remind
run: |
set -euo pipefail
echo "🔍 조직 '${ORG_NAME}' 내 모든 Open PR 조회 중..."
# 현재 시각 기준 STALE_HOURS 이전 시각 계산 (ISO 8601)
STALE_THRESHOLD=$(date -u -d "${STALE_HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ)
echo "⏱️ 비활성 기준 시각: ${STALE_THRESHOLD}"
# 조직 내 모든 Open PR 조회 (draft 제외)
# GitHub Search API: is:pr is:open org:<org> draft:false
ALL_PRS=$(gh api --paginate "search/issues?q=is:pr+is:open+org:${ORG_NAME}+draft:false&per_page=100" \
--jq '.items[] | {
number: .number,
title: .title,
html_url: .html_url,
user: .user.login,
updated_at: .updated_at,
repo: (.repository_url | split("/") | last),
repo_full: (.repository_url | split("/")[-2:] | join("/")),
labels: [.labels[].name]
}' | jq -s '.')
PR_COUNT=$(echo "$ALL_PRS" | jq 'length')
echo "📋 조회된 Open PR 수: ${PR_COUNT}"
# 제외 라벨 목록을 JSON 배열로 변환
EXEMPT_ARRAY=$(echo "$EXEMPT_LABELS" | jq -R 'split(",")')
STALE_PRS="[]"
FRESH_PRS_WITH_LABEL="[]"
for i in $(seq 0 $((PR_COUNT - 1))); do
PR=$(echo "$ALL_PRS" | jq ".[$i]")
PR_LABELS=$(echo "$PR" | jq '.labels')
REPO_FULL=$(echo "$PR" | jq -r '.repo_full')
PR_NUMBER=$(echo "$PR" | jq -r '.number')
HAS_REMIND=$(echo "$PR_LABELS" | jq --arg label "$REMIND_LABEL" 'any(. == $label)')
HAS_EXEMPT=$(echo "$PR_LABELS" | jq --argjson exempt "$EXEMPT_ARRAY" 'any(. as $l | $exempt | any(. == $l))')
# 제외 라벨이 있으면 스킵
if [ "$HAS_EXEMPT" = "true" ]; then
continue
fi
# 마지막 의미 있는 활동 시각 조회 (커밋, 리뷰, 코멘트)
# Bot 사용자의 코멘트/리뷰는 제외하여 리마인드 코멘트로 인한 피드백 루프 방지
LAST_COMMIT=$(gh api "repos/${REPO_FULL}/pulls/${PR_NUMBER}/commits" \
--jq 'last | .commit.committer.date // empty' 2>/dev/null || echo "")
LAST_REVIEW=$(gh api "repos/${REPO_FULL}/pulls/${PR_NUMBER}/reviews" \
--jq '[.[] | select(.user.type != "Bot")] | last | .submitted_at // empty' 2>/dev/null || echo "")
LAST_COMMENT=$(gh api "repos/${REPO_FULL}/issues/${PR_NUMBER}/comments" \
--jq '[.[] | select(.user.type != "Bot")] | last | .created_at // empty' 2>/dev/null || echo "")
LAST_ACTIVITY=$(printf '%s\n' "$LAST_COMMIT" "$LAST_REVIEW" "$LAST_COMMENT" \
| grep -v '^$' | sort -r | head -1)
# 활동 기록이 전혀 없으면 PR 생성 시각(updated_at) 사용
if [ -z "$LAST_ACTIVITY" ]; then
LAST_ACTIVITY=$(echo "$PR" | jq -r '.updated_at')
fi
# 비활성 여부 판단 (실제 활동 기반)
if [[ "$LAST_ACTIVITY" < "$STALE_THRESHOLD" ]]; then
# 비활성 PR → 라벨 부착 + 코멘트
STALE_PRS=$(echo "$STALE_PRS" | jq --argjson pr "$PR" '. + [$pr]')
if [ "$HAS_REMIND" = "false" ]; then
echo "🏷️ 라벨 부착: ${REPO_FULL}#${PR_NUMBER}"
gh api --method POST \
"repos/${REPO_FULL}/issues/${PR_NUMBER}/labels" \
-f "labels[]=${REMIND_LABEL}" > /dev/null 2>&1 || true
# PR 코멘트 작성 (최초 라벨 부착 시에만)
COMMENT_BODY=$'👋 **PR 리마인드 알림입니다**\n\n이 PR이 몇 시간 동안 업데이트되지 않았어요.\n\n- 🙋 **PR 작성자**: 추가 작업 중이거나 설명이 필요하면 코멘트 남겨주세요\n- 👀 **리뷰어 / 팀원**: 이미 봤다면 LGTM 한 줄도 좋아요!\n\n바쁘실 수 있다는 점 이해합니다 🙏\n다만 작은 한 줄이 PR 흐름을 살려줘요 🙂'
gh api --method POST \
"repos/${REPO_FULL}/issues/${PR_NUMBER}/comments" \
-f body="$COMMENT_BODY" > /dev/null 2>&1 || true
fi
else
# 활성 PR인데 remind 라벨이 있으면 제거
if [ "$HAS_REMIND" = "true" ]; then
echo "🔄 라벨 제거: ${REPO_FULL}#${PR_NUMBER}"
gh api --method DELETE \
"repos/${REPO_FULL}/issues/${PR_NUMBER}/labels/${REMIND_LABEL}" \
> /dev/null 2>&1 || true
FRESH_PRS_WITH_LABEL=$(echo "$FRESH_PRS_WITH_LABEL" | jq --argjson pr "$PR" '. + [$pr]')
fi
fi
done
STALE_COUNT=$(echo "$STALE_PRS" | jq 'length')
FRESH_REMOVED=$(echo "$FRESH_PRS_WITH_LABEL" | jq 'length')
echo ""
echo "========================================="
echo " 결과 요약"
echo "========================================="
echo " 전체 Open PR: ${PR_COUNT}"
echo " 비활성 (리마인드): ${STALE_COUNT}"
echo " 라벨 제거 (활성): ${FRESH_REMOVED}"
echo "========================================="
# 다음 스텝에서 사용할 수 있도록 출력
echo "stale_count=${STALE_COUNT}" >> $GITHUB_OUTPUT
# JSON 데이터를 파일로 저장 (Slack 메시지 구성용)
echo "$STALE_PRS" > /tmp/stale_prs.json
- name: Send Slack reminder
if: steps.check-prs.outputs.stale_count != '0'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
QUIET_HOURS: ${{ vars.QUIET_HOURS }}
QUIET_TIMEZONE: ${{ vars.QUIET_TIMEZONE }}
run: |
set -euo pipefail
if [ -z "${SLACK_WEBHOOK_URL:-}" ]; then
echo "⚠️ SLACK_WEBHOOK_URL이 설정되지 않아 슬랙 알림을 스킵합니다."
exit 0
fi
# Quiet Hours 체크 (설정된 시간대에는 Slack 알림 스킵)
QUIET_HOURS="${QUIET_HOURS:-}"
QUIET_TZ="${QUIET_TIMEZONE:-UTC}"
if [ -n "$QUIET_HOURS" ]; then
QUIET_START=$(echo "$QUIET_HOURS" | cut -d'-' -f1 | sed 's/^0*//')
QUIET_END=$(echo "$QUIET_HOURS" | cut -d'-' -f2 | sed 's/^0*//')
CURRENT_HOUR=$(TZ="$QUIET_TZ" date +%H | sed 's/^0*//')
# 00시 처리 (sed로 빈 문자열이 되는 경우)
QUIET_START=${QUIET_START:-0}
QUIET_END=${QUIET_END:-0}
CURRENT_HOUR=${CURRENT_HOUR:-0}
if [ "$QUIET_START" -le "$QUIET_END" ]; then
# 일반 범위: 00-06 → 0시~5시
IN_QUIET=$(( CURRENT_HOUR >= QUIET_START && CURRENT_HOUR < QUIET_END ))
else
# 자정 경과 범위: 22-06 → 22시~5시
IN_QUIET=$(( CURRENT_HOUR >= QUIET_START || CURRENT_HOUR < QUIET_END ))
fi
if [ "$IN_QUIET" -eq 1 ]; then
echo "🌙 Quiet Hours (${QUIET_HOURS} ${QUIET_TZ}) - Slack 알림을 스킵합니다."
exit 0
fi
fi
STALE_PRS=$(cat /tmp/stale_prs.json)
STALE_COUNT=$(echo "$STALE_PRS" | jq 'length')
# 레포별로 그룹핑하여 PR 목록 생성
PR_LIST=$(echo "$STALE_PRS" | jq -r '
group_by(.repo) |
map(
"📂 *" + .[0].repo + "*\n" +
(map(" - <" + .html_url + "|#" + (.number | tostring) + "> " + .title + " 👤 " + .user) | join("\n"))
) | join("\n\n")')
MESSAGE="*👀 PR Reminder*
최근 *업데이트가 없는 PR*이 *${STALE_COUNT}건* 있어요 ⏳
짧게라도 확인 부탁드려요!
━━━━━━━━━━━━━━
${PR_LIST}
━━━━━━━━━━━━━━
👉 *PR 작성자*: 진행 상황 공유 또는 추가 설명 부탁드려요
👉 *리뷰어 / 팀원*: 이미 봤다면 👍 LGTM 한 줄이면 충분해요!
🙏 작은 액션 하나가 PR 흐름을 살려줘요"
# Slack 전송 (jq로 JSON escape 처리)
curl -fsS -X POST -H 'Content-type: application/json' \
--data "$(jq -n --arg text "$MESSAGE" '{text: $text}')" \
"$SLACK_WEBHOOK_URL"
echo "✅ Slack 리마인드 메시지 전송 완료 (${STALE_COUNT}건)"