Skip to content

Commit e649fe1

Browse files
committed
fix(bot): restore comments and improve inactivity logging
Signed-off-by: Akshat Kumar <akshat230405@gmail.com>
1 parent 73eb47a commit e649fe1

File tree

2 files changed

+86
-77
lines changed

2 files changed

+86
-77
lines changed

.github/scripts/inactivity_unassign.sh

Lines changed: 81 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
set -euo pipefail
33

44
# Unified Inactivity Bot (Phase 1 + Phase 2)
5-
# DRY_RUN:
6-
# 1 → simulate only (no changes)
7-
# 0 → real actions
5+
# DRY_RUN controls behaviour:
6+
# DRY_RUN = 1 → simulate only (no changes, just logs)
7+
# DRY_RUN = 0 → real actions (comments, closes, unassigns)
88

99
REPO="${REPO:-${GITHUB_REPOSITORY:-}}"
1010
DAYS="${DAYS:-21}"
@@ -25,91 +25,82 @@ fi
2525

2626
echo "------------------------------------------------------------"
2727
echo " Unified Inactivity Unassign Bot"
28-
echo " Repo: $REPO"
29-
echo " Threshold: $DAYS days"
30-
echo " DRY_RUN: $DRY_RUN"
28+
echo " Repo: $REPO"
29+
echo " Threshold $DAYS days"
30+
echo " DRY_RUN: $DRY_RUN"
3131
echo "------------------------------------------------------------"
3232

3333
NOW_TS=$(date +%s)
3434

35-
# Convert GitHub timestamp → unix epoch
35+
# Convert GitHub ISO timestamp → epoch seconds
3636
parse_ts() {
3737
local ts="$1"
3838
if date --version >/dev/null 2>&1; then
39-
# GNU date
39+
# GNU date (Linux)
4040
date -d "$ts" +%s
4141
else
42-
# macOS / BSD
42+
# BSD / macOS
4343
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s"
4444
fi
4545
}
4646

47-
# Fetch all open issues with assignees (non-PRs)
47+
# Fetch all open issues with assignees (no PRs)
4848
ISSUES=$(
4949
gh api "repos/$REPO/issues" --paginate \
50-
--jq '.[]
51-
| select(.state=="open" and (.assignees|length>0) and (.pull_request|not))
50+
--jq '.[]
51+
| select(.state=="open" and (.assignees | length > 0) and (.pull_request | not))
5252
| .number'
5353
)
5454

55-
if [[ -z "$ISSUES" ]]; then
56-
echo "[INFO] No open issues with assignees found."
57-
fi
58-
5955
for ISSUE in $ISSUES; do
6056
echo "============================================================"
6157
echo " ISSUE #$ISSUE"
6258
echo "============================================================"
6359

6460
ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE")
6561
ISSUE_CREATED_AT=$(echo "$ISSUE_JSON" | jq -r '.created_at')
66-
ISSUE_CREATED_TS=$(parse_ts "$ISSUE_CREATED_AT")
67-
6862
ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login')
6963

7064
echo " [INFO] Issue created at: $ISSUE_CREATED_AT"
65+
echo
7166

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"
67+
# Fetch timeline once (used for assignment events + PR links)
68+
TIMELINE=$(
69+
gh api \
70+
-H "Accept: application/vnd.github.mockingbird-preview+json" \
71+
"repos/$REPO/issues/$ISSUE/timeline"
7672
)
7773

7874
for USER in $ASSIGNEES; do
79-
echo
8075
echo " → Checking assignee: $USER"
8176

82-
# -------------------------------
83-
# Determine assignment time for USER
84-
# -------------------------------
85-
ASSIGNED_AT_STR=$(
86-
echo "$TIMELINE" | jq -r --arg user "$USER" '
77+
# Determine assignment timestamp for this user
78+
ASSIGN_EVENT_JSON=$(
79+
echo "$TIMELINE" | jq -c --arg user "$USER" '
8780
[ .[]
88-
| select(.event=="assigned" and .assignee.login==$user)
89-
| .created_at
90-
]
91-
| sort
92-
| last // "null"
81+
| select(.event == "assigned")
82+
| select(.assignee.login == $user)
83+
]
84+
| last // empty
9385
'
9486
)
9587

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)"
88+
if [[ -n "$ASSIGN_EVENT_JSON" && "$ASSIGN_EVENT_JSON" != "null" ]]; then
89+
ASSIGNED_AT=$(echo "$ASSIGN_EVENT_JSON" | jq -r '.created_at')
90+
ASSIGN_SOURCE="assignment_event"
91+
else
92+
# Fallback: use issue creation time when no explicit assignment event
93+
ASSIGNED_AT="$ISSUE_CREATED_AT"
94+
ASSIGN_SOURCE="issue_created_at (no explicit assignment event)"
10295
fi
10396

104-
ASSIGNED_TS=$(parse_ts "$ASSIGNED_AT_STR")
97+
ASSIGNED_TS=$(parse_ts "$ASSIGNED_AT")
10598
ASSIGNED_AGE_DAYS=$(( (NOW_TS - ASSIGNED_TS) / 86400 ))
10699

107-
echo " [INFO] Assignment source: $ASSIGNMENT_SOURCE"
108-
echo " [INFO] Assigned at: $ASSIGNED_AT_STR (~${ASSIGNED_AGE_DAYS} days ago)"
100+
echo " [INFO] Assignment source: $ASSIGN_SOURCE"
101+
echo " [INFO] Assigned at: $ASSIGNED_AT (~${ASSIGNED_AGE_DAYS} days ago)"
109102

110-
# -------------------------------
111-
# Find linked PRs for THIS user in THIS repo
112-
# -------------------------------
103+
# Determine PRs linked to this issue for this user
113104
PR_NUMBERS=$(
114105
echo "$TIMELINE" | jq -r --arg repo "$REPO" --arg user "$USER" '
115106
.[]
@@ -121,40 +112,50 @@ for ISSUE in $ISSUES; do
121112
'
122113
)
123114

115+
# ===========================
116+
# PHASE 1: ISSUE HAS NO PR(s)
117+
# ===========================
124118
if [[ -z "$PR_NUMBERS" ]]; then
125119
echo " [INFO] Linked PRs: none"
126-
else
127-
echo " [INFO] Linked PRs: $PR_NUMBERS"
128-
fi
129120

130-
# ============================================================
131-
# PHASE 1: ISSUE HAS NO PR FOR THIS USER
132-
# ============================================================
133-
if [[ -z "$PR_NUMBERS" ]]; then
134121
if (( ASSIGNED_AGE_DAYS >= DAYS )); then
135-
echo " [RESULT] Phase 1 → no PR linked + stale (>= $DAYS days)"
122+
echo " [RESULT] Phase 1 → stale assignment (>= $DAYS days, no PR)"
136123

137124
if (( DRY_RUN == 0 )); then
125+
MESSAGE=$(
126+
cat <<EOF
127+
Hi @$USER, this is InactivityBot 👋
128+
129+
You were assigned to this issue **${ASSIGNED_AGE_DAYS} days** ago, and there is currently no open pull request linked to it.
130+
To keep the backlog available for active contributors, I'm unassigning you for now.
131+
132+
If you'd like to continue working on this later, feel free to get re-assigned or comment here and we'll gladly assign it back to you. 🙂
133+
EOF
134+
)
135+
136+
gh issue comment "$ISSUE" --repo "$REPO" --body "$MESSAGE"
138137
gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER"
139-
echo " [ACTION] Unassigned @$USER from issue #$ISSUE"
138+
echo " [ACTION] Commented and unassigned @$USER from issue #$ISSUE"
140139
else
141-
echo " [DRY RUN] Would unassign @$USER from issue #$ISSUE"
140+
echo " [DRY RUN] Would comment + unassign @$USER from issue #$ISSUE (Phase 1 stale)"
142141
fi
143142
else
144143
echo " [RESULT] Phase 1 → no PR linked but not stale (< $DAYS days) → KEEP"
145144
fi
146145

147-
# No PRs means no Phase 2 work required for this user
146+
echo
148147
continue
149148
fi
150149

151-
# ============================================================
152-
# PHASE 2: ISSUE HAS PR(s) → check last commit activity
153-
# ============================================================
154-
PHASE2_TOOK_ACTION=0
150+
# ===========================
151+
# PHASE 2: ISSUE HAS PR(s)
152+
# ===========================
153+
echo " [INFO] Linked PRs: $PR_NUMBERS"
154+
155+
PHASE2_TOUCHED=0
155156

156157
for PR_NUM in $PR_NUMBERS; do
157-
# Ensure PR exists in this repo
158+
# Safe PR existence check
158159
if ! PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null); then
159160
echo " [SKIP] #$PR_NUM is not a valid PR in $REPO"
160161
continue
@@ -167,40 +168,48 @@ for ISSUE in $ISSUES; do
167168
continue
168169
fi
169170

170-
# Fetch all commits & take the last one (API order + paginate)
171-
COMMITS=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate)
172-
LAST_TS_STR=$(echo "$COMMITS" | jq -r 'last | (.commit.committer.date // .commit.author.date)')
171+
COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate)
172+
LAST_TS_STR=$(echo "$COMMITS_JSON" | jq -r 'last | (.commit.committer.date // .commit.author.date)')
173173
LAST_TS=$(parse_ts "$LAST_TS_STR")
174174
PR_AGE_DAYS=$(( (NOW_TS - LAST_TS) / 86400 ))
175175

176176
echo " [INFO] PR #$PR_NUM last commit: $LAST_TS_STR (~${PR_AGE_DAYS} days ago)"
177177

178178
if (( PR_AGE_DAYS >= DAYS )); then
179+
PHASE2_TOUCHED=1
179180
echo " [RESULT] Phase 2 → PR #$PR_NUM is stale (>= $DAYS days since last commit)"
180-
PHASE2_TOOK_ACTION=1
181181

182182
if (( DRY_RUN == 0 )); then
183+
MESSAGE=$(
184+
cat <<EOF
185+
Hi @$USER, this is InactivityBot 👋
186+
187+
This pull request has had no new commits for **${PR_AGE_DAYS} days**, so I'm closing it and unassigning you from the linked issue to keep the backlog healthy.
188+
189+
You're very welcome to open a new PR or ask to be re-assigned when you're ready to continue working on this. 🚀
190+
EOF
191+
)
192+
193+
gh pr comment "$PR_NUM" --repo "$REPO" --body "$MESSAGE"
183194
gh pr close "$PR_NUM" --repo "$REPO"
184195
gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER"
185-
echo " [ACTION] Closed PR #$PR_NUM and unassigned @$USER from issue #$ISSUE"
196+
197+
echo " [ACTION] Commented on PR #$PR_NUM, closed it, and unassigned @$USER from issue #$ISSUE"
186198
else
187-
echo " [DRY RUN] Would close PR #$PR_NUM and unassign @$USER from issue #$ISSUE"
199+
echo " [DRY RUN] Would comment, close PR #$PR_NUM, and unassign @$USER from issue #$ISSUE"
188200
fi
189-
190-
# Per current spec, first stale PR per user/issue is enough
191-
break
192201
else
193202
echo " [INFO] PR #$PR_NUM is active (< $DAYS days) → KEEP"
194203
fi
195204
done
196205

197-
if (( PHASE2_TOOK_ACTION == 0 )); then
206+
if (( PHASE2_TOUCHED == 0 )); then
198207
echo " [RESULT] Phase 2 → all linked PRs active or not applicable → KEEP"
199208
fi
200209

210+
echo
201211
done
202212

203-
echo
204213
done
205214

206215
echo "------------------------------------------------------------"

.github/workflows/bot-inactivity-unassign-phase.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
dry_run:
99
description: "Run in dry-run mode (no unassign / close / comment)"
1010
required: true
11-
default: true
11+
default: "true"
1212
type: boolean
1313

1414
permissions:
@@ -25,7 +25,7 @@ jobs:
2525
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
2626

2727
- name: Harden the runner
28-
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76
28+
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2
2929
with:
3030
egress-policy: audit
3131

@@ -34,8 +34,8 @@ jobs:
3434
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3535
REPO: ${{ github.repository }}
3636
DAYS: 21
37-
# Default behaviour:
38-
# - schedule: DRY_RUN=0 (real actions)
39-
# - workflow_dispatch: DRY_RUN comes from the "dry_run" input (true → 1, false → 0)
37+
# Behaviour:
38+
# - schedule: DRY_RUN = 0 (real actions)
39+
# - workflow_dispatch: DRY_RUN derived from the "dry_run" input
4040
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.dry_run == 'true' && '1' || '0') || '0' }}
4141
run: bash .github/scripts/inactivity_unassign.sh

0 commit comments

Comments
 (0)