Files
awesome-copilot/.github/workflows/external-plugin-rereview.yml
T
Aaron Powell e66aa80240 feat: add public external plugin workflows (#1723)
* feat: add external plugin submission workflows

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

* minor adjustment to contributing guide

* fix: address external plugin review feedback

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

* Reverting some changes to the readme.agents.md file

* fix: address follow-up review feedback

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

* fix: tighten external plugin workflows

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-15 15:37:18 +10:00

272 lines
11 KiB
YAML

name: External Plugin Re-review
on:
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
sync-rereview:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Sync six-month re-review queue
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const path = require('path');
const { pathToFileURL } = require('url');
const rereview = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-rereview.mjs')).href);
const validation = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-validation.mjs')).href);
const managedLabels = {
[rereview.REREVIEW_LABELS.due]: {
color: 'FBCA04',
description: 'Approved external plugin is due for six-month re-review'
},
[rereview.REREVIEW_LABELS.followUp]: {
color: 'D4C5F9',
description: 'Six-month re-review needs maintainer follow-up before a final decision'
},
[rereview.REREVIEW_LABELS.removed]: {
color: 'B60205',
description: 'External plugin was removed from the marketplace after re-review'
}
};
async function ensureLabel(name, config) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
async function removeLabel(issueNumber, label) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
async function addLabel(issueNumber, label) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: [label]
});
}
function formatDate(dateValue) {
return new Date(dateValue).toISOString().slice(0, 10);
}
function daysPastThreshold(closedAt, threshold) {
const diff = Date.parse(threshold.toISOString()) - Date.parse(closedAt);
return Math.max(0, Math.floor(Math.abs(diff) / (1000 * 60 * 60 * 24)));
}
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
const { plugins, errors } = validation.readExternalPlugins({ policy: 'marketplace' });
if (errors.length > 0) {
core.setFailed(errors.join('\n'));
return;
}
const threshold = new Date();
threshold.setUTCDate(threshold.getUTCDate() - 183);
const approvedIssues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed',
labels: 'external-plugin,approved',
per_page: 100
});
const issueRecords = approvedIssues
.filter((issue) => !issue.pull_request && issue.closed_at)
.map((issue) => {
const match = rereview.matchExternalPluginForIssue(issue, plugins);
return {
issue,
match
};
});
const dueRecords = issueRecords.filter(({ issue, match }) => {
if (!match.plugin) {
return false;
}
return Date.parse(issue.closed_at) <= threshold.getTime();
});
const unmatchedDueRecords = issueRecords.filter(({ issue, match }) => {
if (match.plugin) {
return false;
}
return Date.parse(issue.closed_at) <= threshold.getTime();
});
const dueIssueNumbers = new Set([
...dueRecords.map((record) => record.issue.number),
...unmatchedDueRecords.map((record) => record.issue.number)
]);
for (const { issue, match } of issueRecords) {
const labelNames = new Set((issue.labels || []).map((label) => label.name));
const shouldHaveDueLabel = dueIssueNumbers.has(issue.number);
if (shouldHaveDueLabel && !labelNames.has(rereview.REREVIEW_LABELS.due)) {
await addLabel(issue.number, rereview.REREVIEW_LABELS.due);
}
if (!shouldHaveDueLabel && labelNames.has(rereview.REREVIEW_LABELS.due)) {
await removeLabel(issue.number, rereview.REREVIEW_LABELS.due);
}
if (shouldHaveDueLabel && match.plugin && labelNames.has(rereview.REREVIEW_LABELS.removed)) {
await removeLabel(issue.number, rereview.REREVIEW_LABELS.removed);
}
}
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
const existingTrackerIssues = openIssues
.filter((issue) => !issue.pull_request && issue.body?.includes(rereview.REREVIEW_REPORT_MARKER))
.sort((left, right) => left.number - right.number);
if (dueRecords.length === 0 && unmatchedDueRecords.length === 0) {
for (const tracker of existingTrackerIssues) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: tracker.number,
state: 'closed'
});
}
core.info('No external plugins are currently due for six-month re-review.');
return;
}
const dueRows = dueRecords
.sort((left, right) => Date.parse(left.issue.closed_at) - Date.parse(right.issue.closed_at))
.map(({ issue, match }) => {
const labelNames = new Set((issue.labels || []).map((label) => label.name));
const status = labelNames.has(rereview.REREVIEW_LABELS.followUp) ? 'Needs follow-up' : 'Awaiting decision';
const repo = match.plugin.source?.repo ?? match.submission.sourceRepo ?? '_unknown_';
return `| ${match.plugin.name} | ${match.plugin.version} | \`${repo}\` | #${issue.number} | ${formatDate(issue.closed_at)} | ${daysPastThreshold(issue.closed_at, threshold)} | ${status} |`;
});
const unmatchedRows = unmatchedDueRecords
.sort((left, right) => Date.parse(left.issue.closed_at) - Date.parse(right.issue.closed_at))
.map(({ issue, match }) => {
const pluginName = match.submission.pluginName ?? '_unknown_';
const repo = match.submission.sourceRepo ? `\`${match.submission.sourceRepo}\`` : '_unknown_';
return `| #${issue.number} | ${pluginName} | ${repo} | ${formatDate(issue.closed_at)} |`;
});
const body = [
rereview.REREVIEW_REPORT_MARKER,
'## 🔁 External plugin six-month re-review queue',
'',
'The following approved external plugin submissions have reached the six-month re-review threshold.',
'Review the linked plugin, then comment on the **original approved submission issue** with one of:',
'',
`- \`${rereview.REREVIEW_COMMANDS.keep}\` — renew the plugin for another six months`,
`- \`${rereview.REREVIEW_COMMANDS.needsChanges}\` — keep the plugin in the due queue while follow-up work happens`,
`- \`${rereview.REREVIEW_COMMANDS.remove}\` — open or update a PR against \`staged\` that removes the plugin from the marketplace`,
'',
`- **Threshold date used by this run:** ${formatDate(threshold.toISOString())}`,
'',
'### Plugins due now',
'',
dueRows.length > 0
? [
'| Plugin | Version | Source repo | Submission issue | Closed at | Days past threshold | Status |',
'|---|---|---|---:|---|---:|---|',
...dueRows
].join('\n')
: '_No currently listed plugins are due right now._',
unmatchedRows.length > 0
? [
'',
'### Approved issues without a current marketplace match',
'',
'These closed approved issues are older than six months, but no matching entry was found in `plugins/external.json`. Review them manually if the listing was renamed or removed outside the re-review flow.',
'',
'| Submission issue | Parsed plugin name | Parsed repo | Closed at |',
'|---:|---|---|---|',
...unmatchedRows
].join('\n')
: '',
].filter(Boolean).join('\n');
if (existingTrackerIssues.length > 0) {
const [primary, ...duplicates] = existingTrackerIssues;
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: primary.number,
title: '🔁 External Plugin Six-Month Review',
body,
labels: [rereview.REREVIEW_LABELS.due]
});
for (const duplicate of duplicates) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: duplicate.number,
state: 'closed'
});
}
core.info(`Updated re-review tracker issue #${primary.number}.`);
return;
}
const created = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '🔁 External Plugin Six-Month Review',
body,
labels: [rereview.REREVIEW_LABELS.due]
});
core.info(`Created re-review tracker issue #${created.data.number}.`);