-
Notifications
You must be signed in to change notification settings - Fork 2.5k
292 lines (251 loc) · 13.6 KB
/
validate-plugin-entry.yml
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
name: Validate Plugin Entry
on:
pull_request_target:
branches:
- master
paths:
- community-plugins.json
jobs:
plugin-validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- uses: actions/setup-node@v2
- uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
// Don't run any validation checks if the user is just modifying existing plugin config
if (context.payload.pull_request.additions <= context.payload.pull_request.deletions) {
return;
}
const escapeHtml = (unsafe) => unsafe.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
const errors = [];
const addError = (error) => {
errors.push(`:x: ${error}`);
console.log('Found issue: ' + error);
};
const warnings = [];
const addWarning = (warning) => {
warnings.push(`:warning: ${warning}`);
console.log('Found issue: ' + warning);
}
let plugin;
// Core validation logic
await (async () => {
if (context.payload.pull_request.changed_files > 1) {
addError('You modified files other than `community-plugins.json`.');
}
if (!context.payload.pull_request.maintainer_can_modify) {
addWarning('Maintainers of this repo should be allowed to edit this pull request. This speeds up the approval process.');
}
if (!context.payload.pull_request.body.includes('I have added a license in the LICENSE file.')) {
addError('You did not follow the pull request template');
}
let plugins = [];
try {
plugins = JSON.parse(fs.readFileSync('community-plugins.json', 'utf8'));
} catch (e) {
addError('Could not parse `community-plugins.json`, invalid JSON. ' + e.message);
return;
}
plugin = plugins[plugins.length - 1];
let validKeys = ['id', 'name', 'description', 'author', 'repo'];
for (let key of validKeys) {
if (!plugin.hasOwnProperty(key)) {
addError(`Your PR does not have the required \`${key}\` property.`);
}
}
for (let key of Object.keys(plugin)) {
if (plugin.hasOwnProperty(key) && validKeys.indexOf(key) === -1) {
addError(`Your PR has the invalid \`${key}\` property.`);
}
}
// Validate plugin repo
let repoInfo = plugin.repo.split('/');
if (repoInfo.length !== 2) {
addError(`It seems like you made a typo in the repository field \`${plugin.repo}\`.`);
}
let [owner, repo] = repoInfo;
console.log(`Repo info: ${owner}/${repo}`);
const author = context.payload.pull_request.user.login;
if (owner.toLowerCase() !== author.toLowerCase()) {
try {
const isInOrg = await github.rest.orgs.checkMembershipForUser({org: owner, username: author});
if (!isInOrg) {
throw undefined;
}
} catch (e) {
addError(`The newly added entry is not at the end, or you are submitting on someone else's behalf. The last plugin in the list is: \`${plugin.repo}\`. If you are submitting from a GitHub org, you need to be a public member of the org.`);
}
}
try {
const repository = await github.rest.repos.get({owner, repo});
if (!repository.data.has_issues) {
addWarning('Your repository does not have issues enabled. Users will not be able to report bugs and request features.');
}
} catch (e) {
addError(`It seems like you made a typo in the repository field \`${plugin.repo}\`.`);
}
if (plugin.id.toLowerCase().includes('obsidian')) {
addError(`Please don't use the word \`obsidian\` in the plugin ID. The ID is used for your plugin's folder so keeping it short and simple avoids clutter and helps with sorting.`);
}
if (plugin.id.toLowerCase().endsWith('plugin')) {
addError(`Please don't use the word \`plugin\` in the plugin ID. The ID is used for your plugin's folder so keeping it short and simple avoids clutter and helps with sorting.`);
}
if (!/^[a-z0-9-_]+$/.test(plugin.id)) {
addError('The plugin ID is not valid. Only alphanumeric characters and dashes are allowed.');
}
else if (plugin.name.toLowerCase().includes('obsidian')) {
addError(`Please don't use the word \`Obsidian\` in your plugin name since it's redundant and adds clutter to the plugin list.`);
}
if (plugin.name.toLowerCase().endsWith('plugin')) {
addError(`Please don't use the word \`Plugin\` in the plugin name since it's redundant and adds clutter to the plugin list.`);
}
if (/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(plugin.author)) {
addWarning(`We generally discourage including your email addresses in the \`author\` field.`);
}
if (plugin.description.length > 300) {
addWarning(`Your plugin has a long description. Users typically find it difficult to read a very long description, so you should keep it short and concise.`);
}
if (plugins.filter(p => p.id === plugin.id).length > 1) {
addError(`There is already a plugin with this ID.`);
}
if (plugins.filter(p => p.name === plugin.name).length > 1) {
addError(`There is already a plugin with this name.`);
}
if (plugins.filter(p => p.repo === plugin.repo).length > 1) {
addError(`There is already a entry pointing to the \`${plugin.repo}\` repository.`);
}
let manifest = null;
try {
let manifestFile = await github.rest.repos.getContent({
owner,
repo,
path: 'manifest.json',
});
manifest = JSON.parse(Buffer.from(manifestFile.data.content, 'base64').toString('utf-8'));
} catch (e) {
addError(`You don't have a \`manifest.json\` at the root of your repo, or it could not be parsed.`);
}
if (manifest) {
let validManifestKeys = ['id', 'name', 'description', 'author', 'version', 'minAppVersion', 'isDesktopOnly'];
for (let key of validManifestKeys) {
if (!manifest.hasOwnProperty(key)) {
addError(`Your manifest does not have the required \`${key}\` property.`);
}
}
if (manifest.id !== plugin.id) {
addError(`Plugin ID mismatch, the ID in this PR (\`${plugin.id}\`) is not the same as the one in your repo (\`${manifest.id}\`). If you just changed your plugin ID, remember to change it in the manifest.json in your repo and your latest GitHub release.`);
}
if (manifest.name !== plugin.name) {
addError(`Plugin name mismatch, the name in this PR (\`${plugin.name}\`) is not the same as the one in your repo (\`${manifest.name}\`). If you just changed your plugin name, remember to change it in the manifest.json in your repo and your latest GitHub release.`);
}
if (manifest.authorUrl) {
if (manifest.authorUrl === "https://obsidian.md") {
addError(`The \`authorUrl\` field in your manifest should not point to the Obsidian Website. If you don't have a website you can just point it to your GitHub profile.`);
}
if (manifest.authorUrl.toLowerCase().includes("github.com/" + plugin.repo.toLowerCase())) {
addError(`The \`authorUrl\` field in your manifest should not point to the GitHub repository of the plugin.`);
}
}
if (manifest.fundingUrl && manifest.fundingUrl === "https://obsidian.md/pricing") {
addError(`The \`fundingUrl\` field in your manifest should not point to the Obsidian Website, If you don't have a link were users can donate to you, you can just remove it from the manifest.`);
}
if (!/^[0-9.]+$/.test(manifest.version)) {
addError('Your latest version number is not valid. Only numbers and dots are allowed.');
}
try {
let release = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag: manifest.version,
});
const assets = release.data.assets || [];
if (!assets.find(p => p.name === 'main.js')) {
addError('Your latest Release is missing the `main.js` file.');
}
if (!assets.find(p => p.name === 'manifest.json')) {
addError('Your latest Release is missing the `manifest.json` file.');
}
} catch (e) {
addError(`Unable to find a release with the tag \`${manifest.version}\`. Make sure that the version in your manifest.json file in your repo points to the correct Github Release.`);
}
}
try {
await github.rest.licenses.getForRepo({owner, repo});
} catch (e) {
addWarning(`Your repository does not include a license. It is generally recommended for open-source projects to have a license. Go to <https://choosealicense.com/> to compare different open source licenses.`);
}
})();
if (errors.length > 0 || warnings.length > 0) {
let message = [`#### Hello!\n`]
message.push(`**I found the following issues in your plugin submission**\n`);
if (errors.length > 0) {
message.push(`**Errors:**\n`);
message = message.concat(errors);
message.push(`\n---\n`);
}
if (warnings.length > 0) {
message.push(`**Warnings:**\n`);
message = message.concat(warnings);
message.push(`\n---\n`);
}
message.push(`<sup>This check was done automatically. Do <b>NOT</b> open a new PR for re-validation. Instead, to trigger this check again, make a change to your PR and wait a few minutes, or close and re-open it.</sup>`);
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: message.join('\n'),
});
}
const labels = [];
if (errors.length > 0) {
labels.push("Validation failed");
core.setFailed("Failed to validate plugin");
}
if (errors.length === 0) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
title: `Add plugin: ${plugin.name}`
});
if(!context.payload.pull_request.labels.filter(label => label.name === 'Changes requested').length > 0) {
labels.push("Ready for review");
}
}
if (context.payload.pull_request.labels.filter(label => label.name === 'Changes requested').length > 0) {
labels.push('Changes requested');
}
if (context.payload.pull_request.labels.filter(label => label.name === 'Additional review required').length > 0) {
labels.push('Additional review required');
}
if (context.payload.pull_request.labels.filter(label => label.name === 'Minor changes requested').length > 0) {
labels.push('Minor changes requested');
}
if (context.payload.pull_request.labels.filter(label => label.name === 'requires author rebase').length > 0) {
labels.push('requires author rebase');
}
if (context.payload.pull_request.labels.filter(label => label.name === 'Installation not recommended').length > 0) {
labels.push('Installation not recommended');
}
if (context.payload.pull_request.labels.filter(label => label.name === 'Changes made').length > 0) {
labels.push('Changes made');
}
if (context.payload.pull_request.labels.filter(label => label.name === 'Skipped code scan').length > 0) {
labels.push('Skipped code scan');
}
labels.push('plugin');
await github.rest.issues.setLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels,
});
permissions:
contents: read
issues: write
pull-requests: write