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

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