Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -568,18 +568,39 @@ layer("BitbucketPullRequestApi.layer", (it) => {
const next = "https://api.bitbucket.org/2.0/statuses?page=2";
mockedRequest
.mockReturnValueOnce(
Effect.succeed(response(valuePage([{ name: "Build", state: "SUCCESSFUL" }], next))),
Effect.succeed(
response(
valuePage(
[
{ name: "Build", state: "SUCCESSFUL" },
{ name: "Tests", state: "STOPPED" },
],
next,
),
),
),
)
.mockReturnValueOnce(
Effect.succeed(response(valuePage([{ name: "Lint", state: "FAILED" }]))),
Effect.succeed(
response(
valuePage([
{ name: "Lint", state: "FAILED" },
{ name: "Deploy", state: "INPROGRESS" },
{ name: "Audit", state: "FAILED" },
]),
),
),
);
const api = yield* BitbucketPullRequestApi.BitbucketPullRequestApi;

const checks = yield* api.listChecks({ repository: "acme/web", number: 7 });

expect(checks.map((check) => [check.name, check.status])).toEqual([
["Build", "success"],
["Tests", "cancelled"],
["Lint", "failure"],
["Audit", "failure"],
["Build", "success"],
["Deploy", "pending"],
]);
expect(callAt(1).url).toBe(next);
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {

import * as BitbucketApi from "./BitbucketApi.ts";
import { parseDiffFileRevisions } from "@t3tools/source-control-core/server/diffFileRevisions";
import { sortChecks } from "@t3tools/source-control-core/server/pullRequestChecks";
import {
buildReviewThreads,
decodeCommentsJson,
Expand Down Expand Up @@ -672,7 +673,7 @@ export const make = Effect.gen(function* () {
decode: decodeStatusesJson,
items: [],
prepend: false,
}),
}).pipe(Effect.map(sortChecks)),
),

listReviewerCandidates: (input) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,8 @@ describe("decodeStatusesJson", () => {
);

expect(decoded.items.map((check) => [check.name, check.status])).toEqual([
["build / Pipeline", "success"],
["deploy / Pipeline", "failure"],
["build / Pipeline", "success"],
]);
});
});
Expand Down
23 changes: 21 additions & 2 deletions packages/source-control-core/src/server/pullRequestChecks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ describe("dedupeChecks", () => {
]);
});

it("puts failed and cancelled checks first, preserving each group's order after reruns", () => {
const checks = dedupeChecks([
entry("lint", "failure", { at: "2026-08-11T16:00:00Z" }),
entry("integration", "cancelled"),
entry("types", "pending"),
entry("unit", "failure"),
entry("format", "success"),
entry("lint", "success", { at: "2026-08-11T18:00:00Z" }),
]);

expect(checks.map((check) => [check.name, check.status])).toEqual([
["integration", "cancelled"],
["unit", "failure"],
["lint", "success"],
["types", "pending"],
["format", "success"],
]);
});

it("keeps distinct workflow and check pairs even when their names join to the same text", () => {
const checks = dedupeChecks([
entry("Linux tests", "success", {
Expand Down Expand Up @@ -181,8 +200,8 @@ describe("dedupeChecks", () => {
]);

expect(checks.map((check) => [check.name, check.status])).toEqual([
["CI / build", "success"],
["Release / build", "failure"],
["CI / build", "success"],
]);
});

Expand All @@ -193,6 +212,6 @@ describe("dedupeChecks", () => {
entry("build", "failure", { workflowName: "CI", at: "2026-08-11T16:00:00Z" }),
]);

expect(checks.map((check) => check.name)).toEqual(["build", "CI / build"]);
expect(checks.map((check) => check.name)).toEqual(["CI / build", "build"]);
});
});
24 changes: 18 additions & 6 deletions packages/source-control-core/src/server/pullRequestChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ function isAtLeastAsNew(candidate: string | null, kept: string | null): boolean
);
}

export function sortChecks(
checks: ReadonlyArray<PullRequestCheck>,
): ReadonlyArray<PullRequestCheck> {
return checks.toSorted(
(a, b) =>
Number(b.status === "failure" || b.status === "cancelled") -
Number(a.status === "failure" || a.status === "cancelled"),
);
}

/**
* A host's rollup is a list of runs, not a list of checks: while a workflow is being re-run — or
* while a second run of it is already live — the same check arrives twice, and both copies reach
Expand All @@ -61,10 +71,12 @@ export function dedupeChecks(
for (const entry of survivors) {
countsByName.set(entry.check.name, (countsByName.get(entry.check.name) ?? 0) + 1);
}
return survivors.map((entry) => {
const workflowName = entry.workflowName ?? "";
return workflowName.length > 0 && (countsByName.get(entry.check.name) ?? 0) > 1
? { ...entry.check, name: `${workflowName} / ${entry.check.name}` }
: entry.check;
});
return sortChecks(
survivors.map((entry) => {
const workflowName = entry.workflowName ?? "";
return workflowName.length > 0 && (countsByName.get(entry.check.name) ?? 0) > 1
? { ...entry.check, name: `${workflowName} / ${entry.check.name}` }
: entry.check;
}),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ describe("GitCafePullRequestProvider", () => {
});
assert.deepStrictEqual(
detail.checks.map((check) => check.name),
["early", "late"],
["late", "early"],
);
assert.deepStrictEqual(
detail.reviewers.map((reviewer) => reviewer.login),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,12 @@ describe("toDiff", () => {

describe("toChecks", () => {
it("maps GitCafe's status and conclusion onto the shared check states", () => {
const check = (status: string, conclusion: string | null) => ({
name: `${status}-${conclusion}`,
const check = (
status: string,
conclusion: string | null,
name = `${status}-${conclusion}`,
) => ({
name,
status,
conclusion,
summary: null,
Expand All @@ -506,16 +510,24 @@ describe("toChecks", () => {
items: [
check("queued", null),
check("in_progress", null),
check("completed", "success"),
check("completed", "success", "Build"),
check("completed", "cancelled"),
check("completed", "action_required"),
check("completed", "timed_out"),
check("completed", "timed_out", "Build"),
],
next: null,
}),
);
assert.deepStrictEqual(
checks.map((item) => item.status),
["pending", "pending", "success", "action-required", "failure"],
checks.map((item) => [item.name, item.status]),
[
["completed-cancelled", "cancelled"],
["Build", "failure"],
["queued-null", "pending"],
["in_progress-null", "pending"],
["Build", "success"],
["completed-action_required", "action-required"],
],
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { quoteGitPatchPath } from "@t3tools/shared/gitPatchPath";
import { sortChecks } from "@t3tools/source-control-core/server/pullRequestChecks";
import * as DateTime from "effect/DateTime";
import * as Schema from "effect/Schema";
import {
Expand Down Expand Up @@ -549,10 +550,12 @@ function checkStatus(check: (typeof GitCafeChecks.Type.items)[number]): PullRequ
}

export function toChecks(checks: typeof GitCafeChecks.Type): ReadonlyArray<PullRequestCheck> {
return checks.items.map((check) => ({
name: check.name,
status: checkStatus(check),
description: check.summary,
url: check.detailsUrl,
}));
return sortChecks(
checks.items.map((check) => ({
name: check.name,
status: checkStatus(check),
description: check.summary,
url: check.detailsUrl,
})),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -623,8 +623,8 @@ describe("pull request detail decoding", () => {
check({ __typename: "StatusContext", context: "ci/legacy", state: "SUCCESS" }),
]);
expect(detail.checks.map((check) => [check.name, check.status])).toEqual([
["build", "pending"],
["test", "failure"],
["build", "pending"],
["ci/legacy", "success"],
]);
});
Expand Down
Loading