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,617 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

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

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