flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/web/app/lib/catch-up.ts

72 lines2,900 bytesCodeBlame
1/**
2 * Where a pull request's catch-up stands once it has been handed to a
3 * sandbox: still going, done, failed, or taking far longer than it should.
4 * Pure, so it is tested on its own; it imports only types.
5 */
6import type { AgentRun, PullBranchUpdate } from "@g1t/contracts";
7
8/** A sandbox catch-up usually takes about a minute; past this, say so. */
9export const CATCH_UP_TIMEOUT_MS = 5 * 60_000;
10/** Runs created this long before the request was made still count as its run. */
11const CLOCK_SLACK_MS = 30_000;
12
13export type CatchUpPhase = "working" | "done" | "failed" | "timed_out";
14
15/**
16 * The `update` run started for a request made at `startedAt`, newest
17 * first, if it has shown up yet.
18 */
19export function catchUpRun(runs: AgentRun[], startedAt: number): AgentRun | null {
20 return (
21 runs
22 .filter((run) => run.kind === "update" && Date.parse(run.createdAt) >= startedAt - CLOCK_SLACK_MS)
23 .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))[0] ?? null
24 );
25}
26
27/**
28 * Where it stands. It is done as soon as the pull request is no longer
29 * behind, whatever the run says; failed when its run ended without that;
30 * timed out when nothing has settled it for too long.
31 */
32export function catchUpPhase({
33 behind,
34 run,
35 startedAt,
36 now,
37}: {
38 behind: boolean;
39 run: AgentRun | null;
40 startedAt: number;
41 now: number;
42}): CatchUpPhase {
43 if (!behind) return "done";
44 if (run && (run.status === "failed" || run.status === "stopped")) return "failed";
45 // Succeeded, but the page has not seen the push yet: give it the same time.
46 if (now - startedAt > CATCH_UP_TIMEOUT_MS) return "timed_out";
47 return "working";
48}
49
50type NeedsAgent = Extract<PullBranchUpdate, { outcome: "needs_agent" }>;
51
52/** What the box says while a sandbox brings the pull request up to date. */
53export function catchUpTitle(reason: NeedsAgent["reason"], defaultBranch: string): string {
54 return reason === "conflicting"
55 ? `g1t-agent is resolving conflicts with ${defaultBranch}`
56 : `g1t-agent is merging ${defaultBranch} into this pull request`;
57}
58
59/** Why it went to a sandbox, in a sentence. */
60export function catchUpWhy(update: Pick<NeedsAgent, "reason" | "paths">, defaultBranch: string): string {
61 const files = update.paths.length === 1 ? "one file" : `${update.paths.length} files`;
62 switch (update.reason) {
63 case "conflicting":
64 return `Merging ${defaultBranch} conflicts in ${files}, so g1t-agent resolves them in a sandbox and pushes the result.`;
65 case "overlap":
66 return update.paths.length > 0
67 ? `This pull request and ${defaultBranch} both changed ${files}, so they are merged with git in a sandbox. g1t-agent resolves any conflicts.`
68 : `They are merged with git in a sandbox. g1t-agent resolves any conflicts.`;
69 default:
70 return `They are merged with git in a sandbox. g1t-agent resolves any conflicts.`;
71 }
72}