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

Catching up with main takes seconds when the two sides touched different files1//! 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 remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r97use crate::customize::CustomizeOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca8use crate::deploy_keys::DeployKeysOp;
Merge branch 'mirroring' into artifacts-mode9use crate::mirrors::MirrorsOp;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9710use crate::deployments::DeploymentsOp;
Merge packages: roles, Actions access, source label, soft delete, API11use crate::packages::PackagesOp;
Merge main into Artifacts Phase 212use crate::folios::FoliosOp;
Merge branch 'worktree-agent-a3abfcce648e87dca'13use crate::protection::ProtectionOp;
API and MCP for a workspace's personal access token rules, members' tokens and approvals14use crate::token_policy::TokenOp;
Catching up with main takes seconds when the two sides touched different files15use crate::operations::Op;
Merge checks: statuses and check runs on every commit16use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge17use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar18use crate::security::SecurityOp;
Catching up with main takes seconds when the two sides touched different files19
20pub struct Route {
21 pub method: &'static str,
22 /// Segments starting with `:` are parameters.
23 pub path: &'static str,
24 pub op: Op,
25 /// Query parameters the route reads, as `(name in the URL, input name)`.
26 pub query: &'static [(&'static str, &'static str)],
27}
28
29const fn route(
30 method: &'static str,
31 path: &'static str,
32 op: Op,
33 query: &'static [(&'static str, &'static str)],
34) -> Route {
35 Route {
36 method,
37 path,
38 op,
39 query,
40 }
41}
42
43pub const ROUTES: &[Route] = &[
44 route("GET", "/user", Op::Whoami, &[]),
45 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
Merge branch 'worktree-agent-ad7c6d88d93adc817'46 route("GET", "/workspaces/:workspace", Op::GetWorkspace, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
48 route("GET", "/user/emails", Op::ListEmails, &[]),
49 route("POST", "/user/emails", Op::AddEmail, &[]),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)50 route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
52 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
53 route("GET", "/user/invites", Op::ListInvites, &[]),
54 route("POST", "/user/invites", Op::CreateInvite, &[]),
55 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)56 // Invitations to workspaces waiting for your answer.
57 route("GET", "/user/invitations", Op::ListInvitations, &[]),
58 route("POST", "/user/invitations/:id/accept", Op::AcceptInvitation, &[]),
59 route("POST", "/user/invitations/:id/decline", Op::DeclineInvitation, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look60 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals61 // A workspace's rules for personal access tokens, and its members' tokens.
62 route("GET", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::GetTokenPolicy), &[]),
63 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 managers64 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 approvals65 route("POST", "/workspaces/:workspace/personal-access-tokens/:id", Op::Tokens(TokenOp::RevokeMemberToken), &[]),
66 route("GET", "/workspaces/:workspace/personal-access-token-requests", Op::Tokens(TokenOp::ListTokenRequests), &[]),
67 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 look68 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
69 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
70 // Who has access. GitHub's addresses, but for adding someone, which
71 // takes an email address as well as a username.
72 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
73 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
74 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
75 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
76 route(
77 "GET",
78 "/repos/:owner/:name/collaborators/:username/permission",
79 Op::GetCollaboratorPermission,
80 &[],
81 ),
82 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
83 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
84 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca85 // Deploy keys, at GitHub's addresses.
86 route("GET", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::ListDeployKeys), &[]),
87 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
88 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
89 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
Merge branch 'mirroring' into artifacts-mode90 // Mirroring: a repository's remotes, takeovers and hand-backs.
91 route("GET", "/repos/:owner/:name/mirror", Op::Mirrors(MirrorsOp::GetMirror), &[]),
92 route("GET", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::GetHandBackPlan), &[]),
93 route("POST", "/repos/:owner/:name/mirror/take-over", Op::Mirrors(MirrorsOp::TakeOver), &[]),
94 route("POST", "/repos/:owner/:name/mirror/ci", Op::Mirrors(MirrorsOp::SetCiFailover), &[]),
95 route("POST", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::HandBack), &[]),
96 route("POST", "/repos/:owner/:name/mirror/move-to-g1t", Op::Mirrors(MirrorsOp::MoveToG1t), &[]),
97 route("POST", "/repos/:owner/:name/mirror/sync", Op::Mirrors(MirrorsOp::SyncMirror), &[]),
98 route("POST", "/repos/:owner/:name/mirror/remotes", Op::Mirrors(MirrorsOp::AddRemote), &[]),
99 route("PATCH", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::UpdateRemote), &[]),
100 route("DELETE", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::RemoveRemote), &[]),
API: notifications over REST and MCP, with notifications scopes101 // Your notifications: threads, marking them, and what you subscribe
102 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
103 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")]),
104 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
105 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
106 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
107 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
108 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
109 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
110 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
111 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
112 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
113 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
114 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
115 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")]),
116 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
117 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
118 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
119 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
120 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
121 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
122 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
123 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97124 // Stars: yours, and who starred a repository.
125 route("GET", "/user/starred", Op::About(AboutOp::ListStarred), &[]),
126 route("GET", "/user/starred/:owner/:name", Op::About(AboutOp::CheckStarred), &[]),
127 route("PUT", "/user/starred/:owner/:name", Op::About(AboutOp::Star), &[]),
128 route("DELETE", "/user/starred/:owner/:name", Op::About(AboutOp::Unstar), &[]),
129 route("GET", "/repos/:owner/:name/stargazers", Op::About(AboutOp::ListStargazers), &[("page", "page")]),
130 // What the default branch says about a repository, kept by commit.
131 route("GET", "/repos/:owner/:name/languages", Op::About(AboutOp::GetLanguages), &[]),
132 route("GET", "/repos/:owner/:name/contributors", Op::About(AboutOp::ListContributors), &[]),
133 route("GET", "/repos/:owner/:name/license", Op::About(AboutOp::GetLicense), &[]),
134 // Releases: `latest` and `tags/…` before an id.
135 route("GET", "/repos/:owner/:name/releases", Op::About(AboutOp::ListReleases), &[]),
136 route("POST", "/repos/:owner/:name/releases", Op::About(AboutOp::CreateRelease), &[]),
137 route("GET", "/repos/:owner/:name/releases/latest", Op::About(AboutOp::GetLatestRelease), &[]),
138 route("GET", "/repos/:owner/:name/releases/tags/:tag", Op::About(AboutOp::GetReleaseByTag), &[]),
139 route("GET", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::GetRelease), &[]),
140 route("PATCH", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::UpdateRelease), &[]),
141 route("DELETE", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::DeleteRelease), &[]),
API: pinned projects over REST and MCP142 // Your pinned projects in a workspace, in your order.
143 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
144 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
145 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
146 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look147 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
148 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily149 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
Merge main (membership, two-factor, GitHub repo roles) into tokens150 // Members and owners: GitHub's organization members, by username.
151 route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]),
152 route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]),
153 route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]),
154 route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]),
155 route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97156 // A workspace's projects: what each is, where it runs, its links.
157 route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
158 route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
159 route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily160 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 route(
162 "GET",
163 "/workspaces/:workspace/outside_collaborators",
164 Op::ListOutsideCollaborators,
165 &[],
166 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar167 // Teams: a workspace's groups of members, with roles on repositories.
168 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
169 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
170 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
171 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
172 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
173 route(
174 "GET",
175 "/workspaces/:workspace/teams/:team/members",
176 Op::ListTeamMembers,
177 &[("include_child_teams", "include_child_teams")],
178 ),
179 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
180 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
181 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
182 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
183 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
184 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
185 route(
186 "PUT",
187 "/workspaces/:workspace/teams/:team/review_assignment",
188 Op::SetTeamReviewAssignment,
189 &[],
190 ),
191 route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit192 // A workspace's billing: usage, budget, AI credit and invoices.
193 route(
194 "GET",
195 "/workspaces/:workspace/usage",
196 Op::GetUsage,
197 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
198 ),
199 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
200 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
201 route("GET", "/workspaces/:workspace/ai_credit", Op::GetAiCredit, &[]),
202 route("POST", "/workspaces/:workspace/ai_credit/checkout", Op::BuyAiCredit, &[]),
203 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
204 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens205 // The AI Gateway's log of a workspace's requests.
206 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 bar207 // Code owners: the CODEOWNERS file, checked.
208 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 daily209 // Security alerts: secrets and vulnerable dependencies.
210 route(
211 "GET",
212 "/repos/:owner/:name/security/alerts",
213 Op::ListSecurityAlerts,
214 &[("state", "state"), ("kind", "kind")],
215 ),
216 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
217 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 bar218 // The security suite: secret scanning, code scanning, vulnerability
219 // alerts and the supply chain, at the common addresses.
220 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
221 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
222 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
223 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
224 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
225 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
226 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
227 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
228 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
229 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
230 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
231 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
232 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
233 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
234 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
235 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
236 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
237 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
238 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
239 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
240 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
241 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
242 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
243 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
244 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
245 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
246 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
247 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
248 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
249 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
250 route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]),
251 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
252 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
253 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
254 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
255 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
256 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
257 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
258 route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9259 // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
260 route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
261 route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
262 route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
263 route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
264 route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
265 route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
266 route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
267 route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
Catching up with main takes seconds when the two sides touched different files268 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
269 route(
270 "GET",
271 "/search",
272 Op::Search,
273 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
274 ),
275 route("POST", "/repos", Op::CreateRepo, &[]),
276 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
277 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look278 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
279 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
280 route(
281 "GET",
282 "/workspaces/:workspace/repos/deleted",
283 Op::ListDeletedRepos,
284 &[],
285 ),
286 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
287 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
288 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
289 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
290 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
291 route(
292 "POST",
293 "/repos/:owner/:name/visibility",
294 Op::SetRepoVisibility,
295 &[],
296 ),
297 route(
298 "POST",
299 "/repos/:owner/:name/branches/:branch/rename",
300 Op::RenameBranch,
301 &[],
302 ),
The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days.303 route("POST", "/repos/:owner/:name/branches", Op::CreateBranch, &[]),
304 route("DELETE", "/repos/:owner/:name/branches/:branch", Op::DeleteBranch, &[("head", "head")]),
Catching up with main takes seconds when the two sides touched different files305 route(
306 "GET",
307 "/repos/:owner/:name/settings",
308 Op::GetRepoSettings,
309 &[],
310 ),
311 route(
312 "PATCH",
313 "/repos/:owner/:name/settings",
314 Op::UpdateRepoSettings,
315 &[],
316 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents317 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit318 // Checks: GitHub's addresses for statuses, check runs and check suites.
319 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
320 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
321 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
322 route(
323 "GET",
324 "/repos/:owner/:name/commits/:ref/check-runs",
325 Op::Checks(ChecksOp::ListCheckRunsForRef),
326 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
327 ),
328 route(
329 "GET",
330 "/repos/:owner/:name/commits/:ref/check-suites",
331 Op::Checks(ChecksOp::ListCheckSuitesForRef),
332 &[("app", "app"), ("check_name", "check_name")],
333 ),
334 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
335 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
336 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
337 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
338 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
339 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
340 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 merge341 // Rulesets: a repository's, a workspace's, the rules of one branch,
342 // and how they judged pushes and merges.
343 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
344 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
345 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
346 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
347 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
348 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
349 route(
350 "GET",
351 "/repos/:owner/:name/rules/evaluations",
352 Op::Rules(RulesOp::ListRuleEvaluations),
353 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
354 ),
355 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
356 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
357 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
358 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
359 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
360 route(
361 "GET",
362 "/workspaces/:workspace/rules/evaluations",
363 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
364 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
365 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97366 // Deployments wherever they run, their statuses, and environments.
367 route(
368 "GET",
369 "/repos/:owner/:name/deployments",
370 Op::Deployments(DeploymentsOp::ListDeployments),
371 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
372 ),
373 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9374 // A g1t Actions job opening a g1t.page build it runs itself.
375 route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97376 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
377 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
378 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
379 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
380 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'381 // Environments' protection rules, and the runs they hold.
382 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
383 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
384 route(
385 "GET",
386 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
387 Op::Protection(ProtectionOp::GetPendingDeployments),
388 &[],
389 ),
390 route(
391 "POST",
392 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
393 Op::Protection(ProtectionOp::ReviewPendingDeployments),
394 &[],
395 ),
396 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
397 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
398 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
399 route(
400 "GET",
401 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
402 Op::Protection(ProtectionOp::GetForkPrApproval),
403 &[],
404 ),
405 route(
406 "PUT",
407 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
408 Op::Protection(ProtectionOp::SetForkPrApproval),
409 &[],
410 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts411 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
412 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'413 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
414 route(
415 "GET",
416 "/workspaces/:workspace/actions/permissions/workflow",
417 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
418 &[],
419 ),
420 route(
421 "PUT",
422 "/workspaces/:workspace/actions/permissions/workflow",
423 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
424 &[],
425 ),
Catching up with main takes seconds when the two sides touched different files426 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
427 route(
428 "POST",
429 "/repos/:owner/:name/pulls/:number/messages",
430 Op::MessageAgent,
431 &[],
432 ),
433 route(
434 "POST",
435 "/repos/:owner/:name/pulls/:number/messages/take",
436 Op::TakeMessages,
437 &[],
438 ),
439 route(
440 "POST",
441 "/repos/:owner/:name/messages/:id/answer",
442 Op::AnswerMessage,
443 &[],
444 ),
445 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
446 route(
447 "GET",
448 "/repos/:owner/:name/memory",
449 Op::Recall,
450 &[("q", "query"), ("limit", "limit")],
451 ),
452 route(
453 "GET",
454 "/repos/:owner/:name/events",
455 Op::ListEvents,
456 &[("before", "before")],
457 ),
458 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar459 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
460 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
461 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
462 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
463 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
464 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
465 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
466 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
467 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
468 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
469 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
470 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
471 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
472 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Catching up with main takes seconds when the two sides touched different files473 route(
474 "GET",
475 "/repos/:owner/:name/issues",
476 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar477 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Catching up with main takes seconds when the two sides touched different files478 ),
479 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
480 route(
481 "GET",
482 "/repos/:owner/:name/issues/:number",
483 Op::GetIssue,
484 &[],
485 ),
486 route(
487 "PATCH",
488 "/repos/:owner/:name/issues/:number",
489 Op::UpdateIssue,
490 &[],
491 ),
492 route(
493 "POST",
494 "/repos/:owner/:name/issues/:number/close",
495 Op::CloseIssue,
496 &[],
497 ),
498 route(
499 "POST",
500 "/repos/:owner/:name/issues/:number/reopen",
501 Op::ReopenIssue,
502 &[],
503 ),
504 route(
505 "POST",
506 "/repos/:owner/:name/issues/:number/assign",
507 Op::AssignIssue,
508 &[],
509 ),
510 route(
511 "POST",
512 "/repos/:owner/:name/issues/import",
513 Op::ImportIssue,
514 &[],
515 ),
516 route(
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step517 "POST",
518 "/repos/:owner/:name/issues/delegate",
519 Op::Delegate,
520 &[],
521 ),
522 route(
Catching up with main takes seconds when the two sides touched different files523 "GET",
524 "/repos/:owner/:name/context",
525 Op::GetContext,
526 &[("reference", "reference")],
527 ),
528 route(
529 "GET",
530 "/workspaces/:workspace/context/search",
531 Op::SearchContext,
532 &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
533 ),
534 route(
535 "GET",
536 "/workspaces/:workspace/context/:kind/:id",
537 Op::GetEntity,
538 &[],
539 ),
540 route(
541 "GET",
542 "/workspaces/:workspace/integrations",
543 Op::ListIntegrations,
544 &[],
545 ),
546 route(
547 "POST",
548 "/workspaces/:workspace/integrations",
549 Op::ConnectIntegration,
550 &[],
551 ),
552 route(
553 "GET",
554 "/repos/:owner/:name/hooks",
555 Op::ListWebhooks,
556 &[],
557 ),
558 route(
559 "POST",
560 "/repos/:owner/:name/hooks",
561 Op::CreateWebhook,
562 &[],
563 ),
564 route(
565 "PATCH",
566 "/repos/:owner/:name/hooks/:id",
567 Op::UpdateWebhook,
568 &[],
569 ),
570 route(
571 "DELETE",
572 "/repos/:owner/:name/hooks/:id",
573 Op::DeleteWebhook,
574 &[],
575 ),
576 route(
577 "POST",
578 "/repos/:owner/:name/hooks/:id/pings",
579 Op::PingWebhook,
580 &[],
581 ),
582 route(
583 "GET",
584 "/repos/:owner/:name/hooks/:id/deliveries",
585 Op::ListWebhookDeliveries,
586 &[],
587 ),
588 route(
589 "POST",
590 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
591 Op::RedeliverWebhook,
592 &[],
593 ),
594 route(
595 "GET",
596 "/workspaces/:workspace/hooks",
597 Op::ListWebhooks,
598 &[],
599 ),
600 route(
601 "POST",
602 "/workspaces/:workspace/hooks",
603 Op::CreateWebhook,
604 &[],
605 ),
606 route(
607 "PATCH",
608 "/workspaces/:workspace/hooks/:id",
609 Op::UpdateWebhook,
610 &[],
611 ),
612 route(
613 "DELETE",
614 "/workspaces/:workspace/hooks/:id",
615 Op::DeleteWebhook,
616 &[],
617 ),
618 route(
619 "POST",
620 "/workspaces/:workspace/hooks/:id/pings",
621 Op::PingWebhook,
622 &[],
623 ),
624 route(
625 "GET",
626 "/workspaces/:workspace/hooks/:id/deliveries",
627 Op::ListWebhookDeliveries,
628 &[],
629 ),
630 route(
631 "POST",
632 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
633 Op::RedeliverWebhook,
634 &[],
635 ),
636 route(
637 "GET",
638 "/workspaces/:workspace/model-routes",
639 Op::GetModelRoutes,
640 &[],
641 ),
642 route(
643 "PUT",
644 "/workspaces/:workspace/model-routes",
645 Op::SetModelRoutes,
646 &[],
647 ),
648 route(
AI Gateway: OpenAI's format, open models, and your own providers649 "PATCH",
650 "/workspaces/:workspace/integrations/:id",
651 Op::UpdateIntegration,
652 &[],
653 ),
654 route(
Catching up with main takes seconds when the two sides touched different files655 "DELETE",
656 "/workspaces/:workspace/integrations/:id",
657 Op::DisconnectIntegration,
658 &[],
659 ),
660 route(
661 "POST",
662 "/workspaces/:workspace/integrations/:id/test",
663 Op::TestIntegration,
664 &[],
665 ),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9666 // Customize: marketplaces, Discover, plugins and connectors. A plugin's
667 // ref is `name@marketplace`, URL-encoded (`@` as `%40`).
668 route("GET", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), &[]),
669 route("POST", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::AddMarketplace), &[]),
670 route("DELETE", "/workspaces/:workspace/customize/marketplaces/:marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), &[]),
671 route("POST", "/workspaces/:workspace/customize/marketplaces/:marketplace/refresh", Op::Customize(CustomizeOp::RefreshMarketplace), &[]),
672 route("GET", "/workspaces/:workspace/customize/discover", Op::Customize(CustomizeOp::Discover), &[]),
673 route("GET", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::ListPlugins), &[]),
674 route("POST", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::InstallPlugin), &[]),
675 route("GET", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::GetPlugin), &[]),
676 route("PATCH", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::SetPluginEnabled), &[]),
677 route("DELETE", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::UninstallPlugin), &[]),
678 route("POST", "/workspaces/:workspace/customize/plugins/:ref/update", Op::Customize(CustomizeOp::UpdatePlugin), &[]),
679 route("GET", "/workspaces/:workspace/customize/agents", Op::Customize(CustomizeOp::ListPluginAgents), &[]),
680 route("GET", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::ListConnectors), &[]),
681 route("POST", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::AddConnector), &[]),
682 route("GET", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::GetConnector), &[]),
683 route("PATCH", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::SetConnectorKey), &[]),
684 route("DELETE", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::RemoveConnector), &[]),
685 route("POST", "/workspaces/:workspace/customize/connectors/:id/refresh", Op::Customize(CustomizeOp::RefreshConnector), &[]),
686 route("POST", "/workspaces/:workspace/customize/connectors/:id/grants", Op::Customize(CustomizeOp::GrantConnector), &[]),
687 route("DELETE", "/workspaces/:workspace/customize/connectors/:id/grants/:grant", Op::Customize(CustomizeOp::RevokeConnector), &[]),
688 route("GET", "/workspaces/:workspace/customize/bundle", Op::Customize(CustomizeOp::Bundle), &[]),
Catching up with main takes seconds when the two sides touched different files689 route(
690 "GET",
691 "/repos/:owner/:name/actions/workflows",
692 Op::ListWorkflows,
693 &[],
694 ),
695 route(
696 "GET",
697 "/repos/:owner/:name/actions/workflows/:workflow/runs",
698 Op::ListWorkflowRuns,
699 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
700 ),
701 route(
702 "POST",
703 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
704 Op::DispatchWorkflow,
705 &[],
706 ),
707 route(
708 "PATCH",
709 "/repos/:owner/:name/actions/workflows/:workflow",
710 Op::UpdateWorkflow,
711 &[],
712 ),
713 route(
714 "PUT",
715 "/repos/:owner/:name/actions/workflows/:workflow/enable",
716 Op::UpdateWorkflow,
717 &[],
718 ),
719 route(
720 "PUT",
721 "/repos/:owner/:name/actions/workflows/:workflow/disable",
722 Op::UpdateWorkflow,
723 &[],
724 ),
725 route(
726 "GET",
727 "/repos/:owner/:name/actions/runs",
728 Op::ListWorkflowRuns,
729 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
730 ),
731 route(
732 "GET",
733 "/repos/:owner/:name/actions/runs/:id",
734 Op::GetWorkflowRun,
735 &[],
736 ),
737 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)738 "GET",
739 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
740 Op::GetWorkflowRun,
741 &[],
742 ),
743 route(
Catching up with main takes seconds when the two sides touched different files744 "POST",
745 "/repos/:owner/:name/actions/runs/:id/cancel",
746 Op::CancelWorkflowRun,
747 &[],
748 ),
749 route(
750 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)751 "/repos/:owner/:name/actions/runs/:id/force-cancel",
752 Op::CancelWorkflowRun,
753 &[],
754 ),
755 route(
756 "POST",
Catching up with main takes seconds when the two sides touched different files757 "/repos/:owner/:name/actions/runs/:id/rerun",
758 Op::RerunWorkflowRun,
759 &[],
760 ),
761 route(
762 "POST",
763 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
764 Op::RerunWorkflowRun,
765 &[],
766 ),
767 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)768 "POST",
769 "/repos/:owner/:name/actions/jobs/:job/rerun",
770 Op::RerunWorkflowRun,
771 &[],
772 ),
773 route(
Catching up with main takes seconds when the two sides touched different files774 "GET",
775 "/repos/:owner/:name/actions/jobs/:job/logs",
776 Op::GetJobLogs,
777 &[("after", "after")],
778 ),
Merge packages: roles, Actions access, source label, soft delete, API779 // Packages, at GitHub's addresses with the workspace in place of the
780 // organization. A name with a slash is one URL-encoded segment.
781 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
782 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
783 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
784 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
785 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
786 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
787 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
788 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
789 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
790 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
791 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
792 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
793 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
794 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
795 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
796 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
797 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 R2798 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
799 // a signed link (lib.rs).
800 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
801 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
802 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
803 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
804 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
805 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
806 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
Catching up with main takes seconds when the two sides touched different files807 route(
808 "GET",
809 "/repos/:owner/:name/actions/secrets",
810 Op::ListActionsSecrets,
811 &[],
812 ),
813 route(
814 "PUT",
815 "/repos/:owner/:name/actions/secrets/:setting",
816 Op::SetActionsSecret,
817 &[],
818 ),
819 route(
820 "DELETE",
821 "/repos/:owner/:name/actions/secrets/:setting",
822 Op::DeleteActionsSecret,
823 &[],
824 ),
825 route(
826 "GET",
827 "/repos/:owner/:name/actions/variables",
828 Op::ListActionsVariables,
829 &[],
830 ),
831 route(
832 "POST",
833 "/repos/:owner/:name/actions/variables",
834 Op::SetActionsVariable,
835 &[],
836 ),
837 route(
838 "PATCH",
839 "/repos/:owner/:name/actions/variables/:setting",
840 Op::SetActionsVariable,
841 &[],
842 ),
843 route(
844 "DELETE",
845 "/repos/:owner/:name/actions/variables/:setting",
846 Op::DeleteActionsVariable,
847 &[],
848 ),
849 route(
850 "GET",
851 "/workspaces/:workspace/actions/secrets",
852 Op::ListActionsSecrets,
853 &[],
854 ),
855 route(
856 "PUT",
857 "/workspaces/:workspace/actions/secrets/:setting",
858 Op::SetActionsSecret,
859 &[],
860 ),
861 route(
862 "DELETE",
863 "/workspaces/:workspace/actions/secrets/:setting",
864 Op::DeleteActionsSecret,
865 &[],
866 ),
867 route(
868 "GET",
869 "/workspaces/:workspace/actions/variables",
870 Op::ListActionsVariables,
871 &[],
872 ),
873 route(
874 "POST",
875 "/workspaces/:workspace/actions/variables",
876 Op::SetActionsVariable,
877 &[],
878 ),
879 route(
880 "PATCH",
881 "/workspaces/:workspace/actions/variables/:setting",
882 Op::SetActionsVariable,
883 &[],
884 ),
885 route(
886 "DELETE",
887 "/workspaces/:workspace/actions/variables/:setting",
888 Op::DeleteActionsVariable,
889 &[],
890 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents891 // Self-hosted runners: a repository's own, or a workspace's.
892 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
893 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
894 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
895 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
896 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
897 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
898 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
899 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
900 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
901 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
902 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
903 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
904 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
905 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.906 // A workspace agent's own computer.
907 route("GET", "/workspaces/:workspace/agents/:agent/computer", Op::GetAgentComputer, &[]),
908 route("POST", "/workspaces/:workspace/agents/:agent/computer/wake", Op::WakeAgentComputer, &[]),
909 route("POST", "/workspaces/:workspace/agents/:agent/computer/sleep", Op::SleepAgentComputer, &[]),
910 route("POST", "/workspaces/:workspace/agents/:agent/computer/reset", Op::ResetAgentComputer, &[]),
911 route("GET", "/workspaces/:workspace/agents/:agent/computer/commands", Op::ListAgentComputerCommands, &[]),
Catching up with main takes seconds when the two sides touched different files912 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
913 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
914 route(
915 "POST",
916 "/repos/:owner/:name/plans/:plan/apply",
917 Op::ApplyPlan,
918 &[],
919 ),
920 route(
921 "POST",
922 "/repos/:owner/:name/issues/:number/comments",
923 Op::AddComment,
924 &[],
925 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts926 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
927 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Catching up with main takes seconds when the two sides touched different files928 route(
929 "GET",
930 "/repos/:owner/:name/pulls",
931 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar932 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Catching up with main takes seconds when the two sides touched different files933 ),
934 route(
935 "POST",
936 "/repos/:owner/:name/pulls",
937 Op::CreatePullRequest,
938 &[],
939 ),
940 route(
941 "GET",
942 "/repos/:owner/:name/pulls/:number",
943 Op::GetPullRequest,
944 &[],
945 ),
946 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar947 "PATCH",
948 "/repos/:owner/:name/pulls/:number",
949 Op::UpdatePullRequest,
950 &[],
951 ),
952 route(
Catching up with main takes seconds when the two sides touched different files953 "GET",
954 "/repos/:owner/:name/pulls/:number/changes",
955 Op::GetPullRequestChanges,
956 &[],
957 ),
958 route(
959 "POST",
960 "/repos/:owner/:name/pulls/:number/reviews",
961 Op::ReviewPullRequest,
962 &[],
963 ),
964 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar965 "POST",
966 "/repos/:owner/:name/pulls/:number/requested_reviewers",
967 Op::RequestReviewers,
968 &[],
969 ),
970 route(
971 "DELETE",
972 "/repos/:owner/:name/pulls/:number/requested_reviewers",
973 Op::RemoveRequestedReviewers,
974 &[],
975 ),
976 route(
Catching up with main takes seconds when the two sides touched different files977 "GET",
978 "/repos/:owner/:name/pulls/:number/session",
979 Op::ReadSession,
980 &[("after", "after")],
981 ),
982 route(
983 "POST",
984 "/repos/:owner/:name/pulls/:number/session",
985 Op::RecordSession,
986 &[],
987 ),
988 route(
989 "POST",
990 "/repos/:owner/:name/pulls/:number/ready",
991 Op::MarkPullRequestReady,
992 &[],
993 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts994 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Catching up with main takes seconds when the two sides touched different files995 route(
996 "POST",
997 "/repos/:owner/:name/pulls/:number/close",
998 Op::ClosePullRequest,
999 &[],
1000 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1001 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Catching up with main takes seconds when the two sides touched different files1002 route(
1003 "POST",
1004 "/repos/:owner/:name/pulls/:number/merge",
1005 Op::MergePullRequest,
1006 &[],
1007 ),
Merge main into Artifacts Phase 21008 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1009 // before an artifact by id, which it would otherwise match.
1010 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")]),
1011 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1012 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1013 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1014 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1015 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1016 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1017 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1018 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1019 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1020 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1021 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1022 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1023 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1024 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1025 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1026 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Catching up with main takes seconds when the two sides touched different files1027];
1028
1029impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1030 /// Whether the route answers success with `204` and no body, as
1031 /// GitHub's address for the same does. MCP still answers `true`.
1032 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91033 matches!(
1034 self.op,
1035 Op::DeleteComment
1036 | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector)
1037 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1038 }
1039
Catching up with main takes seconds when the two sides touched different files1040 /// The names of the route's path parameters, in order.
1041 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1042 self.path
1043 .split('/')
1044 .filter_map(|segment| segment.strip_prefix(':'))
1045 }
1046
1047 /// The values of the path parameters, if `path` is this route's.
1048 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1049 let mut values = Vec::new();
1050 let mut actual = path.trim_end_matches('/').split('/');
1051 for expected in self.path.split('/') {
1052 let segment = actual.next()?;
1053 match expected.strip_prefix(':') {
1054 Some(name) if !segment.is_empty() => values.push((name, segment)),
1055 Some(_) => return None,
1056 None if expected == segment => {}
1057 None => return None,
1058 }
1059 }
1060 actual.next().is_none().then_some(values)
1061 }
1062}
1063
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1064/// A path segment with its `%XX` escapes decoded; as given when that is not
1065/// UTF-8.
1066fn percent_decoded(segment: &str) -> String {
1067 let bytes = segment.as_bytes();
1068 let mut out = Vec::with_capacity(bytes.len());
1069 let mut i = 0;
1070 while i < bytes.len() {
1071 let escaped = (bytes[i] == b'%')
1072 .then(|| segment.get(i + 1..i + 3))
1073 .flatten()
1074 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1075 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1076 match escaped {
1077 Some(byte) => {
1078 out.push(byte);
1079 i += 3;
1080 }
1081 None => {
1082 out.push(bytes[i]);
1083 i += 1;
1084 }
1085 }
1086 }
1087 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1088}
1089
Catching up with main takes seconds when the two sides touched different files1090/// The route for a request, and the operation input it describes.
1091///
1092/// The input is the JSON body, overlaid with the query parameters the route
1093/// 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 bar1094/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1095/// becomes an integer.
Catching up with main takes seconds when the two sides touched different files1096pub fn resolve(
1097 method: &str,
1098 path: &str,
1099 query: &[(String, String)],
1100 body: Value,
1101) -> Option<(&'static Route, Value)> {
1102 let (route, params) = ROUTES
1103 .iter()
1104 .filter(|route| route.method == method)
1105 .find_map(|route| Some((route, route.matches(path)?)))?;
1106
1107 let mut input = match body {
1108 Value::Object(fields) => fields,
1109 _ => Map::new(),
1110 };
1111 for (name, key) in route.query {
1112 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1113 input.insert((*key).to_owned(), Value::String(value.clone()));
1114 }
1115 }
1116 let param = |wanted: &str| {
1117 params
1118 .iter()
1119 .find(|(name, _)| *name == wanted)
1120 .map(|(_, value)| *value)
1121 };
1122 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1123 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1124 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971125 // Every other name the path gives, under that name; the ones below that
1126 // need more (a repository, a number, an encoded name) are set after.
1127 for (key, value) in &params {
1128 if !matches!(*key, "owner" | "name") {
1129 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Catching up with main takes seconds when the two sides touched different files1130 }
1131 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1132 // A repository of a team's workspace, named by itself.
1133 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1134 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1135 }
1136 // A branch name may hold slashes, sent URL-encoded as one segment, and
1137 // a label's name spaces.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1138 if let Some(branch) = param("branch") {
1139 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1140 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971141 // An environment's name may hold slashes and spaces, URL-encoded.
1142 if let Some(environment) = param("environment") {
1143 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1144 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1145 if let Some(label) = param("label") {
1146 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1147 }
1148 if let Some(milestone) = param("milestone") {
1149 // Not a number: zero, which no milestone has.
1150 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1151 }
Catching up with main takes seconds when the two sides touched different files1152 // GitHub says some things with the path alone.
1153 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1154 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1155 }
1156 if route.path.ends_with("/rerun-failed-jobs") {
1157 input.insert("failed_only".to_owned(), Value::Bool(true));
1158 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1159 if route.path.ends_with("/force-cancel") {
1160 input.insert("force".to_owned(), Value::Bool(true));
1161 }
API: notifications over REST and MCP, with notifications scopes1162 // Unsaving and waking a thread are a DELETE of what PUT made.
1163 if route.method == "DELETE" && route.path.ends_with("/saved") {
1164 input.insert("saved".to_owned(), Value::Bool(false));
1165 }
1166 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1167 input.remove("until");
1168 }
Catching up with main takes seconds when the two sides touched different files1169 if let Some(number) = param("number") {
1170 // Not a number: zero, which no issue or pull request has.
1171 input.insert(
1172 "number".to_owned(),
1173 number.parse::<u32>().unwrap_or(0).into(),
1174 );
1175 }
1176 Some((route, Value::Object(input)))
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use serde_json::json;
1182
1183 use super::*;
1184
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971185 /// Every name a route's path gives reaches the operation: a name left
1186 /// off the lists above is dropped, and the operation answers that it
1187 /// was not given (the project routes were, until this test).
1188 #[test]
1189 fn every_path_parameter_reaches_the_input() {
1190 for route in ROUTES.iter() {
1191 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1192 if names.is_empty() {
1193 continue;
1194 }
1195 let path: String = route
1196 .path
1197 .split('/')
1198 .map(|part| match part.strip_prefix(':') {
1199 Some("number" | "milestone") => "7".to_owned(),
1200 Some(name) => format!("{name}-x"),
1201 None => part.to_owned(),
1202 })
1203 .collect::<Vec<_>>()
1204 .join("/");
1205 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1206 // A path two routes could take is checked under the first.
1207 if found.path != route.path {
1208 continue;
1209 }
1210 for name in names {
1211 let key = match name {
1212 "owner" | "name" => "repo",
1213 "repo" if names_has_workspace(route.path) => "repo",
1214 other => other,
1215 };
1216 assert!(
1217 input.get(key).is_some_and(|value| !value.is_null()),
1218 "{} {}: :{name} does not reach the input",
1219 route.method,
1220 route.path
1221 );
1222 }
1223 }
1224 }
1225
1226 fn names_has_workspace(path: &str) -> bool {
1227 path.split('/').any(|part| part == ":workspace")
1228 }
1229
Catching up with main takes seconds when the two sides touched different files1230 #[test]
1231 fn a_path_resolves_to_its_operation_and_input() {
1232 let (route, input) = resolve(
1233 "POST",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1234 "/repos/flagon-io/hello/pulls/14/merge",
Catching up with main takes seconds when the two sides touched different files1235 &[],
1236 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1237 )
1238 .unwrap();
1239 assert_eq!(route.op, Op::MergePullRequest);
1240 // What the path names wins over the body.
1241 assert_eq!(
1242 input,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1243 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
Catching up with main takes seconds when the two sides touched different files1244 );
1245 }
1246
1247 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1248 fn a_branch_with_slashes_is_one_encoded_segment() {
1249 let (route, input) = resolve(
1250 "POST",
1251 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1252 &[],
1253 json!({ "new_name": "feature/sign-in" }),
1254 )
1255 .unwrap();
1256 assert_eq!(route.op, Op::RenameBranch);
1257 assert_eq!(
1258 input,
1259 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1260 );
1261 assert_eq!(percent_decoded("100%"), "100%");
1262 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1263 }
1264
1265 #[test]
1266 fn a_collaborator_is_named_by_username() {
1267 let (route, input) = resolve(
1268 "PATCH",
1269 "/repos/flagon-io/hello/collaborators/ada",
1270 &[],
1271 json!({ "role": "maintain" }),
1272 )
1273 .unwrap();
1274 assert_eq!(route.op, Op::UpdateCollaborator);
1275 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1276 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1277 assert_eq!(route.op, Op::DeclineRepoInvitation);
1278 assert_eq!(input, json!({ "id": "rin_1" }));
1279 }
1280
1281 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1282 fn teams_are_addressed_by_workspace_and_slug() {
1283 let query = [("q".to_owned(), "back".to_owned())];
1284 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1285 assert_eq!(route.op, Op::ListTeams);
1286 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1287 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1288 assert_eq!(route.op, Op::UpdateTeam);
1289 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1290 let (route, input) =
1291 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1292 assert_eq!(route.op, Op::SetTeamMember);
1293 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1294 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1295 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1296 assert_eq!(route.op, Op::ListTeamMembers);
1297 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1298 // A repository is named by itself, in the team's workspace.
1299 let (route, input) =
1300 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1301 assert_eq!(route.op, Op::SetTeamRepo);
1302 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1303 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1304 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1305 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1306 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1307 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1308 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1309 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1310 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1311 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1312 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1313 assert_eq!(route.op, Op::ListUserTeams);
1314 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1315 }
1316
1317 #[test]
1318 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1319 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1320 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1321 assert_eq!(route.op, Op::RequestReviewers);
1322 assert_eq!(
1323 input,
1324 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1325 );
1326 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1327 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1328 let query = [("ref".to_owned(), "main".to_owned())];
1329 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1330 assert_eq!(route.op, Op::GetCodeownersErrors);
1331 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1332 }
1333
1334 #[test]
API: notifications over REST and MCP, with notifications scopes1335 fn notifications_are_addressed_as_threads_and_by_issue() {
1336 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1337 assert_eq!(route.op, Op::MarkThreadDone);
1338 assert_eq!(input, json!({ "id": "ntf_1" }));
1339 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1340 assert_eq!(route.op, Op::SaveThread);
1341 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1342 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1343 assert_eq!(route.op, Op::SnoozeThread);
1344 assert_eq!(input, json!({ "id": "ntf_1" }));
1345 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1346 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1347 assert_eq!(route.op, Op::ListNotifications);
1348 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1349 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1350 assert_eq!(route.op, Op::SetThreadSubscription);
1351 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1352 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1353 }
1354
1355 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1356 fn labels_and_milestones_are_named_in_the_path() {
1357 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1358 assert_eq!(route.op, Op::UpdateLabel);
1359 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1360 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1361 assert_eq!(route.op, Op::RemoveIssueLabels);
1362 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1363 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1364 assert_eq!(route.op, Op::RemoveIssueLabels);
1365 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1366 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1367 assert_eq!(route.op, Op::AddDefaultLabels);
1368 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1369 assert_eq!(route.op, Op::UpdateMilestone);
1370 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1371 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1372 assert_eq!(route.op, Op::UpdatePullRequest);
1373 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1374 }
1375
1376 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1377 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1378 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1379 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1380 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1381 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1382 // Reopening and closing with a PATCH, as `state`.
1383 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1384 assert_eq!(route.op, Op::UpdatePullRequest);
1385 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1386 let (route, input) =
1387 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1388 assert_eq!(route.op, Op::EditComment);
1389 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1390 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1391 assert_eq!(route.op, Op::DeleteComment);
1392 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1393 // Still an issue's comments, labels and subscription.
1394 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1395 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1396 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1397 }
1398
1399 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1400 fn billing_is_addressed_by_workspace() {
1401 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1402 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1403 assert_eq!(route.op, Op::GetUsage);
1404 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1405 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1406 assert_eq!(route.op, Op::SetBudget);
1407 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1408 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1409 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1410 assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit);
1411 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
1412 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1413 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1414 assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1415 }
1416
1417 #[test]
Merge checks: statuses and check runs on every commit1418 fn checks_are_at_githubs_addresses() {
1419 let sha = "a".repeat(40);
1420 let body = json!({ "state": "success", "context": "ci/build" });
1421 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1422 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1423 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1424 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1425 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1426 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1427 let query = [("check_name".to_owned(), "lint".to_owned())];
1428 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1429 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1430 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1431 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1432 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1433 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1434 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1435 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1436 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1437 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1438 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1439 }
1440
1441 #[test]
Catching up with main takes seconds when the two sides touched different files1442 fn query_parameters_are_renamed() {
1443 let query = [
1444 ("q".to_owned(), "parser".to_owned()),
1445 ("x".to_owned(), "y".to_owned()),
1446 ];
1447 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1448 assert_eq!(route.op, Op::ListRepos);
1449 assert_eq!(input, json!({ "query": "parser" }));
1450 }
1451
1452 #[test]
1453 fn method_and_shape_must_match() {
1454 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1455 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1456 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1457 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1458 }
1459
1460 #[test]
1461 fn every_parameter_and_query_name_is_an_input() {
1462 for route in ROUTES {
1463 let properties = route.op.properties();
1464 for (_, key) in route.query {
1465 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1466 }
1467 for name in route.params() {
1468 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1469 || properties.contains_key(name);
1470 assert!(covered, "{}: {name}", route.path);
1471 }
1472 }
1473 }
1474
1475 #[test]
1476 fn every_operation_has_a_route() {
1477 for op in Op::ALL {
1478 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1479 }
1480 }
1481}

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