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

Fixes from the page sweep1//! 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;
Fixes from the page sweep15use 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;
Fixes from the page sweep19
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, &[]),
Fixes from the page sweep47 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, &[]),
Fixes from the page sweep51 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, &[]),
Fixes from the page sweep57 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), &[]),
Fixes from the page sweep65 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, &[]),
Fixes from the page sweep133 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), &[]),
Fixes from the page sweep232 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, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar275 // Code owners: the CODEOWNERS file, checked.
276 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
Fixes from the page sweep277 // Security alerts: secrets and vulnerable dependencies.
278 route(
279 "GET",
280 "/repos/:owner/:name/security/alerts",
281 Op::ListSecurityAlerts,
282 &[("state", "state"), ("kind", "kind")],
283 ),
284 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
285 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 bar286 // The security suite: secret scanning, code scanning, vulnerability
287 // alerts and the supply chain, at the common addresses.
288 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
289 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
290 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
291 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
292 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
293 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
294 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
295 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
296 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
297 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
298 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
299 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
300 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
301 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
302 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
303 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
304 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
305 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
306 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
307 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
308 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
309 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
310 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
311 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
312 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
313 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
314 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
315 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
316 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
317 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
318 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
319 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
320 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
321 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
322 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
323 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
324 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
325 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-7nm8r9326 // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
327 route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
328 route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
329 route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
330 route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
331 route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
332 route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
333 route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
334 route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
Fixes from the page sweep335 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
336 route(
337 "GET",
338 "/search",
339 Op::Search,
340 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
341 ),
342 route("POST", "/repos", Op::CreateRepo, &[]),
343 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.344 // A branch, tag or commit as one archive: a redirect to a link good for
345 // ten minutes, as GitHub's go to codeload.
346 route("GET", "/repos/:owner/:name/tarball/:ref", Op::Archives(ArchiveOp::DownloadTarball), &[]),
347 route("GET", "/repos/:owner/:name/zipball/:ref", Op::Archives(ArchiveOp::DownloadZipball), &[]),
Fixes from the page sweep348 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
349 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
350 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
351 route(
352 "GET",
353 "/workspaces/:workspace/repos/deleted",
354 Op::ListDeletedRepos,
355 &[],
356 ),
357 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
358 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
359 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
360 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
361 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
362 route(
363 "POST",
364 "/repos/:owner/:name/visibility",
365 Op::SetRepoVisibility,
366 &[],
367 ),
368 route(
369 "POST",
370 "/repos/:owner/:name/branches/:branch/rename",
371 Op::RenameBranch,
372 &[],
373 ),
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.374 route("POST", "/repos/:owner/:name/branches", Op::CreateBranch, &[]),
375 route("DELETE", "/repos/:owner/:name/branches/:branch", Op::DeleteBranch, &[("head", "head")]),
Fixes from the page sweep376 route(
377 "GET",
378 "/repos/:owner/:name/settings",
379 Op::GetRepoSettings,
380 &[],
381 ),
382 route(
383 "PATCH",
384 "/repos/:owner/:name/settings",
385 Op::UpdateRepoSettings,
386 &[],
387 ),
388 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit389 // Checks: GitHub's addresses for statuses, check runs and check suites.
390 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
391 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
392 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
393 route(
394 "GET",
395 "/repos/:owner/:name/commits/:ref/check-runs",
396 Op::Checks(ChecksOp::ListCheckRunsForRef),
397 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
398 ),
399 route(
400 "GET",
401 "/repos/:owner/:name/commits/:ref/check-suites",
402 Op::Checks(ChecksOp::ListCheckSuitesForRef),
403 &[("app", "app"), ("check_name", "check_name")],
404 ),
405 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
406 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
407 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
408 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
409 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
410 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
411 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 merge412 // Rulesets: a repository's, a workspace's, the rules of one branch,
413 // and how they judged pushes and merges.
414 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
415 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
416 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
417 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
418 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
419 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
420 route(
421 "GET",
422 "/repos/:owner/:name/rules/evaluations",
423 Op::Rules(RulesOp::ListRuleEvaluations),
424 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
425 ),
426 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
427 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
428 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
429 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
430 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
431 route(
432 "GET",
433 "/workspaces/:workspace/rules/evaluations",
434 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
435 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
436 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97437 // Deployments wherever they run, their statuses, and environments.
438 route(
439 "GET",
440 "/repos/:owner/:name/deployments",
441 Op::Deployments(DeploymentsOp::ListDeployments),
442 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
443 ),
444 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9445 // A g1t Actions job opening a g1t.page build it runs itself.
446 route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97447 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
448 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
449 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
450 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
451 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'452 // Environments' protection rules, and the runs they hold.
453 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
454 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
455 route(
456 "GET",
457 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
458 Op::Protection(ProtectionOp::GetPendingDeployments),
459 &[],
460 ),
461 route(
462 "POST",
463 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
464 Op::Protection(ProtectionOp::ReviewPendingDeployments),
465 &[],
466 ),
467 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
468 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
469 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
470 route(
471 "GET",
472 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
473 Op::Protection(ProtectionOp::GetForkPrApproval),
474 &[],
475 ),
476 route(
477 "PUT",
478 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
479 Op::Protection(ProtectionOp::SetForkPrApproval),
480 &[],
481 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts482 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
483 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'484 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
485 route(
486 "GET",
487 "/workspaces/:workspace/actions/permissions/workflow",
488 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
489 &[],
490 ),
491 route(
492 "PUT",
493 "/workspaces/:workspace/actions/permissions/workflow",
494 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
495 &[],
496 ),
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.497 route(
498 "GET",
499 "/workspaces/:workspace/actions/managed-workflows",
500 Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
501 &[],
502 ),
503 route(
504 "POST",
505 "/workspaces/:workspace/actions/managed-workflows",
506 Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
507 &[],
508 ),
509 route(
510 "PATCH",
511 "/workspaces/:workspace/actions/managed-workflows/:workflow",
512 Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
513 &[],
514 ),
515 route(
516 "DELETE",
517 "/workspaces/:workspace/actions/managed-workflows/:workflow",
518 Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
519 &[],
520 ),
Fixes from the page sweep521 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
522 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
523 route(
524 "GET",
525 "/repos/:owner/:name/memory",
526 Op::Recall,
527 &[("q", "query"), ("limit", "limit")],
528 ),
529 route(
530 "GET",
531 "/repos/:owner/:name/events",
532 Op::ListEvents,
Merge branch 'worktree-agent-ad531323a21f4c975'533 &[
534 ("before", "before"),
535 ("after", "after"),
536 ("types", "types"),
537 ("numbers", "numbers"),
538 ("since", "since"),
539 ("limit", "limit"),
540 ("wait", "wait"),
541 ],
Fixes from the page sweep542 ),
543 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar544 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
545 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
546 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
547 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
548 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
549 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
550 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
551 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
552 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
553 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
554 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
555 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
556 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
557 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Fixes from the page sweep558 route(
559 "GET",
560 "/repos/:owner/:name/issues",
561 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar562 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Fixes from the page sweep563 ),
564 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
565 route(
566 "GET",
567 "/repos/:owner/:name/issues/:number",
568 Op::GetIssue,
569 &[],
570 ),
571 route(
572 "PATCH",
573 "/repos/:owner/:name/issues/:number",
574 Op::UpdateIssue,
575 &[],
576 ),
577 route(
578 "POST",
579 "/repos/:owner/:name/issues/:number/close",
580 Op::CloseIssue,
581 &[],
582 ),
583 route(
584 "POST",
585 "/repos/:owner/:name/issues/:number/reopen",
586 Op::ReopenIssue,
587 &[],
588 ),
589 route(
590 "POST",
591 "/repos/:owner/:name/issues/import",
592 Op::ImportIssue,
593 &[],
594 ),
595 route(
596 "GET",
597 "/repos/:owner/:name/context",
598 Op::GetContext,
599 &[("reference", "reference")],
600 ),
601 route(
602 "GET",
603 "/workspaces/:workspace/integrations",
604 Op::ListIntegrations,
605 &[],
606 ),
607 route(
608 "POST",
609 "/workspaces/:workspace/integrations",
610 Op::ConnectIntegration,
611 &[],
612 ),
613 route(
614 "GET",
615 "/repos/:owner/:name/hooks",
616 Op::ListWebhooks,
617 &[],
618 ),
619 route(
620 "POST",
621 "/repos/:owner/:name/hooks",
622 Op::CreateWebhook,
623 &[],
624 ),
625 route(
626 "PATCH",
627 "/repos/:owner/:name/hooks/:id",
628 Op::UpdateWebhook,
629 &[],
630 ),
631 route(
632 "DELETE",
633 "/repos/:owner/:name/hooks/:id",
634 Op::DeleteWebhook,
635 &[],
636 ),
637 route(
638 "POST",
639 "/repos/:owner/:name/hooks/:id/pings",
640 Op::PingWebhook,
641 &[],
642 ),
643 route(
644 "GET",
645 "/repos/:owner/:name/hooks/:id/deliveries",
646 Op::ListWebhookDeliveries,
647 &[],
648 ),
649 route(
650 "POST",
651 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
652 Op::RedeliverWebhook,
653 &[],
654 ),
655 route(
656 "GET",
657 "/workspaces/:workspace/hooks",
658 Op::ListWebhooks,
659 &[],
660 ),
661 route(
662 "POST",
663 "/workspaces/:workspace/hooks",
664 Op::CreateWebhook,
665 &[],
666 ),
667 route(
668 "PATCH",
669 "/workspaces/:workspace/hooks/:id",
670 Op::UpdateWebhook,
671 &[],
672 ),
673 route(
674 "DELETE",
675 "/workspaces/:workspace/hooks/:id",
676 Op::DeleteWebhook,
677 &[],
678 ),
679 route(
680 "POST",
681 "/workspaces/:workspace/hooks/:id/pings",
682 Op::PingWebhook,
683 &[],
684 ),
685 route(
686 "GET",
687 "/workspaces/:workspace/hooks/:id/deliveries",
688 Op::ListWebhookDeliveries,
689 &[],
690 ),
691 route(
692 "POST",
693 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
694 Op::RedeliverWebhook,
695 &[],
696 ),
697 route(
AI Gateway: OpenAI's format, open models, and your own providers698 "PATCH",
699 "/workspaces/:workspace/integrations/:id",
700 Op::UpdateIntegration,
701 &[],
702 ),
703 route(
Fixes from the page sweep704 "DELETE",
705 "/workspaces/:workspace/integrations/:id",
706 Op::DisconnectIntegration,
707 &[],
708 ),
709 route(
710 "POST",
711 "/workspaces/:workspace/integrations/:id/test",
712 Op::TestIntegration,
713 &[],
714 ),
715 route(
716 "GET",
717 "/repos/:owner/:name/actions/workflows",
718 Op::ListWorkflows,
719 &[],
720 ),
721 route(
722 "GET",
723 "/repos/:owner/:name/actions/workflows/:workflow/runs",
724 Op::ListWorkflowRuns,
725 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
726 ),
727 route(
728 "POST",
729 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
730 Op::DispatchWorkflow,
731 &[],
732 ),
733 route(
734 "PATCH",
735 "/repos/:owner/:name/actions/workflows/:workflow",
736 Op::UpdateWorkflow,
737 &[],
738 ),
739 route(
740 "PUT",
741 "/repos/:owner/:name/actions/workflows/:workflow/enable",
742 Op::UpdateWorkflow,
743 &[],
744 ),
745 route(
746 "PUT",
747 "/repos/:owner/:name/actions/workflows/:workflow/disable",
748 Op::UpdateWorkflow,
749 &[],
750 ),
751 route(
752 "GET",
753 "/repos/:owner/:name/actions/runs",
754 Op::ListWorkflowRuns,
755 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
756 ),
757 route(
758 "GET",
759 "/repos/:owner/:name/actions/runs/:id",
760 Op::GetWorkflowRun,
761 &[],
762 ),
763 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)764 "GET",
765 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
766 Op::GetWorkflowRun,
767 &[],
768 ),
769 route(
Fixes from the page sweep770 "POST",
771 "/repos/:owner/:name/actions/runs/:id/cancel",
772 Op::CancelWorkflowRun,
773 &[],
774 ),
775 route(
776 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)777 "/repos/:owner/:name/actions/runs/:id/force-cancel",
778 Op::CancelWorkflowRun,
779 &[],
780 ),
781 route(
782 "POST",
Fixes from the page sweep783 "/repos/:owner/:name/actions/runs/:id/rerun",
784 Op::RerunWorkflowRun,
785 &[],
786 ),
787 route(
788 "POST",
789 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
790 Op::RerunWorkflowRun,
791 &[],
792 ),
793 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)794 "POST",
795 "/repos/:owner/:name/actions/jobs/:job/rerun",
796 Op::RerunWorkflowRun,
797 &[],
798 ),
799 route(
Fixes from the page sweep800 "GET",
801 "/repos/:owner/:name/actions/jobs/:job/logs",
802 Op::GetJobLogs,
803 &[("after", "after")],
804 ),
Merge packages: roles, Actions access, source label, soft delete, API805 // Packages, at GitHub's addresses with the workspace in place of the
806 // organization. A name with a slash is one URL-encoded segment.
807 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
808 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
809 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
810 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
811 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
812 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
813 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
814 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
815 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
816 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
817 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
818 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
819 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
820 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
821 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
822 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
823 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 R2824 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
825 // a signed link (lib.rs).
826 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
827 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
828 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
829 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
830 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
831 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
832 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
Fixes from the page sweep833 route(
834 "GET",
835 "/repos/:owner/:name/actions/secrets",
836 Op::ListActionsSecrets,
837 &[],
838 ),
839 route(
840 "PUT",
841 "/repos/:owner/:name/actions/secrets/:setting",
842 Op::SetActionsSecret,
843 &[],
844 ),
845 route(
846 "DELETE",
847 "/repos/:owner/:name/actions/secrets/:setting",
848 Op::DeleteActionsSecret,
849 &[],
850 ),
851 route(
852 "GET",
853 "/repos/:owner/:name/actions/variables",
854 Op::ListActionsVariables,
855 &[],
856 ),
857 route(
858 "POST",
859 "/repos/:owner/:name/actions/variables",
860 Op::SetActionsVariable,
861 &[],
862 ),
863 route(
864 "PATCH",
865 "/repos/:owner/:name/actions/variables/:setting",
866 Op::SetActionsVariable,
867 &[],
868 ),
869 route(
870 "DELETE",
871 "/repos/:owner/:name/actions/variables/:setting",
872 Op::DeleteActionsVariable,
873 &[],
874 ),
875 route(
876 "GET",
877 "/workspaces/:workspace/actions/secrets",
878 Op::ListActionsSecrets,
879 &[],
880 ),
881 route(
882 "PUT",
883 "/workspaces/:workspace/actions/secrets/:setting",
884 Op::SetActionsSecret,
885 &[],
886 ),
887 route(
888 "DELETE",
889 "/workspaces/:workspace/actions/secrets/:setting",
890 Op::DeleteActionsSecret,
891 &[],
892 ),
893 route(
894 "GET",
895 "/workspaces/:workspace/actions/variables",
896 Op::ListActionsVariables,
897 &[],
898 ),
899 route(
900 "POST",
901 "/workspaces/:workspace/actions/variables",
902 Op::SetActionsVariable,
903 &[],
904 ),
905 route(
906 "PATCH",
907 "/workspaces/:workspace/actions/variables/:setting",
908 Op::SetActionsVariable,
909 &[],
910 ),
911 route(
912 "DELETE",
913 "/workspaces/:workspace/actions/variables/:setting",
914 Op::DeleteActionsVariable,
915 &[],
916 ),
917 // Self-hosted runners: a repository's own, or a workspace's.
918 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
919 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
920 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
921 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
922 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
923 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
924 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
925 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
926 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
927 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
928 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
929 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
930 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
931 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
932 route(
933 "POST",
934 "/repos/:owner/:name/issues/:number/comments",
935 Op::AddComment,
936 &[],
937 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts938 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
939 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Fixes from the page sweep940 route(
941 "GET",
942 "/repos/:owner/:name/pulls",
943 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar944 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Fixes from the page sweep945 ),
946 route(
947 "POST",
948 "/repos/:owner/:name/pulls",
949 Op::CreatePullRequest,
950 &[],
951 ),
952 route(
953 "GET",
954 "/repos/:owner/:name/pulls/:number",
955 Op::GetPullRequest,
956 &[],
957 ),
958 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar959 "PATCH",
960 "/repos/:owner/:name/pulls/:number",
961 Op::UpdatePullRequest,
962 &[],
963 ),
964 route(
Fixes from the page sweep965 "GET",
966 "/repos/:owner/:name/pulls/:number/changes",
967 Op::GetPullRequestChanges,
968 &[],
969 ),
970 route(
971 "POST",
972 "/repos/:owner/:name/pulls/:number/reviews",
973 Op::ReviewPullRequest,
974 &[],
975 ),
976 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar977 "POST",
978 "/repos/:owner/:name/pulls/:number/requested_reviewers",
979 Op::RequestReviewers,
980 &[],
981 ),
982 route(
983 "DELETE",
984 "/repos/:owner/:name/pulls/:number/requested_reviewers",
985 Op::RemoveRequestedReviewers,
986 &[],
987 ),
988 route(
Fixes from the page sweep989 "GET",
990 "/repos/:owner/:name/pulls/:number/session",
991 Op::ReadSession,
992 &[("after", "after")],
993 ),
994 route(
995 "POST",
996 "/repos/:owner/:name/pulls/:number/session",
997 Op::RecordSession,
998 &[],
999 ),
Merge branch 'worktree-agent-ab8033ab973f8d99a'1000 // Decision records, on a pull request or an issue (with its pull
1001 // requests'), and why a line is the way it is.
1002 route("GET", "/repos/:owner/:name/pulls/:number/decisions", Op::ListDecisions, &[]),
1003 route("POST", "/repos/:owner/:name/pulls/:number/decisions", Op::RecordDecision, &[]),
1004 route("GET", "/repos/:owner/:name/issues/:number/decisions", Op::ListDecisions, &[]),
1005 route("GET", "/repos/:owner/:name/why", Op::Why, &[("path", "path"), ("line", "line"), ("ref", "ref")]),
Fixes from the page sweep1006 route(
1007 "POST",
1008 "/repos/:owner/:name/pulls/:number/ready",
1009 Op::MarkPullRequestReady,
1010 &[],
1011 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1012 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Fixes from the page sweep1013 route(
1014 "POST",
1015 "/repos/:owner/:name/pulls/:number/close",
1016 Op::ClosePullRequest,
1017 &[],
1018 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1019 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Fixes from the page sweep1020 route(
1021 "POST",
1022 "/repos/:owner/:name/pulls/:number/merge",
1023 Op::MergePullRequest,
1024 &[],
1025 ),
Merge main into Artifacts Phase 21026 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1027 // before an artifact by id, which it would otherwise match.
1028 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")]),
1029 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1030 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1031 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1032 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1033 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1034 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1035 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1036 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1037 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1038 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1039 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1040 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1041 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1042 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1043 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1044 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Fixes from the page sweep1045];
1046
1047impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1048 /// Whether the route answers success with `204` and no body, as
1049 /// GitHub's address for the same does. MCP still answers `true`.
1050 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91051 matches!(
1052 self.op,
1053 Op::DeleteComment
1054 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1055 }
1056
Fixes from the page sweep1057 /// The names of the route's path parameters, in order.
1058 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1059 self.path
1060 .split('/')
1061 .filter_map(|segment| segment.strip_prefix(':'))
1062 }
1063
1064 /// The values of the path parameters, if `path` is this route's.
1065 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1066 let mut values = Vec::new();
1067 let mut actual = path.trim_end_matches('/').split('/');
1068 for expected in self.path.split('/') {
1069 let segment = actual.next()?;
1070 match expected.strip_prefix(':') {
1071 Some(name) if !segment.is_empty() => values.push((name, segment)),
1072 Some(_) => return None,
1073 None if expected == segment => {}
1074 None => return None,
1075 }
1076 }
1077 actual.next().is_none().then_some(values)
1078 }
1079}
1080
1081/// A path segment with its `%XX` escapes decoded; as given when that is not
1082/// UTF-8.
1083fn percent_decoded(segment: &str) -> String {
1084 let bytes = segment.as_bytes();
1085 let mut out = Vec::with_capacity(bytes.len());
1086 let mut i = 0;
1087 while i < bytes.len() {
1088 let escaped = (bytes[i] == b'%')
1089 .then(|| segment.get(i + 1..i + 3))
1090 .flatten()
1091 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1092 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1093 match escaped {
1094 Some(byte) => {
1095 out.push(byte);
1096 i += 3;
1097 }
1098 None => {
1099 out.push(bytes[i]);
1100 i += 1;
1101 }
1102 }
1103 }
1104 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1105}
1106
1107/// The route for a request, and the operation input it describes.
1108///
1109/// The input is the JSON body, overlaid with the query parameters the route
1110/// 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 bar1111/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1112/// becomes an integer.
Fixes from the page sweep1113pub fn resolve(
1114 method: &str,
1115 path: &str,
1116 query: &[(String, String)],
1117 body: Value,
1118) -> Option<(&'static Route, Value)> {
1119 let (route, params) = ROUTES
1120 .iter()
1121 .filter(|route| route.method == method)
1122 .find_map(|route| Some((route, route.matches(path)?)))?;
1123
1124 let mut input = match body {
1125 Value::Object(fields) => fields,
1126 _ => Map::new(),
1127 };
1128 for (name, key) in route.query {
1129 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1130 input.insert((*key).to_owned(), Value::String(value.clone()));
1131 }
1132 }
1133 let param = |wanted: &str| {
1134 params
1135 .iter()
1136 .find(|(name, _)| *name == wanted)
1137 .map(|(_, value)| *value)
1138 };
1139 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1140 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1141 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971142 // Every other name the path gives, under that name; the ones below that
1143 // need more (a repository, a number, an encoded name) are set after.
1144 for (key, value) in &params {
1145 if !matches!(*key, "owner" | "name") {
1146 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Fixes from the page sweep1147 }
1148 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1149 // A repository of a team's workspace, named by itself.
1150 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1151 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1152 }
1153 // A branch name may hold slashes, sent URL-encoded as one segment, and
1154 // a label's name spaces.
Fixes from the page sweep1155 if let Some(branch) = param("branch") {
1156 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1157 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971158 // An environment's name may hold slashes and spaces, URL-encoded.
1159 if let Some(environment) = param("environment") {
1160 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1161 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1162 if let Some(label) = param("label") {
1163 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1164 }
1165 if let Some(milestone) = param("milestone") {
1166 // Not a number: zero, which no milestone has.
1167 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1168 }
Fixes from the page sweep1169 // GitHub says some things with the path alone.
1170 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1171 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1172 }
1173 if route.path.ends_with("/rerun-failed-jobs") {
1174 input.insert("failed_only".to_owned(), Value::Bool(true));
1175 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1176 if route.path.ends_with("/force-cancel") {
1177 input.insert("force".to_owned(), Value::Bool(true));
1178 }
API: notifications over REST and MCP, with notifications scopes1179 // Unsaving and waking a thread are a DELETE of what PUT made.
1180 if route.method == "DELETE" && route.path.ends_with("/saved") {
1181 input.insert("saved".to_owned(), Value::Bool(false));
1182 }
1183 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1184 input.remove("until");
1185 }
Fixes from the page sweep1186 if let Some(number) = param("number") {
1187 // Not a number: zero, which no issue or pull request has.
1188 input.insert(
1189 "number".to_owned(),
1190 number.parse::<u32>().unwrap_or(0).into(),
1191 );
1192 }
1193 Some((route, Value::Object(input)))
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use serde_json::json;
1199
1200 use super::*;
1201
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971202 /// Every name a route's path gives reaches the operation: a name left
1203 /// off the lists above is dropped, and the operation answers that it
1204 /// was not given (the project routes were, until this test).
1205 #[test]
1206 fn every_path_parameter_reaches_the_input() {
1207 for route in ROUTES.iter() {
1208 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1209 if names.is_empty() {
1210 continue;
1211 }
1212 let path: String = route
1213 .path
1214 .split('/')
1215 .map(|part| match part.strip_prefix(':') {
1216 Some("number" | "milestone") => "7".to_owned(),
1217 Some(name) => format!("{name}-x"),
1218 None => part.to_owned(),
1219 })
1220 .collect::<Vec<_>>()
1221 .join("/");
1222 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1223 // A path two routes could take is checked under the first.
1224 if found.path != route.path {
1225 continue;
1226 }
1227 for name in names {
1228 let key = match name {
1229 "owner" | "name" => "repo",
1230 "repo" if names_has_workspace(route.path) => "repo",
1231 other => other,
1232 };
1233 assert!(
1234 input.get(key).is_some_and(|value| !value.is_null()),
1235 "{} {}: :{name} does not reach the input",
1236 route.method,
1237 route.path
1238 );
1239 }
1240 }
1241 }
1242
1243 fn names_has_workspace(path: &str) -> bool {
1244 path.split('/').any(|part| part == ":workspace")
1245 }
1246
Fixes from the page sweep1247 #[test]
1248 fn a_path_resolves_to_its_operation_and_input() {
1249 let (route, input) = resolve(
1250 "POST",
1251 "/repos/flagon-io/hello/pulls/14/merge",
1252 &[],
1253 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1254 )
1255 .unwrap();
1256 assert_eq!(route.op, Op::MergePullRequest);
1257 // What the path names wins over the body.
1258 assert_eq!(
1259 input,
1260 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
1261 );
1262 }
1263
1264 #[test]
1265 fn a_branch_with_slashes_is_one_encoded_segment() {
1266 let (route, input) = resolve(
1267 "POST",
1268 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1269 &[],
1270 json!({ "new_name": "feature/sign-in" }),
1271 )
1272 .unwrap();
1273 assert_eq!(route.op, Op::RenameBranch);
1274 assert_eq!(
1275 input,
1276 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1277 );
1278 assert_eq!(percent_decoded("100%"), "100%");
1279 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1280 }
1281
1282 #[test]
1283 fn a_collaborator_is_named_by_username() {
1284 let (route, input) = resolve(
1285 "PATCH",
1286 "/repos/flagon-io/hello/collaborators/ada",
1287 &[],
1288 json!({ "role": "maintain" }),
1289 )
1290 .unwrap();
1291 assert_eq!(route.op, Op::UpdateCollaborator);
1292 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1293 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1294 assert_eq!(route.op, Op::DeclineRepoInvitation);
1295 assert_eq!(input, json!({ "id": "rin_1" }));
1296 }
1297
1298 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1299 fn teams_are_addressed_by_workspace_and_slug() {
1300 let query = [("q".to_owned(), "back".to_owned())];
1301 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1302 assert_eq!(route.op, Op::ListTeams);
1303 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1304 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1305 assert_eq!(route.op, Op::UpdateTeam);
1306 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1307 let (route, input) =
1308 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1309 assert_eq!(route.op, Op::SetTeamMember);
1310 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1311 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1312 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1313 assert_eq!(route.op, Op::ListTeamMembers);
1314 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1315 // A repository is named by itself, in the team's workspace.
1316 let (route, input) =
1317 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1318 assert_eq!(route.op, Op::SetTeamRepo);
1319 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1320 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1321 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1322 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1323 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1324 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1325 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1326 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1327 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1328 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1329 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1330 assert_eq!(route.op, Op::ListUserTeams);
1331 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1332 }
1333
1334 #[test]
1335 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1336 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1337 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1338 assert_eq!(route.op, Op::RequestReviewers);
1339 assert_eq!(
1340 input,
1341 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1342 );
1343 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1344 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1345 let query = [("ref".to_owned(), "main".to_owned())];
1346 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1347 assert_eq!(route.op, Op::GetCodeownersErrors);
1348 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1349 }
1350
1351 #[test]
Merge branch 'worktree-agent-ad531323a21f4c975'1352 fn events_are_followed_with_a_cursor_filters_and_a_wait() {
1353 let query = [
1354 ("after".to_owned(), "evt_1".to_owned()),
1355 ("types".to_owned(), "work.claimed,integration.failed".to_owned()),
1356 ("numbers".to_owned(), "12,14".to_owned()),
1357 ("wait".to_owned(), "25".to_owned()),
1358 ("limit".to_owned(), "20".to_owned()),
1359 ("since".to_owned(), "2026-10-12T00:00:00Z".to_owned()),
1360 ];
1361 let (route, input) = resolve("GET", "/repos/acme/rocket/events", &query, Value::Null).unwrap();
1362 assert_eq!(route.op, Op::ListEvents);
1363 let asked = crate::follow::asked(&input).unwrap();
1364 assert_eq!(asked.after.as_deref(), Some("evt_1"));
1365 assert_eq!(asked.types, vec!["integration.failed", "work.claimed"]);
1366 assert_eq!(asked.numbers, vec![12, 14]);
1367 assert_eq!(asked.wait_ms, 25_000);
1368 assert_eq!(asked.limit, Some(20));
1369 assert_eq!(asked.since.as_deref(), Some("2026-10-12T00:00:00Z"));
1370 }
1371
1372 #[test]
API: notifications over REST and MCP, with notifications scopes1373 fn notifications_are_addressed_as_threads_and_by_issue() {
1374 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1375 assert_eq!(route.op, Op::MarkThreadDone);
1376 assert_eq!(input, json!({ "id": "ntf_1" }));
1377 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1378 assert_eq!(route.op, Op::SaveThread);
1379 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1380 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1381 assert_eq!(route.op, Op::SnoozeThread);
1382 assert_eq!(input, json!({ "id": "ntf_1" }));
1383 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1384 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1385 assert_eq!(route.op, Op::ListNotifications);
1386 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1387 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1388 assert_eq!(route.op, Op::SetThreadSubscription);
1389 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1390 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1391 }
1392
1393 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1394 fn labels_and_milestones_are_named_in_the_path() {
1395 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1396 assert_eq!(route.op, Op::UpdateLabel);
1397 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1398 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1399 assert_eq!(route.op, Op::RemoveIssueLabels);
1400 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1401 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1402 assert_eq!(route.op, Op::RemoveIssueLabels);
1403 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1404 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1405 assert_eq!(route.op, Op::AddDefaultLabels);
1406 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1407 assert_eq!(route.op, Op::UpdateMilestone);
1408 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1409 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1410 assert_eq!(route.op, Op::UpdatePullRequest);
1411 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1412 }
1413
1414 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1415 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1416 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1417 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1418 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1419 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1420 // Reopening and closing with a PATCH, as `state`.
1421 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1422 assert_eq!(route.op, Op::UpdatePullRequest);
1423 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1424 let (route, input) =
1425 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1426 assert_eq!(route.op, Op::EditComment);
1427 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1428 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1429 assert_eq!(route.op, Op::DeleteComment);
1430 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1431 // Still an issue's comments, labels and subscription.
1432 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1433 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1434 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1435 }
1436
1437 #[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.1438 fn observability_is_addressed_by_workspace_and_project() {
1439 let query = [("level".to_owned(), "error,warn".to_owned()), ("q".to_owned(), "timeout".to_owned())];
1440 let (route, input) = resolve("GET", "/workspaces/acme/projects/web/observability/logs", &query, Value::Null).unwrap();
1441 assert_eq!(route.op, Op::Telemetry(TelemetryOp::ListLogs));
1442 assert_eq!(input, json!({ "level": "error,warn", "q": "timeout", "workspace": "acme", "project": "web" }));
1443 let (route, input) =
1444 resolve("GET", "/workspaces/acme/projects/web/observability/metrics/http.server.request.duration", &[], Value::Null).unwrap();
1445 assert_eq!(route.op, Op::Telemetry(TelemetryOp::GetMetric));
1446 assert_eq!(input["metric"], json!("http.server.request.duration"));
1447 let (route, input) =
1448 resolve("PATCH", "/workspaces/acme/projects/web/observability/errors/f00d", &[], json!({ "state": "resolved" })).unwrap();
1449 assert_eq!(route.op, Op::Telemetry(TelemetryOp::UpdateError));
1450 assert_eq!(input, json!({ "state": "resolved", "workspace": "acme", "project": "web", "fingerprint": "f00d" }));
1451 }
1452
1453 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1454 fn billing_is_addressed_by_workspace() {
1455 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1456 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1457 assert_eq!(route.op, Op::GetUsage);
1458 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1459 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1460 assert_eq!(route.op, Op::SetBudget);
1461 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1462 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1463 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1464 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1465 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
1466 }
1467
1468 #[test]
Merge checks: statuses and check runs on every commit1469 fn checks_are_at_githubs_addresses() {
1470 let sha = "a".repeat(40);
1471 let body = json!({ "state": "success", "context": "ci/build" });
1472 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1473 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1474 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1475 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1476 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1477 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1478 let query = [("check_name".to_owned(), "lint".to_owned())];
1479 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1480 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1481 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1482 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1483 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1484 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1485 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1486 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1487 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1488 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1489 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1490 }
1491
1492 #[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.1493 fn archives_are_at_githubs_addresses_and_redirect() {
1494 let sha = "4807077b296e6edbf410d55e72749d3e1170c291";
1495 let (route, input) = resolve("GET", &format!("/repos/acme/web/tarball/{sha}"), &[], Value::Null).unwrap();
1496 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadTarball));
1497 assert_eq!(input, json!({ "ref": sha, "repo": "acme/web" }));
1498 // A branch with slashes, URL-encoded as octokit sends it.
1499 let (route, input) = resolve("GET", "/repos/acme/web/zipball/release%2F1.x", &[], Value::Null).unwrap();
1500 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadZipball));
1501 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1502 // Both answer with a redirect to their link, as the artifact download does.
1503 assert!(route.op.redirects());
1504 assert!(Op::Archives(ArchiveOp::DownloadTarball).redirects());
1505 assert!(Op::Artifacts(ArtifactsOp::DownloadArtifact).redirects());
1506 assert!(!Op::GetRepo.redirects());
1507 // Anyone may ask for a public repository's.
1508 assert!(!route.op.needs_user());
1509 }
1510
1511 #[test]
Fixes from the page sweep1512 fn query_parameters_are_renamed() {
1513 let query = [
1514 ("q".to_owned(), "parser".to_owned()),
1515 ("x".to_owned(), "y".to_owned()),
1516 ];
1517 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1518 assert_eq!(route.op, Op::ListRepos);
1519 assert_eq!(input, json!({ "query": "parser" }));
1520 }
1521
1522 #[test]
1523 fn method_and_shape_must_match() {
1524 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1525 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1526 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1527 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1528 }
1529
1530 #[test]
1531 fn every_parameter_and_query_name_is_an_input() {
1532 for route in ROUTES {
1533 let properties = route.op.properties();
1534 for (_, key) in route.query {
1535 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1536 }
1537 for name in route.params() {
1538 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1539 || properties.contains_key(name);
1540 assert!(covered, "{}: {name}", route.path);
1541 }
1542 }
1543 }
1544
1545 #[test]
1546 fn every_operation_has_a_route() {
1547 for op in Op::ALL {
1548 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1549 }
1550 }
1551}

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