Enumerate failing specs/E2E from CI artifacts; mute all github-actions comments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
This commit is contained in:
Joshua Coles
2026-07-29 09:24:20 +00:00
co-authored by Claude Fable 5
parent 3b7850fe1a
commit 2015d20aa5
18 changed files with 650 additions and 3 deletions
+91
View File
@@ -0,0 +1,91 @@
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import type { FailureReport, SpecFailure } from '../../../shared/types';
import { fetchFailures } from '../api';
const FLAT_LIMIT = 15;
const FILE_GROUPS_SHOWN = 50;
function FailureRow({ f }: { f: SpecFailure }) {
return (
<div className="failure-row">
<div className="failure-name">
{f.name}
{f.line != null && <span className="failure-line">:{f.line}</span>}
</div>
{f.message && <div className="failure-msg">{f.message}</div>}
</div>
);
}
function ReportSection({ report }: { report: FailureReport }) {
const [showAllFiles, setShowAllFiles] = useState(false);
const byFile = new Map<string, SpecFailure[]>();
for (const f of report.failures) {
if (!byFile.has(f.path)) byFile.set(f.path, []);
byFile.get(f.path)!.push(f);
}
const files = [...byFile.entries()].sort((a, b) => b[1].length - a[1].length);
const visibleFiles = showAllFiles ? files : files.slice(0, FILE_GROUPS_SHOWN);
const flat = report.failures.length <= FLAT_LIMIT;
return (
<div className="failure-section">
<div className="failure-section-head">
<span className="failure-count">✗ {report.failed}</span>
<span className="failure-label">{report.label} failing</span>
{report.truncated && (
<span className="failure-truncated">showing first {report.failures.length}</span>
)}
</div>
{flat ? (
files.map(([path, fs]) => (
<div key={path} className="failure-file-flat">
<div className="failure-path">{path}</div>
{fs.map((f, i) => (
<FailureRow key={i} f={f} />
))}
</div>
))
) : (
<>
{visibleFiles.map(([path, fs]) => (
<details key={path} className="failure-file">
<summary>
<span className="failure-path">{path}</span>
<span className="failure-file-count">{fs.length}</span>
</summary>
{fs.map((f, i) => (
<FailureRow key={i} f={f} />
))}
</details>
))}
{!showAllFiles && files.length > FILE_GROUPS_SHOWN && (
<button className="load-more" onClick={() => setShowAllFiles(true)}>
Show all {files.length} files
</button>
)}
</>
)}
</div>
);
}
export default function FailuresPanel({ prId }: { prId: string }) {
const query = useQuery({
queryKey: ['failures', prId],
queryFn: () => fetchFailures(prId),
});
const reports = query.data ?? [];
if (reports.length === 0) return null;
return (
<div className="failures-panel">
{reports.map((r) => (
<ReportSection key={r.key} report={r} />
))}
</div>
);
}
+12 -1
View File
@@ -1,4 +1,4 @@
import type { PrSnapshot } from '../../../shared/types';
import type { FailureReportSummary, PrSnapshot } from '../../../shared/types';
import CategoryChips from './CategoryChips';
import ReviewBadge from './ReviewBadge';
import { relativeTime } from '../format';
@@ -6,14 +6,20 @@ import { relativeTime } from '../format';
export default function PrCard({
pr,
unread,
failures,
selected,
onSelect,
}: {
pr: PrSnapshot;
unread: number;
failures: FailureReportSummary[];
selected: boolean;
onSelect: () => void;
}) {
const failureText = failures
.filter((f) => f.failed > 0)
.map((f) => `${f.failed}${f.truncated ? '+' : ''} ${f.label}`)
.join(' · ');
return (
<div
className={`pr-card${selected ? ' selected' : ''}`}
@@ -47,6 +53,11 @@ export default function PrCard({
<ReviewBadge pr={pr} />
<CategoryChips categories={pr.categories} />
</div>
{failureText && (
<div className="fail-callout" title="Click card for the full list">
✗ {failureText} failing
</div>
)}
</div>
);
}
+1
View File
@@ -58,6 +58,7 @@ export default function PrList({
<PrCard
pr={pr}
unread={state.unreadCountsByPr[pr.id] ?? 0}
failures={state.failuresByPr[pr.id] ?? []}
selected={pr.id === selectedPrId}
onSelect={() => onSelect(pr.id)}
/>
+2
View File
@@ -3,6 +3,7 @@ import { useState } from 'react';
import type { TimelineEvent } from '../../../shared/types';
import type { PrSnapshot } from '../../../shared/types';
import { fetchTimeline, markAllRead, markPrRead, markRead } from '../api';
import FailuresPanel from './FailuresPanel';
import { relativeTime, shortRepo } from '../format';
const GROUP_WINDOW_MS = 10 * 60 * 1000;
@@ -162,6 +163,7 @@ export default function Timeline({
</button>
</div>
)}
{selectedPr && <FailuresPanel prId={selectedPr.id} />}
<div className="timeline-list">
{query.isLoading && <div className="empty-note">Loading…</div>}
{!query.isLoading && groups.length === 0 && (