feat(external-plugins): label submissions by repository ownership

Classify org-owned repositories as first party (Microsoft, GitHub, Azure,
dotnet, MicrosoftDocs, microsoftgraph; case-insensitive) or third party.
User-owned repositories receive no ownership label.

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:45:29 +11:00
co-authored by Copilot App
parent 70ddff7fab
commit 36667634be
6 changed files with 180 additions and 5 deletions
+8
View File
@@ -52,6 +52,14 @@ jobs:
color: '1D76DB',
description: 'External plugin submission includes a canvas extension'
},
'first party': {
color: '1D76DB',
description: 'External plugin repository is owned by a Microsoft or GitHub first-party organization'
},
'third party': {
color: '5319E7',
description: 'External plugin repository is owned by another organization'
},
'hooks': {
color: 'C2E0C6',
description: 'PR touches hooks'
+3
View File
@@ -253,6 +253,7 @@ The public-submission policy builds on those rules and also requires `license` p
1. **Open an issue** using the external plugin issue form. Automation applies the `external-plugin` and `awaiting-review` labels.
2. **Automated intake validation** checks that the required fields are present and correctly formatted for a GitHub-hosted plugin. Invalid submissions are labeled `requires-submitter-fixes` with a comment explaining what must be fixed before maintainer review.
Intake also checks the repository owner's type using the GitHub API. Organization-owned repositories receive `first party` for the `Microsoft`, `GitHub`, `Azure`, `dotnet`, `MicrosoftDocs`, or `microsoftgraph` organizations (case-insensitive), or `third party` for any other organization. User-owned repositories receive neither label. Ownership labels are informational only and are updated on each intake pass.
3. **Automated quality gates** run after metadata validation:
- `vally lint` against the submitted plugin path/ref/sha
- install smoke test via Copilot CLI against an ephemeral marketplace entry generated from the submission
@@ -289,6 +290,8 @@ Maintainers are responsible for confirming that the submission:
##### Review cadence and label semantics
- `external-plugin`: applied to every public external plugin submission and retained on approved issues so scheduled review automation can find them later
- `first party`: the submitted repository is owned by a first-party organization (`Microsoft`, `GitHub`, `Azure`, `dotnet`, `MicrosoftDocs`, or `microsoftgraph`)
- `third party`: the submitted repository is owned by another organization; user-owned repositories have neither ownership label
- `awaiting-review`: initial intake state before automation finishes validating the issue
- `ready-for-review`: the issue passed automated intake checks and is waiting on a maintainer decision
- `requires-submitter-fixes`: automated intake found metadata or quality-gate issues; submitter updates are required before human review
+25 -2
View File
@@ -7,6 +7,14 @@ export const EXTERNAL_PLUGIN_INTAKE_LABELS = Object.freeze({
color: "1D76DB",
description: "External plugin submission includes a canvas extension",
},
"first party": {
color: "1D76DB",
description: "External plugin repository is owned by a Microsoft or GitHub first-party organization",
},
"third party": {
color: "5319E7",
description: "External plugin repository is owned by another organization",
},
"awaiting-review": {
color: "FBCA04",
description: "Submission is waiting for automated intake validation",
@@ -53,7 +61,14 @@ async function removeLabel({ github, owner, repo, issueNumber, name }) {
}
}
export async function syncExternalPluginIntakeLabels({ github, owner, repo, issueNumber, desiredLabels }) {
export async function syncExternalPluginIntakeLabels({
github,
owner,
repo,
issueNumber,
desiredLabels,
syncOwnershipLabels = false,
}) {
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner,
repo,
@@ -63,7 +78,10 @@ export async function syncExternalPluginIntakeLabels({ github, owner, repo, issu
const currentManagedLabels = currentLabels
.map((label) => label.name)
.filter((name) => EXTERNAL_PLUGIN_INTAKE_SYNC_LABELS.includes(name));
.filter((name) =>
EXTERNAL_PLUGIN_INTAKE_SYNC_LABELS.includes(name) ||
(syncOwnershipLabels && ["first party", "third party"].includes(name))
);
const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name));
const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name));
@@ -137,6 +155,10 @@ export async function applyExternalPluginIntakeEvaluation({
if (evaluation.isCanvasPlugin) {
desiredLabels.add("external-plugin-canvas");
}
const ownershipLabel = evaluation.reviewSignals?.repository?.ownershipLabel;
if (ownershipLabel === "first party" || ownershipLabel === "third party") {
desiredLabels.add(ownershipLabel);
}
await syncExternalPluginIntakeLabels({
github,
@@ -144,6 +166,7 @@ export async function applyExternalPluginIntakeEvaluation({
repo,
issueNumber,
desiredLabels,
syncOwnershipLabels: true,
});
await upsertExternalPluginIntakeComment({
+70
View File
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
applyExternalPluginIntakeEvaluation,
syncExternalPluginIntakeLabels,
} from "./external-plugin-intake-state.mjs";
function mockGitHub(currentLabels) {
const labels = new Set(currentLabels);
return {
labels,
async paginate() {
return [...labels].map((name) => ({ name }));
},
rest: {
issues: {
listLabelsOnIssue() {},
async addLabels({ labels: addedLabels }) {
for (const name of addedLabels) labels.add(name);
},
async removeLabel({ name }) {
labels.delete(name);
},
async listComments() {
return { data: [] };
},
async createComment() {},
},
},
};
}
for (const state of ["ready-for-review", "requires-submitter-fixes", "awaiting-review", "rejected"]) {
for (const ownershipLabel of ["first party", "third party", null, undefined]) {
test(`intake syncs ${ownershipLabel ?? "no ownership label"} in ${state} state`, async () => {
const github = mockGitHub(["external-plugin", "first party", "third party", "unrelated"]);
const { desiredLabels } = await applyExternalPluginIntakeEvaluation({
github,
owner: "github",
repo: "awesome-copilot",
issueNumber: 1,
evaluation: {
intakeState: state,
isCanvasPlugin: true,
reviewSignals: { repository: { ownershipLabel } },
commentMarker: "<!-- external-plugin-intake -->",
commentBody: "Intake result",
},
});
const expected = ["external-plugin", state, "external-plugin-canvas"];
if (ownershipLabel) expected.push(ownershipLabel);
assert.deepEqual(desiredLabels, new Set(expected));
assert.deepEqual(github.labels, new Set([...expected, "unrelated"]));
});
}
}
test("state-only label sync preserves ownership labels for maintainer overrides and PR checks", async () => {
const github = mockGitHub(["external-plugin", "requires-submitter-fixes", "first party"]);
await syncExternalPluginIntakeLabels({
github,
owner: "github",
repo: "awesome-copilot",
issueNumber: 1,
desiredLabels: new Set(["external-plugin", "ready-for-review"]),
});
assert.deepEqual(github.labels, new Set(["external-plugin", "ready-for-review", "first party"]));
});
+14 -1
View File
@@ -75,6 +75,14 @@ const MARKETING_SIGNAL_PATTERNS = Object.freeze([
["trial", /\bfree\s+trial\b|\bstart\s+your\s+trial\b|\bget\s+started\s+free\b/i],
["checkout", /\bstripe\b|\bcheckout\b|\bsubscribe\s+now\b|\bbuy\s+now\b/i],
]);
const FIRST_PARTY_ORGANIZATIONS = new Set([
"microsoft",
"github",
"azure",
"dotnet",
"microsoftdocs",
"microsoftgraph",
]);
const EXTERNAL_PLUGIN_ROOT_MANIFEST_PATHS = Object.freeze([
".github/plugin/plugin.json",
".plugin/plugin.json",
@@ -532,6 +540,11 @@ function buildRepositorySignals(repository) {
return {
status: "pass",
ownershipLabel: repository.owner?.type === "Organization"
? FIRST_PARTY_ORGANIZATIONS.has(repository.owner.login?.toLowerCase())
? "first party"
: "third party"
: null,
age_days: ageDays,
stars: repository.stargazers_count,
watchers: repository.subscribers_count,
@@ -1346,7 +1359,7 @@ export async function evaluateExternalPluginIssue({ issue, token, runId, owner,
}
}
if (parsed.plugin?.source?.repo && (parsed.plugin?.source?.ref || parsed.plugin?.source?.sha)) {
if (parsed.plugin?.source?.repo) {
repositorySignals = await validateRemoteRepository(parsed.plugin.source.repo, parsed.plugin.source, errors, warnings, token);
}
const homepageSignals = await inspectHomepage(parsed.plugin?.homepage);
+60 -2
View File
@@ -567,11 +567,12 @@ test("pinned homepage dispatcher connects to the validated address and preserves
}
});
function installMockFetch() {
function installMockFetch({ repositoryOwner = { type: "User", login: "octo" }, repositoryStatus = 200 } = {}) {
global.fetch = async (url) => {
const requestUrl = String(url);
if (requestUrl === `https://api.github.com/repos/${INTAKE_REPO}`) {
return jsonResponse({
owner: repositoryOwner,
private: false,
archived: false,
created_at: new Date().toISOString(),
@@ -579,7 +580,7 @@ function installMockFetch() {
subscribers_count: 3,
forks_count: 0,
open_issues_count: 4,
});
}, { status: repositoryStatus });
}
if (
@@ -602,6 +603,63 @@ function installMockFetch() {
};
}
for (const [type, login, expectedLabel] of [
["Organization", "github", "first party"],
["Organization", "Microsoft", "first party"],
["Organization", "MICROSOFT", "first party"],
["Organization", "GitHub", "first party"],
["Organization", "Azure", "first party"],
["Organization", "azure", "first party"],
["Organization", "dotnet", "first party"],
["Organization", "DotNet", "first party"],
["Organization", "MicrosoftDocs", "first party"],
["Organization", "microsoftdocs", "first party"],
["Organization", "microsoftgraph", "first party"],
["Organization", "MicrosoftGraph", "first party"],
["Organization", "vendor", "third party"],
["Organization", "github-vendor", "third party"],
["User", "microsoft", null],
["User", "octo", null],
]) {
test(`evaluateExternalPluginIssue classifies ${type} owner ${login}`, async () => {
installMockFetch({ repositoryOwner: { type, login } });
const result = await evaluateExternalPluginIssue({
issue: { body: buildIssueBody({ sha: PROVIDED_SHA }) },
});
assert.equal(result.valid, true);
assert.equal(result.reviewSignals.repository.ownershipLabel, expectedLabel);
const mergedResult = applyQualityGateResult(result, { overall_status: "pass" });
assert.equal(mergedResult.reviewSignals.repository.ownershipLabel, expectedLabel);
});
}
for (const status of [404, 503]) {
test(`evaluateExternalPluginIssue does not classify unavailable repository metadata (${status})`, async () => {
installMockFetch({ repositoryStatus: status });
const result = await evaluateExternalPluginIssue({
issue: { body: buildIssueBody({ sha: PROVIDED_SHA }) },
});
assert.equal(result.reviewSignals.repository.ownershipLabel, undefined);
if (status === 404) {
assert.match(result.errors.join("\n"), /was not found/);
} else {
assert.match(result.warnings.join("\n"), /could not verify GitHub repository/);
}
});
}
test("evaluateExternalPluginIssue classifies ownership even when the submission has no release locator", async () => {
installMockFetch({ repositoryOwner: { type: "Organization", login: "github" } });
const result = await evaluateExternalPluginIssue({
issue: { body: buildIssueBody({}) },
});
assert.equal(result.valid, false);
assert.equal(result.reviewSignals.repository.ownershipLabel, "first party");
});
test("evaluateExternalPluginIssue fails when ref and sha resolve to different commits", async () => {
installMockFetch();
const issue = { body: buildIssueBody({ ref: "v1.2.3", sha: PROVIDED_SHA }) };