3.8 KB128 lines
Blame
1export interface InvocationRun {
2 id: number;
3 status: string;
4 trigger_type?: string | null;
5 trigger_ref?: string | null;
6 commit_id: string | null;
7 commit_message: string | null;
8 created_at: string;
9}
10
11export interface InvocationGroup<T extends InvocationRun = InvocationRun> {
12 key: string;
13 commitId: string | null;
14 commitMessage: string | null;
15 triggerType: string | null | undefined;
16 triggerRef: string | null;
17 runs: T[];
18 newestRunId: number;
19 newestMs: number;
20}
21
22const INVOCATION_WINDOW_MS = 120000;
23
24export function groupByInvocation<T extends InvocationRun>(runs: T[]): InvocationGroup<T>[] {
25 const sorted = [...runs].sort((a, b) => b.id - a.id);
26 const groups: InvocationGroup<T>[] = [];
27
28 for (const run of sorted) {
29 const runMs = Date.parse(run.created_at);
30 const last = groups[groups.length - 1];
31 const sameSeed =
32 last &&
33 (last.commitId ?? "") === (run.commit_id ?? "") &&
34 (last.triggerType ?? "") === (run.trigger_type ?? "") &&
35 (last.triggerRef ?? "") === (run.trigger_ref ?? "");
36 const withinWindow =
37 sameSeed &&
38 Number.isFinite(runMs) &&
39 last.newestMs - runMs <= INVOCATION_WINDOW_MS;
40
41 if (withinWindow) {
42 last.runs.push(run);
43 continue;
44 }
45
46 groups.push({
47 key: `inv-${run.id}`,
48 commitId: run.commit_id,
49 commitMessage: run.commit_message,
50 triggerType: run.trigger_type,
51 triggerRef: run.trigger_ref ?? null,
52 runs: [run],
53 newestRunId: run.id,
54 newestMs: Number.isFinite(runMs) ? runMs : 0,
55 });
56 }
57
58 for (const group of groups) {
59 // Keep build numbers in ascending order within each invocation.
60 group.runs.sort((a, b) => a.id - b.id);
61 }
62
63 return groups;
64}
65
66export function formatTriggerType(triggerType: string | null | undefined): string {
67 if (!triggerType) return "Build";
68 return triggerType.replace("_", " ").replace(/\b\w/g, (m) => m.toUpperCase());
69}
70
71export function getInvocationStatus<T extends InvocationRun>(runs: T[]): string {
72 const statuses = new Set(runs.map((run) => run.status));
73 if (statuses.has("running")) return "running";
74 if (statuses.has("failed")) return "failed";
75 if (statuses.has("pending")) return "pending";
76 if (statuses.has("cancelled")) return "cancelled";
77 return "passed";
78}
79
80function slugify(input: string): string {
81 return input
82 .toLowerCase()
83 .replace(/['"]/g, "")
84 .replace(/[^a-z0-9]+/g, "-")
85 .replace(/^-+|-+$/g, "")
86 .slice(0, 72);
87}
88
89export function getInvocationBaseSlug<T extends InvocationRun>(invocation: InvocationGroup<T>): string {
90 if (invocation.commitId) return invocation.commitId.substring(0, 8).toLowerCase();
91 const fromMessage = invocation.commitMessage ? slugify(invocation.commitMessage) : "";
92 if (fromMessage) return fromMessage;
93 const fromTrigger = invocation.triggerType ? slugify(invocation.triggerType) : "";
94 return fromTrigger || `run-${invocation.newestRunId}`;
95}
96
97export function getInvocationRunSlug<T extends InvocationRun>(
98 invocation: InvocationGroup<T>,
99 allInvocations?: InvocationGroup<T>[],
100): string {
101 const base = getInvocationBaseSlug(invocation);
102 if (allInvocations) {
103 const duplicates = allInvocations.filter(
104 (g) => getInvocationBaseSlug(g) === base
105 );
106 if (duplicates.length > 1) {
107 return `${base}-${invocation.newestRunId}`;
108 }
109 }
110 return base;
111}
112
113export function getSeedRunIdFromInvocationSlug(slug: string): number | null {
114 // Match "run-<id>" format
115 const runMatch = slug.match(/^run-(\d+)$/);
116 if (runMatch) {
117 const id = Number.parseInt(runMatch[1], 10);
118 return Number.isNaN(id) ? null : id;
119 }
120 // Match disambiguated "<base>-<id>" suffix (e.g. "5fbd7f38-42")
121 const suffixMatch = slug.match(/-(\d+)$/);
122 if (suffixMatch) {
123 const id = Number.parseInt(suffixMatch[1], 10);
124 return Number.isNaN(id) ? null : id;
125 }
126 return null;
127}
128