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

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

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