Skip to content

Commit da14bc7

Browse files
committed
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.
1 parent 471895c commit da14bc7

7 files changed

Lines changed: 318 additions & 116 deletions

.github/workflows/external-plugin-approval-command.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ jobs:
118118
'',
119119
'### Required fixes',
120120
'',
121-
...(errors.length > 0 ? errors.map((error) => `- ${error}`) : ['- Re-run intake validation by updating the issue details.'])
121+
...(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.'])
122122
].join('\n');
123123
124124
const { data: comments } = await github.rest.issues.listComments({
@@ -493,7 +493,7 @@ jobs:
493493
'',
494494
reason,
495495
'',
496-
'If you address the feedback, open a new external plugin submission issue with the updated details.'
496+
'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.'
497497
].join('\n');
498498
499499
const { data: comments } = await github.rest.issues.listComments({

.github/workflows/external-plugin-intake.yml

Lines changed: 11 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ on:
44
issues:
55
types: [opened, edited, reopened]
66

7+
concurrency:
8+
group: external-plugin-intake-${{ github.event.issue.number }}
9+
cancel-in-progress: false
10+
711
permissions:
812
contents: read
913
issues: write
@@ -36,81 +40,10 @@ jobs:
3640
RESULT_JSON: ${{ steps.evaluation.outputs.result }}
3741
with:
3842
script: |
39-
const managedLabels = {
40-
'external-plugin': {
41-
color: 'FEF2C0',
42-
description: 'Public external plugin submission'
43-
},
44-
'awaiting-review': {
45-
color: 'FBCA04',
46-
description: 'Submission is waiting for automated intake validation'
47-
},
48-
'ready-for-review': {
49-
color: '0E8A16',
50-
description: 'Submission passed intake validation and is ready for maintainer review'
51-
},
52-
'approved': {
53-
color: '1D76DB',
54-
description: 'Submission was approved by a maintainer'
55-
},
56-
'rejected': {
57-
color: 'B60205',
58-
description: 'Submission was rejected or failed intake validation'
59-
}
60-
};
61-
62-
async function ensureLabel(name, config) {
63-
try {
64-
await github.rest.issues.createLabel({
65-
owner: context.repo.owner,
66-
repo: context.repo.repo,
67-
name,
68-
color: config.color,
69-
description: config.description
70-
});
71-
} catch (error) {
72-
if (error.status !== 422) {
73-
throw error;
74-
}
75-
}
76-
}
77-
78-
async function syncManagedLabels(issueNumber, desiredLabels) {
79-
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
43+
const path = require('path');
44+
const { pathToFileURL } = require('url');
8045
81-
const managedForSync = ['external-plugin', 'awaiting-review', 'ready-for-review', 'rejected'];
82-
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
83-
owner: context.repo.owner,
84-
repo: context.repo.repo,
85-
issue_number: issueNumber,
86-
per_page: 100
87-
});
88-
89-
const currentManagedLabels = currentLabels
90-
.map((label) => label.name)
91-
.filter((name) => managedForSync.includes(name));
92-
93-
const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name));
94-
const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name));
95-
96-
if (labelsToAdd.length > 0) {
97-
await github.rest.issues.addLabels({
98-
owner: context.repo.owner,
99-
repo: context.repo.repo,
100-
issue_number: issueNumber,
101-
labels: labelsToAdd
102-
});
103-
}
104-
105-
for (const name of labelsToRemove) {
106-
await github.rest.issues.removeLabel({
107-
owner: context.repo.owner,
108-
repo: context.repo.repo,
109-
issue_number: issueNumber,
110-
name
111-
});
112-
}
113-
}
46+
const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href);
11447
11548
const result = JSON.parse(process.env.RESULT_JSON);
11649
const issueNumber = context.issue.number;
@@ -128,40 +61,14 @@ jobs:
12861
return;
12962
}
13063
131-
const desiredLabels = result.valid
132-
? new Set(['external-plugin', 'ready-for-review'])
133-
: new Set(['external-plugin', 'rejected']);
134-
135-
await syncManagedLabels(issueNumber, desiredLabels);
136-
137-
const { data: comments } = await github.rest.issues.listComments({
64+
await intakeState.applyExternalPluginIntakeEvaluation({
65+
github,
13866
owner: context.repo.owner,
13967
repo: context.repo.repo,
140-
issue_number: issueNumber,
141-
per_page: 100
68+
issueNumber,
69+
evaluation: result
14270
});
14371
144-
const existingComment = comments.find((comment) =>
145-
comment.user?.login === 'github-actions[bot]' &&
146-
comment.body?.includes(result.commentMarker)
147-
);
148-
149-
if (existingComment) {
150-
await github.rest.issues.updateComment({
151-
owner: context.repo.owner,
152-
repo: context.repo.repo,
153-
comment_id: existingComment.id,
154-
body: result.commentBody
155-
});
156-
} else {
157-
await github.rest.issues.createComment({
158-
owner: context.repo.owner,
159-
repo: context.repo.repo,
160-
issue_number: issueNumber,
161-
body: result.commentBody
162-
});
163-
}
164-
16572
if (!result.valid && issueState === 'open') {
16673
await github.rest.issues.update({
16774
owner: context.repo.owner,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: External Plugin Rerun Intake Commands
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
concurrency:
8+
group: external-plugin-intake-${{ github.event.issue.number }}
9+
cancel-in-progress: false
10+
11+
permissions:
12+
contents: read
13+
issues: write
14+
15+
jobs:
16+
handle-command:
17+
runs-on: ubuntu-latest
18+
if: >-
19+
!github.event.issue.pull_request &&
20+
contains(github.event.comment.body, '/rerun-intake')
21+
steps:
22+
- name: Checkout staged branch
23+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
24+
with:
25+
ref: staged
26+
27+
- name: Re-run external plugin intake
28+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
29+
env:
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
with:
32+
script: |
33+
const path = require('path');
34+
const { pathToFileURL } = require('url');
35+
36+
const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href);
37+
const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href);
38+
39+
const commentAuthor = context.payload.comment.user?.login;
40+
if (!commentAuthor || context.payload.comment.user?.type === 'Bot' || commentAuthor === 'github-actions[bot]') {
41+
core.info('Ignoring /rerun-intake from a bot or unknown actor.');
42+
return;
43+
}
44+
45+
if (!intake.parseRerunIntakeCommand(context.payload.comment.body)) {
46+
core.info('No supported /rerun-intake command was found.');
47+
return;
48+
}
49+
50+
const { data: currentIssue } = await github.rest.issues.get({
51+
owner: context.repo.owner,
52+
repo: context.repo.repo,
53+
issue_number: context.issue.number
54+
});
55+
56+
const labelNames = new Set((currentIssue.labels || []).map((label) => label.name));
57+
const isExternalPluginIssue =
58+
labelNames.has('external-plugin') ||
59+
String(currentIssue.body || '').includes(intake.ISSUE_FORM_MARKER);
60+
if (!isExternalPluginIssue) {
61+
core.info('Ignoring /rerun-intake because the issue is not an external plugin submission.');
62+
return;
63+
}
64+
65+
if (labelNames.has('approved') || labelNames.has('re-review-due') || labelNames.has('re-review-follow-up')) {
66+
core.info('Ignoring /rerun-intake because the issue is already approved or in the six-month re-review flow.');
67+
return;
68+
}
69+
70+
const issueAuthor = currentIssue.user?.login;
71+
const isIssueAuthor = Boolean(issueAuthor && commentAuthor === issueAuthor);
72+
73+
let hasWriteAccess = false;
74+
if (!isIssueAuthor) {
75+
const permission = await github.rest.repos.getCollaboratorPermissionLevel({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
username: commentAuthor
79+
});
80+
hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission);
81+
}
82+
83+
if (!isIssueAuthor && !hasWriteAccess) {
84+
core.info(`Ignoring /rerun-intake because ${commentAuthor} is neither the issue author nor a maintainer.`);
85+
return;
86+
}
87+
88+
const canRerunFromCurrentState = currentIssue.state === 'open' || labelNames.has('rejected');
89+
if (!canRerunFromCurrentState) {
90+
core.info('Ignoring /rerun-intake because the issue is closed outside the intake/rejection flow.');
91+
return;
92+
}
93+
94+
const evaluation = await intake.evaluateExternalPluginIssue({
95+
issue: currentIssue,
96+
token: process.env.GITHUB_TOKEN
97+
});
98+
99+
await intakeState.applyExternalPluginIntakeEvaluation({
100+
github,
101+
owner: context.repo.owner,
102+
repo: context.repo.repo,
103+
issueNumber: context.issue.number,
104+
evaluation
105+
});
106+
107+
if (evaluation.valid && currentIssue.state === 'closed' && labelNames.has('rejected')) {
108+
await github.rest.issues.update({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
issue_number: context.issue.number,
112+
state: 'open'
113+
});
114+
return;
115+
}
116+
117+
if (!evaluation.valid && currentIssue.state === 'open') {
118+
await github.rest.issues.update({
119+
owner: context.repo.owner,
120+
repo: context.repo.repo,
121+
issue_number: context.issue.number,
122+
state: 'closed'
123+
});
124+
}

AGENTS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,11 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
167167
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`
168168
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
169169
5. Submission issues move through `external-plugin` + `awaiting-review` -> `ready-for-review` -> `approved` or `rejected`
170-
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
171-
7. Approval automation creates or updates the PR against `staged`, updates `plugins/external.json`, and regenerates marketplace outputs
172-
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
173-
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`
170+
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
171+
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
172+
8. Approval automation creates or updates the PR against `staged`, updates `plugins/external.json`, and regenerates marketplace outputs
173+
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
174+
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`
174175

175176
### Testing Instructions
176177

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,10 @@ The public-submission policy builds on those rules and also requires `license` p
230230
1. **Open an issue** using the external plugin issue form. Automation applies the `external-plugin` and `awaiting-review` labels.
231231
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.
232232
3. **Ready for maintainer review**: if the issue passes intake validation, automation removes `awaiting-review` and adds `ready-for-review`.
233-
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.
234-
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.
235-
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.
233+
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`.
234+
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.
235+
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.
236+
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.
236237

237238
##### Maintainer review responsibilities
238239

0 commit comments

Comments
 (0)