-
Notifications
You must be signed in to change notification settings - Fork 10
133 lines (107 loc) · 4.46 KB
/
validate-schemas.yml
File metadata and controls
133 lines (107 loc) · 4.46 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
name: Validate Schemas
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '22'
- name: Install dependencies
run: npm install ajv@8.17.1 ajv-formats@3.0.1 glob@11.0.0
- name: Validate schemas
run: |
node << 'EOF'
const fs = require('fs');
const { globSync } = require('glob');
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
let errors = 0, warnings = 0;
const schemas = {};
const stats = {};
const err = (m, f) => {
errors++;
console.log(`✗ ${m}`);
if (f) console.log(`::error file=${f}::${m}`);
};
const warn = (m, f) => {
warnings++;
console.log(`⚠ ${m}`);
if (f) console.log(`::warning file=${f}::${m}`);
};
const readJSON = (f) => {
try { return JSON.parse(fs.readFileSync(f, 'utf8')); }
catch (e) { err(`Failed to read ${f}: ${e.message}`, f); process.exit(1); }
};
const validate = (file, schema, checkKebab = true) => {
if (!schemas[schema]) schemas[schema] = ajv.compile(readJSON(`schemas/${schema}`));
const data = readJSON(file);
if (!schemas[schema](data)) {
err(`Invalid ${file}:\n${ajv.errorsText(schemas[schema].errors)}`, file);
return null;
}
// Extract expected name from path
const parts = file.split('/');
const jsonFile = parts[parts.length - 1];
const expectedName = jsonFile === 'projects.json' ? null : parts[parts.length - 2];
if (expectedName && data.name !== expectedName) {
err(`Name mismatch in ${file}: "${data.name}" vs "${expectedName}"`, file);
return null;
}
if (checkKebab && expectedName && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(expectedName)) {
warn(`Non-kebab-case: ${expectedName}`, file);
}
console.log(`✓ ${file}`);
return data;
};
const vscodeConfig = readJSON('.vscode/settings.json');
const schemaRules = vscodeConfig['json.schemas']
.filter(s => s.url.startsWith('/schemas/'))
.map(s => ({
pattern: s.fileMatch[0],
schema: s.url.replace('/schemas/', ''),
statKey: s.url.replace('/schemas/', '').replace('.schema.json', '')
}));
console.log('Validating schemas...\n');
for (const rule of schemaRules) {
const files = globSync(rule.pattern);
// Skip kebab-case check for versions and apps (they may have numbers or special naming)
const checkKebab = !['version', 'app'].includes(rule.statKey);
for (const file of files) {
const data = validate(file, rule.schema, checkKebab);
if (data) {
stats[rule.statKey] = (stats[rule.statKey] || 0) + 1;
// Check app source files
if (rule.statKey === 'app' && data.source?.path) {
const srcFile = file.replace('/app.json', `/${data.source.path}`);
if (!fs.existsSync(srcFile)) {
warn(`Missing source: ${data.source.path}`, file);
}
}
}
}
}
console.log(`\nSummary:`);
console.log(Object.entries(stats).map(([k, v]) => `${k}: ${v}`).join(' | '));
console.log(`Errors: ${errors} | Warnings: ${warnings}\n`);
const statusIcon = errors ? '❌' : '✅';
const summary = [
'## Schema Validation',
'',
`${statusIcon} **${errors} errors, ${warnings} warnings**`,
'',
'### Statistics',
'',
...Object.entries(stats).map(([k, v]) => `- **${k}**: ${v}`),
].join('\n');
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
process.exit(errors > 0 ? 1 : 0);
EOF