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