Route workshop changes to workshop reviewers (#4743)

* Cleanup of CODEOWNER

Removed a lot of excessive codeowners
Created a new team for reviewing/approving workshops
Removed workflow that did auto-assignment

* Route workshop changes to workshop reviewers

Add workshop ownership, intent labeling, reviewer routing, approval-domain coverage, and maintainer documentation for the new workshop review team.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-10-09 11:34:51 +11:00
committed by GitHub
co-authored by Copilot
parent 7cce7cfb4b
commit c06f74af0d
12 changed files with 53 additions and 2120 deletions
+6
View File
@@ -52,10 +52,16 @@ pools:
team: github/awesome-copilot-content-reviewers
reviewers: [] # TODO: add at least three reviewers for skills, agents, and instructions.
backup: [] # TODO: add backup content reviewers.
workshops:
team: github/awesome-copilot-workshop-reviewers
reviewers: [] # TODO: add at least three workshop reviewers.
backup: [] # TODO: add backup workshop reviewers.
# Intent labels (applied by .github/workflows/label-pr-intent*.yml) mapped to
# pools. Order is priority: the first route whose label is on the PR wins.
routes:
- label: workshops
pool: workshops
- label: canvas-extension
pool: canvas
- label: external-plugin
+6
View File
@@ -143,6 +143,12 @@ domains:
content:
paths: ["agents/**", "instructions/**", "skills/**", "workflows/**", "hooks/**"]
pools: ["content"]
workshops:
paths:
- "website/**/cli-for-beginners/**"
- "website/**/app-for-beginners/**"
- "website/**/copilot-workshops/**"
pools: ["workshops"]
# Files outside every domain (repository automation such as `.github/workflows/**`,
# `eng/**`, review policy, and docs) are owned by the core pools.
File diff suppressed because one or more lines are too long
-94
View File
@@ -1,94 +0,0 @@
---
description: "Updates the CODEOWNERS file when a maintainer comments #codeowner on a pull request"
on:
issue_comment:
types: [created]
if: ${{ contains(github.event.comment.body, '#codeowner') && github.event.issue.pull_request }}
permissions:
contents: read
pull-requests: read
issues: read
copilot-requests: write
tools:
github:
toolsets: [default]
safe-outputs:
create-pull-request:
base-branch: main
title-prefix: "[codeowner] "
draft: false
add-comment:
max: 1
noop:
---
# Codeowner Update Agent
You are a CODEOWNERS file updater for the **${{ github.repository }}** repository. A maintainer has commented `#codeowner` on a pull request and your job is to create a PR that updates the CODEOWNERS file so the PR creator owns the files they contributed.
## Context
- **Triggering PR:** #${{ github.event.issue.number }}
- **Comment author:** @${{ github.actor }}
- **Comment body:** "${{ steps.sanitized.outputs.text }}"
## Instructions
### 1. Validate the Trigger
- Confirm the comment body contains `#codeowner`.
- If the check fails, exit with a `noop`.
### 2. Gather PR Information
- Use the GitHub tools to get details for pull request #${{ github.event.issue.number }}.
- Record the **PR creator's username** (the user who opened the PR — `user.login` from the PR object).
- Retrieve the full list of files changed in the PR.
### 3. Filter Relevant Files
Only include files whose paths start with one of these directories:
- `agents/`
- `skills/`
- `instructions/`
- `workflows/`
- `hooks/`
- `plugins/`
If **no files** match these directories, exit with a `noop` message: "No files in agents/, skills/, instructions/, workflows/, hooks/, or plugins/ directories were found in this PR."
### 4. Read the Current CODEOWNERS File
Read the `CODEOWNERS` file from the root of the repository on the `main` branch. Parse its existing entries so you can avoid creating duplicates.
### 5. Build the Updated CODEOWNERS File
For each matched file path from the PR:
- Construct a CODEOWNERS entry: `/<file-path> @<pr-creator-username>`
- For files inside `skills/`, `hooks/`, or `plugins/` (which are directory-based resources), use the **directory pattern** instead of individual file paths. For example, if the PR touches `skills/my-skill/SKILL.md` and `skills/my-skill/template.txt`, add a single entry: `/skills/my-skill/ @<pr-creator-username>`
- If an entry for that exact path already exists in CODEOWNERS, **replace** the owner with the PR creator rather than adding a duplicate line.
Insert the new entries in the CODEOWNERS file grouped under a comment block:
```
# Added via #codeowner from PR #<pr-number>
/<path> @<username>
```
Place this block at the end of the file, before any trailing newline.
### 6. Create the Pull Request
Use `create-pull-request` to open a PR with the updated `CODEOWNERS` file. The PR should:
- **Title:** `Update CODEOWNERS for PR #${{ github.event.issue.number }}`
- **Body:** A summary listing every new or updated CODEOWNERS entry and the PR creator who was assigned ownership.
- **Only modify the `CODEOWNERS` file** — do not touch any other files.
### 7. Post a Confirmation Comment
After successfully creating the PR, use `add-comment` on the triggering PR to let the team know. Include a link to the newly created CODEOWNERS PR.
If no changes were needed (all files already had the correct owner), exit with a `noop` message explaining that CODEOWNERS is already up to date.
@@ -46,6 +46,7 @@ jobs:
'agent',
'instructions',
'new-submission',
'workshops',
'website-update',
'external-plugin',
'hooks',
+8
View File
@@ -27,6 +27,7 @@ jobs:
'agent': true,
'instructions': true,
'new-submission': true,
'workshops': true,
'website-update': true,
'external-plugin': true,
'hooks': true,
@@ -78,6 +79,9 @@ jobs:
websiteUpdate: [
/^website\//
],
workshops: [
/^website\/(?:.*\/)?(?:cli-for-beginners|app-for-beginners|copilot-workshops)\//
],
externalPlugin: [
/^plugins\/external\.json$/
],
@@ -130,6 +134,10 @@ jobs:
desiredLabels.add('instructions');
}
if (filenames.some((filename) => matchesAny(filename, patterns.workshops))) {
desiredLabels.add('workshops');
}
if (filenames.some((filename) => matchesAny(filename, patterns.websiteUpdate))) {
desiredLabels.add('website-update');
}
+4
View File
@@ -36,6 +36,10 @@ jobs:
color: '006B75',
description: 'PR adds at least one new contribution'
},
'workshops': {
color: '7057FF',
description: 'PR touches Copilot workshop content'
},
'website-update': {
color: '0052CC',
description: 'PR touches website content or code'
+5 -57
View File
@@ -2,9 +2,7 @@
#
# - Teams own paths; no individual is the default owner. Do not add individual
# users to the `*` rule or to the team block below.
# - The last matching rule wins, so per-resource owners appended at the end of
# this file (for example by the `#codeowner` automation) override the team
# defaults for their specific paths.
# - The last matching rule wins.
# - Reviewer pools used for automatic review requests live in
# .github/review-routing.yml.
@@ -22,57 +20,7 @@
/.github/ @github/awesome-copilot-core-maintainers
/eng/ @github/awesome-copilot-core-maintainers
# Adding Dan Wahlin as the owner for the CLI for Beginners content and images
/website/src/content/docs/learning-hub/cli-for-beginners/ @DanWahlin
/website/src/images/cli-for-beginners/ @DanWahlin
# Copilot app for Beginners course and public images
/website/src/content/docs/learning-hub/app-for-beginners/ @DanWahlin
/website/public/images/learning-hub/copilot-app-for-beginners/ @DanWahlin
# Agentic Workflows
/workflows/ @brunoborges
/.github/workflows/validate-agentic-workflows-pr.yml @brunoborges
# Copilot review policy
/.github/skills/code-review/ @aaronpowell
/.github/copilot-instructions.md @aaronpowell
# Added via #codeowner from PR #930
/plugins/automate-this/ @dvelton
/skills/automate-this/ @dvelton
# Added via #codeowner from PR #915
/skills/cli-mastery/ @DUBSOpenHub
# Added via #codeowner from PR #929
/plugins/napkin/ @dvelton
/skills/napkin/ @dvelton
# Added via #codeowner from PR #888
/skills/github-issues/ @labudis
# Added via #codeowner from PR #884
/skills/github-issues/ @labudis
# Added via #codeowner from PR #889
/skills/copilot-spaces/ @labudis
# Added via #codeowner from PR #978
/agents/doublecheck.agent.md @dvelton
/plugins/doublecheck/ @dvelton
/skills/doublecheck/ @dvelton
# Added via #codeowner from PR #990
/skills/issue-fields-migration/ @labudis
# Added via #codeowner from PR #1009
/skills/publish-to-pages/ @AndreaGriffiths11
# Added via #codeowner from PR #1049
/skills/codeql/ @VeVarunSharma
/skills/dependabot/ @VeVarunSharma
/skills/secret-scanning/ @VeVarunSharma
# Added via #codeowner from PR #1118
/agents/github-actions-node-upgrade.agent.md @joshjohanning
# Copilot workshops
/website/**/cli-for-beginners/ @github/awesome-copilot-workshop-reviewers
/website/**/app-for-beginners/ @github/awesome-copilot-workshop-reviewers
/website/**/copilot-workshops/ @github/awesome-copilot-workshop-reviewers
+4 -2
View File
@@ -13,12 +13,13 @@ The root [`CODEOWNERS`](../../CODEOWNERS) file assigns every path to a team, not
| `/plugins/` | `@github/awesome-copilot-plugin-reviewers` |
| `/agents/`, `/instructions/`, `/skills/`, `/hooks/`, `/workflows/` | `@github/awesome-copilot-content-reviewers` |
| `/.github/` (including GitHub Actions workflows), `/eng/` | `@github/awesome-copilot-core-maintainers` |
| Copilot workshop content under `/website/` | `@github/awesome-copilot-workshop-reviewers` |
GitHub applies the **last** matching rule, so the per-resource entries at the bottom of the file override the team defaults for their paths. Those entries are added through the `#codeowner` command (`.github/workflows/codeowner-update.md`), which only appends a block at the end of the file. The existing per-resource entries are kept as they are for now; removing them is planned as a separate cleanup. When you edit `CODEOWNERS` by hand:
GitHub applies the **last** matching rule, so the workshop entries at the bottom of the file override the default team for their paths. When you edit `CODEOWNERS` by hand:
- Keep the `*` rule and the domain team block at the top of the file.
- Never add an individual user to the `*` rule.
- Append new per-resource owners at the end of the file.
- Add more-specific path owners after the broader rules they override.
## Reviewer pools (`.github/review-routing.yml`)
@@ -44,6 +45,7 @@ Current routes, in priority order:
| Intent label | Pool |
|---|---|
| `workshops` | `workshops` |
| `canvas-extension` | `canvas` |
| `external-plugin`, `plugin` | `plugin` |
| `skills`, `agent`, `instructions`, `workflow`, `hooks` | `content` |
+1
View File
@@ -23,6 +23,7 @@ export const INTENT_LABELS = Object.freeze([
"new-submission",
"plugin",
"skills",
"workshops",
"website-update",
"workflow",
]);
+5 -1
View File
@@ -46,9 +46,11 @@ function rawConfig(overrides = {}) {
canvas: { team: "github/canvas", reviewers: ["canvas1", "canvas2", "canvas3"], backup: ["canvasBackup"] },
plugin: { team: "github/plugin", reviewers: ["plugin1"], backup: [] },
content: { team: "github/content", reviewers: [], backup: [] },
workshops: { team: "github/workshops", reviewers: ["workshop1"], backup: [] },
automation: { team: "github/automation", reviewers: ["auto1"], backup: ["autoBackup"] },
},
routes: [
{ label: "workshops", pool: "workshops" },
{ label: "workflow", pool: "automation" },
{ label: "hooks", pool: "automation" },
{ label: "canvas-extension", pool: "canvas" },
@@ -97,6 +99,7 @@ describe("configuration", () => {
assert.deepEqual(
loaded.routes.map((route) => [route.label, route.pool]),
[
["workshops", "workshops"],
["canvas-extension", "canvas"],
["external-plugin", "plugin"],
["plugin", "plugin"],
@@ -108,7 +111,7 @@ describe("configuration", () => {
["website-update", "core-maintainers"],
]
);
assert.deepEqual(Object.keys(loaded.pools).sort(), ["canvas", "content", "core-maintainers", "plugin"]);
assert.deepEqual(Object.keys(loaded.pools).sort(), ["canvas", "content", "core-maintainers", "plugin", "workshops"]);
assert.equal(loaded.defaultPool, "core-maintainers");
assert.equal(loaded.escalationPool, "core-maintainers");
});
@@ -165,6 +168,7 @@ describe("reviewer selection", () => {
assert.equal(selectPool(["skills", "plugin"], cfg), "plugin");
assert.equal(selectPool(["plugin", "canvas-extension"], cfg), "canvas");
assert.equal(selectPool(["canvas-extension", "workflow"], cfg), "automation");
assert.equal(selectPool(["website-update", "workshops"], cfg), "workshops");
assert.equal(selectPool(["website-update"], cfg), "core-maintainers");
});
+13 -1
View File
@@ -214,6 +214,7 @@ const routing = {
canvas: { team: "github/canvas", reviewers: ["canvasa"], backup: [] },
plugin: { team: "github/plugin", reviewers: [], backup: [] },
content: { reviewers: [] },
workshops: { team: "github/workshops", reviewers: ["workshopa"], backup: [] },
},
};
@@ -255,7 +256,11 @@ test("latest review state wins and changes requested blocks", () => {
});
test("medium tier requires a domain reviewer when the pool is staffed", () => {
const permissions = new Map([["alice", "write"], ["canvasa", "write"]]);
const permissions = new Map([
["alice", "write"],
["canvasa", "write"],
["workshopa", "write"],
]);
const files = [file("extensions/x/extension.mjs", { status: "added" })];
const base = { tier: "medium", tiers, author: "author", permissions, routing, files };
assert.equal(evaluateApprovals({ ...base, reviews: [review("alice", "APPROVED")] }).satisfied, false);
@@ -270,6 +275,13 @@ test("medium tier requires a domain reviewer when the pool is staffed", () => {
assert.equal(unstaffed.satisfied, true, "falls back to any writer when the domain pool is empty");
assert.ok(unstaffed.notes.length > 0);
const workshop = evaluateApprovals({
...base,
files: [file("website/src/content/docs/learning-hub/copilot-workshops/lesson.md", { status: "added" })],
reviews: [review("workshopa", "APPROVED")],
});
assert.equal(workshop.satisfied, true);
const noRouting = evaluateApprovals({ ...base, routing: null, reviews: [review("alice", "APPROVED")] });
assert.equal(noRouting.satisfied, true);
});