Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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