Skip to content
1,434 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.

Integrations: your own model provider, alerts that open issues, tickets agents read1//! REST: each route maps an HTTP request onto one operation.
2
3use serde_json::{Map, Value};
4
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb975use crate::about::AboutOp;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R26use crate::artifacts::ArtifactsOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca7use crate::deploy_keys::DeployKeysOp;
Merge branch 'mirroring' into artifacts-mode8use crate::mirrors::MirrorsOp;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb979use crate::deployments::DeploymentsOp;
Merge packages: roles, Actions access, source label, soft delete, API10use crate::packages::PackagesOp;
The API answers for artifacts: the artifact MCP tool and /workspaces/{workspace}/artifacts list, search, read and edit a workspace's docs as the person sees them, make, move, trash, restore and purge them, show their versions and who can open them, and share them, each action offered only to tokens whose artifacts scope allows it, through a DOCS binding to the docs service, with the OpenAPI document and its examples.11use crate::folios::FoliosOp;
Merge branch 'worktree-agent-a3abfcce648e87dca'12use crate::protection::ProtectionOp;
API and MCP for a workspace's personal access token rules, members' tokens and approvals13use crate::token_policy::TokenOp;
Integrations: your own model provider, alerts that open issues, tickets agents read14use crate::operations::Op;
Merge checks: statuses and check runs on every commit15use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge16use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar17use crate::security::SecurityOp;
Integrations: your own model provider, alerts that open issues, tickets agents read18
19pub struct Route {
20 pub method: &'static str,
21 /// Segments starting with `:` are parameters.
22 pub path: &'static str,
23 pub op: Op,
24 /// Query parameters the route reads, as `(name in the URL, input name)`.
25 pub query: &'static [(&'static str, &'static str)],
26}
27
28const fn route(
29 method: &'static str,
30 path: &'static str,
31 op: Op,
32 query: &'static [(&'static str, &'static str)],
33) -> Route {
34 Route {
35 method,
36 path,
37 op,
38 query,
39 }
40}
41
42pub const ROUTES: &[Route] = &[
43 route("GET", "/user", Op::Whoami, &[]),
44 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
Merge branch 'worktree-agent-ad7c6d88d93adc817'45 route("GET", "/workspaces/:workspace", Op::GetWorkspace, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
47 route("GET", "/user/emails", Op::ListEmails, &[]),
48 route("POST", "/user/emails", Op::AddEmail, &[]),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)49 route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
51 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
52 route("GET", "/user/invites", Op::ListInvites, &[]),
53 route("POST", "/user/invites", Op::CreateInvite, &[]),
54 route("DELETE", "/user/invites/:id", Op::RevokeInvite, &[]),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)55 // Invitations to workspaces waiting for your answer.
56 route("GET", "/user/invitations", Op::ListInvitations, &[]),
57 route("POST", "/user/invitations/:id/accept", Op::AcceptInvitation, &[]),
58 route("POST", "/user/invitations/:id/decline", Op::DeclineInvitation, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals60 // A workspace's rules for personal access tokens, and its members' tokens.
61 route("GET", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::GetTokenPolicy), &[]),
62 route("PATCH", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::SetTokenPolicy), &[]),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers63 route("GET", "/workspaces/:workspace/personal-access-tokens", Op::Tokens(TokenOp::ListMemberTokens), &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals64 route("POST", "/workspaces/:workspace/personal-access-tokens/:id", Op::Tokens(TokenOp::RevokeMemberToken), &[]),
65 route("GET", "/workspaces/:workspace/personal-access-token-requests", Op::Tokens(TokenOp::ListTokenRequests), &[]),
66 route("POST", "/workspaces/:workspace/personal-access-token-requests/:id", Op::Tokens(TokenOp::ReviewTokenRequest), &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
68 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
69 // Who has access. GitHub's addresses, but for adding someone, which
70 // takes an email address as well as a username.
71 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
72 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
73 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
74 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
75 route(
76 "GET",
77 "/repos/:owner/:name/collaborators/:username/permission",
78 Op::GetCollaboratorPermission,
79 &[],
80 ),
81 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
82 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
83 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca84 // Deploy keys, at GitHub's addresses.
85 route("GET", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::ListDeployKeys), &[]),
86 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
87 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
88 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
Merge branch 'mirroring' into artifacts-mode89 // Mirroring: a repository's remotes, takeovers and hand-backs.
90 route("GET", "/repos/:owner/:name/mirror", Op::Mirrors(MirrorsOp::GetMirror), &[]),
91 route("GET", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::GetHandBackPlan), &[]),
92 route("POST", "/repos/:owner/:name/mirror/take-over", Op::Mirrors(MirrorsOp::TakeOver), &[]),
93 route("POST", "/repos/:owner/:name/mirror/ci", Op::Mirrors(MirrorsOp::SetCiFailover), &[]),
94 route("POST", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::HandBack), &[]),
95 route("POST", "/repos/:owner/:name/mirror/move-to-g1t", Op::Mirrors(MirrorsOp::MoveToG1t), &[]),
96 route("POST", "/repos/:owner/:name/mirror/sync", Op::Mirrors(MirrorsOp::SyncMirror), &[]),
97 route("POST", "/repos/:owner/:name/mirror/remotes", Op::Mirrors(MirrorsOp::AddRemote), &[]),
98 route("PATCH", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::UpdateRemote), &[]),
99 route("DELETE", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::RemoveRemote), &[]),
API: notifications over REST and MCP, with notifications scopes100 // Your notifications: threads, marking them, and what you subscribe
101 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
102 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")]),
103 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
104 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
105 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
106 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
107 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
108 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
109 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
110 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
111 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
112 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
113 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
114 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")]),
115 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
116 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
117 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
118 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
119 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
120 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
121 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
122 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97123 // Stars: yours, and who starred a repository.
124 route("GET", "/user/starred", Op::About(AboutOp::ListStarred), &[]),
125 route("GET", "/user/starred/:owner/:name", Op::About(AboutOp::CheckStarred), &[]),
126 route("PUT", "/user/starred/:owner/:name", Op::About(AboutOp::Star), &[]),
127 route("DELETE", "/user/starred/:owner/:name", Op::About(AboutOp::Unstar), &[]),
128 route("GET", "/repos/:owner/:name/stargazers", Op::About(AboutOp::ListStargazers), &[("page", "page")]),
129 // What the default branch says about a repository, kept by commit.
130 route("GET", "/repos/:owner/:name/languages", Op::About(AboutOp::GetLanguages), &[]),
131 route("GET", "/repos/:owner/:name/contributors", Op::About(AboutOp::ListContributors), &[]),
132 route("GET", "/repos/:owner/:name/license", Op::About(AboutOp::GetLicense), &[]),
133 // Releases: `latest` and `tags/…` before an id.
134 route("GET", "/repos/:owner/:name/releases", Op::About(AboutOp::ListReleases), &[]),
135 route("POST", "/repos/:owner/:name/releases", Op::About(AboutOp::CreateRelease), &[]),
136 route("GET", "/repos/:owner/:name/releases/latest", Op::About(AboutOp::GetLatestRelease), &[]),
137 route("GET", "/repos/:owner/:name/releases/tags/:tag", Op::About(AboutOp::GetReleaseByTag), &[]),
138 route("GET", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::GetRelease), &[]),
139 route("PATCH", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::UpdateRelease), &[]),
140 route("DELETE", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::DeleteRelease), &[]),
API: pinned projects over REST and MCP141 // Your pinned projects in a workspace, in your order.
142 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
143 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
144 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
145 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look146 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
147 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily148 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
Merge main (membership, two-factor, GitHub repo roles) into tokens149 // Members and owners: GitHub's organization members, by username.
150 route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]),
151 route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]),
152 route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]),
153 route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]),
154 route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97155 // A workspace's projects: what each is, where it runs, its links.
156 route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
157 route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
158 route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily159 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160 route(
161 "GET",
162 "/workspaces/:workspace/outside_collaborators",
163 Op::ListOutsideCollaborators,
164 &[],
165 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar166 // Teams: a workspace's groups of members, with roles on repositories.
167 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
168 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
169 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
170 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
171 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
172 route(
173 "GET",
174 "/workspaces/:workspace/teams/:team/members",
175 Op::ListTeamMembers,
176 &[("include_child_teams", "include_child_teams")],
177 ),
178 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
179 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
180 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
181 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
182 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
183 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
184 route(
185 "PUT",
186 "/workspaces/:workspace/teams/:team/review_assignment",
187 Op::SetTeamReviewAssignment,
188 &[],
189 ),
190 route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit191 // A workspace's billing: usage, budget, AI credit and invoices.
192 route(
193 "GET",
194 "/workspaces/:workspace/usage",
195 Op::GetUsage,
196 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
197 ),
198 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
199 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
200 route("GET", "/workspaces/:workspace/ai_credit", Op::GetAiCredit, &[]),
201 route("POST", "/workspaces/:workspace/ai_credit/checkout", Op::BuyAiCredit, &[]),
202 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
203 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens204 // The AI Gateway's log of a workspace's requests.
205 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 bar206 // Code owners: the CODEOWNERS file, checked.
207 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 daily208 // Security alerts: secrets and vulnerable dependencies.
209 route(
210 "GET",
211 "/repos/:owner/:name/security/alerts",
212 Op::ListSecurityAlerts,
213 &[("state", "state"), ("kind", "kind")],
214 ),
215 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
216 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 bar217 // The security suite: secret scanning, code scanning, vulnerability
218 // alerts and the supply chain, at the common addresses.
219 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
220 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
221 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
222 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
223 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
224 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
225 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
226 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
227 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
228 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
229 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
230 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
231 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
232 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
233 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
234 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
235 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
236 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
237 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
238 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
239 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
240 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
241 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
242 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
243 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
244 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
245 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
246 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
247 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
248 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
249 route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]),
250 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
251 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
252 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
253 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
254 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
255 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
256 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
257 route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
Integrations: your own model provider, alerts that open issues, tickets agents read258 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
Search across all of g1t, Explore, and a command palette259 route(
260 "GET",
261 "/search",
262 Op::Search,
263 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
264 ),
Integrations: your own model provider, alerts that open issues, tickets agents read265 route("POST", "/repos", Op::CreateRepo, &[]),
266 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
267 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look268 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
269 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
270 route(
271 "GET",
272 "/workspaces/:workspace/repos/deleted",
273 Op::ListDeletedRepos,
274 &[],
275 ),
276 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
277 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
278 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
279 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
280 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
281 route(
282 "POST",
283 "/repos/:owner/:name/visibility",
284 Op::SetRepoVisibility,
285 &[],
286 ),
287 route(
288 "POST",
289 "/repos/:owner/:name/branches/:branch/rename",
290 Op::RenameBranch,
291 &[],
292 ),
Integrations: your own model provider, alerts that open issues, tickets agents read293 route(
294 "GET",
295 "/repos/:owner/:name/settings",
296 Op::GetRepoSettings,
297 &[],
298 ),
299 route(
300 "PATCH",
301 "/repos/:owner/:name/settings",
302 Op::UpdateRepoSettings,
303 &[],
304 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents305 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit306 // Checks: GitHub's addresses for statuses, check runs and check suites.
307 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
308 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
309 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
310 route(
311 "GET",
312 "/repos/:owner/:name/commits/:ref/check-runs",
313 Op::Checks(ChecksOp::ListCheckRunsForRef),
314 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
315 ),
316 route(
317 "GET",
318 "/repos/:owner/:name/commits/:ref/check-suites",
319 Op::Checks(ChecksOp::ListCheckSuitesForRef),
320 &[("app", "app"), ("check_name", "check_name")],
321 ),
322 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
323 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
324 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
325 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
326 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
327 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
328 route("POST", "/repos/:owner/:name/check-suites/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckSuite), &[]),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge329 // Rulesets: a repository's, a workspace's, the rules of one branch,
330 // and how they judged pushes and merges.
331 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
332 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
333 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
334 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
335 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
336 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
337 route(
338 "GET",
339 "/repos/:owner/:name/rules/evaluations",
340 Op::Rules(RulesOp::ListRuleEvaluations),
341 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
342 ),
343 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
344 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
345 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
346 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
347 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
348 route(
349 "GET",
350 "/workspaces/:workspace/rules/evaluations",
351 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
352 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
353 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97354 // Deployments wherever they run, their statuses, and environments.
355 route(
356 "GET",
357 "/repos/:owner/:name/deployments",
358 Op::Deployments(DeploymentsOp::ListDeployments),
359 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
360 ),
361 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
362 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
363 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
364 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
365 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
366 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'367 // Environments' protection rules, and the runs they hold.
368 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
369 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
370 route(
371 "GET",
372 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
373 Op::Protection(ProtectionOp::GetPendingDeployments),
374 &[],
375 ),
376 route(
377 "POST",
378 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
379 Op::Protection(ProtectionOp::ReviewPendingDeployments),
380 &[],
381 ),
382 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
383 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
384 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
385 route(
386 "GET",
387 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
388 Op::Protection(ProtectionOp::GetForkPrApproval),
389 &[],
390 ),
391 route(
392 "PUT",
393 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
394 Op::Protection(ProtectionOp::SetForkPrApproval),
395 &[],
396 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts397 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
398 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'399 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
400 route(
401 "GET",
402 "/workspaces/:workspace/actions/permissions/workflow",
403 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
404 &[],
405 ),
406 route(
407 "PUT",
408 "/workspaces/:workspace/actions/permissions/workflow",
409 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
410 &[],
411 ),
Integrations: your own model provider, alerts that open issues, tickets agents read412 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
413 route(
414 "POST",
415 "/repos/:owner/:name/pulls/:number/messages",
416 Op::MessageAgent,
417 &[],
418 ),
419 route(
420 "POST",
421 "/repos/:owner/:name/pulls/:number/messages/take",
422 Op::TakeMessages,
423 &[],
424 ),
425 route(
426 "POST",
427 "/repos/:owner/:name/messages/:id/answer",
428 Op::AnswerMessage,
429 &[],
430 ),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains431 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
432 route(
433 "GET",
434 "/repos/:owner/:name/memory",
435 Op::Recall,
436 &[("q", "query"), ("limit", "limit")],
437 ),
Integrations: your own model provider, alerts that open issues, tickets agents read438 route(
439 "GET",
440 "/repos/:owner/:name/events",
441 Op::ListEvents,
442 &[("before", "before")],
443 ),
444 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar445 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
446 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
447 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
448 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
449 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
450 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
451 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
452 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
453 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
454 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
455 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
456 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
457 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
458 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Integrations: your own model provider, alerts that open issues, tickets agents read459 route(
460 "GET",
461 "/repos/:owner/:name/issues",
462 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar463 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Integrations: your own model provider, alerts that open issues, tickets agents read464 ),
465 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
466 route(
467 "GET",
468 "/repos/:owner/:name/issues/:number",
469 Op::GetIssue,
470 &[],
471 ),
472 route(
473 "PATCH",
474 "/repos/:owner/:name/issues/:number",
475 Op::UpdateIssue,
476 &[],
477 ),
478 route(
479 "POST",
480 "/repos/:owner/:name/issues/:number/close",
481 Op::CloseIssue,
482 &[],
483 ),
484 route(
485 "POST",
486 "/repos/:owner/:name/issues/:number/reopen",
487 Op::ReopenIssue,
488 &[],
489 ),
490 route(
491 "POST",
492 "/repos/:owner/:name/issues/:number/assign",
493 Op::AssignIssue,
494 &[],
495 ),
496 route(
497 "POST",
498 "/repos/:owner/:name/issues/import",
499 Op::ImportIssue,
500 &[],
501 ),
502 route(
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step503 "POST",
504 "/repos/:owner/:name/issues/delegate",
505 Op::Delegate,
506 &[],
507 ),
508 route(
Integrations: your own model provider, alerts that open issues, tickets agents read509 "GET",
510 "/repos/:owner/:name/context",
511 Op::GetContext,
512 &[("reference", "reference")],
513 ),
514 route(
515 "GET",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API516 "/workspaces/:workspace/context/search",
517 Op::SearchContext,
518 &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
519 ),
520 route(
521 "GET",
522 "/workspaces/:workspace/context/:kind/:id",
523 Op::GetEntity,
524 &[],
525 ),
526 route(
527 "GET",
Integrations: your own model provider, alerts that open issues, tickets agents read528 "/workspaces/:workspace/integrations",
529 Op::ListIntegrations,
530 &[],
531 ),
532 route(
533 "POST",
534 "/workspaces/:workspace/integrations",
535 Op::ConnectIntegration,
536 &[],
537 ),
538 route(
Models per workspace: several providers, routed by kind of work539 "GET",
Webhooks: every event, to your own addresses, signed and retried540 "/repos/:owner/:name/hooks",
541 Op::ListWebhooks,
542 &[],
543 ),
544 route(
545 "POST",
546 "/repos/:owner/:name/hooks",
547 Op::CreateWebhook,
548 &[],
549 ),
550 route(
551 "PATCH",
552 "/repos/:owner/:name/hooks/:id",
553 Op::UpdateWebhook,
554 &[],
555 ),
556 route(
557 "DELETE",
558 "/repos/:owner/:name/hooks/:id",
559 Op::DeleteWebhook,
560 &[],
561 ),
562 route(
563 "POST",
564 "/repos/:owner/:name/hooks/:id/pings",
565 Op::PingWebhook,
566 &[],
567 ),
568 route(
569 "GET",
570 "/repos/:owner/:name/hooks/:id/deliveries",
571 Op::ListWebhookDeliveries,
572 &[],
573 ),
574 route(
575 "POST",
576 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
577 Op::RedeliverWebhook,
578 &[],
579 ),
580 route(
581 "GET",
582 "/workspaces/:workspace/hooks",
583 Op::ListWebhooks,
584 &[],
585 ),
586 route(
587 "POST",
588 "/workspaces/:workspace/hooks",
589 Op::CreateWebhook,
590 &[],
591 ),
592 route(
593 "PATCH",
594 "/workspaces/:workspace/hooks/:id",
595 Op::UpdateWebhook,
596 &[],
597 ),
598 route(
599 "DELETE",
600 "/workspaces/:workspace/hooks/:id",
601 Op::DeleteWebhook,
602 &[],
603 ),
604 route(
605 "POST",
606 "/workspaces/:workspace/hooks/:id/pings",
607 Op::PingWebhook,
608 &[],
609 ),
610 route(
611 "GET",
612 "/workspaces/:workspace/hooks/:id/deliveries",
613 Op::ListWebhookDeliveries,
614 &[],
615 ),
616 route(
617 "POST",
618 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
619 Op::RedeliverWebhook,
620 &[],
621 ),
622 route(
623 "GET",
Models per workspace: several providers, routed by kind of work624 "/workspaces/:workspace/model-routes",
625 Op::GetModelRoutes,
626 &[],
627 ),
628 route(
629 "PUT",
630 "/workspaces/:workspace/model-routes",
631 Op::SetModelRoutes,
632 &[],
633 ),
634 route(
AI Gateway: OpenAI's format, open models, and your own providers635 "PATCH",
636 "/workspaces/:workspace/integrations/:id",
637 Op::UpdateIntegration,
638 &[],
639 ),
640 route(
Integrations: your own model provider, alerts that open issues, tickets agents read641 "DELETE",
642 "/workspaces/:workspace/integrations/:id",
643 Op::DisconnectIntegration,
644 &[],
645 ),
646 route(
647 "POST",
648 "/workspaces/:workspace/integrations/:id/test",
649 Op::TestIntegration,
650 &[],
651 ),
Automations: rules in .g1t/automations that act when something happens652 route(
653 "GET",
GitHub Actions on g1t, part two: running workflows654 "/repos/:owner/:name/actions/workflows",
655 Op::ListWorkflows,
656 &[],
657 ),
658 route(
659 "GET",
660 "/repos/:owner/:name/actions/workflows/:workflow/runs",
661 Op::ListWorkflowRuns,
662 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
663 ),
664 route(
665 "POST",
666 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
667 Op::DispatchWorkflow,
668 &[],
669 ),
670 route(
671 "PATCH",
672 "/repos/:owner/:name/actions/workflows/:workflow",
673 Op::UpdateWorkflow,
674 &[],
675 ),
676 route(
677 "PUT",
678 "/repos/:owner/:name/actions/workflows/:workflow/enable",
679 Op::UpdateWorkflow,
680 &[],
681 ),
682 route(
683 "PUT",
684 "/repos/:owner/:name/actions/workflows/:workflow/disable",
685 Op::UpdateWorkflow,
686 &[],
687 ),
688 route(
689 "GET",
690 "/repos/:owner/:name/actions/runs",
691 Op::ListWorkflowRuns,
692 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
693 ),
694 route(
695 "GET",
696 "/repos/:owner/:name/actions/runs/:id",
697 Op::GetWorkflowRun,
698 &[],
699 ),
700 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)701 "GET",
702 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
703 Op::GetWorkflowRun,
704 &[],
705 ),
706 route(
GitHub Actions on g1t, part two: running workflows707 "POST",
708 "/repos/:owner/:name/actions/runs/:id/cancel",
709 Op::CancelWorkflowRun,
710 &[],
711 ),
712 route(
713 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)714 "/repos/:owner/:name/actions/runs/:id/force-cancel",
715 Op::CancelWorkflowRun,
716 &[],
717 ),
718 route(
719 "POST",
GitHub Actions on g1t, part two: running workflows720 "/repos/:owner/:name/actions/runs/:id/rerun",
721 Op::RerunWorkflowRun,
722 &[],
723 ),
724 route(
725 "POST",
726 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
727 Op::RerunWorkflowRun,
728 &[],
729 ),
730 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)731 "POST",
732 "/repos/:owner/:name/actions/jobs/:job/rerun",
733 Op::RerunWorkflowRun,
734 &[],
735 ),
736 route(
GitHub Actions on g1t, part two: running workflows737 "GET",
738 "/repos/:owner/:name/actions/jobs/:job/logs",
739 Op::GetJobLogs,
740 &[("after", "after")],
741 ),
Merge packages: roles, Actions access, source label, soft delete, API742 // Packages, at GitHub's addresses with the workspace in place of the
743 // organization. A name with a slash is one URL-encoded segment.
744 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
745 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
746 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
747 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
748 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
749 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
750 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
751 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
752 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
753 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
754 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
755 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
756 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
757 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
758 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
759 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
760 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access/:repository", Op::Packages(PackagesOp::RemoveActionsAccess), &[]),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2761 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
762 // a signed link (lib.rs).
763 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
764 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
765 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
766 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
767 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
768 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
769 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
GitHub Actions on g1t, part two: running workflows770 route(
771 "GET",
772 "/repos/:owner/:name/actions/secrets",
773 Op::ListActionsSecrets,
774 &[],
775 ),
776 route(
777 "PUT",
778 "/repos/:owner/:name/actions/secrets/:setting",
779 Op::SetActionsSecret,
780 &[],
781 ),
782 route(
783 "DELETE",
784 "/repos/:owner/:name/actions/secrets/:setting",
785 Op::DeleteActionsSecret,
786 &[],
787 ),
788 route(
789 "GET",
790 "/repos/:owner/:name/actions/variables",
791 Op::ListActionsVariables,
792 &[],
793 ),
794 route(
795 "POST",
796 "/repos/:owner/:name/actions/variables",
797 Op::SetActionsVariable,
798 &[],
799 ),
800 route(
801 "PATCH",
802 "/repos/:owner/:name/actions/variables/:setting",
803 Op::SetActionsVariable,
804 &[],
805 ),
806 route(
807 "DELETE",
808 "/repos/:owner/:name/actions/variables/:setting",
809 Op::DeleteActionsVariable,
810 &[],
811 ),
812 route(
813 "GET",
814 "/workspaces/:workspace/actions/secrets",
815 Op::ListActionsSecrets,
816 &[],
817 ),
818 route(
819 "PUT",
820 "/workspaces/:workspace/actions/secrets/:setting",
821 Op::SetActionsSecret,
822 &[],
823 ),
824 route(
825 "DELETE",
826 "/workspaces/:workspace/actions/secrets/:setting",
827 Op::DeleteActionsSecret,
828 &[],
829 ),
830 route(
831 "GET",
832 "/workspaces/:workspace/actions/variables",
833 Op::ListActionsVariables,
834 &[],
835 ),
836 route(
837 "POST",
838 "/workspaces/:workspace/actions/variables",
839 Op::SetActionsVariable,
840 &[],
841 ),
842 route(
843 "PATCH",
844 "/workspaces/:workspace/actions/variables/:setting",
845 Op::SetActionsVariable,
846 &[],
847 ),
848 route(
849 "DELETE",
850 "/workspaces/:workspace/actions/variables/:setting",
851 Op::DeleteActionsVariable,
852 &[],
853 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents854 // Self-hosted runners: a repository's own, or a workspace's.
855 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
856 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
857 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
858 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
859 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
860 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
861 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
862 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
863 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
864 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
865 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
866 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
867 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
868 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
Integrations: your own model provider, alerts that open issues, tickets agents read869 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
870 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
871 route(
872 "POST",
873 "/repos/:owner/:name/plans/:plan/apply",
874 Op::ApplyPlan,
875 &[],
876 ),
877 route(
878 "POST",
879 "/repos/:owner/:name/issues/:number/comments",
880 Op::AddComment,
881 &[],
882 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts883 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
884 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Integrations: your own model provider, alerts that open issues, tickets agents read885 route(
886 "GET",
887 "/repos/:owner/:name/pulls",
888 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar889 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Integrations: your own model provider, alerts that open issues, tickets agents read890 ),
891 route(
892 "POST",
893 "/repos/:owner/:name/pulls",
894 Op::CreatePullRequest,
895 &[],
896 ),
897 route(
898 "GET",
899 "/repos/:owner/:name/pulls/:number",
900 Op::GetPullRequest,
901 &[],
902 ),
903 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar904 "PATCH",
905 "/repos/:owner/:name/pulls/:number",
906 Op::UpdatePullRequest,
907 &[],
908 ),
909 route(
Integrations: your own model provider, alerts that open issues, tickets agents read910 "GET",
911 "/repos/:owner/:name/pulls/:number/changes",
912 Op::GetPullRequestChanges,
913 &[],
914 ),
915 route(
916 "POST",
917 "/repos/:owner/:name/pulls/:number/reviews",
918 Op::ReviewPullRequest,
919 &[],
920 ),
921 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar922 "POST",
923 "/repos/:owner/:name/pulls/:number/requested_reviewers",
924 Op::RequestReviewers,
925 &[],
926 ),
927 route(
928 "DELETE",
929 "/repos/:owner/:name/pulls/:number/requested_reviewers",
930 Op::RemoveRequestedReviewers,
931 &[],
932 ),
933 route(
Integrations: your own model provider, alerts that open issues, tickets agents read934 "GET",
935 "/repos/:owner/:name/pulls/:number/session",
936 Op::ReadSession,
937 &[("after", "after")],
938 ),
939 route(
940 "POST",
941 "/repos/:owner/:name/pulls/:number/session",
942 Op::RecordSession,
943 &[],
944 ),
945 route(
946 "POST",
947 "/repos/:owner/:name/pulls/:number/ready",
948 Op::MarkPullRequestReady,
949 &[],
950 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts951 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Integrations: your own model provider, alerts that open issues, tickets agents read952 route(
953 "POST",
954 "/repos/:owner/:name/pulls/:number/close",
955 Op::ClosePullRequest,
956 &[],
957 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts958 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Integrations: your own model provider, alerts that open issues, tickets agents read959 route(
960 "POST",
961 "/repos/:owner/:name/pulls/:number/merge",
962 Op::MergePullRequest,
963 &[],
964 ),
The API answers for artifacts: the artifact MCP tool and /workspaces/{workspace}/artifacts list, search, read and edit a workspace's docs as the person sees them, make, move, trash, restore and purge them, show their versions and who can open them, and share them, each action offered only to tokens whose artifacts scope allows it, through a DOCS binding to the docs service, with the OpenAPI document and its examples.965 // Artifacts mode's docs, slides, designs and dashboards. Search comes
966 // before an artifact by id, which it would otherwise match.
967 route("GET", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::List), &[("tab", "tab"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("q", "q"), ("state", "state"), ("cursor", "cursor"), ("limit", "limit")]),
968 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
969 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
970 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
971 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
972 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
973 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
974 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
975 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
976 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
977 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
978 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
979 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
980 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
981 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
982 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
983 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Integrations: your own model provider, alerts that open issues, tickets agents read984];
985
986impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts987 /// Whether the route answers success with `204` and no body, as
988 /// GitHub's address for the same does. MCP still answers `true`.
989 pub fn no_content(&self) -> bool {
990 self.op == Op::DeleteComment
991 }
992
Integrations: your own model provider, alerts that open issues, tickets agents read993 /// The names of the route's path parameters, in order.
994 pub fn params(&self) -> impl Iterator<Item = &'static str> {
995 self.path
996 .split('/')
997 .filter_map(|segment| segment.strip_prefix(':'))
998 }
999
1000 /// The values of the path parameters, if `path` is this route's.
1001 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1002 let mut values = Vec::new();
1003 let mut actual = path.trim_end_matches('/').split('/');
1004 for expected in self.path.split('/') {
1005 let segment = actual.next()?;
1006 match expected.strip_prefix(':') {
1007 Some(name) if !segment.is_empty() => values.push((name, segment)),
1008 Some(_) => return None,
1009 None if expected == segment => {}
1010 None => return None,
1011 }
1012 }
1013 actual.next().is_none().then_some(values)
1014 }
1015}
1016
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1017/// A path segment with its `%XX` escapes decoded; as given when that is not
1018/// UTF-8.
1019fn percent_decoded(segment: &str) -> String {
1020 let bytes = segment.as_bytes();
1021 let mut out = Vec::with_capacity(bytes.len());
1022 let mut i = 0;
1023 while i < bytes.len() {
1024 let escaped = (bytes[i] == b'%')
1025 .then(|| segment.get(i + 1..i + 3))
1026 .flatten()
1027 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1028 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1029 match escaped {
1030 Some(byte) => {
1031 out.push(byte);
1032 i += 3;
1033 }
1034 None => {
1035 out.push(bytes[i]);
1036 i += 1;
1037 }
1038 }
1039 }
1040 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1041}
1042
Integrations: your own model provider, alerts that open issues, tickets agents read1043/// The route for a request, and the operation input it describes.
1044///
1045/// The input is the JSON body, overlaid with the query parameters the route
1046/// 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 bar1047/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1048/// becomes an integer.
Integrations: your own model provider, alerts that open issues, tickets agents read1049pub fn resolve(
1050 method: &str,
1051 path: &str,
1052 query: &[(String, String)],
1053 body: Value,
1054) -> Option<(&'static Route, Value)> {
1055 let (route, params) = ROUTES
1056 .iter()
1057 .filter(|route| route.method == method)
1058 .find_map(|route| Some((route, route.matches(path)?)))?;
1059
1060 let mut input = match body {
1061 Value::Object(fields) => fields,
1062 _ => Map::new(),
1063 };
1064 for (name, key) in route.query {
1065 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1066 input.insert((*key).to_owned(), Value::String(value.clone()));
1067 }
1068 }
1069 let param = |wanted: &str| {
1070 params
1071 .iter()
1072 .find(|(name, _)| *name == wanted)
1073 .map(|(_, value)| *value)
1074 };
1075 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1076 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1077 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971078 // Every other name the path gives, under that name; the ones below that
1079 // need more (a repository, a number, an encoded name) are set after.
1080 for (key, value) in &params {
1081 if !matches!(*key, "owner" | "name") {
1082 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Integrations: your own model provider, alerts that open issues, tickets agents read1083 }
1084 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1085 // A repository of a team's workspace, named by itself.
1086 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1087 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1088 }
1089 // A branch name may hold slashes, sent URL-encoded as one segment, and
1090 // a label's name spaces.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1091 if let Some(branch) = param("branch") {
1092 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1093 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971094 // An environment's name may hold slashes and spaces, URL-encoded.
1095 if let Some(environment) = param("environment") {
1096 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1097 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1098 if let Some(label) = param("label") {
1099 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1100 }
1101 if let Some(milestone) = param("milestone") {
1102 // Not a number: zero, which no milestone has.
1103 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1104 }
GitHub Actions on g1t, part two: running workflows1105 // GitHub says some things with the path alone.
1106 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1107 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1108 }
1109 if route.path.ends_with("/rerun-failed-jobs") {
1110 input.insert("failed_only".to_owned(), Value::Bool(true));
1111 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1112 if route.path.ends_with("/force-cancel") {
1113 input.insert("force".to_owned(), Value::Bool(true));
1114 }
API: notifications over REST and MCP, with notifications scopes1115 // Unsaving and waking a thread are a DELETE of what PUT made.
1116 if route.method == "DELETE" && route.path.ends_with("/saved") {
1117 input.insert("saved".to_owned(), Value::Bool(false));
1118 }
1119 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1120 input.remove("until");
1121 }
Integrations: your own model provider, alerts that open issues, tickets agents read1122 if let Some(number) = param("number") {
1123 // Not a number: zero, which no issue or pull request has.
1124 input.insert(
1125 "number".to_owned(),
1126 number.parse::<u32>().unwrap_or(0).into(),
1127 );
1128 }
1129 Some((route, Value::Object(input)))
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use serde_json::json;
1135
1136 use super::*;
1137
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971138 /// Every name a route's path gives reaches the operation: a name left
1139 /// off the lists above is dropped, and the operation answers that it
1140 /// was not given (the project routes were, until this test).
1141 #[test]
1142 fn every_path_parameter_reaches_the_input() {
1143 for route in ROUTES.iter() {
1144 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1145 if names.is_empty() {
1146 continue;
1147 }
1148 let path: String = route
1149 .path
1150 .split('/')
1151 .map(|part| match part.strip_prefix(':') {
1152 Some("number" | "milestone") => "7".to_owned(),
1153 Some(name) => format!("{name}-x"),
1154 None => part.to_owned(),
1155 })
1156 .collect::<Vec<_>>()
1157 .join("/");
1158 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1159 // A path two routes could take is checked under the first.
1160 if found.path != route.path {
1161 continue;
1162 }
1163 for name in names {
1164 let key = match name {
1165 "owner" | "name" => "repo",
1166 "repo" if names_has_workspace(route.path) => "repo",
1167 other => other,
1168 };
1169 assert!(
1170 input.get(key).is_some_and(|value| !value.is_null()),
1171 "{} {}: :{name} does not reach the input",
1172 route.method,
1173 route.path
1174 );
1175 }
1176 }
1177 }
1178
1179 fn names_has_workspace(path: &str) -> bool {
1180 path.split('/').any(|part| part == ":workspace")
1181 }
1182
Integrations: your own model provider, alerts that open issues, tickets agents read1183 #[test]
1184 fn a_path_resolves_to_its_operation_and_input() {
1185 let (route, input) = resolve(
1186 "POST",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1187 "/repos/flagon-io/hello/pulls/14/merge",
Integrations: your own model provider, alerts that open issues, tickets agents read1188 &[],
1189 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1190 )
1191 .unwrap();
1192 assert_eq!(route.op, Op::MergePullRequest);
1193 // What the path names wins over the body.
1194 assert_eq!(
1195 input,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1196 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
Integrations: your own model provider, alerts that open issues, tickets agents read1197 );
1198 }
1199
1200 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1201 fn a_branch_with_slashes_is_one_encoded_segment() {
1202 let (route, input) = resolve(
1203 "POST",
1204 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1205 &[],
1206 json!({ "new_name": "feature/sign-in" }),
1207 )
1208 .unwrap();
1209 assert_eq!(route.op, Op::RenameBranch);
1210 assert_eq!(
1211 input,
1212 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1213 );
1214 assert_eq!(percent_decoded("100%"), "100%");
1215 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1216 }
1217
1218 #[test]
1219 fn a_collaborator_is_named_by_username() {
1220 let (route, input) = resolve(
1221 "PATCH",
1222 "/repos/flagon-io/hello/collaborators/ada",
1223 &[],
1224 json!({ "role": "maintain" }),
1225 )
1226 .unwrap();
1227 assert_eq!(route.op, Op::UpdateCollaborator);
1228 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1229 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1230 assert_eq!(route.op, Op::DeclineRepoInvitation);
1231 assert_eq!(input, json!({ "id": "rin_1" }));
1232 }
1233
1234 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1235 fn teams_are_addressed_by_workspace_and_slug() {
1236 let query = [("q".to_owned(), "back".to_owned())];
1237 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1238 assert_eq!(route.op, Op::ListTeams);
1239 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1240 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1241 assert_eq!(route.op, Op::UpdateTeam);
1242 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1243 let (route, input) =
1244 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1245 assert_eq!(route.op, Op::SetTeamMember);
1246 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1247 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1248 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1249 assert_eq!(route.op, Op::ListTeamMembers);
1250 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1251 // A repository is named by itself, in the team's workspace.
1252 let (route, input) =
1253 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1254 assert_eq!(route.op, Op::SetTeamRepo);
1255 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1256 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1257 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1258 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1259 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1260 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1261 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1262 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1263 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1264 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1265 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1266 assert_eq!(route.op, Op::ListUserTeams);
1267 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1268 }
1269
1270 #[test]
1271 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1272 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1273 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1274 assert_eq!(route.op, Op::RequestReviewers);
1275 assert_eq!(
1276 input,
1277 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1278 );
1279 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1280 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1281 let query = [("ref".to_owned(), "main".to_owned())];
1282 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1283 assert_eq!(route.op, Op::GetCodeownersErrors);
1284 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1285 }
1286
1287 #[test]
API: notifications over REST and MCP, with notifications scopes1288 fn notifications_are_addressed_as_threads_and_by_issue() {
1289 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1290 assert_eq!(route.op, Op::MarkThreadDone);
1291 assert_eq!(input, json!({ "id": "ntf_1" }));
1292 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1293 assert_eq!(route.op, Op::SaveThread);
1294 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1295 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1296 assert_eq!(route.op, Op::SnoozeThread);
1297 assert_eq!(input, json!({ "id": "ntf_1" }));
1298 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1299 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1300 assert_eq!(route.op, Op::ListNotifications);
1301 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1302 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1303 assert_eq!(route.op, Op::SetThreadSubscription);
1304 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1305 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1306 }
1307
1308 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1309 fn labels_and_milestones_are_named_in_the_path() {
1310 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1311 assert_eq!(route.op, Op::UpdateLabel);
1312 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1313 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1314 assert_eq!(route.op, Op::RemoveIssueLabels);
1315 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1316 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1317 assert_eq!(route.op, Op::RemoveIssueLabels);
1318 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1319 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1320 assert_eq!(route.op, Op::AddDefaultLabels);
1321 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1322 assert_eq!(route.op, Op::UpdateMilestone);
1323 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1324 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1325 assert_eq!(route.op, Op::UpdatePullRequest);
1326 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1327 }
1328
1329 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1330 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1331 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1332 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1333 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1334 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1335 // Reopening and closing with a PATCH, as `state`.
1336 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1337 assert_eq!(route.op, Op::UpdatePullRequest);
1338 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1339 let (route, input) =
1340 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1341 assert_eq!(route.op, Op::EditComment);
1342 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1343 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1344 assert_eq!(route.op, Op::DeleteComment);
1345 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1346 // Still an issue's comments, labels and subscription.
1347 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1348 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1349 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1350 }
1351
1352 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1353 fn billing_is_addressed_by_workspace() {
1354 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1355 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1356 assert_eq!(route.op, Op::GetUsage);
1357 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1358 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1359 assert_eq!(route.op, Op::SetBudget);
1360 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1361 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1362 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1363 assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit);
1364 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
1365 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1366 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1367 assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1368 }
1369
1370 #[test]
Merge checks: statuses and check runs on every commit1371 fn checks_are_at_githubs_addresses() {
1372 let sha = "a".repeat(40);
1373 let body = json!({ "state": "success", "context": "ci/build" });
1374 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1375 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1376 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1377 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1378 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1379 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1380 let query = [("check_name".to_owned(), "lint".to_owned())];
1381 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1382 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1383 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1384 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1385 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1386 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1387 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1388 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1389 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1390 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1391 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1392 }
1393
1394 #[test]
Integrations: your own model provider, alerts that open issues, tickets agents read1395 fn query_parameters_are_renamed() {
1396 let query = [
1397 ("q".to_owned(), "parser".to_owned()),
1398 ("x".to_owned(), "y".to_owned()),
1399 ];
1400 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1401 assert_eq!(route.op, Op::ListRepos);
1402 assert_eq!(input, json!({ "query": "parser" }));
1403 }
1404
1405 #[test]
1406 fn method_and_shape_must_match() {
1407 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1408 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1409 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1410 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1411 }
1412
1413 #[test]
1414 fn every_parameter_and_query_name_is_an_input() {
1415 for route in ROUTES {
1416 let properties = route.op.properties();
1417 for (_, key) in route.query {
1418 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1419 }
1420 for name in route.params() {
1421 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1422 || properties.contains_key(name);
1423 assert!(covered, "{}: {name}", route.path);
1424 }
1425 }
1426 }
1427
1428 #[test]
1429 fn every_operation_has_a_route() {
1430 for op in Op::ALL {
1431 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1432 }
1433 }
1434}

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