workflow: Rework permission system of status: labels (#790)

This commit is contained in:
MonsterDruide1 2025-10-31 17:34:04 +01:00 committed by GitHub
parent 01c17e9a80
commit 8f4afefbf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 231 additions and 170 deletions

View file

@ -0,0 +1,24 @@
name: Add "status:waiting for review"-label to new PRs
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
issues: write
jobs:
new_pr:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target' && 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']
})

View file

@ -0,0 +1,36 @@
name: PR-Review submitted
on:
pull_request_review:
types: [submitted]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Write review info to artifact
run: |
mkdir -p review-data
cat > review-data/event.json <<'EOF'
{
"repository": {
"owner": "${{ github.repository_owner }}",
"name": "${{ github.event.repository.name }}"
},
"pull_request": {
"number": ${{ github.event.pull_request.number }},
"author": "${{ github.event.pull_request.user.login }}"
},
"review": {
"state": "${{ github.event.review.state }}",
"body": "${{ github.event.review.body || '' }}",
"reviewer": "${{ github.event.review.user.login }}"
}
}
EOF
- name: Upload review info
uses: actions/upload-artifact@v4
with:
name: review-event
path: review-data/event.json

View file

@ -0,0 +1,86 @@
name: Add "status:"-label to PRs based on current state
on:
workflow_run:
workflows:
- PR-Review submitted
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
review_update:
runs-on: ubuntu-latest
steps:
- name: Download review info
uses: actions/download-artifact@v4
with:
name: review-event
path: .
- name: Update status labels based on review
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('event.json', 'utf8'));
const owner = data.repository.owner;
const repo = data.repository.name;
const prNumber = data.pull_request.number;
const reviewState = data.review.state;
const reviewBody = data.review.body;
const R = data.review.reviewer; // reviewer
const P = data.pull_request.author; // 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: prNumber
})
const statusLabels = existingLabels.filter(l => l.name.startsWith('status:'))
for (const label of statusLabels) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
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 `reviewState == 'approved'`
const is_approved = reviewState === 'approved' || reviewBody.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: ${reviewState}, PR author: ${P}, Reviewer: ${R}, Repo owner: ${O}`)
}
if (newLabel) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [newLabel]
})
core.info(`Added label: ${newLabel}`)
} else {
core.info('No new status label added.')
}

View file

@ -0,0 +1,85 @@
name: Promote "status:"-label to "ready to merge" if soaked for long enough
on:
schedule:
- cron: '0 * * * *' # every hour, on the hour
permissions:
pull-requests: write
issues: write
jobs:
promote_ready_to_merge:
runs-on: ubuntu-latest
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}`)
}
}

View file

@ -1,170 +0,0 @@
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
issues: 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}`)
}
}