mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-11-01 07:02:30 +00:00
Name the failed branch of a fan-out as the stop step
The stop step was the latest step to start, and only if it failed, so a sibling branch starting a few milliseconds after the failed one hid it. Name the latest failed step that no later step went on from, where a concurrent sibling branch or the step's own branches don't count as going on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3107793f2b
commit
285a1d06be
2 changed files with 36 additions and 8 deletions
|
|
@ -135,6 +135,20 @@ describe("buildRunFailureView", () => {
|
|||
expect(failure?.headline).toBe("The steps finished, but publishing the result failed");
|
||||
});
|
||||
|
||||
test("names the failed branch of a fan-out, though a sibling started after it", () => {
|
||||
const runStages = stages(
|
||||
["checks@1", "failed", "2026-10-10T15:20:00.000Z"],
|
||||
["lint@1", "failed", "2026-10-10T15:20:00.010Z"],
|
||||
["test@1", "cancelled", "2026-10-10T15:20:00.013Z"],
|
||||
);
|
||||
runStages["lint@1"]!.parallel_branch_id = "checks@1:0";
|
||||
runStages["test@1"]!.parallel_branch_id = "checks@1:1";
|
||||
expect(view(failedRun({ message: "script exited with status 1" }, { runStages }))?.stage).toBe("lint");
|
||||
|
||||
runStages["report@1"] = { state: "succeeded", started_at: "2026-10-10T15:21:00Z" } as State["stages"][string];
|
||||
expect(view(failedRun({ message: "push rejected" }, { reason: "publish_failed", runStages }))?.stage).toBeNull();
|
||||
});
|
||||
|
||||
test("shows nothing for a cancelled run, and says what happened for other endings", () => {
|
||||
expect(view(failedRun({ message: "Run cancelled before worker launch completed" }, { reason: "cancelled" }))).toBeNull();
|
||||
expect(view(failedRun({ message: "denied at review" }, { reason: "approval_denied" }))?.headline).toBe("Approval was denied");
|
||||
|
|
|
|||
|
|
@ -207,17 +207,31 @@ function stepAdvice(message: string, category: FailureCategory, output: string |
|
|||
}
|
||||
|
||||
/**
|
||||
* The key of the last step to start, if it failed. An earlier failure that a
|
||||
* later step recovered from isn't where the run stopped, so it isn't named.
|
||||
* The key of the failed step the run stopped at: the latest failed step that
|
||||
* no later step went on from. A step that started later in a sibling branch
|
||||
* of the same fan-out ran beside it, not after it, so it doesn't count; an
|
||||
* earlier failure that a later step recovered from isn't named.
|
||||
*/
|
||||
function stoppedAt(stages: FailureState["stages"]): string | null {
|
||||
let last: [string, number] | null = null;
|
||||
for (const [key, stage] of Object.entries(stages)) {
|
||||
const started = stage.started_at ? Date.parse(stage.started_at) : NaN;
|
||||
if (!Number.isNaN(started) && (!last || started > last[1])) last = [key, started];
|
||||
const started = Object.entries(stages).flatMap(([key, stage]) => {
|
||||
const at = stage.started_at ? Date.parse(stage.started_at) : NaN;
|
||||
if (Number.isNaN(at)) return [];
|
||||
const branch = stage.parallel_branch_id ?? null;
|
||||
const group = branch && branch.includes(":") ? branch.slice(0, branch.lastIndexOf(":")) : null;
|
||||
return [{ key, at, branch, group, failed: stage.state === "failed" }];
|
||||
});
|
||||
let stop: (typeof started)[number] | null = null;
|
||||
for (const step of started) {
|
||||
if (!step.failed) continue;
|
||||
const wentOn = started.some(
|
||||
(later) =>
|
||||
later.at > step.at &&
|
||||
later.group !== step.key &&
|
||||
!(step.group !== null && later.group === step.group && later.branch !== step.branch),
|
||||
);
|
||||
if (!wentOn && (!stop || step.at > stop.at)) stop = step;
|
||||
}
|
||||
if (!last || stages[last[0]]?.state !== "failed") return null;
|
||||
return last[0];
|
||||
return stop?.key ?? null;
|
||||
}
|
||||
|
||||
function outputTail(tail: ExecOutputTail | null | undefined): string | null {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue