Add plugin: Weekly Goal Tracker #12085
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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@v4 | |
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 lowercase 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.toLowerCase().includes('obsidian')) { | |
addError('Please don\'t include `Obsidian` in the plugin description'); | |
} | |
if (plugin.description.toLowerCase().includes('this plugin') || plugin.description.toLowerCase().includes('this is a plugin') || plugin.description.toLowerCase().includes('this plugin allows')) { | |
addWarning('Avoid including sentences like `This is a plugin that does` in your description'); | |
} | |
if (plugin.description.length > 250) { | |
addError(`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 the id \`${plugin.id}\`.`); | |
} | |
if (plugins.filter(p => p.name === plugin.name).length > 1) { | |
addError(`There is already a plugin with the name \`${plugin.name}\`.`); | |
} | |
if (plugins.filter(p => p.repo === plugin.repo).length > 1) { | |
addError(`There is already a entry pointing to the \`${plugin.repo}\` repository.`); | |
} | |
const removedPlugins = JSON.parse(fs.readFileSync('community-plugins-removed.json', 'utf8')); | |
if (removedPlugins.filter(p => p.id === plugin.id).length > 1) { | |
addError(`Another plugin used to exist with the id \`${plugin.id}\`. To avoid issues for users that still have the old plugin installed using this plugin ID is not allowed`); | |
} | |
if (removedPlugins.filter(p => p.name === plugin.name).length > 1) { | |
addWarning(`Another plugin used to exist with the name \`${plugin.name}\`. To avoid confussion we recommend against using this name.`); | |
} | |
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 (manifest.fundingUrl && manifest.fundingUrl === "") { | |
addError('The `fundingUrl` is meant for links to services like _Buy me a coffee_, _GitHub sponsors_ and so on, if you don\'t have such a link 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 |