Conversation
Adds a daily scheduled workflow and script to warn on 90-day inactive issues and PRs across all 12 public repos, auto-close them after 30 additional days of inactivity, and un-stale items when new activity occurs.
There was a problem hiding this comment.
Code Review
This pull request introduces automated stale issue and pull request management via a new script and documents its usage in the README. The review feedback identifies critical bugs, such as the unsupported state_reason parameter for pull requests and the lack of sorting support in the GitHub comments API, which would cause incorrect un-staling. Additionally, the reviewer suggests a performance and cross-platform improvement by using jq's built-in fromdateiso8601 function for date parsing instead of spawning platform-specific date subshells.
| gh api --method PATCH "/repos/$ORG/$repo/issues/$num" \ | ||
| -f state="closed" -f state_reason="not_planned" >/dev/null |
There was a problem hiding this comment.
The GitHub REST API does not support state_reason for Pull Requests (it will return a 422 Unprocessable Entity error). Since this script manages both issues and PRs, you should check if the item is a PR before applying state_reason="not_planned".
| gh api --method PATCH "/repos/$ORG/$repo/issues/$num" \ | |
| -f state="closed" -f state_reason="not_planned" >/dev/null | |
| if [[ "$is_pr" == "yes" ]]; then | |
| gh api --method PATCH "/repos/$ORG/$repo/issues/$num" \ | |
| -f state="closed" >/dev/null | |
| else | |
| gh api --method PATCH "/repos/$ORG/$repo/issues/$num" \ | |
| -f state="closed" -f state_reason="not_planned" >/dev/null | |
| fi |
| last_body="$(gh api "/repos/$ORG/$repo/issues/$num/comments?per_page=1&direction=desc" \ | ||
| --jq '.[0].body // ""' 2>/dev/null || echo "")" |
There was a problem hiding this comment.
The GitHub REST API /repos/{owner}/{repo}/issues/{issue_number}/comments endpoint does not support the direction or sort query parameters. It always returns comments in ascending order (oldest first). As a result, direction=desc is ignored, and .[0] returns the oldest comment instead of the newest. This causes any stale issue with prior comments to be immediately un-staled on the next run.
To fix this, you can fetch the comments (up to 100) and use .[-1] in jq to get the last (newest) comment.
| last_body="$(gh api "/repos/$ORG/$repo/issues/$num/comments?per_page=1&direction=desc" \ | |
| --jq '.[0].body // ""' 2>/dev/null || echo "")" | |
| last_body="$(gh api "/repos/$ORG/$repo/issues/$num/comments?per_page=100" \ | |
| --jq '.[-1].body // ""' 2>/dev/null || echo "")" |
| ITEM_JQ='.[] | [ | ||
| .number, | ||
| .html_url, | ||
| .updated_at, | ||
| (if .milestone != null then "yes" else "no" end), | ||
| ([.labels[].name | ascii_downcase] | join(",")) | ||
| ] | @tsv' |
There was a problem hiding this comment.
Instead of parsing ISO 8601 dates using platform-specific date commands in a bash loop (which spawns a subshell for every single issue/PR), you can parse the date directly to an epoch timestamp using jq's built-in fromdateiso8601 function. This is cross-platform, highly efficient, and eliminates the need for the iso_to_epoch helper function.
We can also extract whether the item is a Pull Request here to fix the state_reason issue.
| ITEM_JQ='.[] | [ | |
| .number, | |
| .html_url, | |
| .updated_at, | |
| (if .milestone != null then "yes" else "no" end), | |
| ([.labels[].name | ascii_downcase] | join(",")) | |
| ] | @tsv' | |
| ITEM_JQ='.[] | [ | |
| .number, | |
| .html_url, | |
| (.updated_at | fromdateiso8601), | |
| (if .milestone != null then "yes" else "no" end), | |
| ([.labels[].name | ascii_downcase] | join(",")), | |
| (if .pull_request != null then "yes" else "no" end) | |
| ] | @tsv' |
|
|
||
| ensure_stale_label "$repo" | ||
|
|
||
| while IFS=$'\t' read -r num url updated_at has_milestone labels_csv; do |
There was a problem hiding this comment.
| upd_epoch="$(iso_to_epoch "$updated_at")" | ||
| age_days=$(( (NOW_EPOCH - upd_epoch) / 86400 )) |
There was a problem hiding this comment.
| iso_to_epoch() { | ||
| local iso="$1" | ||
| date -u -d "$iso" +%s 2>/dev/null \ | ||
| || date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$iso" +%s 2>/dev/null \ | ||
| || echo "$NOW_EPOCH" | ||
| } |
There was a problem hiding this comment.
Since date parsing is now handled directly in jq, the iso_to_epoch function is no longer needed and can be removed.
| iso_to_epoch() { | |
| local iso="$1" | |
| date -u -d "$iso" +%s 2>/dev/null \ | |
| || date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$iso" +%s 2>/dev/null \ | |
| || echo "$NOW_EPOCH" | |
| } | |
| # Date parsing is handled directly in jq |
Problem
Across our 12 public repositories, we currently have 1,112 open issues and pull requests, including 824 items (74% of the backlog) that have had zero activity for over 120 days (some dating back to 2021). Without automated backlog hygiene, abandoned PRs and multi-year-old issues clutter our triage views and obscure active partner and community work.
Solution
scripts/manage_stale_items.sh: Runs centrally across all 12 public repositories to apply a two-step staleness policy:stalelabel and posts a polite notice asking if the issue or PR is still relevant.stalelabel if someone comments after the warning notice.keep-open,pinned, orsecurity..github/workflows/manage-stale-items.yml: Runs the script daily at 09:00 UTC (plus manualworkflow_dispatchwithdry_runenabled by default). Caps writes at 150 per run to respect GitHub secondary rate limits.README.md: Documents the stale policy and CLI usage.Verification
mixer(DRY_RUN=1 ./scripts/manage_stale_items.sh mixer): identified 50 items with 90+ days of inactivity to warn, 21 active items (<90 days) to leave untouched, and 0 immediate closures.bash -n scripts/manage_stale_items.sh) and pinnedactions/checkoutto commit hash11d5960(v4.4.0) for zizmor compliance.