import type { CategoryState, CategoryStatus, CheckCategory, CheckInfo, } from '../shared/types.js'; import type { RepoRules } from './config.js'; import type { CheckContext } from './github/queries.js'; const FAIL_CONCLUSIONS = new Set([ 'FAILURE', 'TIMED_OUT', 'ACTION_REQUIRED', 'CANCELLED', 'STARTUP_FAILURE', ]); const PENDING_STATUSES = new Set(['QUEUED', 'WAITING', 'PENDING', 'REQUESTED']); export function checkState(ctx: CheckContext): CategoryState { if (ctx.__typename === 'StatusContext') { if (ctx.state === 'SUCCESS') return 'pass'; if (ctx.state === 'ERROR' || ctx.state === 'FAILURE') return 'fail'; return 'pending'; // PENDING, EXPECTED } if (ctx.status === 'IN_PROGRESS') return 'running'; if (PENDING_STATUSES.has(ctx.status)) return 'pending'; // COMPLETED if (ctx.conclusion === 'SKIPPED') return 'skipped'; if (ctx.conclusion === 'SUCCESS' || ctx.conclusion === 'NEUTRAL') return 'pass'; if (ctx.conclusion && FAIL_CONCLUSIONS.has(ctx.conclusion)) return 'fail'; return 'pending'; // STALE or unknown } export function categoryFor(rules: RepoRules | undefined, name: string): CheckCategory { if (!rules) return 'other'; const exact = rules.exact.get(name); if (exact) return exact; for (const glob of rules.globs) { if (glob.isMatch(name)) return glob.category; } return 'other'; } const AGGREGATE_ORDER: CategoryState[] = ['fail', 'running', 'pending', 'pass', 'skipped']; const CATEGORIES: CheckCategory[] = ['lint', 'specs', 'e2e', 'other']; /** A commit accrues one CheckRun per workflow run, so re-runs and multi-trigger * workflows duplicate names — keep only the latest run per name (like GitHub's UI). */ function dedupeByName(contexts: CheckContext[]): CheckContext[] { const byName = new Map(); for (const ctx of contexts) { const name = ctx.__typename === 'StatusContext' ? ctx.context : ctx.name; const prev = byName.get(name); if (!prev) { byName.set(name, ctx); continue; } const prevAt = prev.__typename === 'CheckRun' ? (prev.startedAt ?? '') : ''; const curAt = ctx.__typename === 'CheckRun' ? (ctx.startedAt ?? '') : ''; if (curAt >= prevAt) byName.set(name, ctx); } return [...byName.values()]; } /** Group check contexts into lint/specs/e2e/other with aggregate state per category. * Every check is kept — skipped placeholders included — so nothing is ever hidden. */ export function categorize(rules: RepoRules | undefined, contexts: CheckContext[]): CategoryStatus[] { const buckets = new Map(CATEGORIES.map((c) => [c, []])); for (const ctx of dedupeByName(contexts)) { const name = ctx.__typename === 'StatusContext' ? ctx.context : ctx.name; buckets.get(categoryFor(rules, name))!.push({ name, state: checkState(ctx), url: ctx.__typename === 'StatusContext' ? ctx.targetUrl : ctx.detailsUrl, }); } return CATEGORIES.map((category) => { const checks = buckets.get(category)!; checks.sort((a, b) => a.name.localeCompare(b.name)); const active = checks.filter((c) => c.state !== 'skipped'); let state: CategoryState = 'none'; if (checks.length > 0) { state = AGGREGATE_ORDER.find((s) => checks.some((c) => c.state === s)) ?? 'none'; } return { category, state, passed: checks.filter((c) => c.state === 'pass').length, total: active.length, checks, }; }); }