diff --git a/.github/scripts/dry_run_inactivity_unassign_phase1.sh b/.github/scripts/dry_run_inactivity_unassign_phase1.sh deleted file mode 100755 index 6d4991ce4..000000000 --- a/.github/scripts/dry_run_inactivity_unassign_phase1.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# =============================================================== -# DRY-RUN: Phase 1 Inactivity Unassign Bot -# - Uses assignment timestamp -# - Only considers OPEN PRs linked to the issue -# - Logs everything without making changes -# - Provides summary of users who would be unassigned -# =============================================================== - -REPO="${REPO:-}" -DAYS="${DAYS:-21}" - -if [ -z "$REPO" ]; then - echo "ERROR: REPO environment variable not set. Example: export REPO=owner/repo" - exit 1 -fi - -echo "------------------------------------------------------------" -echo " DRY RUN: Phase 1 Inactivity Unassign Check" -echo " Repo: $REPO" -echo " Threshold: $DAYS days" -echo "------------------------------------------------------------" -echo - -NOW_TS=$(date +%s) - -# ----------------------------- -# Cross-platform timestamp parsing -# ----------------------------- -parse_timestamp() { - local ts="$1" - if date --version >/dev/null 2>&1; then - date -d "$ts" +%s # GNU date - else - date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s" # macOS/BSD - fi -} - -# ----------------------------- -# Array to store summary of users to message -# ----------------------------- -declare -a TO_UNASSIGN=() - -# ----------------------------- -# Fetch all open issues with assignees (skip PRs) -# ----------------------------- -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 - -# ----------------------------- -# Iterate over issues -# ----------------------------- -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 - - # ----------------------------- - # Iterate over assignees - # ----------------------------- - for USER in $ASSIGNEES; do - echo " → Checking assignee: $USER" - - # ----------------------------- - # Find the assignment timestamp for this user - # ----------------------------- - ASSIGN_TS=$(gh api "repos/$REPO/issues/$ISSUE/events" \ - --jq ".[] | select(.event==\"assigned\" and .assignee.login==\"$USER\") | .created_at" | tail -n1) - - if [ -z "$ASSIGN_TS" ]; then - echo " [WARN] Could not find assignment timestamp. Using issue creation date as fallback." - ASSIGN_TS=$(echo "$ISSUE_JSON" | jq -r '.created_at') - fi - - ASSIGN_TS_SEC=$(parse_timestamp "$ASSIGN_TS") - DIFF_DAYS=$(( (NOW_TS - ASSIGN_TS_SEC) / 86400 )) - - echo " [INFO] Assigned at: $ASSIGN_TS" - echo " [INFO] Assigned for: $DIFF_DAYS days" - - # ----------------------------- - # Check if user has an OPEN PR 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) - | select(.source.issue.user.login == \"$USER\") - | .source.issue.number") - - OPEN_PR_FOUND="" - 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 - OPEN_PR_FOUND="$PR_NUM" - break - fi - done - - if [ -n "$OPEN_PR_FOUND" ]; then - echo " [KEEP] User $USER has an OPEN PR linked to this issue: $OPEN_PR_FOUND → skip unassign." - echo - continue - fi - - echo " [RESULT] User $USER has NO OPEN PRs linked to this issue." - - # ----------------------------- - # Decide on DRY-RUN unassign - # ----------------------------- - if [ "$DIFF_DAYS" -ge "$DAYS" ]; then - UNASSIGN_MESSAGE="Hi @$USER, you have been assigned to this issue for $DIFF_DAYS days without any open PRs linked. You would be automatically unassigned to keep things tidy. Please re-assign yourself if you are still working on this." - - echo " [DRY RUN] Would UNASSIGN $USER (assigned for $DIFF_DAYS days, threshold $DAYS)" - echo " [DRY RUN] Message that would be posted:" - echo " --------------------------------------------------" - echo " $UNASSIGN_MESSAGE" - echo " --------------------------------------------------" - - TO_UNASSIGN+=("Issue #$ISSUE → $USER (assigned $DIFF_DAYS days) → Message: $UNASSIGN_MESSAGE") - else - echo " [KEEP] Only $DIFF_DAYS days old → NOT stale yet." - fi - - - echo - done - - echo -done - -# ----------------------------- -# Summary of all users to message -# ----------------------------- -if [ ${#TO_UNASSIGN[@]} -gt 0 ]; then - echo "============================================================" - echo " SUMMARY: Users who would be unassigned / messaged" - echo "============================================================" - for ITEM in "${TO_UNASSIGN[@]}"; do - echo " - $ITEM" - done -else - echo "No users would be unassigned in this dry-run." -fi - -echo "------------------------------------------------------------" -echo " DRY RUN COMPLETE — No changes were made." -echo "------------------------------------------------------------" diff --git a/.github/scripts/dry_run_inactivity_unassign_phase2.sh b/.github/scripts/dry_run_inactivity_unassign_phase2.sh deleted file mode 100755 index 4c48075a4..000000000 --- a/.github/scripts/dry_run_inactivity_unassign_phase2.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/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 "last commit" logic as the real bot (with pagination) - COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate 2>/dev/null || echo "") - - LAST_COMMIT_DATE=$(echo "$COMMITS_JSON" \ - | jq -r 'last | (.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 "------------------------------------------------------------" diff --git a/.github/scripts/inactivity_unassign.sh b/.github/scripts/inactivity_unassign.sh new file mode 100755 index 000000000..692e6defa --- /dev/null +++ b/.github/scripts/inactivity_unassign.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Unified Inactivity Bot (Phase 1 + Phase 2) +# DRY_RUN controls behaviour: +# DRY_RUN = 1 -> simulate only (no changes, just logs) +# DRY_RUN = 0 -> real actions (comments, closes, unassigns) + +REPO="${REPO:-${GITHUB_REPOSITORY:-}}" +DAYS="${DAYS:-21}" +DRY_RUN="${DRY_RUN:-0}" + +# Normalise DRY_RUN input ("true"/"false" -> 1/0, case-insensitive) +shopt -s nocasematch +case "$DRY_RUN" in + "true") DRY_RUN=1 ;; + "false") DRY_RUN=0 ;; +esac +shopt -u nocasematch + +if [[ -z "$REPO" ]]; then + echo "ERROR: REPO environment variable not set." + exit 1 +fi + +echo "------------------------------------------------------------" +echo " Unified Inactivity Unassign Bot" +echo " Repo: $REPO" +echo " Threshold $DAYS days" +echo " DRY_RUN: $DRY_RUN" +echo "------------------------------------------------------------" + +# current time (epoch seconds) +NOW_TS=$(date +%s) + +# Convert GitHub ISO timestamp -> epoch seconds (works on Linux/BSD) +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 +} + +# Quick gh availability/auth checks +if ! command -v gh >/dev/null 2>&1; then + echo "ERROR: gh CLI not found. Install it and ensure it's on PATH." + exit 1 +fi + +if ! gh auth status >/dev/null 2>&1; then + echo "WARN: gh auth status failed — ensure gh is logged in for non-dry runs." +fi + +# Get list of open issues with assignees (pagination via gh) +ISSUES=$( + gh api "repos/$REPO/issues" --paginate --jq '.[] + | select(.state=="open" and (.assignees|length>0) and (.pull_request|not)) + | .number' 2>/dev/null || true +) + +for ISSUE in $ISSUES; do + echo "============================================================" + echo " ISSUE #$ISSUE" + echo "============================================================" + + ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE" 2>/dev/null || echo "{}") + ISSUE_CREATED_AT=$(echo "$ISSUE_JSON" | jq -r '.created_at // empty') + ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[]?.login' 2>/dev/null || true) + + echo " [INFO] Issue created at: ${ISSUE_CREATED_AT:-(unknown)}" + echo + + # Fetch timeline once (used for assignment events + PR links). + # If gh fails, default to a valid empty JSON array so jq never blocks. + TIMELINE=$(gh api -H "Accept: application/vnd.github.mockingbird-preview+json" "repos/$REPO/issues/$ISSUE/timeline" 2>/dev/null || echo "[]") + TIMELINE=${TIMELINE:-'[]'} # defensive default (ensures valid JSON) + + # if there are no assignees, skip (defensive) + if [[ -z "${ASSIGNEES// }" ]]; then + echo " [INFO] No assignees for this issue, skipping." + echo + continue + fi + + for USER in $ASSIGNEES; do + echo " → Checking assignee: $USER" + + # Determine assignment timestamp for this user: find last assigned event for this user + # Use here-string to pass TIMELINE into jq (prevents jq from reading stdin unexpectedly). + ASSIGN_EVENT_JSON=$(jq -c --arg user "$USER" ' + [ .[] | select(.event == "assigned") | select(.assignee.login == $user) ] | last // empty' <<<"$TIMELINE" 2>/dev/null || echo "") + + if [[ -n "$ASSIGN_EVENT_JSON" && "$ASSIGN_EVENT_JSON" != "null" ]]; then + ASSIGNED_AT=$(echo "$ASSIGN_EVENT_JSON" | jq -r '.created_at // empty') + ASSIGN_SOURCE="assignment_event" + else + # fallback: use issue creation time + ASSIGNED_AT="${ISSUE_CREATED_AT:-}" + ASSIGN_SOURCE="issue_created_at (no explicit assignment event)" + fi + + # compute assignment age safely (if no timestamp, set to 0) + if [[ -n "$ASSIGNED_AT" ]]; then + ASSIGNED_TS=$(parse_ts "$ASSIGNED_AT") + ASSIGNED_AGE_DAYS=$(( (NOW_TS - ASSIGNED_TS) / 86400 )) + else + ASSIGNED_AGE_DAYS=0 + fi + + echo " [INFO] Assignment source: $ASSIGN_SOURCE" + echo " [INFO] Assigned at: ${ASSIGNED_AT:-(unknown)} (~${ASSIGNED_AGE_DAYS} days ago)" + + # Determine PRs cross-referenced from the same repo + PR_NUMBERS=$(jq -r --arg repo "$REPO" ' + .[] | + select(.event == "cross-referenced") | + select(.source.issue.pull_request != null) | + select(.source.issue.repository.full_name == $repo) | + .source.issue.number' <<<"$TIMELINE" 2>/dev/null || true) + + # Defensive normalization: remove blank lines and spaces + PR_NUMBERS=$(echo "$PR_NUMBERS" | sed '/^[[:space:]]*$/d' || true) + + # PHASE 1: no PRs attached + if [[ -z "${PR_NUMBERS// }" ]]; then + echo " [INFO] Linked PRs: none" + + if (( ASSIGNED_AGE_DAYS >= DAYS )); then + echo " [RESULT] Phase 1 -> stale assignment (>= $DAYS days, no PR)" + + if (( DRY_RUN == 0 )); then + MESSAGE=$( + cat < no PR linked but not stale (< $DAYS days) -> KEEP" + fi + + echo + continue + fi + + # PHASE 2: process linked PR(s) + echo " [INFO] Linked PRs: $PR_NUMBERS" + PHASE2_TOUCHED=0 + + for PR_NUM in $PR_NUMBERS; do + # Safe check: verify PR exists in this repo + if ! PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null); then + echo " [SKIP] #$PR_NUM is not a valid PR in $REPO" + continue + fi + + # log state and continue only if open + echo " [INFO] PR #$PR_NUM state: $PR_STATE" + if [[ "$PR_STATE" != "OPEN" ]]; then + echo " [SKIP] PR #$PR_NUM is not open" + continue + fi + + # get last commit time safely + COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate 2>/dev/null || echo "[]") + LAST_TS_STR=$(jq -r 'last? | (.commit.committer.date // .commit.author.date) // empty' <<<"$COMMITS_JSON" 2>/dev/null || echo "") + if [[ -n "$LAST_TS_STR" ]]; then + LAST_TS=$(parse_ts "$LAST_TS_STR") + PR_AGE_DAYS=$(( (NOW_TS - LAST_TS) / 86400 )) + else + PR_AGE_DAYS=$((DAYS+1)) # treat as stale if we cannot find commit timestamp + fi + + echo " [INFO] PR #$PR_NUM last commit: ${LAST_TS_STR:-(unknown)} (~${PR_AGE_DAYS} days ago)" + + if (( PR_AGE_DAYS >= DAYS )); then + PHASE2_TOUCHED=1 + echo " [RESULT] Phase 2 -> PR #$PR_NUM is stale (>= $DAYS days since last commit)" + + if (( DRY_RUN == 0 )); then + MESSAGE=$( + cat < KEEP" + fi + done + + if (( PHASE2_TOUCHED == 0 )); then + echo " [RESULT] Phase 2 -> all linked PRs active or not applicable -> KEEP" + fi + + echo + done + +done + +echo "------------------------------------------------------------" +echo " Unified Inactivity Bot Complete" +echo " DRY_RUN: $DRY_RUN" +echo "------------------------------------------------------------" diff --git a/.github/scripts/inactivity_unassign_phase1.sh b/.github/scripts/inactivity_unassign_phase1.sh deleted file mode 100755 index c50b5a79c..000000000 --- a/.github/scripts/inactivity_unassign_phase1.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/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 1)" -echo " Repo: $REPO" -echo " Threshold: $DAYS days" -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 - - for USER in $ASSIGNEES; do - echo " → Checking assignee: $USER" - - # 1) Find when THIS USER was last assigned to THIS ISSUE - ASSIGN_TS=$(gh api "repos/$REPO/issues/$ISSUE/events" \ - --jq ".[] | select(.event==\"assigned\" and .assignee.login==\"$USER\") | .created_at" \ - | tail -n1) - - if [ -z "$ASSIGN_TS" ]; then - echo " [WARN] No assignment event for $USER, falling back to issue creation." - ASSIGN_TS=$(echo "$ISSUE_JSON" | jq -r '.created_at') - fi - - ASSIGN_TS_SEC=$(parse_ts "$ASSIGN_TS") - DIFF_DAYS=$(( (NOW_TS - ASSIGN_TS_SEC) / 86400 )) - - echo " [INFO] Assigned at: $ASSIGN_TS" - echo " [INFO] Assigned for: $DIFF_DAYS days" - - # 2) Check for OPEN PRs linked to THIS ISSUE by THIS USER (via timeline) - 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") - - OPEN_PR_FOUND="" - 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 - OPEN_PR_FOUND="$PR_NUM" - break - fi - done - - if [ -n "$OPEN_PR_FOUND" ]; then - echo " [KEEP] $USER has OPEN PR #$OPEN_PR_FOUND linked to this issue → skip unassign." - echo - continue - fi - - echo " [RESULT] $USER has NO OPEN PRs linked to this issue." - - # 3) Decide unassign - if [ "$DIFF_DAYS" -lt "$DAYS" ]; then - echo " [KEEP] Only $DIFF_DAYS days (< $DAYS) → not stale yet." - echo - continue - fi - - echo " [UNASSIGN] $USER (assigned $DIFF_DAYS days, threshold $DAYS)" - - # Unassign via gh CLI helper - gh issue edit "$ISSUE" --repo "$REPO" --remove-assignee "$USER" - - # Comment - MESSAGE="Hi @$USER, you were automatically unassigned from this issue because there have been no open PRs linked to it for **$DIFF_DAYS days**. This helps keep issues available for contributors who are currently active. You're very welcome to re-assign yourself or pick this back up whenever you have time 🚀" - - gh issue comment "$ISSUE" --repo "$REPO" --body "$MESSAGE" - - echo " [DONE] Unassigned and commented on issue #$ISSUE for $USER." - echo - done - - echo -done - -echo "------------------------------------------------------------" -echo " Inactivity Unassign Bot (Phase 1) complete." -echo "------------------------------------------------------------" \ No newline at end of file diff --git a/.github/scripts/inactivity_unassign_phase2.sh b/.github/scripts/inactivity_unassign_phase2.sh deleted file mode 100755 index af0ac5976..000000000 --- a/.github/scripts/inactivity_unassign_phase2.sh +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Inactivity Unassign Bot (Phase 2 - PR inactivity) -# 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 (use API order + paginate, take last) - COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate 2>/dev/null || echo "") - - LAST_COMMIT_DATE=$(echo "$COMMITS_JSON" \ - | jq -r 'last | (.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 " [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 < None: - """Create an account whose alias comes from a separate ECDSA key.""" - try: - print("\nSTEP 1: Generating main account key and separate ECDSA alias key...") - - # Main account key (can be any key type, here ed25519) - main_private_key = PrivateKey.generate() - main_public_key = main_private_key.public_key() - - # Separate ECDSA key used only for the EVM alias - alias_private_key = PrivateKey.generate("ecdsa") - alias_public_key = alias_private_key.public_key() - alias_evm_address = alias_public_key.to_evm_address() - - if alias_evm_address is None: - print("❌ Error: Failed to generate EVM address from alias ECDSA key.") - sys.exit(1) - - print(f"✅ Main account public key: {main_public_key}") - print(f"✅ Alias ECDSA public key: {alias_public_key}") - print(f"✅ Alias EVM address: {alias_evm_address}") - - print("\nSTEP 2: Creating the account with the EVM alias from the ECDSA key...") - - # Use the helper that accepts both the main key and the ECDSA alias key - transaction = ( - AccountCreateTransaction( - initial_balance=Hbar(5), - memo="Account with separate ECDSA alias", - ) - .set_key_with_alias(main_private_key, alias_public_key) - ) - - # Freeze and sign: - # - operator key signs as payer (via client) - # - alias private key MUST sign to authorize the alias - transaction = ( - transaction.freeze_with(client) - .sign(alias_private_key) - ) - - response = transaction.execute(client) - new_account_id = response.account_id - - if new_account_id is None: - raise RuntimeError( - "AccountID not found in receipt. Account may not have been created." - ) - - print(f"✅ Account created with ID: {new_account_id}\n") - - account_info = ( - AccountInfoQuery() - .set_account_id(new_account_id) - .execute(client) - ) - - out = info_to_dict(account_info) - print("Account Info:") - print(json.dumps(out, indent=2) + "\n") - - if account_info.contract_account_id is not None: - print( - f"✅ Contract Account ID (EVM alias on-chain): " - f"{account_info.contract_account_id}" - ) - else: - print("❌ Error: Contract Account ID (alias) does not exist.") - - except Exception as error: - print(f"❌ Error: {error}") - sys.exit(1) - -def main(): - """Main entry point.""" - client = setup_client() - create_account_with_separate_ecdsa_alias(client) - - -if __name__ == "__main__": - main()