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

Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1//! 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;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all17use 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;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all21
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, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all49 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, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all53 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, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all64 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), &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all72 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, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all140 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), &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all246 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")]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all291 // 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), &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all349 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), &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all365 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")]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all393 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 ),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all538 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
539 route(
540 "GET",
541 "/repos/:owner/:name/events",
542 Op::ListEvents,
Merge branch 'worktree-agent-ad531323a21f4c975'543 &[
544 ("before", "before"),
545 ("after", "after"),
546 ("types", "types"),
547 ("numbers", "numbers"),
548 ("since", "since"),
549 ("limit", "limit"),
550 ("wait", "wait"),
551 ],
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all552 ),
553 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar554 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
555 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
556 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
557 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
558 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
559 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
560 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
561 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
562 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
563 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
564 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
565 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
566 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
567 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all568 route(
569 "GET",
570 "/repos/:owner/:name/issues",
571 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar572 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all573 ),
574 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
575 route(
576 "GET",
577 "/repos/:owner/:name/issues/:number",
578 Op::GetIssue,
579 &[],
580 ),
581 route(
582 "PATCH",
583 "/repos/:owner/:name/issues/:number",
584 Op::UpdateIssue,
585 &[],
586 ),
587 route(
588 "POST",
589 "/repos/:owner/:name/issues/:number/close",
590 Op::CloseIssue,
591 &[],
592 ),
593 route(
594 "POST",
595 "/repos/:owner/:name/issues/:number/reopen",
596 Op::ReopenIssue,
597 &[],
598 ),
599 route(
600 "POST",
601 "/repos/:owner/:name/issues/import",
602 Op::ImportIssue,
603 &[],
604 ),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'605 // Coordinating work: claims on issues, an issue's whole context, and
606 // sessions of people's own harnesses.
607 route("GET", "/repos/:owner/:name/issues/:number/work", Op::Coordination(CoordinationOp::GetWork), &[]),
608 route("POST", "/repos/:owner/:name/issues/:number/claim", Op::Coordination(CoordinationOp::ClaimIssue), &[]),
Merge branch 'worktree-agent-aef0bb4ceda89e542'609 // Work with no issue yet: open one from a title and claim it.
610 route("POST", "/repos/:owner/:name/claims", Op::Coordination(CoordinationOp::ClaimIssue), &[]),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'611 route("POST", "/repos/:owner/:name/issues/:number/release", Op::Coordination(CoordinationOp::ReleaseClaim), &[]),
612 route("POST", "/repos/:owner/:name/issues/:number/handoff", Op::Coordination(CoordinationOp::HandOffIssue), &[]),
613 route(
614 "GET",
615 "/repos/:owner/:name/claims",
616 Op::Coordination(CoordinationOp::ListClaims),
617 &[("state", "state"), ("issue", "issue")],
618 ),
619 route("POST", "/repos/:owner/:name/sessions", Op::Coordination(CoordinationOp::StartSession), &[]),
620 route(
621 "GET",
622 "/repos/:owner/:name/sessions",
623 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.624 &[("state", "state"), ("issue", "issue"), ("pull", "pull"), ("path", "path"), ("subagents", "subagents")],
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'625 ),
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.626 route("POST", "/repos/:owner/:name/agent-sessions", Op::Coordination(CoordinationOp::StartAgentSession), &[]),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'627 route("GET", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::GetSession), &[("after", "after")]),
628 route("PATCH", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::UpdateSession), &[]),
629 route("POST", "/repos/:owner/:name/sessions/:id/end", Op::Coordination(CoordinationOp::EndSession), &[]),
630 route(
631 "POST",
632 "/repos/:owner/:name/sessions/:id/entries",
633 Op::Coordination(CoordinationOp::RecordSessionEntries),
634 &[],
635 ),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all636 route(
637 "GET",
638 "/repos/:owner/:name/context",
639 Op::GetContext,
640 &[("reference", "reference")],
641 ),
642 route(
643 "GET",
644 "/workspaces/:workspace/integrations",
645 Op::ListIntegrations,
646 &[],
647 ),
648 route(
649 "POST",
650 "/workspaces/:workspace/integrations",
651 Op::ConnectIntegration,
652 &[],
653 ),
654 route(
655 "GET",
656 "/repos/:owner/:name/hooks",
657 Op::ListWebhooks,
658 &[],
659 ),
660 route(
661 "POST",
662 "/repos/:owner/:name/hooks",
663 Op::CreateWebhook,
664 &[],
665 ),
666 route(
667 "PATCH",
668 "/repos/:owner/:name/hooks/:id",
669 Op::UpdateWebhook,
670 &[],
671 ),
672 route(
673 "DELETE",
674 "/repos/:owner/:name/hooks/:id",
675 Op::DeleteWebhook,
676 &[],
677 ),
678 route(
679 "POST",
680 "/repos/:owner/:name/hooks/:id/pings",
681 Op::PingWebhook,
682 &[],
683 ),
684 route(
685 "GET",
686 "/repos/:owner/:name/hooks/:id/deliveries",
687 Op::ListWebhookDeliveries,
688 &[],
689 ),
690 route(
691 "POST",
692 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
693 Op::RedeliverWebhook,
694 &[],
695 ),
696 route(
697 "GET",
698 "/workspaces/:workspace/hooks",
699 Op::ListWebhooks,
700 &[],
701 ),
702 route(
703 "POST",
704 "/workspaces/:workspace/hooks",
705 Op::CreateWebhook,
706 &[],
707 ),
708 route(
709 "PATCH",
710 "/workspaces/:workspace/hooks/:id",
711 Op::UpdateWebhook,
712 &[],
713 ),
714 route(
715 "DELETE",
716 "/workspaces/:workspace/hooks/:id",
717 Op::DeleteWebhook,
718 &[],
719 ),
720 route(
721 "POST",
722 "/workspaces/:workspace/hooks/:id/pings",
723 Op::PingWebhook,
724 &[],
725 ),
726 route(
727 "GET",
728 "/workspaces/:workspace/hooks/:id/deliveries",
729 Op::ListWebhookDeliveries,
730 &[],
731 ),
732 route(
733 "POST",
734 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
735 Op::RedeliverWebhook,
736 &[],
737 ),
738 route(
AI Gateway: OpenAI's format, open models, and your own providers739 "PATCH",
740 "/workspaces/:workspace/integrations/:id",
741 Op::UpdateIntegration,
742 &[],
743 ),
744 route(
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all745 "DELETE",
746 "/workspaces/:workspace/integrations/:id",
747 Op::DisconnectIntegration,
748 &[],
749 ),
750 route(
751 "POST",
752 "/workspaces/:workspace/integrations/:id/test",
753 Op::TestIntegration,
754 &[],
755 ),
756 route(
757 "GET",
758 "/repos/:owner/:name/actions/workflows",
759 Op::ListWorkflows,
760 &[],
761 ),
762 route(
763 "GET",
Merge branch 'worktree-agent-a4db4b523bb973765'764 "/repos/:owner/:name/actions/workflows/:workflow",
765 Op::GetWorkflow,
766 &[("ref", "ref")],
767 ),
768 route(
769 "GET",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all770 "/repos/:owner/:name/actions/workflows/:workflow/runs",
771 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.772 &[("branch", "branch"), ("event", "event"), ("status", "status"), ("head_sha", "sha"), ("per_page", "limit")],
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all773 ),
774 route(
775 "POST",
776 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
777 Op::DispatchWorkflow,
778 &[],
779 ),
780 route(
781 "PATCH",
782 "/repos/:owner/:name/actions/workflows/:workflow",
783 Op::UpdateWorkflow,
784 &[],
785 ),
786 route(
787 "PUT",
788 "/repos/:owner/:name/actions/workflows/:workflow/enable",
789 Op::UpdateWorkflow,
790 &[],
791 ),
792 route(
793 "PUT",
794 "/repos/:owner/:name/actions/workflows/:workflow/disable",
795 Op::UpdateWorkflow,
796 &[],
797 ),
798 route(
799 "GET",
800 "/repos/:owner/:name/actions/runs",
801 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.802 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("status", "status"), ("per_page", "limit")],
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all803 ),
804 route(
805 "GET",
806 "/repos/:owner/:name/actions/runs/:id",
807 Op::GetWorkflowRun,
808 &[],
809 ),
810 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)811 "GET",
812 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
813 Op::GetWorkflowRun,
814 &[],
815 ),
816 route(
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all817 "POST",
818 "/repos/:owner/:name/actions/runs/:id/cancel",
819 Op::CancelWorkflowRun,
820 &[],
821 ),
822 route(
823 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)824 "/repos/:owner/:name/actions/runs/:id/force-cancel",
825 Op::CancelWorkflowRun,
826 &[],
827 ),
828 route(
829 "POST",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all830 "/repos/:owner/:name/actions/runs/:id/rerun",
831 Op::RerunWorkflowRun,
832 &[],
833 ),
834 route(
835 "POST",
836 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
837 Op::RerunWorkflowRun,
838 &[],
839 ),
840 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)841 "POST",
842 "/repos/:owner/:name/actions/jobs/:job/rerun",
843 Op::RerunWorkflowRun,
844 &[],
845 ),
846 route(
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all847 "GET",
848 "/repos/:owner/:name/actions/jobs/:job/logs",
849 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.850 &[("after", "after"), ("tail", "tail")],
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all851 ),
Merge packages: roles, Actions access, source label, soft delete, API852 // Packages, at GitHub's addresses with the workspace in place of the
853 // organization. A name with a slash is one URL-encoded segment.
854 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
855 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
856 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
857 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
858 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
859 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
860 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
861 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
862 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
863 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
864 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
865 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
866 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
867 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
868 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
869 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
870 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 R2871 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
872 // a signed link (lib.rs).
873 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
874 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
875 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
876 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
877 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
878 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
879 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all880 route(
881 "GET",
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'882 "/repos/:owner/:name/actions/caches",
883 Op::Caches(CachesOp::ListCaches),
884 &[("key", "key"), ("ref", "ref"), ("sort", "sort"), ("direction", "direction"), ("page", "page"), ("per_page", "per_page")],
885 ),
886 route("GET", "/repos/:owner/:name/actions/cache/usage", Op::Caches(CachesOp::GetCacheUsage), &[]),
887 route("DELETE", "/repos/:owner/:name/actions/caches", Op::Caches(CachesOp::DeleteCachesByKey), &[("key", "key"), ("ref", "ref")]),
888 route("DELETE", "/repos/:owner/:name/actions/caches/:id", Op::Caches(CachesOp::DeleteCache), &[]),
889 route(
890 "GET",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all891 "/repos/:owner/:name/actions/secrets",
892 Op::ListActionsSecrets,
893 &[],
894 ),
895 route(
896 "PUT",
897 "/repos/:owner/:name/actions/secrets/:setting",
898 Op::SetActionsSecret,
899 &[],
900 ),
901 route(
902 "DELETE",
903 "/repos/:owner/:name/actions/secrets/:setting",
904 Op::DeleteActionsSecret,
905 &[],
906 ),
907 route(
908 "GET",
909 "/repos/:owner/:name/actions/variables",
910 Op::ListActionsVariables,
911 &[],
912 ),
913 route(
914 "POST",
915 "/repos/:owner/:name/actions/variables",
916 Op::SetActionsVariable,
917 &[],
918 ),
919 route(
920 "PATCH",
921 "/repos/:owner/:name/actions/variables/:setting",
922 Op::SetActionsVariable,
923 &[],
924 ),
925 route(
926 "DELETE",
927 "/repos/:owner/:name/actions/variables/:setting",
928 Op::DeleteActionsVariable,
929 &[],
930 ),
931 route(
932 "GET",
933 "/workspaces/:workspace/actions/secrets",
934 Op::ListActionsSecrets,
935 &[],
936 ),
937 route(
938 "PUT",
939 "/workspaces/:workspace/actions/secrets/:setting",
940 Op::SetActionsSecret,
941 &[],
942 ),
943 route(
944 "DELETE",
945 "/workspaces/:workspace/actions/secrets/:setting",
946 Op::DeleteActionsSecret,
947 &[],
948 ),
949 route(
950 "GET",
951 "/workspaces/:workspace/actions/variables",
952 Op::ListActionsVariables,
953 &[],
954 ),
955 route(
956 "POST",
957 "/workspaces/:workspace/actions/variables",
958 Op::SetActionsVariable,
959 &[],
960 ),
961 route(
962 "PATCH",
963 "/workspaces/:workspace/actions/variables/:setting",
964 Op::SetActionsVariable,
965 &[],
966 ),
967 route(
968 "DELETE",
969 "/workspaces/:workspace/actions/variables/:setting",
970 Op::DeleteActionsVariable,
971 &[],
972 ),
973 // Self-hosted runners: a repository's own, or a workspace's.
974 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
975 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
976 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
977 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
978 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
979 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
980 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
981 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
982 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
983 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
984 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
985 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
986 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
987 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
988 route(
989 "POST",
990 "/repos/:owner/:name/issues/:number/comments",
991 Op::AddComment,
992 &[],
993 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts994 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
995 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all996 route(
997 "GET",
998 "/repos/:owner/:name/pulls",
999 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1000 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1001 ),
1002 route(
1003 "POST",
1004 "/repos/:owner/:name/pulls",
1005 Op::CreatePullRequest,
1006 &[],
1007 ),
1008 route(
1009 "GET",
1010 "/repos/:owner/:name/pulls/:number",
1011 Op::GetPullRequest,
1012 &[],
1013 ),
1014 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1015 "PATCH",
1016 "/repos/:owner/:name/pulls/:number",
1017 Op::UpdatePullRequest,
1018 &[],
1019 ),
1020 route(
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1021 "GET",
1022 "/repos/:owner/:name/pulls/:number/changes",
1023 Op::GetPullRequestChanges,
1024 &[],
1025 ),
1026 route(
1027 "POST",
1028 "/repos/:owner/:name/pulls/:number/reviews",
1029 Op::ReviewPullRequest,
1030 &[],
1031 ),
1032 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1033 "POST",
1034 "/repos/:owner/:name/pulls/:number/requested_reviewers",
1035 Op::RequestReviewers,
1036 &[],
1037 ),
1038 route(
1039 "DELETE",
1040 "/repos/:owner/:name/pulls/:number/requested_reviewers",
1041 Op::RemoveRequestedReviewers,
1042 &[],
1043 ),
1044 route(
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1045 "GET",
1046 "/repos/:owner/:name/pulls/:number/session",
1047 Op::ReadSession,
1048 &[("after", "after")],
1049 ),
1050 route(
1051 "POST",
1052 "/repos/:owner/:name/pulls/:number/session",
1053 Op::RecordSession,
1054 &[],
1055 ),
Merge branch 'worktree-agent-ab8033ab973f8d99a'1056 // Decision records, on a pull request or an issue (with its pull
1057 // requests'), and why a line is the way it is.
1058 route("GET", "/repos/:owner/:name/pulls/:number/decisions", Op::ListDecisions, &[]),
1059 route("POST", "/repos/:owner/:name/pulls/:number/decisions", Op::RecordDecision, &[]),
1060 route("GET", "/repos/:owner/:name/issues/:number/decisions", Op::ListDecisions, &[]),
1061 route("GET", "/repos/:owner/:name/why", Op::Why, &[("path", "path"), ("line", "line"), ("ref", "ref")]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1062 route(
1063 "POST",
1064 "/repos/:owner/:name/pulls/:number/ready",
1065 Op::MarkPullRequestReady,
1066 &[],
1067 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1068 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1069 route(
1070 "POST",
1071 "/repos/:owner/:name/pulls/:number/close",
1072 Op::ClosePullRequest,
1073 &[],
1074 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1075 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1076 route(
1077 "POST",
1078 "/repos/:owner/:name/pulls/:number/merge",
1079 Op::MergePullRequest,
1080 &[],
1081 ),
Merge main into Artifacts Phase 21082 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1083 // before an artifact by id, which it would otherwise match.
1084 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")]),
1085 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1086 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1087 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1088 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1089 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1090 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1091 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1092 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1093 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1094 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1095 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1096 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1097 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1098 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1099 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1100 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1101];
1102
1103impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1104 /// Whether the route answers success with `204` and no body, as
1105 /// GitHub's address for the same does. MCP still answers `true`.
1106 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91107 matches!(
1108 self.op,
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'1109 Op::DeleteComment | Op::Caches(CachesOp::DeleteCache)
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91110 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1111 }
1112
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1113 /// The names of the route's path parameters, in order.
1114 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1115 self.path
1116 .split('/')
1117 .filter_map(|segment| segment.strip_prefix(':'))
1118 }
1119
1120 /// The values of the path parameters, if `path` is this route's.
1121 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1122 let mut values = Vec::new();
1123 let mut actual = path.trim_end_matches('/').split('/');
1124 for expected in self.path.split('/') {
1125 let segment = actual.next()?;
1126 match expected.strip_prefix(':') {
1127 Some(name) if !segment.is_empty() => values.push((name, segment)),
1128 Some(_) => return None,
1129 None if expected == segment => {}
1130 None => return None,
1131 }
1132 }
1133 actual.next().is_none().then_some(values)
1134 }
1135}
1136
1137/// A path segment with its `%XX` escapes decoded; as given when that is not
1138/// UTF-8.
1139fn percent_decoded(segment: &str) -> String {
1140 let bytes = segment.as_bytes();
1141 let mut out = Vec::with_capacity(bytes.len());
1142 let mut i = 0;
1143 while i < bytes.len() {
1144 let escaped = (bytes[i] == b'%')
1145 .then(|| segment.get(i + 1..i + 3))
1146 .flatten()
1147 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1148 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1149 match escaped {
1150 Some(byte) => {
1151 out.push(byte);
1152 i += 3;
1153 }
1154 None => {
1155 out.push(bytes[i]);
1156 i += 1;
1157 }
1158 }
1159 }
1160 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1161}
1162
1163/// The route for a request, and the operation input it describes.
1164///
1165/// The input is the JSON body, overlaid with the query parameters the route
1166/// 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 bar1167/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1168/// becomes an integer.
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1169pub fn resolve(
1170 method: &str,
1171 path: &str,
1172 query: &[(String, String)],
1173 body: Value,
1174) -> Option<(&'static Route, Value)> {
1175 let (route, params) = ROUTES
1176 .iter()
1177 .filter(|route| route.method == method)
1178 .find_map(|route| Some((route, route.matches(path)?)))?;
1179
1180 let mut input = match body {
1181 Value::Object(fields) => fields,
1182 _ => Map::new(),
1183 };
1184 for (name, key) in route.query {
1185 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1186 input.insert((*key).to_owned(), Value::String(value.clone()));
1187 }
1188 }
1189 let param = |wanted: &str| {
1190 params
1191 .iter()
1192 .find(|(name, _)| *name == wanted)
1193 .map(|(_, value)| *value)
1194 };
1195 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1196 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1197 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971198 // Every other name the path gives, under that name; the ones below that
1199 // need more (a repository, a number, an encoded name) are set after.
1200 for (key, value) in &params {
1201 if !matches!(*key, "owner" | "name") {
1202 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1203 }
1204 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1205 // A repository of a team's workspace, named by itself.
1206 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1207 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1208 }
1209 // A branch name may hold slashes, sent URL-encoded as one segment, and
1210 // a label's name spaces.
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1211 if let Some(branch) = param("branch") {
1212 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1213 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971214 // An environment's name may hold slashes and spaces, URL-encoded.
1215 if let Some(environment) = param("environment") {
1216 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1217 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1218 if let Some(label) = param("label") {
1219 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1220 }
1221 if let Some(milestone) = param("milestone") {
1222 // Not a number: zero, which no milestone has.
1223 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1224 }
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1225 // GitHub says some things with the path alone.
1226 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1227 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1228 }
1229 if route.path.ends_with("/rerun-failed-jobs") {
1230 input.insert("failed_only".to_owned(), Value::Bool(true));
1231 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1232 if route.path.ends_with("/force-cancel") {
1233 input.insert("force".to_owned(), Value::Bool(true));
1234 }
API: notifications over REST and MCP, with notifications scopes1235 // Unsaving and waking a thread are a DELETE of what PUT made.
1236 if route.method == "DELETE" && route.path.ends_with("/saved") {
1237 input.insert("saved".to_owned(), Value::Bool(false));
1238 }
1239 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1240 input.remove("until");
1241 }
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1242 if let Some(number) = param("number") {
1243 // Not a number: zero, which no issue or pull request has.
1244 input.insert(
1245 "number".to_owned(),
1246 number.parse::<u32>().unwrap_or(0).into(),
1247 );
1248 }
1249 Some((route, Value::Object(input)))
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use serde_json::json;
1255
1256 use super::*;
1257
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971258 /// Every name a route's path gives reaches the operation: a name left
1259 /// off the lists above is dropped, and the operation answers that it
1260 /// was not given (the project routes were, until this test).
1261 #[test]
1262 fn every_path_parameter_reaches_the_input() {
1263 for route in ROUTES.iter() {
1264 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1265 if names.is_empty() {
1266 continue;
1267 }
1268 let path: String = route
1269 .path
1270 .split('/')
1271 .map(|part| match part.strip_prefix(':') {
1272 Some("number" | "milestone") => "7".to_owned(),
1273 Some(name) => format!("{name}-x"),
1274 None => part.to_owned(),
1275 })
1276 .collect::<Vec<_>>()
1277 .join("/");
1278 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1279 // A path two routes could take is checked under the first.
1280 if found.path != route.path {
1281 continue;
1282 }
1283 for name in names {
1284 let key = match name {
1285 "owner" | "name" => "repo",
1286 "repo" if names_has_workspace(route.path) => "repo",
1287 other => other,
1288 };
1289 assert!(
1290 input.get(key).is_some_and(|value| !value.is_null()),
1291 "{} {}: :{name} does not reach the input",
1292 route.method,
1293 route.path
1294 );
1295 }
1296 }
1297 }
1298
1299 fn names_has_workspace(path: &str) -> bool {
1300 path.split('/').any(|part| part == ":workspace")
1301 }
1302
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1303 #[test]
1304 fn a_path_resolves_to_its_operation_and_input() {
1305 let (route, input) = resolve(
1306 "POST",
1307 "/repos/flagon-io/hello/pulls/14/merge",
1308 &[],
1309 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1310 )
1311 .unwrap();
1312 assert_eq!(route.op, Op::MergePullRequest);
1313 // What the path names wins over the body.
1314 assert_eq!(
1315 input,
1316 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
1317 );
1318 }
1319
1320 #[test]
1321 fn a_branch_with_slashes_is_one_encoded_segment() {
1322 let (route, input) = resolve(
1323 "POST",
1324 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1325 &[],
1326 json!({ "new_name": "feature/sign-in" }),
1327 )
1328 .unwrap();
1329 assert_eq!(route.op, Op::RenameBranch);
1330 assert_eq!(
1331 input,
1332 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1333 );
1334 assert_eq!(percent_decoded("100%"), "100%");
1335 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1336 }
1337
1338 #[test]
1339 fn a_collaborator_is_named_by_username() {
1340 let (route, input) = resolve(
1341 "PATCH",
1342 "/repos/flagon-io/hello/collaborators/ada",
1343 &[],
1344 json!({ "role": "maintain" }),
1345 )
1346 .unwrap();
1347 assert_eq!(route.op, Op::UpdateCollaborator);
1348 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1349 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1350 assert_eq!(route.op, Op::DeclineRepoInvitation);
1351 assert_eq!(input, json!({ "id": "rin_1" }));
1352 }
1353
1354 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1355 fn teams_are_addressed_by_workspace_and_slug() {
1356 let query = [("q".to_owned(), "back".to_owned())];
1357 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1358 assert_eq!(route.op, Op::ListTeams);
1359 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1360 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1361 assert_eq!(route.op, Op::UpdateTeam);
1362 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1363 let (route, input) =
1364 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1365 assert_eq!(route.op, Op::SetTeamMember);
1366 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1367 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1368 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1369 assert_eq!(route.op, Op::ListTeamMembers);
1370 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1371 // A repository is named by itself, in the team's workspace.
1372 let (route, input) =
1373 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1374 assert_eq!(route.op, Op::SetTeamRepo);
1375 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1376 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1377 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1378 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1379 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1380 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1381 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1382 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1383 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1384 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1385 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1386 assert_eq!(route.op, Op::ListUserTeams);
1387 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1388 }
1389
1390 #[test]
1391 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1392 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1393 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1394 assert_eq!(route.op, Op::RequestReviewers);
1395 assert_eq!(
1396 input,
1397 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1398 );
1399 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1400 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1401 let query = [("ref".to_owned(), "main".to_owned())];
1402 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1403 assert_eq!(route.op, Op::GetCodeownersErrors);
1404 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1405 }
1406
1407 #[test]
Merge branch 'worktree-agent-ad531323a21f4c975'1408 fn events_are_followed_with_a_cursor_filters_and_a_wait() {
1409 let query = [
1410 ("after".to_owned(), "evt_1".to_owned()),
1411 ("types".to_owned(), "work.claimed,integration.failed".to_owned()),
1412 ("numbers".to_owned(), "12,14".to_owned()),
1413 ("wait".to_owned(), "25".to_owned()),
1414 ("limit".to_owned(), "20".to_owned()),
1415 ("since".to_owned(), "2026-10-12T00:00:00Z".to_owned()),
1416 ];
1417 let (route, input) = resolve("GET", "/repos/acme/rocket/events", &query, Value::Null).unwrap();
1418 assert_eq!(route.op, Op::ListEvents);
1419 let asked = crate::follow::asked(&input).unwrap();
1420 assert_eq!(asked.after.as_deref(), Some("evt_1"));
1421 assert_eq!(asked.types, vec!["integration.failed", "work.claimed"]);
1422 assert_eq!(asked.numbers, vec![12, 14]);
1423 assert_eq!(asked.wait_ms, 25_000);
1424 assert_eq!(asked.limit, Some(20));
1425 assert_eq!(asked.since.as_deref(), Some("2026-10-12T00:00:00Z"));
1426 }
1427
1428 #[test]
API: notifications over REST and MCP, with notifications scopes1429 fn notifications_are_addressed_as_threads_and_by_issue() {
1430 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1431 assert_eq!(route.op, Op::MarkThreadDone);
1432 assert_eq!(input, json!({ "id": "ntf_1" }));
1433 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1434 assert_eq!(route.op, Op::SaveThread);
1435 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1436 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1437 assert_eq!(route.op, Op::SnoozeThread);
1438 assert_eq!(input, json!({ "id": "ntf_1" }));
1439 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1440 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1441 assert_eq!(route.op, Op::ListNotifications);
1442 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1443 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1444 assert_eq!(route.op, Op::SetThreadSubscription);
1445 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1446 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1447 }
1448
1449 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1450 fn labels_and_milestones_are_named_in_the_path() {
1451 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1452 assert_eq!(route.op, Op::UpdateLabel);
1453 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1454 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1455 assert_eq!(route.op, Op::RemoveIssueLabels);
1456 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1457 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1458 assert_eq!(route.op, Op::RemoveIssueLabels);
1459 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1460 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1461 assert_eq!(route.op, Op::AddDefaultLabels);
1462 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1463 assert_eq!(route.op, Op::UpdateMilestone);
1464 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1465 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1466 assert_eq!(route.op, Op::UpdatePullRequest);
1467 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1468 }
1469
1470 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1471 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1472 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1473 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1474 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1475 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1476 // Reopening and closing with a PATCH, as `state`.
1477 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1478 assert_eq!(route.op, Op::UpdatePullRequest);
1479 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1480 let (route, input) =
1481 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1482 assert_eq!(route.op, Op::EditComment);
1483 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1484 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1485 assert_eq!(route.op, Op::DeleteComment);
1486 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1487 // Still an issue's comments, labels and subscription.
1488 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1489 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1490 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1491 }
1492
1493 #[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.1494 fn observability_is_addressed_by_workspace_and_project() {
1495 let query = [("level".to_owned(), "error,warn".to_owned()), ("q".to_owned(), "timeout".to_owned())];
1496 let (route, input) = resolve("GET", "/workspaces/acme/projects/web/observability/logs", &query, Value::Null).unwrap();
1497 assert_eq!(route.op, Op::Telemetry(TelemetryOp::ListLogs));
1498 assert_eq!(input, json!({ "level": "error,warn", "q": "timeout", "workspace": "acme", "project": "web" }));
1499 let (route, input) =
1500 resolve("GET", "/workspaces/acme/projects/web/observability/metrics/http.server.request.duration", &[], Value::Null).unwrap();
1501 assert_eq!(route.op, Op::Telemetry(TelemetryOp::GetMetric));
1502 assert_eq!(input["metric"], json!("http.server.request.duration"));
1503 let (route, input) =
1504 resolve("PATCH", "/workspaces/acme/projects/web/observability/errors/f00d", &[], json!({ "state": "resolved" })).unwrap();
1505 assert_eq!(route.op, Op::Telemetry(TelemetryOp::UpdateError));
1506 assert_eq!(input, json!({ "state": "resolved", "workspace": "acme", "project": "web", "fingerprint": "f00d" }));
1507 }
1508
1509 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1510 fn billing_is_addressed_by_workspace() {
1511 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1512 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1513 assert_eq!(route.op, Op::GetUsage);
1514 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1515 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1516 assert_eq!(route.op, Op::SetBudget);
1517 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1518 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1519 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1520 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1521 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
1522 }
1523
1524 #[test]
Merge checks: statuses and check runs on every commit1525 fn checks_are_at_githubs_addresses() {
1526 let sha = "a".repeat(40);
1527 let body = json!({ "state": "success", "context": "ci/build" });
1528 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1529 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1530 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1531 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1532 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1533 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1534 let query = [("check_name".to_owned(), "lint".to_owned())];
1535 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1536 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1537 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1538 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1539 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1540 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1541 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1542 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1543 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1544 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1545 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1546 }
1547
1548 #[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.1549 fn archives_are_at_githubs_addresses_and_redirect() {
1550 let sha = "4807077b296e6edbf410d55e72749d3e1170c291";
1551 let (route, input) = resolve("GET", &format!("/repos/acme/web/tarball/{sha}"), &[], Value::Null).unwrap();
1552 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadTarball));
1553 assert_eq!(input, json!({ "ref": sha, "repo": "acme/web" }));
1554 // A branch with slashes, URL-encoded as octokit sends it.
1555 let (route, input) = resolve("GET", "/repos/acme/web/zipball/release%2F1.x", &[], Value::Null).unwrap();
1556 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadZipball));
1557 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1558 // Both answer with a redirect to their link, as the artifact download does.
1559 assert!(route.op.redirects());
1560 assert!(Op::Archives(ArchiveOp::DownloadTarball).redirects());
1561 assert!(Op::Artifacts(ArtifactsOp::DownloadArtifact).redirects());
1562 assert!(!Op::GetRepo.redirects());
1563 // Anyone may ask for a public repository's.
1564 assert!(!route.op.needs_user());
1565 }
1566
1567 #[test]
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'1568 fn caches_are_at_githubs_addresses() {
1569 let query = [("key".to_owned(), "cargo-".to_owned()), ("ref".to_owned(), "refs/heads/main".to_owned()), ("sort".to_owned(), "size_in_bytes".to_owned())];
1570 let (route, input) = resolve("GET", "/repos/acme/web/actions/caches", &query, Value::Null).unwrap();
1571 assert_eq!(route.op, Op::Caches(CachesOp::ListCaches));
1572 assert_eq!(input, json!({ "key": "cargo-", "ref": "refs/heads/main", "sort": "size_in_bytes", "repo": "acme/web" }));
1573 let (route, _) = resolve("GET", "/repos/acme/web/actions/cache/usage", &[], Value::Null).unwrap();
1574 assert_eq!(route.op, Op::Caches(CachesOp::GetCacheUsage));
1575 // Deleting by key takes it from the query, as GitHub's does.
1576 let query = [("key".to_owned(), "npm-full-Linux-abc".to_owned())];
1577 let (route, input) = resolve("DELETE", "/repos/acme/web/actions/caches", &query, Value::Null).unwrap();
1578 assert_eq!(route.op, Op::Caches(CachesOp::DeleteCachesByKey));
1579 assert_eq!(input, json!({ "key": "npm-full-Linux-abc", "repo": "acme/web" }));
1580 assert!(!route.no_content());
1581 let (route, input) = resolve("DELETE", "/repos/acme/web/actions/caches/42", &[], Value::Null).unwrap();
1582 assert_eq!(route.op, Op::Caches(CachesOp::DeleteCache));
1583 assert_eq!(input, json!({ "id": "42", "repo": "acme/web" }));
1584 assert!(route.no_content());
1585 // Only someone signed in sees a repository's cache keys.
1586 assert!(Op::Caches(CachesOp::ListCaches).needs_user());
1587 }
1588
1589 #[test]
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1590 fn query_parameters_are_renamed() {
1591 let query = [
1592 ("q".to_owned(), "parser".to_owned()),
1593 ("x".to_owned(), "y".to_owned()),
1594 ];
1595 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1596 assert_eq!(route.op, Op::ListRepos);
1597 assert_eq!(input, json!({ "query": "parser" }));
1598 }
1599
1600 #[test]
1601 fn method_and_shape_must_match() {
1602 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1603 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1604 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1605 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1606 }
1607
1608 #[test]
1609 fn every_parameter_and_query_name_is_an_input() {
1610 for route in ROUTES {
1611 let properties = route.op.properties();
1612 for (_, key) in route.query {
1613 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1614 }
1615 for name in route.params() {
1616 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1617 || properties.contains_key(name);
1618 assert!(covered, "{}: {name}", route.path);
1619 }
1620 }
1621 }
1622
1623 #[test]
1624 fn every_operation_has_a_route() {
1625 for op in Op::ALL {
1626 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1627 }
1628 }
1629}

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