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
36 changes: 36 additions & 0 deletions apps/server/src/pullRequest/gitHubPullRequestJson.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ describe("pull request list decoding", () => {
expect(entry?.reviewRequestLogins).toEqual(["octocat"]);
});

it("keeps label descriptions out of list rows", () => {
const [entry] = expectSuccess(
decodePullRequestListJson(
listJson([
{
labels: [{ name: " bug ", color: " ff0000 ", description: "Not sent here." }],
},
]),
),
).items;

expect(entry?.labels).toEqual([{ name: "bug", color: "ff0000" }]);
});

it("normalizes the review decision and reports nothing for one GitHub does not summarize", () => {
const batch = expectSuccess(
decodePullRequestListJson(
Expand Down Expand Up @@ -223,6 +237,28 @@ describe("pull request detail decoding", () => {
]);
});

it("keeps label descriptions in detail and normalizes an empty one", () => {
const raw = JSON.parse(detailJson) as Record<string, unknown>;
const detail = expectSuccess(
decodePullRequestDetailJson(
JSON.stringify({
...raw,
labels: [
{ name: " bug ", color: " ff0000 ", description: " Something is broken. " },
{ name: "docs", color: null, description: " " },
{ name: "legacy", color: null },
],
}),
),
);

expect(detail.labels).toEqual([
{ name: "bug", color: "ff0000", description: "Something is broken." },
{ name: "docs", color: null, description: null },
{ name: "legacy", color: null, description: null },
]);
});

it("reads an auto-merge request as armed, its null as off and its absence as neither", () => {
const raw = JSON.parse(detailJson) as Record<string, unknown>;
const armed = (entry: Record<string, unknown>) =>
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/pullRequest/gitHubPullRequestJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const RawActorSchema = Schema.Struct({
const RawLabelSchema = Schema.Struct({
name: Schema.String,
color: Schema.optional(Schema.NullOr(Schema.String)),
description: Schema.optional(Schema.NullOr(Schema.String)),
});

const RawReviewRequestSchema = Schema.Struct({
Expand Down Expand Up @@ -1115,10 +1116,19 @@ function toReviewDecision(value: string | null | undefined): PullRequestReviewDe

function toLabels(
raw: ReadonlyArray<Schema.Schema.Type<typeof RawLabelSchema>> | undefined,
includeDescription = false,
): ReadonlyArray<PullRequestLabel> {
return (raw ?? []).flatMap((label) => {
const name = trimmed(label.name);
return name === null ? [] : [{ name, color: trimmed(label.color) }];
return name === null
? []
: [
{
name,
color: trimmed(label.color),
...(includeDescription ? { description: trimmed(label.description) } : {}),
},
];
});
}

Expand Down Expand Up @@ -1349,6 +1359,7 @@ function toListItem(raw: Schema.Schema.Type<typeof RawListItemSchema>): GitHubPu
function toDetail(raw: Schema.Schema.Type<typeof RawDetailSchema>): GitHubPullRequestDetail {
return {
...toListItem(raw),
labels: toLabels(raw.labels, true),
headRepositoryOwner: trimmed(raw.headRepositoryOwner?.login),
body: raw.body ?? "",
changedFiles: raw.changedFiles ?? 0,
Expand Down
65 changes: 49 additions & 16 deletions apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
PullRequestActor,
PullRequestComment,
PullRequestDetailView,
PullRequestLabel,
PullRequestRef,
} from "@t3tools/contracts";
import {
Expand Down Expand Up @@ -58,6 +59,51 @@ function labelDotColor(color: string | null): string | null {
return /^[0-9a-fA-F]{6}$/.test(hex) ? `#${hex}` : null;
}

function PullRequestLabelChip({ label }: { readonly label: PullRequestLabel }) {
const dot = labelDotColor(label.color);
const description = label.description?.trim();
const contents = (
<>
<span
aria-hidden
className="size-2 shrink-0 rounded-full bg-muted-foreground"
{...(dot ? { style: { backgroundColor: dot } } : {})}
/>
<span className="truncate">{label.name}</span>
</>
);

if (!description) {
return (
<span className="inline-flex max-w-48 items-center gap-1.5 rounded-full border border-border/70 bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs">
{contents}
</span>
);
}

return (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="inline-flex max-w-48 cursor-help items-center gap-1.5 rounded-full border border-border/70 bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
}
>
{contents}
</TooltipTrigger>
<TooltipPopup
align="start"
side="bottom"
className="max-w-80 whitespace-normal leading-tight wrap-anywhere"
>
{description}
</TooltipPopup>
</Tooltip>
);
}

/** The avatar carries the attribution alone; who it is arrives on hover, like the reviewer row. */
function CommentAuthor({ actor }: { actor: PullRequestActor | null }) {
const login = actor?.login ?? "ghost";
Expand Down Expand Up @@ -506,22 +552,9 @@ export function PullRequestSummaryTab({
{detail.labels.length > 0 ? (
<MetaRow icon={<TagIcon className="size-3.5" />} label="Labels">
<span className="flex min-w-0 flex-wrap items-center gap-1">
{detail.labels.map((label) => {
const dot = labelDotColor(label.color);
return (
<span
key={label.name}
className="inline-flex max-w-48 items-center gap-1.5 rounded-full border border-border/70 bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs"
>
<span
aria-hidden
className="size-2 shrink-0 rounded-full bg-muted-foreground"
{...(dot ? { style: { backgroundColor: dot } } : {})}
/>
<span className="truncate">{label.name}</span>
</span>
);
})}
{detail.labels.map((label) => (
<PullRequestLabelChip key={label.name} label={label} />
))}
</span>
</MetaRow>
) : null}
Expand Down
14 changes: 13 additions & 1 deletion packages/contracts/src/pullRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const LIST_RESULT: PullRequestListResult = {
createdAt: "2026-07-01T00:00:00Z",
updatedAt: "2026-07-02T00:00:00Z",
viewerReviewRequested: false,
labels: [{ name: "backend", color: null }],
labels: [{ name: "backend", color: null, description: "Touches server behavior." }],
},
],
errors: [],
Expand Down Expand Up @@ -86,6 +86,18 @@ describe("PullRequestListResult", () => {
expect(decoded.viewers["github.com"]).toBe("bilal");
expect(decoded.viewers["github.acme.dev"]).toBe("b.hassan");
});

it("accepts labels from hosts that do not provide descriptions", () => {
const decoded = decodeListResult({
...LIST_RESULT,
entries: LIST_RESULT.entries.map((entry) => ({
...entry,
labels: [{ name: "backend", color: null }],
})),
});

expect(decoded.entries[0]?.labels[0]?.description).toBeUndefined();
});
});

describe("PullRequestListInput", () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/pullRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export type PullRequestActor = typeof PullRequestActor.Type;
export const PullRequestLabel = Schema.Struct({
name: TrimmedNonEmptyString,
color: Schema.NullOr(Schema.String),
/** Absent where the host does not report label metadata, and null for no description. */
description: Schema.optional(Schema.NullOr(Schema.String)),
});
export type PullRequestLabel = typeof PullRequestLabel.Type;

Expand Down
Loading