stoke/scripts/check-governance.js

83 lines
2.8 KiB
JavaScript
Raw Normal View History

2026-08-19 19:23:21 +00:00
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
function parseArgs(argv) {
const options = {
config: path.join(process.cwd(), '.github', 'labels.conf'),
apiUrl: process.env.FORGE_API_URL
|| `${(process.env.FORGE_SERVER_URL || 'https://forgejo.heavyduty.builders').replace(/\/$/, '')}/api/v1`,
};
for (let index = 0; index < argv.length; index += 1) {
const flag = argv[index];
if (flag !== '--config' && flag !== '--api-url') {
throw new Error(`unknown option: ${flag}`);
}
const value = argv[index + 1];
if (!value) throw new Error(`${flag} requires a value`);
options[flag === '--config' ? 'config' : 'apiUrl'] = value;
index += 1;
}
return options;
}
function parseConfig(contents, filename) {
let panel;
let triageActors = [];
const scopes = [];
for (const line of contents.split(/\r?\n/)) {
if (!line) continue;
if (line.startsWith('panel=')) {
if (panel) throw new Error(`duplicate panel line in ${filename}`);
panel = line.slice('panel='.length).trim().split(/\s+/).filter(Boolean);
if (panel.length === 0) throw new Error(`panel must name at least one reviewer in ${filename}`);
continue;
}
if (line.startsWith('triage-actors=')) {
triageActors = line.slice('triage-actors='.length).trim().split(/\s+/).filter(Boolean);
continue;
}
const fields = line.split('|');
if (fields.length !== 3 || !fields[0] || !/^[0-9A-Fa-f]{6}$/.test(fields[1]) || !fields[2]) {
throw new Error(`malformed label row: ${line} in ${filename}`);
}
scopes.push(line);
}
if (!panel) throw new Error(`missing panel= line in ${filename}`);
const identities = [...new Set([...panel, ...triageActors])];
for (const login of identities) {
if (!/^[A-Za-z0-9-]+$/.test(login)) {
throw new Error(`malformed login in ${filename}: ${login}`);
}
}
return { identities, scopes };
}
async function validateIdentities(apiUrl, identities) {
for (const login of identities) {
const endpoint = `${apiUrl.replace(/\/$/, '')}/users/${encodeURIComponent(login)}`;
let response;
try {
response = await fetch(endpoint);
} catch (error) {
throw new Error(`${login}: fetch failed: ${error.message}`);
}
if (response.status !== 200) throw new Error(`${login}: HTTP ${response.status} from ${endpoint}`);
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const contents = fs.readFileSync(options.config, 'utf8');
const { identities, scopes } = parseConfig(contents, options.config);
await validateIdentities(options.apiUrl, identities);
console.log(`governance: ${identities.length} identities resolved; ${scopes.length} scope rows valid`);
}
main().catch((error) => {
console.error(`governance: ${error.message}`);
process.exitCode = 1;
});