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

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

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