~james/core

core/web/src/lib/api/spindle.ts -rw-r--r-- 4.0 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
	}))
});