diff --git a/.github/scripts/issue_reminder_no_pr.sh b/.github/scripts/issue_reminder_no_pr.sh new file mode 100644 index 000000000..70c45cba8 --- /dev/null +++ b/.github/scripts/issue_reminder_no_pr.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Env: +# GH_TOKEN - provided by GitHub Actions +# REPO - owner/repo (fallback to GITHUB_REPOSITORY) +# DAYS - reminder threshold in days (default 7) +# DRY_RUN - if "true", only log actions without posting comments + +REPO="${REPO:-${GITHUB_REPOSITORY:-}}" +DAYS="${DAYS:-7}" +DRY_RUN="${DRY_RUN:-false}" + +if [ -z "$REPO" ]; then + echo "ERROR: REPO environment variable not set." + exit 1 +fi + +echo "------------------------------------------------------------" +echo " Issue Reminder Bot (No PR)" +echo " Repo: $REPO" +echo " Threshold: $DAYS days" +echo " Dry Run: $DRY_RUN" +echo "------------------------------------------------------------" +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 + date -d "$ts" +%s # GNU date (Linux) + else + date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s" # macOS/BSD + 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 + + # Check if this issue already has a reminder comment from ReminderBot + EXISTING_COMMENT=$(gh api "repos/$REPO/issues/$ISSUE/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"ReminderBot\")) | .id" \ + | head -n1) + + if [ -n "$EXISTING_COMMENT" ]; then + echo "[INFO] Reminder comment already posted on this issue." + echo + continue + fi + + # Get assignment time (use the last assigned event) + ASSIGN_TS=$(gh api "repos/$REPO/issues/$ISSUE/events" \ + --jq ".[] | select(.event==\"assigned\") | .created_at" \ + | tail -n1) + + if [ -z "$ASSIGN_TS" ]; then + echo "[WARN] No assignment event found. Skipping." + continue + fi + + ASSIGN_TS_SEC=$(parse_ts "$ASSIGN_TS") + DIFF_DAYS=$(( (NOW_TS - ASSIGN_TS_SEC) / 86400 )) + + echo "[INFO] Assigned at: $ASSIGN_TS" + echo "[INFO] Days since assignment: $DIFF_DAYS" + + # Check if any open PRs are linked to this issue + 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) + | .source.issue.number" 2>/dev/null || true) + + OPEN_PR_FOUND="" + if [ -n "$PR_NUMBERS" ]; then + for PR_NUM in $PR_NUMBERS; do + PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null || true) + if [ "$PR_STATE" = "OPEN" ]; then + OPEN_PR_FOUND="$PR_NUM" + break + fi + done + fi + + if [ -n "$OPEN_PR_FOUND" ]; then + echo "[KEEP] An OPEN PR #$OPEN_PR_FOUND is linked to this issue → skip reminder." + echo + continue + fi + + echo "[RESULT] No OPEN PRs linked to this issue." + + # Check if threshold has been reached + if [ "$DIFF_DAYS" -lt "$DAYS" ]; then + echo "[WAIT] Only $DIFF_DAYS days (< $DAYS) → not yet time for reminder." + echo + continue + fi + + echo "[REMIND] Issue #$ISSUE assigned for $DIFF_DAYS days, posting reminder." + + # Post reminder comment + MESSAGE="Hi, this is ReminderBot. This issue has been assigned but has had no pull request created. Are you still planning on working on the issue? + +From the Python SDK Team" + + if [ "$DRY_RUN" = "true" ]; then + echo "[DRY RUN] Would post comment on issue #$ISSUE:" + echo "$MESSAGE" + else + gh issue comment "$ISSUE" --repo "$REPO" --body "$MESSAGE" + echo "[DONE] Posted reminder comment on issue #$ISSUE." + fi + echo +done + +echo "------------------------------------------------------------" +echo " Issue Reminder Bot (No PR) complete." +echo "------------------------------------------------------------" diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml new file mode 100644 index 000000000..c9eee5600 --- /dev/null +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -0,0 +1,38 @@ +name: bot-issue-reminder-no-pr + +on: + schedule: + - cron: "0 10 * * *" #runs daily at 10:00 UTC + workflow_dispatch: + inputs: + dry_run: + description: "Dry run (log only, do not post comments)" + required: false + default: true #safe default for manual testing + type: boolean + +permissions: + contents: read + issues: write #needed to comment on issues + pull-requests: read #needed to check PR state + +jobs: + reminder: + runs-on: ubuntu-latest + + steps: + - name: Harden the runner + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #6.0.1 + + - name: Post reminder on assigned issues with no PRs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + DAYS: 7 + DRY_RUN: ${{ inputs.dry_run }} + run: bash .github/scripts/issue_reminder_no_pr.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c294aa9a..4ff9e507c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Unified the inactivity-unassign bot into a single script with `DRY_RUN` support, and fixed handling of cross-repo PR references for stale detection. - Added unit tests for `SubscriptionHandle` class covering cancellation state, thread management, and join operations. - Refactored `account_create_transaction_create_with_alias.py` example by splitting monolithic function into modular functions: `generate_main_and_alias_keys()`, `create_account_with_ecdsa_alias()`, `fetch_account_info()`, `print_account_summary()` (#1016) -- - Modularized `transfer_transaction_fungible` example by introducing `account_balance_query()` & `transfer_transaction()`.Renamed `transfer_tokens()` → `main()` - Phase 2 of the inactivity-unassign bot: Automatically detects stale open pull requests (no commit activity for 21+ days), comments with a helpful InactivityBot message, closes the stale PR, and unassigns the contributor from the linked issue. - Added `__str__()` to CustomFixedFee and updated examples and tests accordingly. @@ -35,6 +34,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Add PR inactivity reminder bot for stale pull requests `.github/workflows/pr-inactivity-reminder-bot.yml` - Add comprehensive training documentation for _Executable class `docs/sdk_developers/training/executable.md` - Added empty `docs/maintainers/good_first_issues.md` file for maintainers to write Good First Issue guidelines (#1034) +- Added Issue Reminder (no-PR) bot, `.github/scripts/issue_reminder_no_pr.sh` and `.github/workflows/bot-issue-reminder-no-pr.yml` to automatically detect assigned issues with no linked pull requests for 7+ days and post a gentle ReminderBot comment.(#951) ### Changed