Review SLA #257
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # --------------------------------------------------------------------------- | |
| # Review SLA – nudge reviewers when PRs wait too long for review | |
| # Runs every 6 hours and escalates based on wait time. | |
| # --------------------------------------------------------------------------- | |
| name: Review SLA | |
| on: | |
| schedule: | |
| - cron: '0 */6 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| review-sla: | |
| name: Check review SLA | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Enforce review SLA | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const MS_PER_HOUR = 60 * 60 * 1000; | |
| const SLA_24H = 24 * MS_PER_HOUR; | |
| const SLA_48H = 48 * MS_PER_HOUR; | |
| const SLA_72H = 72 * MS_PER_HOUR; | |
| const MARKER_24 = '<!-- review-sla-24h -->'; | |
| const MARKER_72 = '<!-- review-sla-72h -->'; | |
| const now = Date.now(); | |
| // List all open PRs | |
| const prs = await github.paginate(github.rest.pulls.list, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| }); | |
| for (const pr of prs) { | |
| // Only check PRs that have review requests pending | |
| if (!pr.requested_reviewers?.length && !pr.requested_teams?.length) { | |
| continue; | |
| } | |
| // Determine how long the PR has been waiting | |
| // Use the PR creation time as the baseline | |
| const createdAt = new Date(pr.created_at).getTime(); | |
| const waitMs = now - createdAt; | |
| // Check existing reviews – if any review exists, skip | |
| const { data: reviews } = await github.rest.pulls.listReviews({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| }); | |
| if (reviews.length > 0) { | |
| continue; // Already has at least one review | |
| } | |
| // Fetch existing bot comments to avoid duplicates | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| }); | |
| // --- 72h escalation --- | |
| if (waitMs >= SLA_72H) { | |
| const already72 = comments.some(c => c.body.includes(MARKER_72)); | |
| if (!already72) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: `${MARKER_72}\n🚨 **Review SLA exceeded (72 h)** — This PR has been waiting for review for over 72 hours. Please prioritize.\n\n@team — urgent review needed.`, | |
| }); | |
| } | |
| // Ensure the attention label is present | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: ['status/needs-attention'], | |
| }); | |
| } catch (_) { /* label may already exist */ } | |
| continue; | |
| } | |
| // --- 48h – add label --- | |
| if (waitMs >= SLA_48H) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: ['status/needs-attention'], | |
| }); | |
| } catch (_) { /* label may already exist */ } | |
| } | |
| // --- 24h – first nudge --- | |
| if (waitMs >= SLA_24H) { | |
| const already24 = comments.some(c => c.body.includes(MARKER_24)); | |
| if (!already24) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: `${MARKER_24}\n⏳ **Review reminder** — This PR has been waiting for review for over 24 hours.\n\n@team — this PR needs a review.`, | |
| }); | |
| } | |
| } | |
| } |