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

Icons are cached at the edge1//! 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 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.7use 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;
Icons are cached at the edge15use 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;
Icons are cached at the edge19
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")]),
The API carries the advisory and report operations the service gained, with the two new repository security settings (history_alerts, private_vulnerability_reporting) on get_security_settings and update_security_settings, and the docs' OpenAPI copy is regenerated to match.259 // 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), &[]),
Icons are cached at the edge268 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
Search across all of g1t, Explore, and a command palette269 route(
270 "GET",
271 "/search",
272 Op::Search,
273 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
274 ),
Icons are cached at the edge275 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 ),
Icons are cached at the edge303 route(
304 "GET",
305 "/repos/:owner/:name/settings",
306 Op::GetRepoSettings,
307 &[],
308 ),
309 route(
310 "PATCH",
311 "/repos/:owner/:name/settings",
312 Op::UpdateRepoSettings,
313 &[],
314 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents315 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit316 // Checks: GitHub's addresses for statuses, check runs and check suites.
317 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
318 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
319 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
320 route(
321 "GET",
322 "/repos/:owner/:name/commits/:ref/check-runs",
323 Op::Checks(ChecksOp::ListCheckRunsForRef),
324 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
325 ),
326 route(
327 "GET",
328 "/repos/:owner/:name/commits/:ref/check-suites",
329 Op::Checks(ChecksOp::ListCheckSuitesForRef),
330 &[("app", "app"), ("check_name", "check_name")],
331 ),
332 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
333 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
334 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
335 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
336 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
337 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
338 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 merge339 // Rulesets: a repository's, a workspace's, the rules of one branch,
340 // and how they judged pushes and merges.
341 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
342 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
343 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
344 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
345 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
346 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
347 route(
348 "GET",
349 "/repos/:owner/:name/rules/evaluations",
350 Op::Rules(RulesOp::ListRuleEvaluations),
351 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
352 ),
353 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
354 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
355 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
356 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
357 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
358 route(
359 "GET",
360 "/workspaces/:workspace/rules/evaluations",
361 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
362 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
363 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97364 // Deployments wherever they run, their statuses, and environments.
365 route(
366 "GET",
367 "/repos/:owner/:name/deployments",
368 Op::Deployments(DeploymentsOp::ListDeployments),
369 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
370 ),
371 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
372 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
373 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
374 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
375 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
376 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'377 // Environments' protection rules, and the runs they hold.
378 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
379 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
380 route(
381 "GET",
382 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
383 Op::Protection(ProtectionOp::GetPendingDeployments),
384 &[],
385 ),
386 route(
387 "POST",
388 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
389 Op::Protection(ProtectionOp::ReviewPendingDeployments),
390 &[],
391 ),
392 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
393 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
394 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
395 route(
396 "GET",
397 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
398 Op::Protection(ProtectionOp::GetForkPrApproval),
399 &[],
400 ),
401 route(
402 "PUT",
403 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
404 Op::Protection(ProtectionOp::SetForkPrApproval),
405 &[],
406 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts407 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
408 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'409 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
410 route(
411 "GET",
412 "/workspaces/:workspace/actions/permissions/workflow",
413 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
414 &[],
415 ),
416 route(
417 "PUT",
418 "/workspaces/:workspace/actions/permissions/workflow",
419 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
420 &[],
421 ),
Icons are cached at the edge422 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
423 route(
424 "POST",
425 "/repos/:owner/:name/pulls/:number/messages",
426 Op::MessageAgent,
427 &[],
428 ),
429 route(
430 "POST",
431 "/repos/:owner/:name/pulls/:number/messages/take",
432 Op::TakeMessages,
433 &[],
434 ),
435 route(
436 "POST",
437 "/repos/:owner/:name/messages/:id/answer",
438 Op::AnswerMessage,
439 &[],
440 ),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains441 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
442 route(
443 "GET",
444 "/repos/:owner/:name/memory",
445 Op::Recall,
446 &[("q", "query"), ("limit", "limit")],
447 ),
Icons are cached at the edge448 route(
449 "GET",
450 "/repos/:owner/:name/events",
451 Op::ListEvents,
452 &[("before", "before")],
453 ),
454 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar455 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
456 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
457 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
458 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
459 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
460 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
461 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
462 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
463 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
464 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
465 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
466 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
467 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
468 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Icons are cached at the edge469 route(
470 "GET",
471 "/repos/:owner/:name/issues",
472 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar473 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Icons are cached at the edge474 ),
475 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
476 route(
477 "GET",
478 "/repos/:owner/:name/issues/:number",
479 Op::GetIssue,
480 &[],
481 ),
482 route(
483 "PATCH",
484 "/repos/:owner/:name/issues/:number",
485 Op::UpdateIssue,
486 &[],
487 ),
488 route(
489 "POST",
490 "/repos/:owner/:name/issues/:number/close",
491 Op::CloseIssue,
492 &[],
493 ),
494 route(
495 "POST",
496 "/repos/:owner/:name/issues/:number/reopen",
497 Op::ReopenIssue,
498 &[],
499 ),
500 route(
501 "POST",
502 "/repos/:owner/:name/issues/:number/assign",
503 Op::AssignIssue,
504 &[],
505 ),
506 route(
507 "POST",
508 "/repos/:owner/:name/issues/import",
509 Op::ImportIssue,
510 &[],
511 ),
512 route(
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step513 "POST",
514 "/repos/:owner/:name/issues/delegate",
515 Op::Delegate,
516 &[],
517 ),
518 route(
Icons are cached at the edge519 "GET",
520 "/repos/:owner/:name/context",
521 Op::GetContext,
522 &[("reference", "reference")],
523 ),
524 route(
525 "GET",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API526 "/workspaces/:workspace/context/search",
527 Op::SearchContext,
528 &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
529 ),
530 route(
531 "GET",
532 "/workspaces/:workspace/context/:kind/:id",
533 Op::GetEntity,
534 &[],
535 ),
536 route(
537 "GET",
Icons are cached at the edge538 "/workspaces/:workspace/integrations",
539 Op::ListIntegrations,
540 &[],
541 ),
542 route(
543 "POST",
544 "/workspaces/:workspace/integrations",
545 Op::ConnectIntegration,
546 &[],
547 ),
548 route(
549 "GET",
550 "/repos/:owner/:name/hooks",
551 Op::ListWebhooks,
552 &[],
553 ),
554 route(
555 "POST",
556 "/repos/:owner/:name/hooks",
557 Op::CreateWebhook,
558 &[],
559 ),
560 route(
561 "PATCH",
562 "/repos/:owner/:name/hooks/:id",
563 Op::UpdateWebhook,
564 &[],
565 ),
566 route(
567 "DELETE",
568 "/repos/:owner/:name/hooks/:id",
569 Op::DeleteWebhook,
570 &[],
571 ),
572 route(
573 "POST",
574 "/repos/:owner/:name/hooks/:id/pings",
575 Op::PingWebhook,
576 &[],
577 ),
578 route(
579 "GET",
580 "/repos/:owner/:name/hooks/:id/deliveries",
581 Op::ListWebhookDeliveries,
582 &[],
583 ),
584 route(
585 "POST",
586 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
587 Op::RedeliverWebhook,
588 &[],
589 ),
590 route(
591 "GET",
592 "/workspaces/:workspace/hooks",
593 Op::ListWebhooks,
594 &[],
595 ),
596 route(
597 "POST",
598 "/workspaces/:workspace/hooks",
599 Op::CreateWebhook,
600 &[],
601 ),
602 route(
603 "PATCH",
604 "/workspaces/:workspace/hooks/:id",
605 Op::UpdateWebhook,
606 &[],
607 ),
608 route(
609 "DELETE",
610 "/workspaces/:workspace/hooks/:id",
611 Op::DeleteWebhook,
612 &[],
613 ),
614 route(
615 "POST",
616 "/workspaces/:workspace/hooks/:id/pings",
617 Op::PingWebhook,
618 &[],
619 ),
620 route(
621 "GET",
622 "/workspaces/:workspace/hooks/:id/deliveries",
623 Op::ListWebhookDeliveries,
624 &[],
625 ),
626 route(
627 "POST",
628 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
629 Op::RedeliverWebhook,
630 &[],
631 ),
632 route(
633 "GET",
634 "/workspaces/:workspace/model-routes",
635 Op::GetModelRoutes,
636 &[],
637 ),
638 route(
639 "PUT",
640 "/workspaces/:workspace/model-routes",
641 Op::SetModelRoutes,
642 &[],
643 ),
644 route(
AI Gateway: OpenAI's format, open models, and your own providers645 "PATCH",
646 "/workspaces/:workspace/integrations/:id",
647 Op::UpdateIntegration,
648 &[],
649 ),
650 route(
Icons are cached at the edge651 "DELETE",
652 "/workspaces/:workspace/integrations/:id",
653 Op::DisconnectIntegration,
654 &[],
655 ),
656 route(
657 "POST",
658 "/workspaces/:workspace/integrations/:id/test",
659 Op::TestIntegration,
660 &[],
661 ),
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.662 // Customize: marketplaces, Discover, plugins and connectors. A plugin's
663 // ref is `name@marketplace`, URL-encoded (`@` as `%40`).
664 route("GET", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), &[]),
665 route("POST", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::AddMarketplace), &[]),
666 route("DELETE", "/workspaces/:workspace/customize/marketplaces/:marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), &[]),
667 route("POST", "/workspaces/:workspace/customize/marketplaces/:marketplace/refresh", Op::Customize(CustomizeOp::RefreshMarketplace), &[]),
668 route("GET", "/workspaces/:workspace/customize/discover", Op::Customize(CustomizeOp::Discover), &[]),
669 route("GET", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::ListPlugins), &[]),
670 route("POST", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::InstallPlugin), &[]),
671 route("GET", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::GetPlugin), &[]),
672 route("PATCH", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::SetPluginEnabled), &[]),
673 route("DELETE", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::UninstallPlugin), &[]),
674 route("POST", "/workspaces/:workspace/customize/plugins/:ref/update", Op::Customize(CustomizeOp::UpdatePlugin), &[]),
675 route("GET", "/workspaces/:workspace/customize/agents", Op::Customize(CustomizeOp::ListPluginAgents), &[]),
676 route("GET", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::ListConnectors), &[]),
677 route("POST", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::AddConnector), &[]),
678 route("GET", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::GetConnector), &[]),
679 route("PATCH", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::SetConnectorKey), &[]),
680 route("DELETE", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::RemoveConnector), &[]),
681 route("POST", "/workspaces/:workspace/customize/connectors/:id/refresh", Op::Customize(CustomizeOp::RefreshConnector), &[]),
682 route("POST", "/workspaces/:workspace/customize/connectors/:id/grants", Op::Customize(CustomizeOp::GrantConnector), &[]),
683 route("DELETE", "/workspaces/:workspace/customize/connectors/:id/grants/:grant", Op::Customize(CustomizeOp::RevokeConnector), &[]),
684 route("GET", "/workspaces/:workspace/customize/bundle", Op::Customize(CustomizeOp::Bundle), &[]),
Icons are cached at the edge685 route(
686 "GET",
687 "/repos/:owner/:name/actions/workflows",
688 Op::ListWorkflows,
689 &[],
690 ),
691 route(
692 "GET",
693 "/repos/:owner/:name/actions/workflows/:workflow/runs",
694 Op::ListWorkflowRuns,
695 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
696 ),
697 route(
698 "POST",
699 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
700 Op::DispatchWorkflow,
701 &[],
702 ),
703 route(
704 "PATCH",
705 "/repos/:owner/:name/actions/workflows/:workflow",
706 Op::UpdateWorkflow,
707 &[],
708 ),
709 route(
710 "PUT",
711 "/repos/:owner/:name/actions/workflows/:workflow/enable",
712 Op::UpdateWorkflow,
713 &[],
714 ),
715 route(
716 "PUT",
717 "/repos/:owner/:name/actions/workflows/:workflow/disable",
718 Op::UpdateWorkflow,
719 &[],
720 ),
721 route(
722 "GET",
723 "/repos/:owner/:name/actions/runs",
724 Op::ListWorkflowRuns,
725 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
726 ),
727 route(
728 "GET",
729 "/repos/:owner/:name/actions/runs/:id",
730 Op::GetWorkflowRun,
731 &[],
732 ),
733 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)734 "GET",
735 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
736 Op::GetWorkflowRun,
737 &[],
738 ),
739 route(
Icons are cached at the edge740 "POST",
741 "/repos/:owner/:name/actions/runs/:id/cancel",
742 Op::CancelWorkflowRun,
743 &[],
744 ),
745 route(
746 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)747 "/repos/:owner/:name/actions/runs/:id/force-cancel",
748 Op::CancelWorkflowRun,
749 &[],
750 ),
751 route(
752 "POST",
Icons are cached at the edge753 "/repos/:owner/:name/actions/runs/:id/rerun",
754 Op::RerunWorkflowRun,
755 &[],
756 ),
757 route(
758 "POST",
759 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
760 Op::RerunWorkflowRun,
761 &[],
762 ),
763 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)764 "POST",
765 "/repos/:owner/:name/actions/jobs/:job/rerun",
766 Op::RerunWorkflowRun,
767 &[],
768 ),
769 route(
Icons are cached at the edge770 "GET",
771 "/repos/:owner/:name/actions/jobs/:job/logs",
772 Op::GetJobLogs,
773 &[("after", "after")],
774 ),
Merge packages: roles, Actions access, source label, soft delete, API775 // Packages, at GitHub's addresses with the workspace in place of the
776 // organization. A name with a slash is one URL-encoded segment.
777 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
778 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
779 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
780 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
781 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
782 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
783 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
784 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
785 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
786 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
787 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
788 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
789 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
790 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
791 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
792 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
793 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 R2794 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
795 // a signed link (lib.rs).
796 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
797 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
798 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
799 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
800 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
801 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
802 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
Icons are cached at the edge803 route(
804 "GET",
805 "/repos/:owner/:name/actions/secrets",
806 Op::ListActionsSecrets,
807 &[],
808 ),
809 route(
810 "PUT",
811 "/repos/:owner/:name/actions/secrets/:setting",
812 Op::SetActionsSecret,
813 &[],
814 ),
815 route(
816 "DELETE",
817 "/repos/:owner/:name/actions/secrets/:setting",
818 Op::DeleteActionsSecret,
819 &[],
820 ),
821 route(
822 "GET",
823 "/repos/:owner/:name/actions/variables",
824 Op::ListActionsVariables,
825 &[],
826 ),
827 route(
828 "POST",
829 "/repos/:owner/:name/actions/variables",
830 Op::SetActionsVariable,
831 &[],
832 ),
833 route(
834 "PATCH",
835 "/repos/:owner/:name/actions/variables/:setting",
836 Op::SetActionsVariable,
837 &[],
838 ),
839 route(
840 "DELETE",
841 "/repos/:owner/:name/actions/variables/:setting",
842 Op::DeleteActionsVariable,
843 &[],
844 ),
845 route(
846 "GET",
847 "/workspaces/:workspace/actions/secrets",
848 Op::ListActionsSecrets,
849 &[],
850 ),
851 route(
852 "PUT",
853 "/workspaces/:workspace/actions/secrets/:setting",
854 Op::SetActionsSecret,
855 &[],
856 ),
857 route(
858 "DELETE",
859 "/workspaces/:workspace/actions/secrets/:setting",
860 Op::DeleteActionsSecret,
861 &[],
862 ),
863 route(
864 "GET",
865 "/workspaces/:workspace/actions/variables",
866 Op::ListActionsVariables,
867 &[],
868 ),
869 route(
870 "POST",
871 "/workspaces/:workspace/actions/variables",
872 Op::SetActionsVariable,
873 &[],
874 ),
875 route(
876 "PATCH",
877 "/workspaces/:workspace/actions/variables/:setting",
878 Op::SetActionsVariable,
879 &[],
880 ),
881 route(
882 "DELETE",
883 "/workspaces/:workspace/actions/variables/:setting",
884 Op::DeleteActionsVariable,
885 &[],
886 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents887 // Self-hosted runners: a repository's own, or a workspace's.
888 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
889 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
890 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
891 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
892 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
893 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
894 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
895 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
896 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
897 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
898 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
899 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
900 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
901 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.902 // A workspace agent's own computer.
903 route("GET", "/workspaces/:workspace/agents/:agent/computer", Op::GetAgentComputer, &[]),
904 route("POST", "/workspaces/:workspace/agents/:agent/computer/wake", Op::WakeAgentComputer, &[]),
905 route("POST", "/workspaces/:workspace/agents/:agent/computer/sleep", Op::SleepAgentComputer, &[]),
906 route("POST", "/workspaces/:workspace/agents/:agent/computer/reset", Op::ResetAgentComputer, &[]),
907 route("GET", "/workspaces/:workspace/agents/:agent/computer/commands", Op::ListAgentComputerCommands, &[]),
Icons are cached at the edge908 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
909 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
910 route(
911 "POST",
912 "/repos/:owner/:name/plans/:plan/apply",
913 Op::ApplyPlan,
914 &[],
915 ),
916 route(
917 "POST",
918 "/repos/:owner/:name/issues/:number/comments",
919 Op::AddComment,
920 &[],
921 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts922 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
923 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Icons are cached at the edge924 route(
925 "GET",
926 "/repos/:owner/:name/pulls",
927 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar928 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Icons are cached at the edge929 ),
930 route(
931 "POST",
932 "/repos/:owner/:name/pulls",
933 Op::CreatePullRequest,
934 &[],
935 ),
936 route(
937 "GET",
938 "/repos/:owner/:name/pulls/:number",
939 Op::GetPullRequest,
940 &[],
941 ),
942 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar943 "PATCH",
944 "/repos/:owner/:name/pulls/:number",
945 Op::UpdatePullRequest,
946 &[],
947 ),
948 route(
Icons are cached at the edge949 "GET",
950 "/repos/:owner/:name/pulls/:number/changes",
951 Op::GetPullRequestChanges,
952 &[],
953 ),
954 route(
955 "POST",
956 "/repos/:owner/:name/pulls/:number/reviews",
957 Op::ReviewPullRequest,
958 &[],
959 ),
960 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar961 "POST",
962 "/repos/:owner/:name/pulls/:number/requested_reviewers",
963 Op::RequestReviewers,
964 &[],
965 ),
966 route(
967 "DELETE",
968 "/repos/:owner/:name/pulls/:number/requested_reviewers",
969 Op::RemoveRequestedReviewers,
970 &[],
971 ),
972 route(
Icons are cached at the edge973 "GET",
974 "/repos/:owner/:name/pulls/:number/session",
975 Op::ReadSession,
976 &[("after", "after")],
977 ),
978 route(
979 "POST",
980 "/repos/:owner/:name/pulls/:number/session",
981 Op::RecordSession,
982 &[],
983 ),
984 route(
985 "POST",
986 "/repos/:owner/:name/pulls/:number/ready",
987 Op::MarkPullRequestReady,
988 &[],
989 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts990 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Icons are cached at the edge991 route(
992 "POST",
993 "/repos/:owner/:name/pulls/:number/close",
994 Op::ClosePullRequest,
995 &[],
996 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts997 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Icons are cached at the edge998 route(
999 "POST",
1000 "/repos/:owner/:name/pulls/:number/merge",
1001 Op::MergePullRequest,
1002 &[],
1003 ),
Merge main into Artifacts Phase 21004 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1005 // before an artifact by id, which it would otherwise match.
1006 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")]),
1007 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1008 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1009 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1010 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1011 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1012 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1013 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1014 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1015 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1016 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1017 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1018 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1019 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1020 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1021 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1022 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Icons are cached at the edge1023];
1024
1025impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1026 /// Whether the route answers success with `204` and no body, as
1027 /// GitHub's address for the same does. MCP still answers `true`.
1028 pub fn no_content(&self) -> bool {
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.1029 matches!(
1030 self.op,
1031 Op::DeleteComment
1032 | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector)
1033 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1034 }
1035
Icons are cached at the edge1036 /// The names of the route's path parameters, in order.
1037 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1038 self.path
1039 .split('/')
1040 .filter_map(|segment| segment.strip_prefix(':'))
1041 }
1042
1043 /// The values of the path parameters, if `path` is this route's.
1044 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1045 let mut values = Vec::new();
1046 let mut actual = path.trim_end_matches('/').split('/');
1047 for expected in self.path.split('/') {
1048 let segment = actual.next()?;
1049 match expected.strip_prefix(':') {
1050 Some(name) if !segment.is_empty() => values.push((name, segment)),
1051 Some(_) => return None,
1052 None if expected == segment => {}
1053 None => return None,
1054 }
1055 }
1056 actual.next().is_none().then_some(values)
1057 }
1058}
1059
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1060/// A path segment with its `%XX` escapes decoded; as given when that is not
1061/// UTF-8.
1062fn percent_decoded(segment: &str) -> String {
1063 let bytes = segment.as_bytes();
1064 let mut out = Vec::with_capacity(bytes.len());
1065 let mut i = 0;
1066 while i < bytes.len() {
1067 let escaped = (bytes[i] == b'%')
1068 .then(|| segment.get(i + 1..i + 3))
1069 .flatten()
1070 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1071 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1072 match escaped {
1073 Some(byte) => {
1074 out.push(byte);
1075 i += 3;
1076 }
1077 None => {
1078 out.push(bytes[i]);
1079 i += 1;
1080 }
1081 }
1082 }
1083 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1084}
1085
Icons are cached at the edge1086/// The route for a request, and the operation input it describes.
1087///
1088/// The input is the JSON body, overlaid with the query parameters the route
1089/// 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 bar1090/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1091/// becomes an integer.
Icons are cached at the edge1092pub fn resolve(
1093 method: &str,
1094 path: &str,
1095 query: &[(String, String)],
1096 body: Value,
1097) -> Option<(&'static Route, Value)> {
1098 let (route, params) = ROUTES
1099 .iter()
1100 .filter(|route| route.method == method)
1101 .find_map(|route| Some((route, route.matches(path)?)))?;
1102
1103 let mut input = match body {
1104 Value::Object(fields) => fields,
1105 _ => Map::new(),
1106 };
1107 for (name, key) in route.query {
1108 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1109 input.insert((*key).to_owned(), Value::String(value.clone()));
1110 }
1111 }
1112 let param = |wanted: &str| {
1113 params
1114 .iter()
1115 .find(|(name, _)| *name == wanted)
1116 .map(|(_, value)| *value)
1117 };
1118 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1119 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1120 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971121 // Every other name the path gives, under that name; the ones below that
1122 // need more (a repository, a number, an encoded name) are set after.
1123 for (key, value) in &params {
1124 if !matches!(*key, "owner" | "name") {
1125 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Icons are cached at the edge1126 }
1127 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1128 // A repository of a team's workspace, named by itself.
1129 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1130 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1131 }
1132 // A branch name may hold slashes, sent URL-encoded as one segment, and
1133 // a label's name spaces.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1134 if let Some(branch) = param("branch") {
1135 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1136 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971137 // An environment's name may hold slashes and spaces, URL-encoded.
1138 if let Some(environment) = param("environment") {
1139 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1140 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1141 if let Some(label) = param("label") {
1142 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1143 }
1144 if let Some(milestone) = param("milestone") {
1145 // Not a number: zero, which no milestone has.
1146 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1147 }
Icons are cached at the edge1148 // GitHub says some things with the path alone.
1149 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1150 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1151 }
1152 if route.path.ends_with("/rerun-failed-jobs") {
1153 input.insert("failed_only".to_owned(), Value::Bool(true));
1154 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1155 if route.path.ends_with("/force-cancel") {
1156 input.insert("force".to_owned(), Value::Bool(true));
1157 }
API: notifications over REST and MCP, with notifications scopes1158 // Unsaving and waking a thread are a DELETE of what PUT made.
1159 if route.method == "DELETE" && route.path.ends_with("/saved") {
1160 input.insert("saved".to_owned(), Value::Bool(false));
1161 }
1162 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1163 input.remove("until");
1164 }
Icons are cached at the edge1165 if let Some(number) = param("number") {
1166 // Not a number: zero, which no issue or pull request has.
1167 input.insert(
1168 "number".to_owned(),
1169 number.parse::<u32>().unwrap_or(0).into(),
1170 );
1171 }
1172 Some((route, Value::Object(input)))
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177 use serde_json::json;
1178
1179 use super::*;
1180
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971181 /// Every name a route's path gives reaches the operation: a name left
1182 /// off the lists above is dropped, and the operation answers that it
1183 /// was not given (the project routes were, until this test).
1184 #[test]
1185 fn every_path_parameter_reaches_the_input() {
1186 for route in ROUTES.iter() {
1187 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1188 if names.is_empty() {
1189 continue;
1190 }
1191 let path: String = route
1192 .path
1193 .split('/')
1194 .map(|part| match part.strip_prefix(':') {
1195 Some("number" | "milestone") => "7".to_owned(),
1196 Some(name) => format!("{name}-x"),
1197 None => part.to_owned(),
1198 })
1199 .collect::<Vec<_>>()
1200 .join("/");
1201 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1202 // A path two routes could take is checked under the first.
1203 if found.path != route.path {
1204 continue;
1205 }
1206 for name in names {
1207 let key = match name {
1208 "owner" | "name" => "repo",
1209 "repo" if names_has_workspace(route.path) => "repo",
1210 other => other,
1211 };
1212 assert!(
1213 input.get(key).is_some_and(|value| !value.is_null()),
1214 "{} {}: :{name} does not reach the input",
1215 route.method,
1216 route.path
1217 );
1218 }
1219 }
1220 }
1221
1222 fn names_has_workspace(path: &str) -> bool {
1223 path.split('/').any(|part| part == ":workspace")
1224 }
1225
Icons are cached at the edge1226 #[test]
1227 fn a_path_resolves_to_its_operation_and_input() {
1228 let (route, input) = resolve(
1229 "POST",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1230 "/repos/flagon-io/hello/pulls/14/merge",
Icons are cached at the edge1231 &[],
1232 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1233 )
1234 .unwrap();
1235 assert_eq!(route.op, Op::MergePullRequest);
1236 // What the path names wins over the body.
1237 assert_eq!(
1238 input,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1239 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
Icons are cached at the edge1240 );
1241 }
1242
1243 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1244 fn a_branch_with_slashes_is_one_encoded_segment() {
1245 let (route, input) = resolve(
1246 "POST",
1247 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1248 &[],
1249 json!({ "new_name": "feature/sign-in" }),
1250 )
1251 .unwrap();
1252 assert_eq!(route.op, Op::RenameBranch);
1253 assert_eq!(
1254 input,
1255 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1256 );
1257 assert_eq!(percent_decoded("100%"), "100%");
1258 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1259 }
1260
1261 #[test]
1262 fn a_collaborator_is_named_by_username() {
1263 let (route, input) = resolve(
1264 "PATCH",
1265 "/repos/flagon-io/hello/collaborators/ada",
1266 &[],
1267 json!({ "role": "maintain" }),
1268 )
1269 .unwrap();
1270 assert_eq!(route.op, Op::UpdateCollaborator);
1271 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1272 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1273 assert_eq!(route.op, Op::DeclineRepoInvitation);
1274 assert_eq!(input, json!({ "id": "rin_1" }));
1275 }
1276
1277 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1278 fn teams_are_addressed_by_workspace_and_slug() {
1279 let query = [("q".to_owned(), "back".to_owned())];
1280 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1281 assert_eq!(route.op, Op::ListTeams);
1282 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1283 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1284 assert_eq!(route.op, Op::UpdateTeam);
1285 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1286 let (route, input) =
1287 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1288 assert_eq!(route.op, Op::SetTeamMember);
1289 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1290 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1291 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1292 assert_eq!(route.op, Op::ListTeamMembers);
1293 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1294 // A repository is named by itself, in the team's workspace.
1295 let (route, input) =
1296 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1297 assert_eq!(route.op, Op::SetTeamRepo);
1298 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1299 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1300 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1301 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1302 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1303 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1304 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1305 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1306 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1307 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1308 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1309 assert_eq!(route.op, Op::ListUserTeams);
1310 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1311 }
1312
1313 #[test]
1314 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1315 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1316 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1317 assert_eq!(route.op, Op::RequestReviewers);
1318 assert_eq!(
1319 input,
1320 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1321 );
1322 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1323 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1324 let query = [("ref".to_owned(), "main".to_owned())];
1325 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1326 assert_eq!(route.op, Op::GetCodeownersErrors);
1327 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1328 }
1329
1330 #[test]
API: notifications over REST and MCP, with notifications scopes1331 fn notifications_are_addressed_as_threads_and_by_issue() {
1332 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1333 assert_eq!(route.op, Op::MarkThreadDone);
1334 assert_eq!(input, json!({ "id": "ntf_1" }));
1335 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1336 assert_eq!(route.op, Op::SaveThread);
1337 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1338 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1339 assert_eq!(route.op, Op::SnoozeThread);
1340 assert_eq!(input, json!({ "id": "ntf_1" }));
1341 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1342 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1343 assert_eq!(route.op, Op::ListNotifications);
1344 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1345 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1346 assert_eq!(route.op, Op::SetThreadSubscription);
1347 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1348 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1349 }
1350
1351 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1352 fn labels_and_milestones_are_named_in_the_path() {
1353 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1354 assert_eq!(route.op, Op::UpdateLabel);
1355 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1356 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1357 assert_eq!(route.op, Op::RemoveIssueLabels);
1358 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1359 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1360 assert_eq!(route.op, Op::RemoveIssueLabels);
1361 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1362 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1363 assert_eq!(route.op, Op::AddDefaultLabels);
1364 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1365 assert_eq!(route.op, Op::UpdateMilestone);
1366 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1367 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1368 assert_eq!(route.op, Op::UpdatePullRequest);
1369 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1370 }
1371
1372 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1373 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1374 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1375 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1376 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1377 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1378 // Reopening and closing with a PATCH, as `state`.
1379 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1380 assert_eq!(route.op, Op::UpdatePullRequest);
1381 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1382 let (route, input) =
1383 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1384 assert_eq!(route.op, Op::EditComment);
1385 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1386 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1387 assert_eq!(route.op, Op::DeleteComment);
1388 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1389 // Still an issue's comments, labels and subscription.
1390 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1391 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1392 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1393 }
1394
1395 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1396 fn billing_is_addressed_by_workspace() {
1397 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1398 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1399 assert_eq!(route.op, Op::GetUsage);
1400 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1401 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1402 assert_eq!(route.op, Op::SetBudget);
1403 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1404 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1405 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1406 assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit);
1407 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
1408 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1409 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1410 assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1411 }
1412
1413 #[test]
Merge checks: statuses and check runs on every commit1414 fn checks_are_at_githubs_addresses() {
1415 let sha = "a".repeat(40);
1416 let body = json!({ "state": "success", "context": "ci/build" });
1417 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1418 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1419 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1420 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1421 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1422 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1423 let query = [("check_name".to_owned(), "lint".to_owned())];
1424 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1425 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1426 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1427 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1428 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1429 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1430 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1431 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1432 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1433 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1434 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1435 }
1436
1437 #[test]
Icons are cached at the edge1438 fn query_parameters_are_renamed() {
1439 let query = [
1440 ("q".to_owned(), "parser".to_owned()),
1441 ("x".to_owned(), "y".to_owned()),
1442 ];
1443 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1444 assert_eq!(route.op, Op::ListRepos);
1445 assert_eq!(input, json!({ "query": "parser" }));
1446 }
1447
1448 #[test]
1449 fn method_and_shape_must_match() {
1450 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1451 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1452 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1453 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1454 }
1455
1456 #[test]
1457 fn every_parameter_and_query_name_is_an_input() {
1458 for route in ROUTES {
1459 let properties = route.op.properties();
1460 for (_, key) in route.query {
1461 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1462 }
1463 for name in route.params() {
1464 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1465 || properties.contains_key(name);
1466 assert!(covered, "{}: {name}", route.path);
1467 }
1468 }
1469 }
1470
1471 #[test]
1472 fn every_operation_has_a_route() {
1473 for op in Op::ALL {
1474 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1475 }
1476 }
1477}

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