Skip to content

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

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

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

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