mirror of
https://github.com/github/awesome-copilot.git
synced 2026-05-29 18:11:45 +00:00
Adding a new /rerun-intake command for when updates are required (#1786)
* Adding a new /rerun-intake command for when updates are required Reruns the intake process if feedback is given that will require the submitter to update something about the submittion. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Use rerun command constant in parser regex Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
This commit is contained in:
@@ -118,7 +118,7 @@ jobs:
|
|||||||
'',
|
'',
|
||||||
'### Required fixes',
|
'### Required fixes',
|
||||||
'',
|
'',
|
||||||
...(errors.length > 0 ? errors.map((error) => `- ${error}`) : ['- Re-run intake validation by updating the issue details.'])
|
...(errors.length > 0 ? errors.map((error) => `- ${error}`) : ['- Edit the issue details and let intake rerun automatically, or comment `/rerun-intake` to trigger it again on demand.'])
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
@@ -493,7 +493,7 @@ jobs:
|
|||||||
'',
|
'',
|
||||||
reason,
|
reason,
|
||||||
'',
|
'',
|
||||||
'If you address the feedback, open a new external plugin submission issue with the updated details.'
|
'If you address the feedback, edit this issue with the updated details and have the issue author or a maintainer comment `/rerun-intake` to re-run automated intake.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ on:
|
|||||||
issues:
|
issues:
|
||||||
types: [opened, edited, reopened]
|
types: [opened, edited, reopened]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: external-plugin-intake-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
@@ -36,81 +40,10 @@ jobs:
|
|||||||
RESULT_JSON: ${{ steps.evaluation.outputs.result }}
|
RESULT_JSON: ${{ steps.evaluation.outputs.result }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const managedLabels = {
|
const path = require('path');
|
||||||
'external-plugin': {
|
const { pathToFileURL } = require('url');
|
||||||
color: 'FEF2C0',
|
|
||||||
description: 'Public external plugin submission'
|
|
||||||
},
|
|
||||||
'awaiting-review': {
|
|
||||||
color: 'FBCA04',
|
|
||||||
description: 'Submission is waiting for automated intake validation'
|
|
||||||
},
|
|
||||||
'ready-for-review': {
|
|
||||||
color: '0E8A16',
|
|
||||||
description: 'Submission passed intake validation and is ready for maintainer review'
|
|
||||||
},
|
|
||||||
'approved': {
|
|
||||||
color: '1D76DB',
|
|
||||||
description: 'Submission was approved by a maintainer'
|
|
||||||
},
|
|
||||||
'rejected': {
|
|
||||||
color: 'B60205',
|
|
||||||
description: 'Submission was rejected or failed intake validation'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function ensureLabel(name, config) {
|
const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href);
|
||||||
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 syncManagedLabels(issueNumber, desiredLabels) {
|
|
||||||
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
|
|
||||||
|
|
||||||
const managedForSync = ['external-plugin', 'awaiting-review', 'ready-for-review', 'rejected'];
|
|
||||||
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
per_page: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentManagedLabels = currentLabels
|
|
||||||
.map((label) => label.name)
|
|
||||||
.filter((name) => managedForSync.includes(name));
|
|
||||||
|
|
||||||
const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name));
|
|
||||||
const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name));
|
|
||||||
|
|
||||||
if (labelsToAdd.length > 0) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
labels: labelsToAdd
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const name of labelsToRemove) {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = JSON.parse(process.env.RESULT_JSON);
|
const result = JSON.parse(process.env.RESULT_JSON);
|
||||||
const issueNumber = context.issue.number;
|
const issueNumber = context.issue.number;
|
||||||
@@ -128,40 +61,14 @@ jobs:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const desiredLabels = result.valid
|
await intakeState.applyExternalPluginIntakeEvaluation({
|
||||||
? new Set(['external-plugin', 'ready-for-review'])
|
github,
|
||||||
: new Set(['external-plugin', 'rejected']);
|
|
||||||
|
|
||||||
await syncManagedLabels(issueNumber, desiredLabels);
|
|
||||||
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: issueNumber,
|
issueNumber,
|
||||||
per_page: 100
|
evaluation: result
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingComment = comments.find((comment) =>
|
|
||||||
comment.user?.login === 'github-actions[bot]' &&
|
|
||||||
comment.body?.includes(result.commentMarker)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingComment) {
|
|
||||||
await github.rest.issues.updateComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
comment_id: existingComment.id,
|
|
||||||
body: result.commentBody
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
body: result.commentBody
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.valid && issueState === 'open') {
|
if (!result.valid && issueState === 'open') {
|
||||||
await github.rest.issues.update({
|
await github.rest.issues.update({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
name: External Plugin Rerun Intake Commands
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: external-plugin-intake-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
handle-command:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!github.event.issue.pull_request &&
|
||||||
|
startsWith(github.event.comment.body, '/rerun-intake')
|
||||||
|
steps:
|
||||||
|
- name: Checkout staged branch
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
ref: staged
|
||||||
|
|
||||||
|
- name: Re-run external plugin intake
|
||||||
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const path = require('path');
|
||||||
|
const { pathToFileURL } = require('url');
|
||||||
|
|
||||||
|
const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href);
|
||||||
|
const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href);
|
||||||
|
|
||||||
|
const commentAuthor = context.payload.comment.user?.login;
|
||||||
|
if (!commentAuthor || context.payload.comment.user?.type === 'Bot' || commentAuthor === 'github-actions[bot]') {
|
||||||
|
core.info('Ignoring /rerun-intake from a bot or unknown actor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intake.parseRerunIntakeCommand(context.payload.comment.body)) {
|
||||||
|
core.info('No supported /rerun-intake command was found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: currentIssue } = await github.rest.issues.get({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelNames = new Set((currentIssue.labels || []).map((label) => label.name));
|
||||||
|
const isExternalPluginIssue =
|
||||||
|
labelNames.has('external-plugin') ||
|
||||||
|
String(currentIssue.body || '').includes(intake.ISSUE_FORM_MARKER);
|
||||||
|
if (!isExternalPluginIssue) {
|
||||||
|
core.info('Ignoring /rerun-intake because the issue is not an external plugin submission.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelNames.has('approved') || labelNames.has('re-review-due') || labelNames.has('re-review-follow-up')) {
|
||||||
|
core.info('Ignoring /rerun-intake because the issue is already approved or in the six-month re-review flow.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueAuthor = currentIssue.user?.login;
|
||||||
|
const isIssueAuthor = Boolean(issueAuthor && commentAuthor === issueAuthor);
|
||||||
|
|
||||||
|
let hasWriteAccess = false;
|
||||||
|
if (!isIssueAuthor) {
|
||||||
|
const permission = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
username: commentAuthor
|
||||||
|
});
|
||||||
|
hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isIssueAuthor && !hasWriteAccess) {
|
||||||
|
core.info(`Ignoring /rerun-intake because ${commentAuthor} is neither the issue author nor a maintainer.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRerunFromCurrentState = currentIssue.state === 'open' || labelNames.has('rejected');
|
||||||
|
if (!canRerunFromCurrentState) {
|
||||||
|
core.info('Ignoring /rerun-intake because the issue is closed outside the intake/rejection flow.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluation = await intake.evaluateExternalPluginIssue({
|
||||||
|
issue: currentIssue,
|
||||||
|
token: process.env.GITHUB_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
await intakeState.applyExternalPluginIntakeEvaluation({
|
||||||
|
github,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issueNumber: context.issue.number,
|
||||||
|
evaluation
|
||||||
|
});
|
||||||
|
|
||||||
|
if (evaluation.valid && currentIssue.state === 'closed' && labelNames.has('rejected')) {
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
state: 'open'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!evaluation.valid && currentIssue.state === 'open') {
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
state: 'closed'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -167,10 +167,11 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
|
|||||||
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`
|
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`
|
||||||
4. The shared validator in `eng/external-plugin-validation.mjs` is the canonical source of truth for external plugin data rules; reuse it instead of duplicating checks in scripts or workflows
|
4. The shared validator in `eng/external-plugin-validation.mjs` is the canonical source of truth for external plugin data rules; reuse it instead of duplicating checks in scripts or workflows
|
||||||
5. Submission issues move through `external-plugin` + `awaiting-review` -> `ready-for-review` -> `approved` or `rejected`
|
5. Submission issues move through `external-plugin` + `awaiting-review` -> `ready-for-review` -> `approved` or `rejected`
|
||||||
6. Maintainers make the decision with `/approve` or `/reject <reason>` issue comments; approved issues are closed and used as the six-month re-review anchor
|
6. After issue edits, the issue author or a maintainer can comment `/rerun-intake` to re-run automated intake without opening a new submission issue
|
||||||
7. Approval automation creates or updates the PR against `staged`, updates `plugins/external.json`, and regenerates marketplace outputs
|
7. Maintainers make the decision with `/approve` or `/reject <reason>` issue comments; approved issues are closed and used as the six-month re-review anchor
|
||||||
8. Nightly re-review automation finds closed `external-plugin` + `approved` issues that are at least six months old, applies `re-review-due`, and opens or updates a tracking issue for maintainers
|
8. Approval automation creates or updates the PR against `staged`, updates `plugins/external.json`, and regenerates marketplace outputs
|
||||||
9. Maintainers complete re-review on the original approved submission issue with `/re-review-keep`, `/re-review-needs-changes`, or `/re-review-remove`; keep resets the issue `closed_at`, and remove opens a PR against `staged`
|
9. Nightly re-review automation finds closed `external-plugin` + `approved` issues that are at least six months old, applies `re-review-due`, and opens or updates a tracking issue for maintainers
|
||||||
|
10. Maintainers complete re-review on the original approved submission issue with `/re-review-keep`, `/re-review-needs-changes`, or `/re-review-remove`; keep resets the issue `closed_at`, and remove opens a PR against `staged`
|
||||||
|
|
||||||
### Testing Instructions
|
### Testing Instructions
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -230,9 +230,10 @@ 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.
|
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 closed with a comment explaining what must be fixed before resubmitting.
|
2. **Automated intake validation** checks that the required fields are present and correctly formatted for a GitHub-hosted plugin. Invalid submissions are closed with a comment explaining what must be fixed before resubmitting.
|
||||||
3. **Ready for maintainer review**: if the issue passes intake validation, automation removes `awaiting-review` and adds `ready-for-review`.
|
3. **Ready for maintainer review**: if the issue passes intake validation, automation removes `awaiting-review` and adds `ready-for-review`.
|
||||||
4. **Maintainer decision**: a maintainer with write access performs the manual review, then comments `/approve` or `/reject <reason>` on the issue. Commands from non-maintainers are ignored.
|
4. **Requesting another intake pass**: after updating the issue body, the issue author or a maintainer can comment `/rerun-intake` to re-run automated intake on demand. Open issues still re-trigger intake automatically on edit, but closed rejected issues need `/rerun-intake`.
|
||||||
5. **Approval path**: on `/approve`, automation removes `ready-for-review`, adds `approved`, closes the issue, and opens or updates a PR against `staged` that updates `plugins/external.json` and generated marketplace outputs.
|
5. **Maintainer decision**: a maintainer with write access performs the manual review, then comments `/approve` or `/reject <reason>` on the issue. Commands from non-maintainers are ignored.
|
||||||
6. **Rejection path**: on `/reject <reason>`, automation removes `ready-for-review`, adds `rejected`, closes the issue, and records the reason in an issue comment. Submitters can open a new issue after addressing the feedback.
|
6. **Approval path**: on `/approve`, automation removes `ready-for-review`, adds `approved`, closes the issue, and opens or updates a PR against `staged` that updates `plugins/external.json` and generated marketplace outputs.
|
||||||
|
7. **Rejection path**: on `/reject <reason>`, automation removes `ready-for-review`, adds `rejected`, closes the issue, and records the reason in an issue comment. After addressing the feedback, update the same issue and use `/rerun-intake` to re-queue intake.
|
||||||
|
|
||||||
##### Maintainer review responsibilities
|
##### Maintainer review responsibilities
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
export const EXTERNAL_PLUGIN_INTAKE_LABELS = Object.freeze({
|
||||||
|
"external-plugin": {
|
||||||
|
color: "FEF2C0",
|
||||||
|
description: "Public external plugin submission",
|
||||||
|
},
|
||||||
|
"awaiting-review": {
|
||||||
|
color: "FBCA04",
|
||||||
|
description: "Submission is waiting for automated intake validation",
|
||||||
|
},
|
||||||
|
"ready-for-review": {
|
||||||
|
color: "0E8A16",
|
||||||
|
description: "Submission passed intake validation and is ready for maintainer review",
|
||||||
|
},
|
||||||
|
approved: {
|
||||||
|
color: "1D76DB",
|
||||||
|
description: "Submission was approved by a maintainer",
|
||||||
|
},
|
||||||
|
rejected: {
|
||||||
|
color: "B60205",
|
||||||
|
description: "Submission was rejected or failed intake validation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const EXTERNAL_PLUGIN_INTAKE_SYNC_LABELS = Object.freeze([
|
||||||
|
"external-plugin",
|
||||||
|
"awaiting-review",
|
||||||
|
"ready-for-review",
|
||||||
|
"rejected",
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function ensureLabel({ github, owner, repo, name, config }) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
name,
|
||||||
|
color: config.color,
|
||||||
|
description: config.description,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 422) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeLabel({ github, owner, repo, issueNumber, name }) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncExternalPluginIntakeLabels({ github, owner, repo, issueNumber, desiredLabels }) {
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(EXTERNAL_PLUGIN_INTAKE_LABELS).map(([name, config]) =>
|
||||||
|
ensureLabel({ github, owner, repo, name, config })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentManagedLabels = currentLabels
|
||||||
|
.map((label) => label.name)
|
||||||
|
.filter((name) => EXTERNAL_PLUGIN_INTAKE_SYNC_LABELS.includes(name));
|
||||||
|
|
||||||
|
const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name));
|
||||||
|
const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name));
|
||||||
|
|
||||||
|
if (labelsToAdd.length > 0) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
labels: labelsToAdd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of labelsToRemove) {
|
||||||
|
await removeLabel({ github, owner, repo, issueNumber, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertExternalPluginIntakeComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
marker,
|
||||||
|
body,
|
||||||
|
}) {
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingComment = comments.find(
|
||||||
|
(comment) => comment.user?.login === "github-actions[bot]" && comment.body?.includes(marker)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingComment) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyExternalPluginIntakeEvaluation({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
evaluation,
|
||||||
|
}) {
|
||||||
|
const desiredLabels = evaluation.valid
|
||||||
|
? new Set(["external-plugin", "ready-for-review"])
|
||||||
|
: new Set(["external-plugin", "rejected"]);
|
||||||
|
|
||||||
|
await syncExternalPluginIntakeLabels({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
desiredLabels,
|
||||||
|
});
|
||||||
|
|
||||||
|
await upsertExternalPluginIntakeComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
marker: evaluation.commentMarker,
|
||||||
|
body: evaluation.commentBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { desiredLabels };
|
||||||
|
}
|
||||||
@@ -6,7 +6,13 @@ import { fileURLToPath } from "url";
|
|||||||
import { ROOT_FOLDER } from "./constants.mjs";
|
import { ROOT_FOLDER } from "./constants.mjs";
|
||||||
import { readExternalPlugins, validateExternalPlugin } from "./external-plugin-validation.mjs";
|
import { readExternalPlugins, validateExternalPlugin } from "./external-plugin-validation.mjs";
|
||||||
|
|
||||||
const ISSUE_FORM_MARKER = "<!-- external-plugin-submission -->";
|
export const ISSUE_FORM_MARKER = "<!-- external-plugin-submission -->";
|
||||||
|
export const EXTERNAL_PLUGIN_INTAKE_COMMENT_MARKER = "<!-- external-plugin-intake -->";
|
||||||
|
export const RERUN_INTAKE_COMMAND = "/rerun-intake";
|
||||||
|
const RERUN_INTAKE_COMMAND_PATTERN = new RegExp(
|
||||||
|
`^\\s*${RERUN_INTAKE_COMMAND.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
||||||
|
"m",
|
||||||
|
);
|
||||||
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
||||||
|
|
||||||
const REQUIRED_CHECKLIST_ITEMS = [
|
const REQUIRED_CHECKLIST_ITEMS = [
|
||||||
@@ -261,6 +267,10 @@ export function parseExternalPluginIssueBody(body) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseRerunIntakeCommand(body) {
|
||||||
|
return RERUN_INTAKE_COMMAND_PATTERN.test(String(body ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
export async function evaluateExternalPluginIssue({ issue, token } = {}) {
|
export async function evaluateExternalPluginIssue({ issue, token } = {}) {
|
||||||
const issueBody = issue?.body ?? "";
|
const issueBody = issue?.body ?? "";
|
||||||
const parsed = parseExternalPluginIssueBody(issueBody);
|
const parsed = parseExternalPluginIssueBody(issueBody);
|
||||||
@@ -294,7 +304,7 @@ export async function evaluateExternalPluginIssue({ issue, token } = {}) {
|
|||||||
const dedupedErrors = [...new Set(errors)];
|
const dedupedErrors = [...new Set(errors)];
|
||||||
const dedupedWarnings = [...new Set(warnings)];
|
const dedupedWarnings = [...new Set(warnings)];
|
||||||
const valid = dedupedErrors.length === 0;
|
const valid = dedupedErrors.length === 0;
|
||||||
const marker = "<!-- external-plugin-intake -->";
|
const marker = EXTERNAL_PLUGIN_INTAKE_COMMENT_MARKER;
|
||||||
const normalizedKeywords = parsed.plugin?.keywords?.length ? parsed.plugin.keywords.join(", ") : "_None provided_";
|
const normalizedKeywords = parsed.plugin?.keywords?.length ? parsed.plugin.keywords.join(", ") : "_None provided_";
|
||||||
const notes = parsed.additionalNotes ?? "_No additional notes provided._";
|
const notes = parsed.additionalNotes ?? "_No additional notes provided._";
|
||||||
const payload = parsed.plugin
|
const payload = parsed.plugin
|
||||||
@@ -333,7 +343,7 @@ export async function evaluateExternalPluginIssue({ issue, token } = {}) {
|
|||||||
"## ❌ External plugin intake failed",
|
"## ❌ External plugin intake failed",
|
||||||
"",
|
"",
|
||||||
"This submission did not pass automated intake validation, so the issue has been closed.",
|
"This submission did not pass automated intake validation, so the issue has been closed.",
|
||||||
"Update the issue form, then reopen the issue to run intake validation again.",
|
`Edit the issue form to address the fixes below, then have the issue author or a maintainer comment \`${RERUN_INTAKE_COMMAND}\` to re-run intake for this closed submission.`,
|
||||||
"",
|
"",
|
||||||
"### Required fixes",
|
"### Required fixes",
|
||||||
"",
|
"",
|
||||||
|
|||||||
Reference in New Issue
Block a user