Skip to content

Commit 7d15228

Browse files
committed
chore: test PR for inactivity phase 2
Signed-off-by: Akshat Kumar <akshat230405@gmail.com>
1 parent 5ec352f commit 7d15228

File tree

5 files changed

+343
-2
lines changed

5 files changed

+343
-2
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# DRY-RUN: Phase 2 Inactivity Unassign Bot
5+
# - Does NOT change anything
6+
# - Logs which PRs/issues WOULD be affected
7+
8+
REPO="${REPO:-}"
9+
DAYS="${DAYS:-21}"
10+
11+
if [ -z "$REPO" ]; then
12+
echo "ERROR: REPO environment variable not set."
13+
echo "Example: export REPO=owner/repo"
14+
exit 1
15+
fi
16+
17+
echo "------------------------------------------------------------"
18+
echo " DRY RUN: Phase 2 Inactivity Unassign (PR inactivity)"
19+
echo " Repo: $REPO"
20+
echo " Threshold: $DAYS days (no commit activity on PR)"
21+
echo "------------------------------------------------------------"
22+
23+
NOW_TS=$(date +%s)
24+
25+
parse_ts() {
26+
local ts="$1"
27+
if date --version >/dev/null 2>&1; then
28+
date -d "$ts" +%s
29+
else
30+
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s"
31+
fi
32+
}
33+
34+
declare -a SUMMARY=()
35+
36+
ISSUES=$(gh api "repos/$REPO/issues" \
37+
--paginate \
38+
--jq '.[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not)) | .number')
39+
40+
if [ -z "$ISSUES" ]; then
41+
echo "No open issues with assignees found."
42+
exit 0
43+
fi
44+
45+
echo "[INFO] Found issues: $ISSUES"
46+
echo
47+
48+
for ISSUE in $ISSUES; do
49+
echo "============================================================"
50+
echo " ISSUE #$ISSUE"
51+
echo "============================================================"
52+
53+
ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE")
54+
ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login')
55+
56+
if [ -z "$ASSIGNEES" ]; then
57+
echo "[INFO] No assignees? Skipping."
58+
echo
59+
continue
60+
fi
61+
62+
echo "[INFO] Assignees: $ASSIGNEES"
63+
echo
64+
65+
for USER in $ASSIGNEES; do
66+
echo " → Checking assignee: $USER"
67+
68+
PR_NUMBERS=$(gh api \
69+
-H "Accept: application/vnd.github.mockingbird-preview+json" \
70+
"repos/$REPO/issues/$ISSUE/timeline" \
71+
--jq ".[]
72+
| select(.event == \"cross-referenced\")
73+
| select(.source.issue.pull_request != null)
74+
| select(.source.issue.user.login == \"$USER\")
75+
| .source.issue.number")
76+
77+
if [ -z "$PR_NUMBERS" ]; then
78+
echo " [INFO] No linked PRs by $USER for this issue."
79+
echo
80+
continue
81+
fi
82+
83+
echo " [INFO] Linked PRs by $USER: $PR_NUMBERS"
84+
85+
STALE_PR=""
86+
STALE_AGE_DAYS=0
87+
88+
for PR_NUM in $PR_NUMBERS; do
89+
PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state')
90+
91+
if [ "$PR_STATE" != "OPEN" ]; then
92+
echo " [SKIP] PR #$PR_NUM is not open ($PR_STATE)."
93+
continue
94+
fi
95+
96+
LAST_COMMIT_DATE=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" \
97+
--jq '.[-1].commit.committer.date // .[-1].commit.author.date' 2>/dev/null || echo "")
98+
99+
if [ -z "$LAST_COMMIT_DATE" ]; then
100+
echo " [WARN] Could not determine last commit date for PR #$PR_NUM, skipping."
101+
continue
102+
fi
103+
104+
LAST_COMMIT_TS=$(parse_ts "$LAST_COMMIT_DATE")
105+
AGE_DAYS=$(( (NOW_TS - LAST_COMMIT_TS) / 86400 ))
106+
107+
echo " [INFO] PR #$PR_NUM last commit: $LAST_COMMIT_DATE (~${AGE_DAYS} days ago)"
108+
109+
if [ "$AGE_DAYS" -ge "$DAYS" ]; then
110+
STALE_PR="$PR_NUM"
111+
STALE_AGE_DAYS="$AGE_DAYS"
112+
break
113+
fi
114+
done
115+
116+
if [ -z "$STALE_PR" ]; then
117+
echo " [KEEP] No OPEN PR for $USER is stale (>= $DAYS days)."
118+
echo
119+
continue
120+
fi
121+
122+
echo " [DRY RUN] Would CLOSE PR #$STALE_PR (no commits for $STALE_AGE_DAYS days)"
123+
echo " [DRY RUN] Would UNASSIGN @$USER from issue #$ISSUE"
124+
echo
125+
126+
SUMMARY+=("Issue #$ISSUE → user @$USER → stale PR #$STALE_PR (no commits for $STALE_AGE_DAYS days)")
127+
done
128+
129+
echo
130+
done
131+
132+
if [ ${#SUMMARY[@]} -gt 0 ]; then
133+
echo "============================================================"
134+
echo " SUMMARY: Actions that WOULD be taken (no changes made)"
135+
echo "============================================================"
136+
for ITEM in "${SUMMARY[@]}"; do
137+
echo " - $ITEM"
138+
done
139+
else
140+
echo "No stale PRs / unassignments detected in this dry-run."
141+
fi
142+
143+
echo "------------------------------------------------------------"
144+
echo " DRY RUN COMPLETE — No changes were made."
145+
echo "------------------------------------------------------------"
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Env:
5+
# GH_TOKEN - provided by GitHub Actions
6+
# REPO - owner/repo (fallback to GITHUB_REPOSITORY)
7+
# DAYS - inactivity threshold in days (default 21)
8+
9+
REPO="${REPO:-${GITHUB_REPOSITORY:-}}"
10+
DAYS="${DAYS:-21}"
11+
12+
if [ -z "$REPO" ]; then
13+
echo "ERROR: REPO environment variable not set."
14+
exit 1
15+
fi
16+
17+
echo "------------------------------------------------------------"
18+
echo " Inactivity Unassign Bot (Phase 2 - PR inactivity)"
19+
echo " Repo: $REPO"
20+
echo " Threshold: $DAYS days (no commit activity on PR)"
21+
echo "------------------------------------------------------------"
22+
23+
NOW_TS=$(date +%s)
24+
25+
# Cross-platform timestamp parsing (Linux + macOS/BSD)
26+
parse_ts() {
27+
local ts="$1"
28+
if date --version >/dev/null 2>&1; then
29+
# GNU date (Linux)
30+
date -d "$ts" +%s
31+
else
32+
# macOS / BSD
33+
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s"
34+
fi
35+
}
36+
37+
# Fetch open ISSUES (not PRs) that have assignees
38+
ISSUES=$(gh api "repos/$REPO/issues" \
39+
--paginate \
40+
--jq '.[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not)) | .number')
41+
42+
if [ -z "$ISSUES" ]; then
43+
echo "No open issues with assignees found."
44+
exit 0
45+
fi
46+
47+
for ISSUE in $ISSUES; do
48+
echo "============================================================"
49+
echo " ISSUE #$ISSUE"
50+
echo "============================================================"
51+
52+
ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE")
53+
ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login')
54+
55+
if [ -z "$ASSIGNEES" ]; then
56+
echo "[INFO] No assignees? Skipping."
57+
echo
58+
continue
59+
fi
60+
61+
echo "[INFO] Assignees: $ASSIGNEES"
62+
echo
63+
64+
for USER in $ASSIGNEES; do
65+
echo " → Checking assignee: $USER"
66+
67+
# Find OPEN PRs linked to THIS issue, authored by THIS user
68+
PR_NUMBERS=$(gh api \
69+
-H "Accept: application/vnd.github.mockingbird-preview+json" \
70+
"repos/$REPO/issues/$ISSUE/timeline" \
71+
--jq ".[]
72+
| select(.event == \"cross-referenced\")
73+
| select(.source.issue.pull_request != null)
74+
| select(.source.issue.user.login == \"$USER\")
75+
| .source.issue.number")
76+
77+
if [ -z "$PR_NUMBERS" ]; then
78+
echo " [INFO] No linked PRs by $USER for this issue → Phase 1 covers the no-PR case."
79+
echo
80+
continue
81+
fi
82+
83+
echo " [INFO] Linked PRs by $USER: $PR_NUMBERS"
84+
85+
STALE_PR=""
86+
STALE_AGE_DAYS=0
87+
88+
# Look for a stale OPEN PR
89+
for PR_NUM in $PR_NUMBERS; do
90+
PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state')
91+
92+
if [ "$PR_STATE" != "OPEN" ]; then
93+
echo " [SKIP] PR #$PR_NUM is not open ($PR_STATE)."
94+
continue
95+
fi
96+
97+
# Last commit date on the PR
98+
LAST_COMMIT_DATE=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" \
99+
--jq '.[-1].commit.committer.date // .[-1].commit.author.date' 2>/dev/null || echo "")
100+
101+
if [ -z "$LAST_COMMIT_DATE" ]; then
102+
echo " [WARN] Could not determine last commit date for PR #$PR_NUM, skipping."
103+
continue
104+
fi
105+
106+
LAST_COMMIT_TS=$(parse_ts "$LAST_COMMIT_DATE")
107+
AGE_DAYS=$(( (NOW_TS - LAST_COMMIT_TS) / 86400 ))
108+
109+
echo " [INFO] PR #$PR_NUM last commit: $LAST_COMMIT_DATE (~${AGE_DAYS} days ago)"
110+
111+
if [ "$AGE_DAYS" -ge "$DAYS" ]; then
112+
STALE_PR="$PR_NUM"
113+
STALE_AGE_DAYS="$AGE_DAYS"
114+
break
115+
fi
116+
done
117+
118+
if [ -z "$STALE_PR" ]; then
119+
echo " [KEEP] No OPEN PR for $USER is stale (>= $DAYS days)."
120+
echo
121+
continue
122+
fi
123+
124+
echo " [STALE] PR #$STALE_PR by $USER has had no commit activity for $STALE_AGE_DAYS days (>= $DAYS)."
125+
echo " [ACTION] Closing PR #$STALE_PR and unassigning @$USER from issue #$ISSUE."
126+
127+
MESSAGE=$(
128+
cat <<EOF
129+
Hi @$USER, this is InactivityBot.
130+
131+
This pull request has become stale, with no development activity for **$STALE_AGE_DAYS days**. As a result, we have closed this pull request and unassigned you from the linked issue to keep the backlog available for active contributors.
132+
133+
You are welcome to get assigned to an issue once again once you have capacity.
134+
135+
In the future, please close old pull requests that will not have development activity and request to be unassigned if you are no longer working on the issue.
136+
EOF
137+
)
138+
139+
# Comment on the PR
140+
gh pr comment "$STALE_PR" --repo "$REPO" --body "$MESSAGE"
141+
echo " [DONE] Commented on PR #$STALE_PR."
142+
143+
# Close the PR
144+
gh pr close "$STALE_PR" --repo "$REPO"
145+
echo " [DONE] Closed PR #$STALE_PR."
146+
147+
# Unassign the user from the issue
148+
gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER"
149+
echo " [DONE] Unassigned @$USER from issue #$ISSUE."
150+
echo
151+
done
152+
153+
echo
154+
done
155+
156+
echo "------------------------------------------------------------"
157+
echo " Inactivity Unassign Bot (Phase 2) complete."
158+
echo "------------------------------------------------------------"

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
permissions:
99
contents: read
1010
issues: write
11-
pull-requests: read
11+
pull-requests: write
1212

1313
jobs:
1414
inactivity-unassign:
@@ -17,14 +17,22 @@ jobs:
1717
steps:
1818
- name: Checkout repository
1919
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
20+
2021
- name: Harden the runner
2122
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2
2223
with:
2324
egress-policy: audit
2425

25-
- name: Unassign inactive assignees with NO PRs (Phase 1)
26+
- name: Phase 1 – unassign assignees with NO PRs
2627
env:
2728
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2829
REPO: ${{ github.repository }}
2930
DAYS: 21
3031
run: bash .github/scripts/inactivity_unassign_phase1.sh
32+
33+
- name: Phase 2 – unassign assignees with stale PRs
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
REPO: ${{ github.repository }}
37+
DAYS: 21
38+
run: bash .github/scripts/inactivity_unassign_phase2.sh
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: bot-inactivity-unassign-phase2
2+
3+
on:
4+
schedule:
5+
- cron: "0 12 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
inactivity-unassign-phase2:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
20+
21+
- name: Harden the runner
22+
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2
23+
24+
- name: Unassign stale PR contributors (Phase 2)
25+
env:
26+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
REPO: ${{ github.repository }}
28+
DAYS: 21
29+
run: bash .github/scripts/inactivity_unassign_phase2.sh

test-phase2.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# test

0 commit comments

Comments
 (0)