Files
awesome-copilot/.github/workflows/external-plugin-rereview.yml
T
Aaron Powell 0eb6062f94 chore(phase2): retarget all automation and contributor guidance from staged to main (#2122)
* chore(phase2): retarget all automation from staged to main

- publish.yml: trigger on main, publish only to marketplace
- check-pr-target.yml: invert — now blocks PRs targeting staged, welcomes main
- 10 PR validation workflows: branches [staged] → [main]
- external-plugin-command-router.yml: --base staged → main (3×), message text
- external-plugin-rereview-command.yml: --base staged → main (2×), message text
- external-plugin-rereview.yml: staged reference in review comment text
- external-plugin-intake.yml: ref: staged checkout → main
- external-plugin-pr-quality-gates.yml: ref: staged checkout → main
- external-plugin-quality-gates.yml: ref: staged checkout → main
- check-plugin-structure.yml: error messages updated for new branch model
- contributors.yml: ref and base target → main
- setup-labels.yml: targets-main label description updated
- cli-for-beginners-sync.md + .lock.yml: base-branch staged → main
- codeowner-update.md + .lock.yml: base-branch staged → main
- learning-hub-updater.md + .lock.yml: base-branch staged → main

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

* docs(phase2): update contributor guidance from staged to main

- CONTRIBUTING.md: branch from main, PR targets main; remove Phase 2 gate note
- AGENTS.md: PR target + external plugin PR automation references
- .github/pull_request_template.md: PR checklist targets main
- website/src/content/docs/learning-hub/agentic-workflows.md: PR target

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

* aw updates

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 14:36:53 +10:00

239 lines
9.8 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);
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)));
}
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 \`main\` 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')
: '',
].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}.`);