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/crates/contracts/src/github.rs

371 lines12,042 bytesCodeBlame
1//! GitHub: signing in with a GitHub account, and bringing repositories
2//! across through g1t's GitHub App. Mirrors `packages/contracts/src/github.ts`.
3//!
4//! One GitHub App does both. Its user authorization (OAuth web flow, with
5//! PKCE) signs people in and lets g1t list what they installed it on; its
6//! installations give g1t access to the repositories they chose. The app
7//! is optional: with none configured every method here says so, and the
8//! site shows no GitHub buttons.
9//!
10//! The identity service (`POST /rpc/<method>`) keeps linked accounts and
11//! their user tokens:
12//!
13//! - `github_enabled` (no arguments) returns `bool`.
14//! - `github_start` takes `GithubStartArgs`, returns `Outcome<GithubStart>`.
15//! - `github_finish` takes `GithubFinishArgs`, returns `Outcome<GithubFinished>`.
16//! - `github_pending` takes `GithubPendingArgs`, returns `Outcome<GithubPending>`.
17//! - `github_sign_up` takes `GithubSignUpArgs`, returns `Outcome<SignedIn>`.
18//! - `github_claim` takes `GithubClaimArgs`, returns `Outcome<GithubAccount>`.
19//! - `github_account` takes `UserArgs`, returns `GithubAccountView`.
20//! - `github_unlink` takes `UserArgs`, returns `Outcome<bool>`.
21//! - `github_user_token` takes `GithubUserTokenArgs`, returns `Outcome<String>`.
22//! - `github_revoked` takes `GithubRevokedArgs`, returns `u32`.
23//! - `github_usernames` takes `GithubUsernamesArgs`, returns a map.
24//!
25//! The integrations service keeps installations and linked repositories,
26//! and receives the app's webhook at `https://api.g1t.sh/hooks/github`:
27//!
28//! - `github_status` takes `GithubStatusArgs`, returns `Outcome<GithubAppStatus>`.
29//! - `github_add_installation` takes `GithubInstallationArgs`, returns
30//! `Outcome<GithubInstallation>`.
31//! - `github_remove_installation` takes `GithubInstallationArgs`, returns
32//! `Outcome<bool>`.
33//! - `github_repositories` takes `GithubRepositoriesArgs`, returns
34//! `Outcome<GithubRepositories>`.
35//! - `github_import` takes `GithubImportArgs`, returns `Outcome<GithubRepoLink>`.
36//! - `github_link` takes `GithubLinkArgs`, returns `Option<GithubRepoLink>`.
37//! - `github_unlink_repo` takes `GithubUnlinkRepoArgs`, returns `Outcome<bool>`.
38//! - `github_sync` takes `GithubUnlinkRepoArgs`, returns `Outcome<GithubRepoLink>`.
39//! - `github_receive` takes `GithubReceiveArgs`, returns `integrations::Received`.
40
41use std::collections::HashMap;
42
43use serde::{Deserialize, Serialize};
44
45use crate::User;
46use crate::identity::SignedIn;
47
48/// Why someone is sent to GitHub.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum GithubPurpose {
52 /// Sign in, or create an account.
53 SignIn,
54 /// Link GitHub to the signed-in account.
55 Link,
56}
57
58impl GithubPurpose {
59 pub fn as_str(self) -> &'static str {
60 match self {
61 GithubPurpose::SignIn => "sign_in",
62 GithubPurpose::Link => "link",
63 }
64 }
65}
66
67#[derive(Debug, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct GithubStartArgs {
70 pub purpose: GithubPurpose,
71 /// The signed-in person, for `link`.
72 #[serde(default)]
73 pub user: Option<User>,
74 /// Exactly the callback registered on the app, such as
75 /// `https://g1t.sh/auth/github/callback`.
76 pub redirect_uri: String,
77 /// A same-site path to return to afterwards.
78 #[serde(default)]
79 pub next: String,
80 /// An invite code, from `/register?invite=…`, carried through GitHub
81 /// for a new account while g1t is invite-only.
82 #[serde(default)]
83 pub invite_code: Option<String>,
84}
85
86/// Where to send the browser, and the state to bind to it in a cookie.
87#[derive(Debug, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct GithubStart {
90 pub authorize_url: String,
91 pub state: String,
92}
93
94#[derive(Debug, Serialize, Deserialize)]
95pub struct GithubFinishArgs {
96 pub state: String,
97 pub code: String,
98}
99
100/// How a return from GitHub ended.
101#[derive(Debug, Serialize, Deserialize)]
102#[serde(tag = "kind", rename_all = "snake_case")]
103pub enum GithubFinished {
104 /// Signed in to the account the GitHub account is linked to, or to a
105 /// new one made with it.
106 SignedIn {
107 #[serde(rename = "signedIn")]
108 signed_in: SignedIn,
109 created: bool,
110 next: String,
111 },
112 /// Linked to the account that asked.
113 Linked { login: String, next: String },
114 /// An account with one of its verified emails exists: the person signs
115 /// in to it, and `github_claim` then links it.
116 NeedsLink { pending: String, login: String, next: String },
117 /// A new account, but the GitHub login cannot be its username.
118 /// A new account, but the GitHub login cannot be its username, or g1t
119 /// is invite-only and no invite code came with the sign-in.
120 NeedsUsername {
121 pending: String,
122 login: String,
123 suggestion: String,
124 next: String,
125 #[serde(rename = "inviteRequired")]
126 invite_required: bool,
127 },
128}
129
130#[derive(Debug, Serialize, Deserialize)]
131pub struct GithubPendingArgs {
132 pub pending: String,
133}
134
135/// A GitHub sign-in waiting on a username or on signing in to link.
136#[derive(Debug, Serialize, Deserialize)]
137pub struct GithubPending {
138 pub login: String,
139 /// `link` or `username`.
140 pub kind: String,
141 pub suggestion: Option<String>,
142 pub next: String,
143 /// Whether the person must give an invite code to create the account.
144 #[serde(rename = "inviteRequired", default)]
145 pub invite_required: bool,
146}
147
148#[derive(Debug, Serialize, Deserialize)]
149pub struct GithubSignUpArgs {
150 pub pending: String,
151 pub username: String,
152 /// Needed while g1t is invite-only, unless one came with the sign-in.
153 #[serde(rename = "inviteCode", default)]
154 pub invite_code: Option<String>,
155}
156
157#[derive(Debug, Serialize, Deserialize)]
158pub struct GithubClaimArgs {
159 pub pending: String,
160 pub user: User,
161}
162
163/// A linked GitHub account. The numeric id is what identifies it; the
164/// login is for showing, and is refreshed at each sign-in.
165#[derive(Clone, Debug, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct GithubAccount {
168 pub github_id: u64,
169 pub login: String,
170 /// RFC 3339.
171 pub linked_at: String,
172 /// Whether g1t holds a working user token, needed to list installations.
173 pub authorized: bool,
174}
175
176#[derive(Debug, Default, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct GithubAccountView {
179 /// Whether this g1t has a GitHub App configured for sign-in.
180 pub enabled: bool,
181 pub account: Option<GithubAccount>,
182 /// Whether the account has a password, so GitHub is not its only way in.
183 pub has_password: bool,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct GithubUserTokenArgs {
189 pub user_id: String,
190}
191
192#[derive(Debug, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct GithubRevokedArgs {
195 pub github_id: u64,
196}
197
198/// `github_usernames`: the g1t usernames of linked GitHub accounts. Returns
199/// a map from GitHub id, as a string, to username.
200#[derive(Debug, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct GithubUsernamesArgs {
203 pub github_ids: Vec<u64>,
204}
205
206// --- The app's installations and repositories (integrations) -------------
207
208/// How a GitHub repository comes to g1t.
209#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum GithubMode {
212 /// Copied once. The g1t repository is then its own.
213 Import,
214 /// g1t follows GitHub: every push there is fetched here.
215 Mirror,
216 /// GitHub follows g1t: every push here is pushed there.
217 Push,
218}
219
220impl GithubMode {
221 pub fn as_str(self) -> &'static str {
222 match self {
223 GithubMode::Import => "import",
224 GithubMode::Mirror => "mirror",
225 GithubMode::Push => "push",
226 }
227 }
228
229 pub fn parse(text: &str) -> GithubMode {
230 match text {
231 "mirror" => GithubMode::Mirror,
232 "push" => GithubMode::Push,
233 _ => GithubMode::Import,
234 }
235 }
236}
237
238#[derive(Clone, Debug, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct GithubInstallation {
241 pub id: u64,
242 pub workspace: String,
243 /// The GitHub user or organization it is installed on.
244 pub account: String,
245 /// `User` or `Organization`.
246 pub account_type: String,
247 /// `all` or `selected` repositories.
248 pub repository_selection: String,
249 pub suspended: bool,
250 /// Where to change which repositories it can see.
251 pub settings_url: String,
252 pub created_at: String,
253}
254
255#[derive(Debug, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct GithubStatusArgs {
258 pub viewer: User,
259 pub workspace: String,
260}
261
262#[derive(Debug, Default, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct GithubAppStatus {
265 /// Whether this g1t has a GitHub App configured for repositories.
266 pub configured: bool,
267 /// `https://github.com/apps/<slug>/installations/new`.
268 pub install_url: Option<String>,
269 /// Whether the viewer has linked GitHub with a working user token.
270 pub linked: bool,
271 pub installations: Vec<GithubInstallation>,
272}
273
274#[derive(Debug, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct GithubInstallationArgs {
277 pub actor: User,
278 pub workspace: String,
279 pub installation_id: u64,
280}
281
282#[derive(Debug, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct GithubRepositoriesArgs {
285 pub actor: User,
286 pub workspace: String,
287 pub installation_id: u64,
288 #[serde(default)]
289 pub page: Option<u32>,
290}
291
292#[derive(Clone, Debug, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub struct GithubRepository {
295 pub id: u64,
296 /// `owner/name`.
297 pub full_name: String,
298 pub name: String,
299 pub private: bool,
300 pub description: Option<String>,
301 pub default_branch: String,
302 /// The g1t repository already linked to it, as `workspace/name`.
303 pub linked_to: Option<String>,
304}
305
306#[derive(Debug, Default, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct GithubRepositories {
309 pub repositories: Vec<GithubRepository>,
310 pub total: u32,
311 pub page: u32,
312 pub per_page: u32,
313}
314
315#[derive(Debug, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct GithubImportArgs {
318 pub actor: User,
319 pub workspace: String,
320 pub installation_id: u64,
321 pub github_repo_id: u64,
322 /// The g1t repository's name; the GitHub one's when left out.
323 #[serde(default)]
324 pub name: Option<String>,
325 pub mode: GithubMode,
326 /// Private on g1t; as on GitHub when left out.
327 #[serde(default)]
328 pub private: Option<bool>,
329 /// Also copy issues, with their labels, milestone and state.
330 #[serde(default)]
331 pub issues: bool,
332}
333
334/// A g1t repository's tie to a GitHub repository.
335#[derive(Clone, Debug, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct GithubRepoLink {
338 pub repo_id: String,
339 /// `workspace/name` on g1t.
340 pub repo: String,
341 pub installation_id: u64,
342 pub github_repo_id: u64,
343 /// `owner/name` on GitHub.
344 pub full_name: String,
345 pub mode: GithubMode,
346 /// RFC 3339; the last time refs were copied either way.
347 pub synced_at: Option<String>,
348 pub last_error: Option<String>,
349 /// Issues copied so far, when they were asked for.
350 pub issues_imported: u32,
351}
352
353#[derive(Debug, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct GithubLinkArgs {
356 pub repo_id: String,
357}
358
359#[derive(Debug, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct GithubUnlinkRepoArgs {
362 pub actor: User,
363 pub repo_id: String,
364}
365
366#[derive(Debug, Serialize, Deserialize)]
367pub struct GithubReceiveArgs {
368 /// Header names in lowercase.
369 pub headers: HashMap<String, String>,
370 pub body: String,
371}