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