Skip to content

Commit c414e47

Browse files
authored
Add plugin schema validator workflow (#505)
* Remove code formatting workflows and related configuration files * Remove formatting instructions from README * Add plugin validator * Add workflow * Limit workflow to plugins/themes directories
1 parent 3c78135 commit c414e47

File tree

6 files changed

+716
-0
lines changed

6 files changed

+716
-0
lines changed

.github/workflows/validate.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Validate Plugins
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'plugins/**'
8+
- 'themes/**'
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- 'plugins/**'
13+
- 'themes/**'
14+
15+
jobs:
16+
validate:
17+
runs-on: ubuntu-22.04
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: '20.x'
23+
- run: cd ./validator && yarn install --frozen-lockfile
24+
- run: node ./validate.js --ci

validate.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
require('./validator/index.js')();

validator/index.js

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const path = require('path');
6+
7+
const safeRequire = (name) => {
8+
try {
9+
return require(name);
10+
} catch (error) {
11+
if (error && error.code === 'MODULE_NOT_FOUND') {
12+
console.log(`Error: Cannot find module '${name}', have you installed the dependencies?`);
13+
process.exit(1);
14+
}
15+
throw error;
16+
}
17+
};
18+
19+
const Ajv = safeRequire('ajv').default;
20+
const betterAjvErrors = safeRequire('better-ajv-errors').default;
21+
const chalk = safeRequire('chalk');
22+
const YAML = safeRequire('yaml');
23+
const addFormats = safeRequire('ajv-formats');
24+
25+
// https://www.peterbe.com/plog/nodejs-fs-walk-or-glob-or-fast-glob
26+
function walk(directory, ext, filepaths = []) {
27+
const files = fs.readdirSync(directory);
28+
for (const filename of files) {
29+
const filepath = path.join(directory, filename);
30+
if (fs.statSync(filepath).isDirectory()) {
31+
walk(filepath, ext, filepaths);
32+
} else if (path.extname(filename) === ext && !filename.includes('config')) {
33+
filepaths.push(filepath);
34+
}
35+
}
36+
return filepaths;
37+
}
38+
39+
// https://stackoverflow.com/a/53833620
40+
const isSorted = arr => arr.every((v,i,a) => !i || a[i-1] <= v);
41+
42+
class Validator {
43+
constructor(flags) {
44+
this.allowDeprecations = flags.includes('-d');
45+
this.stopOnError = !flags.includes('-a');
46+
this.sortedURLs = flags.includes('-s');
47+
this.verbose = flags.includes('-v');
48+
49+
const schemaPath = path.resolve(__dirname, './plugin.schema.json');
50+
this.schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
51+
this.ajv = new Ajv({
52+
// allErrors: true,
53+
allowUnionTypes: true, // Use allowUnionTypes instead of ignoreKeywordsWithRef
54+
strict: true,
55+
allowMatchingProperties: true, // Allow properties that match a pattern
56+
});
57+
addFormats(this.ajv);
58+
}
59+
60+
run(files) {
61+
let plugins;
62+
63+
if (files && Array.isArray(files) && files.length > 0) {
64+
plugins = files.map(file => path.resolve(file));
65+
} else {
66+
const pluginsDir = path.resolve(__dirname, '../plugins');
67+
const themesDir = path.resolve(__dirname, '../themes');
68+
plugins = walk(pluginsDir, '.yml').concat(walk(themesDir, '.yml'));
69+
}
70+
71+
let result = true;
72+
const validate = this.ajv.compile(this.schema);
73+
74+
for (const file of plugins) {
75+
const relPath = path.relative(process.cwd(), file);
76+
let contents, data;
77+
try {
78+
contents = fs.readFileSync(file, 'utf8');
79+
data = YAML.parse(contents);
80+
} catch (error) {
81+
console.error(`${chalk.red(chalk.bold('ERROR'))} in: ${relPath}:`);
82+
error.stack = null;
83+
console.error(error);
84+
result = result && false;
85+
if (this.stopOnError) break;
86+
else continue;
87+
}
88+
89+
let valid = validate(data);
90+
91+
// Output validation errors
92+
if (!valid) {
93+
const output = betterAjvErrors(this.schema, data, validate.errors, { indent: 2 });
94+
console.log(output);
95+
96+
// Detailed error checks
97+
validate.errors.forEach(err => {
98+
switch (err.keyword) {
99+
case 'required':
100+
console.error(`${chalk.red('Missing Required Property:')} ${err.params.missingProperty}`);
101+
break;
102+
case 'type':
103+
console.error(`${chalk.red('Type Mismatch:')} ${err.dataPath} should be ${err.params.type}`);
104+
break;
105+
case 'pattern':
106+
console.error(`${chalk.red('Pattern Mismatch:')} ${err.dataPath} should match pattern ${err.params.pattern}`);
107+
break;
108+
case 'enum':
109+
console.error(`${chalk.red('Enum Violation:')} ${err.dataPath} should be one of ${err.params.allowedValues.join(', ')}`);
110+
break;
111+
case 'additionalProperties':
112+
console.error(`${chalk.red('Additional Properties:')} ${err.params.additionalProperty} is not allowed`);
113+
break;
114+
case '$ref':
115+
console.error(`${chalk.red('Invalid Reference:')} ${err.dataPath} ${err.message}`);
116+
break;
117+
case 'items':
118+
console.error(`${chalk.red('Array Item Type Mismatch:')} ${err.dataPath} ${err.message}`);
119+
break;
120+
case 'format':
121+
console.error(`${chalk.red('Invalid Format:')} ${err.dataPath} should match format ${err.params.format}`);
122+
break;
123+
default:
124+
console.error(`${chalk.red('Validation Error:')} ${err.dataPath} ${err.message}`);
125+
}
126+
});
127+
}
128+
129+
if (this.verbose || !valid) {
130+
const validColor = valid ? chalk.green : chalk.red;
131+
console.log(`${relPath} Valid: ${validColor(valid)}`);
132+
}
133+
134+
result = result && valid;
135+
136+
if (!valid && this.stopOnError) break;
137+
}
138+
139+
if (!this.verbose && result) {
140+
console.log(chalk.green('Validation passed!'));
141+
}
142+
143+
return result;
144+
}
145+
}
146+
147+
function main(flags, files) {
148+
const args = process.argv.slice(2)
149+
flags = (flags === undefined) ? args.filter(arg => arg.startsWith('-')) : flags;
150+
files = (files === undefined) ? args.filter(arg => !arg.startsWith('-')) : files;
151+
const validator = new Validator(flags);
152+
const result = validator.run(files);
153+
if (flags.includes('--ci')) {
154+
process.exit(result ? 0 : 1);
155+
}
156+
}
157+
158+
if (require.main === module) {
159+
main();
160+
}
161+
162+
module.exports = main;
163+
module.exports.Validator = Validator;

validator/package.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "stash-script-validator",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"main": "node index.js"
8+
},
9+
"dependencies": {
10+
"ajv": "7",
11+
"ajv-formats": "^3.0.1",
12+
"better-ajv-errors": "^1.2.0",
13+
"chalk": "4",
14+
"yaml": "^2.5.1"
15+
}
16+
}

0 commit comments

Comments
 (0)