Skip to content
1,154 linesCodeBlameRaw
1//! The OpenAPI document, generated from the same list the routes are.
2//!
3//! The docs site builds its API reference from a copy of this document,
4//! `apps/docs/src/data/openapi.json`. A test keeps the copy current: run
5//! `G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi` to rewrite it.
6
7use g1t_contracts::scopes::scope_for;
8use serde_json::{Map, Value, json};
9
10use crate::about::AboutOp;
11use crate::operations::Op;
12use crate::rules::RulesOp;
13use crate::security::SecurityOp;
14use crate::rest::{ROUTES, Route};
15
16/// The sections of the API reference: a name, what it covers, and its
17/// operations in the order a reader meets them.
18const SECTIONS: &[(&str, &str, &[Op])] = &[
19 (
20 "Accounts",
21 "Signing in from a tool, who a token acts as, and your email addresses.",
22 &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::RemoveEmail, Op::UpdateEmailSettings],
23 ),
24 (
25 "Notifications",
26 "Your inbox: a thread for each thing you were told about (an issue, a pull request, a workflow on a branch, a deployment), why you were told, and what you subscribe to and watch. Your own: personal tokens and sessions only.",
27 &[
28 Op::ListNotifications,
29 Op::MarkNotificationsRead,
30 Op::GetNotificationThread,
31 Op::MarkThreadRead,
32 Op::MarkThreadDone,
33 Op::SaveThread,
34 Op::SnoozeThread,
35 Op::GetThreadSubscription,
36 Op::SetThreadSubscription,
37 Op::DeleteThreadSubscription,
38 Op::GetRepoSubscription,
39 Op::SetRepoSubscription,
40 Op::DeleteRepoSubscription,
41 Op::ListWatchedRepos,
42 ],
43 ),
44 (
45 "Pinned projects",
46 "The projects you keep at the top of a workspace's sidebar, in your order, up to eight a workspace. Your own: personal tokens and sessions only.",
47 &[Op::ListPinnedProjects, Op::PinProject, Op::UnpinProject, Op::ReorderPinnedProjects],
48 ),
49 (
50 "Workspaces",
51 "A workspace owns repositories and is the first part of their address. People and agents work in workspaces.",
52 &[Op::GetWorkspace, Op::CreateWorkspace, Op::UpdateWorkspace, Op::DeleteWorkspace],
53 ),
54 (
55 "Invites",
56 "While g1t is invite-only, every new account needs an invite. Your invites, and inviting people into a workspace by email.",
57 &[
58 Op::ListInvites,
59 Op::CreateInvite,
60 Op::RevokeInvite,
61 Op::ListWorkspaceInvites,
62 Op::InviteMember,
63 Op::RevokeWorkspaceInvite,
64 ],
65 ),
66 (
67 "Billing",
68 "A workspace's usage, its budget, its AI credit, its invoices and its AI Gateway requests. Members read them; owners change the budget and buy credit, as people. g1t's agents never change billing.",
69 &[
70 Op::GetUsage,
71 Op::GetBudget,
72 Op::SetBudget,
73 Op::GetAiCredit,
74 Op::BuyAiCredit,
75 Op::ListInvoices,
76 Op::GetBillingDetails,
77 Op::ListGatewayRequests,
78 ],
79 ),
80 (
81 "Repositories",
82 "A repository, how it handles pull requests, and its timeline: renaming, archiving, moving and deleting it.",
83 &[
84 Op::ListRepos,
85 Op::CreateRepo,
86 Op::GetRepo,
87 Op::UpdateRepo,
88 Op::RenameRepo,
89 Op::RenameBranch,
90 Op::SetRepoVisibility,
91 Op::ArchiveRepo,
92 Op::UnarchiveRepo,
93 Op::TransferRepo,
94 Op::DeleteRepo,
95 Op::ListDeletedRepos,
96 Op::RestoreRepo,
97 Op::PurgeRepo,
98 Op::GetRepoSettings,
99 Op::UpdateRepoSettings,
100 Op::ListCheckNames,
101 Op::GetCodeownersErrors,
102 Op::ListEvents,
103 ],
104 ),
105 (
106 "Repository insights",
107 "What a repository's default branch says about it, read in the background and kept by commit: the languages it is written in, who made it, and its license.",
108 &[Op::About(AboutOp::GetLanguages), Op::About(AboutOp::ListContributors), Op::About(AboutOp::GetLicense)],
109 ),
110 (
111 "Stars",
112 "Starring a repository, to keep it and to say you like it: who starred one, and what you starred.",
113 &[
114 Op::About(AboutOp::ListStargazers),
115 Op::About(AboutOp::ListStarred),
116 Op::About(AboutOp::CheckStarred),
117 Op::About(AboutOp::Star),
118 Op::About(AboutOp::Unstar),
119 ],
120 ),
121 (
122 "Releases",
123 "A release is a tag published with a title and notes. The latest is the newest published one that is neither a draft nor a prerelease.",
124 &[
125 Op::About(AboutOp::ListReleases),
126 Op::About(AboutOp::CreateRelease),
127 Op::About(AboutOp::GetLatestRelease),
128 Op::About(AboutOp::GetReleaseByTag),
129 Op::About(AboutOp::GetRelease),
130 Op::About(AboutOp::UpdateRelease),
131 Op::About(AboutOp::DeleteRelease),
132 ],
133 ),
134 (
135 "Access",
136 "Who can do what in a repository: repository roles, people given a role on one repository (outside collaborators when they are not members), invitations, and a workspace's base permission.",
137 &[
138 Op::ListCollaborators,
139 Op::AddCollaborator,
140 Op::UpdateCollaborator,
141 Op::RemoveCollaborator,
142 Op::GetCollaboratorPermission,
143 Op::ListRepoInvitations,
144 Op::RevokeRepoInvitation,
145 Op::ListMyRepoInvitations,
146 Op::AcceptRepoInvitation,
147 Op::DeclineRepoInvitation,
148 Op::SetBasePermission,
149 Op::ListOutsideCollaborators,
150 ],
151 ),
152 (
153 "Teams",
154 "Groups of a workspace's members: given a role on repositories together, mentioned together as @workspace/team, and asked to review together. Any member may create a team; the workspace's owners and the team's maintainers manage it.",
155 &[
156 Op::ListTeams,
157 Op::CreateTeam,
158 Op::GetTeam,
159 Op::UpdateTeam,
160 Op::DeleteTeam,
161 Op::ListTeamMembers,
162 Op::SetTeamMember,
163 Op::RemoveTeamMember,
164 Op::ListChildTeams,
165 Op::ListTeamRepos,
166 Op::SetTeamRepo,
167 Op::RemoveTeamRepo,
168 Op::SetTeamReviewAssignment,
169 Op::ListUserTeams,
170 ],
171 ),
172 (
173 "Security",
174 "Secrets found in what is pushed and in a repository's history, and dependencies with known vulnerabilities: listing the alerts, and dismissing or reopening them.",
175 &[Op::ListSecurityAlerts, Op::DismissSecurityAlert, Op::ReopenSecurityAlert],
176 ),
177 (
178 "Secret scanning",
179 "Secrets found in pushes and history, where each one is, pushing past push protection with a reason (and asking for approval when the workspace delegates bypasses), checking with a secret's issuer whether it still works, and custom patterns.",
180 &[
181 Op::Security(SecurityOp::ListSecretAlerts),
182 Op::Security(SecurityOp::GetSecretAlert),
183 Op::Security(SecurityOp::UpdateSecretAlert),
184 Op::Security(SecurityOp::ListSecretLocations),
185 Op::Security(SecurityOp::BypassPushProtection),
186 Op::Security(SecurityOp::CheckSecretValidity),
187 Op::Security(SecurityOp::ListBypassRequests),
188 Op::Security(SecurityOp::ReviewBypassRequest),
189 Op::Security(SecurityOp::ListCustomPatterns),
190 Op::Security(SecurityOp::CreateCustomPattern),
191 Op::Security(SecurityOp::UpdateCustomPattern),
192 Op::Security(SecurityOp::DeleteCustomPattern),
193 Op::Security(SecurityOp::DryRunCustomPattern),
194 ],
195 ),
196 (
197 "Code scanning",
198 "Results of static analysis tools, uploaded as SARIF: alerts on the default branch, the analyses that made them, uploads, and putting g1t on an alert to fix it.",
199 &[
200 Op::Security(SecurityOp::ListCodeAlerts),
201 Op::Security(SecurityOp::GetCodeAlert),
202 Op::Security(SecurityOp::UpdateCodeAlert),
203 Op::Security(SecurityOp::ListAnalyses),
204 Op::Security(SecurityOp::UploadSarif),
205 Op::Security(SecurityOp::GetSarifUpload),
206 Op::Security(SecurityOp::FixAlert),
207 ],
208 ),
209 (
210 "Supply chain",
211 "What a repository depends on: vulnerability alerts, the dependency graph, an SPDX SBOM of it, and comparing two commits' dependencies as dependency review does.",
212 &[
213 Op::Security(SecurityOp::ListVulnerabilityAlerts),
214 Op::Security(SecurityOp::GetVulnerabilityAlert),
215 Op::Security(SecurityOp::UpdateVulnerabilityAlert),
216 Op::Security(SecurityOp::GetDependencyGraph),
217 Op::Security(SecurityOp::GetSbom),
218 Op::Security(SecurityOp::CompareDependencies),
219 ],
220 ),
221 (
222 "Security settings",
223 "When pull request checks fail, dependency review's policy, delegated bypass and validity checks, and a workspace's security overview.",
224 &[
225 Op::Security(SecurityOp::GetSettings),
226 Op::Security(SecurityOp::UpdateSettings),
227 Op::Security(SecurityOp::GetWorkspaceSettings),
228 Op::Security(SecurityOp::UpdateWorkspaceSettings),
229 Op::Security(SecurityOp::GetOverview),
230 ],
231 ),
232 (
233 "Rules",
234 "Rulesets: what may happen to a repository's branches and tags and what a pull request needs before it merges, for a repository or across a workspace; the rules that hold for one branch; and how they judged each push and merge, with insights.",
235 &[
236 Op::Rules(RulesOp::ListRepoRulesets),
237 Op::Rules(RulesOp::CreateRepoRuleset),
238 Op::Rules(RulesOp::GetRepoRuleset),
239 Op::Rules(RulesOp::UpdateRepoRuleset),
240 Op::Rules(RulesOp::DeleteRepoRuleset),
241 Op::Rules(RulesOp::GetBranchRules),
242 Op::Rules(RulesOp::ListRuleEvaluations),
243 Op::Rules(RulesOp::ListWorkspaceRulesets),
244 Op::Rules(RulesOp::CreateWorkspaceRuleset),
245 Op::Rules(RulesOp::GetWorkspaceRuleset),
246 Op::Rules(RulesOp::UpdateWorkspaceRuleset),
247 Op::Rules(RulesOp::DeleteWorkspaceRuleset),
248 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
249 ],
250 ),
251 (
252 "Issues",
253 "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
254 &[
255 Op::ListIssues,
256 Op::CreateIssue,
257 Op::GetIssue,
258 Op::UpdateIssue,
259 Op::CloseIssue,
260 Op::ReopenIssue,
261 Op::AssignIssue,
262 Op::Delegate,
263 Op::AddComment,
264 Op::ListIssueLabels,
265 Op::AddIssueLabels,
266 Op::SetIssueLabels,
267 Op::RemoveIssueLabels,
268 ],
269 ),
270 (
271 "Labels and milestones",
272 "A repository's labels, which issues and pull requests carry by name, and its milestones, which gather them under a goal and a due date.",
273 &[
274 Op::ListLabels,
275 Op::CreateLabel,
276 Op::UpdateLabel,
277 Op::DeleteLabel,
278 Op::AddDefaultLabels,
279 Op::ListMilestones,
280 Op::CreateMilestone,
281 Op::GetMilestone,
282 Op::UpdateMilestone,
283 Op::DeleteMilestone,
284 ],
285 ),
286 (
287 "Plans",
288 "An outcome turned into the issues that would get there, with the order they must merge in.",
289 &[Op::PlanWork, Op::GetPlan, Op::ApplyPlan],
290 ),
291 (
292 "Pull requests",
293 "A proposed change in its own fork or on a branch. Several can be made for one issue; the one merged resolves it.",
294 &[
295 Op::ListPullRequests,
296 Op::CreatePullRequest,
297 Op::GetPullRequest,
298 Op::UpdatePullRequest,
299 Op::GetPullRequestChanges,
300 Op::MarkPullRequestReady,
301 Op::RequestReviewers,
302 Op::RemoveRequestedReviewers,
303 Op::ReviewPullRequest,
304 Op::MergePullRequest,
305 Op::ClosePullRequest,
306 Op::GetMergeQueue,
307 Op::MessageAgent,
308 Op::AnswerMessage,
309 Op::TakeMessages,
310 ],
311 ),
312 (
313 "Sessions",
314 "The record of how a pull request was made: prompts, reasoning and the tools that ran.",
315 &[Op::ReadSession, Op::RecordSession],
316 ),
317 (
318 "Memory",
319 "What agents and people learned that the next agent should know, for one project or across a workspace. Members and g1t's agents only; never a secret.",
320 &[Op::Remember, Op::Recall],
321 ),
322 (
323 "Search",
324 "One search across all of g1t: repositories, code, issues, pull requests, people and workspaces. Public content for everyone, and private content in workspaces you belong to.",
325 &[Op::Search],
326 ),
327 (
328 "Context",
329 "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
330 &[Op::SearchContext, Op::GetEntity],
331 ),
332 (
333 "Actions",
334 "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.",
335 &[
336 Op::ListWorkflows,
337 Op::ListWorkflowRuns,
338 Op::GetWorkflowRun,
339 Op::GetJobLogs,
340 Op::DispatchWorkflow,
341 Op::CancelWorkflowRun,
342 Op::RerunWorkflowRun,
343 Op::UpdateWorkflow,
344 ],
345 ),
346 (
347 "Secrets and variables",
348 "Values that workflows and deployments read, per repository or for a whole workspace, with a row per environment.",
349 &[
350 Op::ListActionsSecrets,
351 Op::SetActionsSecret,
352 Op::DeleteActionsSecret,
353 Op::ListActionsVariables,
354 Op::SetActionsVariable,
355 Op::DeleteActionsVariable,
356 ],
357 ),
358 (
359 "Runners",
360 "Self-hosted runners: your own machines, which run your workflow jobs (and, if you choose, your agents' work) for $0 of g1t compute. They register with a short-lived token and only ever connect out.",
361 &[
362 Op::ListRunners,
363 Op::CreateRunnerRegistrationToken,
364 Op::RemoveRunner,
365 Op::ListRunnerGroups,
366 Op::CreateRunnerGroup,
367 Op::UpdateRunnerGroup,
368 Op::DeleteRunnerGroup,
369 Op::GetRunnerSettings,
370 Op::UpdateRunnerSettings,
371 ],
372 ),
373 (
374 "Webhooks",
375 "Signed HTTPS requests sent to your own address as things happen, for a repository or a whole workspace.",
376 &[
377 Op::ListWebhooks,
378 Op::CreateWebhook,
379 Op::UpdateWebhook,
380 Op::DeleteWebhook,
381 Op::PingWebhook,
382 Op::ListWebhookDeliveries,
383 Op::RedeliverWebhook,
384 ],
385 ),
386 (
387 "Integrations",
388 "A workspace's connections to outside systems: model providers, alert sources and issue trackers.",
389 &[
390 Op::ListIntegrations,
391 Op::ConnectIntegration,
392 Op::DisconnectIntegration,
393 Op::TestIntegration,
394 Op::GetModelRoutes,
395 Op::SetModelRoutes,
396 Op::GetContext,
397 Op::ImportIssue,
398 ],
399 ),
400];
401
402/// The section of the API reference an operation is listed under.
403fn tag(op: Op) -> &'static str {
404 SECTIONS
405 .iter()
406 .find(|(_, _, ops)| ops.contains(&op))
407 .map_or("Repositories", |(name, _, _)| name)
408}
409
410/// What an operation's page is called, as a short sentence.
411fn title(op: Op) -> &'static str {
412 match op {
413 Op::Whoami => "Get the current user",
414 Op::GetWorkspace => "Get a workspace",
415 Op::CreateWorkspace => "Create a workspace",
416 Op::DeleteWorkspace => "Delete a workspace",
417 Op::UpdateWorkspace => "Update a workspace",
418 Op::ListEmails => "List your email addresses",
419 Op::AddEmail => "Add an email address",
420 Op::RemoveEmail => "Remove an email address",
421 Op::UpdateEmailSettings => "Change your email settings",
422 Op::ListInvites => "List your invites",
423 Op::CreateInvite => "Create an invite",
424 Op::RevokeInvite => "Revoke an invite",
425 Op::ListWorkspaceInvites => "List a workspace's invites",
426 Op::InviteMember => "Invite someone to a workspace",
427 Op::RevokeWorkspaceInvite => "Revoke a workspace's invite",
428 Op::TransferRepo => "Transfer a repository",
429 Op::RenameRepo => "Rename a repository",
430 Op::RenameBranch => "Rename a branch",
431 Op::ArchiveRepo => "Archive a repository",
432 Op::UnarchiveRepo => "Unarchive a repository",
433 Op::SetRepoVisibility => "Change a repository's visibility",
434 Op::DeleteRepo => "Delete a repository",
435 Op::ListDeletedRepos => "List recently deleted repositories",
436 Op::RestoreRepo => "Restore a deleted repository",
437 Op::PurgeRepo => "Purge a deleted repository",
438 Op::ListRepos => "List repositories",
439 Op::GetRepo => "Get a repository",
440 Op::CreateRepo => "Create a repository",
441 Op::UpdateRepo => "Update a repository",
442 Op::GetRepoSettings => "Get repository settings",
443 Op::UpdateRepoSettings => "Update repository settings",
444 Op::ListCheckNames => "List check names",
445 Op::GetMergeQueue => "Get the merge queue",
446 Op::MessageAgent => "Message an agent",
447 Op::AnswerMessage => "Answer a message",
448 Op::TakeMessages => "Take new messages",
449 Op::Remember => "Remember something",
450 Op::Recall => "Recall memory",
451 Op::SearchContext => "Search the context hub",
452 Op::GetEntity => "Get a catalog entry",
453 Op::Search => "Search g1t",
454 Op::ListIssues => "List issues",
455 Op::GetIssue => "Get an issue",
456 Op::CreateIssue => "Create an issue",
457 Op::UpdateIssue => "Update an issue",
458 Op::CloseIssue => "Close an issue",
459 Op::ReopenIssue => "Reopen an issue",
460 Op::AssignIssue => "Assign an issue to g1t",
461 Op::Delegate => "Put an agent on it",
462 Op::PlanWork => "Plan work",
463 Op::GetPlan => "Get a plan",
464 Op::ApplyPlan => "Apply a plan",
465 Op::ListLabels => "List labels",
466 Op::CreateLabel => "Create a label",
467 Op::UpdateLabel => "Update a label",
468 Op::DeleteLabel => "Delete a label",
469 Op::AddDefaultLabels => "Add the default labels",
470 Op::ListIssueLabels => "List an issue's labels",
471 Op::AddIssueLabels => "Add labels to an issue",
472 Op::SetIssueLabels => "Set an issue's labels",
473 Op::RemoveIssueLabels => "Remove labels from an issue",
474 Op::ListMilestones => "List milestones",
475 Op::GetMilestone => "Get a milestone",
476 Op::CreateMilestone => "Create a milestone",
477 Op::UpdateMilestone => "Update a milestone",
478 Op::DeleteMilestone => "Delete a milestone",
479 Op::UpdatePullRequest => "Update a pull request",
480 Op::AddComment => "Add a comment",
481 Op::ReviewPullRequest => "Review a pull request",
482 Op::ListPullRequests => "List pull requests",
483 Op::GetPullRequest => "Get a pull request",
484 Op::CreatePullRequest => "Create a pull request",
485 Op::RecordSession => "Record session entries",
486 Op::ReadSession => "Read a session",
487 Op::MarkPullRequestReady => "Mark a pull request ready",
488 Op::ClosePullRequest => "Close a pull request",
489 Op::GetPullRequestChanges => "Get a pull request's changes",
490 Op::MergePullRequest => "Merge a pull request",
491 Op::ListEvents => "List repository events",
492 Op::ListIntegrations => "List integrations",
493 Op::ConnectIntegration => "Connect an integration",
494 Op::DisconnectIntegration => "Disconnect an integration",
495 Op::TestIntegration => "Test an integration",
496 Op::GetContext => "Look up a ticket",
497 Op::ImportIssue => "Import an issue",
498 Op::GetModelRoutes => "Get model routes",
499 Op::SetModelRoutes => "Set model routes",
500 Op::ListWebhooks => "List webhooks",
501 Op::CreateWebhook => "Create a webhook",
502 Op::UpdateWebhook => "Update a webhook",
503 Op::DeleteWebhook => "Delete a webhook",
504 Op::PingWebhook => "Ping a webhook",
505 Op::ListWebhookDeliveries => "List webhook deliveries",
506 Op::RedeliverWebhook => "Redeliver a webhook delivery",
507 Op::ListWorkflows => "List workflows",
508 Op::ListWorkflowRuns => "List workflow runs",
509 Op::GetWorkflowRun => "Get a workflow run",
510 Op::GetJobLogs => "Get a job's log",
511 Op::DispatchWorkflow => "Run a workflow",
512 Op::CancelWorkflowRun => "Cancel a workflow run",
513 Op::RerunWorkflowRun => "Re-run a workflow run",
514 Op::UpdateWorkflow => "Turn a workflow on or off",
515 Op::ListActionsSecrets => "List secrets",
516 Op::SetActionsSecret => "Set a secret",
517 Op::DeleteActionsSecret => "Delete a secret",
518 Op::ListActionsVariables => "List variables",
519 Op::SetActionsVariable => "Set a variable",
520 Op::DeleteActionsVariable => "Delete a variable",
521 Op::ListRunners => "List self-hosted runners",
522 Op::ListRunnerGroups => "List runner groups",
523 Op::GetRunnerSettings => "Get runner settings",
524 Op::CreateRunnerRegistrationToken => "Create a runner registration token",
525 Op::RemoveRunner => "Remove a self-hosted runner",
526 Op::CreateRunnerGroup => "Create a runner group",
527 Op::UpdateRunnerGroup => "Change a runner group",
528 Op::DeleteRunnerGroup => "Delete a runner group",
529 Op::UpdateRunnerSettings => "Change runner settings",
530 Op::ListCollaborators => "List who has access",
531 Op::AddCollaborator => "Add a collaborator",
532 Op::UpdateCollaborator => "Change a collaborator's role",
533 Op::RemoveCollaborator => "Remove a collaborator",
534 Op::GetCollaboratorPermission => "Get someone's permission",
535 Op::ListRepoInvitations => "List a repository's invitations",
536 Op::RevokeRepoInvitation => "Revoke a repository invitation",
537 Op::ListMyRepoInvitations => "List your repository invitations",
538 Op::AcceptRepoInvitation => "Accept a repository invitation",
539 Op::DeclineRepoInvitation => "Decline a repository invitation",
540 Op::SetBasePermission => "Set the base permission",
541 Op::ListOutsideCollaborators => "List outside collaborators",
542 Op::ListSecurityAlerts => "List security alerts",
543 Op::DismissSecurityAlert => "Dismiss a security alert",
544 Op::ReopenSecurityAlert => "Reopen a security alert",
545 Op::ListNotifications => "List notifications",
546 Op::MarkNotificationsRead => "Mark notifications read",
547 Op::GetNotificationThread => "Get a thread",
548 Op::MarkThreadRead => "Mark a thread read",
549 Op::MarkThreadDone => "Mark a thread done",
550 Op::SaveThread => "Save a thread",
551 Op::SnoozeThread => "Snooze a thread",
552 Op::GetThreadSubscription => "Get a thread subscription",
553 Op::SetThreadSubscription => "Set a thread subscription",
554 Op::DeleteThreadSubscription => "Unsubscribe from a thread",
555 Op::GetRepoSubscription => "Get how you watch a repository",
556 Op::SetRepoSubscription => "Watch a repository",
557 Op::DeleteRepoSubscription => "Stop watching a repository",
558 Op::ListWatchedRepos => "List repositories you watch",
559 Op::ListPinnedProjects => "List your pinned projects",
560 Op::GetUsage => "Get a workspace's usage",
561 Op::GetBudget => "Get a workspace's budget",
562 Op::SetBudget => "Change a workspace's budget",
563 Op::GetAiCredit => "Get a workspace's AI credit",
564 Op::BuyAiCredit => "Buy AI credit",
565 Op::ListInvoices => "List a workspace's invoices",
566 Op::GetBillingDetails => "Get a workspace's billing details",
567 Op::ListGatewayRequests => "List a workspace's AI Gateway requests",
568 Op::PinProject => "Pin a project",
569 Op::UnpinProject => "Unpin a project",
570 Op::ReorderPinnedProjects => "Reorder your pinned projects",
571 Op::ListTeams => "List teams",
572 Op::GetTeam => "Get a team",
573 Op::CreateTeam => "Create a team",
574 Op::UpdateTeam => "Update a team",
575 Op::DeleteTeam => "Delete a team",
576 Op::ListTeamMembers => "List a team's members",
577 Op::SetTeamMember => "Add or change a team member",
578 Op::RemoveTeamMember => "Remove a team member",
579 Op::ListChildTeams => "List child teams",
580 Op::ListTeamRepos => "List a team's repositories",
581 Op::SetTeamRepo => "Give a team a role on a repository",
582 Op::RemoveTeamRepo => "Remove a team from a repository",
583 Op::SetTeamReviewAssignment => "Set a team's review assignment",
584 Op::ListUserTeams => "List someone's teams",
585 Op::RequestReviewers => "Request reviewers",
586 Op::RemoveRequestedReviewers => "Remove requested reviewers",
587 Op::GetCodeownersErrors => "List CODEOWNERS errors",
588 Op::Security(op) => op.title(),
589 Op::Rules(op) => op.title(),
590 Op::About(op) => op.title(),
591 }
592}
593
594/// Why an operation can be refused with `402 payment_required`, if it
595/// can: the ones that start an agent, when the workspace has no credit,
596/// and the ones that make a repository private in a workspace, when a free
597/// workspace's private storage has no room for it.
598fn may_need_payment(op: Op) -> Option<&'static str> {
599 match op {
600 Op::AssignIssue | Op::PlanWork | Op::ApplyPlan => Some("The workspace has no agent credit."),
601 Op::UpdateRepo | Op::SetRepoVisibility | Op::TransferRepo => Some(
602 "A free workspace's private storage has no room for this private repository.",
603 ),
604 _ => None,
605 }
606}
607
608/// What the reference says beyond each operation's own description, keyed
609/// by operation id, written by hand from what the services return: `notes`
610/// (Markdown, added to the description) and example `params` (path),
611/// `query`, `request` (body) and `response`.
612const REFERENCE: &str = include_str!("reference.json");
613
614fn examples() -> Map<String, Value> {
615 match serde_json::from_str(REFERENCE) {
616 Ok(Value::Object(examples)) => examples,
617 _ => Map::new(),
618 }
619}
620
621/// `/repos/:owner/:name` as OpenAPI writes it: `/repos/{owner}/{name}`.
622fn openapi_path(route: &Route) -> String {
623 route
624 .path
625 .split('/')
626 .map(|segment| match segment.strip_prefix(':') {
627 Some(name) => format!("{{{name}}}"),
628 None => segment.to_owned(),
629 })
630 .collect::<Vec<_>>()
631 .join("/")
632}
633
634fn error_response(description: &str) -> Value {
635 json!({
636 "description": description,
637 "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } },
638 })
639}
640
641/// A parameter in the path or the query, described by the operation's
642/// input schema where it has the same name.
643fn parameter(name: &str, place: &str, required: bool, schema: Option<&Value>) -> Value {
644 let mut schema = schema.cloned().unwrap_or_else(|| json!({ "type": "string" }));
645 let description = match name {
646 "owner" => Some(Value::from("The workspace that owns the repository.")),
647 "name" => Some(Value::from("The repository's name.")),
648 _ => schema.as_object_mut().and_then(|schema| schema.remove("description")),
649 };
650 let mut parameter = json!({
651 "name": name,
652 "in": place,
653 "required": required,
654 "schema": schema,
655 });
656 if let Some(description) = description {
657 parameter["description"] = description;
658 }
659 parameter
660}
661
662/// The operation id of a route. An operation reached at a workspace's
663/// address as well as a repository's is documented once for each, with its
664/// own id; GitHub's alternative addresses for one operation keep GitHub's
665/// names.
666fn operation_id(route: &Route) -> String {
667 let op = route.op;
668 let base = match (route.method, route.path.rsplit('/').next().unwrap_or_default()) {
669 ("PUT", "enable") => "enable_workflow".to_owned(),
670 ("PUT", "disable") => "disable_workflow".to_owned(),
671 ("POST", "rerun-failed-jobs") => "rerun_failed_jobs".to_owned(),
672 ("PATCH", ":setting") => "update_actions_variable".to_owned(),
673 ("GET", "runs") if route.path.contains("/workflows/:workflow/") => "list_runs_of_workflow".to_owned(),
674 // One repository's notifications, and an issue's subscription by
675 // its number rather than a thread's id.
676 (_, "notifications") if route.path.starts_with("/repos/") => match op {
677 Op::ListNotifications => "list_repo_notifications".to_owned(),
678 _ => "mark_repo_notifications_read".to_owned(),
679 },
680 (method, "subscription") if route.path.contains("/issues/:number/") => match method {
681 "GET" => "get_issue_subscription".to_owned(),
682 "PUT" => "set_issue_subscription".to_owned(),
683 _ => "delete_issue_subscription".to_owned(),
684 },
685 // One label off an issue, by its name in the path.
686 ("DELETE", ":label") if route.path.contains("/issues/:number/") => "remove_issue_label".to_owned(),
687 ("DELETE", "saved") => "unsave_thread".to_owned(),
688 ("DELETE", "snooze") => "unsnooze_thread".to_owned(),
689 _ => op.name().to_owned(),
690 };
691 if route.path.starts_with("/workspaces/") && ROUTES.iter().any(|other| other.op == op && other.path.starts_with("/repos/")) {
692 format!("{base}_for_workspace")
693 } else {
694 base
695 }
696}
697
698/// The summary of a route: its operation's title, or for one of GitHub's
699/// alternative addresses, what that address does.
700fn summary(route: &Route, id: &str) -> String {
701 let base = match id.trim_end_matches("_for_workspace") {
702 "enable_workflow" => "Turn a workflow on",
703 "disable_workflow" => "Turn a workflow off",
704 "rerun_failed_jobs" => "Re-run failed jobs",
705 "update_actions_variable" => "Update a variable",
706 "list_runs_of_workflow" => "List a workflow's runs",
707 "list_repo_notifications" => "List a repository's notifications",
708 "mark_repo_notifications_read" => "Mark a repository's notifications read",
709 "get_issue_subscription" => "Get your subscription to an issue",
710 "set_issue_subscription" => "Subscribe to an issue",
711 "delete_issue_subscription" => "Unsubscribe from an issue",
712 "unsave_thread" => "Unsave a thread",
713 "unsnooze_thread" => "Bring a snoozed thread back",
714 _ => title(route.op),
715 };
716 if id.ends_with("_for_workspace") {
717 format!("{base} for a workspace")
718 } else {
719 base.to_owned()
720 }
721}
722
723fn operation(route: &Route) -> Value {
724 let op = route.op;
725 let path_params: Vec<&str> = route.params().collect();
726 // `owner` and `name` in the path stand for the operation's `repo` input.
727 let covered = |name: &str| name == "repo" || path_params.contains(&name);
728 let all_properties = op.properties();
729 let mut properties = all_properties.clone();
730 properties.retain(|name, _| !covered(name));
731 let required: Vec<String> = op
732 .required()
733 .into_iter()
734 .filter(|name| !covered(name))
735 .collect();
736
737 let mut parameters: Vec<Value> = path_params
738 .iter()
739 .map(|name| parameter(name, "path", true, all_properties.get(*name)))
740 .collect();
741 let mut body = Value::Null;
742 if route.method == "GET" {
743 for (name, key) in route.query {
744 parameters.push(parameter(
745 name,
746 "query",
747 required.iter().any(|required| required == key),
748 properties.get(*key),
749 ));
750 }
751 } else if !properties.is_empty() {
752 let mut schema = json!({ "type": "object", "properties": properties });
753 if !required.is_empty() {
754 schema["required"] = json!(required);
755 }
756 body = json!({
757 "required": !required.is_empty(),
758 "content": { "application/json": { "schema": schema } },
759 });
760 }
761
762 let id = operation_id(route);
763 let mut responses = Map::new();
764 responses.insert(
765 "200".into(),
766 json!({
767 "description": "Success.",
768 "content": { "application/json": { "schema": {} } },
769 }),
770 );
771 responses.insert(
772 "401".into(),
773 error_response("A token is required, or the one sent is not valid."),
774 );
775 if let Some(reason) = may_need_payment(op) {
776 responses.insert("402".into(), error_response(reason));
777 }
778 responses.insert(
779 "403".into(),
780 error_response("Signed in, but not allowed to do this: the role you have is not enough, or the token lacks the scope it needs, which `needed_scope` names."),
781 );
782 if !matches!(op, Op::Whoami | Op::ListRepos | Op::Search) {
783 responses.insert("404".into(), error_response("It does not exist, or you cannot see it."));
784 }
785 if route.method != "GET" {
786 responses.insert(
787 "409".into(),
788 error_response("The request conflicts with the current state."),
789 );
790 }
791 if op != Op::Whoami {
792 responses.insert("422".into(), error_response("The input is not valid."));
793 }
794 // Public data can be read without a token; everything else needs one.
795 let scope: Vec<&str> = scope_for(op.name()).map(|scope| scope.as_str()).into_iter().collect();
796 let security = if op.needs_user() {
797 json!([{ "token": scope }])
798 } else {
799 json!([{ "token": scope }, {}])
800 };
801 let (tool, action) = crate::tools::TOOLS
802 .iter()
803 .find_map(|tool| {
804 tool.actions
805 .iter()
806 .find(|action| action.op == op)
807 .map(|action| (tool.name, action.name))
808 })
809 .unwrap_or_default();
810 let mut described = json!({
811 "operationId": id,
812 "tags": [tag(op)],
813 "summary": summary(route, &id),
814 "description": op.description(),
815 "x-operation": op.name(),
816 "x-mcp-tool": tool,
817 "x-mcp-action": action,
818 "x-scope": scope.first().copied(),
819 "security": security,
820 "parameters": parameters,
821 "responses": responses,
822 });
823 if !body.is_null() {
824 described["requestBody"] = body;
825 }
826 described
827}
828
829/// Entries for device sign-in, which is not an operation.
830fn onboarding() -> Map<String, Value> {
831 let paths = json!({
832 "/device/code": {
833 "post": {
834 "operationId": "device_code",
835 "tags": ["Accounts"],
836 "summary": "Start signing in",
837 "description": "Begins a device sign-in. Show the person `verification_uri_complete` and have them open it in a browser, where they sign in or register and approve the code. Then poll `/device/token`.",
838 "security": [],
839 "requestBody": {
840 "content": { "application/json": { "schema": {
841 "type": "object",
842 "properties": {
843 "client_name": {
844 "type": "string",
845 "description": "What is asking, shown to the person approving. For example, Claude Code.",
846 },
847 },
848 } } },
849 },
850 "responses": { "200": {
851 "description": "The codes for this sign-in.",
852 "content": { "application/json": { "schema": {
853 "type": "object",
854 "properties": {
855 "device_code": { "type": "string", "description": "Secret. Send it to /device/token." },
856 "user_code": { "type": "string", "description": "Shown to the person, like WDJB-MJHT." },
857 "verification_uri": { "type": "string" },
858 "verification_uri_complete": {
859 "type": "string",
860 "description": "The link to give the person; it carries the code.",
861 },
862 "expires_in": { "type": "integer", "description": "Seconds until the codes expire." },
863 "interval": { "type": "integer", "description": "Seconds to wait between polls." },
864 },
865 } } },
866 } },
867 },
868 },
869 "/device/token": {
870 "post": {
871 "operationId": "device_token",
872 "tags": ["Accounts"],
873 "summary": "Finish signing in",
874 "description": "Asks whether the person has approved. Poll no faster than the interval. The token is returned once.",
875 "security": [],
876 "requestBody": {
877 "required": true,
878 "content": { "application/json": { "schema": {
879 "type": "object",
880 "required": ["device_code"],
881 "properties": { "device_code": { "type": "string" } },
882 } } },
883 },
884 "responses": { "200": {
885 "description": "The state of the sign-in.",
886 "content": { "application/json": { "schema": {
887 "type": "object",
888 "required": ["status"],
889 "properties": {
890 "status": { "type": "string", "enum": ["pending", "approved", "denied", "expired"] },
891 "token": { "type": "string", "description": "Present when approved." },
892 "username": { "type": "string" },
893 "verified": {
894 "type": "boolean",
895 "description": "Whether the account's email is confirmed.",
896 },
897 },
898 } } },
899 } },
900 },
901 },
902 });
903 match paths {
904 Value::Object(paths) => paths,
905 _ => Map::new(),
906 }
907}
908
909
910/// Puts each operation's examples, where it has them, into its request
911/// and response. Path and query values go under `x-example-params` and
912/// `x-example-query`, which tools that build a request can use.
913fn attach_examples(paths: &mut Map<String, Value>) {
914 let examples = examples();
915 for methods in paths.values_mut() {
916 let Some(methods) = methods.as_object_mut() else { continue };
917 for operation in methods.values_mut() {
918 let id = operation["operationId"].as_str().unwrap_or_default().to_owned();
919 let name = operation["x-operation"].as_str().unwrap_or_default().to_owned();
920 let Some(example) = examples.get(&id).or_else(|| examples.get(&name)) else {
921 continue;
922 };
923 if let Some(notes) = example.get("notes").and_then(Value::as_str) {
924 let description = operation["description"].as_str().unwrap_or_default();
925 operation["description"] = json!(format!("{description}\n\n{notes}"));
926 }
927 if let Some(response) = example.get("response") {
928 let content = &mut operation["responses"]["200"]["content"]["application/json"];
929 if content.is_object() {
930 content["example"] = response.clone();
931 }
932 }
933 if let Some(request) = example.get("request") {
934 let content = &mut operation["requestBody"]["content"]["application/json"];
935 if content.is_object() {
936 content["example"] = request.clone();
937 }
938 }
939 for (key, extension) in [("params", "x-example-params"), ("query", "x-example-query")] {
940 if let Some(values) = example.get(key) {
941 operation[extension] = values.clone();
942 }
943 }
944 }
945 }
946}
947
948pub fn document() -> Value {
949 let mut paths = onboarding();
950 for route in ROUTES {
951 let entry = paths
952 .entry(openapi_path(route))
953 .or_insert_with(|| json!({}));
954 entry[route.method.to_lowercase()] = operation(route);
955 }
956 attach_examples(&mut paths);
957 let tags: Vec<Value> = SECTIONS
958 .iter()
959 .map(|(name, description, ops)| {
960 json!({
961 "name": name,
962 "description": description,
963 // The section's operations in reading order, by MCP tool name.
964 "x-tools": ops.iter().map(|op| op.name()).collect::<Vec<_>>(),
965 })
966 })
967 .collect();
968 let codes = ["unauthenticated", "payment_required", "forbidden", "not_found", "conflict", "invalid"];
969 json!({
970 "openapi": "3.1.0",
971 "info": {
972 "title": "g1t API",
973 "version": "1",
974 "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh. Every name in a request or response body is `snake_case`; names you chose, such as a workflow's inputs or a secret's name, are returned as you wrote them.",
975 "license": { "name": "MIT", "identifier": "MIT" },
976 },
977 "servers": [{ "url": "https://api.g1t.sh" }],
978 "security": [{ "token": [] }, {}],
979 "tags": tags,
980 "paths": paths,
981 "components": {
982 "securitySchemes": {
983 "token": {
984 "type": "http",
985 "scheme": "bearer",
986 "description": "An access token, `g1t_…`. Public data needs none. Each operation names the scope a token needs for it; see https://docs.g1t.sh/guides/authentication/#scopes.",
987 },
988 },
989 "schemas": {
990 "Error": {
991 "type": "object",
992 "required": ["error"],
993 "properties": {
994 "error": {
995 "type": "object",
996 "required": ["code", "message"],
997 "properties": {
998 "code": { "type": "string", "enum": codes },
999 "message": { "type": "string" },
1000 "needed_scope": {
1001 "type": "string",
1002 "description": "On a 403 for an access token without the scope the call needs: that scope, such as `issues:write`.",
1003 },
1004 },
1005 },
1006 },
1007 },
1008 },
1009 },
1010 })
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 #[test]
1018 fn every_route_is_documented_once() {
1019 let document = document();
1020 let mut ids = Vec::new();
1021 for (_, methods) in document["paths"].as_object().unwrap() {
1022 for (_, operation) in methods.as_object().unwrap() {
1023 ids.push(operation["operationId"].as_str().unwrap().to_owned());
1024 }
1025 }
1026 for op in Op::ALL {
1027 assert_eq!(
1028 ids.iter().filter(|id| *id == op.name()).count(),
1029 1,
1030 "{}",
1031 op.name()
1032 );
1033 }
1034 let mut unique = ids.clone();
1035 unique.sort();
1036 unique.dedup();
1037 assert_eq!(unique.len(), ids.len(), "operation ids repeat");
1038 }
1039
1040 #[test]
1041 fn path_and_query_inputs_are_not_repeated_in_the_body() {
1042 let document = document();
1043 let merge = &document["paths"]["/repos/{owner}/{name}/pulls/{number}/merge"]["post"];
1044 let body = &merge["requestBody"]["content"]["application/json"]["schema"]["properties"];
1045 assert!(body.get("keep_issue_open").is_some());
1046 assert!(body.get("repo").is_none() && body.get("number").is_none());
1047 let list = &document["paths"]["/repos"]["get"];
1048 assert_eq!(list["parameters"][0]["name"], "q");
1049 assert!(list.get("requestBody").is_none());
1050 }
1051
1052 #[test]
1053 fn every_operation_is_in_one_section() {
1054 for op in Op::ALL {
1055 let sections = SECTIONS
1056 .iter()
1057 .filter(|(_, _, ops)| ops.contains(&op))
1058 .count();
1059 assert_eq!(sections, 1, "{}", op.name());
1060 }
1061 }
1062
1063 #[test]
1064 fn titles_read_as_sentences() {
1065 assert_eq!(title(Op::CreateIssue), "Create an issue");
1066 assert_eq!(title(Op::Whoami), "Get the current user");
1067 }
1068
1069 #[test]
1070 fn every_operation_has_an_example_response() {
1071 let examples = examples();
1072 assert!(!examples.is_empty(), "reference.json does not parse");
1073 let document = document();
1074 let mut known = Vec::new();
1075 for (path, methods) in document["paths"].as_object().unwrap() {
1076 for (method, operation) in methods.as_object().unwrap() {
1077 known.push(operation["operationId"].as_str().unwrap().to_owned());
1078 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
1079 assert!(!example.is_null(), "{method} {path} has no example response");
1080 }
1081 }
1082 for id in examples.keys() {
1083 assert!(known.contains(id), "reference.json names {id}, which is not an operation");
1084 }
1085 }
1086
1087 #[test]
1088 fn example_requests_send_only_what_the_body_takes() {
1089 let document = document();
1090 for (path, methods) in document["paths"].as_object().unwrap() {
1091 for (method, operation) in methods.as_object().unwrap() {
1092 let content = &operation["requestBody"]["content"]["application/json"];
1093 let Some(example) = content["example"].as_object() else { continue };
1094 let properties = &content["schema"]["properties"];
1095 for key in example.keys() {
1096 assert!(!properties[key].is_null(), "{method} {path}: {key} is not in the body");
1097 }
1098 }
1099 }
1100 }
1101
1102 /// The docs site's copy of the document. Run with `G1T_WRITE_OPENAPI=1`
1103 /// to rewrite it after changing an operation.
1104 #[test]
1105 fn the_docs_copy_is_current() {
1106 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../docs/src/data/openapi.json");
1107 let current = serde_json::to_string_pretty(&document()).unwrap() + "\n";
1108 if std::env::var_os("G1T_WRITE_OPENAPI").is_some() {
1109 std::fs::write(path, &current).unwrap();
1110 return;
1111 }
1112 let copy = std::fs::read_to_string(path).unwrap_or_default().replace("\r\n", "\n");
1113 assert!(
1114 copy == current,
1115 "apps/docs/src/data/openapi.json is out of date: run G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi"
1116 );
1117 }
1118
1119 /// The reference shows responses as they are sent: `snake_case`.
1120 #[test]
1121 fn example_responses_are_snake_case() {
1122 let document = document();
1123 for (path, methods) in document["paths"].as_object().unwrap() {
1124 for (method, operation) in methods.as_object().unwrap() {
1125 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
1126 let leaked = g1t_kit::wire::camel_case_keys(example);
1127 assert!(leaked.is_empty(), "{method} {path} shows {leaked:?}");
1128 }
1129 }
1130 }
1131
1132 /// Examples never hold anything that reads as a real credential, which
1133 /// secret scanners rightly flag in a public repository: they end in `…`
1134 /// after the prefix, as `whsec_…` and `g1t_…` do.
1135 #[test]
1136 fn examples_hold_no_real_looking_secrets() {
1137 let prefixes = ["whsec_", "g1t_", "g1tr_", "g1trt_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"];
1138 for (line, text) in REFERENCE.lines().enumerate() {
1139 for prefix in prefixes {
1140 let mut rest = text;
1141 while let Some(at) = rest.find(prefix) {
1142 let after = &rest[at + prefix.len()..];
1143 let run = after.chars().take_while(|c| c.is_ascii_alphanumeric()).count();
1144 assert!(
1145 run < 12,
1146 "reference.json line {}: `{prefix}` followed by {run} characters reads as a real secret; write `{prefix}…`",
1147 line + 1
1148 );
1149 rest = after;
1150 }
1151 }
1152 }
1153 }
1154}