Skip to content

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

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

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

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