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:
co-authored by
Claude Fable 5
parent
3b7850fe1a
commit
2015d20aa5
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user