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/packages/contracts/src/github.ts

152 lines5,870 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import type { ServiceBinding } from "./clients";
2import type { User } from "./identity";
3import type { Result } from "./result";
4
5/**
6 * GitHub: signing in with a GitHub account, and bringing repositories
7 * across through g1t's GitHub App. Mirrors `crates/contracts/src/github.rs`.
8 *
9 * The identity service keeps linked accounts and their user tokens; the
10 * integrations service keeps installations, linked repositories and the
11 * app's webhook. With no app configured, `enabled`/`configured` are false
12 * and the site shows no GitHub buttons.
13 */
14
15export type GithubPurpose = "sign_in" | "link";
16
17/** How a return from GitHub ended. */
18export type GithubFinished =
19 | { kind: "signed_in"; signedIn: { user: User; sessionToken: string }; created: boolean; next: string }
20 | { kind: "linked"; login: string; next: string }
21 /** An account has one of its verified emails: sign in to it, then claim. */
22 | { kind: "needs_link"; pending: string; login: string; next: string }
23 /** A new account waiting on a username, or on an invite code. */
24 | { kind: "needs_username"; pending: string; login: string; suggestion: string; next: string; inviteRequired: boolean };
25
26export type GithubPending = {
27 login: string;
28 kind: "link" | "username";
29 suggestion: string | null;
30 next: string;
31 inviteRequired: boolean;
32};
33
34export type GithubAccount = {
35 githubId: number;
36 login: string;
37 linkedAt: string;
38 /** Whether g1t holds a working user token for it. */
39 authorized: boolean;
40};
41
42export type GithubAccountView = {
43 enabled: boolean;
44 account: GithubAccount | null;
45 hasPassword: boolean;
46};
47
48/** How a GitHub repository comes to g1t. */
49export type GithubMode = "import" | "mirror" | "push";
50
51export type GithubInstallation = {
52 id: number;
53 workspace: string;
54 account: string;
55 accountType: string;
56 repositorySelection: "all" | "selected" | string;
57 suspended: boolean;
58 settingsUrl: string;
59 createdAt: string;
60};
61
62export type GithubAppStatus = {
63 configured: boolean;
64 installUrl: string | null;
65 linked: boolean;
66 installations: GithubInstallation[];
67};
68
69export type GithubRepository = {
70 id: number;
71 fullName: string;
72 name: string;
73 private: boolean;
74 description: string | null;
75 defaultBranch: string;
76 /** The g1t repository already linked to it, as `workspace/name`. */
77 linkedTo: string | null;
78};
79
80export type GithubRepositories = {
81 repositories: GithubRepository[];
82 total: number;
83 page: number;
84 perPage: number;
85};
86
87export type GithubRepoLink = {
88 repoId: string;
89 repo: string;
90 installationId: number;
91 githubRepoId: number;
92 fullName: string;
93 mode: GithubMode;
94 syncedAt: string | null;
95 lastError: string | null;
96 issuesImported: number;
97};
98
99export type GithubImportInput = {
100 installationId: number;
101 githubRepoId: number;
102 name?: string;
103 mode: GithubMode;
104 private?: boolean;
105 issues: boolean;
106};
107
108async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
109 const response = await service.fetch(`https://service/rpc/${method}`, {
110 method: "POST",
111 headers: { "content-type": "application/json" },
112 body: JSON.stringify(args),
113 });
114 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
115 return (await response.json()) as T;
116}
117
118/** Signing in with GitHub: methods of the identity service. */
119export function githubSignInClient(identity: ServiceBinding) {
120 const call = <T>(method: string, args: object) => rpc<T>(identity, method, args);
121 return {
122 enabled: () => call<boolean>("github_enabled", {}),
123 start: (input: { purpose: GithubPurpose; user?: User | null; redirectUri: string; next: string; inviteCode?: string | null }) =>
124 call<Result<{ authorizeUrl: string; state: string }>>("github_start", input),
125 finish: (state: string, code: string) => call<Result<GithubFinished>>("github_finish", { state, code }),
126 pending: (pending: string) => call<Result<GithubPending>>("github_pending", { pending }),
127 signUp: (pending: string, username: string, inviteCode?: string | null) =>
128 call<Result<{ user: User; sessionToken: string }>>("github_sign_up", { pending, username, inviteCode: inviteCode ?? null }),
129 claim: (pending: string, user: User) => call<Result<GithubAccount>>("github_claim", { pending, user }),
130 account: (user: User) => call<GithubAccountView>("github_account", { user }),
131 unlink: (user: User) => call<Result<boolean>>("github_unlink", { user }),
132 };
133}
134
135/** g1t's GitHub App: methods of the integrations service. */
136export function githubAppClient(integrations: ServiceBinding) {
137 const call = <T>(method: string, args: object) => rpc<T>(integrations, method, args);
138 return {
139 status: (viewer: User, workspace: string) => call<Result<GithubAppStatus>>("github_status", { viewer, workspace }),
140 addInstallation: (actor: User, workspace: string, installationId: number) =>
141 call<Result<GithubInstallation>>("github_add_installation", { actor, workspace, installationId }),
142 removeInstallation: (actor: User, workspace: string, installationId: number) =>
143 call<Result<boolean>>("github_remove_installation", { actor, workspace, installationId }),
144 repositories: (actor: User, workspace: string, installationId: number, page?: number) =>
145 call<Result<GithubRepositories>>("github_repositories", { actor, workspace, installationId, page: page ?? null }),
146 import: (actor: User, workspace: string, input: GithubImportInput) =>
147 call<Result<GithubRepoLink>>("github_import", { actor, workspace, ...input }),
148 link: (repoId: string) => call<GithubRepoLink | null>("github_link", { repoId }),
149 unlinkRepo: (actor: User, repoId: string) => call<Result<boolean>>("github_unlink_repo", { actor, repoId }),
150 sync: (actor: User, repoId: string) => call<Result<GithubRepoLink>>("github_sync", { actor, repoId }),
151 };
152}