Skip to content
1,123 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server in Rust; a public index at the API root1//! REST: each route maps an HTTP request onto one operation.
2
3use serde_json::{Map, Value};
4
Deployments wherever they run: API, g1t Actions environments, and a Deployments section5use crate::deployments::DeploymentsOp;
API and MCP server in Rust; a public index at the API root6use crate::operations::Op;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge7use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar8use crate::security::SecurityOp;
API and MCP server in Rust; a public index at the API root9
10pub struct Route {
11 pub method: &'static str,
12 /// Segments starting with `:` are parameters.
13 pub path: &'static str,
14 pub op: Op,
15 /// Query parameters the route reads, as `(name in the URL, input name)`.
16 pub query: &'static [(&'static str, &'static str)],
17}
18
19const fn route(
20 method: &'static str,
21 path: &'static str,
22 op: Op,
23 query: &'static [(&'static str, &'static str)],
24) -> Route {
25 Route {
26 method,
27 path,
28 op,
29 query,
30 }
31}
32
33pub const ROUTES: &[Route] = &[
Agents as a team: lifecycle, merge queue, billing and a new shell34 route("GET", "/user", Op::Whoami, &[]),
35 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
Merge branch 'worktree-agent-ad7c6d88d93adc817'36 route("GET", "/workspaces/:workspace", Op::GetWorkspace, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look37 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
38 route("GET", "/user/emails", Op::ListEmails, &[]),
39 route("POST", "/user/emails", Op::AddEmail, &[]),
40 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
41 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
42 route("GET", "/user/invites", Op::ListInvites, &[]),
43 route("POST", "/user/invites", Op::CreateInvite, &[]),
44 route("DELETE", "/user/invites/:id", Op::RevokeInvite, &[]),
45 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
46 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
47 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
48 // Who has access. GitHub's addresses, but for adding someone, which
49 // takes an email address as well as a username.
50 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
51 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
52 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
53 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
54 route(
55 "GET",
56 "/repos/:owner/:name/collaborators/:username/permission",
57 Op::GetCollaboratorPermission,
58 &[],
59 ),
60 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
61 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
62 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
API: notifications over REST and MCP, with notifications scopes63 // Your notifications: threads, marking them, and what you subscribe
64 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
65 route("GET", "/notifications", Op::ListNotifications, &[("all", "all"), ("participating", "participating"), ("view", "view"), ("reason", "reason"), ("severity", "severity"), ("since", "since"), ("before", "before"), ("cursor", "cursor"), ("per_page", "per_page")]),
66 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
67 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
68 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
69 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
70 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
71 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
72 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
73 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
74 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
75 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
76 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
77 route("GET", "/repos/:owner/:name/notifications", Op::ListNotifications, &[("all", "all"), ("participating", "participating"), ("view", "view"), ("reason", "reason"), ("severity", "severity"), ("since", "since"), ("before", "before"), ("cursor", "cursor"), ("per_page", "per_page")]),
78 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
79 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
80 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
81 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
82 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
83 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
84 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
85 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
API: pinned projects over REST and MCP86 // Your pinned projects in a workspace, in your order.
87 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
88 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
89 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
90 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
92 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily93 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
94 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look95 route(
96 "GET",
97 "/workspaces/:workspace/outside_collaborators",
98 Op::ListOutsideCollaborators,
99 &[],
100 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar101 // Teams: a workspace's groups of members, with roles on repositories.
102 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
103 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
104 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
105 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
106 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
107 route(
108 "GET",
109 "/workspaces/:workspace/teams/:team/members",
110 Op::ListTeamMembers,
111 &[("include_child_teams", "include_child_teams")],
112 ),
113 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
114 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
115 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
116 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
117 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
118 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
119 route(
120 "PUT",
121 "/workspaces/:workspace/teams/:team/review_assignment",
122 Op::SetTeamReviewAssignment,
123 &[],
124 ),
125 route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit126 // A workspace's billing: usage, budget, AI credit and invoices.
127 route(
128 "GET",
129 "/workspaces/:workspace/usage",
130 Op::GetUsage,
131 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
132 ),
133 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
134 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
135 route("GET", "/workspaces/:workspace/ai_credit", Op::GetAiCredit, &[]),
136 route("POST", "/workspaces/:workspace/ai_credit/checkout", Op::BuyAiCredit, &[]),
137 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
138 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens139 // The AI Gateway's log of a workspace's requests.
140 route("GET", "/workspaces/:workspace/gateway/requests", Op::ListGatewayRequests, &[("limit", "limit"), ("before", "before")]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar141 // Code owners: the CODEOWNERS file, checked.
142 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily143 // Security alerts: secrets and vulnerable dependencies.
144 route(
145 "GET",
146 "/repos/:owner/:name/security/alerts",
147 Op::ListSecurityAlerts,
148 &[("state", "state"), ("kind", "kind")],
149 ),
150 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
151 route("POST", "/repos/:owner/:name/security/alerts/:id/reopen", Op::ReopenSecurityAlert, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar152 // The security suite: secret scanning, code scanning, vulnerability
153 // alerts and the supply chain, at the common addresses.
154 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
155 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
156 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
157 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
158 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
159 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
160 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
161 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
162 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
163 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
164 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
165 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
166 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
167 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
168 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
169 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
170 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
171 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
172 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
173 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
174 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
175 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
176 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
177 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
178 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
179 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
180 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
181 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
182 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
183 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
184 route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]),
185 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
186 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
187 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
188 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
189 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
190 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
191 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
192 route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
Agents as a team: lifecycle, merge queue, billing and a new shell193 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
Search across all of g1t, Explore, and a command palette194 route(
195 "GET",
196 "/search",
197 Op::Search,
198 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
199 ),
Agents as a team: lifecycle, merge queue, billing and a new shell200 route("POST", "/repos", Op::CreateRepo, &[]),
201 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
202 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look203 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
204 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
205 route(
206 "GET",
207 "/workspaces/:workspace/repos/deleted",
208 Op::ListDeletedRepos,
209 &[],
210 ),
211 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
212 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
213 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
214 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
215 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
216 route(
217 "POST",
218 "/repos/:owner/:name/visibility",
219 Op::SetRepoVisibility,
220 &[],
221 ),
222 route(
223 "POST",
224 "/repos/:owner/:name/branches/:branch/rename",
225 Op::RenameBranch,
226 &[],
227 ),
Agents as a team: lifecycle, merge queue, billing and a new shell228 route(
229 "GET",
230 "/repos/:owner/:name/settings",
231 Op::GetRepoSettings,
232 &[],
233 ),
234 route(
235 "PATCH",
236 "/repos/:owner/:name/settings",
237 Op::UpdateRepoSettings,
238 &[],
239 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents240 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge241 // Rulesets: a repository's, a workspace's, the rules of one branch,
242 // and how they judged pushes and merges.
243 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
244 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
245 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
246 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
247 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
248 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
249 route(
250 "GET",
251 "/repos/:owner/:name/rules/evaluations",
252 Op::Rules(RulesOp::ListRuleEvaluations),
253 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
254 ),
255 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
256 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
257 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
258 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
259 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
260 route(
261 "GET",
262 "/workspaces/:workspace/rules/evaluations",
263 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
264 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
265 ),
Deployments wherever they run: API, g1t Actions environments, and a Deployments section266 // Deployments wherever they run, their statuses, and environments.
267 route(
268 "GET",
269 "/repos/:owner/:name/deployments",
270 Op::Deployments(DeploymentsOp::ListDeployments),
271 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
272 ),
273 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
274 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
275 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
276 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
277 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
278 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Agents as a team: lifecycle, merge queue, billing and a new shell279 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
API and MCP server in Rust; a public index at the API root280 route(
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request281 "POST",
282 "/repos/:owner/:name/pulls/:number/messages",
283 Op::MessageAgent,
284 &[],
285 ),
286 route(
287 "POST",
288 "/repos/:owner/:name/pulls/:number/messages/take",
289 Op::TakeMessages,
290 &[],
291 ),
292 route(
Docs worth reading, and kept that way293 "POST",
294 "/repos/:owner/:name/messages/:id/answer",
295 Op::AnswerMessage,
296 &[],
297 ),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains298 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
299 route(
300 "GET",
301 "/repos/:owner/:name/memory",
302 Op::Recall,
303 &[("q", "query"), ("limit", "limit")],
304 ),
Docs worth reading, and kept that way305 route(
API and MCP server in Rust; a public index at the API root306 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell307 "/repos/:owner/:name/events",
API and MCP server in Rust; a public index at the API root308 Op::ListEvents,
309 &[("before", "before")],
310 ),
Agents as a team: lifecycle, merge queue, billing and a new shell311 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar312 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
313 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
314 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
315 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
316 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
317 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
318 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
319 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
320 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
321 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
322 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
323 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
324 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
325 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
API and MCP server in Rust; a public index at the API root326 route(
327 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell328 "/repos/:owner/:name/issues",
API and MCP server in Rust; a public index at the API root329 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar330 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
API and MCP server in Rust; a public index at the API root331 ),
Agents as a team: lifecycle, merge queue, billing and a new shell332 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
API and MCP server in Rust; a public index at the API root333 route(
334 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell335 "/repos/:owner/:name/issues/:number",
API and MCP server in Rust; a public index at the API root336 Op::GetIssue,
337 &[],
338 ),
339 route(
340 "PATCH",
Agents as a team: lifecycle, merge queue, billing and a new shell341 "/repos/:owner/:name/issues/:number",
API and MCP server in Rust; a public index at the API root342 Op::UpdateIssue,
343 &[],
344 ),
345 route(
346 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell347 "/repos/:owner/:name/issues/:number/close",
API and MCP server in Rust; a public index at the API root348 Op::CloseIssue,
349 &[],
350 ),
351 route(
352 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell353 "/repos/:owner/:name/issues/:number/reopen",
API and MCP server in Rust; a public index at the API root354 Op::ReopenIssue,
355 &[],
356 ),
357 route(
358 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell359 "/repos/:owner/:name/issues/:number/assign",
360 Op::AssignIssue,
361 &[],
362 ),
Integrations: your own model provider, alerts that open issues, tickets agents read363 route(
364 "POST",
365 "/repos/:owner/:name/issues/import",
366 Op::ImportIssue,
367 &[],
368 ),
369 route(
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step370 "POST",
371 "/repos/:owner/:name/issues/delegate",
372 Op::Delegate,
373 &[],
374 ),
375 route(
Integrations: your own model provider, alerts that open issues, tickets agents read376 "GET",
377 "/repos/:owner/:name/context",
378 Op::GetContext,
379 &[("reference", "reference")],
380 ),
381 route(
382 "GET",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API383 "/workspaces/:workspace/context/search",
384 Op::SearchContext,
385 &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
386 ),
387 route(
388 "GET",
389 "/workspaces/:workspace/context/:kind/:id",
390 Op::GetEntity,
391 &[],
392 ),
393 route(
394 "GET",
Integrations: your own model provider, alerts that open issues, tickets agents read395 "/workspaces/:workspace/integrations",
396 Op::ListIntegrations,
397 &[],
398 ),
399 route(
400 "POST",
401 "/workspaces/:workspace/integrations",
402 Op::ConnectIntegration,
403 &[],
404 ),
405 route(
Models per workspace: several providers, routed by kind of work406 "GET",
Webhooks: every event, to your own addresses, signed and retried407 "/repos/:owner/:name/hooks",
408 Op::ListWebhooks,
409 &[],
410 ),
411 route(
412 "POST",
413 "/repos/:owner/:name/hooks",
414 Op::CreateWebhook,
415 &[],
416 ),
417 route(
418 "PATCH",
419 "/repos/:owner/:name/hooks/:id",
420 Op::UpdateWebhook,
421 &[],
422 ),
423 route(
424 "DELETE",
425 "/repos/:owner/:name/hooks/:id",
426 Op::DeleteWebhook,
427 &[],
428 ),
429 route(
430 "POST",
431 "/repos/:owner/:name/hooks/:id/pings",
432 Op::PingWebhook,
433 &[],
434 ),
435 route(
436 "GET",
437 "/repos/:owner/:name/hooks/:id/deliveries",
438 Op::ListWebhookDeliveries,
439 &[],
440 ),
441 route(
442 "POST",
443 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
444 Op::RedeliverWebhook,
445 &[],
446 ),
447 route(
448 "GET",
449 "/workspaces/:workspace/hooks",
450 Op::ListWebhooks,
451 &[],
452 ),
453 route(
454 "POST",
455 "/workspaces/:workspace/hooks",
456 Op::CreateWebhook,
457 &[],
458 ),
459 route(
460 "PATCH",
461 "/workspaces/:workspace/hooks/:id",
462 Op::UpdateWebhook,
463 &[],
464 ),
465 route(
466 "DELETE",
467 "/workspaces/:workspace/hooks/:id",
468 Op::DeleteWebhook,
469 &[],
470 ),
471 route(
472 "POST",
473 "/workspaces/:workspace/hooks/:id/pings",
474 Op::PingWebhook,
475 &[],
476 ),
477 route(
478 "GET",
479 "/workspaces/:workspace/hooks/:id/deliveries",
480 Op::ListWebhookDeliveries,
481 &[],
482 ),
483 route(
484 "POST",
485 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
486 Op::RedeliverWebhook,
487 &[],
488 ),
489 route(
490 "GET",
Models per workspace: several providers, routed by kind of work491 "/workspaces/:workspace/model-routes",
492 Op::GetModelRoutes,
493 &[],
494 ),
495 route(
496 "PUT",
497 "/workspaces/:workspace/model-routes",
498 Op::SetModelRoutes,
499 &[],
500 ),
501 route(
Integrations: your own model provider, alerts that open issues, tickets agents read502 "DELETE",
503 "/workspaces/:workspace/integrations/:id",
504 Op::DisconnectIntegration,
505 &[],
506 ),
507 route(
508 "POST",
509 "/workspaces/:workspace/integrations/:id/test",
510 Op::TestIntegration,
511 &[],
512 ),
Automations: rules in .g1t/automations that act when something happens513 route(
514 "GET",
GitHub Actions on g1t, part two: running workflows515 "/repos/:owner/:name/actions/workflows",
516 Op::ListWorkflows,
517 &[],
518 ),
519 route(
520 "GET",
521 "/repos/:owner/:name/actions/workflows/:workflow/runs",
522 Op::ListWorkflowRuns,
523 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
524 ),
525 route(
526 "POST",
527 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
528 Op::DispatchWorkflow,
529 &[],
530 ),
531 route(
532 "PATCH",
533 "/repos/:owner/:name/actions/workflows/:workflow",
534 Op::UpdateWorkflow,
535 &[],
536 ),
537 route(
538 "PUT",
539 "/repos/:owner/:name/actions/workflows/:workflow/enable",
540 Op::UpdateWorkflow,
541 &[],
542 ),
543 route(
544 "PUT",
545 "/repos/:owner/:name/actions/workflows/:workflow/disable",
546 Op::UpdateWorkflow,
547 &[],
548 ),
549 route(
550 "GET",
551 "/repos/:owner/:name/actions/runs",
552 Op::ListWorkflowRuns,
553 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
554 ),
555 route(
556 "GET",
557 "/repos/:owner/:name/actions/runs/:id",
558 Op::GetWorkflowRun,
559 &[],
560 ),
561 route(
562 "POST",
563 "/repos/:owner/:name/actions/runs/:id/cancel",
564 Op::CancelWorkflowRun,
565 &[],
566 ),
567 route(
568 "POST",
569 "/repos/:owner/:name/actions/runs/:id/rerun",
570 Op::RerunWorkflowRun,
571 &[],
572 ),
573 route(
574 "POST",
575 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
576 Op::RerunWorkflowRun,
577 &[],
578 ),
579 route(
580 "GET",
581 "/repos/:owner/:name/actions/jobs/:job/logs",
582 Op::GetJobLogs,
583 &[("after", "after")],
584 ),
585 route(
586 "GET",
587 "/repos/:owner/:name/actions/secrets",
588 Op::ListActionsSecrets,
589 &[],
590 ),
591 route(
592 "PUT",
593 "/repos/:owner/:name/actions/secrets/:setting",
594 Op::SetActionsSecret,
595 &[],
596 ),
597 route(
598 "DELETE",
599 "/repos/:owner/:name/actions/secrets/:setting",
600 Op::DeleteActionsSecret,
601 &[],
602 ),
603 route(
604 "GET",
605 "/repos/:owner/:name/actions/variables",
606 Op::ListActionsVariables,
607 &[],
608 ),
609 route(
610 "POST",
611 "/repos/:owner/:name/actions/variables",
612 Op::SetActionsVariable,
613 &[],
614 ),
615 route(
616 "PATCH",
617 "/repos/:owner/:name/actions/variables/:setting",
618 Op::SetActionsVariable,
619 &[],
620 ),
621 route(
622 "DELETE",
623 "/repos/:owner/:name/actions/variables/:setting",
624 Op::DeleteActionsVariable,
625 &[],
626 ),
627 route(
628 "GET",
629 "/workspaces/:workspace/actions/secrets",
630 Op::ListActionsSecrets,
631 &[],
632 ),
633 route(
634 "PUT",
635 "/workspaces/:workspace/actions/secrets/:setting",
636 Op::SetActionsSecret,
637 &[],
638 ),
639 route(
640 "DELETE",
641 "/workspaces/:workspace/actions/secrets/:setting",
642 Op::DeleteActionsSecret,
643 &[],
644 ),
645 route(
646 "GET",
647 "/workspaces/:workspace/actions/variables",
648 Op::ListActionsVariables,
649 &[],
650 ),
651 route(
652 "POST",
653 "/workspaces/:workspace/actions/variables",
654 Op::SetActionsVariable,
655 &[],
656 ),
657 route(
658 "PATCH",
659 "/workspaces/:workspace/actions/variables/:setting",
660 Op::SetActionsVariable,
661 &[],
662 ),
663 route(
664 "DELETE",
665 "/workspaces/:workspace/actions/variables/:setting",
666 Op::DeleteActionsVariable,
667 &[],
668 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents669 // Self-hosted runners: a repository's own, or a workspace's.
670 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
671 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
672 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
673 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
674 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
675 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
676 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
677 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
678 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
679 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
680 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
681 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
682 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
683 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
Agents as a team: lifecycle, merge queue, billing and a new shell684 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
685 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
686 route(
687 "POST",
688 "/repos/:owner/:name/plans/:plan/apply",
689 Op::ApplyPlan,
690 &[],
691 ),
692 route(
693 "POST",
694 "/repos/:owner/:name/issues/:number/comments",
API and MCP server in Rust; a public index at the API root695 Op::AddComment,
696 &[],
697 ),
698 route(
699 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell700 "/repos/:owner/:name/pulls",
API and MCP server in Rust; a public index at the API root701 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar702 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
API and MCP server in Rust; a public index at the API root703 ),
704 route(
705 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell706 "/repos/:owner/:name/pulls",
API and MCP server in Rust; a public index at the API root707 Op::CreatePullRequest,
708 &[],
709 ),
710 route(
711 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell712 "/repos/:owner/:name/pulls/:number",
API and MCP server in Rust; a public index at the API root713 Op::GetPullRequest,
714 &[],
715 ),
716 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar717 "PATCH",
718 "/repos/:owner/:name/pulls/:number",
719 Op::UpdatePullRequest,
720 &[],
721 ),
722 route(
API and MCP server in Rust; a public index at the API root723 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell724 "/repos/:owner/:name/pulls/:number/changes",
API and MCP server in Rust; a public index at the API root725 Op::GetPullRequestChanges,
726 &[],
727 ),
728 route(
Acceptance checks in sandboxes, line comments and review verdicts729 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell730 "/repos/:owner/:name/pulls/:number/reviews",
Acceptance checks in sandboxes, line comments and review verdicts731 Op::ReviewPullRequest,
732 &[],
733 ),
734 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar735 "POST",
736 "/repos/:owner/:name/pulls/:number/requested_reviewers",
737 Op::RequestReviewers,
738 &[],
739 ),
740 route(
741 "DELETE",
742 "/repos/:owner/:name/pulls/:number/requested_reviewers",
743 Op::RemoveRequestedReviewers,
744 &[],
745 ),
746 route(
API and MCP server in Rust; a public index at the API root747 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell748 "/repos/:owner/:name/pulls/:number/session",
API and MCP server in Rust; a public index at the API root749 Op::ReadSession,
750 &[("after", "after")],
751 ),
752 route(
753 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell754 "/repos/:owner/:name/pulls/:number/session",
API and MCP server in Rust; a public index at the API root755 Op::RecordSession,
756 &[],
757 ),
758 route(
759 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell760 "/repos/:owner/:name/pulls/:number/ready",
API and MCP server in Rust; a public index at the API root761 Op::MarkPullRequestReady,
762 &[],
763 ),
764 route(
765 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell766 "/repos/:owner/:name/pulls/:number/close",
API and MCP server in Rust; a public index at the API root767 Op::ClosePullRequest,
768 &[],
769 ),
770 route(
771 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell772 "/repos/:owner/:name/pulls/:number/merge",
API and MCP server in Rust; a public index at the API root773 Op::MergePullRequest,
774 &[],
775 ),
776];
777
778impl Route {
779 /// The names of the route's path parameters, in order.
780 pub fn params(&self) -> impl Iterator<Item = &'static str> {
781 self.path
782 .split('/')
783 .filter_map(|segment| segment.strip_prefix(':'))
784 }
785
786 /// The values of the path parameters, if `path` is this route's.
787 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
788 let mut values = Vec::new();
789 let mut actual = path.trim_end_matches('/').split('/');
790 for expected in self.path.split('/') {
791 let segment = actual.next()?;
792 match expected.strip_prefix(':') {
793 Some(name) if !segment.is_empty() => values.push((name, segment)),
794 Some(_) => return None,
795 None if expected == segment => {}
796 None => return None,
797 }
798 }
799 actual.next().is_none().then_some(values)
800 }
801}
802
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look803/// A path segment with its `%XX` escapes decoded; as given when that is not
804/// UTF-8.
805fn percent_decoded(segment: &str) -> String {
806 let bytes = segment.as_bytes();
807 let mut out = Vec::with_capacity(bytes.len());
808 let mut i = 0;
809 while i < bytes.len() {
810 let escaped = (bytes[i] == b'%')
811 .then(|| segment.get(i + 1..i + 3))
812 .flatten()
813 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
814 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
815 match escaped {
816 Some(byte) => {
817 out.push(byte);
818 i += 3;
819 }
820 None => {
821 out.push(bytes[i]);
822 i += 1;
823 }
824 }
825 }
826 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
827}
828
API and MCP server in Rust; a public index at the API root829/// The route for a request, and the operation input it describes.
830///
831/// The input is the JSON body, overlaid with the query parameters the route
832/// reads and then with what the path names: `owner` and `name` become
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar833/// `repo`, as does a team's `repo` with its `workspace`, and `number`
834/// becomes an integer.
API and MCP server in Rust; a public index at the API root835pub fn resolve(
836 method: &str,
837 path: &str,
838 query: &[(String, String)],
839 body: Value,
840) -> Option<(&'static Route, Value)> {
841 let (route, params) = ROUTES
842 .iter()
843 .filter(|route| route.method == method)
844 .find_map(|route| Some((route, route.matches(path)?)))?;
845
846 let mut input = match body {
847 Value::Object(fields) => fields,
848 _ => Map::new(),
849 };
850 for (name, key) in route.query {
851 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
852 input.insert((*key).to_owned(), Value::String(value.clone()));
853 }
854 }
855 let param = |wanted: &str| {
856 params
857 .iter()
858 .find(|(name, _)| *name == wanted)
859 .map(|(_, value)| *value)
860 };
861 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
862 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
863 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar864 for key in ["plan", "id", "workspace", "delivery", "workflow", "job", "setting", "username", "team", "basehead"] {
Docs worth reading, and kept that way865 if let Some(value) = param(key) {
866 input.insert(key.to_owned(), Value::String(value.to_owned()));
867 }
Agents as a team: lifecycle, merge queue, billing and a new shell868 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar869 // A repository of a team's workspace, named by itself.
870 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
871 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
872 }
873 // A branch name may hold slashes, sent URL-encoded as one segment, and
874 // a label's name spaces.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look875 if let Some(branch) = param("branch") {
876 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
877 }
Deployments wherever they run: API, g1t Actions environments, and a Deployments section878 // An environment's name may hold slashes and spaces, URL-encoded.
879 if let Some(environment) = param("environment") {
880 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
881 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar882 if let Some(label) = param("label") {
883 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
884 }
885 if let Some(milestone) = param("milestone") {
886 // Not a number: zero, which no milestone has.
887 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
888 }
GitHub Actions on g1t, part two: running workflows889 // GitHub says some things with the path alone.
890 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
891 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
892 }
893 if route.path.ends_with("/rerun-failed-jobs") {
894 input.insert("failed_only".to_owned(), Value::Bool(true));
895 }
API: notifications over REST and MCP, with notifications scopes896 // Unsaving and waking a thread are a DELETE of what PUT made.
897 if route.method == "DELETE" && route.path.ends_with("/saved") {
898 input.insert("saved".to_owned(), Value::Bool(false));
899 }
900 if route.method == "DELETE" && route.path.ends_with("/snooze") {
901 input.remove("until");
902 }
API and MCP server in Rust; a public index at the API root903 if let Some(number) = param("number") {
904 // Not a number: zero, which no issue or pull request has.
905 input.insert(
906 "number".to_owned(),
907 number.parse::<u32>().unwrap_or(0).into(),
908 );
909 }
910 Some((route, Value::Object(input)))
911}
912
913#[cfg(test)]
914mod tests {
915 use serde_json::json;
916
917 use super::*;
918
919 #[test]
920 fn a_path_resolves_to_its_operation_and_input() {
921 let (route, input) = resolve(
922 "POST",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look923 "/repos/flagon-io/hello/pulls/14/merge",
API and MCP server in Rust; a public index at the API root924 &[],
925 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
926 )
927 .unwrap();
928 assert_eq!(route.op, Op::MergePullRequest);
929 // What the path names wins over the body.
930 assert_eq!(
931 input,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look932 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
API and MCP server in Rust; a public index at the API root933 );
934 }
935
936 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look937 fn a_branch_with_slashes_is_one_encoded_segment() {
938 let (route, input) = resolve(
939 "POST",
940 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
941 &[],
942 json!({ "new_name": "feature/sign-in" }),
943 )
944 .unwrap();
945 assert_eq!(route.op, Op::RenameBranch);
946 assert_eq!(
947 input,
948 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
949 );
950 assert_eq!(percent_decoded("100%"), "100%");
951 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
952 }
953
954 #[test]
955 fn a_collaborator_is_named_by_username() {
956 let (route, input) = resolve(
957 "PATCH",
958 "/repos/flagon-io/hello/collaborators/ada",
959 &[],
960 json!({ "role": "maintain" }),
961 )
962 .unwrap();
963 assert_eq!(route.op, Op::UpdateCollaborator);
964 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
965 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
966 assert_eq!(route.op, Op::DeclineRepoInvitation);
967 assert_eq!(input, json!({ "id": "rin_1" }));
968 }
969
970 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar971 fn teams_are_addressed_by_workspace_and_slug() {
972 let query = [("q".to_owned(), "back".to_owned())];
973 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
974 assert_eq!(route.op, Op::ListTeams);
975 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
976 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
977 assert_eq!(route.op, Op::UpdateTeam);
978 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
979 let (route, input) =
980 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
981 assert_eq!(route.op, Op::SetTeamMember);
982 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
983 let query = [("include_child_teams".to_owned(), "true".to_owned())];
984 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
985 assert_eq!(route.op, Op::ListTeamMembers);
986 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
987 // A repository is named by itself, in the team's workspace.
988 let (route, input) =
989 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
990 assert_eq!(route.op, Op::SetTeamRepo);
991 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
992 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
993 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
994 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
995 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
996 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
997 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
998 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
999 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1000 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1001 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1002 assert_eq!(route.op, Op::ListUserTeams);
1003 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1004 }
1005
1006 #[test]
1007 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1008 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1009 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1010 assert_eq!(route.op, Op::RequestReviewers);
1011 assert_eq!(
1012 input,
1013 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1014 );
1015 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1016 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1017 let query = [("ref".to_owned(), "main".to_owned())];
1018 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1019 assert_eq!(route.op, Op::GetCodeownersErrors);
1020 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1021 }
1022
1023 #[test]
API: notifications over REST and MCP, with notifications scopes1024 fn notifications_are_addressed_as_threads_and_by_issue() {
1025 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1026 assert_eq!(route.op, Op::MarkThreadDone);
1027 assert_eq!(input, json!({ "id": "ntf_1" }));
1028 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1029 assert_eq!(route.op, Op::SaveThread);
1030 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1031 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1032 assert_eq!(route.op, Op::SnoozeThread);
1033 assert_eq!(input, json!({ "id": "ntf_1" }));
1034 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1035 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1036 assert_eq!(route.op, Op::ListNotifications);
1037 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1038 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1039 assert_eq!(route.op, Op::SetThreadSubscription);
1040 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1041 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1042 }
1043
1044 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1045 fn labels_and_milestones_are_named_in_the_path() {
1046 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1047 assert_eq!(route.op, Op::UpdateLabel);
1048 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1049 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1050 assert_eq!(route.op, Op::RemoveIssueLabels);
1051 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1052 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1053 assert_eq!(route.op, Op::RemoveIssueLabels);
1054 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1055 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1056 assert_eq!(route.op, Op::AddDefaultLabels);
1057 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1058 assert_eq!(route.op, Op::UpdateMilestone);
1059 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1060 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1061 assert_eq!(route.op, Op::UpdatePullRequest);
1062 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1063 }
1064
1065 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1066 fn billing_is_addressed_by_workspace() {
1067 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1068 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1069 assert_eq!(route.op, Op::GetUsage);
1070 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1071 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1072 assert_eq!(route.op, Op::SetBudget);
1073 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1074 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1075 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1076 assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit);
1077 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
1078 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1079 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1080 assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1081 }
1082
1083 #[test]
API and MCP server in Rust; a public index at the API root1084 fn query_parameters_are_renamed() {
1085 let query = [
1086 ("q".to_owned(), "parser".to_owned()),
1087 ("x".to_owned(), "y".to_owned()),
1088 ];
Agents as a team: lifecycle, merge queue, billing and a new shell1089 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
API and MCP server in Rust; a public index at the API root1090 assert_eq!(route.op, Op::ListRepos);
1091 assert_eq!(input, json!({ "query": "parser" }));
1092 }
1093
1094 #[test]
1095 fn method_and_shape_must_match() {
Agents as a team: lifecycle, merge queue, billing and a new shell1096 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1097 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1098 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1099 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
API and MCP server in Rust; a public index at the API root1100 }
1101
1102 #[test]
1103 fn every_parameter_and_query_name_is_an_input() {
1104 for route in ROUTES {
1105 let properties = route.op.properties();
1106 for (_, key) in route.query {
1107 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1108 }
1109 for name in route.params() {
1110 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1111 || properties.contains_key(name);
1112 assert!(covered, "{}: {name}", route.path);
1113 }
1114 }
1115 }
1116
1117 #[test]
1118 fn every_operation_has_a_route() {
1119 for op in Op::ALL {
1120 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1121 }
1122 }
1123}

This file's history is long; its oldest lines are credited to the oldest commit read.