Skip to content

Commit 9f62ccc

Browse files
authored
Merge branch 'staged' into feat/md-to-docx-plus-equation-support
2 parents d848a8d + 1d75827 commit 9f62ccc

11 files changed

Lines changed: 566 additions & 247 deletions

.github/ISSUE_TEMPLATE/external-plugin.yml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ body:
1414
Before you continue:
1515
- Public submissions are **GitHub-only** in v1.
1616
- The plugin must live in a **public GitHub repository**.
17-
- Use an **immutable ref** for review: a release tag or full 40-character commit SHA.
17+
- Provide an immutable **ref**, **sha**, or both for review.
1818
- Do **not** open a PR that edits `plugins/external.json` directly.
1919
- type: input
2020
id: plugin-name
@@ -51,11 +51,19 @@ body:
5151
- type: input
5252
id: immutable-ref
5353
attributes:
54-
label: Immutable ref to review
55-
description: Release tag or full 40-character commit SHA.
56-
placeholder: refs/tags/v1.2.3 or 0123456789abcdef0123456789abcdef01234567
54+
label: Ref to review
55+
description: Optional release tag or tag ref. Submit this, a commit SHA, or both.
56+
placeholder: v1.2.3
5757
validations:
58-
required: true
58+
required: false
59+
- type: input
60+
id: immutable-sha
61+
attributes:
62+
label: Commit SHA to review
63+
description: Optional full 40-character commit SHA. Submit this, a ref, or both.
64+
placeholder: 0123456789abcdef0123456789abcdef01234567
65+
validations:
66+
required: false
5967
- type: input
6068
id: version
6169
attributes:
@@ -119,7 +127,7 @@ body:
119127
options:
120128
- label: The plugin lives in a public GitHub repository.
121129
required: true
122-
- label: The ref I provided is an immutable release tag or full 40-character commit SHA, not a branch.
130+
- label: The ref and/or sha I provided is immutable (release tag and/or full 40-character commit SHA), not a branch.
123131
required: true
124132
- label: This submission follows this repository's contribution, security, and responsible AI policies.
125133
required: true

.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: true
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+
startsWith(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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,14 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
164164

165165
1. Do not open a direct PR that edits `plugins/external.json` for a public third-party plugin submission
166166
2. Public external plugin submissions use the external plugin issue workflow documented in [CONTRIBUTING.md](CONTRIBUTING.md#adding-external-plugins)
167-
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`
167+
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`, `sha`, or both
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

0 commit comments

Comments
 (0)