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.
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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; |
| Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2 | 6 | use crate::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; |
| The API answers for artifacts: the artifact MCP tool and /workspaces/{workspace}/artifacts list, search, read and edit a workspace's docs as the person sees them, make, move, trash, restore and purge them, show their versions and who can open them, and share them, each action offered only to tokens whose artifacts scope allows it, through a DOCS binding to the docs service, with the OpenAPI document and its examples. | 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; |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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; |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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")]), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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 | ), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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 | ), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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 | ), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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 | ), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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, &[]), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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")], |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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( | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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", | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 528 | "/workspaces/:workspace/integrations", |
| 529 | Op::ListIntegrations, | |
| 530 | &[], | |
| 531 | ), | |
| 532 | route( | |
| 533 | "POST", | |
| 534 | "/workspaces/:workspace/integrations", | |
| 535 | Op::ConnectIntegration, | |
| 536 | &[], | |
| 537 | ), | |
| 538 | route( | |
| Models per workspace: several providers, routed by kind of work | 539 | "GET", |
| Webhooks: every event, to your own addresses, signed and retried | 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", | |
| Models per workspace: several providers, routed by kind of work | 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( | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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 | ), | |
| Automations: rules in .g1t/automations that act when something happens | 652 | route( |
| 653 | "GET", | |
| GitHub Actions on g1t, part two: running workflows | 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( | |
| GitHub Actions on g1t, part two: running workflows | 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", | |
| GitHub Actions on g1t, part two: running workflows | 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( | |
| GitHub Actions on g1t, part two: running workflows | 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), &[]), | |
| GitHub Actions on g1t, part two: running workflows | 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, &[]), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 869 | route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]), |
| 870 | route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]), | |
| 871 | route( | |
| 872 | "POST", | |
| 873 | "/repos/:owner/:name/plans/:plan/apply", | |
| 874 | Op::ApplyPlan, | |
| 875 | &[], | |
| 876 | ), | |
| 877 | route( | |
| 878 | "POST", | |
| 879 | "/repos/:owner/:name/issues/:number/comments", | |
| 880 | Op::AddComment, | |
| 881 | &[], | |
| 882 | ), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 883 | route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]), |
| 884 | route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 885 | route( |
| 886 | "GET", | |
| 887 | "/repos/:owner/:name/pulls", | |
| 888 | Op::ListPullRequests, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 889 | &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")], |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 890 | ), |
| 891 | route( | |
| 892 | "POST", | |
| 893 | "/repos/:owner/:name/pulls", | |
| 894 | Op::CreatePullRequest, | |
| 895 | &[], | |
| 896 | ), | |
| 897 | route( | |
| 898 | "GET", | |
| 899 | "/repos/:owner/:name/pulls/:number", | |
| 900 | Op::GetPullRequest, | |
| 901 | &[], | |
| 902 | ), | |
| 903 | route( | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 904 | "PATCH", |
| 905 | "/repos/:owner/:name/pulls/:number", | |
| 906 | Op::UpdatePullRequest, | |
| 907 | &[], | |
| 908 | ), | |
| 909 | route( | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 910 | "GET", |
| 911 | "/repos/:owner/:name/pulls/:number/changes", | |
| 912 | Op::GetPullRequestChanges, | |
| 913 | &[], | |
| 914 | ), | |
| 915 | route( | |
| 916 | "POST", | |
| 917 | "/repos/:owner/:name/pulls/:number/reviews", | |
| 918 | Op::ReviewPullRequest, | |
| 919 | &[], | |
| 920 | ), | |
| 921 | route( | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 922 | "POST", |
| 923 | "/repos/:owner/:name/pulls/:number/requested_reviewers", | |
| 924 | Op::RequestReviewers, | |
| 925 | &[], | |
| 926 | ), | |
| 927 | route( | |
| 928 | "DELETE", | |
| 929 | "/repos/:owner/:name/pulls/:number/requested_reviewers", | |
| 930 | Op::RemoveRequestedReviewers, | |
| 931 | &[], | |
| 932 | ), | |
| 933 | route( | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 934 | "GET", |
| 935 | "/repos/:owner/:name/pulls/:number/session", | |
| 936 | Op::ReadSession, | |
| 937 | &[("after", "after")], | |
| 938 | ), | |
| 939 | route( | |
| 940 | "POST", | |
| 941 | "/repos/:owner/:name/pulls/:number/session", | |
| 942 | Op::RecordSession, | |
| 943 | &[], | |
| 944 | ), | |
| 945 | route( | |
| 946 | "POST", | |
| 947 | "/repos/:owner/:name/pulls/:number/ready", | |
| 948 | Op::MarkPullRequestReady, | |
| 949 | &[], | |
| 950 | ), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 951 | route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]), |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 952 | route( |
| 953 | "POST", | |
| 954 | "/repos/:owner/:name/pulls/:number/close", | |
| 955 | Op::ClosePullRequest, | |
| 956 | &[], | |
| 957 | ), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 958 | route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]), |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 959 | route( |
| 960 | "POST", | |
| 961 | "/repos/:owner/:name/pulls/:number/merge", | |
| 962 | Op::MergePullRequest, | |
| 963 | &[], | |
| 964 | ), | |
| The API answers for artifacts: the artifact MCP tool and /workspaces/{workspace}/artifacts list, search, read and edit a workspace's docs as the person sees them, make, move, trash, restore and purge them, show their versions and who can open them, and share them, each action offered only to tokens whose artifacts scope allows it, through a DOCS binding to the docs service, with the OpenAPI document and its examples. | 965 | // Artifacts mode's docs, slides, designs and dashboards. Search comes |
| 966 | // before an artifact by id, which it would otherwise match. | |
| 967 | 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")]), | |
| 968 | route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]), | |
| 969 | route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]), | |
| 970 | route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]), | |
| 971 | route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]), | |
| 972 | route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]), | |
| 973 | route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]), | |
| 974 | route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]), | |
| 975 | route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]), | |
| 976 | route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]), | |
| 977 | route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]), | |
| 978 | route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]), | |
| 979 | route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]), | |
| 980 | route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]), | |
| 981 | route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]), | |
| 982 | route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]), | |
| 983 | route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]), | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 984 | ]; |
| 985 | ||
| 986 | impl Route { | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 987 | /// Whether the route answers success with `204` and no body, as |
| 988 | /// GitHub's address for the same does. MCP still answers `true`. | |
| 989 | pub fn no_content(&self) -> bool { | |
| 990 | self.op == Op::DeleteComment | |
| 991 | } | |
| 992 | ||
| Integrations: your own model provider, alerts that open issues, tickets agents read | 993 | /// The names of the route's path parameters, in order. |
| 994 | pub fn params(&self) -> impl Iterator<Item = &'static str> { | |
| 995 | self.path | |
| 996 | .split('/') | |
| 997 | .filter_map(|segment| segment.strip_prefix(':')) | |
| 998 | } | |
| 999 | ||
| 1000 | /// The values of the path parameters, if `path` is this route's. | |
| 1001 | fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> { | |
| 1002 | let mut values = Vec::new(); | |
| 1003 | let mut actual = path.trim_end_matches('/').split('/'); | |
| 1004 | for expected in self.path.split('/') { | |
| 1005 | let segment = actual.next()?; | |
| 1006 | match expected.strip_prefix(':') { | |
| 1007 | Some(name) if !segment.is_empty() => values.push((name, segment)), | |
| 1008 | Some(_) => return None, | |
| 1009 | None if expected == segment => {} | |
| 1010 | None => return None, | |
| 1011 | } | |
| 1012 | } | |
| 1013 | actual.next().is_none().then_some(values) | |
| 1014 | } | |
| 1015 | } | |
| 1016 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1017 | /// A path segment with its `%XX` escapes decoded; as given when that is not |
| 1018 | /// UTF-8. | |
| 1019 | fn percent_decoded(segment: &str) -> String { | |
| 1020 | let bytes = segment.as_bytes(); | |
| 1021 | let mut out = Vec::with_capacity(bytes.len()); | |
| 1022 | let mut i = 0; | |
| 1023 | while i < bytes.len() { | |
| 1024 | let escaped = (bytes[i] == b'%') | |
| 1025 | .then(|| segment.get(i + 1..i + 3)) | |
| 1026 | .flatten() | |
| 1027 | .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit())) | |
| 1028 | .and_then(|hex| u8::from_str_radix(hex, 16).ok()); | |
| 1029 | match escaped { | |
| 1030 | Some(byte) => { | |
| 1031 | out.push(byte); | |
| 1032 | i += 3; | |
| 1033 | } | |
| 1034 | None => { | |
| 1035 | out.push(bytes[i]); | |
| 1036 | i += 1; | |
| 1037 | } | |
| 1038 | } | |
| 1039 | } | |
| 1040 | String::from_utf8(out).unwrap_or_else(|_| segment.to_owned()) | |
| 1041 | } | |
| 1042 | ||
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1043 | /// The route for a request, and the operation input it describes. |
| 1044 | /// | |
| 1045 | /// The input is the JSON body, overlaid with the query parameters the route | |
| 1046 | /// 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 | 1047 | /// `repo`, as does a team's `repo` with its `workspace`, and `number` |
| 1048 | /// becomes an integer. | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1049 | pub fn resolve( |
| 1050 | method: &str, | |
| 1051 | path: &str, | |
| 1052 | query: &[(String, String)], | |
| 1053 | body: Value, | |
| 1054 | ) -> Option<(&'static Route, Value)> { | |
| 1055 | let (route, params) = ROUTES | |
| 1056 | .iter() | |
| 1057 | .filter(|route| route.method == method) | |
| 1058 | .find_map(|route| Some((route, route.matches(path)?)))?; | |
| 1059 | ||
| 1060 | let mut input = match body { | |
| 1061 | Value::Object(fields) => fields, | |
| 1062 | _ => Map::new(), | |
| 1063 | }; | |
| 1064 | for (name, key) in route.query { | |
| 1065 | if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) { | |
| 1066 | input.insert((*key).to_owned(), Value::String(value.clone())); | |
| 1067 | } | |
| 1068 | } | |
| 1069 | let param = |wanted: &str| { | |
| 1070 | params | |
| 1071 | .iter() | |
| 1072 | .find(|(name, _)| *name == wanted) | |
| 1073 | .map(|(_, value)| *value) | |
| 1074 | }; | |
| 1075 | if let (Some(owner), Some(name)) = (param("owner"), param("name")) { | |
| 1076 | input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}"))); | |
| 1077 | } | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1078 | // Every other name the path gives, under that name; the ones below that |
| 1079 | // need more (a repository, a number, an encoded name) are set after. | |
| 1080 | for (key, value) in ¶ms { | |
| 1081 | if !matches!(*key, "owner" | "name") { | |
| 1082 | input.insert((*key).to_owned(), Value::String(percent_decoded(value))); | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1083 | } |
| 1084 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1085 | // A repository of a team's workspace, named by itself. |
| 1086 | if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) { | |
| 1087 | input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}"))); | |
| 1088 | } | |
| 1089 | // A branch name may hold slashes, sent URL-encoded as one segment, and | |
| 1090 | // a label's name spaces. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1091 | if let Some(branch) = param("branch") { |
| 1092 | input.insert("branch".to_owned(), Value::String(percent_decoded(branch))); | |
| 1093 | } | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1094 | // An environment's name may hold slashes and spaces, URL-encoded. |
| 1095 | if let Some(environment) = param("environment") { | |
| 1096 | input.insert("environment".to_owned(), Value::String(percent_decoded(environment))); | |
| 1097 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1098 | if let Some(label) = param("label") { |
| 1099 | input.insert("label".to_owned(), Value::String(percent_decoded(label))); | |
| 1100 | } | |
| 1101 | if let Some(milestone) = param("milestone") { | |
| 1102 | // Not a number: zero, which no milestone has. | |
| 1103 | input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into()); | |
| 1104 | } | |
| GitHub Actions on g1t, part two: running workflows | 1105 | // GitHub says some things with the path alone. |
| 1106 | if route.path.ends_with("/enable") || route.path.ends_with("/disable") { | |
| 1107 | input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable"))); | |
| 1108 | } | |
| 1109 | if route.path.ends_with("/rerun-failed-jobs") { | |
| 1110 | input.insert("failed_only".to_owned(), Value::Bool(true)); | |
| 1111 | } | |
| Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009) | 1112 | if route.path.ends_with("/force-cancel") { |
| 1113 | input.insert("force".to_owned(), Value::Bool(true)); | |
| 1114 | } | |
| API: notifications over REST and MCP, with notifications scopes | 1115 | // Unsaving and waking a thread are a DELETE of what PUT made. |
| 1116 | if route.method == "DELETE" && route.path.ends_with("/saved") { | |
| 1117 | input.insert("saved".to_owned(), Value::Bool(false)); | |
| 1118 | } | |
| 1119 | if route.method == "DELETE" && route.path.ends_with("/snooze") { | |
| 1120 | input.remove("until"); | |
| 1121 | } | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1122 | if let Some(number) = param("number") { |
| 1123 | // Not a number: zero, which no issue or pull request has. | |
| 1124 | input.insert( | |
| 1125 | "number".to_owned(), | |
| 1126 | number.parse::<u32>().unwrap_or(0).into(), | |
| 1127 | ); | |
| 1128 | } | |
| 1129 | Some((route, Value::Object(input))) | |
| 1130 | } | |
| 1131 | ||
| 1132 | #[cfg(test)] | |
| 1133 | mod tests { | |
| 1134 | use serde_json::json; | |
| 1135 | ||
| 1136 | use super::*; | |
| 1137 | ||
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1138 | /// Every name a route's path gives reaches the operation: a name left |
| 1139 | /// off the lists above is dropped, and the operation answers that it | |
| 1140 | /// was not given (the project routes were, until this test). | |
| 1141 | #[test] | |
| 1142 | fn every_path_parameter_reaches_the_input() { | |
| 1143 | for route in ROUTES.iter() { | |
| 1144 | let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect(); | |
| 1145 | if names.is_empty() { | |
| 1146 | continue; | |
| 1147 | } | |
| 1148 | let path: String = route | |
| 1149 | .path | |
| 1150 | .split('/') | |
| 1151 | .map(|part| match part.strip_prefix(':') { | |
| 1152 | Some("number" | "milestone") => "7".to_owned(), | |
| 1153 | Some(name) => format!("{name}-x"), | |
| 1154 | None => part.to_owned(), | |
| 1155 | }) | |
| 1156 | .collect::<Vec<_>>() | |
| 1157 | .join("/"); | |
| 1158 | let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap(); | |
| 1159 | // A path two routes could take is checked under the first. | |
| 1160 | if found.path != route.path { | |
| 1161 | continue; | |
| 1162 | } | |
| 1163 | for name in names { | |
| 1164 | let key = match name { | |
| 1165 | "owner" | "name" => "repo", | |
| 1166 | "repo" if names_has_workspace(route.path) => "repo", | |
| 1167 | other => other, | |
| 1168 | }; | |
| 1169 | assert!( | |
| 1170 | input.get(key).is_some_and(|value| !value.is_null()), | |
| 1171 | "{} {}: :{name} does not reach the input", | |
| 1172 | route.method, | |
| 1173 | route.path | |
| 1174 | ); | |
| 1175 | } | |
| 1176 | } | |
| 1177 | } | |
| 1178 | ||
| 1179 | fn names_has_workspace(path: &str) -> bool { | |
| 1180 | path.split('/').any(|part| part == ":workspace") | |
| 1181 | } | |
| 1182 | ||
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1183 | #[test] |
| 1184 | fn a_path_resolves_to_its_operation_and_input() { | |
| 1185 | let (route, input) = resolve( | |
| 1186 | "POST", | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1187 | "/repos/flagon-io/hello/pulls/14/merge", |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1188 | &[], |
| 1189 | json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }), | |
| 1190 | ) | |
| 1191 | .unwrap(); | |
| 1192 | assert_eq!(route.op, Op::MergePullRequest); | |
| 1193 | // What the path names wins over the body. | |
| 1194 | assert_eq!( | |
| 1195 | input, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1196 | json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" }) |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1197 | ); |
| 1198 | } | |
| 1199 | ||
| 1200 | #[test] | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1201 | fn a_branch_with_slashes_is_one_encoded_segment() { |
| 1202 | let (route, input) = resolve( | |
| 1203 | "POST", | |
| 1204 | "/repos/flagon-io/hello/branches/feature%2Flogin/rename", | |
| 1205 | &[], | |
| 1206 | json!({ "new_name": "feature/sign-in" }), | |
| 1207 | ) | |
| 1208 | .unwrap(); | |
| 1209 | assert_eq!(route.op, Op::RenameBranch); | |
| 1210 | assert_eq!( | |
| 1211 | input, | |
| 1212 | json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" }) | |
| 1213 | ); | |
| 1214 | assert_eq!(percent_decoded("100%"), "100%"); | |
| 1215 | assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz"); | |
| 1216 | } | |
| 1217 | ||
| 1218 | #[test] | |
| 1219 | fn a_collaborator_is_named_by_username() { | |
| 1220 | let (route, input) = resolve( | |
| 1221 | "PATCH", | |
| 1222 | "/repos/flagon-io/hello/collaborators/ada", | |
| 1223 | &[], | |
| 1224 | json!({ "role": "maintain" }), | |
| 1225 | ) | |
| 1226 | .unwrap(); | |
| 1227 | assert_eq!(route.op, Op::UpdateCollaborator); | |
| 1228 | assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" })); | |
| 1229 | let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap(); | |
| 1230 | assert_eq!(route.op, Op::DeclineRepoInvitation); | |
| 1231 | assert_eq!(input, json!({ "id": "rin_1" })); | |
| 1232 | } | |
| 1233 | ||
| 1234 | #[test] | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1235 | fn teams_are_addressed_by_workspace_and_slug() { |
| 1236 | let query = [("q".to_owned(), "back".to_owned())]; | |
| 1237 | let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap(); | |
| 1238 | assert_eq!(route.op, Op::ListTeams); | |
| 1239 | assert_eq!(input, json!({ "query": "back", "workspace": "acme" })); | |
| 1240 | let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap(); | |
| 1241 | assert_eq!(route.op, Op::UpdateTeam); | |
| 1242 | assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" })); | |
| 1243 | let (route, input) = | |
| 1244 | resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap(); | |
| 1245 | assert_eq!(route.op, Op::SetTeamMember); | |
| 1246 | assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" })); | |
| 1247 | let query = [("include_child_teams".to_owned(), "true".to_owned())]; | |
| 1248 | let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap(); | |
| 1249 | assert_eq!(route.op, Op::ListTeamMembers); | |
| 1250 | assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" })); | |
| 1251 | // A repository is named by itself, in the team's workspace. | |
| 1252 | let (route, input) = | |
| 1253 | resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap(); | |
| 1254 | assert_eq!(route.op, Op::SetTeamRepo); | |
| 1255 | assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" })); | |
| 1256 | let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op; | |
| 1257 | assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo); | |
| 1258 | assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams); | |
| 1259 | assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos); | |
| 1260 | assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment); | |
| 1261 | assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam); | |
| 1262 | assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam); | |
| 1263 | assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam); | |
| 1264 | assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember); | |
| 1265 | let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap(); | |
| 1266 | assert_eq!(route.op, Op::ListUserTeams); | |
| 1267 | assert_eq!(input, json!({ "workspace": "acme", "username": "ana" })); | |
| 1268 | } | |
| 1269 | ||
| 1270 | #[test] | |
| 1271 | fn reviewers_are_requested_and_code_owners_checked_on_a_repository() { | |
| 1272 | let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] }); | |
| 1273 | let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap(); | |
| 1274 | assert_eq!(route.op, Op::RequestReviewers); | |
| 1275 | assert_eq!( | |
| 1276 | input, | |
| 1277 | json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 }) | |
| 1278 | ); | |
| 1279 | let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap(); | |
| 1280 | assert_eq!(route.op, Op::RemoveRequestedReviewers); | |
| 1281 | let query = [("ref".to_owned(), "main".to_owned())]; | |
| 1282 | let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap(); | |
| 1283 | assert_eq!(route.op, Op::GetCodeownersErrors); | |
| 1284 | assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" })); | |
| 1285 | } | |
| 1286 | ||
| 1287 | #[test] | |
| API: notifications over REST and MCP, with notifications scopes | 1288 | fn notifications_are_addressed_as_threads_and_by_issue() { |
| 1289 | let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap(); | |
| 1290 | assert_eq!(route.op, Op::MarkThreadDone); | |
| 1291 | assert_eq!(input, json!({ "id": "ntf_1" })); | |
| 1292 | let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap(); | |
| 1293 | assert_eq!(route.op, Op::SaveThread); | |
| 1294 | assert_eq!(input, json!({ "id": "ntf_1", "saved": false })); | |
| 1295 | let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap(); | |
| 1296 | assert_eq!(route.op, Op::SnoozeThread); | |
| 1297 | assert_eq!(input, json!({ "id": "ntf_1" })); | |
| 1298 | let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())]; | |
| 1299 | let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap(); | |
| 1300 | assert_eq!(route.op, Op::ListNotifications); | |
| 1301 | assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" })); | |
| 1302 | let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap(); | |
| 1303 | assert_eq!(route.op, Op::SetThreadSubscription); | |
| 1304 | assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" })); | |
| 1305 | assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos); | |
| 1306 | } | |
| 1307 | ||
| 1308 | #[test] | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1309 | fn labels_and_milestones_are_named_in_the_path() { |
| 1310 | let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap(); | |
| 1311 | assert_eq!(route.op, Op::UpdateLabel); | |
| 1312 | assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" })); | |
| 1313 | let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap(); | |
| 1314 | assert_eq!(route.op, Op::RemoveIssueLabels); | |
| 1315 | assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" })); | |
| 1316 | let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap(); | |
| 1317 | assert_eq!(route.op, Op::RemoveIssueLabels); | |
| 1318 | assert_eq!(input, json!({ "number": 7, "repo": "acme/web" })); | |
| 1319 | let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap(); | |
| 1320 | assert_eq!(route.op, Op::AddDefaultLabels); | |
| 1321 | let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap(); | |
| 1322 | assert_eq!(route.op, Op::UpdateMilestone); | |
| 1323 | assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" })); | |
| 1324 | let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap(); | |
| 1325 | assert_eq!(route.op, Op::UpdatePullRequest); | |
| 1326 | assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" })); | |
| 1327 | } | |
| 1328 | ||
| 1329 | #[test] | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1330 | fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() { |
| 1331 | let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op; | |
| 1332 | assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest); | |
| 1333 | assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft); | |
| 1334 | assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest); | |
| 1335 | // Reopening and closing with a PATCH, as `state`. | |
| 1336 | let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap(); | |
| 1337 | assert_eq!(route.op, Op::UpdatePullRequest); | |
| 1338 | assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" })); | |
| 1339 | let (route, input) = | |
| 1340 | resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap(); | |
| 1341 | assert_eq!(route.op, Op::EditComment); | |
| 1342 | assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" })); | |
| 1343 | let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap(); | |
| 1344 | assert_eq!(route.op, Op::DeleteComment); | |
| 1345 | assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" })); | |
| 1346 | // Still an issue's comments, labels and subscription. | |
| 1347 | assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment); | |
| 1348 | assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels); | |
| 1349 | assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription); | |
| 1350 | } | |
| 1351 | ||
| 1352 | #[test] | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1353 | fn billing_is_addressed_by_workspace() { |
| 1354 | let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())]; | |
| 1355 | let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap(); | |
| 1356 | assert_eq!(route.op, Op::GetUsage); | |
| 1357 | assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" })); | |
| 1358 | let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap(); | |
| 1359 | assert_eq!(route.op, Op::SetBudget); | |
| 1360 | assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" })); | |
| 1361 | let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op; | |
| 1362 | assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget); | |
| 1363 | assert_eq!(op("GET", "/workspaces/acme/ai_credit"), Op::GetAiCredit); | |
| 1364 | assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit); | |
| 1365 | assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices); | |
| 1366 | assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails); | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 1367 | assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1368 | } |
| 1369 | ||
| 1370 | #[test] | |
| Merge checks: statuses and check runs on every commit | 1371 | fn checks_are_at_githubs_addresses() { |
| 1372 | let sha = "a".repeat(40); | |
| 1373 | let body = json!({ "state": "success", "context": "ci/build" }); | |
| 1374 | let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap(); | |
| 1375 | assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus)); | |
| 1376 | assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" })); | |
| 1377 | let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap(); | |
| 1378 | assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus)); | |
| 1379 | assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" })); | |
| 1380 | let query = [("check_name".to_owned(), "lint".to_owned())]; | |
| 1381 | let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap(); | |
| 1382 | assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef)); | |
| 1383 | assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" })); | |
| 1384 | let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap(); | |
| 1385 | assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun)); | |
| 1386 | assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" })); | |
| 1387 | let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op; | |
| 1388 | assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun)); | |
| 1389 | assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations)); | |
| 1390 | assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite)); | |
| 1391 | assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef)); | |
| 1392 | } | |
| 1393 | ||
| 1394 | #[test] | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 1395 | fn query_parameters_are_renamed() { |
| 1396 | let query = [ | |
| 1397 | ("q".to_owned(), "parser".to_owned()), | |
| 1398 | ("x".to_owned(), "y".to_owned()), | |
| 1399 | ]; | |
| 1400 | let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap(); | |
| 1401 | assert_eq!(route.op, Op::ListRepos); | |
| 1402 | assert_eq!(input, json!({ "query": "parser" })); | |
| 1403 | } | |
| 1404 | ||
| 1405 | #[test] | |
| 1406 | fn method_and_shape_must_match() { | |
| 1407 | assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none()); | |
| 1408 | assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none()); | |
| 1409 | assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none()); | |
| 1410 | assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some()); | |
| 1411 | } | |
| 1412 | ||
| 1413 | #[test] | |
| 1414 | fn every_parameter_and_query_name_is_an_input() { | |
| 1415 | for route in ROUTES { | |
| 1416 | let properties = route.op.properties(); | |
| 1417 | for (_, key) in route.query { | |
| 1418 | assert!(properties.contains_key(*key), "{}: {key}", route.path); | |
| 1419 | } | |
| 1420 | for name in route.params() { | |
| 1421 | let covered = matches!(name, "owner" | "name") && properties.contains_key("repo") | |
| 1422 | || properties.contains_key(name); | |
| 1423 | assert!(covered, "{}: {name}", route.path); | |
| 1424 | } | |
| 1425 | } | |
| 1426 | } | |
| 1427 | ||
| 1428 | #[test] | |
| 1429 | fn every_operation_has_a_route() { | |
| 1430 | for op in Op::ALL { | |
| 1431 | assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name()); | |
| 1432 | } | |
| 1433 | } | |
| 1434 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.