import { createBobbinClient, type BobbinContext, type QueryValue } from "#lib/api/client.ts";
import type { XrpcRequestInit } from "#lib/api/client.ts";
import { hostForKnot, jsonGet } from "#lib/api/_request.ts";
import { didFromUri, rkeyFromUri } from "#lib/api/uri.ts";
import { publicOnly } from "#lib/publicFetch.ts";
import type * as GetPipeline from "#lib/api/lexicons/types/org/tangled/ci/getPipeline.ts";
import type * as CiDefs from "#lib/api/lexicons/types/org/tangled/ci/defs.ts";
import type * as QueryPipelines from "#lib/api/lexicons/types/org/tangled/ci/queryPipelines.ts";
import type { PipelineSummary, PipelineTrigger } from "#lib/components/repo/types.ts";
import type { Fetch } from "#lib/fetch.ts";
const asParams = (params: object): Record<string, QueryValue> =>
params as unknown as Record<string, QueryValue>;
export const spindleUrl = (host: string): string => {
const trimmed = host.trim().replace(/\/+$/, "");
if (trimmed.includes("://")) return trimmed;
const bare = hostForKnot(trimmed);
return bare.startsWith("localhost:") ? `http://${bare}` : `https://${bare}`;
};
// the spindle is whatever the repo record names, and the server fetches from it while
// rendering, so its requests only go to a public https host and follow no redirects. a
// refused one rejects like an unreachable spindle, which every caller already handles
export const createSpindleClient = (host: string, fetch: Fetch): BobbinContext =>
createBobbinClient({ serviceUrl: spindleUrl(host), fetch: publicOnly(fetch) });
// queryPipelines filters by trigger nsid
const TRIGGER_NSIDS = {
push: "org.tangled.event.push",
pull_request: "org.tangled.event.pullRequest",
manual: "org.tangled.ci.trigger.manual"
} as const;
export const triggersOf = (filter: PipelineTrigger["kind"] | "all") =>
filter === "all" ? undefined : [TRIGGER_NSIDS[filter]];
export const queryPipelines = (
ctx: BobbinContext,
params: QueryPipelines.$params,
init?: XrpcRequestInit
) => jsonGet<QueryPipelines.$output>(ctx, "org.tangled.ci.queryPipelines", asParams(params), init);
export const getPipeline = (
ctx: BobbinContext,
params: GetPipeline.$params,
init?: XrpcRequestInit
) => jsonGet<GetPipeline.$output>(ctx, "org.tangled.ci.getPipeline", asParams(params), init);
const toTrigger = (trigger: CiDefs.Pipeline["trigger"], repoDid?: string): PipelineTrigger => {
switch (trigger.$type) {
case "org.tangled.event.push":
return { kind: "push", targetRef: trigger.ref.replace(/^refs\/(?:heads|tags)\//, "") };
case "org.tangled.event.pullRequest": {
const sourceRepo = trigger.sourceRepo ?? "";
const pull = trigger.pull;
return {
kind: "pull_request",
targetRef: trigger.targetBranch ?? "",
// fork ci labels are handled by the page resolving the name of the fork first
sourceLabel: trigger.sourceBranch ?? "",
...(sourceRepo && sourceRepo !== repoDid ? { sourceRepo } : {}),
// a pull lives in the repo it targets, whose did path redirects to owner/name
...(pull && repoDid && didFromUri(pull) === repoDid
? { pullPath: `/${repoDid}/pulls/${rkeyFromUri(pull)}` }
: {})
};
}
default:
return { kind: "manual" };
}
};
const durationOf = (workflow: CiDefs.WorkflowRun): number => {
if (!workflow.startedAt || !workflow.finishedAt) return 0;
const started = Date.parse(workflow.startedAt);
const finished = Date.parse(workflow.finishedAt);
if (Number.isNaN(started) || Number.isNaN(finished)) return 0;
return Math.max(0, finished - started);
};
export const toPipelineSummary = (
pipeline: CiDefs.Pipeline,
repoDid?: string
): PipelineSummary => ({
id: pipeline.id,
sha: pipeline.commit,
createdAt: pipeline.createdAt ?? "",
trigger: toTrigger(pipeline.trigger, repoDid),
workflows: (pipeline.workflows ?? []).map((workflow) => ({
// id is the key logs and getPipeline go by, name is optional
name: workflow.id,
status: workflow.status,
duration: durationOf(workflow),
error: workflow.error,
startedAt: workflow.startedAt,
finishedAt: workflow.finishedAt
}))
});