Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

178 lines · 6.8 KBRaw History
1/**
2 * JSON-safe values. Typed loosely on purpose: a recursive JSON type is too deep
3 * for the Durable Object RPC type mapping, and `unknown` is not serialisable.
4 */
5// biome-ignore lint/suspicious/noExplicitAny: see above
6export type Json = any;
7
8/**
9 * Contract between the Diffinery control plane (Worker) and the runner agent
10 * that executes a single job inside a Cloudflare Container.
11 *
12 * All runner endpoints live under `${apiUrl}/runner/jobs/:jobId/...` and are
13 * authenticated with `Authorization: Bearer <job token>`. The job token is
14 * scoped to one job, one repository (read access for clone) and expires when
15 * the job's timeout elapses.
16 */
17
18export type Conclusion = "success" | "failure" | "cancelled" | "skipped";
19
20export type StepSpec = {
21 id?: string;
22 name?: string;
23 if?: string;
24 run?: string;
25 uses?: string;
26 with?: Record<string, string>;
27 env?: Record<string, string>;
28 shell?: string;
29 "working-directory"?: string;
30 "continue-on-error"?: boolean | string;
31 "timeout-minutes"?: number;
32};
33
34/** `jobs.<id>.container` or one of `jobs.<id>.services`; values may contain `${{ }}`. */
35export type ContainerSpec = {
36 image: string;
37 credentials?: { username?: string; password?: string };
38 env?: Record<string, string>;
39 ports?: Array<string | number>;
40 volumes?: string[];
41 options?: string;
42};
43
44export interface NeedsResult {
45 result: Conclusion;
46 outputs: Record<string, string>;
47}
48
49export type JobSpec = {
50 /** Hosted runners: a log line naming the runner's size and why the job got it (apps/diffinery/src/runner/sizes.ts). */
51 runnerSize?: string;
52 runId: string;
53 jobId: string;
54 /** Key of the job in the workflow file, e.g. `build`. */
55 jobKey: string;
56 /** Display name, already evaluated (matrix values substituted). */
57 name: string;
58 repo: { owner: string; name: string; cloneUrl: string; defaultBranch: string };
59 sha: string;
60 ref: string;
61 workflowPath: string;
62 /** The full `github` context (event name, payload, actor, sha, ref, ...). */
63 github: Record<string, Json>;
64 /** Workflow-level env merged with job-level env; values may contain `${{ }}`. */
65 env: Record<string, string>;
66 defaults?: { run?: { shell?: string; "working-directory"?: string } };
67 matrix: Record<string, Json>;
68 strategy: { "fail-fast": boolean; "job-index": number; "job-total": number; "max-parallel"?: number };
69 needs: Record<string, NeedsResult>;
70 inputs: Record<string, Json>;
71 vars: Record<string, string>;
72 /** Only the secrets this job may see. Values must be masked in logs. */
73 secrets: Record<string, string>;
74 /** Job `outputs:` map; values are expressions evaluated after the steps. */
75 outputs: Record<string, string>;
76 steps: StepSpec[];
77 /** Job container: steps run inside it (needs Docker on the runner). */
78 container?: ContainerSpec;
79 /** Service containers, reachable by name from a job container or via mapped ports. */
80 services?: Record<string, ContainerSpec>;
81 timeoutMinutes: number;
82 continueOnError: boolean;
83 /** Whether `permissions: id-token: write` was granted. */
84 idToken: boolean;
85 /** Base URL of the Diffinery API, e.g. `https://diffinery.com`. */
86 apiUrl: string;
87 /** Bearer token for runner endpoints and for `git clone` (as password). */
88 token: string;
89 /**
90 * GitHub's Actions runtime services (cache and artifacts, as the official
91 * toolkit speaks them). Absent when the server cannot sign runtime tokens.
92 */
93 runtime?: {
94 /** ACTIONS_RUNTIME_TOKEN: a JWT for this job only; it expires with the job's timeout. */
95 token: string;
96 /** ACTIONS_RESULTS_URL (cache v2 and artifacts v4 Twirp services). */
97 resultsUrl: string;
98 /** ACTIONS_CACHE_URL (legacy cache v1 REST API). */
99 cacheUrl: string;
100 /** ACTIONS_RUNTIME_URL. */
101 runtimeUrl: string;
102 };
103};
104
105export type LogLine = {
106 /** ISO timestamp. */
107 t: string;
108 /** Index of the step this line belongs to; -1 for job setup/teardown. */
109 step: number;
110 text: string;
111};
112
113export type StepReport = {
114 status: "in_progress" | "completed";
115 conclusion?: Conclusion;
116 name: string;
117 startedAt?: string;
118 completedAt?: string;
119};
120
121export type JobCompletion = {
122 conclusion: Conclusion;
123 outputs: Record<string, string>;
124 /** Markdown written to $GITHUB_STEP_SUMMARY, if any. */
125 summary?: string;
126 /** Short machine-readable reason when the job failed (for agents). */
127 failure?: { step?: number; message: string };
128 /**
129 * Hosted runners: the platform began replacing the runner's container (a rollout's SIGTERM)
130 * and the job didn't succeed, so Diffinery may start it again on a new runner.
131 */
132 replaced?: boolean;
133 /** What the job used of its machine, measured by the runner (hosted runners size later runs by it). */
134 usage?: JobUsage;
135};
136
137/**
138 * A job's use of its machine, sampled by the runner while the job ran (containers/runner/src/usage.ts).
139 * Busy cores come from /proc/stat, memory from /proc/meminfo (total less available), disk from the
140 * root filesystem. Sampled every half second, so a spike shorter than that can be missed.
141 */
142/** Upper bounds (busy cores) of the levels JobUsage.busySeconds counts. */
143export const BUSY_BUCKETS: readonly number[] = [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8, 16, 32, 64];
144
145export type JobUsage = {
146 /** Seconds measured, and CPU seconds used (all cores) in that time. */
147 seconds: number;
148 cpuSeconds: number;
149 /** Busy cores at the 95th percentile and in the busiest window (see windowSeconds). */
150 coresP95: number;
151 coresMax: number;
152 /** Share of samples (0 to 1) in which the job kept nearly all its cores busy (90%, or all but a quarter core): it was held back by the CPU. */
153 saturated: number;
154 /** Seconds spent at each level of busy cores: entry i counts windows with at most BUSY_BUCKETS[i] cores busy (and more than the one before). */
155 busySeconds: number[];
156 /** How long the windows busy cores were measured over, in seconds (absent from runners before 2026-10-16, which used 2 s). */
157 windowSeconds?: number;
158 /** Peak memory and disk in use, in bytes. */
159 memoryPeak: number;
160 diskPeak: number;
161 /** What the machine had: vCPUs (its share, which can be a fraction), memory and disk in bytes. */
162 cores: number;
163 memory: number;
164 disk: number;
165 /** Processes the kernel killed for want of memory while the job ran (null when it doesn't say). */
166 oomKills: number | null;
167};
168
169/** Runner API paths, relative to `${apiUrl}/runner/jobs/${jobId}`. */
170export const RunnerPaths = {
171 spec: "",
172 logs: "/logs", // POST { lines: LogLine[] }
173 step: (index: number) => `/steps/${index}`, // POST StepReport
174 complete: "/complete", // POST JobCompletion
175 cache: "/cache", // GET ?key=&restore=a,b -> { key, url } | 404 ; PUT /cache/:key body=tar.gz
176 artifacts: "/artifacts", // PUT /artifacts/:name body=zip|tar ; GET /artifacts/:name
177 oidc: "/oidc", // POST { audience } -> { value }
178} as const;