-
Notifications
You must be signed in to change notification settings - Fork 108
feat(github-actions): add inactivity bot phase 2 (stale PR detection) #989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+320
−3
Merged
Changes from 9 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
7d15228
chore: test PR for inactivity phase 2
Akshat8510 5912895
Set DAYS to 0 for unassigning stale PRs
Akshat8510 9c921c7
Change DAYS from 21 to 0 in unassign workflow
Akshat8510 1e2d911
feat: add inactivity bot phase 2 (stale PR detection)
Akshat8510 2964a1a
Delete test-phase2.txt
Akshat8510 59af4a3
Merge branch 'main' into phase2-final
Akshat8510 121dc2f
Merge branch 'main' into phase2-final
Akshat8510 2261c14
Merge branch 'main' into phase2-final
Akshat8510 3f03d19
fix: improve latest commit detection via timestamp sorting in Phase 2…
Akshat8510 8f593fd
fix: align Phase 2 inactivity bot with paginated last-commit detection
Akshat8510 eec3f26
Merge branch 'main' into phase2-final
Akshat8510 e4daf9f
Update CHANGELOG.md
Akshat8510 6933b02
Update CHANGELOG with recent changes and additions
Akshat8510 cbc5691
chore: remove duplicate Phase 2 workflow
Akshat8510 c4632e2
Merge branch 'main' into phase2-final
Akshat8510 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # DRY-RUN: Phase 2 Inactivity Unassign Bot | ||
| # - Does NOT change anything | ||
| # - Logs which PRs/issues WOULD be affected | ||
|
|
||
| REPO="${REPO:-}" | ||
| DAYS="${DAYS:-21}" | ||
|
|
||
| if [ -z "$REPO" ]; then | ||
| echo "ERROR: REPO environment variable not set." | ||
| echo "Example: export REPO=owner/repo" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "------------------------------------------------------------" | ||
| echo " DRY RUN: Phase 2 Inactivity Unassign (PR inactivity)" | ||
| echo " Repo: $REPO" | ||
| echo " Threshold: $DAYS days (no commit activity on PR)" | ||
| echo "------------------------------------------------------------" | ||
|
|
||
| NOW_TS=$(date +%s) | ||
|
|
||
| parse_ts() { | ||
| local ts="$1" | ||
| if date --version >/dev/null 2>&1; then | ||
| date -d "$ts" +%s | ||
| else | ||
| date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s" | ||
| fi | ||
| } | ||
|
|
||
| declare -a SUMMARY=() | ||
|
|
||
| ISSUES=$(gh api "repos/$REPO/issues" \ | ||
| --paginate \ | ||
| --jq '.[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not)) | .number') | ||
|
|
||
| if [ -z "$ISSUES" ]; then | ||
| echo "No open issues with assignees found." | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "[INFO] Found issues: $ISSUES" | ||
| echo | ||
|
|
||
| for ISSUE in $ISSUES; do | ||
| echo "============================================================" | ||
| echo " ISSUE #$ISSUE" | ||
| echo "============================================================" | ||
|
|
||
| ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE") | ||
| ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login') | ||
|
|
||
| if [ -z "$ASSIGNEES" ]; then | ||
| echo "[INFO] No assignees? Skipping." | ||
| echo | ||
| continue | ||
| fi | ||
|
|
||
| echo "[INFO] Assignees: $ASSIGNEES" | ||
| echo | ||
|
|
||
| for USER in $ASSIGNEES; do | ||
| echo " → Checking assignee: $USER" | ||
|
|
||
| PR_NUMBERS=$(gh api \ | ||
| -H "Accept: application/vnd.github.mockingbird-preview+json" \ | ||
| "repos/$REPO/issues/$ISSUE/timeline" \ | ||
| --jq ".[] | ||
| | select(.event == \"cross-referenced\") | ||
| | select(.source.issue.pull_request != null) | ||
| | select(.source.issue.user.login == \"$USER\") | ||
| | .source.issue.number") | ||
|
|
||
| if [ -z "$PR_NUMBERS" ]; then | ||
| echo " [INFO] No linked PRs by $USER for this issue." | ||
| echo | ||
| continue | ||
| fi | ||
|
|
||
| echo " [INFO] Linked PRs by $USER: $PR_NUMBERS" | ||
|
|
||
| STALE_PR="" | ||
| STALE_AGE_DAYS=0 | ||
|
|
||
| for PR_NUM in $PR_NUMBERS; do | ||
| PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state') | ||
|
|
||
| if [ "$PR_STATE" != "OPEN" ]; then | ||
| echo " [SKIP] PR #$PR_NUM is not open ($PR_STATE)." | ||
| continue | ||
| fi | ||
|
|
||
| # Use the same "max by timestamp" logic as the real bot | ||
| COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" 2>/dev/null || echo "") | ||
|
|
||
| LAST_COMMIT_DATE=$(echo "$COMMITS_JSON" \ | ||
| | jq -r 'max_by(.commit.committer.date // .commit.author.date) | ||
| | (.commit.committer.date // .commit.author.date)' 2>/dev/null || echo "") | ||
|
|
||
| if [ -z "$LAST_COMMIT_DATE" ] || [ "$LAST_COMMIT_DATE" = "null" ]; then | ||
| echo " [WARN] Could not determine last commit date for PR #$PR_NUM, skipping." | ||
| continue | ||
| fi | ||
|
|
||
| LAST_COMMIT_TS=$(parse_ts "$LAST_COMMIT_DATE") | ||
| AGE_DAYS=$(( (NOW_TS - LAST_COMMIT_TS) / 86400 )) | ||
|
|
||
| echo " [INFO] PR #$PR_NUM last commit: $LAST_COMMIT_DATE (~${AGE_DAYS} days ago)" | ||
|
|
||
| if [ "$AGE_DAYS" -ge "$DAYS" ]; then | ||
| STALE_PR="$PR_NUM" | ||
| STALE_AGE_DAYS="$AGE_DAYS" | ||
| break | ||
| fi | ||
| done | ||
|
|
||
| if [ -z "$STALE_PR" ]; then | ||
| echo " [KEEP] No OPEN PR for $USER is stale (>= $DAYS days)." | ||
| echo | ||
| continue | ||
| fi | ||
|
|
||
| echo " [DRY RUN] Would CLOSE PR #$STALE_PR (no commits for $STALE_AGE_DAYS days)" | ||
| echo " [DRY RUN] Would UNASSIGN @$USER from issue #$ISSUE" | ||
| echo | ||
|
|
||
| SUMMARY+=("Issue #$ISSUE → user @$USER → stale PR #$STALE_PR (no commits for $STALE_AGE_DAYS days)") | ||
| done | ||
|
|
||
| echo | ||
| done | ||
|
|
||
| if [ ${#SUMMARY[@]} -gt 0 ]; then | ||
| echo "============================================================" | ||
| echo " SUMMARY: Actions that WOULD be taken (no changes made)" | ||
| echo "============================================================" | ||
| for ITEM in "${SUMMARY[@]}"; do | ||
| echo " - $ITEM" | ||
| done | ||
| else | ||
| echo "No stale PRs / unassignments detected in this dry-run." | ||
| fi | ||
|
|
||
| echo "------------------------------------------------------------" | ||
| echo " DRY RUN COMPLETE — No changes were made." | ||
| echo "------------------------------------------------------------" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Env: | ||
| # GH_TOKEN - provided by GitHub Actions | ||
| # REPO - owner/repo (fallback to GITHUB_REPOSITORY) | ||
| # DAYS - inactivity threshold in days (default 21) | ||
|
|
||
| REPO="${REPO:-${GITHUB_REPOSITORY:-}}" | ||
| DAYS="${DAYS:-21}" | ||
|
|
||
| if [ -z "$REPO" ]; then | ||
| echo "ERROR: REPO environment variable not set." | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "------------------------------------------------------------" | ||
| echo " Inactivity Unassign Bot (Phase 2 - PR inactivity)" | ||
| echo " Repo: $REPO" | ||
| echo " Threshold: $DAYS days (no commit activity on PR)" | ||
| echo "------------------------------------------------------------" | ||
|
|
||
| NOW_TS=$(date +%s) | ||
|
|
||
| # Cross-platform timestamp parsing (Linux + macOS/BSD) | ||
| parse_ts() { | ||
| local ts="$1" | ||
| if date --version >/dev/null 2>&1; then | ||
| # GNU date (Linux) | ||
| date -d "$ts" +%s | ||
| else | ||
| # macOS / BSD | ||
| date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s" | ||
| fi | ||
| } | ||
|
|
||
| # Fetch open ISSUES (not PRs) that have assignees | ||
| ISSUES=$(gh api "repos/$REPO/issues" \ | ||
| --paginate \ | ||
| --jq '.[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not)) | .number') | ||
|
|
||
| if [ -z "$ISSUES" ]; then | ||
| echo "No open issues with assignees found." | ||
| exit 0 | ||
| fi | ||
|
|
||
| for ISSUE in $ISSUES; do | ||
| echo "============================================================" | ||
| echo " ISSUE #$ISSUE" | ||
| echo "============================================================" | ||
|
|
||
| ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE") | ||
| ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login') | ||
|
|
||
| if [ -z "$ASSIGNEES" ]; then | ||
| echo "[INFO] No assignees? Skipping." | ||
| echo | ||
| continue | ||
| fi | ||
|
|
||
| echo "[INFO] Assignees: $ASSIGNEES" | ||
| echo | ||
|
|
||
| for USER in $ASSIGNEES; do | ||
| echo " → Checking assignee: $USER" | ||
|
|
||
| # Find OPEN PRs linked to THIS issue, authored by THIS user | ||
| PR_NUMBERS=$(gh api \ | ||
| -H "Accept: application/vnd.github.mockingbird-preview+json" \ | ||
| "repos/$REPO/issues/$ISSUE/timeline" \ | ||
| --jq ".[] | ||
| | select(.event == \"cross-referenced\") | ||
| | select(.source.issue.pull_request != null) | ||
| | select(.source.issue.user.login == \"$USER\") | ||
| | .source.issue.number") | ||
|
|
||
| if [ -z "$PR_NUMBERS" ]; then | ||
| echo " [INFO] No linked PRs by $USER for this issue → Phase 1 covers the no-PR case." | ||
| echo | ||
| continue | ||
| fi | ||
|
|
||
| echo " [INFO] Linked PRs by $USER: $PR_NUMBERS" | ||
|
|
||
| STALE_PR="" | ||
| STALE_AGE_DAYS=0 | ||
|
|
||
| # Look for a stale OPEN PR | ||
| for PR_NUM in $PR_NUMBERS; do | ||
| PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state') | ||
|
|
||
| if [ "$PR_STATE" != "OPEN" ]; then | ||
| echo " [SKIP] PR #$PR_NUM is not open ($PR_STATE)." | ||
| continue | ||
| fi | ||
|
|
||
| # Last commit date on the PR: pick the truly latest by timestamp | ||
| COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate 2>/dev/null || echo "[]") | ||
|
|
||
| LAST_COMMIT_DATE=$(echo "$COMMITS_JSON" \ | ||
| | jq -r ' | ||
| select(length > 0) | ||
| | max_by(.commit.committer.date // .commit.author.date) | ||
| | (.commit.committer.date // .commit.author.date) | ||
| ') | ||
|
|
||
| if [ -z "$LAST_COMMIT_DATE" ] || [ "$LAST_COMMIT_DATE" = "null" ]; then | ||
| echo " [WARN] Could not determine last commit date for PR #$PR_NUM, skipping." | ||
| continue | ||
| fi | ||
|
|
||
| LAST_COMMIT_TS=$(parse_ts "$LAST_COMMIT_DATE") | ||
| AGE_DAYS=$(( (NOW_TS - LAST_COMMIT_TS) / 86400 )) | ||
|
|
||
| echo " [INFO] PR #$PR_NUM last commit: $LAST_COMMIT_DATE (~${AGE_DAYS} days ago)" | ||
|
|
||
| if [ "$AGE_DAYS" -ge "$DAYS" ]; then | ||
| STALE_PR="$PR_NUM" | ||
| STALE_AGE_DAYS="$AGE_DAYS" | ||
| break | ||
| fi | ||
| done | ||
|
|
||
| if [ -z "$STALE_PR" ]; then | ||
| echo " [KEEP] No OPEN PR for $USER is stale (>= $DAYS days)." | ||
| echo | ||
| continue | ||
| fi | ||
|
|
||
| echo " [STALE] PR #$STALE_PR by $USER has had no commit activity for $STALE_AGE_DAYS days (>= $DAYS)." | ||
| echo " [ACTION] Closing PR #$STALE_PR and unassigning @$USER from issue #$ISSUE." | ||
|
|
||
| MESSAGE=$( | ||
| cat <<EOF | ||
| Hi @$USER, this is InactivityBot. | ||
|
|
||
| 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. | ||
|
|
||
| You are welcome to get assigned to an issue once again once you have capacity. | ||
|
|
||
| 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. | ||
| EOF | ||
| ) | ||
|
|
||
| # Comment on the PR | ||
| gh pr comment "$STALE_PR" --repo "$REPO" --body "$MESSAGE" | ||
| echo " [DONE] Commented on PR #$STALE_PR." | ||
|
|
||
| # Close the PR | ||
| gh pr close "$STALE_PR" --repo "$REPO" | ||
| echo " [DONE] Closed PR #$STALE_PR." | ||
|
|
||
| # Unassign the user from the issue | ||
| gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER" | ||
| echo " [DONE] Unassigned @$USER from issue #$ISSUE." | ||
| echo | ||
| done | ||
|
|
||
| echo | ||
| done | ||
|
|
||
| echo "------------------------------------------------------------" | ||
| echo " Inactivity Unassign Bot (Phase 2) complete." | ||
| echo "------------------------------------------------------------" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| name: bot-inactivity-unassign-phase2 | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "0 12 * * *" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
Akshat8510 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| inactivity-unassign-phase2: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 | ||
|
|
||
| - name: Harden the runner | ||
| uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: Unassign stale PR contributors (Phase 2) | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPO: ${{ github.repository }} | ||
| DAYS: 21 | ||
| run: bash .github/scripts/inactivity_unassign_phase2.sh | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.