import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { spawnSync } from 'node:child_process'; import Ajv2020 from 'ajv/dist/2020.js'; import addFormats from 'ajv-formats'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const repoRoot = join(__dirname, '..'); const moduleRoot = join(repoRoot, 'src', 'Attestor', 'StellaOps.Attestor.Types'); const schemasDir = join(moduleRoot, 'schemas'); const fixturesDir = join(moduleRoot, 'fixtures', 'v1'); const tsDir = join(moduleRoot, 'generated', 'ts'); const goDir = join(moduleRoot, 'generated', 'go'); const schemaFiles = [ { schema: 'stellaops-build-provenance.v1.schema.json', sample: 'build-provenance.sample.json' }, { schema: 'stellaops-sbom-attestation.v1.schema.json', sample: 'sbom-attestation.sample.json' }, { schema: 'stellaops-scan-results.v1.schema.json', sample: 'scan-results.sample.json' }, { schema: 'stellaops-vex-attestation.v1.schema.json', sample: 'vex-attestation.sample.json' }, { schema: 'stellaops-policy-evaluation.v1.schema.json', sample: 'policy-evaluation.sample.json' }, { schema: 'stellaops-risk-profile.v1.schema.json', sample: 'risk-profile-evidence.sample.json' }, { schema: 'stellaops-custom-evidence.v1.schema.json', sample: 'custom-evidence.sample.json' } ]; const commonSchemaPath = join(schemasDir, 'attestation-common.v1.schema.json'); const ajv = new Ajv2020({ strict: false, allErrors: true }); addFormats(ajv); const commonSchema = JSON.parse(readFileSync(commonSchemaPath, 'utf8')); const commonId = commonSchema.$id || 'https://schemas.stella-ops.org/attestations/common/v1'; ajv.addSchema(commonSchema, commonId); let failed = false; function stableStringify(value) { if (Array.isArray(value)) { return '[' + value.map(stableStringify).join(',') + ']'; } if (value && typeof value === 'object') { const entries = Object.keys(value) .sort() .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`); return '{' + entries.join(',') + '}'; } return JSON.stringify(value); } function runCommand(command, args, options) { const result = spawnSync(command, args, { stdio: 'inherit', ...options }); if (result.error) { if (result.error.code === 'ENOENT') { throw new Error(`Command not found: ${command}`); } throw result.error; } if (result.status !== 0) { throw new Error(`Command failed: ${command} ${args.join(' ')}`); } } function commandExists(command) { const result = spawnSync(command, ['--version'], { stdio: 'ignore', env: { ...process.env, PATH: `/usr/local/go/bin:${process.env.PATH ?? ''}`, }, }); if (result.error && result.error.code === 'ENOENT') { return false; } return (result.status ?? 0) === 0; } for (const mapping of schemaFiles) { const schemaFile = mapping.schema; const sample = mapping.sample; const schemaPath = join(schemasDir, schemaFile); const samplePath = join(fixturesDir, sample); const schemaJson = JSON.parse(readFileSync(schemaPath, 'utf8')); const sampleJson = JSON.parse(readFileSync(samplePath, 'utf8')); const schemaId = schemaJson.$id || ('https://stella-ops.org/schemas/attestor/' + schemaFile); ajv.removeSchema(schemaId); ajv.addSchema(schemaJson, schemaId); const alias = new URL('attestation-common.v1.schema.json', new URL(schemaId)); if (!ajv.getSchema(alias.href)) { ajv.addSchema(commonSchema, alias.href); } const validate = ajv.getSchema(schemaId) || ajv.compile(schemaJson); const valid = validate(sampleJson); if (!valid) { failed = true; console.error('✖ ' + schemaFile + ' failed for fixture ' + sample); console.error(validate.errors || []); } else { const canonical = stableStringify(sampleJson); const digest = Buffer.from(canonical, 'utf8').toString('base64'); console.log('✔ ' + schemaFile + ' ✓ ' + sample + ' (canonical b64: ' + digest.slice(0, 16) + '… )'); } } if (failed) { console.error('One or more schema validations failed.'); process.exit(1); } try { console.log('\n▶ Installing TypeScript dependencies...'); runCommand('npm', ['install', '--no-fund', '--no-audit'], { cwd: tsDir }); console.log('▶ Running TypeScript build/tests...'); runCommand('npm', ['run', 'test'], { cwd: tsDir }); const goCandidates = [ 'go', '/usr/local/go/bin/go', process.env.GO || '', ].filter(Boolean); const goCommand = goCandidates.find((candidate) => commandExists(candidate)); if (goCommand) { console.log('▶ Running Go tests...'); const goEnv = { ...process.env, PATH: `/usr/local/go/bin:${process.env.PATH ?? ''}`, }; runCommand(goCommand, ['test', './...'], { cwd: goDir, env: goEnv }); } else { console.warn('⚠️ Go toolchain not found; skipping Go SDK tests.'); } } catch (err) { console.error(err.message); process.exit(1); } console.log('All attestation schemas and SDKs validated successfully.');