Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,517 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.

Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1//! 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 GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.7use crate::archives::ArchiveOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca8use crate::deploy_keys::DeployKeysOp;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb979use crate::deployments::DeploymentsOp;
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.10use crate::telemetry::TelemetryOp;
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;
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths15use 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;
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths19
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, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths47 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, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths51 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
52 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
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)53 // Invitations to workspaces waiting for your answer.
54 route("GET", "/user/invitations", Op::ListInvitations, &[]),
55 route("POST", "/user/invitations/:id/accept", Op::AcceptInvitation, &[]),
56 route("POST", "/user/invitations/:id/decline", Op::DeclineInvitation, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths57 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals58 // A workspace's rules for personal access tokens, and its members' tokens.
59 route("GET", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::GetTokenPolicy), &[]),
60 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 managers61 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 approvals62 route("POST", "/workspaces/:workspace/personal-access-tokens/:id", Op::Tokens(TokenOp::RevokeMemberToken), &[]),
63 route("GET", "/workspaces/:workspace/personal-access-token-requests", Op::Tokens(TokenOp::ListTokenRequests), &[]),
64 route("POST", "/workspaces/:workspace/personal-access-token-requests/:id", Op::Tokens(TokenOp::ReviewTokenRequest), &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths65 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
66 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
67 // Who has access. GitHub's addresses, but for adding someone, which
68 // takes an email address as well as a username.
69 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
70 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
71 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
72 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
73 route(
74 "GET",
75 "/repos/:owner/:name/collaborators/:username/permission",
76 Op::GetCollaboratorPermission,
77 &[],
78 ),
79 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
80 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
81 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca82 // Deploy keys, at GitHub's addresses.
83 route("GET", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::ListDeployKeys), &[]),
84 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
85 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
86 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
API: notifications over REST and MCP, with notifications scopes87 // Your notifications: threads, marking them, and what you subscribe
88 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
89 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")]),
90 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
91 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
92 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
93 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
94 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
95 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
96 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
97 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
98 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
99 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
100 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
101 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")]),
102 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
103 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
104 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
105 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
106 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
107 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
108 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
109 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97110 // Stars: yours, and who starred a repository.
111 route("GET", "/user/starred", Op::About(AboutOp::ListStarred), &[]),
112 route("GET", "/user/starred/:owner/:name", Op::About(AboutOp::CheckStarred), &[]),
113 route("PUT", "/user/starred/:owner/:name", Op::About(AboutOp::Star), &[]),
114 route("DELETE", "/user/starred/:owner/:name", Op::About(AboutOp::Unstar), &[]),
115 route("GET", "/repos/:owner/:name/stargazers", Op::About(AboutOp::ListStargazers), &[("page", "page")]),
116 // What the default branch says about a repository, kept by commit.
117 route("GET", "/repos/:owner/:name/languages", Op::About(AboutOp::GetLanguages), &[]),
118 route("GET", "/repos/:owner/:name/contributors", Op::About(AboutOp::ListContributors), &[]),
119 route("GET", "/repos/:owner/:name/license", Op::About(AboutOp::GetLicense), &[]),
120 // Releases: `latest` and `tags/…` before an id.
121 route("GET", "/repos/:owner/:name/releases", Op::About(AboutOp::ListReleases), &[]),
122 route("POST", "/repos/:owner/:name/releases", Op::About(AboutOp::CreateRelease), &[]),
123 route("GET", "/repos/:owner/:name/releases/latest", Op::About(AboutOp::GetLatestRelease), &[]),
124 route("GET", "/repos/:owner/:name/releases/tags/:tag", Op::About(AboutOp::GetReleaseByTag), &[]),
125 route("GET", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::GetRelease), &[]),
126 route("PATCH", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::UpdateRelease), &[]),
127 route("DELETE", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::DeleteRelease), &[]),
API: pinned projects over REST and MCP128 // Your pinned projects in a workspace, in your order.
129 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
130 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
131 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
132 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths133 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
134 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
135 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
Merge main (membership, two-factor, GitHub repo roles) into tokens136 // Members and owners: GitHub's organization members, by username.
137 route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]),
138 route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]),
139 route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]),
140 route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]),
141 route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97142 // A workspace's projects: what each is, where it runs, its links.
143 route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
144 route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
145 route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]),
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.146 // A project's observability: what its software did, and how it is collected.
147 route(
148 "GET",
149 "/workspaces/:workspace/projects/:project/observability/overview",
150 Op::Telemetry(TelemetryOp::GetOverview),
151 &[("from", "from"), ("to", "to"), ("environment", "environment"), ("service", "service"), ("deployment_id", "deployment_id"), ("step", "step")],
152 ),
153 route(
154 "GET",
155 "/workspaces/:workspace/projects/:project/observability/requests",
156 Op::Telemetry(TelemetryOp::ListRequests),
157 &[
158 ("from", "from"),
159 ("to", "to"),
160 ("q", "q"),
161 ("status", "status"),
162 ("errors_only", "errors_only"),
163 ("route", "route"),
164 ("min_duration_ms", "min_duration_ms"),
165 ("environment", "environment"),
166 ("service", "service"),
167 ("deployment_id", "deployment_id"),
168 ("cursor", "cursor"),
169 ("limit", "limit"),
170 ],
171 ),
172 route("GET", "/workspaces/:workspace/projects/:project/observability/requests/:id", Op::Telemetry(TelemetryOp::GetRequest), &[]),
173 route(
174 "GET",
175 "/workspaces/:workspace/projects/:project/observability/logs",
176 Op::Telemetry(TelemetryOp::ListLogs),
177 &[
178 ("from", "from"),
179 ("to", "to"),
180 ("q", "q"),
181 ("level", "level"),
182 ("trace_id", "trace_id"),
183 ("request_id", "request_id"),
184 ("environment", "environment"),
185 ("service", "service"),
186 ("deployment_id", "deployment_id"),
187 ("cursor", "cursor"),
188 ("limit", "limit"),
189 ],
190 ),
191 route(
192 "GET",
193 "/workspaces/:workspace/projects/:project/observability/traces",
194 Op::Telemetry(TelemetryOp::ListTraces),
195 &[
196 ("from", "from"),
197 ("to", "to"),
198 ("q", "q"),
199 ("errors_only", "errors_only"),
200 ("min_duration_ms", "min_duration_ms"),
201 ("environment", "environment"),
202 ("service", "service"),
203 ("cursor", "cursor"),
204 ("limit", "limit"),
205 ],
206 ),
207 route("GET", "/workspaces/:workspace/projects/:project/observability/traces/:trace_id", Op::Telemetry(TelemetryOp::GetTrace), &[]),
208 route(
209 "GET",
210 "/workspaces/:workspace/projects/:project/observability/errors",
211 Op::Telemetry(TelemetryOp::ListErrors),
212 &[("state", "state"), ("environment", "environment"), ("service", "service"), ("from", "from"), ("to", "to")],
213 ),
214 route("PATCH", "/workspaces/:workspace/projects/:project/observability/errors/:fingerprint", Op::Telemetry(TelemetryOp::UpdateError), &[]),
215 route("GET", "/workspaces/:workspace/projects/:project/observability/metrics", Op::Telemetry(TelemetryOp::ListMetrics), &[]),
216 route(
217 "GET",
218 "/workspaces/:workspace/projects/:project/observability/metrics/:metric",
219 Op::Telemetry(TelemetryOp::GetMetric),
220 &[("from", "from"), ("to", "to"), ("aggregate", "aggregate"), ("group_by", "group_by"), ("step", "step"), ("service", "service"), ("environment", "environment")],
221 ),
222 route("GET", "/workspaces/:workspace/projects/:project/observability/settings", Op::Telemetry(TelemetryOp::GetSettings), &[]),
223 route("PATCH", "/workspaces/:workspace/projects/:project/observability/settings", Op::Telemetry(TelemetryOp::UpdateSettings), &[]),
224 route("GET", "/workspaces/:workspace/projects/:project/observability/tokens", Op::Telemetry(TelemetryOp::ListTokens), &[]),
225 route("POST", "/workspaces/:workspace/projects/:project/observability/tokens", Op::Telemetry(TelemetryOp::CreateToken), &[]),
226 route("DELETE", "/workspaces/:workspace/projects/:project/observability/tokens/:id", Op::Telemetry(TelemetryOp::DeleteToken), &[]),
227 route("GET", "/workspaces/:workspace/projects/:project/observability/exports", Op::Telemetry(TelemetryOp::ListExports), &[]),
228 route("POST", "/workspaces/:workspace/projects/:project/observability/exports", Op::Telemetry(TelemetryOp::CreateExport), &[]),
229 route("PATCH", "/workspaces/:workspace/projects/:project/observability/exports/:id", Op::Telemetry(TelemetryOp::UpdateExport), &[]),
230 route("DELETE", "/workspaces/:workspace/projects/:project/observability/exports/:id", Op::Telemetry(TelemetryOp::DeleteExport), &[]),
231 route("GET", "/workspaces/:workspace/projects/:project/observability/usage", Op::Telemetry(TelemetryOp::GetUsage), &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths232 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
233 route(
234 "GET",
235 "/workspaces/:workspace/outside_collaborators",
236 Op::ListOutsideCollaborators,
237 &[],
238 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar239 // Teams: a workspace's groups of members, with roles on repositories.
240 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
241 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
242 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
243 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
244 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
245 route(
246 "GET",
247 "/workspaces/:workspace/teams/:team/members",
248 Op::ListTeamMembers,
249 &[("include_child_teams", "include_child_teams")],
250 ),
251 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
252 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
253 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
254 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
255 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
256 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
257 route(
258 "PUT",
259 "/workspaces/:workspace/teams/:team/review_assignment",
260 Op::SetTeamReviewAssignment,
261 &[],
262 ),
263 route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.264 // A workspace's billing: usage, budget and invoices.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit265 route(
266 "GET",
267 "/workspaces/:workspace/usage",
268 Op::GetUsage,
269 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
270 ),
271 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
272 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
273 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
274 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens275 // The AI Gateway's log of a workspace's requests.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar276 // Code owners: the CODEOWNERS file, checked.
277 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths278 // Security alerts: secrets and vulnerable dependencies.
279 route(
280 "GET",
281 "/repos/:owner/:name/security/alerts",
282 Op::ListSecurityAlerts,
283 &[("state", "state"), ("kind", "kind")],
284 ),
285 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
286 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 bar287 // The security suite: secret scanning, code scanning, vulnerability
288 // alerts and the supply chain, at the common addresses.
289 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
290 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
291 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
292 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
293 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
294 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
295 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
296 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
297 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
298 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
299 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
300 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
301 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
302 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
303 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
304 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
305 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
306 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
307 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
308 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
309 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
310 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
311 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
312 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
313 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
314 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
315 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
316 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
317 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
318 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
319 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
320 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
321 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
322 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
323 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
324 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
325 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
326 route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9327 // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
328 route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
329 route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
330 route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
331 route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
332 route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
333 route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
334 route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
335 route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths336 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
337 route(
338 "GET",
339 "/search",
340 Op::Search,
341 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
342 ),
343 route("POST", "/repos", Op::CreateRepo, &[]),
344 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.345 // A branch, tag or commit as one archive: a redirect to a link good for
346 // ten minutes, as GitHub's go to codeload.
347 route("GET", "/repos/:owner/:name/tarball/:ref", Op::Archives(ArchiveOp::DownloadTarball), &[]),
348 route("GET", "/repos/:owner/:name/zipball/:ref", Op::Archives(ArchiveOp::DownloadZipball), &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths349 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
350 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
351 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
352 route(
353 "GET",
354 "/workspaces/:workspace/repos/deleted",
355 Op::ListDeletedRepos,
356 &[],
357 ),
358 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
359 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
360 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
361 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
362 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
363 route(
364 "POST",
365 "/repos/:owner/:name/visibility",
366 Op::SetRepoVisibility,
367 &[],
368 ),
369 route(
370 "POST",
371 "/repos/:owner/:name/branches/:branch/rename",
372 Op::RenameBranch,
373 &[],
374 ),
The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days.375 route("POST", "/repos/:owner/:name/branches", Op::CreateBranch, &[]),
376 route("DELETE", "/repos/:owner/:name/branches/:branch", Op::DeleteBranch, &[("head", "head")]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths377 route(
378 "GET",
379 "/repos/:owner/:name/settings",
380 Op::GetRepoSettings,
381 &[],
382 ),
383 route(
384 "PATCH",
385 "/repos/:owner/:name/settings",
386 Op::UpdateRepoSettings,
387 &[],
388 ),
389 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit390 // Checks: GitHub's addresses for statuses, check runs and check suites.
391 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
392 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
393 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
394 route(
395 "GET",
396 "/repos/:owner/:name/commits/:ref/check-runs",
397 Op::Checks(ChecksOp::ListCheckRunsForRef),
398 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
399 ),
400 route(
401 "GET",
402 "/repos/:owner/:name/commits/:ref/check-suites",
403 Op::Checks(ChecksOp::ListCheckSuitesForRef),
404 &[("app", "app"), ("check_name", "check_name")],
405 ),
406 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
407 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
408 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
409 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
410 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
411 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
412 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 merge413 // Rulesets: a repository's, a workspace's, the rules of one branch,
414 // and how they judged pushes and merges.
415 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
416 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
417 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
418 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
419 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
420 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
421 route(
422 "GET",
423 "/repos/:owner/:name/rules/evaluations",
424 Op::Rules(RulesOp::ListRuleEvaluations),
425 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
426 ),
427 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
428 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
429 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
430 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
431 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
432 route(
433 "GET",
434 "/workspaces/:workspace/rules/evaluations",
435 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
436 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
437 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97438 // Deployments wherever they run, their statuses, and environments.
439 route(
440 "GET",
441 "/repos/:owner/:name/deployments",
442 Op::Deployments(DeploymentsOp::ListDeployments),
443 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
444 ),
445 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9446 // A g1t Actions job opening a g1t.page build it runs itself.
447 route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97448 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
449 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
450 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
451 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
452 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'453 // Environments' protection rules, and the runs they hold.
454 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
455 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
456 route(
457 "GET",
458 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
459 Op::Protection(ProtectionOp::GetPendingDeployments),
460 &[],
461 ),
462 route(
463 "POST",
464 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
465 Op::Protection(ProtectionOp::ReviewPendingDeployments),
466 &[],
467 ),
468 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
469 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
470 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
471 route(
472 "GET",
473 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
474 Op::Protection(ProtectionOp::GetForkPrApproval),
475 &[],
476 ),
477 route(
478 "PUT",
479 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
480 Op::Protection(ProtectionOp::SetForkPrApproval),
481 &[],
482 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts483 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
484 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'485 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
486 route(
487 "GET",
488 "/workspaces/:workspace/actions/permissions/workflow",
489 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
490 &[],
491 ),
492 route(
493 "PUT",
494 "/workspaces/:workspace/actions/permissions/workflow",
495 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
496 &[],
497 ),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.498 route(
499 "GET",
500 "/workspaces/:workspace/actions/managed-workflows",
501 Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
502 &[],
503 ),
504 route(
505 "POST",
506 "/workspaces/:workspace/actions/managed-workflows",
507 Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
508 &[],
509 ),
510 route(
511 "PATCH",
512 "/workspaces/:workspace/actions/managed-workflows/:workflow",
513 Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
514 &[],
515 ),
516 route(
517 "DELETE",
518 "/workspaces/:workspace/actions/managed-workflows/:workflow",
519 Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
520 &[],
521 ),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths522 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
523 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
524 route(
525 "GET",
526 "/repos/:owner/:name/memory",
527 Op::Recall,
528 &[("q", "query"), ("limit", "limit")],
529 ),
530 route(
531 "GET",
532 "/repos/:owner/:name/events",
533 Op::ListEvents,
534 &[("before", "before")],
535 ),
536 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar537 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
538 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
539 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
540 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
541 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
542 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
543 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
544 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
545 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
546 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
547 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
548 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
549 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
550 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths551 route(
552 "GET",
553 "/repos/:owner/:name/issues",
554 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar555 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths556 ),
557 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
558 route(
559 "GET",
560 "/repos/:owner/:name/issues/:number",
561 Op::GetIssue,
562 &[],
563 ),
564 route(
565 "PATCH",
566 "/repos/:owner/:name/issues/:number",
567 Op::UpdateIssue,
568 &[],
569 ),
570 route(
571 "POST",
572 "/repos/:owner/:name/issues/:number/close",
573 Op::CloseIssue,
574 &[],
575 ),
576 route(
577 "POST",
578 "/repos/:owner/:name/issues/:number/reopen",
579 Op::ReopenIssue,
580 &[],
581 ),
582 route(
583 "POST",
584 "/repos/:owner/:name/issues/import",
585 Op::ImportIssue,
586 &[],
587 ),
588 route(
589 "GET",
590 "/repos/:owner/:name/context",
591 Op::GetContext,
592 &[("reference", "reference")],
593 ),
594 route(
595 "GET",
596 "/workspaces/:workspace/integrations",
597 Op::ListIntegrations,
598 &[],
599 ),
600 route(
601 "POST",
602 "/workspaces/:workspace/integrations",
603 Op::ConnectIntegration,
604 &[],
605 ),
606 route(
607 "GET",
608 "/repos/:owner/:name/hooks",
609 Op::ListWebhooks,
610 &[],
611 ),
612 route(
613 "POST",
614 "/repos/:owner/:name/hooks",
615 Op::CreateWebhook,
616 &[],
617 ),
618 route(
619 "PATCH",
620 "/repos/:owner/:name/hooks/:id",
621 Op::UpdateWebhook,
622 &[],
623 ),
624 route(
625 "DELETE",
626 "/repos/:owner/:name/hooks/:id",
627 Op::DeleteWebhook,
628 &[],
629 ),
630 route(
631 "POST",
632 "/repos/:owner/:name/hooks/:id/pings",
633 Op::PingWebhook,
634 &[],
635 ),
636 route(
637 "GET",
638 "/repos/:owner/:name/hooks/:id/deliveries",
639 Op::ListWebhookDeliveries,
640 &[],
641 ),
642 route(
643 "POST",
644 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
645 Op::RedeliverWebhook,
646 &[],
647 ),
648 route(
649 "GET",
650 "/workspaces/:workspace/hooks",
651 Op::ListWebhooks,
652 &[],
653 ),
654 route(
655 "POST",
656 "/workspaces/:workspace/hooks",
657 Op::CreateWebhook,
658 &[],
659 ),
660 route(
661 "PATCH",
662 "/workspaces/:workspace/hooks/:id",
663 Op::UpdateWebhook,
664 &[],
665 ),
666 route(
667 "DELETE",
668 "/workspaces/:workspace/hooks/:id",
669 Op::DeleteWebhook,
670 &[],
671 ),
672 route(
673 "POST",
674 "/workspaces/:workspace/hooks/:id/pings",
675 Op::PingWebhook,
676 &[],
677 ),
678 route(
679 "GET",
680 "/workspaces/:workspace/hooks/:id/deliveries",
681 Op::ListWebhookDeliveries,
682 &[],
683 ),
684 route(
685 "POST",
686 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
687 Op::RedeliverWebhook,
688 &[],
689 ),
690 route(
AI Gateway: OpenAI's format, open models, and your own providers691 "PATCH",
692 "/workspaces/:workspace/integrations/:id",
693 Op::UpdateIntegration,
694 &[],
695 ),
696 route(
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths697 "DELETE",
698 "/workspaces/:workspace/integrations/:id",
699 Op::DisconnectIntegration,
700 &[],
701 ),
702 route(
703 "POST",
704 "/workspaces/:workspace/integrations/:id/test",
705 Op::TestIntegration,
706 &[],
707 ),
708 route(
709 "GET",
710 "/repos/:owner/:name/actions/workflows",
711 Op::ListWorkflows,
712 &[],
713 ),
714 route(
715 "GET",
716 "/repos/:owner/:name/actions/workflows/:workflow/runs",
717 Op::ListWorkflowRuns,
718 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
719 ),
720 route(
721 "POST",
722 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
723 Op::DispatchWorkflow,
724 &[],
725 ),
726 route(
727 "PATCH",
728 "/repos/:owner/:name/actions/workflows/:workflow",
729 Op::UpdateWorkflow,
730 &[],
731 ),
732 route(
733 "PUT",
734 "/repos/:owner/:name/actions/workflows/:workflow/enable",
735 Op::UpdateWorkflow,
736 &[],
737 ),
738 route(
739 "PUT",
740 "/repos/:owner/:name/actions/workflows/:workflow/disable",
741 Op::UpdateWorkflow,
742 &[],
743 ),
744 route(
745 "GET",
746 "/repos/:owner/:name/actions/runs",
747 Op::ListWorkflowRuns,
748 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
749 ),
750 route(
751 "GET",
752 "/repos/:owner/:name/actions/runs/:id",
753 Op::GetWorkflowRun,
754 &[],
755 ),
756 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)757 "GET",
758 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
759 Op::GetWorkflowRun,
760 &[],
761 ),
762 route(
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths763 "POST",
764 "/repos/:owner/:name/actions/runs/:id/cancel",
765 Op::CancelWorkflowRun,
766 &[],
767 ),
768 route(
769 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)770 "/repos/:owner/:name/actions/runs/:id/force-cancel",
771 Op::CancelWorkflowRun,
772 &[],
773 ),
774 route(
775 "POST",
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths776 "/repos/:owner/:name/actions/runs/:id/rerun",
777 Op::RerunWorkflowRun,
778 &[],
779 ),
780 route(
781 "POST",
782 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
783 Op::RerunWorkflowRun,
784 &[],
785 ),
786 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)787 "POST",
788 "/repos/:owner/:name/actions/jobs/:job/rerun",
789 Op::RerunWorkflowRun,
790 &[],
791 ),
792 route(
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths793 "GET",
794 "/repos/:owner/:name/actions/jobs/:job/logs",
795 Op::GetJobLogs,
796 &[("after", "after")],
797 ),
Merge packages: roles, Actions access, source label, soft delete, API798 // Packages, at GitHub's addresses with the workspace in place of the
799 // organization. A name with a slash is one URL-encoded segment.
800 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
801 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
802 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
803 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
804 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
805 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
806 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
807 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
808 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
809 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
810 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
811 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
812 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
813 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
814 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
815 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
816 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 R2817 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
818 // a signed link (lib.rs).
819 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
820 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
821 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
822 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
823 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
824 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
825 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths826 route(
827 "GET",
828 "/repos/:owner/:name/actions/secrets",
829 Op::ListActionsSecrets,
830 &[],
831 ),
832 route(
833 "PUT",
834 "/repos/:owner/:name/actions/secrets/:setting",
835 Op::SetActionsSecret,
836 &[],
837 ),
838 route(
839 "DELETE",
840 "/repos/:owner/:name/actions/secrets/:setting",
841 Op::DeleteActionsSecret,
842 &[],
843 ),
844 route(
845 "GET",
846 "/repos/:owner/:name/actions/variables",
847 Op::ListActionsVariables,
848 &[],
849 ),
850 route(
851 "POST",
852 "/repos/:owner/:name/actions/variables",
853 Op::SetActionsVariable,
854 &[],
855 ),
856 route(
857 "PATCH",
858 "/repos/:owner/:name/actions/variables/:setting",
859 Op::SetActionsVariable,
860 &[],
861 ),
862 route(
863 "DELETE",
864 "/repos/:owner/:name/actions/variables/:setting",
865 Op::DeleteActionsVariable,
866 &[],
867 ),
868 route(
869 "GET",
870 "/workspaces/:workspace/actions/secrets",
871 Op::ListActionsSecrets,
872 &[],
873 ),
874 route(
875 "PUT",
876 "/workspaces/:workspace/actions/secrets/:setting",
877 Op::SetActionsSecret,
878 &[],
879 ),
880 route(
881 "DELETE",
882 "/workspaces/:workspace/actions/secrets/:setting",
883 Op::DeleteActionsSecret,
884 &[],
885 ),
886 route(
887 "GET",
888 "/workspaces/:workspace/actions/variables",
889 Op::ListActionsVariables,
890 &[],
891 ),
892 route(
893 "POST",
894 "/workspaces/:workspace/actions/variables",
895 Op::SetActionsVariable,
896 &[],
897 ),
898 route(
899 "PATCH",
900 "/workspaces/:workspace/actions/variables/:setting",
901 Op::SetActionsVariable,
902 &[],
903 ),
904 route(
905 "DELETE",
906 "/workspaces/:workspace/actions/variables/:setting",
907 Op::DeleteActionsVariable,
908 &[],
909 ),
910 // Self-hosted runners: a repository's own, or a workspace's.
911 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
912 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
913 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
914 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
915 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
916 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
917 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
918 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
919 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
920 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
921 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
922 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
923 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
924 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
925 route(
926 "POST",
927 "/repos/:owner/:name/issues/:number/comments",
928 Op::AddComment,
929 &[],
930 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts931 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
932 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths933 route(
934 "GET",
935 "/repos/:owner/:name/pulls",
936 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar937 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths938 ),
939 route(
940 "POST",
941 "/repos/:owner/:name/pulls",
942 Op::CreatePullRequest,
943 &[],
944 ),
945 route(
946 "GET",
947 "/repos/:owner/:name/pulls/:number",
948 Op::GetPullRequest,
949 &[],
950 ),
951 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar952 "PATCH",
953 "/repos/:owner/:name/pulls/:number",
954 Op::UpdatePullRequest,
955 &[],
956 ),
957 route(
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths958 "GET",
959 "/repos/:owner/:name/pulls/:number/changes",
960 Op::GetPullRequestChanges,
961 &[],
962 ),
963 route(
964 "POST",
965 "/repos/:owner/:name/pulls/:number/reviews",
966 Op::ReviewPullRequest,
967 &[],
968 ),
969 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar970 "POST",
971 "/repos/:owner/:name/pulls/:number/requested_reviewers",
972 Op::RequestReviewers,
973 &[],
974 ),
975 route(
976 "DELETE",
977 "/repos/:owner/:name/pulls/:number/requested_reviewers",
978 Op::RemoveRequestedReviewers,
979 &[],
980 ),
981 route(
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths982 "GET",
983 "/repos/:owner/:name/pulls/:number/session",
984 Op::ReadSession,
985 &[("after", "after")],
986 ),
987 route(
988 "POST",
989 "/repos/:owner/:name/pulls/:number/session",
990 Op::RecordSession,
991 &[],
992 ),
993 route(
994 "POST",
995 "/repos/:owner/:name/pulls/:number/ready",
996 Op::MarkPullRequestReady,
997 &[],
998 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts999 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1000 route(
1001 "POST",
1002 "/repos/:owner/:name/pulls/:number/close",
1003 Op::ClosePullRequest,
1004 &[],
1005 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1006 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1007 route(
1008 "POST",
1009 "/repos/:owner/:name/pulls/:number/merge",
1010 Op::MergePullRequest,
1011 &[],
1012 ),
Merge main into Artifacts Phase 21013 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1014 // before an artifact by id, which it would otherwise match.
1015 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")]),
1016 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1017 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1018 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1019 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1020 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1021 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1022 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1023 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1024 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1025 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1026 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1027 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1028 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1029 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1030 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1031 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1032];
1033
1034impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1035 /// Whether the route answers success with `204` and no body, as
1036 /// GitHub's address for the same does. MCP still answers `true`.
1037 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91038 matches!(
1039 self.op,
1040 Op::DeleteComment
1041 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1042 }
1043
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1044 /// The names of the route's path parameters, in order.
1045 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1046 self.path
1047 .split('/')
1048 .filter_map(|segment| segment.strip_prefix(':'))
1049 }
1050
1051 /// The values of the path parameters, if `path` is this route's.
1052 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1053 let mut values = Vec::new();
1054 let mut actual = path.trim_end_matches('/').split('/');
1055 for expected in self.path.split('/') {
1056 let segment = actual.next()?;
1057 match expected.strip_prefix(':') {
1058 Some(name) if !segment.is_empty() => values.push((name, segment)),
1059 Some(_) => return None,
1060 None if expected == segment => {}
1061 None => return None,
1062 }
1063 }
1064 actual.next().is_none().then_some(values)
1065 }
1066}
1067
1068/// A path segment with its `%XX` escapes decoded; as given when that is not
1069/// UTF-8.
1070fn percent_decoded(segment: &str) -> String {
1071 let bytes = segment.as_bytes();
1072 let mut out = Vec::with_capacity(bytes.len());
1073 let mut i = 0;
1074 while i < bytes.len() {
1075 let escaped = (bytes[i] == b'%')
1076 .then(|| segment.get(i + 1..i + 3))
1077 .flatten()
1078 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1079 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1080 match escaped {
1081 Some(byte) => {
1082 out.push(byte);
1083 i += 3;
1084 }
1085 None => {
1086 out.push(bytes[i]);
1087 i += 1;
1088 }
1089 }
1090 }
1091 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1092}
1093
1094/// The route for a request, and the operation input it describes.
1095///
1096/// The input is the JSON body, overlaid with the query parameters the route
1097/// 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 bar1098/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1099/// becomes an integer.
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1100pub fn resolve(
1101 method: &str,
1102 path: &str,
1103 query: &[(String, String)],
1104 body: Value,
1105) -> Option<(&'static Route, Value)> {
1106 let (route, params) = ROUTES
1107 .iter()
1108 .filter(|route| route.method == method)
1109 .find_map(|route| Some((route, route.matches(path)?)))?;
1110
1111 let mut input = match body {
1112 Value::Object(fields) => fields,
1113 _ => Map::new(),
1114 };
1115 for (name, key) in route.query {
1116 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1117 input.insert((*key).to_owned(), Value::String(value.clone()));
1118 }
1119 }
1120 let param = |wanted: &str| {
1121 params
1122 .iter()
1123 .find(|(name, _)| *name == wanted)
1124 .map(|(_, value)| *value)
1125 };
1126 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1127 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1128 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971129 // Every other name the path gives, under that name; the ones below that
1130 // need more (a repository, a number, an encoded name) are set after.
1131 for (key, value) in &params {
1132 if !matches!(*key, "owner" | "name") {
1133 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1134 }
1135 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1136 // A repository of a team's workspace, named by itself.
1137 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1138 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1139 }
1140 // A branch name may hold slashes, sent URL-encoded as one segment, and
1141 // a label's name spaces.
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1142 if let Some(branch) = param("branch") {
1143 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1144 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971145 // An environment's name may hold slashes and spaces, URL-encoded.
1146 if let Some(environment) = param("environment") {
1147 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1148 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1149 if let Some(label) = param("label") {
1150 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1151 }
1152 if let Some(milestone) = param("milestone") {
1153 // Not a number: zero, which no milestone has.
1154 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1155 }
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1156 // GitHub says some things with the path alone.
1157 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1158 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1159 }
1160 if route.path.ends_with("/rerun-failed-jobs") {
1161 input.insert("failed_only".to_owned(), Value::Bool(true));
1162 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1163 if route.path.ends_with("/force-cancel") {
1164 input.insert("force".to_owned(), Value::Bool(true));
1165 }
API: notifications over REST and MCP, with notifications scopes1166 // Unsaving and waking a thread are a DELETE of what PUT made.
1167 if route.method == "DELETE" && route.path.ends_with("/saved") {
1168 input.insert("saved".to_owned(), Value::Bool(false));
1169 }
1170 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1171 input.remove("until");
1172 }
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1173 if let Some(number) = param("number") {
1174 // Not a number: zero, which no issue or pull request has.
1175 input.insert(
1176 "number".to_owned(),
1177 number.parse::<u32>().unwrap_or(0).into(),
1178 );
1179 }
1180 Some((route, Value::Object(input)))
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use serde_json::json;
1186
1187 use super::*;
1188
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971189 /// Every name a route's path gives reaches the operation: a name left
1190 /// off the lists above is dropped, and the operation answers that it
1191 /// was not given (the project routes were, until this test).
1192 #[test]
1193 fn every_path_parameter_reaches_the_input() {
1194 for route in ROUTES.iter() {
1195 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1196 if names.is_empty() {
1197 continue;
1198 }
1199 let path: String = route
1200 .path
1201 .split('/')
1202 .map(|part| match part.strip_prefix(':') {
1203 Some("number" | "milestone") => "7".to_owned(),
1204 Some(name) => format!("{name}-x"),
1205 None => part.to_owned(),
1206 })
1207 .collect::<Vec<_>>()
1208 .join("/");
1209 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1210 // A path two routes could take is checked under the first.
1211 if found.path != route.path {
1212 continue;
1213 }
1214 for name in names {
1215 let key = match name {
1216 "owner" | "name" => "repo",
1217 "repo" if names_has_workspace(route.path) => "repo",
1218 other => other,
1219 };
1220 assert!(
1221 input.get(key).is_some_and(|value| !value.is_null()),
1222 "{} {}: :{name} does not reach the input",
1223 route.method,
1224 route.path
1225 );
1226 }
1227 }
1228 }
1229
1230 fn names_has_workspace(path: &str) -> bool {
1231 path.split('/').any(|part| part == ":workspace")
1232 }
1233
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1234 #[test]
1235 fn a_path_resolves_to_its_operation_and_input() {
1236 let (route, input) = resolve(
1237 "POST",
1238 "/repos/flagon-io/hello/pulls/14/merge",
1239 &[],
1240 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1241 )
1242 .unwrap();
1243 assert_eq!(route.op, Op::MergePullRequest);
1244 // What the path names wins over the body.
1245 assert_eq!(
1246 input,
1247 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
1248 );
1249 }
1250
1251 #[test]
1252 fn a_branch_with_slashes_is_one_encoded_segment() {
1253 let (route, input) = resolve(
1254 "POST",
1255 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1256 &[],
1257 json!({ "new_name": "feature/sign-in" }),
1258 )
1259 .unwrap();
1260 assert_eq!(route.op, Op::RenameBranch);
1261 assert_eq!(
1262 input,
1263 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1264 );
1265 assert_eq!(percent_decoded("100%"), "100%");
1266 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1267 }
1268
1269 #[test]
1270 fn a_collaborator_is_named_by_username() {
1271 let (route, input) = resolve(
1272 "PATCH",
1273 "/repos/flagon-io/hello/collaborators/ada",
1274 &[],
1275 json!({ "role": "maintain" }),
1276 )
1277 .unwrap();
1278 assert_eq!(route.op, Op::UpdateCollaborator);
1279 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1280 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1281 assert_eq!(route.op, Op::DeclineRepoInvitation);
1282 assert_eq!(input, json!({ "id": "rin_1" }));
1283 }
1284
1285 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1286 fn teams_are_addressed_by_workspace_and_slug() {
1287 let query = [("q".to_owned(), "back".to_owned())];
1288 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1289 assert_eq!(route.op, Op::ListTeams);
1290 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1291 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1292 assert_eq!(route.op, Op::UpdateTeam);
1293 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1294 let (route, input) =
1295 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1296 assert_eq!(route.op, Op::SetTeamMember);
1297 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1298 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1299 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1300 assert_eq!(route.op, Op::ListTeamMembers);
1301 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1302 // A repository is named by itself, in the team's workspace.
1303 let (route, input) =
1304 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1305 assert_eq!(route.op, Op::SetTeamRepo);
1306 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1307 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1308 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1309 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1310 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1311 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1312 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1313 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1314 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1315 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1316 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1317 assert_eq!(route.op, Op::ListUserTeams);
1318 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1319 }
1320
1321 #[test]
1322 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1323 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1324 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1325 assert_eq!(route.op, Op::RequestReviewers);
1326 assert_eq!(
1327 input,
1328 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1329 );
1330 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1331 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1332 let query = [("ref".to_owned(), "main".to_owned())];
1333 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1334 assert_eq!(route.op, Op::GetCodeownersErrors);
1335 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1336 }
1337
1338 #[test]
API: notifications over REST and MCP, with notifications scopes1339 fn notifications_are_addressed_as_threads_and_by_issue() {
1340 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1341 assert_eq!(route.op, Op::MarkThreadDone);
1342 assert_eq!(input, json!({ "id": "ntf_1" }));
1343 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1344 assert_eq!(route.op, Op::SaveThread);
1345 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1346 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1347 assert_eq!(route.op, Op::SnoozeThread);
1348 assert_eq!(input, json!({ "id": "ntf_1" }));
1349 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1350 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1351 assert_eq!(route.op, Op::ListNotifications);
1352 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1353 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1354 assert_eq!(route.op, Op::SetThreadSubscription);
1355 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1356 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1357 }
1358
1359 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1360 fn labels_and_milestones_are_named_in_the_path() {
1361 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1362 assert_eq!(route.op, Op::UpdateLabel);
1363 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1364 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1365 assert_eq!(route.op, Op::RemoveIssueLabels);
1366 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1367 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1368 assert_eq!(route.op, Op::RemoveIssueLabels);
1369 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1370 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1371 assert_eq!(route.op, Op::AddDefaultLabels);
1372 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1373 assert_eq!(route.op, Op::UpdateMilestone);
1374 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1375 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1376 assert_eq!(route.op, Op::UpdatePullRequest);
1377 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1378 }
1379
1380 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1381 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1382 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1383 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1384 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1385 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1386 // Reopening and closing with a PATCH, as `state`.
1387 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1388 assert_eq!(route.op, Op::UpdatePullRequest);
1389 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1390 let (route, input) =
1391 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1392 assert_eq!(route.op, Op::EditComment);
1393 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1394 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1395 assert_eq!(route.op, Op::DeleteComment);
1396 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1397 // Still an issue's comments, labels and subscription.
1398 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1399 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1400 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1401 }
1402
1403 #[test]
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.1404 fn observability_is_addressed_by_workspace_and_project() {
1405 let query = [("level".to_owned(), "error,warn".to_owned()), ("q".to_owned(), "timeout".to_owned())];
1406 let (route, input) = resolve("GET", "/workspaces/acme/projects/web/observability/logs", &query, Value::Null).unwrap();
1407 assert_eq!(route.op, Op::Telemetry(TelemetryOp::ListLogs));
1408 assert_eq!(input, json!({ "level": "error,warn", "q": "timeout", "workspace": "acme", "project": "web" }));
1409 let (route, input) =
1410 resolve("GET", "/workspaces/acme/projects/web/observability/metrics/http.server.request.duration", &[], Value::Null).unwrap();
1411 assert_eq!(route.op, Op::Telemetry(TelemetryOp::GetMetric));
1412 assert_eq!(input["metric"], json!("http.server.request.duration"));
1413 let (route, input) =
1414 resolve("PATCH", "/workspaces/acme/projects/web/observability/errors/f00d", &[], json!({ "state": "resolved" })).unwrap();
1415 assert_eq!(route.op, Op::Telemetry(TelemetryOp::UpdateError));
1416 assert_eq!(input, json!({ "state": "resolved", "workspace": "acme", "project": "web", "fingerprint": "f00d" }));
1417 }
1418
1419 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1420 fn billing_is_addressed_by_workspace() {
1421 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1422 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1423 assert_eq!(route.op, Op::GetUsage);
1424 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1425 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1426 assert_eq!(route.op, Op::SetBudget);
1427 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1428 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1429 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1430 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1431 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
1432 }
1433
1434 #[test]
Merge checks: statuses and check runs on every commit1435 fn checks_are_at_githubs_addresses() {
1436 let sha = "a".repeat(40);
1437 let body = json!({ "state": "success", "context": "ci/build" });
1438 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1439 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1440 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1441 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1442 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1443 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1444 let query = [("check_name".to_owned(), "lint".to_owned())];
1445 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1446 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1447 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1448 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1449 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1450 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1451 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1452 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1453 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1454 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1455 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1456 }
1457
1458 #[test]
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.1459 fn archives_are_at_githubs_addresses_and_redirect() {
1460 let sha = "4807077b296e6edbf410d55e72749d3e1170c291";
1461 let (route, input) = resolve("GET", &format!("/repos/acme/web/tarball/{sha}"), &[], Value::Null).unwrap();
1462 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadTarball));
1463 assert_eq!(input, json!({ "ref": sha, "repo": "acme/web" }));
1464 // A branch with slashes, URL-encoded as octokit sends it.
1465 let (route, input) = resolve("GET", "/repos/acme/web/zipball/release%2F1.x", &[], Value::Null).unwrap();
1466 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadZipball));
1467 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1468 // Both answer with a redirect to their link, as the artifact download does.
1469 assert!(route.op.redirects());
1470 assert!(Op::Archives(ArchiveOp::DownloadTarball).redirects());
1471 assert!(Op::Artifacts(ArtifactsOp::DownloadArtifact).redirects());
1472 assert!(!Op::GetRepo.redirects());
1473 // Anyone may ask for a public repository's.
1474 assert!(!route.op.needs_user());
1475 }
1476
1477 #[test]
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1478 fn query_parameters_are_renamed() {
1479 let query = [
1480 ("q".to_owned(), "parser".to_owned()),
1481 ("x".to_owned(), "y".to_owned()),
1482 ];
1483 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1484 assert_eq!(route.op, Op::ListRepos);
1485 assert_eq!(input, json!({ "query": "parser" }));
1486 }
1487
1488 #[test]
1489 fn method_and_shape_must_match() {
1490 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1491 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1492 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1493 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1494 }
1495
1496 #[test]
1497 fn every_parameter_and_query_name_is_an_input() {
1498 for route in ROUTES {
1499 let properties = route.op.properties();
1500 for (_, key) in route.query {
1501 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1502 }
1503 for name in route.params() {
1504 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1505 || properties.contains_key(name);
1506 assert!(covered, "{}: {name}", route.path);
1507 }
1508 }
1509 }
1510
1511 #[test]
1512 fn every_operation_has_a_route() {
1513 for op in Op::ALL {
1514 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1515 }
1516 }
1517}

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