From 6b11e30d3464f2d3f59af0af4f22c910cb3bdbf1 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 18 Apr 2026 16:43:44 +0000 Subject: [PATCH 1/4] feat: add pending-maintainer workflow with conflict/CI/draft guards --- .github/workflows/pending-maintainer.yml | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/pending-maintainer.yml diff --git a/.github/workflows/pending-maintainer.yml b/.github/workflows/pending-maintainer.yml new file mode 100644 index 0000000..df87a35 --- /dev/null +++ b/.github/workflows/pending-maintainer.yml @@ -0,0 +1,105 @@ +name: Label pending-maintainer PRs + +on: + issue_comment: + types: [created] + pull_request_review: + types: [submitted] + workflow_dispatch: + +jobs: + check-pending: + if: github.event_name == 'workflow_dispatch' || github.event.issue.pull_request != null || github.event.pull_request != null + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + checks: read + steps: + - uses: actions/github-script@v7 + with: + script: | + const MAINTAINER = 'pending-maintainer'; + const CONTRIBUTOR = 'pending-contributor'; + + let prNumbers = []; + + if (context.eventName === 'workflow_dispatch') { + const prs = await github.rest.pulls.list({ + ...context.repo, + state: 'open', + per_page: 100 + }); + prNumbers = prs.data.map(pr => pr.number); + } else if (context.eventName === 'issue_comment') { + prNumbers = [context.payload.issue.number]; + } else if (context.eventName === 'pull_request_review') { + prNumbers = [context.payload.pull_request.number]; + } + + for (const prNumber of prNumbers) { + const { data: pr } = await github.rest.pulls.get({ + ...context.repo, + pull_number: prNumber + }); + + const labels = pr.labels.map(l => l.name); + + // Skip drafts — not ready for maintainer review + if (pr.draft) { + console.log(`#${prNumber} — draft, skipping`); + continue; + } + + // Skip if has merge conflicts — ball is on contributor + if (pr.mergeable === false) { + console.log(`#${prNumber} — has conflicts, skipping`); + continue; + } + + // Check CI status on head commit + const { data: status } = await github.rest.repos.getCombinedStatusForRef({ + ...context.repo, + ref: pr.head.sha + }); + const { data: checks } = await github.rest.checks.listForRef({ + ...context.repo, + ref: pr.head.sha, + per_page: 100 + }); + const ciRed = status.state === 'failure' + || checks.check_runs.some(c => c.conclusion === 'failure'); + if (ciRed) { + console.log(`#${prNumber} — CI failing, skipping`); + continue; + } + + // Check last comment is from PR author + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, + issue_number: prNumber, + per_page: 1, + direction: 'desc' + }); + if (comments.length === 0) continue; + + const lastCommenter = comments[0].user.login; + if (lastCommenter !== pr.user.login) continue; + + // All conditions met: not draft, no conflicts, CI green, author replied + if (!labels.includes(MAINTAINER)) { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: prNumber, + labels: [MAINTAINER] + }); + } + if (labels.includes(CONTRIBUTOR)) { + await github.rest.issues.removeLabel({ + ...context.repo, + issue_number: prNumber, + name: CONTRIBUTOR + }).catch(() => {}); + } + console.log(`#${prNumber} — all clear, set ${MAINTAINER}`); + } From cb351a2721f225d98194464b410e2ca6cead4948 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 18 Apr 2026 16:44:27 +0000 Subject: [PATCH 2/4] feat: skip pending-maintainer flip when closing-soon is applied --- .github/workflows/pending-maintainer.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pending-maintainer.yml b/.github/workflows/pending-maintainer.yml index df87a35..00232ac 100644 --- a/.github/workflows/pending-maintainer.yml +++ b/.github/workflows/pending-maintainer.yml @@ -51,6 +51,12 @@ jobs: continue; } + // Skip if closing-soon — contributor has incomplete work + if (labels.includes('closing-soon')) { + console.log(`#${prNumber} — closing-soon, skipping`); + continue; + } + // Skip if has merge conflicts — ball is on contributor if (pr.mergeable === false) { console.log(`#${prNumber} — has conflicts, skipping`); From a17db6e225317e062ffed313108044afe79893da Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 18 Apr 2026 16:46:23 +0000 Subject: [PATCH 3/4] feat: add needs-rebase/wontfix guards and filter bot comments --- .github/workflows/pending-maintainer.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pending-maintainer.yml b/.github/workflows/pending-maintainer.yml index 00232ac..a6f03f5 100644 --- a/.github/workflows/pending-maintainer.yml +++ b/.github/workflows/pending-maintainer.yml @@ -57,9 +57,15 @@ jobs: continue; } - // Skip if has merge conflicts — ball is on contributor - if (pr.mergeable === false) { - console.log(`#${prNumber} — has conflicts, skipping`); + // Skip if has merge conflicts or already labeled needs-rebase + if (pr.mergeable === false || labels.includes('needs-rebase')) { + console.log(`#${prNumber} — has conflicts or needs-rebase, skipping`); + continue; + } + + // Skip if wontfix or not-planned + if (labels.includes('wontfix') || labels.includes('not-planned')) { + console.log(`#${prNumber} — wontfix/not-planned, skipping`); continue; } @@ -80,16 +86,16 @@ jobs: continue; } - // Check last comment is from PR author - const { data: comments } = await github.rest.issues.listComments({ + // Check last comment from a human (skip bot comments) is from PR author + const { data: allComments } = await github.rest.issues.listComments({ ...context.repo, issue_number: prNumber, - per_page: 1, - direction: 'desc' + per_page: 100 }); - if (comments.length === 0) continue; + const humanComments = allComments.filter(c => !c.user.type || c.user.type !== 'Bot'); + if (humanComments.length === 0) continue; - const lastCommenter = comments[0].user.login; + const lastCommenter = humanComments[humanComments.length - 1].user.login; if (lastCommenter !== pr.user.login) continue; // All conditions met: not draft, no conflicts, CI green, author replied From 4c542d83f430583959186de5da996c29ce97ef56 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 18 Apr 2026 16:48:09 +0000 Subject: [PATCH 4/4] feat: add hourly schedule as safety net trigger --- .github/workflows/pending-maintainer.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pending-maintainer.yml b/.github/workflows/pending-maintainer.yml index a6f03f5..522f442 100644 --- a/.github/workflows/pending-maintainer.yml +++ b/.github/workflows/pending-maintainer.yml @@ -1,6 +1,8 @@ name: Label pending-maintainer PRs on: + schedule: + - cron: '0 * * * *' # hourly safety net issue_comment: types: [created] pull_request_review: @@ -9,7 +11,7 @@ on: jobs: check-pending: - if: github.event_name == 'workflow_dispatch' || github.event.issue.pull_request != null || github.event.pull_request != null + if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event.issue.pull_request != null || github.event.pull_request != null runs-on: ubuntu-latest permissions: pull-requests: write @@ -24,7 +26,7 @@ jobs: let prNumbers = []; - if (context.eventName === 'workflow_dispatch') { + if (context.eventName === 'workflow_dispatch' || context.eventName === 'schedule') { const prs = await github.rest.pulls.list({ ...context.repo, state: 'open',