feat(external-plugins): fast-track first-party reviews and note org ownership

Auto-accept first-party cards on the review board with /approve suggested,
skipping AI review unless the maintainer opts the card back into Unreviewed.
Show ownership and fast-track pills on cards.

Add a "Repository owner" reviewer-signal row to intake comments for
organization-owned repositories; user-owned repositories get no row.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 821f4796-505e-4fb8-96cf-0b5bbf80a636
This commit is contained in:
Aaron Powell
2026-10-13 10:51:42 +11:00
co-authored by Copilot App
parent 36667634be
commit b653c23244
8 changed files with 158 additions and 2 deletions
@@ -18,6 +18,11 @@ Open it by asking Copilot to "open the plugin review board" (canvas id `external
back and it's recorded on the card along with your guidance.
- **Refresh** fetches open `external-plugin` + `ready-for-review` issues, adds new ones, and removes any
that are closed or no longer ready for review.
- **First-party fast-track**: cards labelled `first party` (repository owned by a Microsoft/GitHub
organization) skip AI review and go straight to *Accept* with a suggested `/approve`. Only submissions
that passed automated intake reach the board, so hard validation failures are still held back by intake.
Fast-track never overrides an existing review or a bucket you chose; drag a card to *Unreviewed* to opt it
out and have it reviewed normally. Cards show *First party* / *Third party* pills from the intake labels.
- **Details panel** shows the AI review, the rendered issue body, and all comments.
- **Quick actions** post `/approve` (with an optional note) or `/reject <reason>` straight to the issue
after a confirm click. The reject reason is prefilled from the AI's suggested comment.
@@ -15,6 +15,33 @@ export const COLUMNS = [
];
const HISTORY_LIMIT = 200;
export const FIRST_PARTY_LABEL = "first party";
// Submissions from first-party organizations skip AI triage and land in Accept. Only cards without
// an AI review, queued review, or manual bucket are fast-tracked so maintainer choices are kept;
// dragging a card back to Unreviewed opts it out so it gets a normal AI review.
function applyFirstPartyFastTrack(item) {
if (
!item.labels?.includes(FIRST_PARTY_LABEL) ||
item.fastTrackDismissed ||
item.decision ||
item.review ||
item.manualColumn ||
item.reviewStatus === "queued"
) {
return false;
}
item.review = {
recommendation: "accept",
fastTracked: true,
rationale:
"First-party submission (repository owned by a Microsoft/GitHub organization) that passed automated intake validation; fast-tracked to approval.",
suggestedComment: "/approve",
reviewedAt: new Date().toISOString(),
};
item.reviewStatus = "reviewed";
return true;
}
function newQueueId() {
return randomBytes(6).toString("base64url");
@@ -101,6 +128,7 @@ export class BoardStore {
const incoming = new Map(issues.map((issue) => [String(issue.number), issue]));
const added = [];
const removed = [];
const fastTracked = [];
for (const [key, issue] of incoming) {
const existing = this.state.items[key];
if (existing) {
@@ -123,6 +151,7 @@ export class BoardStore {
};
added.push(issue.number);
}
if (applyFirstPartyFastTrack(this.state.items[key])) fastTracked.push(issue.number);
}
for (const key of Object.keys(this.state.items)) {
if (!incoming.has(key)) {
@@ -132,7 +161,7 @@ export class BoardStore {
}
this.state.lastRefreshedAt = new Date().toISOString();
await this.commit();
return { added, removed, total: incoming.size };
return { added, removed, fastTracked, total: incoming.size };
}
pendingReview(numbers) {
@@ -218,6 +247,7 @@ export class BoardStore {
const item = this.getItem(number);
if (item.decision) throw new Error(`#${number} has already been actioned.`);
if (column === "unreviewed") {
if (item.review?.fastTracked || item.labels?.includes(FIRST_PARTY_LABEL)) item.fastTrackDismissed = true;
item.review = null;
item.reviewStatus = "unreviewed";
item.manualColumn = null;
@@ -115,8 +115,15 @@ function signalPills(item) {
pills.push(h("span", { class: `pill pill-${level.toLowerCase()}`, title: "AGT contributor risk" }, `AGT ${level}`));
} else if (label === "external-plugin-canvas") {
pills.push(h("span", { class: "pill" }, "canvas"));
} else if (label === "first party") {
pills.push(h("span", { class: "pill pill-first-party", title: "Repository owned by a Microsoft/GitHub organization" }, "First party"));
} else if (label === "third party") {
pills.push(h("span", { class: "pill pill-third-party", title: "Repository owned by a third-party organization" }, "Third party"));
}
}
if (item.review?.fastTracked && !item.decision) {
pills.push(h("span", { class: "pill pill-approve", title: "First-party submission fast-tracked to approval" }, "Fast-tracked"));
}
if (item.decision) {
pills.push(h("span", { class: `pill pill-${item.decision.kind}` }, item.decision.kind === "approve" ? "Approved" : "Rejected"));
} else if (item.pendingDecision) {
@@ -391,7 +398,7 @@ function renderReview(item) {
return wrapper;
}
const rows = [
["Recommendation", RECOMMENDATION_LABELS[review.recommendation] + (item.manualColumn ? ` (you moved it to ${RECOMMENDATION_LABELS[item.manualColumn]})` : "")],
["Recommendation", RECOMMENDATION_LABELS[review.recommendation] + (review.fastTracked ? " (first-party fast-track, no AI review)" : "") + (item.manualColumn ? ` (you moved it to ${RECOMMENDATION_LABELS[item.manualColumn]})` : "")],
["Plugin", review.pluginName],
["Repository", review.repository ? h("a", { href: `https://github.com/${review.repository}`, target: "_blank", rel: "noopener noreferrer" }, review.repository) : null],
["Repo facts", review.repoFacts],
@@ -546,6 +553,7 @@ ui.refresh.addEventListener("click", () =>
const parts = [`${result.total} open`];
if (result.added.length) parts.push(`${result.added.length} new`);
if (result.removed.length) parts.push(`${result.removed.length} removed`);
if (result.fastTracked?.length) parts.push(`${result.fastTracked.length} first-party fast-tracked`);
toast(`Refreshed: ${parts.join(", ")}`);
if (selected != null && activeTab !== "review") loadDetail(selected, { force: true });
} catch (error) {
@@ -376,6 +376,16 @@ h2 {
background: var(--red-muted);
color: var(--red);
}
.pill-first-party {
border-color: transparent;
background: var(--green-muted);
color: var(--green);
}
.pill-third-party {
border-color: transparent;
background: var(--purple-muted);
color: var(--purple);
}
.pill-queued {
border-color: transparent;
background: var(--purple-muted);
@@ -69,3 +69,14 @@ and read the `/approve` / `/reject` comments on the most recent ones.
- `probably-reject` — likely reject, but something specific deserves a quick maintainer look.
- `needs-review` — genuine judgement call.
- `accept` — fits the approval pattern.
## Ownership labels
Intake labels submissions by who owns the plugin repository:
- `first party` — owned by a Microsoft/GitHub organization (Microsoft, GitHub, Azure, dotnet, MicrosoftDocs,
microsoftgraph). The board fast-tracks these straight to `accept` without an AI review. If one is sent for
review anyway, recommend `accept` unless there is a hard failure (broken/missing plugin contents,
security concern, or something that would have failed intake validation).
- `third party` — owned by another organization. This is a signal only; apply the normal decision pattern.
- No ownership label — owned by a user account (or ownership couldn't be determined).
+7
View File
@@ -1221,6 +1221,13 @@ function buildReviewSignalsCommentSection(reviewSignals) {
"|---|---|",
];
if (repository?.status === "pass") {
const ownershipNote = {
"first party": "Organization-owned (first party: Microsoft/GitHub organization)",
"third party": "Organization-owned (third party)",
}[repository.ownershipLabel];
if (ownershipNote) {
rows.push(`| Repository owner | ${ownershipNote} |`);
}
rows.push(
`| Repository age | ${repository.age_days === undefined ? "unknown" : `${repository.age_days} day(s)`} |`,
`| Repository activity | ${repository.signals?.length ? repository.signals.join("; ") : "no configured signal"} |`,
+12
View File
@@ -631,6 +631,17 @@ for (const [type, login, expectedLabel] of [
assert.equal(result.reviewSignals.repository.ownershipLabel, expectedLabel);
const mergedResult = applyQualityGateResult(result, { overall_status: "pass" });
assert.equal(mergedResult.reviewSignals.repository.ownershipLabel, expectedLabel);
const expectedNote = {
"first party": "| Repository owner | Organization-owned (first party: Microsoft/GitHub organization) |",
"third party": "| Repository owner | Organization-owned (third party) |",
}[expectedLabel];
for (const body of [result.commentBody, mergedResult.commentBody]) {
if (expectedNote) {
assert.ok(body.includes(expectedNote), `expected ownership note in:\n${body}`);
} else {
assert.doesNotMatch(body, /Repository owner/);
}
}
});
}
@@ -658,6 +669,7 @@ test("evaluateExternalPluginIssue classifies ownership even when the submission
assert.equal(result.valid, false);
assert.equal(result.reviewSignals.repository.ownershipLabel, "first party");
assert.match(result.commentBody, /\| Repository owner \| Organization-owned \(first party/);
});
test("evaluateExternalPluginIssue fails when ref and sha resolve to different commits", async () => {
@@ -0,0 +1,73 @@
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { BoardStore, effectiveColumn } from "../.github/extensions/external-plugin-review-board/lib/state.mjs";
function issue(number, labels = []) {
return {
number,
title: `[External Plugin]: Example ${number}`,
author: "contributor",
url: `https://github.com/github/awesome-copilot/issues/${number}`,
labels: ["external-plugin", "ready-for-review", ...labels],
};
}
async function createStore(t) {
const dir = await mkdtemp(path.join(os.tmpdir(), "epr-board-"));
t.after(() => rm(dir, { recursive: true, force: true }));
return new BoardStore(path.join(dir, "board.json"), "github/awesome-copilot");
}
test("first-party submissions are fast-tracked to accept on refresh", async (t) => {
const store = await createStore(t);
const result = await store.syncIssues([issue(1, ["first party"]), issue(2, ["third party"]), issue(3)]);
assert.deepEqual(result.fastTracked, [1]);
const first = store.getItem(1);
assert.equal(effectiveColumn(first), "accept");
assert.equal(first.reviewStatus, "reviewed");
assert.equal(first.review.fastTracked, true);
assert.equal(first.review.suggestedComment, "/approve");
assert.equal(effectiveColumn(store.getItem(2)), "unreviewed");
assert.equal(effectiveColumn(store.getItem(3)), "unreviewed");
assert.deepEqual(store.pendingReview().map((item) => item.number).sort(), [2, 3]);
});
test("fast-track applies when an existing card gains the first party label", async (t) => {
const store = await createStore(t);
await store.syncIssues([issue(1)]);
const result = await store.syncIssues([issue(1, ["first party"])]);
assert.deepEqual(result.fastTracked, [1]);
assert.equal(effectiveColumn(store.getItem(1)), "accept");
});
test("fast-track does not override an existing review or manual bucket", async (t) => {
const store = await createStore(t);
await store.syncIssues([issue(1), issue(2)]);
store.getItem(1).review = { recommendation: "needs-review", reviewedAt: new Date().toISOString() };
store.getItem(1).reviewStatus = "reviewed";
await store.moveItem(2, "probably-reject");
const result = await store.syncIssues([issue(1, ["first party"]), issue(2, ["first party"])]);
assert.deepEqual(result.fastTracked, []);
assert.equal(effectiveColumn(store.getItem(1)), "needs-review");
assert.equal(effectiveColumn(store.getItem(2)), "probably-reject");
});
test("moving a fast-tracked card to unreviewed opts it out of fast-track", async (t) => {
const store = await createStore(t);
await store.syncIssues([issue(1, ["first party"])]);
await store.moveItem(1, "unreviewed");
const result = await store.syncIssues([issue(1, ["first party"])]);
assert.deepEqual(result.fastTracked, []);
assert.equal(effectiveColumn(store.getItem(1)), "unreviewed");
assert.deepEqual(store.pendingReview().map((item) => item.number), [1]);
});