-
-
Notifications
You must be signed in to change notification settings - Fork 38
231 lines (205 loc) · 9.3 KB
/
Copy pathcreate-plugin-pr.yml
File metadata and controls
231 lines (205 loc) · 9.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
name: Create Plugin PR from Issue
on:
issues:
types: [opened]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
create-pr:
if: contains(github.event.issue.labels.*.name, 'plugin-submission')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const body = context.payload.issue.body ?? '';
const issueNumber = context.payload.issue.number;
const issueUrl = context.payload.issue.html_url;
function parseField(body, heading) {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`### ${escaped}\\r?\\n\\r?\\n([\\s\\S]*?)(?=\\r?\\n### |$)`);
const match = body.match(regex);
if (!match) return null;
const val = match[1].trim();
return val === '_No response_' || val === '' ? null : val;
}
function parseCheckboxes(body, heading) {
const section = parseField(body, heading);
if (!section) return [];
return section.split('\n')
.filter(line => /^- \[x\]/i.test(line.trim()))
.map(line => line.replace(/^- \[x\] /i, '').trim());
}
const name = parseField(body, 'Plugin name');
const id = parseField(body, 'Plugin ID');
const description = parseField(body, 'Description');
const website = parseField(body, 'Website / Repository URL');
const source = parseField(body, 'Source code URL');
const author = parseField(body, 'Author');
const preview = parseField(body, 'Preview image URL');
const status = parseField(body, 'Status');
const minimumVersion = parseField(body, 'Minimum Docusaurus version');
const npmPackagesRaw = parseField(body, 'npm package name(s)');
const submittedTags = parseCheckboxes(body, 'Tags');
// These tags curate the directory's highlighted/official listings, so only
// maintainers may apply them — anyone else's selection is stripped below.
const RESTRICTED_TAGS = ['favourite', 'docusaurus'];
const submitter = context.payload.issue.user.login;
let isMaintainer = false;
try {
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: submitter,
});
isMaintainer = permission.permission === 'admin' || permission.permission === 'maintain';
} catch (e) {
if (e.status !== 404) throw e;
// Not a collaborator — expected for external contributors
}
const removedTags = isMaintainer ? [] : submittedTags.filter(t => RESTRICTED_TAGS.includes(t));
const tags = isMaintainer ? submittedTags : submittedTags.filter(t => !RESTRICTED_TAGS.includes(t));
const errors = [];
if (!name) errors.push('Plugin name is required');
if (!id) errors.push('Plugin ID is required');
if (id && !/^[a-z0-9-]+\.[a-z0-9-]+$/.test(id)) errors.push('Plugin ID must be in `author.plugin-name` format using lowercase letters, numbers, and hyphens only');
if (!description) errors.push('Description is required');
if (!website) errors.push('Website / Repository URL is required');
if (!status) errors.push('Status is required');
if (errors.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: [
'Thank you for your plugin submission! Unfortunately some required fields are missing or invalid:',
'',
...errors.map(e => `- ${e}`),
'',
'Please close this issue and open a new one with all fields correctly filled in.',
].join('\n'),
});
return;
}
const npmPackages = npmPackagesRaw
? npmPackagesRaw.split('\n').map(l => l.trim()).filter(Boolean)
: [];
const filename = `${id}.yaml`;
const branch = `plugin/${id}`;
// Check if the file already exists
try {
await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: `data/plugins/${filename}`,
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `A plugin with ID \`${id}\` already exists in the directory. Please choose a unique ID, then close this issue and open a new one.`,
});
return;
} catch (e) {
if (e.status !== 404) throw e;
// File doesn't exist — expected
}
// Generate YAML
const yamlScalar = (value) => (value == null ? 'null' : JSON.stringify(value));
const yamlLines = [
'# yaml-language-server: $schema=https://docusaurus.community/schema/plugin/1.0.0.json',
`id: ${id}`,
`name: ${yamlScalar(name)}`,
`description: ${yamlScalar(description)}`,
`preview: ${yamlScalar(preview)}`,
`website: ${yamlScalar(website)}`,
`source: ${yamlScalar(source)}`,
`author: ${yamlScalar(author)}`,
];
if (tags.length > 0) {
yamlLines.push('tags:');
tags.forEach(t => yamlLines.push(` - ${yamlScalar(t)}`));
} else {
yamlLines.push('tags: []');
}
yamlLines.push(`minimumVersion: ${yamlScalar(minimumVersion)}`);
yamlLines.push(`status: ${yamlScalar(status)}`);
if (npmPackages.length > 0) {
yamlLines.push('npmPackages:');
npmPackages.forEach(p => yamlLines.push(` - ${yamlScalar(p)}`));
} else {
yamlLines.push('npmPackages: []');
}
const yaml = yamlLines.join('\n') + '\n';
// Get main branch SHA
const mainRef = await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/main',
});
const baseSha = mainRef.data.object.sha;
// Create branch, falling back to a unique name if it already exists
let actualBranch = branch;
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/heads/${branch}`,
sha: baseSha,
});
} catch (e) {
// 422 is returned when the ref already exists
if (e.status !== 422) throw e;
actualBranch = `${branch}-${issueNumber}`;
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/heads/${actualBranch}`,
sha: baseSha,
});
}
// Commit the YAML file
await github.rest.repos.createOrUpdateFileContents({
owner: context.repo.owner,
repo: context.repo.repo,
path: `data/plugins/${filename}`,
message: `chore: add plugin ${id} from issue #${issueNumber}`,
content: Buffer.from(yaml).toString('base64'),
branch: actualBranch,
});
// Open a draft PR
const pr = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Add plugin: ${name}`,
head: actualBranch,
base: 'main',
body: [
`Adds \`${id}\` to the plugin directory.`,
'',
`Generated automatically from issue ${issueUrl}.`,
'',
`Closes #${issueNumber}`,
].join('\n'),
draft: true,
});
// Comment on the issue with the PR link
const successBody = [
`Thank you for your plugin submission! A draft PR has been created: ${pr.data.html_url}`,
'',
'A maintainer will review it shortly. Please check the PR for any CI validation errors.',
];
if (removedTags.length > 0) {
successBody.push(
'',
`Note: the ${removedTags.map(t => `\`${t}\``).join(', ')} tag(s) are reserved for maintainers and were removed from your submission. A maintainer can add them back if appropriate.`,
);
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: successBody.join('\n'),
});