web/lib/canopy-invocations.tsblame
View source
9d879c01export interface InvocationRun {
9d879c02 id: number;
9d879c03 status: string;
9d879c04 trigger_type?: string | null;
9d879c05 trigger_ref?: string | null;
9d879c06 commit_id: string | null;
9d879c07 commit_message: string | null;
9d879c08 created_at: string;
9d879c09}
9d879c010
9d879c011export interface InvocationGroup<T extends InvocationRun = InvocationRun> {
9d879c012 key: string;
9d879c013 commitId: string | null;
9d879c014 commitMessage: string | null;
9d879c015 triggerType: string | null | undefined;
9d879c016 triggerRef: string | null;
9d879c017 runs: T[];
9d879c018 newestRunId: number;
9d879c019 newestMs: number;
9d879c020}
9d879c021
9d879c022const INVOCATION_WINDOW_MS = 120000;
9d879c023
9d879c024export function groupByInvocation<T extends InvocationRun>(runs: T[]): InvocationGroup<T>[] {
9d879c025 const sorted = [...runs].sort((a, b) => b.id - a.id);
9d879c026 const groups: InvocationGroup<T>[] = [];
9d879c027
9d879c028 for (const run of sorted) {
9d879c029 const runMs = Date.parse(run.created_at);
9d879c030 const last = groups[groups.length - 1];
9d879c031 const sameSeed =
9d879c032 last &&
9d879c033 (last.commitId ?? "") === (run.commit_id ?? "") &&
9d879c034 (last.triggerType ?? "") === (run.trigger_type ?? "") &&
9d879c035 (last.triggerRef ?? "") === (run.trigger_ref ?? "");
9d879c036 const withinWindow =
9d879c037 sameSeed &&
9d879c038 Number.isFinite(runMs) &&
9d879c039 last.newestMs - runMs <= INVOCATION_WINDOW_MS;
9d879c040
9d879c041 if (withinWindow) {
9d879c042 last.runs.push(run);
9d879c043 continue;
9d879c044 }
9d879c045
9d879c046 groups.push({
9d879c047 key: `inv-${run.id}`,
9d879c048 commitId: run.commit_id,
9d879c049 commitMessage: run.commit_message,
9d879c050 triggerType: run.trigger_type,
9d879c051 triggerRef: run.trigger_ref ?? null,
9d879c052 runs: [run],
9d879c053 newestRunId: run.id,
9d879c054 newestMs: Number.isFinite(runMs) ? runMs : 0,
9d879c055 });
9d879c056 }
9d879c057
9d879c058 for (const group of groups) {
9d879c059 // Keep build numbers in ascending order within each invocation.
9d879c060 group.runs.sort((a, b) => a.id - b.id);
9d879c061 }
9d879c062
9d879c063 return groups;
9d879c064}
9d879c065
9d879c066export function formatTriggerType(triggerType: string | null | undefined): string {
9d879c067 if (!triggerType) return "Build";
9d879c068 return triggerType.replace("_", " ").replace(/\b\w/g, (m) => m.toUpperCase());
9d879c069}
9d879c070
9d879c071export function getInvocationStatus<T extends InvocationRun>(runs: T[]): string {
9d879c072 const statuses = new Set(runs.map((run) => run.status));
9d879c073 if (statuses.has("running")) return "running";
9d879c074 if (statuses.has("failed")) return "failed";
9d879c075 if (statuses.has("pending")) return "pending";
9d879c076 if (statuses.has("cancelled")) return "cancelled";
9d879c077 return "passed";
9d879c078}
9d879c079
9d879c080function slugify(input: string): string {
9d879c081 return input
9d879c082 .toLowerCase()
9d879c083 .replace(/['"]/g, "")
9d879c084 .replace(/[^a-z0-9]+/g, "-")
9d879c085 .replace(/^-+|-+$/g, "")
9d879c086 .slice(0, 72);
9d879c087}
9d879c088
9d879c089export function getInvocationBaseSlug<T extends InvocationRun>(invocation: InvocationGroup<T>): string {
9d879c090 if (invocation.commitId) return invocation.commitId.substring(0, 8).toLowerCase();
9d879c091 const fromMessage = invocation.commitMessage ? slugify(invocation.commitMessage) : "";
9d879c092 if (fromMessage) return fromMessage;
9d879c093 const fromTrigger = invocation.triggerType ? slugify(invocation.triggerType) : "";
9d879c094 return fromTrigger || `run-${invocation.newestRunId}`;
9d879c095}
9d879c096
8a2c7d497export function getInvocationRunSlug<T extends InvocationRun>(
8a2c7d498 invocation: InvocationGroup<T>,
8a2c7d499 allInvocations?: InvocationGroup<T>[],
8a2c7d4100): string {
8a2c7d4101 const base = getInvocationBaseSlug(invocation);
8a2c7d4102 if (allInvocations) {
8a2c7d4103 const duplicates = allInvocations.filter(
8a2c7d4104 (g) => getInvocationBaseSlug(g) === base
8a2c7d4105 );
8a2c7d4106 if (duplicates.length > 1) {
8a2c7d4107 return `${base}-${invocation.newestRunId}`;
8a2c7d4108 }
8a2c7d4109 }
8a2c7d4110 return base;
9d879c0111}
9d879c0112
9d879c0113export function getSeedRunIdFromInvocationSlug(slug: string): number | null {
8a2c7d4114 // Match "run-<id>" format
8a2c7d4115 const runMatch = slug.match(/^run-(\d+)$/);
8a2c7d4116 if (runMatch) {
8a2c7d4117 const id = Number.parseInt(runMatch[1], 10);
8a2c7d4118 return Number.isNaN(id) ? null : id;
8a2c7d4119 }
8a2c7d4120 // Match disambiguated "<base>-<id>" suffix (e.g. "5fbd7f38-42")
8a2c7d4121 const suffixMatch = slug.match(/-(\d+)$/);
8a2c7d4122 if (suffixMatch) {
8a2c7d4123 const id = Number.parseInt(suffixMatch[1], 10);
8a2c7d4124 return Number.isNaN(id) ? null : id;
8a2c7d4125 }
8a2c7d4126 return null;
9d879c0127}