mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 05:31:27 +00:00
fae6a92c9d
* fix: Allow label operations on pull requests in external plugin approval workflow The sync-merged-pr-labels job needs pull-requests: write permission to add/remove labels on merged PRs. Previously it only had issues: write which is for issues, not pull requests. This fixes the permission error when workflows try to modify PR labels from a non-contributor account. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: Handle 403 permission errors when creating external plugin intake labels When running on PRs from fork contributors, the GitHub token may not have permission to create labels in the repository. This is expected and should not cause the workflow to fail. Allow the ensureLabel function to gracefully handle 403 Forbidden errors in addition to 422 (label already exists) errors. This fixes the sync-pr-state job failure in external-plugin-pr-quality-gates.yml when run on PRs from external contributors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: Centralize label management into a single workflow_dispatch workflow Create a new 'setup-labels' workflow that is manually dispatched and handles all label creation and updates. This workflow: - Creates all labels used by the repository - Updates descriptions if labels already exist - Reports success/failure counts - Fails if any labels cannot be created All individual workflows now assume labels exist and will fail (loudly) if they don't. This makes it clear to maintainers when the setup-labels workflow needs to be dispatched: - label-pr-intent.yml - skill-check-comment.yml - external-plugin-approval-command.yml - external-plugin-command-router.yml - external-plugin-rereview.yml - external-plugin-rereview-command.yml - eng/external-plugin-intake-state.mjs This approach is better because: - Single source of truth for label definitions - Avoids permission issues with fork contributors - Clear failure modes when labels are missing - Easier to maintain consistent label configuration - No more scattered label creation logic across workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unused ensureLabel methods and managedLabels constants Labels are now centrally managed by the setup-labels workflow and assumed to exist in all other workflows. Removed: - ensureLabel() methods from all 6 workflows and 1 JS module - managedLabels constants that were only used by ensureLabel - Promise.all() calls that invoked ensureLabel for each label - Updated syncManagedLabels in skill-check-comment.yml to remove ensureLabel call All workflows now assume labels exist and will fail if they don't, which is the desired behavior—it signals maintainers to dispatch the setup-labels workflow when new labels need to be created. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
239 lines
9.8 KiB
YAML
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 \`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')
|
|
: '',
|
|
].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}.`);
|