Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439// fixture pipelines for the pipeline and commit-list stories. the app reads real// runs from a spindle, so nothing here ever reaches the bundle
import type { PipelineStatus, PipelineSummary } from "$lib/components/repo/types";import type { LogControl, LogFrame } from "$lib/api/spindleLogs";import { createLogAccumulator, type LogStep } from "$lib/components/repo/pipelines/logs";
const minutes = (n: number) => n * 60_000;
// fixed base so the times do not depend on when this runsconst base = Date.parse("2026-08-04T09:00:00Z");const ago = (mins: number) => new Date(base - minutes(mins)).toISOString();
export const pipelines: PipelineSummary[] = [ { id: "01K1XJ4WPQ0000000000000001", sha: "9f2c1ab4d8e7350f6b1c2d9e4a5b6c7d8e9f0a1b", createdAt: ago(3), trigger: { kind: "push", targetRef: "master" }, workflows: [ { name: "build", status: "success", duration: minutes(1) + 12_000 }, { name: "test", status: "running", duration: 0 }, { name: "lint", status: "pending", duration: 0 } ] }, { id: "01K1XJ4WPQ0000000000000002", sha: "3d7e91c05fa2b8461d0e5c7a9b3f2e1d4c6a8b0f", createdAt: ago(47), trigger: { kind: "pull_request", targetRef: "master", sourceLabel: "fix-packfile-timeout", sourceRepo: "did:plc:mockfork", pullPath: "/tangled.org/core/pulls/42" }, workflows: [ { name: "build", status: "success", duration: minutes(2) + 4_000 }, { name: "test", status: "failed", duration: minutes(6) + 31_000, error: "3 tests failed" }, { name: "lint", status: "success", duration: 41_000 } ] }, { id: "01K1XJ4WPQ0000000000000003", sha: "c1b4a7e2f9308d65b2a1c4e7f0d3b6a9c2e5f8b1", createdAt: ago(190), trigger: { kind: "push", targetRef: "master" }, workflows: [ { name: "build", status: "success", duration: minutes(1) + 58_000 }, { name: "test", status: "success", duration: minutes(5) + 12_000 }, { name: "lint", status: "success", duration: 39_000 } ] }, { id: "01K1XJ4WPQ0000000000000004", sha: "7a0f3b8c1d2e4f5a6b7c8d9e0f1a2b3c4d5e6f70", createdAt: ago(320), trigger: { kind: "manual" }, workflows: [] }, { id: "01K1XJ4WPQ0000000000000005", sha: "e5d4c3b2a1908f7e6d5c4b3a2918f7e6d5c4b3a2", createdAt: ago(1_450), trigger: { kind: "pull_request", targetRef: "release-1.4", sourceLabel: "bump-deps" }, workflows: [ { name: "build", status: "success", duration: minutes(2) + 9_000 }, { name: "e2e", status: "timeout", duration: minutes(30) } ] }, { id: "01K1XJ4WPQ0000000000000006", sha: "b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9", createdAt: ago(2_880), // the appview strips refs/heads/ and refs/tags/ before it reaches the card trigger: { kind: "push", targetRef: "v1.3.0" }, workflows: [ { name: "build", status: "cancelled", duration: 18_000 }, { name: "release", status: "cancelled", duration: 0 } ] }];
// fake per-commit runs for the commit list and header stories. the real call// is the spindle's ci.queryPipelines with a commits filter, one run per// commit. every third commit is left without a run so the gap renders too.export const pipelinesByCommitHash = (hashes: string[]): Record<string, PipelineSummary> => { const byHash: Record<string, PipelineSummary> = {}; hashes.forEach((hash, index) => { if (index % 3 === 2) return; byHash[hash] = { ...pipelines[index % pipelines.length], sha: hash }; }); return byHash;};
// log fixtures, built through the real accumulator so the stories exercise the// same ansi rendering the stream does. shaped after a live spindle tail: a// runner-injected setup step, then the workflow's own steps.const control = (step: number, content: string, over: Partial<LogControl> = {}): LogFrame => ({ kind: "control", control: { time: ago(6), workflow: "build", step, content, status: "start", ...over }});
const finish = (step: number, seconds: number): LogFrame => ({ kind: "control", control: { time: new Date(base - minutes(6) + seconds * 1000).toISOString(), workflow: "build", step, content: "", status: "end" }});
const output = (step: number, content: string): LogFrame => ({ kind: "data", data: { time: ago(6), workflow: "build", step, content, stream: "stdout" }});
const drain = (frames: LogFrame[]): LogStep[] => { const accumulator = createLogAccumulator(); for (const frame of frames) accumulator.push(frame); return accumulator.steps;};
export const workflowLog: LogStep[] = drain([ control(-1, "microVM setup", { kind: "system" }), output(0, "starting microVM image alpine\n"), output( 0, "agent connected; serial log: /tmp/spindle-microvm-knot1.tangled.sh-3msfzd67fzh22-deploy-web-dev.yml-95471715/serial.log\n" ), finish(-1, 3), control(0, "Clone repository into workspace", { kind: "system", command: "git init\ngit fetch --depth=1 origin 9f2c1ab\ngit checkout FETCH_HEAD" }), output(0, "warning: templates not found\n"), output(0, "HEAD is now at 9f2c1ab\n"), finish(0, 9), control(1, "install dependencies", { command: "pnpm install --frozen-lockfile" }), ...Array.from({ length: 12 }, (_, index) => output(1, `\u001b[90m+\u001b[0m package-${index} \u001b[32m1.${index}.0\u001b[0m\n`) ), finish(1, 41), control(2, "run tests", { command: "pnpm test" }), output(2, "\u001b[1mRUN\u001b[0m v4.1.10\n"), output(2, "\u001b[32m✓\u001b[0m src/lib/format.test.ts (12)\n"), output(2, "\u001b[31m✗\u001b[0m src/lib/api/spindle.test.ts (1 failed)\n"), output(2, "\u001b[31mAssertionError: expected 'pending' to be 'running'\u001b[0m\n"), output( 2, " a line long enough to wrap on a narrow viewport, which is what the log rows measure themselves for\n" ), finish(2, 96)]);
/** the same run mid-flight: the last step is open, so its timer keeps counting */export const runningWorkflowLog: LogStep[] = drain([ control(0, "Clone repository into workspace", { kind: "system", command: "git init" }), output(0, "HEAD is now at 9f2c1ab\n"), finish(0, 9), // the one stamp that has to be live, so the ticking timer shows something sane control(1, "run tests", { time: new Date(Date.now() - 12_000).toISOString(), command: "pnpm test" }), output(1, "\u001b[1mRUN\u001b[0m v4.1.10\n"), output(1, "\u001b[32m✓\u001b[0m src/lib/format.test.ts (12)\n")]);
/** a run that died on its last step: every step closed, and nothing after it */export const failedWorkflowLog: LogStep[] = drain([ control(-1, "microVM setup", { kind: "system" }), output(-1, "starting microVM image alpine\n"), finish(-1, 3), control(0, "Clone repository into workspace", { kind: "system", command: "git init\ngit fetch --depth=1 origin 9f2c1ab\ngit checkout FETCH_HEAD" }), output(0, "HEAD is now at 9f2c1ab\n"), finish(0, 9), control(1, "install dependencies", { command: "pnpm install --frozen-lockfile" }), output(1, "\u001b[90m+\u001b[0m package-a \u001b[32m1.0.0\u001b[0m\n"), output(1, "\u001b[90m+\u001b[0m package-b \u001b[32m2.1.0\u001b[0m\n"), output(1, "\u001b[90m+\u001b[0m package-c \u001b[32m0.4.2\u001b[0m\n"), finish(1, 22), control(2, "run tests", { command: "pnpm test" }), output(2, "\u001b[1mRUN\u001b[0m v4.1.10\n"), output(2, "\u001b[32m✓\u001b[0m src/lib/format.test.ts (12)\n"), output(2, "\u001b[31m✗\u001b[0m src/lib/api/spindleLogs.test.ts (1 failed)\n"), output(2, "\u001b[31mAssertionError: expected 3 to be 4\u001b[0m\n"), output( 2, "\u001b[2mTests\u001b[0m \u001b[31m1 failed\u001b[0m | \u001b[32m11 passed\u001b[0m (12)\n" ), finish(2, 58)]);
/** killed mid-step: the last step never got its end frame */export const cancelledWorkflowLog: LogStep[] = drain([ control(-1, "microVM setup", { kind: "system" }), output(-1, "starting microVM image alpine\n"), finish(-1, 3), control(0, "Clone repository into workspace", { kind: "system", command: "git init\ngit fetch --depth=1 origin 9f2c1ab\ngit checkout FETCH_HEAD" }), output(0, "HEAD is now at 9f2c1ab\n"), finish(0, 9), control(1, "install dependencies", { command: "pnpm install --frozen-lockfile" }), output(1, "\u001b[90m+\u001b[0m package-a \u001b[32m1.0.0\u001b[0m\n"), finish(1, 22), control(2, "build the site", { command: "pnpm build" }), output(2, "[0] \u001b[36mbundle\u001b[0m src/module-0.ts\n"), output(2, "[1] \u001b[36mbundle\u001b[0m src/module-1.ts\n")]);
const tailFrames: LogFrame[] = [ control(-1, "microVM setup", { kind: "system" }), output(-1, "starting microVM image alpine\n"), finish(-1, 3), control(0, "install dependencies", { command: "pnpm install --frozen-lockfile" }), ...Array.from({ length: 3 }, (_, index) => output(0, `\u001b[90m+\u001b[0m package-${index} \u001b[32m1.${index}.0\u001b[0m\n`) ), finish(0, 41), control(1, "run tests", { command: "pnpm test" }), output(1, "\u001b[1mRUN\u001b[0m v4.1.10\n"), ...Array.from({ length: 3 }, (_, index) => output(1, `\u001b[32m✓\u001b[0m src/lib/suite-${index}.test.ts (${index + 2})\n`) ), finish(1, 96), control(2, "build the site", { command: "pnpm build" }), ...Array.from({ length: 3 }, (_, index) => output(2, `[${index}] \u001b[36mbundle\u001b[0m src/module-${index}.ts\n`) )];
// the tail is replayed live, so its stamps have to be too, or every step reads// as an hour longconst restamp = (frame: LogFrame): LogFrame => { const time = new Date().toISOString(); if (frame.kind === "control") return { kind: "control", control: { ...frame.control, time } }; if (frame.kind === "data") return { kind: "data", data: { ...frame.data, time } }; return frame;};
/** * hands out a growing log, one frame per call, through the real accumulator. a * story drives it on a timer, so the scroll behavior can be watched against a * moving tail instead of a fixed fixture. */export const createLogTail = (): { advance: () => LogStep[]; exhausted: () => boolean } => { const accumulator = createLogAccumulator(); let index = 0; return { advance() { const frame = tailFrames[index]; if (frame) { index += 1; accumulator.push(restamp(frame)); } return [...accumulator.steps]; }, exhausted: () => index >= tailFrames.length };};
/** a step big enough that the viewport can only hold a slice of it */export const longWorkflowLog: LogStep[] = drain([ control(1, "build everything", { command: "make -j8" }), ...Array.from({ length: 300 }, (_, index) => output(1, `[${index}] \u001b[36mCC\u001b[0m src/module-${index}.c\n`) )]);
// a whole run, scripted step by step, for the stories that watch the rail fill inconst runScript = [ { name: "microVM setup", system: true, seconds: 3, lines: ["starting microVM image alpine\n", "agent connected\n"] }, { name: "Clone repository into workspace", system: true, command: "git init\ngit fetch --depth=1 origin 9f2c1ab\ngit checkout FETCH_HEAD", seconds: 9, lines: ["warning: templates not found\n", "HEAD is now at 9f2c1ab\n"] }, { name: "install dependencies", command: "pnpm install --frozen-lockfile", seconds: 41, lines: [ "\u001b[90m+\u001b[0m package-a \u001b[32m1.0.0\u001b[0m\n", "\u001b[90m+\u001b[0m package-b \u001b[32m2.1.0\u001b[0m\n" ] }, { name: "run tests", command: "pnpm test", seconds: 96, lines: [ "\u001b[1mRUN\u001b[0m v4.1.10\n", "\u001b[32m✓\u001b[0m src/lib/format.test.ts (12)\n" ] }, { name: "build the site", command: "pnpm build", seconds: 58, lines: ["[0] \u001b[36mbundle\u001b[0m src/module-0.ts\n", "wrote dist/index.html\n"] }];
const FAILURE_LINE = "\u001b[31merror: step failed with exit code 1\u001b[0m\n";
type RunAction = | { kind: "start"; step: number; content: string; command?: string; system?: boolean } | { kind: "line"; step: number; content: string } | { kind: "end"; step: number; seconds: number };
export interface WorkflowRun { /** pushes the next frame and hands back the log as the view sees it */ advance: () => LogStep[]; exhausted: () => boolean; /** running until the last frame lands, then whatever the run came to */ status: () => PipelineStatus;}
/** * replays a workflow frame by frame, the way a live tail arrives. `failAt` is the * index of the step that dies — the spindle closes it and starts nothing after it — * and `cancelAt` is a step killed mid-flight, so it never gets its end frame. */export const createWorkflowRun = ( options: { failAt?: number; cancelAt?: number; steps?: number } = {}): WorkflowRun => { const { failAt, cancelAt, steps = runScript.length } = options; const actions: RunAction[] = []; for (const [index, step] of runScript.slice(0, steps).entries()) { // the runner's own setup step is numbered below the workflow's first const id = index - 1; actions.push({ kind: "start", step: id, content: step.name, command: step.command, system: step.system }); for (const line of step.lines) actions.push({ kind: "line", step: id, content: line }); if (index === cancelAt) break; if (index === failAt) { actions.push({ kind: "line", step: id, content: FAILURE_LINE }); actions.push({ kind: "end", step: id, seconds: step.seconds }); break; } actions.push({ kind: "end", step: id, seconds: step.seconds }); }
const accumulator = createLogAccumulator(); const startedAt = new Map<number, number>(); const at = (ms: number) => new Date(ms).toISOString(); let cursor = 0;
const push = (action: RunAction) => { if (action.kind === "line") { accumulator.push({ kind: "data", data: { time: at(Date.now()), workflow: "build", step: action.step, content: action.content, stream: "stdout" } }); return; } if (action.kind === "start") { startedAt.set(action.step, Date.now()); accumulator.push({ kind: "control", control: { time: at(Date.now()), workflow: "build", step: action.step, content: action.content, command: action.command, kind: action.system ? "system" : "user", status: "start" } }); return; } // the scripted duration is what the rail reads out, so the end stamp is measured // off the step's own start and not off the replay's wall clock accumulator.push({ kind: "control", control: { time: at((startedAt.get(action.step) ?? Date.now()) + action.seconds * 1000), workflow: "build", step: action.step, content: "", status: "end" } }); };
return { advance() { const action = actions[cursor]; if (action) { cursor += 1; push(action); } return [...accumulator.steps]; }, exhausted: () => cursor >= actions.length, status: () => { if (cursor < actions.length) return "running"; if (cancelAt !== undefined) return "cancelled"; return failAt !== undefined ? "failed" : "success"; } };};