name: Validate PR on: pull_request: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm install ajv ajv-formats - name: Validate changed scripts run: | node -e " const fs = require('fs'); const crypto = require('crypto'); const Ajv = require('ajv'); const ajv = new Ajv({ allErrors: true }); const schema = JSON.parse(fs.readFileSync('schema/script.schema.json', 'utf8')); const validate = ajv.compile(schema); // Get changed files from git const { execSync } = require('child_process'); const changed = execSync('git diff --name-only --diff-filter=ACMR origin/main...HEAD -- scripts/') .toString().trim().split('\n').filter(Boolean); if (changed.length === 0) { console.log('No script files changed'); process.exit(0); } let errors = 0; for (const file of changed) { const hash = file.replace('scripts/', '').replace('.json', ''); const content = fs.readFileSync(file, 'utf8'); let parsed; try { parsed = JSON.parse(content); } catch (e) { console.error('FAIL [' + file + ']: invalid JSON'); errors++; continue; } // Re-serialize through JSON.stringify to match DB hash format (Postgres jsonb::text) const canonical = JSON.stringify(parsed); const actualHash = crypto.createHash('md5').update(canonical).digest('hex'); // Check filename matches content hash if (hash !== actualHash) { console.error('FAIL [' + file + ']: filename hash mismatch. Expected ' + actualHash + ', got ' + hash); errors++; continue; } const script = parsed; if (!validate(script)) { console.error('FAIL [' + file + ']: schema validation failed'); for (const err of validate.errors) { console.error(' ' + err.instancePath + ' ' + err.message); } errors++; continue; } // Check _meta block exists const meta = script.find(e => e && e.id === '_meta'); if (!meta) { console.error('FAIL [' + file + ']: missing _meta block'); errors++; continue; } console.log('PASS [' + file + ']: ' + meta.name); } if (errors > 0) { console.error(errors + ' file(s) failed validation'); process.exit(1); } console.log('All ' + changed.length + ' file(s) passed validation'); " - name: Check source manifest integrity run: | node -e " const { execSync } = require('child_process'); const changed = execSync('git diff --name-only --diff-filter=ACMR origin/main...HEAD -- sources/') .toString().trim().split('\n').filter(Boolean); if (changed.length === 0) { console.log('No source manifests changed'); process.exit(0); } // PR author should only modify their own source manifest if (changed.length > 1) { console.error('FAIL: PR modifies multiple source manifests: ' + changed.join(', ')); console.error('Each contributor should only modify their own sources/{site}.json'); process.exit(1); } console.log('Source manifest check passed: ' + changed[0]); "