mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-18 14:53:05 +08:00
add platform audit export output
Adds JSON/markdown export and write-to-file support for the platform audit operator artifact.
This commit is contained in:
@@ -29,7 +29,11 @@ function usage() {
|
|||||||
'Operator readiness audit for ECC queue, discussion, roadmap, release, and security evidence.',
|
'Operator readiness audit for ECC queue, discussion, roadmap, release, and security evidence.',
|
||||||
'',
|
'',
|
||||||
'Options:',
|
'Options:',
|
||||||
' --format <text|json> Output format (default: text)',
|
' --format <text|json|markdown>',
|
||||||
|
' Output format (default: text)',
|
||||||
|
' --json Alias for --format json',
|
||||||
|
' --markdown Alias for --format markdown',
|
||||||
|
' --write <path> Write json or markdown output to a file',
|
||||||
' --root <dir> Repository root to inspect (default: cwd)',
|
' --root <dir> Repository root to inspect (default: cwd)',
|
||||||
' --repo <owner/repo> GitHub repo to inspect; repeatable',
|
' --repo <owner/repo> GitHub repo to inspect; repeatable',
|
||||||
' --skip-github Skip live GitHub queue/discussion checks',
|
' --skip-github Skip live GitHub queue/discussion checks',
|
||||||
@@ -71,6 +75,7 @@ function parseArgs(argv) {
|
|||||||
skipGithub: false,
|
skipGithub: false,
|
||||||
thresholds: { ...DEFAULT_THRESHOLDS },
|
thresholds: { ...DEFAULT_THRESHOLDS },
|
||||||
useEnvGithubToken: false,
|
useEnvGithubToken: false,
|
||||||
|
writePath: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
@@ -92,6 +97,16 @@ function parseArgs(argv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg === '--json') {
|
||||||
|
parsed.format = 'json';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--markdown') {
|
||||||
|
parsed.format = 'markdown';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg === '--root') {
|
if (arg === '--root') {
|
||||||
parsed.root = path.resolve(readValue(args, index, arg));
|
parsed.root = path.resolve(readValue(args, index, arg));
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -130,6 +145,17 @@ function parseArgs(argv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg === '--write') {
|
||||||
|
parsed.writePath = path.resolve(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--write=')) {
|
||||||
|
parsed.writePath = path.resolve(arg.slice('--write='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg === '--max-open-prs') {
|
if (arg === '--max-open-prs') {
|
||||||
parsed.thresholds.maxOpenPrs = parseIntegerFlag(readValue(args, index, arg), arg);
|
parsed.thresholds.maxOpenPrs = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -176,8 +202,12 @@ function parseArgs(argv) {
|
|||||||
throw new Error(`Unknown argument: ${arg}`);
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['text', 'json'].includes(parsed.format)) {
|
if (!['text', 'json', 'markdown'].includes(parsed.format)) {
|
||||||
throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
|
throw new Error(`Invalid format: ${parsed.format}. Use text, json, or markdown.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.writePath && parsed.format === 'text') {
|
||||||
|
throw new Error('--write requires --json, --markdown, or --format json|markdown');
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed.allowUntracked = parsed.allowUntracked.map(normalizeRelativePrefix);
|
parsed.allowUntracked = parsed.allowUntracked.map(normalizeRelativePrefix);
|
||||||
@@ -595,6 +625,101 @@ function renderText(report) {
|
|||||||
return `${lines.join('\n')}\n`;
|
return `${lines.join('\n')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markdownEscape(value) {
|
||||||
|
return String(value === undefined || value === null ? '' : value)
|
||||||
|
.replace(/\|/g, '\\|')
|
||||||
|
.replace(/\r?\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownStatus(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'pass':
|
||||||
|
return 'PASS';
|
||||||
|
case 'fail':
|
||||||
|
return 'FAIL';
|
||||||
|
case 'warn':
|
||||||
|
return 'WARN';
|
||||||
|
default:
|
||||||
|
return String(status || 'UNKNOWN').toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdown(report) {
|
||||||
|
const lines = [
|
||||||
|
'# ECC Platform Audit',
|
||||||
|
'',
|
||||||
|
`Generated: ${report.generatedAt}`,
|
||||||
|
`Status: ${report.ready ? 'ready' : 'attention required'}`,
|
||||||
|
`Root: \`${report.root}\``,
|
||||||
|
'',
|
||||||
|
'## Queue Summary',
|
||||||
|
'',
|
||||||
|
'| Surface | Count | Threshold | Status |',
|
||||||
|
'| --- | ---: | ---: | --- |',
|
||||||
|
`| Open PRs | ${report.github.totals.openPrs} | ${report.thresholds.maxOpenPrs} | ${report.github.totals.openPrs <= report.thresholds.maxOpenPrs ? 'PASS' : 'FAIL'} |`,
|
||||||
|
`| Open issues | ${report.github.totals.openIssues} | ${report.thresholds.maxOpenIssues} | ${report.github.totals.openIssues <= report.thresholds.maxOpenIssues ? 'PASS' : 'FAIL'} |`,
|
||||||
|
`| Discussions needing maintainer touch | ${report.github.totals.discussionsNeedingMaintainerTouch} | 0 | ${report.github.totals.discussionsNeedingMaintainerTouch === 0 ? 'PASS' : 'FAIL'} |`,
|
||||||
|
`| Conflicting open PRs | ${report.github.totals.dirtyPrs} | 0 | ${report.github.totals.dirtyPrs === 0 ? 'PASS' : 'FAIL'} |`,
|
||||||
|
`| Blocking dirty files | ${report.git.blockingDirtyCount} | ${report.thresholds.maxDirtyFiles} | ${report.git.blockingDirtyCount <= report.thresholds.maxDirtyFiles ? 'PASS' : 'FAIL'} |`,
|
||||||
|
'',
|
||||||
|
'## Repositories',
|
||||||
|
'',
|
||||||
|
'| Repository | PRs | Issues | Discussions sampled | Needs maintainer | Dirty PRs |',
|
||||||
|
'| --- | ---: | ---: | ---: | ---: | ---: |',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const repo of report.github.repos) {
|
||||||
|
lines.push(
|
||||||
|
`| \`${markdownEscape(repo.repo)}\` | ${repo.openPrs || 0} | ${repo.openIssues || 0} | ${repo.discussions ? repo.discussions.sampledCount : 0} | ${repo.discussions ? repo.discussions.needingMaintainerTouch.length : 0} | ${repo.dirtyPrs ? repo.dirtyPrs.length : 0} |`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'## Checks',
|
||||||
|
'',
|
||||||
|
'| Status | Check | Summary | Evidence |',
|
||||||
|
'| --- | --- | --- | --- |'
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const check of report.checks) {
|
||||||
|
lines.push(
|
||||||
|
`| ${markdownStatus(check.status)} | \`${markdownEscape(check.id)}\` | ${markdownEscape(check.summary)} | ${check.path ? `\`${markdownEscape(check.path)}\`` : ''} |`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', '## Top Actions', '');
|
||||||
|
if (report.top_actions.length === 0) {
|
||||||
|
lines.push('- none');
|
||||||
|
} else {
|
||||||
|
for (const action of report.top_actions) {
|
||||||
|
lines.push(`- \`${markdownEscape(action.id)}\`: ${markdownEscape(action.fix)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', '## Git State', '');
|
||||||
|
lines.push(`- Branch: ${report.git.branch ? `\`${markdownEscape(report.git.branch)}\`` : '(unknown)'}`);
|
||||||
|
lines.push(`- Ignored dirty files: ${report.git.ignoredDirty.length}`);
|
||||||
|
if (report.git.ignoredDirty.length > 0) {
|
||||||
|
for (const line of report.git.ignoredDirty) {
|
||||||
|
lines.push(` - \`${markdownEscape(line)}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(`- Blocking dirty files: ${report.git.blockingDirty.length}`);
|
||||||
|
if (report.git.blockingDirty.length > 0) {
|
||||||
|
for (const line of report.git.blockingDirty) {
|
||||||
|
lines.push(` - \`${markdownEscape(line)}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeOutput(writePath, output) {
|
||||||
|
fs.mkdirSync(path.dirname(writePath), { recursive: true });
|
||||||
|
fs.writeFileSync(writePath, output, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
try {
|
try {
|
||||||
const options = parseArgs(process.argv);
|
const options = parseArgs(process.argv);
|
||||||
@@ -606,7 +731,12 @@ function main() {
|
|||||||
const report = buildReport(options);
|
const report = buildReport(options);
|
||||||
const output = options.format === 'json'
|
const output = options.format === 'json'
|
||||||
? `${JSON.stringify(report, null, 2)}\n`
|
? `${JSON.stringify(report, null, 2)}\n`
|
||||||
: renderText(report);
|
: options.format === 'markdown'
|
||||||
|
? renderMarkdown(report)
|
||||||
|
: renderText(report);
|
||||||
|
if (options.writePath) {
|
||||||
|
writeOutput(options.writePath, output);
|
||||||
|
}
|
||||||
process.stdout.write(output);
|
process.stdout.write(output);
|
||||||
|
|
||||||
if (options.exitCode && !report.ready) {
|
if (options.exitCode && !report.ready) {
|
||||||
@@ -625,6 +755,7 @@ if (require.main === module) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
buildReport,
|
buildReport,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
|
renderMarkdown,
|
||||||
renderText,
|
renderText,
|
||||||
runGhJson,
|
runGhJson,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ function runTests() {
|
|||||||
'script',
|
'script',
|
||||||
'--format=json',
|
'--format=json',
|
||||||
`--root=${rootDir}`,
|
`--root=${rootDir}`,
|
||||||
|
'--json',
|
||||||
'--repo',
|
'--repo',
|
||||||
'affaan-m/everything-claude-code',
|
'affaan-m/everything-claude-code',
|
||||||
'--max-open-prs',
|
'--max-open-prs',
|
||||||
@@ -166,6 +167,7 @@ function runTests() {
|
|||||||
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
|
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
|
||||||
|
|
||||||
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--write', 'audit.md']), /--write requires/);
|
||||||
assert.throws(() => parseArgs(['node', 'script', '--repo']), /--repo requires a value/);
|
assert.throws(() => parseArgs(['node', 'script', '--repo']), /--repo requires a value/);
|
||||||
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
|
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
|
||||||
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
||||||
@@ -192,6 +194,33 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('markdown output can be written as an operator artifact', () => {
|
||||||
|
const projectRoot = createTempDir('platform-audit-markdown-');
|
||||||
|
const outputPath = path.join(projectRoot, 'artifacts', 'platform-audit.md');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(projectRoot);
|
||||||
|
const stdout = run([
|
||||||
|
'--markdown',
|
||||||
|
'--write',
|
||||||
|
outputPath,
|
||||||
|
`--root=${projectRoot}`,
|
||||||
|
'--skip-github'
|
||||||
|
], { cwd: projectRoot });
|
||||||
|
const written = fs.readFileSync(outputPath, 'utf8');
|
||||||
|
|
||||||
|
assert.strictEqual(stdout, written);
|
||||||
|
assert.ok(written.includes('# ECC Platform Audit'));
|
||||||
|
assert.ok(written.includes('## Queue Summary'));
|
||||||
|
assert.ok(written.includes('| Open PRs | 0 | 20 | PASS |'));
|
||||||
|
assert.ok(written.includes('`roadmap-linear-mirror`'));
|
||||||
|
assert.ok(written.includes('## Top Actions'));
|
||||||
|
assert.ok(written.includes('- none'));
|
||||||
|
} finally {
|
||||||
|
cleanup(projectRoot);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('github queue and discussion budgets pass with maintainer touch', () => {
|
if (test('github queue and discussion budgets pass with maintainer touch', () => {
|
||||||
const projectRoot = createTempDir('platform-audit-github-pass-');
|
const projectRoot = createTempDir('platform-audit-github-pass-');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user