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