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

Projects live: fixes from walking them end to end1//! 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;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.6use crate::run_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;
Merge main into Artifacts Phase 211use 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;
Projects live: fixes from walking them end to end14use 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;
Projects live: fixes from walking them end to end18
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")]),
Projects live: fixes from walking them end to end258 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 ),
Projects live: fixes from walking them end to end265 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 ),
Projects live: fixes from walking them end to end293 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 ),
Projects live: fixes from walking them end to end412 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 ),
Projects live: fixes from walking them end to end438 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, &[]),
Projects live: fixes from walking them end to end459 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")],
Projects live: fixes from walking them end to end464 ),
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(
Projects live: fixes from walking them end to end509 "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",
Projects live: fixes from walking them end to end528 "/workspaces/:workspace/integrations",
529 Op::ListIntegrations,
530 &[],
531 ),
532 route(
533 "POST",
534 "/workspaces/:workspace/integrations",
535 Op::ConnectIntegration,
536 &[],
537 ),
538 route(
539 "GET",
540 "/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",
624 "/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(
Projects live: fixes from walking them end to end641 "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 ),
652 route(
653 "GET",
654 "/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(
Projects live: fixes from walking them end to end707 "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",
Projects live: fixes from walking them end to end720 "/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(
Projects live: fixes from walking them end to end737 "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), &[]),
Projects live: fixes from walking them end to end770 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, &[]),
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.869 // A workspace agent's own computer.
870 route("GET", "/workspaces/:workspace/agents/:agent/computer", Op::GetAgentComputer, &[]),
871 route("POST", "/workspaces/:workspace/agents/:agent/computer/wake", Op::WakeAgentComputer, &[]),
872 route("POST", "/workspaces/:workspace/agents/:agent/computer/sleep", Op::SleepAgentComputer, &[]),
873 route("POST", "/workspaces/:workspace/agents/:agent/computer/reset", Op::ResetAgentComputer, &[]),
874 route("GET", "/workspaces/:workspace/agents/:agent/computer/commands", Op::ListAgentComputerCommands, &[]),
Projects live: fixes from walking them end to end875 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
876 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
877 route(
878 "POST",
879 "/repos/:owner/:name/plans/:plan/apply",
880 Op::ApplyPlan,
881 &[],
882 ),
883 route(
884 "POST",
885 "/repos/:owner/:name/issues/:number/comments",
886 Op::AddComment,
887 &[],
888 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts889 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
890 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Projects live: fixes from walking them end to end891 route(
892 "GET",
893 "/repos/:owner/:name/pulls",
894 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar895 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Projects live: fixes from walking them end to end896 ),
897 route(
898 "POST",
899 "/repos/:owner/:name/pulls",
900 Op::CreatePullRequest,
901 &[],
902 ),
903 route(
904 "GET",
905 "/repos/:owner/:name/pulls/:number",
906 Op::GetPullRequest,
907 &[],
908 ),
909 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar910 "PATCH",
911 "/repos/:owner/:name/pulls/:number",
912 Op::UpdatePullRequest,
913 &[],
914 ),
915 route(
Projects live: fixes from walking them end to end916 "GET",
917 "/repos/:owner/:name/pulls/:number/changes",
918 Op::GetPullRequestChanges,
919 &[],
920 ),
921 route(
922 "POST",
923 "/repos/:owner/:name/pulls/:number/reviews",
924 Op::ReviewPullRequest,
925 &[],
926 ),
927 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar928 "POST",
929 "/repos/:owner/:name/pulls/:number/requested_reviewers",
930 Op::RequestReviewers,
931 &[],
932 ),
933 route(
934 "DELETE",
935 "/repos/:owner/:name/pulls/:number/requested_reviewers",
936 Op::RemoveRequestedReviewers,
937 &[],
938 ),
939 route(
Projects live: fixes from walking them end to end940 "GET",
941 "/repos/:owner/:name/pulls/:number/session",
942 Op::ReadSession,
943 &[("after", "after")],
944 ),
945 route(
946 "POST",
947 "/repos/:owner/:name/pulls/:number/session",
948 Op::RecordSession,
949 &[],
950 ),
951 route(
952 "POST",
953 "/repos/:owner/:name/pulls/:number/ready",
954 Op::MarkPullRequestReady,
955 &[],
956 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts957 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Projects live: fixes from walking them end to end958 route(
959 "POST",
960 "/repos/:owner/:name/pulls/:number/close",
961 Op::ClosePullRequest,
962 &[],
963 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts964 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Projects live: fixes from walking them end to end965 route(
966 "POST",
967 "/repos/:owner/:name/pulls/:number/merge",
968 Op::MergePullRequest,
969 &[],
970 ),
Merge main into Artifacts Phase 2971 // Artifacts mode's docs, slides, designs and dashboards. Search comes
972 // before an artifact by id, which it would otherwise match.
973 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")]),
974 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
975 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
976 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
977 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
978 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
979 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
980 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
981 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
982 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
983 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
984 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
985 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
986 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
987 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
988 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
989 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Projects live: fixes from walking them end to end990];
991
992impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts993 /// Whether the route answers success with `204` and no body, as
994 /// GitHub's address for the same does. MCP still answers `true`.
995 pub fn no_content(&self) -> bool {
996 self.op == Op::DeleteComment
997 }
998
Projects live: fixes from walking them end to end999 /// The names of the route's path parameters, in order.
1000 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1001 self.path
1002 .split('/')
1003 .filter_map(|segment| segment.strip_prefix(':'))
1004 }
1005
1006 /// The values of the path parameters, if `path` is this route's.
1007 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1008 let mut values = Vec::new();
1009 let mut actual = path.trim_end_matches('/').split('/');
1010 for expected in self.path.split('/') {
1011 let segment = actual.next()?;
1012 match expected.strip_prefix(':') {
1013 Some(name) if !segment.is_empty() => values.push((name, segment)),
1014 Some(_) => return None,
1015 None if expected == segment => {}
1016 None => return None,
1017 }
1018 }
1019 actual.next().is_none().then_some(values)
1020 }
1021}
1022
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1023/// A path segment with its `%XX` escapes decoded; as given when that is not
1024/// UTF-8.
1025fn percent_decoded(segment: &str) -> String {
1026 let bytes = segment.as_bytes();
1027 let mut out = Vec::with_capacity(bytes.len());
1028 let mut i = 0;
1029 while i < bytes.len() {
1030 let escaped = (bytes[i] == b'%')
1031 .then(|| segment.get(i + 1..i + 3))
1032 .flatten()
1033 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1034 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1035 match escaped {
1036 Some(byte) => {
1037 out.push(byte);
1038 i += 3;
1039 }
1040 None => {
1041 out.push(bytes[i]);
1042 i += 1;
1043 }
1044 }
1045 }
1046 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1047}
1048
Projects live: fixes from walking them end to end1049/// The route for a request, and the operation input it describes.
1050///
1051/// The input is the JSON body, overlaid with the query parameters the route
1052/// 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 bar1053/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1054/// becomes an integer.
Projects live: fixes from walking them end to end1055pub fn resolve(
1056 method: &str,
1057 path: &str,
1058 query: &[(String, String)],
1059 body: Value,
1060) -> Option<(&'static Route, Value)> {
1061 let (route, params) = ROUTES
1062 .iter()
1063 .filter(|route| route.method == method)
1064 .find_map(|route| Some((route, route.matches(path)?)))?;
1065
1066 let mut input = match body {
1067 Value::Object(fields) => fields,
1068 _ => Map::new(),
1069 };
1070 for (name, key) in route.query {
1071 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1072 input.insert((*key).to_owned(), Value::String(value.clone()));
1073 }
1074 }
1075 let param = |wanted: &str| {
1076 params
1077 .iter()
1078 .find(|(name, _)| *name == wanted)
1079 .map(|(_, value)| *value)
1080 };
1081 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1082 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1083 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971084 // Every other name the path gives, under that name; the ones below that
1085 // need more (a repository, a number, an encoded name) are set after.
1086 for (key, value) in &params {
1087 if !matches!(*key, "owner" | "name") {
1088 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Projects live: fixes from walking them end to end1089 }
1090 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1091 // A repository of a team's workspace, named by itself.
1092 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1093 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1094 }
1095 // A branch name may hold slashes, sent URL-encoded as one segment, and
1096 // a label's name spaces.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1097 if let Some(branch) = param("branch") {
1098 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1099 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971100 // An environment's name may hold slashes and spaces, URL-encoded.
1101 if let Some(environment) = param("environment") {
1102 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1103 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1104 if let Some(label) = param("label") {
1105 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1106 }
1107 if let Some(milestone) = param("milestone") {
1108 // Not a number: zero, which no milestone has.
1109 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1110 }
Projects live: fixes from walking them end to end1111 // GitHub says some things with the path alone.
1112 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1113 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1114 }
1115 if route.path.ends_with("/rerun-failed-jobs") {
1116 input.insert("failed_only".to_owned(), Value::Bool(true));
1117 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1118 if route.path.ends_with("/force-cancel") {
1119 input.insert("force".to_owned(), Value::Bool(true));
1120 }
API: notifications over REST and MCP, with notifications scopes1121 // Unsaving and waking a thread are a DELETE of what PUT made.
1122 if route.method == "DELETE" && route.path.ends_with("/saved") {
1123 input.insert("saved".to_owned(), Value::Bool(false));
1124 }
1125 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1126 input.remove("until");
1127 }
Projects live: fixes from walking them end to end1128 if let Some(number) = param("number") {
1129 // Not a number: zero, which no issue or pull request has.
1130 input.insert(
1131 "number".to_owned(),
1132 number.parse::<u32>().unwrap_or(0).into(),
1133 );
1134 }
1135 Some((route, Value::Object(input)))
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use serde_json::json;
1141
1142 use super::*;
1143
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971144 /// Every name a route's path gives reaches the operation: a name left
1145 /// off the lists above is dropped, and the operation answers that it
1146 /// was not given (the project routes were, until this test).
1147 #[test]
1148 fn every_path_parameter_reaches_the_input() {
1149 for route in ROUTES.iter() {
1150 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1151 if names.is_empty() {
1152 continue;
1153 }
1154 let path: String = route
1155 .path
1156 .split('/')
1157 .map(|part| match part.strip_prefix(':') {
1158 Some("number" | "milestone") => "7".to_owned(),
1159 Some(name) => format!("{name}-x"),
1160 None => part.to_owned(),
1161 })
1162 .collect::<Vec<_>>()
1163 .join("/");
1164 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1165 // A path two routes could take is checked under the first.
1166 if found.path != route.path {
1167 continue;
1168 }
1169 for name in names {
1170 let key = match name {
1171 "owner" | "name" => "repo",
1172 "repo" if names_has_workspace(route.path) => "repo",
1173 other => other,
1174 };
1175 assert!(
1176 input.get(key).is_some_and(|value| !value.is_null()),
1177 "{} {}: :{name} does not reach the input",
1178 route.method,
1179 route.path
1180 );
1181 }
1182 }
1183 }
1184
1185 fn names_has_workspace(path: &str) -> bool {
1186 path.split('/').any(|part| part == ":workspace")
1187 }
1188
Projects live: fixes from walking them end to end1189 #[test]
1190 fn a_path_resolves_to_its_operation_and_input() {
1191 let (route, input) = resolve(
1192 "POST",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1193 "/repos/flagon-io/hello/pulls/14/merge",
Projects live: fixes from walking them end to end1194 &[],
1195 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1196 )
1197 .unwrap();
1198 assert_eq!(route.op, Op::MergePullRequest);
1199 // What the path names wins over the body.
1200 assert_eq!(
1201 input,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1202 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
Projects live: fixes from walking them end to end1203 );
1204 }
1205
1206 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1207 fn a_branch_with_slashes_is_one_encoded_segment() {
1208 let (route, input) = resolve(
1209 "POST",
1210 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1211 &[],
1212 json!({ "new_name": "feature/sign-in" }),
1213 )
1214 .unwrap();
1215 assert_eq!(route.op, Op::RenameBranch);
1216 assert_eq!(
1217 input,
1218 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1219 );
1220 assert_eq!(percent_decoded("100%"), "100%");
1221 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1222 }
1223
1224 #[test]
1225 fn a_collaborator_is_named_by_username() {
1226 let (route, input) = resolve(
1227 "PATCH",
1228 "/repos/flagon-io/hello/collaborators/ada",
1229 &[],
1230 json!({ "role": "maintain" }),
1231 )
1232 .unwrap();
1233 assert_eq!(route.op, Op::UpdateCollaborator);
1234 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1235 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1236 assert_eq!(route.op, Op::DeclineRepoInvitation);
1237 assert_eq!(input, json!({ "id": "rin_1" }));
1238 }
1239
1240 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1241 fn teams_are_addressed_by_workspace_and_slug() {
1242 let query = [("q".to_owned(), "back".to_owned())];
1243 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1244 assert_eq!(route.op, Op::ListTeams);
1245 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1246 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1247 assert_eq!(route.op, Op::UpdateTeam);
1248 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1249 let (route, input) =
1250 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1251 assert_eq!(route.op, Op::SetTeamMember);
1252 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1253 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1254 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1255 assert_eq!(route.op, Op::ListTeamMembers);
1256 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1257 // A repository is named by itself, in the team's workspace.
1258 let (route, input) =
1259 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1260 assert_eq!(route.op, Op::SetTeamRepo);
1261 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1262 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1263 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1264 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1265 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1266 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1267 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1268 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1269 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1270 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1271 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1272 assert_eq!(route.op, Op::ListUserTeams);
1273 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1274 }
1275
1276 #[test]
1277 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1278 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1279 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1280 assert_eq!(route.op, Op::RequestReviewers);
1281 assert_eq!(
1282 input,
1283 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1284 );
1285 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1286 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1287 let query = [("ref".to_owned(), "main".to_owned())];
1288 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1289 assert_eq!(route.op, Op::GetCodeownersErrors);
1290 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1291 }
1292
1293 #[test]
API: notifications over REST and MCP, with notifications scopes1294 fn notifications_are_addressed_as_threads_and_by_issue() {
1295 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1296 assert_eq!(route.op, Op::MarkThreadDone);
1297 assert_eq!(input, json!({ "id": "ntf_1" }));
1298 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1299 assert_eq!(route.op, Op::SaveThread);
1300 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1301 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1302 assert_eq!(route.op, Op::SnoozeThread);
1303 assert_eq!(input, json!({ "id": "ntf_1" }));
1304 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1305 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1306 assert_eq!(route.op, Op::ListNotifications);
1307 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1308 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1309 assert_eq!(route.op, Op::SetThreadSubscription);
1310 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1311 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1312 }
1313
1314 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1315 fn labels_and_milestones_are_named_in_the_path() {
1316 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1317 assert_eq!(route.op, Op::UpdateLabel);
1318 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1319 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1320 assert_eq!(route.op, Op::RemoveIssueLabels);
1321 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1322 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1323 assert_eq!(route.op, Op::RemoveIssueLabels);
1324 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1325 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1326 assert_eq!(route.op, Op::AddDefaultLabels);
1327 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1328 assert_eq!(route.op, Op::UpdateMilestone);
1329 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1330 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1331 assert_eq!(route.op, Op::UpdatePullRequest);
1332 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1333 }
1334
1335 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1336 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1337 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1338 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1339 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1340 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1341 // Reopening and closing with a PATCH, as `state`.
1342 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1343 assert_eq!(route.op, Op::UpdatePullRequest);
1344 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1345 let (route, input) =
1346 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1347 assert_eq!(route.op, Op::EditComment);
1348 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1349 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1350 assert_eq!(route.op, Op::DeleteComment);
1351 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1352 // Still an issue's comments, labels and subscription.
1353 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1354 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1355 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1356 }
1357
1358 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1359 fn billing_is_addressed_by_workspace() {
1360 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1361 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1362 assert_eq!(route.op, Op::GetUsage);
1363 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1364 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1365 assert_eq!(route.op, Op::SetBudget);
1366 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1367 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1368 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1369 assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit);
1370 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
1371 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1372 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1373 assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1374 }
1375
1376 #[test]
Merge checks: statuses and check runs on every commit1377 fn checks_are_at_githubs_addresses() {
1378 let sha = "a".repeat(40);
1379 let body = json!({ "state": "success", "context": "ci/build" });
1380 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1381 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1382 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1383 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1384 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1385 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1386 let query = [("check_name".to_owned(), "lint".to_owned())];
1387 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1388 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1389 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1390 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1391 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1392 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1393 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1394 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1395 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1396 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1397 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1398 }
1399
1400 #[test]
Projects live: fixes from walking them end to end1401 fn query_parameters_are_renamed() {
1402 let query = [
1403 ("q".to_owned(), "parser".to_owned()),
1404 ("x".to_owned(), "y".to_owned()),
1405 ];
1406 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1407 assert_eq!(route.op, Op::ListRepos);
1408 assert_eq!(input, json!({ "query": "parser" }));
1409 }
1410
1411 #[test]
1412 fn method_and_shape_must_match() {
1413 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1414 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1415 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1416 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1417 }
1418
1419 #[test]
1420 fn every_parameter_and_query_name_is_an_input() {
1421 for route in ROUTES {
1422 let properties = route.op.properties();
1423 for (_, key) in route.query {
1424 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1425 }
1426 for name in route.params() {
1427 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1428 || properties.contains_key(name);
1429 assert!(covered, "{}: {name}", route.path);
1430 }
1431 }
1432 }
1433
1434 #[test]
1435 fn every_operation_has_a_route() {
1436 for op in Op::ALL {
1437 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1438 }
1439 }
1440}

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