Skip to content

Add plugin: Epiphany #10405

Add plugin: Epiphany

Add plugin: Epiphany #10405

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
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