#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { loadConfig, DIRS } from './lib/config.js'; import { compareWeeks } from './lib/week.js'; /** * Emits the weekly Markdown summary posted as a GitHub issue comment, * matching the open-scans pattern: one comment per week, table per * domain, deltas against the previous week, links to the Pages report. * * Reads the same data/ summaries the HTML reports use. One truth. * * Usage: node src/issue-comment.js [--pages-base https://USER.github.io/REPO] * Output: docs/comment.md (also printed to stdout) */ const args = Object.fromEntries( process.argv.slice(2).join(' ').split('--').filter(Boolean).map((s) => { const [k, ...v] = s.trim().split(' '); return [k, v.join(' ') || true]; }) ); const pagesBase = (args['pages-base'] ?? process.env.PAGES_BASE_URL ?? '').replace(/\/$/, ''); const config = loadConfig(); let out = `## Weekly scan summary\n\n`; let latestWeek = null; for (const target of config.targets) { const domainDir = path.join(DIRS.data, target.key); if (!fs.existsSync(domainDir)) continue; const weeks = fs .readdirSync(domainDir) .filter((w) => /^\d{4}-W\d{2}$/.test(w) && fs.existsSync(path.join(domainDir, w, 'summary.json'))) .sort(compareWeeks); if (weeks.length === 0) continue; const week = weeks[weeks.length - 1]; latestWeek = latestWeek && compareWeeks(latestWeek, week) > 0 ? latestWeek : week; const curr = JSON.parse(fs.readFileSync(path.join(domainDir, week, 'summary.json'), 'utf8')); const prev = weeks.length > 1 ? JSON.parse(fs.readFileSync(path.join(domainDir, weeks[weeks.length - 2], 'summary.json'), 'utf8')) : null; const d = (a, b) => (prev ? ` (${a - b >= 0 ? '+' : ''}${a - b})` : ''); const reportLink = pagesBase ? `${pagesBase}/reports/${target.key}/${week}/` : `docs/reports/${target.key}/${week}/`; out += `### ${target.domain}: ${week}\n\n`; out += `| Metric | This week | Notes |\n|---|---:|---|\n`; out += `| Pages audited (unique) | ${curr.pagesAudited ?? curr.pagesScanned}${prev ? d(curr.pagesAudited ?? curr.pagesScanned, prev.pagesAudited ?? prev.pagesScanned) : ''} | scanned by axe and/or Alfa |\n`; out += `| Median axe violations / page | ${curr.axe.medianViolations ?? 'n/a'}${prev && curr.axe.medianViolations != null && prev.axe.medianViolations != null ? d(curr.axe.medianViolations, prev.axe.medianViolations) : ''} | ${curr.axe.pagesWithViolations} pages affected |\n`; out += `| Median Alfa failures / page | ${curr.alfa.medianFailures ?? 'n/a'}${prev && curr.alfa.medianFailures != null && prev.alfa.medianFailures != null ? d(curr.alfa.medianFailures, prev.alfa.medianFailures) : ''} | ${curr.alfa.pagesWithFailures} pages affected |\n`; if (curr.sustainability) { out += `| Median page weight | ${Math.round(curr.sustainability.medianBytes / 1024)} KB${prev?.sustainability ? d(Math.round(curr.sustainability.medianBytes / 1024), Math.round(prev.sustainability.medianBytes / 1024)) : ''} | est. ${curr.sustainability.meanCo2g} g CO₂/page (SWD v4) |\n`; } const topAxe = Object.entries(curr.axe.rules) .sort(([, a], [, b]) => b.pages - a.pages) .slice(0, 5); if (topAxe.length) { out += `\nTop axe rules by pages affected: ${topAxe.map(([id, r]) => `\`${id}\` (${r.pages})`).join(', ')}.\n`; } out += `\n[Full report](${reportLink})\n\n`; } if (!latestWeek) out += `No scan data found yet.\n`; out += `\n---\n*Generated by [vital-scans](https://github.com/${process.env.GITHUB_REPOSITORY ?? 'mgifford/vital-core'}). Automated checks cover roughly a third of accessibility barriers; manual testing with assistive technology remains essential.*\n`; fs.mkdirSync(DIRS.docs, { recursive: true }); fs.writeFileSync(path.join(DIRS.docs, 'comment.md'), out); console.log(out);