22set -euo pipefail
33
44# Unified Inactivity Bot (Phase 1 + Phase 2)
5- # Supports DRY_RUN=1 mode.
5+ # Supports DRY_RUN mode:
6+ # DRY_RUN = 1 → simulate only (no changes)
7+ # DRY_RUN = 0 → real actions
68
79REPO=" ${REPO:- ${GITHUB_REPOSITORY:- } } "
810DAYS=" ${DAYS:- 21} "
911DRY_RUN=" ${DRY_RUN:- 0} "
1012
13+ # Normalize DRY_RUN input ("true"/"false" → 1/0)
14+ shopt -s nocasematch
15+ case " $DRY_RUN " in
16+ " true" ) DRY_RUN=1 ;;
17+ " false" ) DRY_RUN=0 ;;
18+ esac
19+ shopt -u nocasematch
20+
1121if [[ -z " $REPO " ]]; then
1222 echo " ERROR: REPO environment variable not set."
1323 exit 1
@@ -17,11 +27,12 @@ echo "------------------------------------------------------------"
1727echo " Unified Inactivity Unassign Bot"
1828echo " Repo: $REPO "
1929echo " Threshold: $DAYS days"
20- echo " Dry Run : $DRY_RUN "
30+ echo " DRY_RUN : $DRY_RUN "
2131echo " ------------------------------------------------------------"
2232
2333NOW_TS=$( date +%s)
2434
35+ # Converts GitHub timestamps → epoch
2536parse_ts () {
2637 local ts=" $1 "
2738 if date --version > /dev/null 2>&1 ; then
@@ -31,9 +42,13 @@ parse_ts() {
3142 fi
3243}
3344
34- ISSUES=$( gh api " repos/$REPO /issues" \
35- --paginate \
36- --jq ' .[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not)) | .number' )
45+ # Fetch all open issues that have assignees
46+ ISSUES=$(
47+ gh api " repos/$REPO /issues" --paginate \
48+ --jq ' .[]
49+ | select(.state=="open" and (.assignees|length>0) and (.pull_request|not))
50+ | .number'
51+ )
3752
3853for ISSUE in $ISSUES ; do
3954 echo " ============================================================"
@@ -48,11 +63,13 @@ for ISSUE in $ISSUES; do
4863 for USER in $ASSIGNEES ; do
4964 echo " → Checking assignee: $USER "
5065
51- # timeline check with cross-repo filtering
66+ # Fetch timeline (for PR cross-references)
5267 TIMELINE=$( gh api \
5368 -H " Accept: application/vnd.github.mockingbird-preview+json" \
54- " repos/$REPO /issues/$ISSUE /timeline" )
69+ " repos/$REPO /issues/$ISSUE /timeline"
70+ )
5571
72+ # Filter only PRs from SAME repository
5673 PR_NUMBERS=$( echo " $TIMELINE " | jq -r --arg repo " $REPO " '
5774 .[]
5875 | select(.event == "cross-referenced")
@@ -62,20 +79,20 @@ for ISSUE in $ISSUES; do
6279 ' )
6380
6481 # -------------------------------
65- # PHASE 1 — No PR linked
82+ # PHASE 1: ISSUE HAS NO PR
6683 # -------------------------------
6784 if [[ -z " $PR_NUMBERS " ]]; then
6885 AGE_DAYS=$(( (NOW_TS - CREATED_TS) / 86400 ))
6986 echo " [INFO] Assigned for: ${AGE_DAYS} days"
7087
71- if [[ " $ AGE_DAYS" -ge " $ DAYS" ]] ; then
72- echo " [PHASE 1 STALE] No PR & stale assignment "
88+ if (( AGE_DAYS >= DAYS )) ; then
89+ echo " [PHASE 1 STALE] No PR linked + stale "
7390
74- if [[ " $ DRY_RUN" -eq 0 ]] ; then
91+ if (( DRY_RUN == 0 )) ; then
7592 gh issue edit " $ISSUE " --repo " $REPO " --remove-assignee " $USER "
76- echo " [ACTION] Unassigned $USER (no PR & stale) "
93+ echo " [ACTION] Unassigned $USER "
7794 else
78- echo " [DRY RUN] Would unassign $USER (no PR & stale) "
95+ echo " [DRY RUN] Would unassign $USER "
7996 fi
8097 else
8198 echo " [KEEP] Not stale yet"
@@ -85,13 +102,13 @@ for ISSUE in $ISSUES; do
85102 fi
86103
87104 # -------------------------------
88- # PHASE 2 — Stale PR detection
105+ # PHASE 2: ISSUE HAS PR(s)
89106 # -------------------------------
90107 echo " [INFO] Linked PRs: $PR_NUMBERS "
91108
92109 for PR_NUM in $PR_NUMBERS ; do
93110
94- # Check PR existence safely
111+ # Safe PR check
95112 if ! PR_STATE=$( gh pr view " $PR_NUM " --repo " $REPO " --json state --jq ' .state' 2> /dev/null) ; then
96113 echo " [SKIP] #$PR_NUM is not a valid PR in $REPO "
97114 continue
@@ -102,23 +119,22 @@ for ISSUE in $ISSUES; do
102119 continue
103120 fi
104121
105- # Last commit (pagination + last)
122+ # Last commit (paginate + last)
106123 COMMITS=$( gh api " repos/$REPO /pulls/$PR_NUM /commits" --paginate)
107- LAST=$( echo " $COMMITS " | jq -r ' last | (.commit.committer.date // .commit.author.date)' )
124+ LAST_TS_STR=$( echo " $COMMITS " | jq -r ' last | (.commit.committer.date // .commit.author.date)' )
125+ LAST_TS=$( parse_ts " $LAST_TS_STR " )
108126
109- LAST_TS=$( parse_ts " $LAST " )
110127 AGE_DAYS=$(( (NOW_TS - LAST_TS) / 86400 ))
111128
112- echo " [INFO] PR #$PR_NUM → Last commit = $LAST (~${AGE_DAYS} days)"
129+ echo " [INFO] PR #$PR_NUM → Last commit = $LAST_TS_STR (~${AGE_DAYS} days)"
113130
114- if [[ " $ AGE_DAYS" -ge " $ DAYS" ]] ; then
115- echo " [STALE PR] $PR_NUM by $USER is stale"
131+ if (( AGE_DAYS >= DAYS )) ; then
132+ echo " [STALE PR] PR # $PR_NUM is stale"
116133
117- if [[ " $DRY_RUN " -eq 0 ]]; then
118- # comment + close + unassign
134+ if (( DRY_RUN == 0 )) ; then
119135 gh pr close " $PR_NUM " --repo " $REPO "
120136 gh issue edit " $ISSUE " --repo " $REPO " --remove-assignee " $USER "
121- echo " [ACTION] Closed PR + unassigned user "
137+ echo " [ACTION] Closed PR + unassigned $USER "
122138 else
123139 echo " [DRY RUN] Would close PR #$PR_NUM + unassign $USER "
124140 fi
134150
135151echo " ------------------------------------------------------------"
136152echo " Unified Inactivity Bot Complete"
137- echo " DRY MODE : $DRY_RUN "
153+ echo " DRY_RUN : $DRY_RUN "
138154echo " ------------------------------------------------------------"
0 commit comments