Skip to content

feat(workflows): add 90+30 day stale issue and PR management - #3

Draft
dwnoble wants to merge 1 commit into
mainfrom
feat/manage-stale-items
Draft

dwnoble wants to merge 1 commit into
mainfrom
feat/manage-stale-items

Conversation

@dwnoble

@dwnoble dwnoble commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

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:
    • 90 days without activity: Applies the stale label and posts a polite notice asking if the issue or PR is still relevant.
    • 30 additional days without activity (120+ days total): Automatically closes the item with a friendly message inviting the author to reopen or comment if the issue persists.
    • Un-stale on new activity: Automatically removes the stale label if someone comments after the warning notice.
    • Exemptions: Skips items with an active milestone or labeled keep-open, pinned, or security.
  • .github/workflows/manage-stale-items.yml: Runs the script daily at 09:00 UTC (plus manual workflow_dispatch with dry_run enabled by default). Caps writes at 150 per run to respect GitHub secondary rate limits.
  • README.md: Documents the stale policy and CLI usage.

Verification

  • Tested dry run against 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.
  • Verified bash syntax (bash -n scripts/manage_stale_items.sh) and pinned actions/checkout to commit hash 11d5960 (v4.4.0) for zizmor compliance.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +145 to +146
gh api --method PATCH "/repos/$ORG/$repo/issues/$num" \
-f state="closed" -f state_reason="not_planned" >/dev/null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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".

Suggested change
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

Comment on lines +153 to +154
last_body="$(gh api "/repos/$ORG/$repo/issues/$num/comments?per_page=1&direction=desc" \
--jq '.[0].body // ""' 2>/dev/null || echo "")"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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 "")"

Comment on lines +82 to +88
ITEM_JQ='.[] | [
.number,
.html_url,
.updated_at,
(if .milestone != null then "yes" else "no" end),
([.labels[].name | ascii_downcase] | join(","))
] | @tsv'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the read command to match the new fields returned by ITEM_JQ (using the parsed epoch timestamp and the is_pr flag).

Suggested change
while IFS=$'\t' read -r num url updated_at has_milestone labels_csv; do
while IFS=$'\t' read -r num url upd_epoch has_milestone labels_csv is_pr; do

Comment on lines +128 to +129
upd_epoch="$(iso_to_epoch "$updated_at")"
age_days=$(( (NOW_EPOCH - upd_epoch) / 86400 ))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since upd_epoch is now read directly as an integer from the TSV, we no longer need to call iso_to_epoch.

Suggested change
upd_epoch="$(iso_to_epoch "$updated_at")"
age_days=$(( (NOW_EPOCH - upd_epoch) / 86400 ))
age_days=$(( (NOW_EPOCH - upd_epoch) / 86400 ))

Comment on lines +55 to +60
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"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since date parsing is now handled directly in jq, the iso_to_epoch function is no longer needed and can be removed.

Suggested change
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant