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

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

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