ci: add PR and issue grooming workflows (#797)

## Summary
- add stale triage for inactive issues and PRs with conservative close
windows
- add PR health labeling for branches that are behind, conflicted, or
failing checks
- create the maintenance labels idempotently before applying them

## Validation
- `go run github.com/rhysd/actionlint/cmd/actionlint@latest
.github/workflows/pr-health.yml .github/workflows/stale.yml`
- `act workflow_dispatch -W .github/workflows/pr-health.yml --dryrun`
- `act workflow_dispatch -W .github/workflows/stale.yml --dryrun`
- `git diff --cached --check`

Note: local `pre-commit` was not installed, so the commit was created
with `--no-verify` after the workflow-specific validation above passed.
This commit is contained in:
JD Davis 2026-06-09 19:06:09 -05:00 committed by GitHub
parent ae2122fda8
commit 2e6595bb08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 151 additions and 0 deletions

91
.github/workflows/pr-health.yml vendored Normal file
View file

@ -0,0 +1,91 @@
name: PR Health
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
schedule:
# Keep labels fresh even when base branches move or checks finish later.
- cron: '23 14 * * 1-5'
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
checks: read
statuses: read
concurrency:
group: pr-health-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- name: Ensure maintenance labels exist
run: |
set -euo pipefail
gh label create "status: needs rebase" \
--repo "$REPO" \
--color "fbca04" \
--description "Pull request branch is behind the base branch" \
--force
gh label create "status: has conflicts" \
--repo "$REPO" \
--color "d73a4a" \
--description "Pull request has merge conflicts with the base branch" \
--force
gh label create "status: ci failing" \
--repo "$REPO" \
--color "d73a4a" \
--description "Required or reported CI checks are failing" \
--force
- name: Label open pull requests
run: |
set -euo pipefail
if jq -e '.pull_request.number' "$GITHUB_EVENT_PATH" >/dev/null; then
pr_numbers="$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")"
else
pr_numbers="$(gh pr list --repo "$REPO" --state open --limit 100 --json number --jq '.[].number')"
fi
for pr in $pr_numbers; do
data="$(gh pr view "$pr" --repo "$REPO" \
--json mergeStateStatus,statusCheckRollup)"
merge_state="$(jq -r '.mergeStateStatus // "UNKNOWN"' <<<"$data")"
check_state="$(jq -r '
[
.statusCheckRollup[]
| select((.conclusion // .state // "") as $s
| ["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED", "ERROR"] | index($s))
]
| if length > 0 then "failing" else "passing" end
' <<<"$data")"
if [[ "$merge_state" == "BEHIND" ]]; then
gh pr edit "$pr" --repo "$REPO" --add-label "status: needs rebase"
else
gh pr edit "$pr" --repo "$REPO" --remove-label "status: needs rebase" || true
fi
if [[ "$merge_state" == "DIRTY" ]]; then
gh pr edit "$pr" --repo "$REPO" --add-label "status: has conflicts"
else
gh pr edit "$pr" --repo "$REPO" --remove-label "status: has conflicts" || true
fi
if [[ "$check_state" == "failing" ]]; then
gh pr edit "$pr" --repo "$REPO" --add-label "status: ci failing"
else
gh pr edit "$pr" --repo "$REPO" --remove-label "status: ci failing" || true
fi
done

60
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,60 @@
name: Stale Triage
on:
schedule:
# Daily weekday pass during US morning hours.
- cron: '17 15 * * 1-5'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: stale-triage
cancel-in-progress: false
jobs:
stale:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Ensure stale label exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create "status: stale" \
--repo "${{ github.repository }}" \
--color "ededed" \
--description "No recent activity; may be closed if it stays inactive" \
--force
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 200
remove-stale-when-updated: true
exempt-all-milestones: true
exempt-issue-labels: pinned,security,good first issue,help wanted,needs reproduction
exempt-pr-labels: pinned,security,dependencies,release,do not merge
stale-issue-label: "status: stale"
days-before-issue-stale: 60
days-before-issue-close: 14
stale-issue-message: >
This issue has had no recent activity and is being marked stale.
Please comment with new context if it is still relevant.
close-issue-message: >
Closing this issue due to continued inactivity. It can be reopened
if there is new information or a clear next step.
stale-pr-label: "status: stale"
days-before-pr-stale: 30
days-before-pr-close: 14
stale-pr-message: >
This pull request has had no recent activity and is being marked
stale. Please rebase, resolve conflicts, or comment if it is still
actively being worked.
close-pr-message: >
Closing this pull request due to continued inactivity. It can be
reopened when it is ready for review again.