Skip to content

Commit 73eb47a

Browse files
committed
fix(bot): use assignment age and improve logging
Signed-off-by: Akshat Kumar <akshat230405@gmail.com>
1 parent fd6e3b0 commit 73eb47a

File tree

1 file changed

+99
-44
lines changed

1 file changed

+99
-44
lines changed

.github/scripts/inactivity_unassign.sh

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
set -euo pipefail
33

44
# Unified Inactivity Bot (Phase 1 + Phase 2)
5-
# Supports DRY_RUN mode:
6-
# DRY_RUN = 1 → simulate only (no changes)
7-
# DRY_RUN = 0 → real actions
5+
# DRY_RUN:
6+
# 1 → simulate only (no changes)
7+
# 0 → real actions
88

99
REPO="${REPO:-${GITHUB_REPOSITORY:-}}"
1010
DAYS="${DAYS:-21}"
1111
DRY_RUN="${DRY_RUN:-0}"
1212

13-
# Normalize DRY_RUN input ("true"/"false" → 1/0)
13+
# Normalise DRY_RUN input ("true"/"false" → 1/0, case-insensitive)
1414
shopt -s nocasematch
1515
case "$DRY_RUN" in
1616
"true") DRY_RUN=1 ;;
@@ -32,120 +32,175 @@ echo "------------------------------------------------------------"
3232

3333
NOW_TS=$(date +%s)
3434

35-
# Converts GitHub timestamps → epoch
35+
# Convert GitHub timestamp → unix epoch
3636
parse_ts() {
3737
local ts="$1"
3838
if date --version >/dev/null 2>&1; then
39+
# GNU date
3940
date -d "$ts" +%s
4041
else
42+
# macOS / BSD
4143
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s"
4244
fi
4345
}
4446

45-
# Fetch all open issues that have assignees
47+
# Fetch all open issues with assignees (non-PRs)
4648
ISSUES=$(
4749
gh api "repos/$REPO/issues" --paginate \
4850
--jq '.[]
4951
| select(.state=="open" and (.assignees|length>0) and (.pull_request|not))
5052
| .number'
5153
)
5254

55+
if [[ -z "$ISSUES" ]]; then
56+
echo "[INFO] No open issues with assignees found."
57+
fi
58+
5359
for ISSUE in $ISSUES; do
5460
echo "============================================================"
5561
echo " ISSUE #$ISSUE"
5662
echo "============================================================"
5763

5864
ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE")
65+
ISSUE_CREATED_AT=$(echo "$ISSUE_JSON" | jq -r '.created_at')
66+
ISSUE_CREATED_TS=$(parse_ts "$ISSUE_CREATED_AT")
67+
5968
ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login')
60-
CREATED_AT=$(echo "$ISSUE_JSON" | jq -r '.created_at')
61-
CREATED_TS=$(parse_ts "$CREATED_AT")
69+
70+
echo " [INFO] Issue created at: $ISSUE_CREATED_AT"
71+
72+
# Fetch timeline once per issue (used for assignment + PR links)
73+
TIMELINE=$(gh api \
74+
-H "Accept: application/vnd.github.mockingbird-preview+json" \
75+
"repos/$REPO/issues/$ISSUE/timeline"
76+
)
6277

6378
for USER in $ASSIGNEES; do
79+
echo
6480
echo " → Checking assignee: $USER"
6581

66-
# Fetch timeline (for PR cross-references)
67-
TIMELINE=$(gh api \
68-
-H "Accept: application/vnd.github.mockingbird-preview+json" \
69-
"repos/$REPO/issues/$ISSUE/timeline"
82+
# -------------------------------
83+
# Determine assignment time for USER
84+
# -------------------------------
85+
ASSIGNED_AT_STR=$(
86+
echo "$TIMELINE" | jq -r --arg user "$USER" '
87+
[ .[]
88+
| select(.event=="assigned" and .assignee.login==$user)
89+
| .created_at
90+
]
91+
| sort
92+
| last // "null"
93+
'
7094
)
7195

72-
# Filter only PRs from SAME repository
73-
PR_NUMBERS=$(echo "$TIMELINE" | jq -r --arg repo "$REPO" '
74-
.[]
75-
| select(.event == "cross-referenced")
76-
| select(.source.issue.pull_request != null)
77-
| select(.source.issue.repository.full_name == $repo)
78-
| .source.issue.number
79-
')
96+
ASSIGNMENT_SOURCE="assignment_event"
97+
98+
if [[ -z "$ASSIGNED_AT_STR" || "$ASSIGNED_AT_STR" == "null" ]]; then
99+
# Fallback: no explicit assignment event -> use issue creation time
100+
ASSIGNED_AT_STR="$ISSUE_CREATED_AT"
101+
ASSIGNMENT_SOURCE="issue_created_at (no explicit assignment event)"
102+
fi
103+
104+
ASSIGNED_TS=$(parse_ts "$ASSIGNED_AT_STR")
105+
ASSIGNED_AGE_DAYS=$(( (NOW_TS - ASSIGNED_TS) / 86400 ))
106+
107+
echo " [INFO] Assignment source: $ASSIGNMENT_SOURCE"
108+
echo " [INFO] Assigned at: $ASSIGNED_AT_STR (~${ASSIGNED_AGE_DAYS} days ago)"
80109

81110
# -------------------------------
82-
# PHASE 1: ISSUE HAS NO PR
111+
# Find linked PRs for THIS user in THIS repo
83112
# -------------------------------
113+
PR_NUMBERS=$(
114+
echo "$TIMELINE" | jq -r --arg repo "$REPO" --arg user "$USER" '
115+
.[]
116+
| select(.event == "cross-referenced")
117+
| select(.source.issue.pull_request != null)
118+
| select(.source.issue.repository.full_name == $repo)
119+
| select(.source.issue.user.login == $user)
120+
| .source.issue.number
121+
'
122+
)
123+
84124
if [[ -z "$PR_NUMBERS" ]]; then
85-
AGE_DAYS=$(( (NOW_TS - CREATED_TS) / 86400 ))
86-
echo " [INFO] Assigned for: ${AGE_DAYS} days"
125+
echo " [INFO] Linked PRs: none"
126+
else
127+
echo " [INFO] Linked PRs: $PR_NUMBERS"
128+
fi
87129

88-
if (( AGE_DAYS >= DAYS )); then
89-
echo " [PHASE 1 STALE] No PR linked + stale"
130+
# ============================================================
131+
# PHASE 1: ISSUE HAS NO PR FOR THIS USER
132+
# ============================================================
133+
if [[ -z "$PR_NUMBERS" ]]; then
134+
if (( ASSIGNED_AGE_DAYS >= DAYS )); then
135+
echo " [RESULT] Phase 1 → no PR linked + stale (>= $DAYS days)"
90136

91137
if (( DRY_RUN == 0 )); then
92138
gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER"
93-
echo " [ACTION] Unassigned $USER"
139+
echo " [ACTION] Unassigned @$USER from issue #$ISSUE"
94140
else
95-
echo " [DRY RUN] Would unassign $USER"
141+
echo " [DRY RUN] Would unassign @$USER from issue #$ISSUE"
96142
fi
97143
else
98-
echo " [KEEP] Not stale yet"
144+
echo " [RESULT] Phase 1 → no PR linked but not stale (< $DAYS days) → KEEP"
99145
fi
100146

147+
# No PRs means no Phase 2 work required for this user
101148
continue
102149
fi
103150

104-
# -------------------------------
105-
# PHASE 2: ISSUE HAS PR(s)
106-
# -------------------------------
107-
echo " [INFO] Linked PRs: $PR_NUMBERS"
151+
# ============================================================
152+
# PHASE 2: ISSUE HAS PR(s) → check last commit activity
153+
# ============================================================
154+
PHASE2_TOOK_ACTION=0
108155

109156
for PR_NUM in $PR_NUMBERS; do
110-
111-
# Safe PR check
157+
# Ensure PR exists in this repo
112158
if ! PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null); then
113159
echo " [SKIP] #$PR_NUM is not a valid PR in $REPO"
114160
continue
115161
fi
116162

163+
echo " [INFO] PR #$PR_NUM state: $PR_STATE"
164+
117165
if [[ "$PR_STATE" != "OPEN" ]]; then
118166
echo " [SKIP] PR #$PR_NUM is not open"
119167
continue
120168
fi
121169

122-
# Last commit (paginate + last)
170+
# Fetch all commits & take the last one (API order + paginate)
123171
COMMITS=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate)
124172
LAST_TS_STR=$(echo "$COMMITS" | jq -r 'last | (.commit.committer.date // .commit.author.date)')
125173
LAST_TS=$(parse_ts "$LAST_TS_STR")
174+
PR_AGE_DAYS=$(( (NOW_TS - LAST_TS) / 86400 ))
126175

127-
AGE_DAYS=$(( (NOW_TS - LAST_TS) / 86400 ))
128-
129-
echo " [INFO] PR #$PR_NUM → Last commit = $LAST_TS_STR (~${AGE_DAYS} days)"
176+
echo " [INFO] PR #$PR_NUM last commit: $LAST_TS_STR (~${PR_AGE_DAYS} days ago)"
130177

131-
if (( AGE_DAYS >= DAYS )); then
132-
echo " [STALE PR] PR #$PR_NUM is stale"
178+
if (( PR_AGE_DAYS >= DAYS )); then
179+
echo " [RESULT] Phase 2 → PR #$PR_NUM is stale (>= $DAYS days since last commit)"
180+
PHASE2_TOOK_ACTION=1
133181

134182
if (( DRY_RUN == 0 )); then
135183
gh pr close "$PR_NUM" --repo "$REPO"
136184
gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER"
137-
echo " [ACTION] Closed PR + unassigned $USER"
185+
echo " [ACTION] Closed PR #$PR_NUM and unassigned @$USER from issue #$ISSUE"
138186
else
139-
echo " [DRY RUN] Would close PR #$PR_NUM + unassign $USER"
187+
echo " [DRY RUN] Would close PR #$PR_NUM and unassign @$USER from issue #$ISSUE"
140188
fi
189+
190+
# Per current spec, first stale PR per user/issue is enough
191+
break
141192
else
142-
echo " [KEEP] PR is active"
193+
echo " [INFO] PR #$PR_NUM is active (< $DAYS days) → KEEP"
143194
fi
144-
145195
done
146196

197+
if (( PHASE2_TOOK_ACTION == 0 )); then
198+
echo " [RESULT] Phase 2 → all linked PRs active or not applicable → KEEP"
199+
fi
200+
147201
done
148202

203+
echo
149204
done
150205

151206
echo "------------------------------------------------------------"

0 commit comments

Comments
 (0)