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

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

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