diff --git a/.github/scripts/issue_reminder_no_pr.sh b/.github/scripts/issue_reminder_no_pr.sh new file mode 100644 index 000000000..d0db901cb --- /dev/null +++ b/.github/scripts/issue_reminder_no_pr.sh @@ -0,0 +1,154 @@ +#!/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}" + +# Normalize DRY_RUN to "true" or "false" +if [[ "$DRY_RUN" == "true" || "$DRY_RUN" == "yes" || "$DRY_RUN" == "1" ]]; then + DRY_RUN="true" +else + DRY_RUN="false" +fi + +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 +ALL_ISSUES_JSON=$(gh api "repos/$REPO/issues" \ + --paginate \ + --jq '.[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not))') + +if [ -z "$ALL_ISSUES_JSON" ]; then + echo "No open issues with assignees found." + exit 0 +fi + +echo "$ALL_ISSUES_JSON" | jq -c '.' | while read -r ISSUE_JSON; do + ISSUE=$(echo "$ISSUE_JSON" | jq -r '.number') + echo "============================================================" + echo " ISSUE #$ISSUE" + echo "============================================================" + + 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..cd33cee38 --- /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 13 * * *" #runs daily at 13: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 || 'false' }} + run: bash .github/scripts/issue_reminder_no_pr.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ed40df694..b8241df2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added new `.github/ISSUE_TEMPLATE/04_good_first_issue_candidate.yml` file (1068)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1068) - Enhanced `.github/ISSUE_TEMPLATE/01_good_first_issue.yml` with welcoming message and acceptance criteria sections to guide contributors in creating quality GFIs (#1052) - Add workflow to notify team about P0 issues `bot-p0-issues-notify-team.yml` +- 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 - Refactored `account_create_transaction_with_fallback_alias.py` by splitting the monolithic `create_account_with_fallback_alias` function into modular functions: `generate_fallback_key`, `fetch_account_info`, and `print_account_summary`. The existing `setup_client()` function was reused for improved readability and structure (#1018)