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,620 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.

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

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