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"; const asParams = (params: object): Record => params as unknown as Record; 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: typeof globalThis.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(ctx, "org.tangled.ci.queryPipelines", asParams(params), init); export const getPipeline = ( ctx: BobbinContext, params: GetPipeline.$params, init?: XrpcRequestInit ) => jsonGet(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 })) });