Files
awesome-copilot/.github/workflows/review-escalation.yml
T
0c54bcee82 Phase 1: distributed CODEOWNERS and automated reviewer routing (#4184) (#4189)
* Phase 1: distributed CODEOWNERS and automated reviewer routing (#4184)

- Replace the individual default CODEOWNERS owner with the core-maintainer
  team and add domain team ownership for extensions, plugins, content,
  hooks, workflows, .github, and eng.
- Add .github/review-routing.yml as the single source of truth for reviewer
  pools, routes, labels, and SLA settings (ships in dry-run mode).
- Add eng/review-routing.mjs (+ unit tests) with business-day math,
  least-loaded reviewer selection, routing and escalation planning, and
  workflow_run artifact validation.
- Add Review Routing (workflow_run/dispatch/schedule), Review Routing Request
  (read-only needs-reviewer reader), and Review Escalation (weekday schedule)
  workflows.
- Add needs-reviewer, review-overdue, review-escalated to setup-labels.
- Document the model in docs/maintainers/review-routing.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address review feedback on reviewer routing and escalation

- Escalate at routedOn + escalation business days (Mon -> Fri)
- Leave labels untouched when requestReviewers fails and fail the job
- Serialize read/plan/apply per PR across routing and escalation
- Treat Label PR Intent artifact labels as authoritative
- Re-route to an uncovered pool on head changes without restarting SLA
- Use the due-label timestamp as the review boundary
- Fall back to pool/escalation teams and report what was requested

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address second-round review feedback on review routing (#4184)

- CODEOWNERS: route /hooks/ and /workflows/ to content reviewers, drop the
  separate /.github/workflows/ rule, and leave per-resource owner lines as on
  main (their removal is deferred to a separate cleanup).
- Routing: ignore pending requests for unavailable reviewers when checking
  coverage; needs-reviewer excludes prior reviewers and changes nothing when no
  new reviewer can be requested.
- Escalation comments show the actual routing date.
- Cap sweep matrices at 256 targets and defer the remainder.
- Docs: best-effort load balancing, matrix cap, and needs-reviewer semantics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Route workflow and hooks labels to the content pool (#4184)

Drop the workflow-security reviewer pool per maintainer feedback: agentic
workflows and hooks are reviewed like other content, and the repository's
own GitHub Actions workflows stay with core maintainers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Pin routing contract in tests; make CONTRIBUTING routing note dry-run aware

- Assert the exact label-to-pool mapping and priority order, pool set, and default/escalation pools for the shipped config.
- CONTRIBUTING.md: describe automatic routing as active only once dry_run is turned off.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Simplify dry_run validation condition

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Aaron Powell <me@aaron-powell.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-01 00:51:10 +00:00

161 lines
6.4 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Review Escalation
# Enforces the review SLA from .github/review-routing.yml for routed PRs
# (those with a `review-due:YYYY-MM-DD` label):
# - no review after the due date -> request a backup (or a team), add `review-overdue`
# - no review escalation_business_days after routing -> request the
# core-maintainer pool, add `review-escalated`
# - first human review since routing -> clear the SLA labels
# Business days skip weekends and configured holidays, so the schedule only
# runs Monday–Friday.
#
# The read-only `plan` job selects PRs needing action; the `escalate` job then
# handles each PR in the `review-routing-pr-<number>` concurrency group shared
# with Review Routing. Only trusted default-branch code is executed.
on:
schedule:
- cron: "0 14 * * 1-5"
workflow_dispatch:
permissions: {}
jobs:
plan:
runs-on: ubuntu-latest
permissions:
contents: read
issues: read
pull-requests: read
outputs:
targets: ${{ steps.plan.outputs.targets }}
steps:
- name: Checkout default branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Select pull requests needing SLA action
id: plan
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const path = require('path');
const { pathToFileURL } = require('url');
const routing = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'review-routing.mjs')).href);
const config = routing.loadReviewRoutingConfig(path.join(process.env.GITHUB_WORKSPACE, '.github', 'review-routing.yml'));
const plans = await routing.planEscalationSweep({ github, owner: context.repo.owner, repo: context.repo.repo, config });
const actionable = plans.filter((plan) => ['reviewed', 'overdue', 'escalate'].includes(plan.action));
await core.summary.addRaw(routing.formatResultsSummary(actionable, { title: 'Review escalation plan', dryRun: config.dryRun })).write();
const limited = routing.limitTargets(actionable.map((plan) => ({ pr: plan.prNumber })));
if (limited.deferred > 0) core.warning(`${limited.deferred} PR(s) exceed the ${routing.MAX_MATRIX_TARGETS}-job matrix limit and are deferred to the next run.`);
core.setOutput('targets', JSON.stringify(limited.targets));
escalate:
needs: plan
if: needs.plan.outputs.targets != '' && needs.plan.outputs.targets != '[]'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
strategy:
fail-fast: false
# Sequential so each PR sees reviewer load updated by the previous one.
max-parallel: 1
matrix:
target: ${{ fromJSON(needs.plan.outputs.targets) }}
concurrency:
group: review-routing-pr-${{ matrix.target.pr }}
cancel-in-progress: false
steps:
- name: Checkout default branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Apply review SLA
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
TARGET_PR: ${{ matrix.target.pr }}
with:
script: |
const path = require('path');
const { pathToFileURL } = require('url');
const routing = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'review-routing.mjs')).href);
const config = routing.loadReviewRoutingConfig(path.join(process.env.GITHUB_WORKSPACE, '.github', 'review-routing.yml'));
const prNumber = Number(process.env.TARGET_PR);
if (!Number.isInteger(prNumber) || prNumber < 1) throw new Error(`Invalid target PR: ${process.env.TARGET_PR}`);
const plan = await routing.escalatePullRequest({
github,
owner: context.repo.owner,
repo: context.repo.repo,
config,
prNumber,
log: core,
});
await core.summary.addRaw(routing.formatResultsSummary([plan], { title: `Review escalation for #${prNumber}`, dryRun: config.dryRun })).write();
if (plan.requestError) core.setFailed(`Reviewer request failed for PR #${prNumber}: ${plan.requestError}`);
cleanup:
needs: plan
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Checkout default branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Delete stale review-due labels
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const path = require('path');
const { pathToFileURL } = require('url');
const routing = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'review-routing.mjs')).href);
const config = routing.loadReviewRoutingConfig(path.join(process.env.GITHUB_WORKSPACE, '.github', 'review-routing.yml'));
const deleted = await routing.cleanupStaleDueLabels({ github, owner: context.repo.owner, repo: context.repo.repo, config, log: core });
core.info(`Stale review-due labels ${config.dryRun ? 'that would be ' : ''}deleted: ${deleted.join(', ') || 'none'}`);