From 9d8c2c3759d7c6e50e2dada77b267e0fe7245bf1 Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Fri, 31 Oct 2025 01:13:53 +0100 Subject: [PATCH] workflow: Add status label to PRs (#778) --- .github/workflows/pr-status-label.yml | 169 ++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 .github/workflows/pr-status-label.yml diff --git a/.github/workflows/pr-status-label.yml b/.github/workflows/pr-status-label.yml new file mode 100644 index 00000000..4fd67752 --- /dev/null +++ b/.github/workflows/pr-status-label.yml @@ -0,0 +1,169 @@ +name: Add "status:"-label to PRs based on current state + +on: + pull_request_review: + types: [submitted] + pull_request: + types: [opened] + schedule: + - cron: '0 * * * *' # every hour, on the hour + +permissions: + pull-requests: write + +jobs: + new_pr: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.event.action == 'opened' + steps: + - name: Label new PR as "status:waiting for review" + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + ...context.repo, + issue_number: context.payload.pull_request.number, + labels: ['status:waiting for review'] + }) + + review_update: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_review' && github.event.action == 'submitted' + steps: + - name: Update status labels based on review + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request + const review = context.payload.review + const owner = context.repo.owner + const repo = context.repo.repo + + const R = review.user.login // reviewer + const P = pr.user.login // PR author + const O = owner // repo owner (by repo name) + + // 1. Remove all "status:[...]" labels + const { data: existingLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr.number + }) + + const statusLabels = existingLabels.filter(l => l.name.startsWith('status:')) + for (const label of statusLabels) { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr.number, + name: label.name + }).catch(() => {}) // ignore if already removed + } + + // 2. Determine new label + let newLabel = null + + // https://github.com/Reviewable/Reviewable/issues/1163 + // other users cannot approve reviewers, requires permissions to actually say `review.state == 'approved'` + const is_approved = review.state === 'approved' || review.body.includes("complete! all files reviewed, all discussions resolved") + + if (is_approved && R === O) { + newLabel = 'status:approved' // standard way of approval: owner appoves PR + } else if (is_approved && P === O && R !== O) { + newLabel = 'status:approved' // if owner is author, someone else must approve + } else if (P === R) { + newLabel = 'status:waiting for review' + } else if (P !== R) { + newLabel = 'status:waiting for author' + } else { + core.info('No matching condition for new label.') + core.info(`Review state: ${review.state}, PR author: ${P}, Reviewer: ${R}, Repo owner: ${O}`) + } + + if (newLabel) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr.number, + labels: [newLabel] + }) + core.info(`Added label: ${newLabel}`) + } else { + core.info('No new status label added.') + } + + promote_ready_to_merge: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + steps: + - name: Promote approved PRs to "ready to merge" + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const now = new Date() + + // 1. Search for open PRs with "status:approved" + const searchQuery = `repo:${owner}/${repo} is:pr is:open label:"status:approved"` + const { data: searchResults } = await github.rest.search.issuesAndPullRequests({ q: searchQuery }) + + for (const pr of searchResults.items) { + const prNumber = pr.number + + // Get full PR info (for timestamps) + const { data: prData } = await github.rest.pulls.get({ + owner, + repo, + pull_number: prNumber + }) + + const createdAt = new Date(prData.created_at) + const hoursSinceCreation = (now - createdAt) / (1000 * 60 * 60) + if (hoursSinceCreation < 24) { + core.info(`Skipping #${prNumber} (created ${hoursSinceCreation.toFixed(1)}h ago)`) + continue + } + + // Find the last "status:" label change (via timeline events) + const { data: events } = await github.rest.issues.listEvents({ + owner, + repo, + issue_number: prNumber, + per_page: 100 + }) + + const lastStatusChange = events + .filter(e => e.event === 'labeled' || e.event === 'unlabeled') + .filter(e => e.label && e.label.name.startsWith('status:')) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0] + + if (lastStatusChange) { + const hoursSinceStatusChange = (now - new Date(lastStatusChange.created_at)) / (1000 * 60 * 60) + if (hoursSinceStatusChange < 12) { + core.info(`Skipping #${prNumber} (status changed ${hoursSinceStatusChange.toFixed(1)}h ago)`) + continue + } + } + + // 2. Conditions met → remove "status:approved", add "status:ready to merge" + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: prNumber, + name: 'status:approved' + }).catch(() => {}) + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: prNumber, + labels: ['status:ready to merge'] + }) + + core.info(`Promoted #${prNumber} to status:ready to merge`) + } catch (err) { + core.warning(`Failed to update #${prNumber}: ${err.message}`) + } + }