Skip to content

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.sh
382 linesCodeBlameRaw
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 the repositories
26//! imported through them, 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_visible_installations` takes `GithubVisibleArgs`, returns
34//! `Outcome<Vec<GithubVisibleInstallation>>`.
35//! - `github_repositories` takes `GithubRepositoriesArgs`, returns
36//! `Outcome<GithubRepositories>`.
37//! - `github_import` takes `GithubImportArgs`, returns `Outcome<GithubRepoLink>`.
38//! - `github_link` takes `GithubLinkArgs`, returns `Option<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#[derive(Clone, Debug, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct GithubInstallation {
211 pub id: u64,
212 pub workspace: String,
213 /// The GitHub user or organization it is installed on.
214 pub account: String,
215 /// `User` or `Organization`.
216 pub account_type: String,
217 /// `all` or `selected` repositories.
218 pub repository_selection: String,
219 pub suspended: bool,
220 /// Where to change which repositories it can see.
221 pub settings_url: String,
222 pub created_at: String,
223}
224
225#[derive(Debug, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct GithubStatusArgs {
228 pub viewer: User,
229 pub workspace: String,
230}
231
232#[derive(Debug, Default, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct GithubAppStatus {
235 /// Whether this g1t has a GitHub App configured for repositories.
236 pub configured: bool,
237 /// `https://github.com/apps/<slug>/installations/new`.
238 pub install_url: Option<String>,
239 /// Whether the viewer has linked GitHub with a working user token.
240 pub linked: bool,
241 pub installations: Vec<GithubInstallation>,
242}
243
244#[derive(Debug, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct GithubInstallationArgs {
247 pub actor: User,
248 pub workspace: String,
249 pub installation_id: u64,
250}
251
252/// `github_visible_installations`: the app's installations the person's
253/// own GitHub account can see, for claiming one that was installed on
254/// GitHub directly rather than from g1t.
255#[derive(Debug, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct GithubVisibleArgs {
258 pub actor: User,
259}
260
261/// An installation of the app, as the person's GitHub account sees it.
262#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct GithubVisibleInstallation {
265 pub id: u64,
266 /// The GitHub user or organization it is installed on.
267 pub account: String,
268 /// `User` or `Organization`.
269 pub account_type: String,
270 /// `all` or `selected` repositories.
271 pub repository_selection: String,
272 pub suspended: bool,
273 pub settings_url: String,
274 /// The person's workspaces it is recorded in already.
275 pub recorded_in: Vec<String>,
276}
277
278#[derive(Debug, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct GithubRepositoriesArgs {
281 pub actor: User,
282 pub workspace: String,
283 pub installation_id: u64,
284 #[serde(default)]
285 pub page: Option<u32>,
286}
287
288#[derive(Clone, Debug, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct GithubRepository {
291 pub id: u64,
292 /// `owner/name`.
293 pub full_name: String,
294 pub name: String,
295 pub private: bool,
296 pub description: Option<String>,
297 pub default_branch: String,
298 /// The g1t repository already imported from it, as `workspace/name`.
299 pub linked_to: Option<String>,
300}
301
302#[derive(Debug, Default, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct GithubRepositories {
305 pub repositories: Vec<GithubRepository>,
306 pub total: u32,
307 pub page: u32,
308 pub per_page: u32,
309}
310
311#[derive(Debug, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct GithubImportArgs {
314 pub actor: User,
315 pub workspace: String,
316 pub installation_id: u64,
317 pub github_repo_id: u64,
318 /// The g1t repository's name; the GitHub one's when left out.
319 #[serde(default)]
320 pub name: Option<String>,
321 /// Private on g1t; as on GitHub when left out.
322 #[serde(default)]
323 pub private: Option<bool>,
324 /// Also copy issues, with their labels, milestone and state.
325 #[serde(default)]
326 pub issues: bool,
327}
328
329/// Where a g1t repository was imported from on GitHub. The import is
330/// copied once: the g1t repository is then its own.
331#[derive(Clone, Debug, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct GithubRepoLink {
334 pub repo_id: String,
335 /// `workspace/name` on g1t.
336 pub repo: String,
337 pub installation_id: u64,
338 pub github_repo_id: u64,
339 /// `owner/name` on GitHub.
340 pub full_name: String,
341 /// RFC 3339; when it was imported.
342 pub synced_at: Option<String>,
343 pub last_error: Option<String>,
344 /// Issues copied so far, when they were asked for.
345 pub issues_imported: u32,
346}
347
348#[derive(Debug, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct GithubLinkArgs {
351 pub repo_id: String,
352}
353
354#[derive(Debug, Serialize, Deserialize)]
355pub struct GithubReceiveArgs {
356 /// Header names in lowercase.
357 pub headers: HashMap<String, String>,
358 pub body: String,
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn a_visible_installation_reads_as_the_site_expects() {
367 let item = GithubVisibleInstallation {
368 id: 7,
369 account: "flagon-io".into(),
370 account_type: "Organization".into(),
371 repository_selection: "all".into(),
372 suspended: false,
373 settings_url: "https://github.com/organizations/flagon-io/settings/installations/7".into(),
374 recorded_in: vec!["flagon-io".into()],
375 };
376 let json = serde_json::to_value(&item).unwrap();
377 assert_eq!(json["accountType"], "Organization");
378 assert_eq!(json["repositorySelection"], "all");
379 assert_eq!(json["recordedIn"][0], "flagon-io");
380 assert_eq!(serde_json::from_value::<GithubVisibleInstallation>(json).unwrap(), item);
381 }
382}