mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-13 15:24:25 +00:00
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:
co-authored by
Copilot App
parent
70ddff7fab
commit
36667634be
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"]));
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }) };
|
||||
|
||||
Reference in New Issue
Block a user