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

Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1//! 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 remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r97use crate::customize::CustomizeOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca8use crate::deploy_keys::DeployKeysOp;
Merge branch 'mirroring' into artifacts-mode9use crate::mirrors::MirrorsOp;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9710use crate::deployments::DeploymentsOp;
Merge packages: roles, Actions access, source label, soft delete, API11use crate::packages::PackagesOp;
Merge main into Artifacts Phase 212use crate::folios::FoliosOp;
Merge branch 'worktree-agent-a3abfcce648e87dca'13use crate::protection::ProtectionOp;
API and MCP for a workspace's personal access token rules, members' tokens and approvals14use crate::token_policy::TokenOp;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put15use crate::operations::Op;
Merge checks: statuses and check runs on every commit16use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge17use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar18use crate::security::SecurityOp;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put19
20pub struct Route {
21 pub method: &'static str,
22 /// Segments starting with `:` are parameters.
23 pub path: &'static str,
24 pub op: Op,
25 /// Query parameters the route reads, as `(name in the URL, input name)`.
26 pub query: &'static [(&'static str, &'static str)],
27}
28
29const fn route(
30 method: &'static str,
31 path: &'static str,
32 op: Op,
33 query: &'static [(&'static str, &'static str)],
34) -> Route {
35 Route {
36 method,
37 path,
38 op,
39 query,
40 }
41}
42
43pub const ROUTES: &[Route] = &[
44 route("GET", "/user", Op::Whoami, &[]),
45 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
Merge branch 'worktree-agent-ad7c6d88d93adc817'46 route("GET", "/workspaces/:workspace", Op::GetWorkspace, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
48 route("GET", "/user/emails", Op::ListEmails, &[]),
49 route("POST", "/user/emails", Op::AddEmail, &[]),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)50 route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
52 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
53 route("GET", "/user/invites", Op::ListInvites, &[]),
54 route("POST", "/user/invites", Op::CreateInvite, &[]),
55 route("DELETE", "/user/invites/:id", Op::RevokeInvite, &[]),
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)56 // Invitations to workspaces waiting for your answer.
57 route("GET", "/user/invitations", Op::ListInvitations, &[]),
58 route("POST", "/user/invitations/:id/accept", Op::AcceptInvitation, &[]),
59 route("POST", "/user/invitations/:id/decline", Op::DeclineInvitation, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look60 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals61 // A workspace's rules for personal access tokens, and its members' tokens.
62 route("GET", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::GetTokenPolicy), &[]),
63 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 managers64 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 approvals65 route("POST", "/workspaces/:workspace/personal-access-tokens/:id", Op::Tokens(TokenOp::RevokeMemberToken), &[]),
66 route("GET", "/workspaces/:workspace/personal-access-token-requests", Op::Tokens(TokenOp::ListTokenRequests), &[]),
67 route("POST", "/workspaces/:workspace/personal-access-token-requests/:id", Op::Tokens(TokenOp::ReviewTokenRequest), &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look68 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
69 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
70 // Who has access. GitHub's addresses, but for adding someone, which
71 // takes an email address as well as a username.
72 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
73 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
74 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
75 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
76 route(
77 "GET",
78 "/repos/:owner/:name/collaborators/:username/permission",
79 Op::GetCollaboratorPermission,
80 &[],
81 ),
82 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
83 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
84 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca85 // Deploy keys, at GitHub's addresses.
86 route("GET", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::ListDeployKeys), &[]),
87 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
88 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
89 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
Merge branch 'mirroring' into artifacts-mode90 // Mirroring: a repository's remotes, takeovers and hand-backs.
91 route("GET", "/repos/:owner/:name/mirror", Op::Mirrors(MirrorsOp::GetMirror), &[]),
92 route("GET", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::GetHandBackPlan), &[]),
93 route("POST", "/repos/:owner/:name/mirror/take-over", Op::Mirrors(MirrorsOp::TakeOver), &[]),
94 route("POST", "/repos/:owner/:name/mirror/ci", Op::Mirrors(MirrorsOp::SetCiFailover), &[]),
95 route("POST", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::HandBack), &[]),
96 route("POST", "/repos/:owner/:name/mirror/move-to-g1t", Op::Mirrors(MirrorsOp::MoveToG1t), &[]),
97 route("POST", "/repos/:owner/:name/mirror/sync", Op::Mirrors(MirrorsOp::SyncMirror), &[]),
98 route("POST", "/repos/:owner/:name/mirror/remotes", Op::Mirrors(MirrorsOp::AddRemote), &[]),
99 route("PATCH", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::UpdateRemote), &[]),
100 route("DELETE", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::RemoveRemote), &[]),
API: notifications over REST and MCP, with notifications scopes101 // Your notifications: threads, marking them, and what you subscribe
102 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
103 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")]),
104 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
105 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
106 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
107 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
108 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
109 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
110 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
111 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
112 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
113 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
114 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
115 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")]),
116 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
117 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
118 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
119 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
120 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
121 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
122 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
123 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97124 // Stars: yours, and who starred a repository.
125 route("GET", "/user/starred", Op::About(AboutOp::ListStarred), &[]),
126 route("GET", "/user/starred/:owner/:name", Op::About(AboutOp::CheckStarred), &[]),
127 route("PUT", "/user/starred/:owner/:name", Op::About(AboutOp::Star), &[]),
128 route("DELETE", "/user/starred/:owner/:name", Op::About(AboutOp::Unstar), &[]),
129 route("GET", "/repos/:owner/:name/stargazers", Op::About(AboutOp::ListStargazers), &[("page", "page")]),
130 // What the default branch says about a repository, kept by commit.
131 route("GET", "/repos/:owner/:name/languages", Op::About(AboutOp::GetLanguages), &[]),
132 route("GET", "/repos/:owner/:name/contributors", Op::About(AboutOp::ListContributors), &[]),
133 route("GET", "/repos/:owner/:name/license", Op::About(AboutOp::GetLicense), &[]),
134 // Releases: `latest` and `tags/…` before an id.
135 route("GET", "/repos/:owner/:name/releases", Op::About(AboutOp::ListReleases), &[]),
136 route("POST", "/repos/:owner/:name/releases", Op::About(AboutOp::CreateRelease), &[]),
137 route("GET", "/repos/:owner/:name/releases/latest", Op::About(AboutOp::GetLatestRelease), &[]),
138 route("GET", "/repos/:owner/:name/releases/tags/:tag", Op::About(AboutOp::GetReleaseByTag), &[]),
139 route("GET", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::GetRelease), &[]),
140 route("PATCH", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::UpdateRelease), &[]),
141 route("DELETE", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::DeleteRelease), &[]),
API: pinned projects over REST and MCP142 // Your pinned projects in a workspace, in your order.
143 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
144 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
145 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
146 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look147 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
148 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily149 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
Merge main (membership, two-factor, GitHub repo roles) into tokens150 // Members and owners: GitHub's organization members, by username.
151 route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]),
152 route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]),
153 route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]),
154 route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]),
155 route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97156 // A workspace's projects: what each is, where it runs, its links.
157 route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
158 route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
159 route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily160 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 route(
162 "GET",
163 "/workspaces/:workspace/outside_collaborators",
164 Op::ListOutsideCollaborators,
165 &[],
166 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar167 // Teams: a workspace's groups of members, with roles on repositories.
168 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
169 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
170 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
171 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
172 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
173 route(
174 "GET",
175 "/workspaces/:workspace/teams/:team/members",
176 Op::ListTeamMembers,
177 &[("include_child_teams", "include_child_teams")],
178 ),
179 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
180 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
181 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
182 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
183 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
184 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
185 route(
186 "PUT",
187 "/workspaces/:workspace/teams/:team/review_assignment",
188 Op::SetTeamReviewAssignment,
189 &[],
190 ),
191 route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit192 // A workspace's billing: usage, budget, AI credit and invoices.
193 route(
194 "GET",
195 "/workspaces/:workspace/usage",
196 Op::GetUsage,
197 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
198 ),
199 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
200 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
201 route("GET", "/workspaces/:workspace/ai_credit", Op::GetAiCredit, &[]),
202 route("POST", "/workspaces/:workspace/ai_credit/checkout", Op::BuyAiCredit, &[]),
203 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
204 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens205 // The AI Gateway's log of a workspace's requests.
206 route("GET", "/workspaces/:workspace/gateway/requests", Op::ListGatewayRequests, &[("limit", "limit"), ("before", "before")]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar207 // Code owners: the CODEOWNERS file, checked.
208 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily209 // Security alerts: secrets and vulnerable dependencies.
210 route(
211 "GET",
212 "/repos/:owner/:name/security/alerts",
213 Op::ListSecurityAlerts,
214 &[("state", "state"), ("kind", "kind")],
215 ),
216 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
217 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 bar218 // The security suite: secret scanning, code scanning, vulnerability
219 // alerts and the supply chain, at the common addresses.
220 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
221 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
222 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
223 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
224 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
225 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
226 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
227 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
228 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
229 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
230 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
231 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
232 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
233 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
234 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
235 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
236 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
237 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
238 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
239 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
240 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
241 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
242 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
243 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
244 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
245 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
246 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
247 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
248 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
249 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
250 route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]),
251 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
252 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
253 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
254 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
255 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
256 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
257 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
258 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-7nm8r9259 // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
260 route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
261 route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
262 route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
263 route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
264 route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
265 route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
266 route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
267 route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put268 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
269 route(
270 "GET",
271 "/search",
272 Op::Search,
273 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
274 ),
275 route("POST", "/repos", Op::CreateRepo, &[]),
276 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
277 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look278 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
279 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
280 route(
281 "GET",
282 "/workspaces/:workspace/repos/deleted",
283 Op::ListDeletedRepos,
284 &[],
285 ),
286 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
287 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
288 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
289 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
290 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
291 route(
292 "POST",
293 "/repos/:owner/:name/visibility",
294 Op::SetRepoVisibility,
295 &[],
296 ),
297 route(
298 "POST",
299 "/repos/:owner/:name/branches/:branch/rename",
300 Op::RenameBranch,
301 &[],
302 ),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put303 route(
304 "GET",
305 "/repos/:owner/:name/settings",
306 Op::GetRepoSettings,
307 &[],
308 ),
309 route(
310 "PATCH",
311 "/repos/:owner/:name/settings",
312 Op::UpdateRepoSettings,
313 &[],
314 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents315 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit316 // Checks: GitHub's addresses for statuses, check runs and check suites.
317 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
318 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
319 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
320 route(
321 "GET",
322 "/repos/:owner/:name/commits/:ref/check-runs",
323 Op::Checks(ChecksOp::ListCheckRunsForRef),
324 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
325 ),
326 route(
327 "GET",
328 "/repos/:owner/:name/commits/:ref/check-suites",
329 Op::Checks(ChecksOp::ListCheckSuitesForRef),
330 &[("app", "app"), ("check_name", "check_name")],
331 ),
332 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
333 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
334 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
335 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
336 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
337 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
338 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 merge339 // Rulesets: a repository's, a workspace's, the rules of one branch,
340 // and how they judged pushes and merges.
341 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
342 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
343 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
344 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
345 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
346 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
347 route(
348 "GET",
349 "/repos/:owner/:name/rules/evaluations",
350 Op::Rules(RulesOp::ListRuleEvaluations),
351 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
352 ),
353 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
354 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
355 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
356 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
357 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
358 route(
359 "GET",
360 "/workspaces/:workspace/rules/evaluations",
361 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
362 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
363 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97364 // Deployments wherever they run, their statuses, and environments.
365 route(
366 "GET",
367 "/repos/:owner/:name/deployments",
368 Op::Deployments(DeploymentsOp::ListDeployments),
369 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
370 ),
371 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9372 // A g1t Actions job opening a g1t.page build it runs itself.
373 route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97374 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
375 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
376 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
377 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
378 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'379 // Environments' protection rules, and the runs they hold.
380 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
381 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
382 route(
383 "GET",
384 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
385 Op::Protection(ProtectionOp::GetPendingDeployments),
386 &[],
387 ),
388 route(
389 "POST",
390 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
391 Op::Protection(ProtectionOp::ReviewPendingDeployments),
392 &[],
393 ),
394 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
395 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
396 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
397 route(
398 "GET",
399 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
400 Op::Protection(ProtectionOp::GetForkPrApproval),
401 &[],
402 ),
403 route(
404 "PUT",
405 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
406 Op::Protection(ProtectionOp::SetForkPrApproval),
407 &[],
408 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts409 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
410 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'411 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
412 route(
413 "GET",
414 "/workspaces/:workspace/actions/permissions/workflow",
415 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
416 &[],
417 ),
418 route(
419 "PUT",
420 "/workspaces/:workspace/actions/permissions/workflow",
421 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
422 &[],
423 ),
A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.424 route(
425 "GET",
426 "/workspaces/:workspace/actions/managed-workflows",
427 Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
428 &[],
429 ),
430 route(
431 "POST",
432 "/workspaces/:workspace/actions/managed-workflows",
433 Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
434 &[],
435 ),
436 route(
437 "PATCH",
438 "/workspaces/:workspace/actions/managed-workflows/:workflow",
439 Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
440 &[],
441 ),
442 route(
443 "DELETE",
444 "/workspaces/:workspace/actions/managed-workflows/:workflow",
445 Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
446 &[],
447 ),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put448 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
449 route(
450 "POST",
451 "/repos/:owner/:name/pulls/:number/messages",
452 Op::MessageAgent,
453 &[],
454 ),
455 route(
456 "POST",
457 "/repos/:owner/:name/pulls/:number/messages/take",
458 Op::TakeMessages,
459 &[],
460 ),
461 route(
462 "POST",
463 "/repos/:owner/:name/messages/:id/answer",
464 Op::AnswerMessage,
465 &[],
466 ),
467 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
468 route(
469 "GET",
470 "/repos/:owner/:name/memory",
471 Op::Recall,
472 &[("q", "query"), ("limit", "limit")],
473 ),
474 route(
475 "GET",
476 "/repos/:owner/:name/events",
477 Op::ListEvents,
478 &[("before", "before")],
479 ),
480 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar481 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
482 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
483 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
484 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
485 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
486 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
487 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
488 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
489 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
490 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
491 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
492 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
493 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
494 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put495 route(
496 "GET",
497 "/repos/:owner/:name/issues",
498 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar499 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put500 ),
501 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
502 route(
503 "GET",
504 "/repos/:owner/:name/issues/:number",
505 Op::GetIssue,
506 &[],
507 ),
508 route(
509 "PATCH",
510 "/repos/:owner/:name/issues/:number",
511 Op::UpdateIssue,
512 &[],
513 ),
514 route(
515 "POST",
516 "/repos/:owner/:name/issues/:number/close",
517 Op::CloseIssue,
518 &[],
519 ),
520 route(
521 "POST",
522 "/repos/:owner/:name/issues/:number/reopen",
523 Op::ReopenIssue,
524 &[],
525 ),
526 route(
527 "POST",
528 "/repos/:owner/:name/issues/:number/assign",
529 Op::AssignIssue,
530 &[],
531 ),
532 route(
533 "POST",
534 "/repos/:owner/:name/issues/import",
535 Op::ImportIssue,
536 &[],
537 ),
538 route(
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step539 "POST",
540 "/repos/:owner/:name/issues/delegate",
541 Op::Delegate,
542 &[],
543 ),
544 route(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put545 "GET",
546 "/repos/:owner/:name/context",
547 Op::GetContext,
548 &[("reference", "reference")],
549 ),
550 route(
551 "GET",
552 "/workspaces/:workspace/context/search",
553 Op::SearchContext,
554 &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
555 ),
556 route(
557 "GET",
558 "/workspaces/:workspace/context/:kind/:id",
559 Op::GetEntity,
560 &[],
561 ),
562 route(
563 "GET",
564 "/workspaces/:workspace/integrations",
565 Op::ListIntegrations,
566 &[],
567 ),
568 route(
569 "POST",
570 "/workspaces/:workspace/integrations",
571 Op::ConnectIntegration,
572 &[],
573 ),
574 route(
575 "GET",
576 "/repos/:owner/:name/hooks",
577 Op::ListWebhooks,
578 &[],
579 ),
580 route(
581 "POST",
582 "/repos/:owner/:name/hooks",
583 Op::CreateWebhook,
584 &[],
585 ),
586 route(
587 "PATCH",
588 "/repos/:owner/:name/hooks/:id",
589 Op::UpdateWebhook,
590 &[],
591 ),
592 route(
593 "DELETE",
594 "/repos/:owner/:name/hooks/:id",
595 Op::DeleteWebhook,
596 &[],
597 ),
598 route(
599 "POST",
600 "/repos/:owner/:name/hooks/:id/pings",
601 Op::PingWebhook,
602 &[],
603 ),
604 route(
605 "GET",
606 "/repos/:owner/:name/hooks/:id/deliveries",
607 Op::ListWebhookDeliveries,
608 &[],
609 ),
610 route(
611 "POST",
612 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
613 Op::RedeliverWebhook,
614 &[],
615 ),
616 route(
617 "GET",
618 "/workspaces/:workspace/hooks",
619 Op::ListWebhooks,
620 &[],
621 ),
622 route(
623 "POST",
624 "/workspaces/:workspace/hooks",
625 Op::CreateWebhook,
626 &[],
627 ),
628 route(
629 "PATCH",
630 "/workspaces/:workspace/hooks/:id",
631 Op::UpdateWebhook,
632 &[],
633 ),
634 route(
635 "DELETE",
636 "/workspaces/:workspace/hooks/:id",
637 Op::DeleteWebhook,
638 &[],
639 ),
640 route(
641 "POST",
642 "/workspaces/:workspace/hooks/:id/pings",
643 Op::PingWebhook,
644 &[],
645 ),
646 route(
647 "GET",
648 "/workspaces/:workspace/hooks/:id/deliveries",
649 Op::ListWebhookDeliveries,
650 &[],
651 ),
652 route(
653 "POST",
654 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
655 Op::RedeliverWebhook,
656 &[],
657 ),
658 route(
659 "GET",
660 "/workspaces/:workspace/model-routes",
661 Op::GetModelRoutes,
662 &[],
663 ),
664 route(
665 "PUT",
666 "/workspaces/:workspace/model-routes",
667 Op::SetModelRoutes,
668 &[],
669 ),
670 route(
AI Gateway: OpenAI's format, open models, and your own providers671 "PATCH",
672 "/workspaces/:workspace/integrations/:id",
673 Op::UpdateIntegration,
674 &[],
675 ),
676 route(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put677 "DELETE",
678 "/workspaces/:workspace/integrations/:id",
679 Op::DisconnectIntegration,
680 &[],
681 ),
682 route(
683 "POST",
684 "/workspaces/:workspace/integrations/:id/test",
685 Op::TestIntegration,
686 &[],
687 ),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9688 // Customize: marketplaces, Discover, plugins and connectors. A plugin's
689 // ref is `name@marketplace`, URL-encoded (`@` as `%40`).
690 route("GET", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), &[]),
691 route("POST", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::AddMarketplace), &[]),
692 route("DELETE", "/workspaces/:workspace/customize/marketplaces/:marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), &[]),
693 route("POST", "/workspaces/:workspace/customize/marketplaces/:marketplace/refresh", Op::Customize(CustomizeOp::RefreshMarketplace), &[]),
694 route("GET", "/workspaces/:workspace/customize/discover", Op::Customize(CustomizeOp::Discover), &[]),
695 route("GET", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::ListPlugins), &[]),
696 route("POST", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::InstallPlugin), &[]),
697 route("GET", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::GetPlugin), &[]),
698 route("PATCH", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::SetPluginEnabled), &[]),
699 route("DELETE", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::UninstallPlugin), &[]),
700 route("POST", "/workspaces/:workspace/customize/plugins/:ref/update", Op::Customize(CustomizeOp::UpdatePlugin), &[]),
701 route("GET", "/workspaces/:workspace/customize/agents", Op::Customize(CustomizeOp::ListPluginAgents), &[]),
702 route("GET", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::ListConnectors), &[]),
703 route("POST", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::AddConnector), &[]),
704 route("GET", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::GetConnector), &[]),
705 route("PATCH", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::SetConnectorKey), &[]),
706 route("DELETE", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::RemoveConnector), &[]),
707 route("POST", "/workspaces/:workspace/customize/connectors/:id/refresh", Op::Customize(CustomizeOp::RefreshConnector), &[]),
708 route("POST", "/workspaces/:workspace/customize/connectors/:id/grants", Op::Customize(CustomizeOp::GrantConnector), &[]),
709 route("DELETE", "/workspaces/:workspace/customize/connectors/:id/grants/:grant", Op::Customize(CustomizeOp::RevokeConnector), &[]),
710 route("GET", "/workspaces/:workspace/customize/bundle", Op::Customize(CustomizeOp::Bundle), &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put711 route(
712 "GET",
713 "/repos/:owner/:name/actions/workflows",
714 Op::ListWorkflows,
715 &[],
716 ),
717 route(
718 "GET",
719 "/repos/:owner/:name/actions/workflows/:workflow/runs",
720 Op::ListWorkflowRuns,
721 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
722 ),
723 route(
724 "POST",
725 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
726 Op::DispatchWorkflow,
727 &[],
728 ),
729 route(
730 "PATCH",
731 "/repos/:owner/:name/actions/workflows/:workflow",
732 Op::UpdateWorkflow,
733 &[],
734 ),
735 route(
736 "PUT",
737 "/repos/:owner/:name/actions/workflows/:workflow/enable",
738 Op::UpdateWorkflow,
739 &[],
740 ),
741 route(
742 "PUT",
743 "/repos/:owner/:name/actions/workflows/:workflow/disable",
744 Op::UpdateWorkflow,
745 &[],
746 ),
747 route(
748 "GET",
749 "/repos/:owner/:name/actions/runs",
750 Op::ListWorkflowRuns,
751 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
752 ),
753 route(
754 "GET",
755 "/repos/:owner/:name/actions/runs/:id",
756 Op::GetWorkflowRun,
757 &[],
758 ),
759 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)760 "GET",
761 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
762 Op::GetWorkflowRun,
763 &[],
764 ),
765 route(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put766 "POST",
767 "/repos/:owner/:name/actions/runs/:id/cancel",
768 Op::CancelWorkflowRun,
769 &[],
770 ),
771 route(
772 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)773 "/repos/:owner/:name/actions/runs/:id/force-cancel",
774 Op::CancelWorkflowRun,
775 &[],
776 ),
777 route(
778 "POST",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put779 "/repos/:owner/:name/actions/runs/:id/rerun",
780 Op::RerunWorkflowRun,
781 &[],
782 ),
783 route(
784 "POST",
785 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
786 Op::RerunWorkflowRun,
787 &[],
788 ),
789 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)790 "POST",
791 "/repos/:owner/:name/actions/jobs/:job/rerun",
792 Op::RerunWorkflowRun,
793 &[],
794 ),
795 route(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put796 "GET",
797 "/repos/:owner/:name/actions/jobs/:job/logs",
798 Op::GetJobLogs,
799 &[("after", "after")],
800 ),
Merge packages: roles, Actions access, source label, soft delete, API801 // Packages, at GitHub's addresses with the workspace in place of the
802 // organization. A name with a slash is one URL-encoded segment.
803 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
804 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
805 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
806 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
807 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
808 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
809 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
810 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
811 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
812 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
813 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
814 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
815 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
816 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
817 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
818 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
819 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 R2820 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
821 // a signed link (lib.rs).
822 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
823 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
824 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
825 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
826 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
827 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
828 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put829 route(
830 "GET",
831 "/repos/:owner/:name/actions/secrets",
832 Op::ListActionsSecrets,
833 &[],
834 ),
835 route(
836 "PUT",
837 "/repos/:owner/:name/actions/secrets/:setting",
838 Op::SetActionsSecret,
839 &[],
840 ),
841 route(
842 "DELETE",
843 "/repos/:owner/:name/actions/secrets/:setting",
844 Op::DeleteActionsSecret,
845 &[],
846 ),
847 route(
848 "GET",
849 "/repos/:owner/:name/actions/variables",
850 Op::ListActionsVariables,
851 &[],
852 ),
853 route(
854 "POST",
855 "/repos/:owner/:name/actions/variables",
856 Op::SetActionsVariable,
857 &[],
858 ),
859 route(
860 "PATCH",
861 "/repos/:owner/:name/actions/variables/:setting",
862 Op::SetActionsVariable,
863 &[],
864 ),
865 route(
866 "DELETE",
867 "/repos/:owner/:name/actions/variables/:setting",
868 Op::DeleteActionsVariable,
869 &[],
870 ),
871 route(
872 "GET",
873 "/workspaces/:workspace/actions/secrets",
874 Op::ListActionsSecrets,
875 &[],
876 ),
877 route(
878 "PUT",
879 "/workspaces/:workspace/actions/secrets/:setting",
880 Op::SetActionsSecret,
881 &[],
882 ),
883 route(
884 "DELETE",
885 "/workspaces/:workspace/actions/secrets/:setting",
886 Op::DeleteActionsSecret,
887 &[],
888 ),
889 route(
890 "GET",
891 "/workspaces/:workspace/actions/variables",
892 Op::ListActionsVariables,
893 &[],
894 ),
895 route(
896 "POST",
897 "/workspaces/:workspace/actions/variables",
898 Op::SetActionsVariable,
899 &[],
900 ),
901 route(
902 "PATCH",
903 "/workspaces/:workspace/actions/variables/:setting",
904 Op::SetActionsVariable,
905 &[],
906 ),
907 route(
908 "DELETE",
909 "/workspaces/:workspace/actions/variables/:setting",
910 Op::DeleteActionsVariable,
911 &[],
912 ),
Fast pages, required checks on the branch, self-hosted runners, honest incidents913 // Self-hosted runners: a repository's own, or a workspace's.
914 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
915 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
916 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
917 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
918 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
919 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
920 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
921 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
922 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
923 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
924 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
925 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
926 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
927 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.928 // A workspace agent's own computer.
929 route("GET", "/workspaces/:workspace/agents/:agent/computer", Op::GetAgentComputer, &[]),
930 route("POST", "/workspaces/:workspace/agents/:agent/computer/wake", Op::WakeAgentComputer, &[]),
931 route("POST", "/workspaces/:workspace/agents/:agent/computer/sleep", Op::SleepAgentComputer, &[]),
932 route("POST", "/workspaces/:workspace/agents/:agent/computer/reset", Op::ResetAgentComputer, &[]),
933 route("GET", "/workspaces/:workspace/agents/:agent/computer/commands", Op::ListAgentComputerCommands, &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put934 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
935 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
936 route(
937 "POST",
938 "/repos/:owner/:name/plans/:plan/apply",
939 Op::ApplyPlan,
940 &[],
941 ),
942 route(
943 "POST",
944 "/repos/:owner/:name/issues/:number/comments",
945 Op::AddComment,
946 &[],
947 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts948 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
949 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put950 route(
951 "GET",
952 "/repos/:owner/:name/pulls",
953 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar954 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put955 ),
956 route(
957 "POST",
958 "/repos/:owner/:name/pulls",
959 Op::CreatePullRequest,
960 &[],
961 ),
962 route(
963 "GET",
964 "/repos/:owner/:name/pulls/:number",
965 Op::GetPullRequest,
966 &[],
967 ),
968 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar969 "PATCH",
970 "/repos/:owner/:name/pulls/:number",
971 Op::UpdatePullRequest,
972 &[],
973 ),
974 route(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put975 "GET",
976 "/repos/:owner/:name/pulls/:number/changes",
977 Op::GetPullRequestChanges,
978 &[],
979 ),
980 route(
981 "POST",
982 "/repos/:owner/:name/pulls/:number/reviews",
983 Op::ReviewPullRequest,
984 &[],
985 ),
986 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar987 "POST",
988 "/repos/:owner/:name/pulls/:number/requested_reviewers",
989 Op::RequestReviewers,
990 &[],
991 ),
992 route(
993 "DELETE",
994 "/repos/:owner/:name/pulls/:number/requested_reviewers",
995 Op::RemoveRequestedReviewers,
996 &[],
997 ),
998 route(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put999 "GET",
1000 "/repos/:owner/:name/pulls/:number/session",
1001 Op::ReadSession,
1002 &[("after", "after")],
1003 ),
1004 route(
1005 "POST",
1006 "/repos/:owner/:name/pulls/:number/session",
1007 Op::RecordSession,
1008 &[],
1009 ),
1010 route(
1011 "POST",
1012 "/repos/:owner/:name/pulls/:number/ready",
1013 Op::MarkPullRequestReady,
1014 &[],
1015 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1016 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1017 route(
1018 "POST",
1019 "/repos/:owner/:name/pulls/:number/close",
1020 Op::ClosePullRequest,
1021 &[],
1022 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1023 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1024 route(
1025 "POST",
1026 "/repos/:owner/:name/pulls/:number/merge",
1027 Op::MergePullRequest,
1028 &[],
1029 ),
Merge main into Artifacts Phase 21030 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1031 // before an artifact by id, which it would otherwise match.
1032 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")]),
1033 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1034 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1035 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1036 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1037 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1038 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1039 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1040 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1041 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1042 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1043 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1044 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1045 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1046 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1047 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1048 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1049];
1050
1051impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1052 /// Whether the route answers success with `204` and no body, as
1053 /// GitHub's address for the same does. MCP still answers `true`.
1054 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91055 matches!(
1056 self.op,
1057 Op::DeleteComment
1058 | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector)
1059 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1060 }
1061
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1062 /// The names of the route's path parameters, in order.
1063 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1064 self.path
1065 .split('/')
1066 .filter_map(|segment| segment.strip_prefix(':'))
1067 }
1068
1069 /// The values of the path parameters, if `path` is this route's.
1070 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1071 let mut values = Vec::new();
1072 let mut actual = path.trim_end_matches('/').split('/');
1073 for expected in self.path.split('/') {
1074 let segment = actual.next()?;
1075 match expected.strip_prefix(':') {
1076 Some(name) if !segment.is_empty() => values.push((name, segment)),
1077 Some(_) => return None,
1078 None if expected == segment => {}
1079 None => return None,
1080 }
1081 }
1082 actual.next().is_none().then_some(values)
1083 }
1084}
1085
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1086/// A path segment with its `%XX` escapes decoded; as given when that is not
1087/// UTF-8.
1088fn percent_decoded(segment: &str) -> String {
1089 let bytes = segment.as_bytes();
1090 let mut out = Vec::with_capacity(bytes.len());
1091 let mut i = 0;
1092 while i < bytes.len() {
1093 let escaped = (bytes[i] == b'%')
1094 .then(|| segment.get(i + 1..i + 3))
1095 .flatten()
1096 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1097 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1098 match escaped {
1099 Some(byte) => {
1100 out.push(byte);
1101 i += 3;
1102 }
1103 None => {
1104 out.push(bytes[i]);
1105 i += 1;
1106 }
1107 }
1108 }
1109 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1110}
1111
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1112/// The route for a request, and the operation input it describes.
1113///
1114/// The input is the JSON body, overlaid with the query parameters the route
1115/// 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 bar1116/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1117/// becomes an integer.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1118pub fn resolve(
1119 method: &str,
1120 path: &str,
1121 query: &[(String, String)],
1122 body: Value,
1123) -> Option<(&'static Route, Value)> {
1124 let (route, params) = ROUTES
1125 .iter()
1126 .filter(|route| route.method == method)
1127 .find_map(|route| Some((route, route.matches(path)?)))?;
1128
1129 let mut input = match body {
1130 Value::Object(fields) => fields,
1131 _ => Map::new(),
1132 };
1133 for (name, key) in route.query {
1134 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1135 input.insert((*key).to_owned(), Value::String(value.clone()));
1136 }
1137 }
1138 let param = |wanted: &str| {
1139 params
1140 .iter()
1141 .find(|(name, _)| *name == wanted)
1142 .map(|(_, value)| *value)
1143 };
1144 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1145 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1146 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971147 // Every other name the path gives, under that name; the ones below that
1148 // need more (a repository, a number, an encoded name) are set after.
1149 for (key, value) in &params {
1150 if !matches!(*key, "owner" | "name") {
1151 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1152 }
1153 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1154 // A repository of a team's workspace, named by itself.
1155 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1156 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1157 }
1158 // A branch name may hold slashes, sent URL-encoded as one segment, and
1159 // a label's name spaces.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1160 if let Some(branch) = param("branch") {
1161 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1162 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971163 // An environment's name may hold slashes and spaces, URL-encoded.
1164 if let Some(environment) = param("environment") {
1165 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1166 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1167 if let Some(label) = param("label") {
1168 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1169 }
1170 if let Some(milestone) = param("milestone") {
1171 // Not a number: zero, which no milestone has.
1172 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1173 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1174 // GitHub says some things with the path alone.
1175 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1176 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1177 }
1178 if route.path.ends_with("/rerun-failed-jobs") {
1179 input.insert("failed_only".to_owned(), Value::Bool(true));
1180 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1181 if route.path.ends_with("/force-cancel") {
1182 input.insert("force".to_owned(), Value::Bool(true));
1183 }
API: notifications over REST and MCP, with notifications scopes1184 // Unsaving and waking a thread are a DELETE of what PUT made.
1185 if route.method == "DELETE" && route.path.ends_with("/saved") {
1186 input.insert("saved".to_owned(), Value::Bool(false));
1187 }
1188 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1189 input.remove("until");
1190 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1191 if let Some(number) = param("number") {
1192 // Not a number: zero, which no issue or pull request has.
1193 input.insert(
1194 "number".to_owned(),
1195 number.parse::<u32>().unwrap_or(0).into(),
1196 );
1197 }
1198 Some((route, Value::Object(input)))
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use serde_json::json;
1204
1205 use super::*;
1206
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971207 /// Every name a route's path gives reaches the operation: a name left
1208 /// off the lists above is dropped, and the operation answers that it
1209 /// was not given (the project routes were, until this test).
1210 #[test]
1211 fn every_path_parameter_reaches_the_input() {
1212 for route in ROUTES.iter() {
1213 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1214 if names.is_empty() {
1215 continue;
1216 }
1217 let path: String = route
1218 .path
1219 .split('/')
1220 .map(|part| match part.strip_prefix(':') {
1221 Some("number" | "milestone") => "7".to_owned(),
1222 Some(name) => format!("{name}-x"),
1223 None => part.to_owned(),
1224 })
1225 .collect::<Vec<_>>()
1226 .join("/");
1227 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1228 // A path two routes could take is checked under the first.
1229 if found.path != route.path {
1230 continue;
1231 }
1232 for name in names {
1233 let key = match name {
1234 "owner" | "name" => "repo",
1235 "repo" if names_has_workspace(route.path) => "repo",
1236 other => other,
1237 };
1238 assert!(
1239 input.get(key).is_some_and(|value| !value.is_null()),
1240 "{} {}: :{name} does not reach the input",
1241 route.method,
1242 route.path
1243 );
1244 }
1245 }
1246 }
1247
1248 fn names_has_workspace(path: &str) -> bool {
1249 path.split('/').any(|part| part == ":workspace")
1250 }
1251
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1252 #[test]
1253 fn a_path_resolves_to_its_operation_and_input() {
1254 let (route, input) = resolve(
1255 "POST",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1256 "/repos/flagon-io/hello/pulls/14/merge",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1257 &[],
1258 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1259 )
1260 .unwrap();
1261 assert_eq!(route.op, Op::MergePullRequest);
1262 // What the path names wins over the body.
1263 assert_eq!(
1264 input,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1265 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1266 );
1267 }
1268
1269 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1270 fn a_branch_with_slashes_is_one_encoded_segment() {
1271 let (route, input) = resolve(
1272 "POST",
1273 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1274 &[],
1275 json!({ "new_name": "feature/sign-in" }),
1276 )
1277 .unwrap();
1278 assert_eq!(route.op, Op::RenameBranch);
1279 assert_eq!(
1280 input,
1281 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1282 );
1283 assert_eq!(percent_decoded("100%"), "100%");
1284 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1285 }
1286
1287 #[test]
1288 fn a_collaborator_is_named_by_username() {
1289 let (route, input) = resolve(
1290 "PATCH",
1291 "/repos/flagon-io/hello/collaborators/ada",
1292 &[],
1293 json!({ "role": "maintain" }),
1294 )
1295 .unwrap();
1296 assert_eq!(route.op, Op::UpdateCollaborator);
1297 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1298 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1299 assert_eq!(route.op, Op::DeclineRepoInvitation);
1300 assert_eq!(input, json!({ "id": "rin_1" }));
1301 }
1302
1303 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1304 fn teams_are_addressed_by_workspace_and_slug() {
1305 let query = [("q".to_owned(), "back".to_owned())];
1306 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1307 assert_eq!(route.op, Op::ListTeams);
1308 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1309 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1310 assert_eq!(route.op, Op::UpdateTeam);
1311 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1312 let (route, input) =
1313 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1314 assert_eq!(route.op, Op::SetTeamMember);
1315 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1316 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1317 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1318 assert_eq!(route.op, Op::ListTeamMembers);
1319 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1320 // A repository is named by itself, in the team's workspace.
1321 let (route, input) =
1322 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1323 assert_eq!(route.op, Op::SetTeamRepo);
1324 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1325 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1326 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1327 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1328 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1329 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1330 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1331 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1332 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1333 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1334 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1335 assert_eq!(route.op, Op::ListUserTeams);
1336 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1337 }
1338
1339 #[test]
1340 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1341 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1342 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1343 assert_eq!(route.op, Op::RequestReviewers);
1344 assert_eq!(
1345 input,
1346 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1347 );
1348 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1349 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1350 let query = [("ref".to_owned(), "main".to_owned())];
1351 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1352 assert_eq!(route.op, Op::GetCodeownersErrors);
1353 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1354 }
1355
1356 #[test]
API: notifications over REST and MCP, with notifications scopes1357 fn notifications_are_addressed_as_threads_and_by_issue() {
1358 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1359 assert_eq!(route.op, Op::MarkThreadDone);
1360 assert_eq!(input, json!({ "id": "ntf_1" }));
1361 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1362 assert_eq!(route.op, Op::SaveThread);
1363 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1364 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1365 assert_eq!(route.op, Op::SnoozeThread);
1366 assert_eq!(input, json!({ "id": "ntf_1" }));
1367 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1368 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1369 assert_eq!(route.op, Op::ListNotifications);
1370 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1371 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1372 assert_eq!(route.op, Op::SetThreadSubscription);
1373 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1374 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1375 }
1376
1377 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1378 fn labels_and_milestones_are_named_in_the_path() {
1379 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1380 assert_eq!(route.op, Op::UpdateLabel);
1381 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1382 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1383 assert_eq!(route.op, Op::RemoveIssueLabels);
1384 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1385 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1386 assert_eq!(route.op, Op::RemoveIssueLabels);
1387 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1388 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1389 assert_eq!(route.op, Op::AddDefaultLabels);
1390 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1391 assert_eq!(route.op, Op::UpdateMilestone);
1392 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1393 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1394 assert_eq!(route.op, Op::UpdatePullRequest);
1395 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1396 }
1397
1398 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1399 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1400 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1401 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1402 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1403 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1404 // Reopening and closing with a PATCH, as `state`.
1405 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1406 assert_eq!(route.op, Op::UpdatePullRequest);
1407 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1408 let (route, input) =
1409 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1410 assert_eq!(route.op, Op::EditComment);
1411 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1412 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1413 assert_eq!(route.op, Op::DeleteComment);
1414 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1415 // Still an issue's comments, labels and subscription.
1416 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1417 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1418 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1419 }
1420
1421 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1422 fn billing_is_addressed_by_workspace() {
1423 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1424 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1425 assert_eq!(route.op, Op::GetUsage);
1426 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1427 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1428 assert_eq!(route.op, Op::SetBudget);
1429 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1430 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1431 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1432 assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit);
1433 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
1434 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1435 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1436 assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1437 }
1438
1439 #[test]
Merge checks: statuses and check runs on every commit1440 fn checks_are_at_githubs_addresses() {
1441 let sha = "a".repeat(40);
1442 let body = json!({ "state": "success", "context": "ci/build" });
1443 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1444 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1445 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1446 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1447 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1448 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1449 let query = [("check_name".to_owned(), "lint".to_owned())];
1450 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1451 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1452 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1453 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1454 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1455 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1456 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1457 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1458 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1459 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1460 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1461 }
1462
1463 #[test]
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1464 fn query_parameters_are_renamed() {
1465 let query = [
1466 ("q".to_owned(), "parser".to_owned()),
1467 ("x".to_owned(), "y".to_owned()),
1468 ];
1469 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1470 assert_eq!(route.op, Op::ListRepos);
1471 assert_eq!(input, json!({ "query": "parser" }));
1472 }
1473
1474 #[test]
1475 fn method_and_shape_must_match() {
1476 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1477 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1478 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1479 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1480 }
1481
1482 #[test]
1483 fn every_parameter_and_query_name_is_an_input() {
1484 for route in ROUTES {
1485 let properties = route.op.properties();
1486 for (_, key) in route.query {
1487 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1488 }
1489 for name in route.params() {
1490 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1491 || properties.contains_key(name);
1492 assert!(covered, "{}: {name}", route.path);
1493 }
1494 }
1495 }
1496
1497 #[test]
1498 fn every_operation_has_a_route() {
1499 for op in Op::ALL {
1500 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1501 }
1502 }
1503}

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