Files
kandev/apps/web/components/github/pr-workflow-attention.ts
2026-09-10 23:39:56 +01:00

102 lines
3.7 KiB
TypeScript

import type { TaskPR, WorkflowAttention, WorkflowAttentionState } from "@/lib/types/github";
import { parseStrictRfc3339Timestamp } from "@/lib/utils/strict-timestamp";
type WorkflowAttentionCarrier = {
state: string;
head_sha?: string | null;
workflow_attention?: WorkflowAttention | null;
};
function isAttentionState(state: WorkflowAttentionState): boolean {
return state === "approval_required" || state === "action_required";
}
function currentWorkflowObservation(
carrier: WorkflowAttentionCarrier,
attention: WorkflowAttention | null | undefined,
): WorkflowAttention | null {
if (
carrier.state !== "open" ||
!carrier.head_sha ||
!attention ||
attention.head_sha !== carrier.head_sha
) {
return null;
}
return attention;
}
function normalizeWorkflowAttention(attention: WorkflowAttention | null): WorkflowAttention | null {
if (!attention || attention.state === "none") return null;
if (attention.runs != null) return attention;
return { ...attention, runs: attention.runs ?? [] };
}
export function getCurrentWorkflowAttention(
carrier: WorkflowAttentionCarrier,
): WorkflowAttention | null {
return normalizeWorkflowAttention(
currentWorkflowObservation(carrier, carrier.workflow_attention),
);
}
function suppliedObservationIsAtLeastAsFresh(
stored: WorkflowAttention,
supplied: WorkflowAttention,
): boolean {
const storedTimestamp = parseStrictRfc3339Timestamp(stored.observed_at);
const suppliedTimestamp = parseStrictRfc3339Timestamp(supplied.observed_at);
if (suppliedTimestamp === null) return storedTimestamp === null;
if (storedTimestamp === null) return true;
return suppliedTimestamp >= storedTimestamp;
}
/**
* Chooses the best observation for a surface that has both stored TaskPR data
* and an optional live feedback response. A failed live read must not hide a
* stored same-head positive observation, while an authoritative empty result
* must clear it.
*/
export function getWorkflowAttentionForDisplay(
carrier: WorkflowAttentionCarrier,
suppliedAttention?: WorkflowAttention | null,
): WorkflowAttention | null {
const stored = currentWorkflowObservation(carrier, carrier.workflow_attention);
if (suppliedAttention == null) return normalizeWorkflowAttention(stored);
const incoming = currentWorkflowObservation(carrier, suppliedAttention);
if (!incoming) return normalizeWorkflowAttention(stored);
const useIncoming = stored === null || suppliedObservationIsAtLeastAsFresh(stored, incoming);
const selected = useIncoming ? incoming : stored;
if (useIncoming && incoming.state === "unknown" && stored && isAttentionState(stored.state)) {
return normalizeWorkflowAttention({ ...stored, stale: true });
}
return normalizeWorkflowAttention(selected);
}
export function getActiveWorkflowAttention(
carrier: WorkflowAttentionCarrier,
attention: WorkflowAttention | null | undefined = carrier.workflow_attention,
): WorkflowAttention | null {
const current = normalizeWorkflowAttention(currentWorkflowObservation(carrier, attention));
return current && isAttentionState(current.state) ? current : null;
}
export function getTaskPRWorkflowAttention(pr: TaskPR): WorkflowAttention | null {
return getActiveWorkflowAttention(pr);
}
export function workflowAttentionWorkflowNames(attention: WorkflowAttention): string {
const names = attention.runs.map((run) => run.name.trim()).filter(Boolean);
return names.join(", ");
}
export function isWorkflowApprovalRequired(attention: WorkflowAttention): boolean {
return attention.state === "approval_required";
}
export function workflowAttentionReasonKey(reason: string): string {
return reason === "approval_required"
? "github:workflowApprovalRequired"
: "github:workflowNeedsAttention";
}