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

A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1//! REST: each route maps an HTTP request onto one operation.
2
3use serde_json::{Map, Value};
4
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb975use crate::about::AboutOp;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.6use crate::run_artifacts::ArtifactsOp;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.7use crate::archives::ArchiveOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca8use crate::deploy_keys::DeployKeysOp;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb979use 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.10use crate::telemetry::TelemetryOp;
Merge packages: roles, Actions access, source label, soft delete, API11use crate::packages::PackagesOp;
Merge main into Artifacts Phase 212use crate::folios::FoliosOp;
The API and MCP server coordinate work: start_session, update_session (an empty body is a heartbeat), end_session, get_session, list_sessions and record_session_entries at /repos/{owner}/{name}/sessions, as the new session tool's start, update, end, get, list and record; claim_issue, release_claim, hand_off_issue and get_work at /repos/{owner}/{name}/issues/{number}/claim, /release, /handoff and /work, and list_claims at /repos/{owner}/{name}/claims, as the issue tool's claim, release, handoff, work and list_claims; and update_issue takes blocked_by. Sessions need pull_requests scopes and claims and get_work issues scopes, reads are open to anyone who can read the repository, the reference has an example of each, and the OpenAPI copy and the CLI's table of operations are regenerated.13use crate::coordination::CoordinationOp;
Merge branch 'worktree-agent-a3abfcce648e87dca'14use crate::protection::ProtectionOp;
API and MCP for a workspace's personal access token rules, members' tokens and approvals15use crate::token_policy::TokenOp;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait16use crate::operations::Op;
Merge checks: statuses and check runs on every commit17use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge18use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar19use crate::security::SecurityOp;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait20
21pub struct Route {
22 pub method: &'static str,
23 /// Segments starting with `:` are parameters.
24 pub path: &'static str,
25 pub op: Op,
26 /// Query parameters the route reads, as `(name in the URL, input name)`.
27 pub query: &'static [(&'static str, &'static str)],
28}
29
30const fn route(
31 method: &'static str,
32 path: &'static str,
33 op: Op,
34 query: &'static [(&'static str, &'static str)],
35) -> Route {
36 Route {
37 method,
38 path,
39 op,
40 query,
41 }
42}
43
44pub const ROUTES: &[Route] = &[
45 route("GET", "/user", Op::Whoami, &[]),
46 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
Merge branch 'worktree-agent-ad7c6d88d93adc817'47 route("GET", "/workspaces/:workspace", Op::GetWorkspace, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait48 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
49 route("GET", "/user/emails", Op::ListEmails, &[]),
50 route("POST", "/user/emails", Op::AddEmail, &[]),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)51 route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait52 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
53 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)54 // Invitations to workspaces waiting for your answer.
55 route("GET", "/user/invitations", Op::ListInvitations, &[]),
56 route("POST", "/user/invitations/:id/accept", Op::AcceptInvitation, &[]),
57 route("POST", "/user/invitations/:id/decline", Op::DeclineInvitation, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait58 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals59 // A workspace's rules for personal access tokens, and its members' tokens.
60 route("GET", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::GetTokenPolicy), &[]),
61 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 managers62 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 approvals63 route("POST", "/workspaces/:workspace/personal-access-tokens/:id", Op::Tokens(TokenOp::RevokeMemberToken), &[]),
64 route("GET", "/workspaces/:workspace/personal-access-token-requests", Op::Tokens(TokenOp::ListTokenRequests), &[]),
65 route("POST", "/workspaces/:workspace/personal-access-token-requests/:id", Op::Tokens(TokenOp::ReviewTokenRequest), &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait66 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
67 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
68 // Who has access. GitHub's addresses, but for adding someone, which
69 // takes an email address as well as a username.
70 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
71 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
72 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
73 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
74 route(
75 "GET",
76 "/repos/:owner/:name/collaborators/:username/permission",
77 Op::GetCollaboratorPermission,
78 &[],
79 ),
80 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
81 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
82 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca83 // Deploy keys, at GitHub's addresses.
84 route("GET", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::ListDeployKeys), &[]),
85 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
86 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
87 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
API: notifications over REST and MCP, with notifications scopes88 // Your notifications: threads, marking them, and what you subscribe
89 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
90 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")]),
91 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
92 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
93 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
94 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
95 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
96 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
97 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
98 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
99 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
100 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
101 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
102 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")]),
103 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
104 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
105 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
106 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
107 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
108 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
109 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
110 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97111 // Stars: yours, and who starred a repository.
112 route("GET", "/user/starred", Op::About(AboutOp::ListStarred), &[]),
113 route("GET", "/user/starred/:owner/:name", Op::About(AboutOp::CheckStarred), &[]),
114 route("PUT", "/user/starred/:owner/:name", Op::About(AboutOp::Star), &[]),
115 route("DELETE", "/user/starred/:owner/:name", Op::About(AboutOp::Unstar), &[]),
116 route("GET", "/repos/:owner/:name/stargazers", Op::About(AboutOp::ListStargazers), &[("page", "page")]),
117 // What the default branch says about a repository, kept by commit.
118 route("GET", "/repos/:owner/:name/languages", Op::About(AboutOp::GetLanguages), &[]),
119 route("GET", "/repos/:owner/:name/contributors", Op::About(AboutOp::ListContributors), &[]),
120 route("GET", "/repos/:owner/:name/license", Op::About(AboutOp::GetLicense), &[]),
121 // Releases: `latest` and `tags/…` before an id.
122 route("GET", "/repos/:owner/:name/releases", Op::About(AboutOp::ListReleases), &[]),
123 route("POST", "/repos/:owner/:name/releases", Op::About(AboutOp::CreateRelease), &[]),
124 route("GET", "/repos/:owner/:name/releases/latest", Op::About(AboutOp::GetLatestRelease), &[]),
125 route("GET", "/repos/:owner/:name/releases/tags/:tag", Op::About(AboutOp::GetReleaseByTag), &[]),
126 route("GET", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::GetRelease), &[]),
127 route("PATCH", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::UpdateRelease), &[]),
128 route("DELETE", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::DeleteRelease), &[]),
API: pinned projects over REST and MCP129 // Your pinned projects in a workspace, in your order.
130 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
131 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
132 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
133 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait134 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
135 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
136 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
Merge main (membership, two-factor, GitHub repo roles) into tokens137 // Members and owners: GitHub's organization members, by username.
138 route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]),
139 route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]),
140 route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]),
141 route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]),
142 route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97143 // A workspace's projects: what each is, where it runs, its links.
144 route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
145 route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
146 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.147 // A project's observability: what its software did, and how it is collected.
148 route(
149 "GET",
150 "/workspaces/:workspace/projects/:project/observability/overview",
151 Op::Telemetry(TelemetryOp::GetOverview),
152 &[("from", "from"), ("to", "to"), ("environment", "environment"), ("service", "service"), ("deployment_id", "deployment_id"), ("step", "step")],
153 ),
154 route(
155 "GET",
156 "/workspaces/:workspace/projects/:project/observability/requests",
157 Op::Telemetry(TelemetryOp::ListRequests),
158 &[
159 ("from", "from"),
160 ("to", "to"),
161 ("q", "q"),
162 ("status", "status"),
163 ("errors_only", "errors_only"),
164 ("route", "route"),
165 ("min_duration_ms", "min_duration_ms"),
166 ("environment", "environment"),
167 ("service", "service"),
168 ("deployment_id", "deployment_id"),
169 ("cursor", "cursor"),
170 ("limit", "limit"),
171 ],
172 ),
173 route("GET", "/workspaces/:workspace/projects/:project/observability/requests/:id", Op::Telemetry(TelemetryOp::GetRequest), &[]),
174 route(
175 "GET",
176 "/workspaces/:workspace/projects/:project/observability/logs",
177 Op::Telemetry(TelemetryOp::ListLogs),
178 &[
179 ("from", "from"),
180 ("to", "to"),
181 ("q", "q"),
182 ("level", "level"),
183 ("trace_id", "trace_id"),
184 ("request_id", "request_id"),
185 ("environment", "environment"),
186 ("service", "service"),
187 ("deployment_id", "deployment_id"),
188 ("cursor", "cursor"),
189 ("limit", "limit"),
190 ],
191 ),
192 route(
193 "GET",
194 "/workspaces/:workspace/projects/:project/observability/traces",
195 Op::Telemetry(TelemetryOp::ListTraces),
196 &[
197 ("from", "from"),
198 ("to", "to"),
199 ("q", "q"),
200 ("errors_only", "errors_only"),
201 ("min_duration_ms", "min_duration_ms"),
202 ("environment", "environment"),
203 ("service", "service"),
204 ("cursor", "cursor"),
205 ("limit", "limit"),
206 ],
207 ),
208 route("GET", "/workspaces/:workspace/projects/:project/observability/traces/:trace_id", Op::Telemetry(TelemetryOp::GetTrace), &[]),
209 route(
210 "GET",
211 "/workspaces/:workspace/projects/:project/observability/errors",
212 Op::Telemetry(TelemetryOp::ListErrors),
213 &[("state", "state"), ("environment", "environment"), ("service", "service"), ("from", "from"), ("to", "to")],
214 ),
215 route("PATCH", "/workspaces/:workspace/projects/:project/observability/errors/:fingerprint", Op::Telemetry(TelemetryOp::UpdateError), &[]),
216 route("GET", "/workspaces/:workspace/projects/:project/observability/metrics", Op::Telemetry(TelemetryOp::ListMetrics), &[]),
217 route(
218 "GET",
219 "/workspaces/:workspace/projects/:project/observability/metrics/:metric",
220 Op::Telemetry(TelemetryOp::GetMetric),
221 &[("from", "from"), ("to", "to"), ("aggregate", "aggregate"), ("group_by", "group_by"), ("step", "step"), ("service", "service"), ("environment", "environment")],
222 ),
223 route("GET", "/workspaces/:workspace/projects/:project/observability/settings", Op::Telemetry(TelemetryOp::GetSettings), &[]),
224 route("PATCH", "/workspaces/:workspace/projects/:project/observability/settings", Op::Telemetry(TelemetryOp::UpdateSettings), &[]),
225 route("GET", "/workspaces/:workspace/projects/:project/observability/tokens", Op::Telemetry(TelemetryOp::ListTokens), &[]),
226 route("POST", "/workspaces/:workspace/projects/:project/observability/tokens", Op::Telemetry(TelemetryOp::CreateToken), &[]),
227 route("DELETE", "/workspaces/:workspace/projects/:project/observability/tokens/:id", Op::Telemetry(TelemetryOp::DeleteToken), &[]),
228 route("GET", "/workspaces/:workspace/projects/:project/observability/exports", Op::Telemetry(TelemetryOp::ListExports), &[]),
229 route("POST", "/workspaces/:workspace/projects/:project/observability/exports", Op::Telemetry(TelemetryOp::CreateExport), &[]),
230 route("PATCH", "/workspaces/:workspace/projects/:project/observability/exports/:id", Op::Telemetry(TelemetryOp::UpdateExport), &[]),
231 route("DELETE", "/workspaces/:workspace/projects/:project/observability/exports/:id", Op::Telemetry(TelemetryOp::DeleteExport), &[]),
232 route("GET", "/workspaces/:workspace/projects/:project/observability/usage", Op::Telemetry(TelemetryOp::GetUsage), &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait233 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
234 route(
235 "GET",
236 "/workspaces/:workspace/outside_collaborators",
237 Op::ListOutsideCollaborators,
238 &[],
239 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar240 // Teams: a workspace's groups of members, with roles on repositories.
241 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
242 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
243 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
244 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
245 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
246 route(
247 "GET",
248 "/workspaces/:workspace/teams/:team/members",
249 Op::ListTeamMembers,
250 &[("include_child_teams", "include_child_teams")],
251 ),
252 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
253 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
254 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
255 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
256 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
257 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
258 route(
259 "PUT",
260 "/workspaces/:workspace/teams/:team/review_assignment",
261 Op::SetTeamReviewAssignment,
262 &[],
263 ),
264 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.265 // A workspace's billing: usage, budget and invoices.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit266 route(
267 "GET",
268 "/workspaces/:workspace/usage",
269 Op::GetUsage,
270 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
271 ),
272 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
273 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
274 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
275 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar276 // Code owners: the CODEOWNERS file, checked.
277 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait278 // Security alerts: secrets and vulnerable dependencies.
279 route(
280 "GET",
281 "/repos/:owner/:name/security/alerts",
282 Op::ListSecurityAlerts,
283 &[("state", "state"), ("kind", "kind")],
284 ),
285 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
286 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 bar287 // The security suite: secret scanning, code scanning, vulnerability
288 // alerts and the supply chain, at the common addresses.
289 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
290 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
291 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
292 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
293 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
294 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
295 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
296 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
297 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
298 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
299 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
300 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
301 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
302 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
303 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
304 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
305 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
306 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
307 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
308 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
309 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
310 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
311 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
312 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
313 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
314 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
315 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
316 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
317 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
318 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
319 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
320 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
321 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
322 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
323 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
324 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
325 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
326 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-7nm8r9327 // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
328 route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
329 route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
330 route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
331 route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
332 route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
333 route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
334 route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
335 route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait336 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
337 route(
338 "GET",
339 "/search",
340 Op::Search,
341 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
342 ),
343 route("POST", "/repos", Op::CreateRepo, &[]),
344 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.345 // A branch, tag or commit as one archive: a redirect to a link good for
346 // ten minutes, as GitHub's go to codeload.
347 route("GET", "/repos/:owner/:name/tarball/:ref", Op::Archives(ArchiveOp::DownloadTarball), &[]),
348 route("GET", "/repos/:owner/:name/zipball/:ref", Op::Archives(ArchiveOp::DownloadZipball), &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait349 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
350 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
351 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
352 route(
353 "GET",
354 "/workspaces/:workspace/repos/deleted",
355 Op::ListDeletedRepos,
356 &[],
357 ),
358 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
359 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
360 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
361 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
362 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
363 route(
364 "POST",
365 "/repos/:owner/:name/visibility",
366 Op::SetRepoVisibility,
367 &[],
368 ),
369 route(
370 "POST",
371 "/repos/:owner/:name/branches/:branch/rename",
372 Op::RenameBranch,
373 &[],
374 ),
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.375 route("POST", "/repos/:owner/:name/branches", Op::CreateBranch, &[]),
376 route("DELETE", "/repos/:owner/:name/branches/:branch", Op::DeleteBranch, &[("head", "head")]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait377 route(
378 "GET",
379 "/repos/:owner/:name/settings",
380 Op::GetRepoSettings,
381 &[],
382 ),
383 route(
384 "PATCH",
385 "/repos/:owner/:name/settings",
386 Op::UpdateRepoSettings,
387 &[],
388 ),
389 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit390 // Checks: GitHub's addresses for statuses, check runs and check suites.
391 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
392 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
393 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
394 route(
395 "GET",
396 "/repos/:owner/:name/commits/:ref/check-runs",
397 Op::Checks(ChecksOp::ListCheckRunsForRef),
398 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
399 ),
400 route(
401 "GET",
402 "/repos/:owner/:name/commits/:ref/check-suites",
403 Op::Checks(ChecksOp::ListCheckSuitesForRef),
404 &[("app", "app"), ("check_name", "check_name")],
405 ),
406 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
407 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
408 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
409 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
410 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
411 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
412 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 merge413 // Rulesets: a repository's, a workspace's, the rules of one branch,
414 // and how they judged pushes and merges.
415 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
416 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
417 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
418 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
419 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
420 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
421 route(
422 "GET",
423 "/repos/:owner/:name/rules/evaluations",
424 Op::Rules(RulesOp::ListRuleEvaluations),
425 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
426 ),
427 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
428 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
429 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
430 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
431 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
432 route(
433 "GET",
434 "/workspaces/:workspace/rules/evaluations",
435 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
436 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
437 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97438 // Deployments wherever they run, their statuses, and environments.
439 route(
440 "GET",
441 "/repos/:owner/:name/deployments",
442 Op::Deployments(DeploymentsOp::ListDeployments),
443 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
444 ),
445 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9446 // A g1t Actions job opening a g1t.page build it runs itself.
447 route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97448 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
449 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
450 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
451 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
452 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'453 // Environments' protection rules, and the runs they hold.
454 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
455 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
456 route(
457 "GET",
458 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
459 Op::Protection(ProtectionOp::GetPendingDeployments),
460 &[],
461 ),
462 route(
463 "POST",
464 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
465 Op::Protection(ProtectionOp::ReviewPendingDeployments),
466 &[],
467 ),
468 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
469 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
470 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
471 route(
472 "GET",
473 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
474 Op::Protection(ProtectionOp::GetForkPrApproval),
475 &[],
476 ),
477 route(
478 "PUT",
479 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
480 Op::Protection(ProtectionOp::SetForkPrApproval),
481 &[],
482 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts483 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
484 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'485 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
486 route(
487 "GET",
488 "/workspaces/:workspace/actions/permissions/workflow",
489 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
490 &[],
491 ),
492 route(
493 "PUT",
494 "/workspaces/:workspace/actions/permissions/workflow",
495 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
496 &[],
497 ),
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.498 route(
499 "GET",
500 "/workspaces/:workspace/actions/managed-workflows",
501 Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
502 &[],
503 ),
504 route(
505 "POST",
506 "/workspaces/:workspace/actions/managed-workflows",
507 Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
508 &[],
509 ),
510 route(
511 "PATCH",
512 "/workspaces/:workspace/actions/managed-workflows/:workflow",
513 Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
514 &[],
515 ),
516 route(
517 "DELETE",
518 "/workspaces/:workspace/actions/managed-workflows/:workflow",
519 Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
520 &[],
521 ),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait522 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
523 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
524 route(
525 "GET",
526 "/repos/:owner/:name/memory",
527 Op::Recall,
528 &[("q", "query"), ("limit", "limit")],
529 ),
530 route(
531 "GET",
532 "/repos/:owner/:name/events",
533 Op::ListEvents,
534 &[("before", "before")],
535 ),
536 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar537 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
538 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
539 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
540 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
541 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
542 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
543 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
544 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
545 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
546 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
547 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
548 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
549 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
550 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait551 route(
552 "GET",
553 "/repos/:owner/:name/issues",
554 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar555 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait556 ),
557 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
558 route(
559 "GET",
560 "/repos/:owner/:name/issues/:number",
561 Op::GetIssue,
562 &[],
563 ),
564 route(
565 "PATCH",
566 "/repos/:owner/:name/issues/:number",
567 Op::UpdateIssue,
568 &[],
569 ),
570 route(
571 "POST",
572 "/repos/:owner/:name/issues/:number/close",
573 Op::CloseIssue,
574 &[],
575 ),
576 route(
577 "POST",
578 "/repos/:owner/:name/issues/:number/reopen",
579 Op::ReopenIssue,
580 &[],
581 ),
582 route(
583 "POST",
584 "/repos/:owner/:name/issues/import",
585 Op::ImportIssue,
586 &[],
587 ),
The API and MCP server coordinate work: start_session, update_session (an empty body is a heartbeat), end_session, get_session, list_sessions and record_session_entries at /repos/{owner}/{name}/sessions, as the new session tool's start, update, end, get, list and record; claim_issue, release_claim, hand_off_issue and get_work at /repos/{owner}/{name}/issues/{number}/claim, /release, /handoff and /work, and list_claims at /repos/{owner}/{name}/claims, as the issue tool's claim, release, handoff, work and list_claims; and update_issue takes blocked_by. Sessions need pull_requests scopes and claims and get_work issues scopes, reads are open to anyone who can read the repository, the reference has an example of each, and the OpenAPI copy and the CLI's table of operations are regenerated.588 // Coordinating work: claims on issues, an issue's whole context, and
589 // sessions of people's own harnesses.
590 route("GET", "/repos/:owner/:name/issues/:number/work", Op::Coordination(CoordinationOp::GetWork), &[]),
591 route("POST", "/repos/:owner/:name/issues/:number/claim", Op::Coordination(CoordinationOp::ClaimIssue), &[]),
592 route("POST", "/repos/:owner/:name/issues/:number/release", Op::Coordination(CoordinationOp::ReleaseClaim), &[]),
593 route("POST", "/repos/:owner/:name/issues/:number/handoff", Op::Coordination(CoordinationOp::HandOffIssue), &[]),
594 route(
595 "GET",
596 "/repos/:owner/:name/claims",
597 Op::Coordination(CoordinationOp::ListClaims),
598 &[("state", "state"), ("issue", "issue")],
599 ),
600 route("POST", "/repos/:owner/:name/sessions", Op::Coordination(CoordinationOp::StartSession), &[]),
601 route(
602 "GET",
603 "/repos/:owner/:name/sessions",
604 Op::Coordination(CoordinationOp::ListSessions),
605 &[("state", "state"), ("issue", "issue"), ("pull", "pull")],
606 ),
607 route("GET", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::GetSession), &[("after", "after")]),
608 route("PATCH", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::UpdateSession), &[]),
609 route("POST", "/repos/:owner/:name/sessions/:id/end", Op::Coordination(CoordinationOp::EndSession), &[]),
610 route(
611 "POST",
612 "/repos/:owner/:name/sessions/:id/entries",
613 Op::Coordination(CoordinationOp::RecordSessionEntries),
614 &[],
615 ),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait616 route(
617 "GET",
618 "/repos/:owner/:name/context",
619 Op::GetContext,
620 &[("reference", "reference")],
621 ),
622 route(
623 "GET",
624 "/workspaces/:workspace/integrations",
625 Op::ListIntegrations,
626 &[],
627 ),
628 route(
629 "POST",
630 "/workspaces/:workspace/integrations",
631 Op::ConnectIntegration,
632 &[],
633 ),
634 route(
635 "GET",
636 "/repos/:owner/:name/hooks",
637 Op::ListWebhooks,
638 &[],
639 ),
640 route(
641 "POST",
642 "/repos/:owner/:name/hooks",
643 Op::CreateWebhook,
644 &[],
645 ),
646 route(
647 "PATCH",
648 "/repos/:owner/:name/hooks/:id",
649 Op::UpdateWebhook,
650 &[],
651 ),
652 route(
653 "DELETE",
654 "/repos/:owner/:name/hooks/:id",
655 Op::DeleteWebhook,
656 &[],
657 ),
658 route(
659 "POST",
660 "/repos/:owner/:name/hooks/:id/pings",
661 Op::PingWebhook,
662 &[],
663 ),
664 route(
665 "GET",
666 "/repos/:owner/:name/hooks/:id/deliveries",
667 Op::ListWebhookDeliveries,
668 &[],
669 ),
670 route(
671 "POST",
672 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
673 Op::RedeliverWebhook,
674 &[],
675 ),
676 route(
677 "GET",
678 "/workspaces/:workspace/hooks",
679 Op::ListWebhooks,
680 &[],
681 ),
682 route(
683 "POST",
684 "/workspaces/:workspace/hooks",
685 Op::CreateWebhook,
686 &[],
687 ),
688 route(
689 "PATCH",
690 "/workspaces/:workspace/hooks/:id",
691 Op::UpdateWebhook,
692 &[],
693 ),
694 route(
695 "DELETE",
696 "/workspaces/:workspace/hooks/:id",
697 Op::DeleteWebhook,
698 &[],
699 ),
700 route(
701 "POST",
702 "/workspaces/:workspace/hooks/:id/pings",
703 Op::PingWebhook,
704 &[],
705 ),
706 route(
707 "GET",
708 "/workspaces/:workspace/hooks/:id/deliveries",
709 Op::ListWebhookDeliveries,
710 &[],
711 ),
712 route(
713 "POST",
714 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
715 Op::RedeliverWebhook,
716 &[],
717 ),
718 route(
AI Gateway: OpenAI's format, open models, and your own providers719 "PATCH",
720 "/workspaces/:workspace/integrations/:id",
721 Op::UpdateIntegration,
722 &[],
723 ),
724 route(
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait725 "DELETE",
726 "/workspaces/:workspace/integrations/:id",
727 Op::DisconnectIntegration,
728 &[],
729 ),
730 route(
731 "POST",
732 "/workspaces/:workspace/integrations/:id/test",
733 Op::TestIntegration,
734 &[],
735 ),
736 route(
737 "GET",
738 "/repos/:owner/:name/actions/workflows",
739 Op::ListWorkflows,
740 &[],
741 ),
742 route(
743 "GET",
744 "/repos/:owner/:name/actions/workflows/:workflow/runs",
745 Op::ListWorkflowRuns,
746 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
747 ),
748 route(
749 "POST",
750 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
751 Op::DispatchWorkflow,
752 &[],
753 ),
754 route(
755 "PATCH",
756 "/repos/:owner/:name/actions/workflows/:workflow",
757 Op::UpdateWorkflow,
758 &[],
759 ),
760 route(
761 "PUT",
762 "/repos/:owner/:name/actions/workflows/:workflow/enable",
763 Op::UpdateWorkflow,
764 &[],
765 ),
766 route(
767 "PUT",
768 "/repos/:owner/:name/actions/workflows/:workflow/disable",
769 Op::UpdateWorkflow,
770 &[],
771 ),
772 route(
773 "GET",
774 "/repos/:owner/:name/actions/runs",
775 Op::ListWorkflowRuns,
776 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
777 ),
778 route(
779 "GET",
780 "/repos/:owner/:name/actions/runs/:id",
781 Op::GetWorkflowRun,
782 &[],
783 ),
784 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)785 "GET",
786 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
787 Op::GetWorkflowRun,
788 &[],
789 ),
790 route(
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait791 "POST",
792 "/repos/:owner/:name/actions/runs/:id/cancel",
793 Op::CancelWorkflowRun,
794 &[],
795 ),
796 route(
797 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)798 "/repos/:owner/:name/actions/runs/:id/force-cancel",
799 Op::CancelWorkflowRun,
800 &[],
801 ),
802 route(
803 "POST",
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait804 "/repos/:owner/:name/actions/runs/:id/rerun",
805 Op::RerunWorkflowRun,
806 &[],
807 ),
808 route(
809 "POST",
810 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
811 Op::RerunWorkflowRun,
812 &[],
813 ),
814 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)815 "POST",
816 "/repos/:owner/:name/actions/jobs/:job/rerun",
817 Op::RerunWorkflowRun,
818 &[],
819 ),
820 route(
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait821 "GET",
822 "/repos/:owner/:name/actions/jobs/:job/logs",
823 Op::GetJobLogs,
824 &[("after", "after")],
825 ),
Merge packages: roles, Actions access, source label, soft delete, API826 // Packages, at GitHub's addresses with the workspace in place of the
827 // organization. A name with a slash is one URL-encoded segment.
828 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
829 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
830 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
831 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
832 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
833 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
834 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
835 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
836 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
837 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
838 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
839 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
840 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
841 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
842 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
843 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
844 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 R2845 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
846 // a signed link (lib.rs).
847 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
848 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
849 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
850 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
851 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
852 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
853 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait854 route(
855 "GET",
856 "/repos/:owner/:name/actions/secrets",
857 Op::ListActionsSecrets,
858 &[],
859 ),
860 route(
861 "PUT",
862 "/repos/:owner/:name/actions/secrets/:setting",
863 Op::SetActionsSecret,
864 &[],
865 ),
866 route(
867 "DELETE",
868 "/repos/:owner/:name/actions/secrets/:setting",
869 Op::DeleteActionsSecret,
870 &[],
871 ),
872 route(
873 "GET",
874 "/repos/:owner/:name/actions/variables",
875 Op::ListActionsVariables,
876 &[],
877 ),
878 route(
879 "POST",
880 "/repos/:owner/:name/actions/variables",
881 Op::SetActionsVariable,
882 &[],
883 ),
884 route(
885 "PATCH",
886 "/repos/:owner/:name/actions/variables/:setting",
887 Op::SetActionsVariable,
888 &[],
889 ),
890 route(
891 "DELETE",
892 "/repos/:owner/:name/actions/variables/:setting",
893 Op::DeleteActionsVariable,
894 &[],
895 ),
896 route(
897 "GET",
898 "/workspaces/:workspace/actions/secrets",
899 Op::ListActionsSecrets,
900 &[],
901 ),
902 route(
903 "PUT",
904 "/workspaces/:workspace/actions/secrets/:setting",
905 Op::SetActionsSecret,
906 &[],
907 ),
908 route(
909 "DELETE",
910 "/workspaces/:workspace/actions/secrets/:setting",
911 Op::DeleteActionsSecret,
912 &[],
913 ),
914 route(
915 "GET",
916 "/workspaces/:workspace/actions/variables",
917 Op::ListActionsVariables,
918 &[],
919 ),
920 route(
921 "POST",
922 "/workspaces/:workspace/actions/variables",
923 Op::SetActionsVariable,
924 &[],
925 ),
926 route(
927 "PATCH",
928 "/workspaces/:workspace/actions/variables/:setting",
929 Op::SetActionsVariable,
930 &[],
931 ),
932 route(
933 "DELETE",
934 "/workspaces/:workspace/actions/variables/:setting",
935 Op::DeleteActionsVariable,
936 &[],
937 ),
938 // Self-hosted runners: a repository's own, or a workspace's.
939 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
940 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
941 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
942 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
943 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
944 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
945 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
946 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
947 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
948 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
949 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
950 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
951 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
952 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
953 route(
954 "POST",
955 "/repos/:owner/:name/issues/:number/comments",
956 Op::AddComment,
957 &[],
958 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts959 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
960 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait961 route(
962 "GET",
963 "/repos/:owner/:name/pulls",
964 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar965 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait966 ),
967 route(
968 "POST",
969 "/repos/:owner/:name/pulls",
970 Op::CreatePullRequest,
971 &[],
972 ),
973 route(
974 "GET",
975 "/repos/:owner/:name/pulls/:number",
976 Op::GetPullRequest,
977 &[],
978 ),
979 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar980 "PATCH",
981 "/repos/:owner/:name/pulls/:number",
982 Op::UpdatePullRequest,
983 &[],
984 ),
985 route(
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait986 "GET",
987 "/repos/:owner/:name/pulls/:number/changes",
988 Op::GetPullRequestChanges,
989 &[],
990 ),
991 route(
992 "POST",
993 "/repos/:owner/:name/pulls/:number/reviews",
994 Op::ReviewPullRequest,
995 &[],
996 ),
997 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar998 "POST",
999 "/repos/:owner/:name/pulls/:number/requested_reviewers",
1000 Op::RequestReviewers,
1001 &[],
1002 ),
1003 route(
1004 "DELETE",
1005 "/repos/:owner/:name/pulls/:number/requested_reviewers",
1006 Op::RemoveRequestedReviewers,
1007 &[],
1008 ),
1009 route(
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1010 "GET",
1011 "/repos/:owner/:name/pulls/:number/session",
1012 Op::ReadSession,
1013 &[("after", "after")],
1014 ),
1015 route(
1016 "POST",
1017 "/repos/:owner/:name/pulls/:number/session",
1018 Op::RecordSession,
1019 &[],
1020 ),
1021 route(
1022 "POST",
1023 "/repos/:owner/:name/pulls/:number/ready",
1024 Op::MarkPullRequestReady,
1025 &[],
1026 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1027 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1028 route(
1029 "POST",
1030 "/repos/:owner/:name/pulls/:number/close",
1031 Op::ClosePullRequest,
1032 &[],
1033 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1034 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1035 route(
1036 "POST",
1037 "/repos/:owner/:name/pulls/:number/merge",
1038 Op::MergePullRequest,
1039 &[],
1040 ),
Merge main into Artifacts Phase 21041 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1042 // before an artifact by id, which it would otherwise match.
1043 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")]),
1044 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1045 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1046 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1047 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1048 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1049 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1050 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1051 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1052 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1053 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1054 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1055 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1056 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1057 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1058 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1059 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1060];
1061
1062impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1063 /// Whether the route answers success with `204` and no body, as
1064 /// GitHub's address for the same does. MCP still answers `true`.
1065 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91066 matches!(
1067 self.op,
1068 Op::DeleteComment
1069 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1070 }
1071
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1072 /// The names of the route's path parameters, in order.
1073 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1074 self.path
1075 .split('/')
1076 .filter_map(|segment| segment.strip_prefix(':'))
1077 }
1078
1079 /// The values of the path parameters, if `path` is this route's.
1080 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1081 let mut values = Vec::new();
1082 let mut actual = path.trim_end_matches('/').split('/');
1083 for expected in self.path.split('/') {
1084 let segment = actual.next()?;
1085 match expected.strip_prefix(':') {
1086 Some(name) if !segment.is_empty() => values.push((name, segment)),
1087 Some(_) => return None,
1088 None if expected == segment => {}
1089 None => return None,
1090 }
1091 }
1092 actual.next().is_none().then_some(values)
1093 }
1094}
1095
1096/// A path segment with its `%XX` escapes decoded; as given when that is not
1097/// UTF-8.
1098fn percent_decoded(segment: &str) -> String {
1099 let bytes = segment.as_bytes();
1100 let mut out = Vec::with_capacity(bytes.len());
1101 let mut i = 0;
1102 while i < bytes.len() {
1103 let escaped = (bytes[i] == b'%')
1104 .then(|| segment.get(i + 1..i + 3))
1105 .flatten()
1106 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1107 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1108 match escaped {
1109 Some(byte) => {
1110 out.push(byte);
1111 i += 3;
1112 }
1113 None => {
1114 out.push(bytes[i]);
1115 i += 1;
1116 }
1117 }
1118 }
1119 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1120}
1121
1122/// The route for a request, and the operation input it describes.
1123///
1124/// The input is the JSON body, overlaid with the query parameters the route
1125/// 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 bar1126/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1127/// becomes an integer.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1128pub fn resolve(
1129 method: &str,
1130 path: &str,
1131 query: &[(String, String)],
1132 body: Value,
1133) -> Option<(&'static Route, Value)> {
1134 let (route, params) = ROUTES
1135 .iter()
1136 .filter(|route| route.method == method)
1137 .find_map(|route| Some((route, route.matches(path)?)))?;
1138
1139 let mut input = match body {
1140 Value::Object(fields) => fields,
1141 _ => Map::new(),
1142 };
1143 for (name, key) in route.query {
1144 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1145 input.insert((*key).to_owned(), Value::String(value.clone()));
1146 }
1147 }
1148 let param = |wanted: &str| {
1149 params
1150 .iter()
1151 .find(|(name, _)| *name == wanted)
1152 .map(|(_, value)| *value)
1153 };
1154 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1155 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1156 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971157 // Every other name the path gives, under that name; the ones below that
1158 // need more (a repository, a number, an encoded name) are set after.
1159 for (key, value) in &params {
1160 if !matches!(*key, "owner" | "name") {
1161 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1162 }
1163 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1164 // A repository of a team's workspace, named by itself.
1165 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1166 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1167 }
1168 // A branch name may hold slashes, sent URL-encoded as one segment, and
1169 // a label's name spaces.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1170 if let Some(branch) = param("branch") {
1171 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1172 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971173 // An environment's name may hold slashes and spaces, URL-encoded.
1174 if let Some(environment) = param("environment") {
1175 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1176 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1177 if let Some(label) = param("label") {
1178 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1179 }
1180 if let Some(milestone) = param("milestone") {
1181 // Not a number: zero, which no milestone has.
1182 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1183 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1184 // GitHub says some things with the path alone.
1185 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1186 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1187 }
1188 if route.path.ends_with("/rerun-failed-jobs") {
1189 input.insert("failed_only".to_owned(), Value::Bool(true));
1190 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1191 if route.path.ends_with("/force-cancel") {
1192 input.insert("force".to_owned(), Value::Bool(true));
1193 }
API: notifications over REST and MCP, with notifications scopes1194 // Unsaving and waking a thread are a DELETE of what PUT made.
1195 if route.method == "DELETE" && route.path.ends_with("/saved") {
1196 input.insert("saved".to_owned(), Value::Bool(false));
1197 }
1198 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1199 input.remove("until");
1200 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1201 if let Some(number) = param("number") {
1202 // Not a number: zero, which no issue or pull request has.
1203 input.insert(
1204 "number".to_owned(),
1205 number.parse::<u32>().unwrap_or(0).into(),
1206 );
1207 }
1208 Some((route, Value::Object(input)))
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213 use serde_json::json;
1214
1215 use super::*;
1216
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971217 /// Every name a route's path gives reaches the operation: a name left
1218 /// off the lists above is dropped, and the operation answers that it
1219 /// was not given (the project routes were, until this test).
1220 #[test]
1221 fn every_path_parameter_reaches_the_input() {
1222 for route in ROUTES.iter() {
1223 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1224 if names.is_empty() {
1225 continue;
1226 }
1227 let path: String = route
1228 .path
1229 .split('/')
1230 .map(|part| match part.strip_prefix(':') {
1231 Some("number" | "milestone") => "7".to_owned(),
1232 Some(name) => format!("{name}-x"),
1233 None => part.to_owned(),
1234 })
1235 .collect::<Vec<_>>()
1236 .join("/");
1237 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1238 // A path two routes could take is checked under the first.
1239 if found.path != route.path {
1240 continue;
1241 }
1242 for name in names {
1243 let key = match name {
1244 "owner" | "name" => "repo",
1245 "repo" if names_has_workspace(route.path) => "repo",
1246 other => other,
1247 };
1248 assert!(
1249 input.get(key).is_some_and(|value| !value.is_null()),
1250 "{} {}: :{name} does not reach the input",
1251 route.method,
1252 route.path
1253 );
1254 }
1255 }
1256 }
1257
1258 fn names_has_workspace(path: &str) -> bool {
1259 path.split('/').any(|part| part == ":workspace")
1260 }
1261
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1262 #[test]
1263 fn a_path_resolves_to_its_operation_and_input() {
1264 let (route, input) = resolve(
1265 "POST",
1266 "/repos/flagon-io/hello/pulls/14/merge",
1267 &[],
1268 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1269 )
1270 .unwrap();
1271 assert_eq!(route.op, Op::MergePullRequest);
1272 // What the path names wins over the body.
1273 assert_eq!(
1274 input,
1275 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
1276 );
1277 }
1278
1279 #[test]
1280 fn a_branch_with_slashes_is_one_encoded_segment() {
1281 let (route, input) = resolve(
1282 "POST",
1283 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1284 &[],
1285 json!({ "new_name": "feature/sign-in" }),
1286 )
1287 .unwrap();
1288 assert_eq!(route.op, Op::RenameBranch);
1289 assert_eq!(
1290 input,
1291 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1292 );
1293 assert_eq!(percent_decoded("100%"), "100%");
1294 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1295 }
1296
1297 #[test]
1298 fn a_collaborator_is_named_by_username() {
1299 let (route, input) = resolve(
1300 "PATCH",
1301 "/repos/flagon-io/hello/collaborators/ada",
1302 &[],
1303 json!({ "role": "maintain" }),
1304 )
1305 .unwrap();
1306 assert_eq!(route.op, Op::UpdateCollaborator);
1307 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1308 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1309 assert_eq!(route.op, Op::DeclineRepoInvitation);
1310 assert_eq!(input, json!({ "id": "rin_1" }));
1311 }
1312
1313 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1314 fn teams_are_addressed_by_workspace_and_slug() {
1315 let query = [("q".to_owned(), "back".to_owned())];
1316 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1317 assert_eq!(route.op, Op::ListTeams);
1318 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1319 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1320 assert_eq!(route.op, Op::UpdateTeam);
1321 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1322 let (route, input) =
1323 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1324 assert_eq!(route.op, Op::SetTeamMember);
1325 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1326 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1327 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1328 assert_eq!(route.op, Op::ListTeamMembers);
1329 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1330 // A repository is named by itself, in the team's workspace.
1331 let (route, input) =
1332 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1333 assert_eq!(route.op, Op::SetTeamRepo);
1334 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1335 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1336 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1337 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1338 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1339 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1340 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1341 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1342 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1343 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1344 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1345 assert_eq!(route.op, Op::ListUserTeams);
1346 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1347 }
1348
1349 #[test]
1350 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1351 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1352 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1353 assert_eq!(route.op, Op::RequestReviewers);
1354 assert_eq!(
1355 input,
1356 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1357 );
1358 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1359 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1360 let query = [("ref".to_owned(), "main".to_owned())];
1361 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1362 assert_eq!(route.op, Op::GetCodeownersErrors);
1363 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1364 }
1365
1366 #[test]
API: notifications over REST and MCP, with notifications scopes1367 fn notifications_are_addressed_as_threads_and_by_issue() {
1368 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1369 assert_eq!(route.op, Op::MarkThreadDone);
1370 assert_eq!(input, json!({ "id": "ntf_1" }));
1371 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1372 assert_eq!(route.op, Op::SaveThread);
1373 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1374 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1375 assert_eq!(route.op, Op::SnoozeThread);
1376 assert_eq!(input, json!({ "id": "ntf_1" }));
1377 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1378 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1379 assert_eq!(route.op, Op::ListNotifications);
1380 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1381 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1382 assert_eq!(route.op, Op::SetThreadSubscription);
1383 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1384 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1385 }
1386
1387 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1388 fn labels_and_milestones_are_named_in_the_path() {
1389 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1390 assert_eq!(route.op, Op::UpdateLabel);
1391 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1392 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1393 assert_eq!(route.op, Op::RemoveIssueLabels);
1394 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1395 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1396 assert_eq!(route.op, Op::RemoveIssueLabels);
1397 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1398 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1399 assert_eq!(route.op, Op::AddDefaultLabels);
1400 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1401 assert_eq!(route.op, Op::UpdateMilestone);
1402 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1403 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1404 assert_eq!(route.op, Op::UpdatePullRequest);
1405 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1406 }
1407
1408 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1409 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1410 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1411 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1412 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1413 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1414 // Reopening and closing with a PATCH, as `state`.
1415 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1416 assert_eq!(route.op, Op::UpdatePullRequest);
1417 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1418 let (route, input) =
1419 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1420 assert_eq!(route.op, Op::EditComment);
1421 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1422 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1423 assert_eq!(route.op, Op::DeleteComment);
1424 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1425 // Still an issue's comments, labels and subscription.
1426 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1427 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1428 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1429 }
1430
1431 #[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.1432 fn observability_is_addressed_by_workspace_and_project() {
1433 let query = [("level".to_owned(), "error,warn".to_owned()), ("q".to_owned(), "timeout".to_owned())];
1434 let (route, input) = resolve("GET", "/workspaces/acme/projects/web/observability/logs", &query, Value::Null).unwrap();
1435 assert_eq!(route.op, Op::Telemetry(TelemetryOp::ListLogs));
1436 assert_eq!(input, json!({ "level": "error,warn", "q": "timeout", "workspace": "acme", "project": "web" }));
1437 let (route, input) =
1438 resolve("GET", "/workspaces/acme/projects/web/observability/metrics/http.server.request.duration", &[], Value::Null).unwrap();
1439 assert_eq!(route.op, Op::Telemetry(TelemetryOp::GetMetric));
1440 assert_eq!(input["metric"], json!("http.server.request.duration"));
1441 let (route, input) =
1442 resolve("PATCH", "/workspaces/acme/projects/web/observability/errors/f00d", &[], json!({ "state": "resolved" })).unwrap();
1443 assert_eq!(route.op, Op::Telemetry(TelemetryOp::UpdateError));
1444 assert_eq!(input, json!({ "state": "resolved", "workspace": "acme", "project": "web", "fingerprint": "f00d" }));
1445 }
1446
1447 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1448 fn billing_is_addressed_by_workspace() {
1449 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1450 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1451 assert_eq!(route.op, Op::GetUsage);
1452 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1453 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1454 assert_eq!(route.op, Op::SetBudget);
1455 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1456 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1457 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1458 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1459 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
1460 }
1461
1462 #[test]
Merge checks: statuses and check runs on every commit1463 fn checks_are_at_githubs_addresses() {
1464 let sha = "a".repeat(40);
1465 let body = json!({ "state": "success", "context": "ci/build" });
1466 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1467 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1468 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1469 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1470 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1471 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1472 let query = [("check_name".to_owned(), "lint".to_owned())];
1473 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1474 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1475 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1476 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1477 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1478 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1479 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1480 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1481 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1482 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1483 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1484 }
1485
1486 #[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.1487 fn archives_are_at_githubs_addresses_and_redirect() {
1488 let sha = "4807077b296e6edbf410d55e72749d3e1170c291";
1489 let (route, input) = resolve("GET", &format!("/repos/acme/web/tarball/{sha}"), &[], Value::Null).unwrap();
1490 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadTarball));
1491 assert_eq!(input, json!({ "ref": sha, "repo": "acme/web" }));
1492 // A branch with slashes, URL-encoded as octokit sends it.
1493 let (route, input) = resolve("GET", "/repos/acme/web/zipball/release%2F1.x", &[], Value::Null).unwrap();
1494 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadZipball));
1495 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1496 // Both answer with a redirect to their link, as the artifact download does.
1497 assert!(route.op.redirects());
1498 assert!(Op::Archives(ArchiveOp::DownloadTarball).redirects());
1499 assert!(Op::Artifacts(ArtifactsOp::DownloadArtifact).redirects());
1500 assert!(!Op::GetRepo.redirects());
1501 // Anyone may ask for a public repository's.
1502 assert!(!route.op.needs_user());
1503 }
1504
1505 #[test]
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1506 fn query_parameters_are_renamed() {
1507 let query = [
1508 ("q".to_owned(), "parser".to_owned()),
1509 ("x".to_owned(), "y".to_owned()),
1510 ];
1511 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1512 assert_eq!(route.op, Op::ListRepos);
1513 assert_eq!(input, json!({ "query": "parser" }));
1514 }
1515
1516 #[test]
1517 fn method_and_shape_must_match() {
1518 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1519 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1520 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1521 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1522 }
1523
1524 #[test]
1525 fn every_parameter_and_query_name_is_an_input() {
1526 for route in ROUTES {
1527 let properties = route.op.properties();
1528 for (_, key) in route.query {
1529 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1530 }
1531 for name in route.params() {
1532 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1533 || properties.contains_key(name);
1534 assert!(covered, "{}: {name}", route.path);
1535 }
1536 }
1537 }
1538
1539 #[test]
1540 fn every_operation_has_a_route() {
1541 for op in Op::ALL {
1542 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1543 }
1544 }
1545}

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