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