From 5b3e4e007b276e6bad43f8e745b95a5ffa71aa5e Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 17 May 2026 11:59:54 -0500 Subject: [PATCH] chore(ci): close failing or conflicted PRs sooner (#480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - close inactive PRs after 7 days only when they have failing checks or merge conflicts - include merge state in the PR closer query and close with the specific reason - keep existing exclusions for @jdx-authored and keep-open PRs ## Validation - actionlint .github/workflows/pr-closer.yml - git diff --check - jq filter sample validation --- > [!NOTE] > **Medium Risk** > Automates PR closure based on CI/merge-state signals; a bug in the query or jq logic could incorrectly close active or recoverable PRs. Limited to GitHub Actions workflow changes but impacts contributor workflow. > > **Overview** > Updates the `pr-closer` GitHub Actions workflow to **close PRs much sooner (7 days inactivity)**, but only when they have *failing checks and/or merge conflicts*. > > The workflow now queries `mergeStateStatus` and expanded check conclusions to generate a specific closure reason, skips “warn-only” states (e.g., cancelled checks/unknown merge state), increases the listing limit to 500, and adds `concurrency` plus additional read permissions (`checks`, `statuses`) to support the new filtering. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 34aded28f6a0f4730e8ffc10745b2403549e7122. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .github/workflows/pr-closer.yml | 47 ++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-closer.yml b/.github/workflows/pr-closer.yml index 7c0cc80..f8e2e58 100644 --- a/.github/workflows/pr-closer.yml +++ b/.github/workflows/pr-closer.yml @@ -5,27 +5,50 @@ on: - cron: "0 0 * * *" # daily at midnight workflow_dispatch: +concurrency: + group: pr-closer + cancel-in-progress: true + jobs: close-stale-prs: runs-on: ubuntu-latest permissions: pull-requests: write + checks: read + statuses: read steps: - name: Close stale PRs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} run: | - gh pr list -R "${{ github.repository }}" --state open --json number,author,labels,updatedAt,statusCheckRollup --limit 100 | \ - jq -r '.[] | select( - (.updatedAt | fromdateiso8601) < (now - 30*24*60*60) and - .author.login != "jdx" and - ([.labels[].name] | index("keep-open") | not) - ) | [.number, (if (.statusCheckRollup | length > 0) and (.statusCheckRollup | any(.conclusion == "FAILURE" or .conclusion == "failure")) then "failing" else "passing" end)] | @tsv' | \ - while read -r pr status; do - echo "Closing PR #$pr (checks: $status)" - if [ "$status" = "failing" ]; then - gh pr close "$pr" -R "${{ github.repository }}" -c "This PR has been open for more than 30 days without activity. Note: CI checks were failing, which may be why it wasn't reviewed. Feel free to reopen or create a new PR if you'd like to continue working on this." - else - gh pr close "$pr" -R "${{ github.repository }}" -c "This PR has been open for more than 30 days without activity. Feel free to reopen or create a new PR if you'd like to continue working on this." + set -o pipefail + CUTOFF=$(date -u -d '6 days ago' +%Y-%m-%d) + gh pr list -R "$REPO" --state open --search "updated:<$CUTOFF -author:jdx -label:keep-open draft:false" --json number,mergeStateStatus,statusCheckRollup --limit 500 | \ + jq -r ' + def failed_check: + (.statusCheckRollup | length > 0) and + ([.statusCheckRollup // [] | .[] | ((.conclusion // .state // "") | ascii_upcase)] | any(. == "FAILURE" or . == "ERROR" or . == "TIMED_OUT" or . == "ACTION_REQUIRED")); + + .[] + | failed_check as $failed + | ([.statusCheckRollup // [] | .[] | ((.conclusion // .state // "") | ascii_upcase)] | any(. == "CANCELLED")) as $cancelled + | (.mergeStateStatus == "DIRTY") as $conflicted + | (.mergeStateStatus == "UNKNOWN") as $unknown + | if $failed and $conflicted then [.number, "failing checks and merge conflicts"] + elif $failed then [.number, "failing checks"] + elif $conflicted then [.number, "merge conflicts"] + elif $cancelled then [.number, "cancelled checks", "warn"] + elif $unknown then [.number, "unknown merge state", "warn"] + else empty + end + | @tsv + ' | \ + while IFS=$'\t' read -r pr reason action; do + if [ "$action" = "warn" ]; then + echo "Skipping PR #$pr ($reason)" + continue fi + echo "Closing PR #$pr ($reason)" + gh pr close "$pr" -R "$REPO" -c "This PR has been inactive for at least 7 days and currently has $reason. Feel free to reopen or create a new PR if you'd like to continue working on this." || echo "Warning: failed to close PR #$pr, skipping" done