Skip to content
1,422 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::run_artifacts::ArtifactsOp;
12use crate::customize::CustomizeOp;
13use crate::deploy_keys::DeployKeysOp;
14use crate::mirrors::MirrorsOp;
15use crate::deployments::DeploymentsOp;
16use crate::packages::PackagesOp;
17use crate::folios::FoliosOp;
18use crate::protection::ProtectionOp;
19use crate::token_policy::TokenOp;
20use crate::operations::Op;
21use crate::checks::ChecksOp;
22use crate::rules::RulesOp;
23use crate::security::SecurityOp;
24use crate::rest::{ROUTES, Route};
25
26/// The sections of the API reference: a name, what it covers, and its
27/// operations in the order a reader meets them.
28const SECTIONS: &[(&str, &str, &[Op])] = &[
29 (
30 "Accounts",
31 "Signing in from a tool, who a token acts as, and your email addresses.",
32 &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::ConfirmEmail, Op::RemoveEmail, Op::UpdateEmailSettings],
33 ),
34 (
35 "Notifications",
36 "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.",
37 &[
38 Op::ListNotifications,
39 Op::MarkNotificationsRead,
40 Op::GetNotificationThread,
41 Op::MarkThreadRead,
42 Op::MarkThreadDone,
43 Op::SaveThread,
44 Op::SnoozeThread,
45 Op::GetThreadSubscription,
46 Op::SetThreadSubscription,
47 Op::DeleteThreadSubscription,
48 Op::GetRepoSubscription,
49 Op::SetRepoSubscription,
50 Op::DeleteRepoSubscription,
51 Op::ListWatchedRepos,
52 ],
53 ),
54 (
55 "Pinned projects",
56 "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.",
57 &[Op::ListPinnedProjects, Op::PinProject, Op::UnpinProject, Op::ReorderPinnedProjects],
58 ),
59 (
60 "Projects",
61 "A project is what a workspace builds and runs, from a repository or a root directory in one. Each says what it is, where it runs and where to find it: its homepage, docs and other links.",
62 &[Op::ListProjects, Op::GetProject, Op::UpdateProject],
63 ),
64 (
65 "Workspaces",
66 "A workspace owns repositories and is the first part of their address. People and agents work in workspaces.",
67 &[Op::GetWorkspace, Op::CreateWorkspace, Op::UpdateWorkspace, Op::DeleteWorkspace],
68 ),
69 (
70 "Personal access tokens",
71 "A workspace's rules for its members' personal access tokens: whether tokens made for all of a member's workspaces reach it, whether tokens may be made for it alone and wait for an owner's approval, and how long a token may last; the tokens that reach it, approving or denying the ones that wait, and revoking one there. Owners only, as people.",
72 &[
73 Op::Tokens(TokenOp::GetTokenPolicy),
74 Op::Tokens(TokenOp::SetTokenPolicy),
75 Op::Tokens(TokenOp::ListMemberTokens),
76 Op::Tokens(TokenOp::ListTokenRequests),
77 Op::Tokens(TokenOp::ReviewTokenRequest),
78 Op::Tokens(TokenOp::RevokeMemberToken),
79 ],
80 ),
81 (
82 "Members",
83 "A workspace's members and owners: who belongs to it, their roles (owner or member, with billing manager and security manager on top), handing it to another member, and leaving it.",
84 &[Op::ListMembers, Op::UpdateMember, Op::RemoveMember, Op::TransferOwnership, Op::LeaveWorkspace],
85 ),
86 (
87 "Invites",
88 "While g1t is invite-only, every new account needs an invite. Your invites, inviting people into a workspace by username or email, and answering the invitations to workspaces sent to you.",
89 &[
90 Op::ListInvites,
91 Op::CreateInvite,
92 Op::RevokeInvite,
93 Op::ListWorkspaceInvites,
94 Op::InviteMember,
95 Op::RevokeWorkspaceInvite,
96 Op::ListInvitations,
97 Op::AcceptInvitation,
98 Op::DeclineInvitation,
99 ],
100 ),
101 (
102 "Billing",
103 "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.",
104 &[
105 Op::GetUsage,
106 Op::GetBudget,
107 Op::SetBudget,
108 Op::GetAiCredit,
109 Op::BuyAiCredit,
110 Op::ListInvoices,
111 Op::GetBillingDetails,
112 Op::ListGatewayRequests,
113 ],
114 ),
115 (
116 "Repositories",
117 "A repository, how it handles pull requests, and its timeline: renaming, archiving, moving and deleting it.",
118 &[
119 Op::ListRepos,
120 Op::CreateRepo,
121 Op::GetRepo,
122 Op::UpdateRepo,
123 Op::RenameRepo,
124 Op::RenameBranch,
125 Op::SetRepoVisibility,
126 Op::ArchiveRepo,
127 Op::UnarchiveRepo,
128 Op::TransferRepo,
129 Op::DeleteRepo,
130 Op::ListDeletedRepos,
131 Op::RestoreRepo,
132 Op::PurgeRepo,
133 Op::GetRepoSettings,
134 Op::UpdateRepoSettings,
135 Op::ListCheckNames,
136 Op::GetCodeownersErrors,
137 Op::ListEvents,
138 ],
139 ),
140 (
141 "Repository insights",
142 "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.",
143 &[Op::About(AboutOp::GetLanguages), Op::About(AboutOp::ListContributors), Op::About(AboutOp::GetLicense)],
144 ),
145 (
146 "Stars",
147 "Starring a repository, to keep it and to say you like it: who starred one, and what you starred.",
148 &[
149 Op::About(AboutOp::ListStargazers),
150 Op::About(AboutOp::ListStarred),
151 Op::About(AboutOp::CheckStarred),
152 Op::About(AboutOp::Star),
153 Op::About(AboutOp::Unstar),
154 ],
155 ),
156 (
157 "Releases",
158 "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.",
159 &[
160 Op::About(AboutOp::ListReleases),
161 Op::About(AboutOp::CreateRelease),
162 Op::About(AboutOp::GetLatestRelease),
163 Op::About(AboutOp::GetReleaseByTag),
164 Op::About(AboutOp::GetRelease),
165 Op::About(AboutOp::UpdateRelease),
166 Op::About(AboutOp::DeleteRelease),
167 ],
168 ),
169 (
170 "Access",
171 "Who can do what in a repository: repository roles, people given a role on one repository (outside collaborators when they are not members), invitations, a workspace's base permission, and deploy keys: SSH keys that reach one repository.",
172 &[
173 Op::ListCollaborators,
174 Op::AddCollaborator,
175 Op::UpdateCollaborator,
176 Op::RemoveCollaborator,
177 Op::GetCollaboratorPermission,
178 Op::ListRepoInvitations,
179 Op::RevokeRepoInvitation,
180 Op::ListMyRepoInvitations,
181 Op::AcceptRepoInvitation,
182 Op::DeclineRepoInvitation,
183 Op::SetBasePermission,
184 Op::ListOutsideCollaborators,
185 Op::DeployKeys(DeployKeysOp::ListDeployKeys),
186 Op::DeployKeys(DeployKeysOp::GetDeployKey),
187 Op::DeployKeys(DeployKeysOp::CreateDeployKey),
188 Op::DeployKeys(DeployKeysOp::DeleteDeployKey),
189 ],
190 ),
191 (
192 "Mirroring",
193 "A repository's links to copies of it on other hosts: a mirror stands by as a read-only copy of a remote that leads, until someone takes over on g1t and later hands back, or moves it to g1t for good. A repository g1t leads can be mirrored to other hosts.",
194 &[
195 Op::Mirrors(MirrorsOp::GetMirror),
196 Op::Mirrors(MirrorsOp::GetHandBackPlan),
197 Op::Mirrors(MirrorsOp::TakeOver),
198 Op::Mirrors(MirrorsOp::SetCiFailover),
199 Op::Mirrors(MirrorsOp::HandBack),
200 Op::Mirrors(MirrorsOp::MoveToG1t),
201 Op::Mirrors(MirrorsOp::SyncMirror),
202 Op::Mirrors(MirrorsOp::AddRemote),
203 Op::Mirrors(MirrorsOp::UpdateRemote),
204 Op::Mirrors(MirrorsOp::RemoveRemote),
205 ],
206 ),
207 (
208 "Teams",
209 "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.",
210 &[
211 Op::ListTeams,
212 Op::CreateTeam,
213 Op::GetTeam,
214 Op::UpdateTeam,
215 Op::DeleteTeam,
216 Op::ListTeamMembers,
217 Op::SetTeamMember,
218 Op::RemoveTeamMember,
219 Op::ListChildTeams,
220 Op::ListTeamRepos,
221 Op::SetTeamRepo,
222 Op::RemoveTeamRepo,
223 Op::SetTeamReviewAssignment,
224 Op::ListUserTeams,
225 ],
226 ),
227 (
228 "Security",
229 "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.",
230 &[Op::ListSecurityAlerts, Op::DismissSecurityAlert, Op::ReopenSecurityAlert],
231 ),
232 (
233 "Secret scanning",
234 "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.",
235 &[
236 Op::Security(SecurityOp::ListSecretAlerts),
237 Op::Security(SecurityOp::GetSecretAlert),
238 Op::Security(SecurityOp::UpdateSecretAlert),
239 Op::Security(SecurityOp::ListSecretLocations),
240 Op::Security(SecurityOp::BypassPushProtection),
241 Op::Security(SecurityOp::CheckSecretValidity),
242 Op::Security(SecurityOp::ListBypassRequests),
243 Op::Security(SecurityOp::ReviewBypassRequest),
244 Op::Security(SecurityOp::ListCustomPatterns),
245 Op::Security(SecurityOp::CreateCustomPattern),
246 Op::Security(SecurityOp::UpdateCustomPattern),
247 Op::Security(SecurityOp::DeleteCustomPattern),
248 Op::Security(SecurityOp::DryRunCustomPattern),
249 ],
250 ),
251 (
252 "Code scanning",
253 "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.",
254 &[
255 Op::Security(SecurityOp::ListCodeAlerts),
256 Op::Security(SecurityOp::GetCodeAlert),
257 Op::Security(SecurityOp::UpdateCodeAlert),
258 Op::Security(SecurityOp::ListAnalyses),
259 Op::Security(SecurityOp::UploadSarif),
260 Op::Security(SecurityOp::GetSarifUpload),
261 Op::Security(SecurityOp::FixAlert),
262 ],
263 ),
264 (
265 "Supply chain",
266 "What a repository depends on: vulnerability alerts, the dependency graph, an SPDX SBOM of it, and comparing two commits' dependencies as dependency review does.",
267 &[
268 Op::Security(SecurityOp::ListVulnerabilityAlerts),
269 Op::Security(SecurityOp::GetVulnerabilityAlert),
270 Op::Security(SecurityOp::UpdateVulnerabilityAlert),
271 Op::Security(SecurityOp::GetDependencyGraph),
272 Op::Security(SecurityOp::GetSbom),
273 Op::Security(SecurityOp::CompareDependencies),
274 ],
275 ),
276 (
277 "Security settings",
278 "When pull request checks fail, dependency review's policy, delegated bypass and validity checks, and a workspace's security overview.",
279 &[
280 Op::Security(SecurityOp::GetSettings),
281 Op::Security(SecurityOp::UpdateSettings),
282 Op::Security(SecurityOp::GetWorkspaceSettings),
283 Op::Security(SecurityOp::UpdateWorkspaceSettings),
284 Op::Security(SecurityOp::GetOverview),
285 ],
286 ),
287 (
288 "Security advisories",
289 "What a repository says about vulnerabilities in what it ships: advisories drafted, published and withdrawn by its maintainers, and the vulnerability reports people send them privately when the repository takes them.",
290 &[
291 Op::Security(SecurityOp::ListAdvisories),
292 Op::Security(SecurityOp::CreateAdvisory),
293 Op::Security(SecurityOp::GetAdvisory),
294 Op::Security(SecurityOp::UpdateAdvisory),
295 Op::Security(SecurityOp::SetAdvisoryState),
296 Op::Security(SecurityOp::ListReports),
297 Op::Security(SecurityOp::ReportVulnerability),
298 Op::Security(SecurityOp::ReviewReport),
299 ],
300 ),
301 (
302 "Rules",
303 "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.",
304 &[
305 Op::Rules(RulesOp::ListRepoRulesets),
306 Op::Rules(RulesOp::CreateRepoRuleset),
307 Op::Rules(RulesOp::GetRepoRuleset),
308 Op::Rules(RulesOp::UpdateRepoRuleset),
309 Op::Rules(RulesOp::DeleteRepoRuleset),
310 Op::Rules(RulesOp::GetBranchRules),
311 Op::Rules(RulesOp::ListRuleEvaluations),
312 Op::Rules(RulesOp::ListWorkspaceRulesets),
313 Op::Rules(RulesOp::CreateWorkspaceRuleset),
314 Op::Rules(RulesOp::GetWorkspaceRuleset),
315 Op::Rules(RulesOp::UpdateWorkspaceRuleset),
316 Op::Rules(RulesOp::DeleteWorkspaceRuleset),
317 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
318 ],
319 ),
320 (
321 "Checks",
322 "What CI, integrations and g1t Actions say about a commit, in the shapes CI tools already send: statuses (a state per context) and check runs (a lifecycle, a conclusion, a Markdown report, annotations on lines and buttons), grouped per reporter into check suites. g1t Actions jobs are check runs too. Required checks are met by either.",
323 &[
324 Op::Checks(ChecksOp::CreateCommitStatus),
325 Op::Checks(ChecksOp::ListCommitStatuses),
326 Op::Checks(ChecksOp::GetCombinedStatus),
327 Op::Checks(ChecksOp::CreateCheckRun),
328 Op::Checks(ChecksOp::UpdateCheckRun),
329 Op::Checks(ChecksOp::GetCheckRun),
330 Op::Checks(ChecksOp::ListCheckRunAnnotations),
331 Op::Checks(ChecksOp::RerequestCheckRun),
332 Op::Checks(ChecksOp::ListCheckRunsForRef),
333 Op::Checks(ChecksOp::ListCheckSuitesForRef),
334 Op::Checks(ChecksOp::GetCheckSuite),
335 Op::Checks(ChecksOp::RerequestCheckSuite),
336 ],
337 ),
338 (
339 "Issues",
340 "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
341 &[
342 Op::ListIssues,
343 Op::CreateIssue,
344 Op::GetIssue,
345 Op::UpdateIssue,
346 Op::CloseIssue,
347 Op::ReopenIssue,
348 Op::AssignIssue,
349 Op::Delegate,
350 Op::AddComment,
351 Op::EditComment,
352 Op::DeleteComment,
353 Op::ListIssueLabels,
354 Op::AddIssueLabels,
355 Op::SetIssueLabels,
356 Op::RemoveIssueLabels,
357 ],
358 ),
359 (
360 "Labels and milestones",
361 "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.",
362 &[
363 Op::ListLabels,
364 Op::CreateLabel,
365 Op::UpdateLabel,
366 Op::DeleteLabel,
367 Op::AddDefaultLabels,
368 Op::ListMilestones,
369 Op::CreateMilestone,
370 Op::GetMilestone,
371 Op::UpdateMilestone,
372 Op::DeleteMilestone,
373 ],
374 ),
375 (
376 "Plans",
377 "An outcome turned into the issues that would get there, with the order they must merge in.",
378 &[Op::PlanWork, Op::GetPlan, Op::ApplyPlan],
379 ),
380 (
381 "Pull requests",
382 "A proposed change in its own fork or on a branch. Several can be made for one issue; the one merged resolves it.",
383 &[
384 Op::ListPullRequests,
385 Op::CreatePullRequest,
386 Op::GetPullRequest,
387 Op::UpdatePullRequest,
388 Op::GetPullRequestChanges,
389 Op::MarkPullRequestReady,
390 Op::ConvertPullRequestToDraft,
391 Op::RequestReviewers,
392 Op::RemoveRequestedReviewers,
393 Op::ReviewPullRequest,
394 Op::MergePullRequest,
395 Op::ClosePullRequest,
396 Op::ReopenPullRequest,
397 Op::GetMergeQueue,
398 Op::MessageAgent,
399 Op::AnswerMessage,
400 Op::TakeMessages,
401 ],
402 ),
403 (
404 "Sessions",
405 "The record of how a pull request was made: prompts, reasoning and the tools that ran.",
406 &[Op::ReadSession, Op::RecordSession],
407 ),
408 (
409 "Memory",
410 "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.",
411 &[Op::Remember, Op::Recall],
412 ),
413 (
414 "Search",
415 "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.",
416 &[Op::Search],
417 ),
418 (
419 "Context",
420 "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.",
421 &[Op::SearchContext, Op::GetEntity],
422 ),
423 (
424 "Actions",
425 "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.",
426 &[
427 Op::ListWorkflows,
428 Op::ListWorkflowRuns,
429 Op::GetWorkflowRun,
430 Op::GetJobLogs,
431 Op::DispatchWorkflow,
432 Op::CancelWorkflowRun,
433 Op::RerunWorkflowRun,
434 Op::UpdateWorkflow,
435 Op::Artifacts(ArtifactsOp::ListArtifacts),
436 Op::Artifacts(ArtifactsOp::ListRunArtifacts),
437 Op::Artifacts(ArtifactsOp::GetArtifact),
438 Op::Artifacts(ArtifactsOp::DownloadArtifact),
439 Op::Artifacts(ArtifactsOp::DeleteArtifact),
440 Op::Artifacts(ArtifactsOp::GetArtifactRetention),
441 Op::Artifacts(ArtifactsOp::SetArtifactRetention),
442 ],
443 ),
444 (
445 "Packages",
446 "A workspace's packages in every registry: their versions and download counts, deleting and restoring them within 30 days, their visibility and repository, the people and teams with a role on them, and which repositories' workflows may use them (Manage Actions access). A package is named by its type and its name, URL-encoded where it holds a slash.",
447 &[
448 Op::Packages(PackagesOp::ListPackages),
449 Op::Packages(PackagesOp::GetPackage),
450 Op::Packages(PackagesOp::UpdatePackage),
451 Op::Packages(PackagesOp::DeletePackage),
452 Op::Packages(PackagesOp::RestorePackage),
453 Op::Packages(PackagesOp::ListVersions),
454 Op::Packages(PackagesOp::GetVersion),
455 Op::Packages(PackagesOp::DeleteVersion),
456 Op::Packages(PackagesOp::RestoreVersion),
457 Op::Packages(PackagesOp::LinkPackage),
458 Op::Packages(PackagesOp::UnlinkPackage),
459 Op::Packages(PackagesOp::ListAccess),
460 Op::Packages(PackagesOp::SetAccess),
461 Op::Packages(PackagesOp::RemoveAccess),
462 Op::Packages(PackagesOp::ListActionsAccess),
463 Op::Packages(PackagesOp::SetActionsAccess),
464 Op::Packages(PackagesOp::RemoveActionsAccess),
465 ],
466 ),
467 (
468 "Deployments",
469 "A repository's deployments wherever they run: reported from any CI with these routes, made by g1t Actions jobs with an `environment:`, or built on g1t.page. Each has statuses, shows on its commit as the check `deploy / <environment>`, and belongs to an environment.",
470 &[
471 Op::Deployments(DeploymentsOp::ListDeployments),
472 Op::Deployments(DeploymentsOp::CreateDeployment),
473 Op::Deployments(DeploymentsOp::GetDeployment),
474 Op::Deployments(DeploymentsOp::ListDeploymentStatuses),
475 Op::Deployments(DeploymentsOp::CreateDeploymentStatus),
476 Op::Deployments(DeploymentsOp::ListEnvironments),
477 Op::Deployments(DeploymentsOp::GetEnvironment),
478 Op::Deployments(DeploymentsOp::StartDeploymentBuild),
479 ],
480 ),
481 (
482 "Run protection",
483 "What keeps workflow runs safe: environments' protection rules (required reviewers, a wait timer, which branches may deploy) and the reviews of the jobs they hold, approving a pull request's run from outside, what a job's token gets when its workflow writes no `permissions:`, and repository_dispatch, which a job's own token may send.",
484 &[
485 Op::Protection(ProtectionOp::UpdateEnvironment),
486 Op::Protection(ProtectionOp::DeleteEnvironment),
487 Op::Protection(ProtectionOp::GetPendingDeployments),
488 Op::Protection(ProtectionOp::ReviewPendingDeployments),
489 Op::Protection(ProtectionOp::ApproveWorkflowRun),
490 Op::Protection(ProtectionOp::GetWorkflowPermissions),
491 Op::Protection(ProtectionOp::SetWorkflowPermissions),
492 Op::Protection(ProtectionOp::GetForkPrApproval),
493 Op::Protection(ProtectionOp::SetForkPrApproval),
494 Op::Protection(ProtectionOp::GetActionsAccess),
495 Op::Protection(ProtectionOp::SetActionsAccess),
496 Op::Protection(ProtectionOp::CreateRepositoryDispatch),
497 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
498 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
499 ],
500 ),
501 (
502 "Secrets and variables",
503 "Values that workflows and deployments read, per repository or for a whole workspace, with a row per environment.",
504 &[
505 Op::ListActionsSecrets,
506 Op::SetActionsSecret,
507 Op::DeleteActionsSecret,
508 Op::ListActionsVariables,
509 Op::SetActionsVariable,
510 Op::DeleteActionsVariable,
511 ],
512 ),
513 (
514 "Runners",
515 "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.",
516 &[
517 Op::ListRunners,
518 Op::CreateRunnerRegistrationToken,
519 Op::RemoveRunner,
520 Op::ListRunnerGroups,
521 Op::CreateRunnerGroup,
522 Op::UpdateRunnerGroup,
523 Op::DeleteRunnerGroup,
524 Op::GetRunnerSettings,
525 Op::UpdateRunnerSettings,
526 ],
527 ),
528 (
529 "Agents' computers",
530 "Each workspace agent has a computer of its own on g1t cloud: a persistent home its sessions run commands on, which sleeps when idle and is saved as it was. Its state and recent commands, and waking, sleeping and resetting it.",
531 &[
532 Op::GetAgentComputer,
533 Op::WakeAgentComputer,
534 Op::SleepAgentComputer,
535 Op::ResetAgentComputer,
536 Op::ListAgentComputerCommands,
537 ],
538 ),
539 (
540 "Webhooks",
541 "Signed HTTPS requests sent to your own address as things happen, for a repository or a whole workspace.",
542 &[
543 Op::ListWebhooks,
544 Op::CreateWebhook,
545 Op::UpdateWebhook,
546 Op::DeleteWebhook,
547 Op::PingWebhook,
548 Op::ListWebhookDeliveries,
549 Op::RedeliverWebhook,
550 ],
551 ),
552 (
553 "Integrations",
554 "A workspace's connections to outside systems: model providers, alert sources and issue trackers.",
555 &[
556 Op::ListIntegrations,
557 Op::ConnectIntegration,
558 Op::UpdateIntegration,
559 Op::DisconnectIntegration,
560 Op::TestIntegration,
561 Op::GetModelRoutes,
562 Op::SetModelRoutes,
563 Op::GetContext,
564 Op::ImportIssue,
565 ],
566 ),
567 (
568 "Customize",
569 "What shapes how a workspace's agents work, in the formats Claude Code reads: the marketplaces it reads plugins from, what Discover lists, the plugins it installed (their skills, subagents and remote MCP servers reach g1t's agents; the rest reaches Claude Code on a runner), and its connectors: MCP servers connected once, then given to every agent, a team or one agent. The bundle is what a job's Claude Code loads on a runner.",
570 &[
571 Op::Customize(CustomizeOp::ListMarketplaces),
572 Op::Customize(CustomizeOp::AddMarketplace),
573 Op::Customize(CustomizeOp::RemoveMarketplace),
574 Op::Customize(CustomizeOp::RefreshMarketplace),
575 Op::Customize(CustomizeOp::Discover),
576 Op::Customize(CustomizeOp::ListPlugins),
577 Op::Customize(CustomizeOp::GetPlugin),
578 Op::Customize(CustomizeOp::InstallPlugin),
579 Op::Customize(CustomizeOp::SetPluginEnabled),
580 Op::Customize(CustomizeOp::UninstallPlugin),
581 Op::Customize(CustomizeOp::UpdatePlugin),
582 Op::Customize(CustomizeOp::ListPluginAgents),
583 Op::Customize(CustomizeOp::ListConnectors),
584 Op::Customize(CustomizeOp::GetConnector),
585 Op::Customize(CustomizeOp::AddConnector),
586 Op::Customize(CustomizeOp::SetConnectorKey),
587 Op::Customize(CustomizeOp::RemoveConnector),
588 Op::Customize(CustomizeOp::RefreshConnector),
589 Op::Customize(CustomizeOp::GrantConnector),
590 Op::Customize(CustomizeOp::RevokeConnector),
591 Op::Customize(CustomizeOp::Bundle),
592 ],
593 ),
594 (
595 "Artifacts",
596 "A workspace's docs, slides, designs and dashboards (Artifacts mode): listing and searching the ones you can open, reading and editing their content in Markdown, making, moving, trashing and restoring them, their versions, and who can open them. Each answers for your own role on it. Not workflow runs' artifacts, which are under Actions. Slides, designs and dashboards answer that they are not here yet.",
597 &[
598 Op::Folios(FoliosOp::List),
599 Op::Folios(FoliosOp::Search),
600 Op::Folios(FoliosOp::Get),
601 Op::Folios(FoliosOp::GetContent),
602 Op::Folios(FoliosOp::ListVersions),
603 Op::Folios(FoliosOp::GetAccess),
604 Op::Folios(FoliosOp::ListTemplates),
605 Op::Folios(FoliosOp::ListSpaces),
606 Op::Folios(FoliosOp::QueryDataset),
607 Op::Folios(FoliosOp::Create),
608 Op::Folios(FoliosOp::Update),
609 Op::Folios(FoliosOp::Edit),
610 Op::Folios(FoliosOp::Trash),
611 Op::Folios(FoliosOp::Restore),
612 Op::Folios(FoliosOp::RestoreVersion),
613 Op::Folios(FoliosOp::SetAccess),
614 Op::Folios(FoliosOp::Purge),
615 ],
616 ),
617];
618
619/// The section of the API reference an operation is listed under.
620fn tag(op: Op) -> &'static str {
621 SECTIONS
622 .iter()
623 .find(|(_, _, ops)| ops.contains(&op))
624 .map_or("Repositories", |(name, _, _)| name)
625}
626
627/// What an operation's page is called, as a short sentence.
628fn title(op: Op) -> &'static str {
629 match op {
630 Op::Whoami => "Get the current user",
631 Op::GetWorkspace => "Get a workspace",
632 Op::CreateWorkspace => "Create a workspace",
633 Op::DeleteWorkspace => "Delete a workspace",
634 Op::UpdateWorkspace => "Update a workspace",
635 Op::ListMembers => "List a workspace's members",
636 Op::UpdateMember => "Change a member's role",
637 Op::RemoveMember => "Remove a member",
638 Op::TransferOwnership => "Transfer a workspace's ownership",
639 Op::LeaveWorkspace => "Leave a workspace",
640 Op::ListEmails => "List your email addresses",
641 Op::AddEmail => "Add an email address",
642 Op::ConfirmEmail => "Confirm an email address",
643 Op::RemoveEmail => "Remove an email address",
644 Op::UpdateEmailSettings => "Change your email settings",
645 Op::ListInvites => "List your invites",
646 Op::CreateInvite => "Create an invite",
647 Op::RevokeInvite => "Revoke an invite",
648 Op::ListWorkspaceInvites => "List a workspace's invites",
649 Op::InviteMember => "Invite someone to a workspace",
650 Op::ListInvitations => "List your workspace invitations",
651 Op::AcceptInvitation => "Accept a workspace invitation",
652 Op::DeclineInvitation => "Decline a workspace invitation",
653 Op::RevokeWorkspaceInvite => "Revoke a workspace's invite",
654 Op::TransferRepo => "Transfer a repository",
655 Op::RenameRepo => "Rename a repository",
656 Op::RenameBranch => "Rename a branch",
657 Op::ArchiveRepo => "Archive a repository",
658 Op::UnarchiveRepo => "Unarchive a repository",
659 Op::SetRepoVisibility => "Change a repository's visibility",
660 Op::DeleteRepo => "Delete a repository",
661 Op::ListDeletedRepos => "List recently deleted repositories",
662 Op::RestoreRepo => "Restore a deleted repository",
663 Op::PurgeRepo => "Purge a deleted repository",
664 Op::ListRepos => "List repositories",
665 Op::GetRepo => "Get a repository",
666 Op::CreateRepo => "Create a repository",
667 Op::UpdateRepo => "Update a repository",
668 Op::GetRepoSettings => "Get repository settings",
669 Op::UpdateRepoSettings => "Update repository settings",
670 Op::ListCheckNames => "List check names",
671 Op::GetMergeQueue => "Get the merge queue",
672 Op::MessageAgent => "Message an agent",
673 Op::AnswerMessage => "Answer a message",
674 Op::TakeMessages => "Take new messages",
675 Op::Remember => "Remember something",
676 Op::Recall => "Recall memory",
677 Op::SearchContext => "Search the context hub",
678 Op::GetEntity => "Get a catalog entry",
679 Op::Search => "Search g1t",
680 Op::ListIssues => "List issues",
681 Op::GetIssue => "Get an issue",
682 Op::CreateIssue => "Create an issue",
683 Op::UpdateIssue => "Update an issue",
684 Op::CloseIssue => "Close an issue",
685 Op::ReopenIssue => "Reopen an issue",
686 Op::AssignIssue => "Assign an issue to g1t",
687 Op::Delegate => "Put an agent on it",
688 Op::PlanWork => "Plan work",
689 Op::GetPlan => "Get a plan",
690 Op::ApplyPlan => "Apply a plan",
691 Op::ListLabels => "List labels",
692 Op::CreateLabel => "Create a label",
693 Op::UpdateLabel => "Update a label",
694 Op::DeleteLabel => "Delete a label",
695 Op::AddDefaultLabels => "Add the default labels",
696 Op::ListIssueLabels => "List an issue's labels",
697 Op::AddIssueLabels => "Add labels to an issue",
698 Op::SetIssueLabels => "Set an issue's labels",
699 Op::RemoveIssueLabels => "Remove labels from an issue",
700 Op::ListMilestones => "List milestones",
701 Op::GetMilestone => "Get a milestone",
702 Op::CreateMilestone => "Create a milestone",
703 Op::UpdateMilestone => "Update a milestone",
704 Op::DeleteMilestone => "Delete a milestone",
705 Op::UpdatePullRequest => "Update a pull request",
706 Op::AddComment => "Add a comment",
707 Op::EditComment => "Edit a comment",
708 Op::DeleteComment => "Delete a comment",
709 Op::ReviewPullRequest => "Review a pull request",
710 Op::ListPullRequests => "List pull requests",
711 Op::GetPullRequest => "Get a pull request",
712 Op::CreatePullRequest => "Create a pull request",
713 Op::RecordSession => "Record session entries",
714 Op::ReadSession => "Read a session",
715 Op::MarkPullRequestReady => "Mark a pull request ready",
716 Op::ClosePullRequest => "Close a pull request",
717 Op::ReopenPullRequest => "Reopen a pull request",
718 Op::ConvertPullRequestToDraft => "Convert a pull request to a draft",
719 Op::GetPullRequestChanges => "Get a pull request's changes",
720 Op::MergePullRequest => "Merge a pull request",
721 Op::ListEvents => "List repository events",
722 Op::ListIntegrations => "List integrations",
723 Op::ConnectIntegration => "Connect an integration",
724 Op::UpdateIntegration => "Update an integration",
725 Op::DisconnectIntegration => "Disconnect an integration",
726 Op::TestIntegration => "Test an integration",
727 Op::GetContext => "Look up a ticket",
728 Op::ImportIssue => "Import an issue",
729 Op::GetModelRoutes => "Get model routes",
730 Op::SetModelRoutes => "Set model routes",
731 Op::ListWebhooks => "List webhooks",
732 Op::CreateWebhook => "Create a webhook",
733 Op::UpdateWebhook => "Update a webhook",
734 Op::DeleteWebhook => "Delete a webhook",
735 Op::PingWebhook => "Ping a webhook",
736 Op::ListWebhookDeliveries => "List webhook deliveries",
737 Op::RedeliverWebhook => "Redeliver a webhook delivery",
738 Op::ListWorkflows => "List workflows",
739 Op::ListWorkflowRuns => "List workflow runs",
740 Op::GetWorkflowRun => "Get a workflow run",
741 Op::GetJobLogs => "Get a job's log",
742 Op::DispatchWorkflow => "Run a workflow",
743 Op::CancelWorkflowRun => "Cancel a workflow run",
744 Op::RerunWorkflowRun => "Re-run a workflow run",
745 Op::UpdateWorkflow => "Turn a workflow on or off",
746 Op::ListActionsSecrets => "List secrets",
747 Op::SetActionsSecret => "Set a secret",
748 Op::DeleteActionsSecret => "Delete a secret",
749 Op::ListActionsVariables => "List variables",
750 Op::SetActionsVariable => "Set a variable",
751 Op::DeleteActionsVariable => "Delete a variable",
752 Op::ListRunners => "List self-hosted runners",
753 Op::ListRunnerGroups => "List runner groups",
754 Op::GetAgentComputer => "Get an agent's computer",
755 Op::WakeAgentComputer => "Wake an agent's computer",
756 Op::SleepAgentComputer => "Put an agent's computer to sleep",
757 Op::ResetAgentComputer => "Reset an agent's computer",
758 Op::ListAgentComputerCommands => "List an agent's computer's commands",
759 Op::GetRunnerSettings => "Get runner settings",
760 Op::CreateRunnerRegistrationToken => "Create a runner registration token",
761 Op::RemoveRunner => "Remove a self-hosted runner",
762 Op::CreateRunnerGroup => "Create a runner group",
763 Op::UpdateRunnerGroup => "Change a runner group",
764 Op::DeleteRunnerGroup => "Delete a runner group",
765 Op::UpdateRunnerSettings => "Change runner settings",
766 Op::ListCollaborators => "List who has access",
767 Op::AddCollaborator => "Add a collaborator",
768 Op::UpdateCollaborator => "Change a collaborator's role",
769 Op::RemoveCollaborator => "Remove a collaborator",
770 Op::GetCollaboratorPermission => "Get someone's permission",
771 Op::ListRepoInvitations => "List a repository's invitations",
772 Op::RevokeRepoInvitation => "Revoke a repository invitation",
773 Op::ListMyRepoInvitations => "List your repository invitations",
774 Op::AcceptRepoInvitation => "Accept a repository invitation",
775 Op::DeclineRepoInvitation => "Decline a repository invitation",
776 Op::SetBasePermission => "Set the base permission",
777 Op::ListOutsideCollaborators => "List outside collaborators",
778 Op::ListSecurityAlerts => "List security alerts",
779 Op::DismissSecurityAlert => "Dismiss a security alert",
780 Op::ReopenSecurityAlert => "Reopen a security alert",
781 Op::ListNotifications => "List notifications",
782 Op::MarkNotificationsRead => "Mark notifications read",
783 Op::GetNotificationThread => "Get a thread",
784 Op::MarkThreadRead => "Mark a thread read",
785 Op::MarkThreadDone => "Mark a thread done",
786 Op::SaveThread => "Save a thread",
787 Op::SnoozeThread => "Snooze a thread",
788 Op::GetThreadSubscription => "Get a thread subscription",
789 Op::SetThreadSubscription => "Set a thread subscription",
790 Op::DeleteThreadSubscription => "Unsubscribe from a thread",
791 Op::GetRepoSubscription => "Get how you watch a repository",
792 Op::SetRepoSubscription => "Watch a repository",
793 Op::DeleteRepoSubscription => "Stop watching a repository",
794 Op::ListWatchedRepos => "List repositories you watch",
795 Op::ListPinnedProjects => "List your pinned projects",
796 Op::GetUsage => "Get a workspace's usage",
797 Op::GetBudget => "Get a workspace's budget",
798 Op::SetBudget => "Change a workspace's budget",
799 Op::GetAiCredit => "Get a workspace's AI credit",
800 Op::BuyAiCredit => "Buy AI credit",
801 Op::ListInvoices => "List a workspace's invoices",
802 Op::GetBillingDetails => "Get a workspace's billing details",
803 Op::ListGatewayRequests => "List a workspace's AI Gateway requests",
804 Op::PinProject => "Pin a project",
805 Op::UnpinProject => "Unpin a project",
806 Op::ReorderPinnedProjects => "Reorder your pinned projects",
807 Op::ListProjects => "List a workspace's projects",
808 Op::GetProject => "Get a project",
809 Op::UpdateProject => "Update a project",
810 Op::ListTeams => "List teams",
811 Op::GetTeam => "Get a team",
812 Op::CreateTeam => "Create a team",
813 Op::UpdateTeam => "Update a team",
814 Op::DeleteTeam => "Delete a team",
815 Op::ListTeamMembers => "List a team's members",
816 Op::SetTeamMember => "Add or change a team member",
817 Op::RemoveTeamMember => "Remove a team member",
818 Op::ListChildTeams => "List child teams",
819 Op::ListTeamRepos => "List a team's repositories",
820 Op::SetTeamRepo => "Give a team a role on a repository",
821 Op::RemoveTeamRepo => "Remove a team from a repository",
822 Op::SetTeamReviewAssignment => "Set a team's review assignment",
823 Op::ListUserTeams => "List someone's teams",
824 Op::RequestReviewers => "Request reviewers",
825 Op::RemoveRequestedReviewers => "Remove requested reviewers",
826 Op::GetCodeownersErrors => "List CODEOWNERS errors",
827 Op::Security(op) => op.title(),
828 Op::Rules(op) => op.title(),
829 Op::Checks(op) => op.title(),
830 Op::About(op) => op.title(),
831 Op::Deployments(op) => op.title(),
832 Op::Protection(op) => op.title(),
833 Op::Tokens(op) => op.title(),
834 Op::Artifacts(op) => op.title(),
835 Op::DeployKeys(op) => op.title(),
836 Op::Mirrors(op) => op.title(),
837 Op::Packages(op) => op.title(),
838 Op::Folios(op) => op.title(),
839 Op::Customize(op) => op.title(),
840 }
841}
842
843/// Why an operation can be refused with `402 payment_required`, if it
844/// can: the ones that start an agent, when the workspace has no credit,
845/// and the ones that make a repository private in a workspace, when a free
846/// workspace's private storage has no room for it.
847fn may_need_payment(op: Op) -> Option<&'static str> {
848 match op {
849 Op::AssignIssue | Op::PlanWork | Op::ApplyPlan => Some("The workspace has no agent credit."),
850 Op::WakeAgentComputer => Some("The workspace's plan refuses compute: it is paused, or over its limit."),
851 Op::UpdateRepo | Op::SetRepoVisibility | Op::TransferRepo => Some(
852 "A free workspace's private storage has no room for this private repository.",
853 ),
854 _ => None,
855 }
856}
857
858/// What the reference says beyond each operation's own description, keyed
859/// by operation id, written by hand from what the services return: `notes`
860/// (Markdown, added to the description) and example `params` (path),
861/// `query`, `request` (body) and `response`.
862const REFERENCE: &str = include_str!("reference.json");
863
864fn examples() -> Map<String, Value> {
865 match serde_json::from_str(REFERENCE) {
866 Ok(Value::Object(examples)) => examples,
867 _ => Map::new(),
868 }
869}
870
871/// `/repos/:owner/:name` as OpenAPI writes it: `/repos/{owner}/{name}`.
872fn openapi_path(route: &Route) -> String {
873 route
874 .path
875 .split('/')
876 .map(|segment| match segment.strip_prefix(':') {
877 Some(name) => format!("{{{name}}}"),
878 None => segment.to_owned(),
879 })
880 .collect::<Vec<_>>()
881 .join("/")
882}
883
884fn error_response(description: &str) -> Value {
885 json!({
886 "description": description,
887 "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } },
888 })
889}
890
891/// A parameter in the path or the query, described by the operation's
892/// input schema where it has the same name.
893fn parameter(name: &str, place: &str, required: bool, schema: Option<&Value>) -> Value {
894 let mut schema = schema.cloned().unwrap_or_else(|| json!({ "type": "string" }));
895 let description = match name {
896 "owner" => Some(Value::from("The workspace that owns the repository.")),
897 "name" => Some(Value::from("The repository's name.")),
898 _ => schema.as_object_mut().and_then(|schema| schema.remove("description")),
899 };
900 let mut parameter = json!({
901 "name": name,
902 "in": place,
903 "required": required,
904 "schema": schema,
905 });
906 if let Some(description) = description {
907 parameter["description"] = description;
908 }
909 parameter
910}
911
912/// The operation id of a route. An operation reached at a workspace's
913/// address as well as a repository's is documented once for each, with its
914/// own id; GitHub's alternative addresses for one operation keep GitHub's
915/// names.
916fn operation_id(route: &Route) -> String {
917 let op = route.op;
918 let base = match (route.method, route.path.rsplit('/').next().unwrap_or_default()) {
919 ("PUT", "enable") => "enable_workflow".to_owned(),
920 ("PUT", "disable") => "disable_workflow".to_owned(),
921 ("POST", "rerun-failed-jobs") => "rerun_failed_jobs".to_owned(),
922 ("POST", "rerun") if route.path.contains("/jobs/:job/") => "rerun_job".to_owned(),
923 ("POST", "force-cancel") => "force_cancel_workflow_run".to_owned(),
924 ("GET", ":attempt") => "get_workflow_run_attempt".to_owned(),
925 ("PATCH", ":setting") => "update_actions_variable".to_owned(),
926 ("GET", "runs") if route.path.contains("/workflows/:workflow/") => "list_runs_of_workflow".to_owned(),
927 // One repository's notifications, and an issue's subscription by
928 // its number rather than a thread's id.
929 (_, "notifications") if route.path.starts_with("/repos/") => match op {
930 Op::ListNotifications => "list_repo_notifications".to_owned(),
931 _ => "mark_repo_notifications_read".to_owned(),
932 },
933 (method, "subscription") if route.path.contains("/issues/:number/") => match method {
934 "GET" => "get_issue_subscription".to_owned(),
935 "PUT" => "set_issue_subscription".to_owned(),
936 _ => "delete_issue_subscription".to_owned(),
937 },
938 // One label off an issue, by its name in the path.
939 ("DELETE", ":label") if route.path.contains("/issues/:number/") => "remove_issue_label".to_owned(),
940 ("DELETE", "saved") => "unsave_thread".to_owned(),
941 ("DELETE", "snooze") => "unsnooze_thread".to_owned(),
942 _ => op.name().to_owned(),
943 };
944 if route.path.starts_with("/workspaces/") && ROUTES.iter().any(|other| other.op == op && other.path.starts_with("/repos/")) {
945 format!("{base}_for_workspace")
946 } else {
947 base
948 }
949}
950
951/// The summary of a route: its operation's title, or for one of GitHub's
952/// alternative addresses, what that address does.
953fn summary(route: &Route, id: &str) -> String {
954 let base = match id.trim_end_matches("_for_workspace") {
955 "enable_workflow" => "Turn a workflow on",
956 "disable_workflow" => "Turn a workflow off",
957 "rerun_failed_jobs" => "Re-run failed jobs",
958 "rerun_job" => "Re-run a job",
959 "force_cancel_workflow_run" => "Force-cancel a workflow run",
960 "get_workflow_run_attempt" => "Get a workflow run attempt",
961 "update_actions_variable" => "Update a variable",
962 "list_runs_of_workflow" => "List a workflow's runs",
963 "list_repo_notifications" => "List a repository's notifications",
964 "mark_repo_notifications_read" => "Mark a repository's notifications read",
965 "get_issue_subscription" => "Get your subscription to an issue",
966 "set_issue_subscription" => "Subscribe to an issue",
967 "delete_issue_subscription" => "Unsubscribe from an issue",
968 "unsave_thread" => "Unsave a thread",
969 "unsnooze_thread" => "Bring a snoozed thread back",
970 _ => title(route.op),
971 };
972 if id.ends_with("_for_workspace") {
973 format!("{base} for a workspace")
974 } else {
975 base.to_owned()
976 }
977}
978
979fn operation(route: &Route) -> Value {
980 let op = route.op;
981 let path_params: Vec<&str> = route.params().collect();
982 // `owner` and `name` in the path stand for the operation's `repo` input,
983 // so a `name` in the body, such as a check run's, is the body's own.
984 let stands_for_repo =
985 |name: &str| matches!(name, "owner" | "name") && path_params.contains(&"owner") && path_params.contains(&"name");
986 let covered = |name: &str| name == "repo" || (path_params.contains(&name) && !stands_for_repo(name));
987 let all_properties = op.properties();
988 let mut properties = all_properties.clone();
989 properties.retain(|name, _| !covered(name));
990 let required: Vec<String> = op
991 .required()
992 .into_iter()
993 .filter(|name| !covered(name))
994 .collect();
995
996 let mut parameters: Vec<Value> = path_params
997 .iter()
998 .map(|name| parameter(name, "path", true, if stands_for_repo(name) { None } else { all_properties.get(*name) }))
999 .collect();
1000 let mut body = Value::Null;
1001 if route.method == "GET" {
1002 for (name, key) in route.query {
1003 parameters.push(parameter(
1004 name,
1005 "query",
1006 required.iter().any(|required| required == key),
1007 properties.get(*key),
1008 ));
1009 }
1010 } else if !properties.is_empty() {
1011 let mut schema = json!({ "type": "object", "properties": properties });
1012 if !required.is_empty() {
1013 schema["required"] = json!(required);
1014 }
1015 body = json!({
1016 "required": !required.is_empty(),
1017 "content": { "application/json": { "schema": schema } },
1018 });
1019 }
1020
1021 let id = operation_id(route);
1022 let mut responses = Map::new();
1023 if route.no_content() {
1024 responses.insert("204".into(), json!({ "description": "Success. There is no body." }));
1025 } else {
1026 responses.insert(
1027 "200".into(),
1028 json!({
1029 "description": "Success.",
1030 "content": { "application/json": { "schema": {} } },
1031 }),
1032 );
1033 }
1034 responses.insert(
1035 "401".into(),
1036 error_response("A token is required, or the one sent is not valid."),
1037 );
1038 if let Some(reason) = may_need_payment(op) {
1039 responses.insert("402".into(), error_response(reason));
1040 }
1041 responses.insert(
1042 "403".into(),
1043 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."),
1044 );
1045 if !matches!(op, Op::Whoami | Op::ListRepos | Op::Search) {
1046 responses.insert("404".into(), error_response("It does not exist, or you cannot see it."));
1047 }
1048 if route.method != "GET" {
1049 responses.insert(
1050 "409".into(),
1051 error_response("The request conflicts with the current state."),
1052 );
1053 }
1054 if op != Op::Whoami {
1055 responses.insert("422".into(), error_response("The input is not valid."));
1056 }
1057 // Public data can be read without a token; everything else needs one.
1058 let scope: Vec<&str> = scope_for(op.name()).map(|scope| scope.as_str()).into_iter().collect();
1059 let security = if op.needs_user() {
1060 json!([{ "token": scope }])
1061 } else {
1062 json!([{ "token": scope }, {}])
1063 };
1064 let (tool, action) = crate::tools::TOOLS
1065 .iter()
1066 .find_map(|tool| {
1067 tool.actions
1068 .iter()
1069 .find(|action| action.op == op)
1070 .map(|action| (tool.name, action.name))
1071 })
1072 .unwrap_or_default();
1073 let mut described = json!({
1074 "operationId": id,
1075 "tags": [tag(op)],
1076 "summary": summary(route, &id),
1077 "description": op.description(),
1078 "x-operation": op.name(),
1079 "x-mcp-tool": tool,
1080 "x-mcp-action": action,
1081 "x-scope": scope.first().copied(),
1082 "security": security,
1083 "parameters": parameters,
1084 "responses": responses,
1085 });
1086 if !body.is_null() {
1087 described["requestBody"] = body;
1088 }
1089 described
1090}
1091
1092/// Entries for device sign-in, which is not an operation.
1093fn onboarding() -> Map<String, Value> {
1094 let paths = json!({
1095 "/device/code": {
1096 "post": {
1097 "operationId": "device_code",
1098 "tags": ["Accounts"],
1099 "summary": "Start signing in",
1100 "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`.",
1101 "security": [],
1102 "requestBody": {
1103 "content": { "application/json": { "schema": {
1104 "type": "object",
1105 "properties": {
1106 "client_name": {
1107 "type": "string",
1108 "description": "What is asking, shown to the person approving. For example, Claude Code.",
1109 },
1110 },
1111 } } },
1112 },
1113 "responses": { "200": {
1114 "description": "The codes for this sign-in.",
1115 "content": { "application/json": { "schema": {
1116 "type": "object",
1117 "properties": {
1118 "device_code": { "type": "string", "description": "Secret. Send it to /device/token." },
1119 "user_code": { "type": "string", "description": "Shown to the person, like WDJB-MJHT." },
1120 "verification_uri": { "type": "string" },
1121 "verification_uri_complete": {
1122 "type": "string",
1123 "description": "The link to give the person; it carries the code.",
1124 },
1125 "expires_in": { "type": "integer", "description": "Seconds until the codes expire." },
1126 "interval": { "type": "integer", "description": "Seconds to wait between polls." },
1127 },
1128 } } },
1129 } },
1130 },
1131 },
1132 "/device/token": {
1133 "post": {
1134 "operationId": "device_token",
1135 "tags": ["Accounts"],
1136 "summary": "Finish signing in",
1137 "description": "Asks whether the person has approved. Poll no faster than the interval. The token is returned once.",
1138 "security": [],
1139 "requestBody": {
1140 "required": true,
1141 "content": { "application/json": { "schema": {
1142 "type": "object",
1143 "required": ["device_code"],
1144 "properties": { "device_code": { "type": "string" } },
1145 } } },
1146 },
1147 "responses": { "200": {
1148 "description": "The state of the sign-in.",
1149 "content": { "application/json": { "schema": {
1150 "type": "object",
1151 "required": ["status"],
1152 "properties": {
1153 "status": { "type": "string", "enum": ["pending", "approved", "denied", "expired"] },
1154 "token": { "type": "string", "description": "Present when approved." },
1155 "username": { "type": "string", "description": "Lowercased: what the account is found and linked by." },
1156 "display_username": { "type": "string", "description": "The username as its owner wrote it; the same as username when they chose no case." },
1157 "verified": {
1158 "type": "boolean",
1159 "description": "Whether the account's email is confirmed.",
1160 },
1161 },
1162 } } },
1163 } },
1164 },
1165 },
1166 });
1167 match paths {
1168 Value::Object(paths) => paths,
1169 _ => Map::new(),
1170 }
1171}
1172
1173
1174/// Puts each operation's examples, where it has them, into its request
1175/// and response. Path and query values go under `x-example-params` and
1176/// `x-example-query`, which tools that build a request can use.
1177fn attach_examples(paths: &mut Map<String, Value>) {
1178 let examples = examples();
1179 for methods in paths.values_mut() {
1180 let Some(methods) = methods.as_object_mut() else { continue };
1181 for operation in methods.values_mut() {
1182 let id = operation["operationId"].as_str().unwrap_or_default().to_owned();
1183 let name = operation["x-operation"].as_str().unwrap_or_default().to_owned();
1184 let Some(example) = examples.get(&id).or_else(|| examples.get(&name)) else {
1185 continue;
1186 };
1187 if let Some(notes) = example.get("notes").and_then(Value::as_str) {
1188 let description = operation["description"].as_str().unwrap_or_default();
1189 operation["description"] = json!(format!("{description}\n\n{notes}"));
1190 }
1191 if let Some(response) = example.get("response") {
1192 let content = &mut operation["responses"]["200"]["content"]["application/json"];
1193 if content.is_object() {
1194 content["example"] = response.clone();
1195 }
1196 }
1197 if let Some(request) = example.get("request") {
1198 let content = &mut operation["requestBody"]["content"]["application/json"];
1199 if content.is_object() {
1200 content["example"] = request.clone();
1201 }
1202 }
1203 for (key, extension) in [("params", "x-example-params"), ("query", "x-example-query")] {
1204 if let Some(values) = example.get(key) {
1205 operation[extension] = values.clone();
1206 }
1207 }
1208 }
1209 }
1210}
1211
1212pub fn document() -> Value {
1213 let mut paths = onboarding();
1214 for route in ROUTES {
1215 let entry = paths
1216 .entry(openapi_path(route))
1217 .or_insert_with(|| json!({}));
1218 entry[route.method.to_lowercase()] = operation(route);
1219 }
1220 attach_examples(&mut paths);
1221 let tags: Vec<Value> = SECTIONS
1222 .iter()
1223 .map(|(name, description, ops)| {
1224 json!({
1225 "name": name,
1226 "description": description,
1227 // The section's operations in reading order, by MCP tool name.
1228 "x-tools": ops.iter().map(|op| op.name()).collect::<Vec<_>>(),
1229 })
1230 })
1231 .collect();
1232 let codes = ["unauthenticated", "payment_required", "forbidden", "not_found", "conflict", "invalid"];
1233 json!({
1234 "openapi": "3.1.0",
1235 "info": {
1236 "title": "g1t API",
1237 "version": "1",
1238 "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.",
1239 "license": { "name": "MIT", "identifier": "MIT" },
1240 },
1241 "servers": [{ "url": "https://api.g1t.sh" }],
1242 "security": [{ "token": [] }, {}],
1243 "tags": tags,
1244 "paths": paths,
1245 "components": {
1246 "securitySchemes": {
1247 "token": {
1248 "type": "http",
1249 "scheme": "bearer",
1250 "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.",
1251 },
1252 },
1253 "schemas": {
1254 "Error": {
1255 "type": "object",
1256 "required": ["error"],
1257 "properties": {
1258 "error": {
1259 "type": "object",
1260 "required": ["code", "message"],
1261 "properties": {
1262 "code": { "type": "string", "enum": codes },
1263 "message": { "type": "string" },
1264 "needed_scope": {
1265 "type": "string",
1266 "description": "On a 403 for an access token without the scope the call needs: that scope, such as `issues:write`.",
1267 },
1268 },
1269 },
1270 },
1271 },
1272 },
1273 },
1274 })
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279 use super::*;
1280
1281 #[test]
1282 fn every_route_is_documented_once() {
1283 let document = document();
1284 let mut ids = Vec::new();
1285 for (_, methods) in document["paths"].as_object().unwrap() {
1286 for (_, operation) in methods.as_object().unwrap() {
1287 ids.push(operation["operationId"].as_str().unwrap().to_owned());
1288 }
1289 }
1290 for op in Op::ALL {
1291 assert_eq!(
1292 ids.iter().filter(|id| *id == op.name()).count(),
1293 1,
1294 "{}",
1295 op.name()
1296 );
1297 }
1298 let mut unique = ids.clone();
1299 unique.sort();
1300 unique.dedup();
1301 assert_eq!(unique.len(), ids.len(), "operation ids repeat");
1302 }
1303
1304 #[test]
1305 fn path_and_query_inputs_are_not_repeated_in_the_body() {
1306 let document = document();
1307 let merge = &document["paths"]["/repos/{owner}/{name}/pulls/{number}/merge"]["post"];
1308 let body = &merge["requestBody"]["content"]["application/json"]["schema"]["properties"];
1309 assert!(body.get("keep_issue_open").is_some());
1310 assert!(body.get("repo").is_none() && body.get("number").is_none());
1311 let list = &document["paths"]["/repos"]["get"];
1312 assert_eq!(list["parameters"][0]["name"], "q");
1313 assert!(list.get("requestBody").is_none());
1314 }
1315
1316 #[test]
1317 fn every_operation_is_in_one_section() {
1318 for op in Op::ALL {
1319 let sections = SECTIONS
1320 .iter()
1321 .filter(|(_, _, ops)| ops.contains(&op))
1322 .count();
1323 assert_eq!(sections, 1, "{}", op.name());
1324 }
1325 }
1326
1327 #[test]
1328 fn titles_read_as_sentences() {
1329 assert_eq!(title(Op::CreateIssue), "Create an issue");
1330 assert_eq!(title(Op::Whoami), "Get the current user");
1331 }
1332
1333 #[test]
1334 fn every_operation_has_an_example_response() {
1335 let examples = examples();
1336 assert!(!examples.is_empty(), "reference.json does not parse");
1337 let document = document();
1338 let mut known = Vec::new();
1339 for (path, methods) in document["paths"].as_object().unwrap() {
1340 for (method, operation) in methods.as_object().unwrap() {
1341 known.push(operation["operationId"].as_str().unwrap().to_owned());
1342 // Nothing to show for a success without a body.
1343 if operation["responses"]["204"].is_object() {
1344 continue;
1345 }
1346 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
1347 assert!(!example.is_null(), "{method} {path} has no example response");
1348 }
1349 }
1350 for id in examples.keys() {
1351 assert!(known.contains(id), "reference.json names {id}, which is not an operation");
1352 }
1353 }
1354
1355 #[test]
1356 fn example_requests_send_only_what_the_body_takes() {
1357 let document = document();
1358 for (path, methods) in document["paths"].as_object().unwrap() {
1359 for (method, operation) in methods.as_object().unwrap() {
1360 let content = &operation["requestBody"]["content"]["application/json"];
1361 let Some(example) = content["example"].as_object() else { continue };
1362 let properties = &content["schema"]["properties"];
1363 for key in example.keys() {
1364 assert!(!properties[key].is_null(), "{method} {path}: {key} is not in the body");
1365 }
1366 }
1367 }
1368 }
1369
1370 /// The docs site's copy of the document. Run with `G1T_WRITE_OPENAPI=1`
1371 /// to rewrite it after changing an operation.
1372 #[test]
1373 fn the_docs_copy_is_current() {
1374 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../docs/src/data/openapi.json");
1375 let current = serde_json::to_string_pretty(&document()).unwrap() + "\n";
1376 if std::env::var_os("G1T_WRITE_OPENAPI").is_some() {
1377 std::fs::write(path, &current).unwrap();
1378 return;
1379 }
1380 let copy = std::fs::read_to_string(path).unwrap_or_default().replace("\r\n", "\n");
1381 assert!(
1382 copy == current,
1383 "apps/docs/src/data/openapi.json is out of date: run G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi"
1384 );
1385 }
1386
1387 /// The reference shows responses as they are sent: `snake_case`.
1388 #[test]
1389 fn example_responses_are_snake_case() {
1390 let document = document();
1391 for (path, methods) in document["paths"].as_object().unwrap() {
1392 for (method, operation) in methods.as_object().unwrap() {
1393 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
1394 let leaked = g1t_kit::wire::camel_case_keys(example);
1395 assert!(leaked.is_empty(), "{method} {path} shows {leaked:?}");
1396 }
1397 }
1398 }
1399
1400 /// Examples never hold anything that reads as a real credential, which
1401 /// secret scanners rightly flag in a public repository: they end in `…`
1402 /// after the prefix, as `whsec_…` and `g1t_…` do.
1403 #[test]
1404 fn examples_hold_no_real_looking_secrets() {
1405 let prefixes = ["whsec_", "g1t_", "g1tr_", "g1trt_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"];
1406 for (line, text) in REFERENCE.lines().enumerate() {
1407 for prefix in prefixes {
1408 let mut rest = text;
1409 while let Some(at) = rest.find(prefix) {
1410 let after = &rest[at + prefix.len()..];
1411 let run = after.chars().take_while(|c| c.is_ascii_alphanumeric()).count();
1412 assert!(
1413 run < 12,
1414 "reference.json line {}: `{prefix}` followed by {run} characters reads as a real secret; write `{prefix}…`",
1415 line + 1
1416 );
1417 rest = after;
1418 }
1419 }
1420 }
1421 }
1422}