Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar
Five workstreams built in parallel and merged together (deployed 2026-10-08). Teams and CODEOWNERS - Workspace teams: visible or secret, nested up to 8 levels (children inherit repository roles), maintainers and members, notify-on-mention, review assignment (round robin or load balance, count, skip busy, exclusions). Team repository roles are repo_grants with principal_kind 'team', folded into each person's effective role (highest wins), shown as "via team X". - @workspace/team mentions (inbox reason team_mention) and autocomplete; team review requests (pull.review_requested carries teams and code owners). - CODEOWNERS read from the base branch: .g1t/, .github/, root, docs/, .gitlab/ (first found). gitignore-style patterns, last match wins, people, teams and emails as owners, sections with optional and N-approval rules. Owners are asked to review; errors with line numbers on the blob page, Settings > Branches, a g1t / codeowners status and REST. "Require review from code owners" gates the merge button, API, auto-merge and the queue for people and agents alike; @g1t counts only where the file names it. - Pages /<ws>/-/teams (list, new, members, child teams, repositories, settings); REST, MCP team tool, webhooks and audit log. - identity 0027_teams, work 0026_code_owners. Labels, milestones, pull requests into any branch - Labels per repository (defaults, colours, labels page, pickers, filters), milestones (list, page with progress), a base branch chosen on open or changed later; merge, catch-up, mergeability and @g1t work use it. Branch protection applies to pull requests into the default branch. - 14 REST/MCP operations; issue/pull labeled, unlabeled, milestoned, demilestoned and pull.base_changed events (webhooks and workflows). - work 0027_labels_milestones_bases. Dependency updates: dependabot.yml, version 2 - Read from .g1t/dependabot.yml, then .github/dependabot.yml, unchanged; every option parsed and validated (unknown keys are errors), ecosystems g1t cannot update yet are accepted and listed. Version update pull requests for npm (npm, pnpm, yarn), cargo, gomod and pip (incl. poetry) on each entry's schedule, grouped, limited, with commit-message, branch-name, labels, milestone, target-branch, reviewers and assignees; security updates follow the same file; Dependabot's comment commands addressed to @g1t. Its own Security tab. security 0004_version_updates. Security suite and the Security and quality activation - Custom secret patterns (linear-time regex, limits, dry run), push protection bypass with reasons and delegated approval, validity checks for issuers with a safe read-only endpoint, alert pages. - Code scanning: SARIF 2.1.0 upload, fingerprints, fixed detection, a starter workflow (Bandit, gosec, ESLint + eslint-plugin-security, clippy via clippy-sarif), PR statuses and comments, Fix with g1t. - Dependency graph, SPDX 2.3 SBOM, dependency review on pull requests, workspace security overview, repository Security tabs. - Security and quality activation ($10/month via the price book; free for public repositories and for the free core). REST, MCP security tool, webhooks, inbox notices. security 0005_security_suite, billing 0037_security_activation. Also - Top bar: the New button is a compact "+" Create new menu (with New team); search lives in the sidebar ("Search or jump to", the palette), with a magnifier on phones; docs updated. - repos read_file: the store answers a missing path with NOT_FOUND; it is remembered as absent for 10 minutes instead of failing. - The pull request page reads PullDetail's snake_case fields again (required checks, earlier checks, review pending). - A dependency-review arm that the version-update arm made unreachable is merged into one.
| 81 | 81 | ] | |
| 82 | 82 | ||
| 83 | 83 | [[package]] | |
| 84 | + | name = "aho-corasick" | |
| 85 | + | version = "1.1.5" | |
| 86 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 87 | + | checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" | |
| 88 | + | dependencies = [ | |
| 89 | + | "memchr", | |
| 90 | + | ] | |
| 91 | + | ||
| 92 | + | [[package]] | |
| 84 | 93 | name = "android_system_properties" | |
| 85 | 94 | version = "0.1.6" | |
| 86 | 95 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ | |||
| 108 | 117 | ] | |
| 109 | 118 | ||
| 110 | 119 | [[package]] | |
| 120 | + | name = "arraydeque" | |
| 121 | + | version = "0.5.1" | |
| 122 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 123 | + | checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" | |
| 124 | + | ||
| 125 | + | [[package]] | |
| 111 | 126 | name = "async-trait" | |
| 112 | 127 | version = "0.1.92" | |
| 113 | 128 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ | |||
| 774 | 789 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | |
| 775 | 790 | ||
| 776 | 791 | [[package]] | |
| 792 | + | name = "foldhash" | |
| 793 | + | version = "0.2.0" | |
| 794 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 795 | + | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | |
| 796 | + | ||
| 797 | + | [[package]] | |
| 777 | 798 | name = "form_urlencoded" | |
| 778 | 799 | version = "1.2.2" | |
| 779 | 800 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ | |||
| 1077 | 1098 | version = "0.1.0" | |
| 1078 | 1099 | dependencies = [ | |
| 1079 | 1100 | "miniz_oxide", | |
| 1101 | + | "regex", | |
| 1080 | 1102 | "serde", | |
| 1081 | 1103 | "serde_json", | |
| 1082 | 1104 | "sha1 0.11.0", | |
| ⋯ | |||
| 1112 | 1134 | version = "0.1.0" | |
| 1113 | 1135 | dependencies = [ | |
| 1114 | 1136 | "futures-util", | |
| 1137 | + | "g1t-actions", | |
| 1115 | 1138 | "g1t-contracts", | |
| 1116 | 1139 | "g1t-kit", | |
| 1117 | 1140 | "g1t-scan", | |
| ⋯ | |||
| 1119 | 1142 | "serde", | |
| 1120 | 1143 | "serde_json", | |
| 1121 | 1144 | "serde_yaml", | |
| 1145 | + | "toml", | |
| 1122 | 1146 | "worker", | |
| 1147 | + | "yaml-rust2", | |
| 1123 | 1148 | ] | |
| 1124 | 1149 | ||
| 1125 | 1150 | [[package]] | |
| ⋯ | |||
| 1154 | 1179 | name = "g1t-work" | |
| 1155 | 1180 | version = "0.1.0" | |
| 1156 | 1181 | dependencies = [ | |
| 1182 | + | "base64 0.22.1", | |
| 1157 | 1183 | "futures-util", | |
| 1158 | 1184 | "g1t-contracts", | |
| 1159 | 1185 | "g1t-kit", | |
| ⋯ | |||
| 1268 | 1294 | version = "0.17.1" | |
| 1269 | 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1270 | 1296 | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | |
| 1297 | + | dependencies = [ | |
| 1298 | + | "foldhash", | |
| 1299 | + | ] | |
| 1271 | 1300 | ||
| 1272 | 1301 | [[package]] | |
| 1302 | + | name = "hashlink" | |
| 1303 | + | version = "0.12.2" | |
| 1304 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1305 | + | checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" | |
| 1306 | + | dependencies = [ | |
| 1307 | + | "hashbrown", | |
| 1308 | + | ] | |
| 1309 | + | ||
| 1310 | + | [[package]] | |
| 1273 | 1311 | name = "heck" | |
| 1274 | 1312 | version = "0.5.0" | |
| 1275 | 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ | |||
| 2267 | 2305 | ] | |
| 2268 | 2306 | ||
| 2269 | 2307 | [[package]] | |
| 2308 | + | name = "regex" | |
| 2309 | + | version = "1.13.1" | |
| 2310 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2311 | + | checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" | |
| 2312 | + | dependencies = [ | |
| 2313 | + | "aho-corasick", | |
| 2314 | + | "memchr", | |
| 2315 | + | "regex-automata", | |
| 2316 | + | "regex-syntax", | |
| 2317 | + | ] | |
| 2318 | + | ||
| 2319 | + | [[package]] | |
| 2320 | + | name = "regex-automata" | |
| 2321 | + | version = "0.4.18" | |
| 2322 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2323 | + | checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" | |
| 2324 | + | dependencies = [ | |
| 2325 | + | "aho-corasick", | |
| 2326 | + | "memchr", | |
| 2327 | + | "regex-syntax", | |
| 2328 | + | ] | |
| 2329 | + | ||
| 2330 | + | [[package]] | |
| 2331 | + | name = "regex-syntax" | |
| 2332 | + | version = "0.8.11" | |
| 2333 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2334 | + | checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" | |
| 2335 | + | ||
| 2336 | + | [[package]] | |
| 2270 | 2337 | name = "reqwest" | |
| 2271 | 2338 | version = "0.13.5" | |
| 2272 | 2339 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ | |||
| 3869 | 3936 | checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" | |
| 3870 | 3937 | ||
| 3871 | 3938 | [[package]] | |
| 3939 | + | name = "yaml-rust2" | |
| 3940 | + | version = "0.13.0" | |
| 3941 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3942 | + | checksum = "57e5b818a27a4cd30884ea380857a5e56f7ec3ba24a3990a3cc0b95af3238e18" | |
| 3943 | + | dependencies = [ | |
| 3944 | + | "arraydeque", | |
| 3945 | + | "hashlink", | |
| 3946 | + | ] | |
| 3947 | + | ||
| 3948 | + | [[package]] | |
| 3872 | 3949 | name = "yoke" | |
| 3873 | 3950 | version = "0.8.3" | |
| 3874 | 3951 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 19 | 19 | mod responses; | |
| 20 | 20 | mod rest; | |
| 21 | 21 | mod runners; | |
| 22 | + | mod security; | |
| 22 | 23 | mod tools; | |
| 23 | 24 | ||
| 24 | 25 | use g1t_contracts::billing::FinishRunArgs; |
| 11 | 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | 12 | ||
| 13 | 13 | const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers. | |
| 14 | − | Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, webhook, access, workspace, account. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs. | |
| 14 | + | Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, security, webhook, access, workspace, account, notifications. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs. | |
| 15 | 15 | Find a repository: account whoami lists your workspaces; repository list or search finds one. | |
| 16 | 16 | Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again. | |
| 17 | 17 | Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money. | |
| 18 | + | Security: security code_alerts, vulnerability_alerts and secret_alerts show what to fix; fix it in your pull request, which the Code scanning and Dependency review checks judge. | |
| 18 | 19 | When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret."; | |
| 19 | 20 | ||
| 20 | 21 | fn result(id: &Value, value: Value) -> Value { |
| 119 | 119 | "token_endpoint_auth_methods_supported": ["none"], | |
| 120 | 120 | // A client may ask for some of these with `scope`; the person | |
| 121 | 121 | // approving can trim them. Asking for none gives the agent preset. | |
| 122 | − | "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()), | |
| 122 | + | "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()).to_vec(), | |
| 123 | 123 | "service_documentation": "https://docs.g1t.sh/guides/authentication/", | |
| 124 | 124 | }) | |
| 125 | 125 | } | |
| ⋯ | |||
| 245 | 245 | "authorization_servers": [services.addresses.api], | |
| 246 | 246 | "bearer_methods_supported": ["header"], | |
| 247 | 247 | "resource_documentation": "https://docs.g1t.sh/guides/bring-your-own-agent/", | |
| 248 | − | "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()), | |
| 248 | + | "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()).to_vec(), | |
| 249 | 249 | }))? | |
| 250 | 250 | } | |
| 251 | 251 | ("POST", "/oauth/register") => register(request).await?, | |
| 8 | 8 | use serde_json::{Map, Value, json}; | |
| 9 | 9 | ||
| 10 | 10 | use crate::operations::Op; | |
| 11 | + | use crate::security::SecurityOp; | |
| 11 | 12 | use crate::rest::{ROUTES, Route}; | |
| 12 | 13 | ||
| 13 | 14 | /// The sections of the API reference: a name, what it covers, and its | |
| ⋯ | |||
| 81 | 82 | Op::GetRepoSettings, | |
| 82 | 83 | Op::UpdateRepoSettings, | |
| 83 | 84 | Op::ListCheckNames, | |
| 85 | + | Op::GetCodeownersErrors, | |
| 84 | 86 | Op::ListEvents, | |
| 85 | 87 | ], | |
| 86 | 88 | ), | |
| ⋯ | |||
| 103 | 105 | ], | |
| 104 | 106 | ), | |
| 105 | 107 | ( | |
| 108 | + | "Teams", | |
| 109 | + | "Groups of a workspace's members: given a role on repositories together, mentioned together as @workspace/team, and asked to review together. Any member may create a team; the workspace's owners and the team's maintainers manage it.", | |
| 110 | + | &[ | |
| 111 | + | Op::ListTeams, | |
| 112 | + | Op::CreateTeam, | |
| 113 | + | Op::GetTeam, | |
| 114 | + | Op::UpdateTeam, | |
| 115 | + | Op::DeleteTeam, | |
| 116 | + | Op::ListTeamMembers, | |
| 117 | + | Op::SetTeamMember, | |
| 118 | + | Op::RemoveTeamMember, | |
| 119 | + | Op::ListChildTeams, | |
| 120 | + | Op::ListTeamRepos, | |
| 121 | + | Op::SetTeamRepo, | |
| 122 | + | Op::RemoveTeamRepo, | |
| 123 | + | Op::SetTeamReviewAssignment, | |
| 124 | + | Op::ListUserTeams, | |
| 125 | + | ], | |
| 126 | + | ), | |
| 127 | + | ( | |
| 106 | 128 | "Security", | |
| 107 | 129 | "Secrets found in what is pushed and in a repository's history, and dependencies with known vulnerabilities: listing the alerts, and dismissing or reopening them.", | |
| 108 | 130 | &[Op::ListSecurityAlerts, Op::DismissSecurityAlert, Op::ReopenSecurityAlert], | |
| 109 | 131 | ), | |
| 110 | 132 | ( | |
| 133 | + | "Secret scanning", | |
| 134 | + | "Secrets found in pushes and history, where each one is, pushing past push protection with a reason (and asking for approval when the workspace delegates bypasses), checking with a secret's issuer whether it still works, and custom patterns.", | |
| 135 | + | &[ | |
| 136 | + | Op::Security(SecurityOp::ListSecretAlerts), | |
| 137 | + | Op::Security(SecurityOp::GetSecretAlert), | |
| 138 | + | Op::Security(SecurityOp::UpdateSecretAlert), | |
| 139 | + | Op::Security(SecurityOp::ListSecretLocations), | |
| 140 | + | Op::Security(SecurityOp::BypassPushProtection), | |
| 141 | + | Op::Security(SecurityOp::CheckSecretValidity), | |
| 142 | + | Op::Security(SecurityOp::ListBypassRequests), | |
| 143 | + | Op::Security(SecurityOp::ReviewBypassRequest), | |
| 144 | + | Op::Security(SecurityOp::ListCustomPatterns), | |
| 145 | + | Op::Security(SecurityOp::CreateCustomPattern), | |
| 146 | + | Op::Security(SecurityOp::UpdateCustomPattern), | |
| 147 | + | Op::Security(SecurityOp::DeleteCustomPattern), | |
| 148 | + | Op::Security(SecurityOp::DryRunCustomPattern), | |
| 149 | + | ], | |
| 150 | + | ), | |
| 151 | + | ( | |
| 152 | + | "Code scanning", | |
| 153 | + | "Results of static analysis tools, uploaded as SARIF: alerts on the default branch, the analyses that made them, uploads, and putting g1t on an alert to fix it.", | |
| 154 | + | &[ | |
| 155 | + | Op::Security(SecurityOp::ListCodeAlerts), | |
| 156 | + | Op::Security(SecurityOp::GetCodeAlert), | |
| 157 | + | Op::Security(SecurityOp::UpdateCodeAlert), | |
| 158 | + | Op::Security(SecurityOp::ListAnalyses), | |
| 159 | + | Op::Security(SecurityOp::UploadSarif), | |
| 160 | + | Op::Security(SecurityOp::GetSarifUpload), | |
| 161 | + | Op::Security(SecurityOp::FixAlert), | |
| 162 | + | ], | |
| 163 | + | ), | |
| 164 | + | ( | |
| 165 | + | "Supply chain", | |
| 166 | + | "What a repository depends on: vulnerability alerts, the dependency graph, an SPDX SBOM of it, and comparing two commits' dependencies as dependency review does.", | |
| 167 | + | &[ | |
| 168 | + | Op::Security(SecurityOp::ListVulnerabilityAlerts), | |
| 169 | + | Op::Security(SecurityOp::GetVulnerabilityAlert), | |
| 170 | + | Op::Security(SecurityOp::UpdateVulnerabilityAlert), | |
| 171 | + | Op::Security(SecurityOp::GetDependencyGraph), | |
| 172 | + | Op::Security(SecurityOp::GetSbom), | |
| 173 | + | Op::Security(SecurityOp::CompareDependencies), | |
| 174 | + | ], | |
| 175 | + | ), | |
| 176 | + | ( | |
| 177 | + | "Security settings", | |
| 178 | + | "When pull request checks fail, dependency review's policy, delegated bypass and validity checks, and a workspace's security overview.", | |
| 179 | + | &[ | |
| 180 | + | Op::Security(SecurityOp::GetSettings), | |
| 181 | + | Op::Security(SecurityOp::UpdateSettings), | |
| 182 | + | Op::Security(SecurityOp::GetWorkspaceSettings), | |
| 183 | + | Op::Security(SecurityOp::UpdateWorkspaceSettings), | |
| 184 | + | Op::Security(SecurityOp::GetOverview), | |
| 185 | + | ], | |
| 186 | + | ), | |
| 187 | + | ( | |
| 111 | 188 | "Issues", | |
| 112 | 189 | "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.", | |
| 113 | 190 | &[ | |
| ⋯ | |||
| 120 | 197 | Op::AssignIssue, | |
| 121 | 198 | Op::Delegate, | |
| 122 | 199 | Op::AddComment, | |
| 200 | + | Op::ListIssueLabels, | |
| 201 | + | Op::AddIssueLabels, | |
| 202 | + | Op::SetIssueLabels, | |
| 203 | + | Op::RemoveIssueLabels, | |
| 204 | + | ], | |
| 205 | + | ), | |
| 206 | + | ( | |
| 207 | + | "Labels and milestones", | |
| 208 | + | "A repository's labels, which issues and pull requests carry by name, and its milestones, which gather them under a goal and a due date.", | |
| 209 | + | &[ | |
| 123 | 210 | Op::ListLabels, | |
| 211 | + | Op::CreateLabel, | |
| 212 | + | Op::UpdateLabel, | |
| 213 | + | Op::DeleteLabel, | |
| 214 | + | Op::AddDefaultLabels, | |
| 215 | + | Op::ListMilestones, | |
| 216 | + | Op::CreateMilestone, | |
| 217 | + | Op::GetMilestone, | |
| 218 | + | Op::UpdateMilestone, | |
| 219 | + | Op::DeleteMilestone, | |
| 124 | 220 | ], | |
| 125 | 221 | ), | |
| 126 | 222 | ( | |
| ⋯ | |||
| 135 | 231 | Op::ListPullRequests, | |
| 136 | 232 | Op::CreatePullRequest, | |
| 137 | 233 | Op::GetPullRequest, | |
| 234 | + | Op::UpdatePullRequest, | |
| 138 | 235 | Op::GetPullRequestChanges, | |
| 139 | 236 | Op::MarkPullRequestReady, | |
| 237 | + | Op::RequestReviewers, | |
| 238 | + | Op::RemoveRequestedReviewers, | |
| 140 | 239 | Op::ReviewPullRequest, | |
| 141 | 240 | Op::MergePullRequest, | |
| 142 | 241 | Op::ClosePullRequest, | |
| ⋯ | |||
| 299 | 398 | Op::GetPlan => "Get a plan", | |
| 300 | 399 | Op::ApplyPlan => "Apply a plan", | |
| 301 | 400 | Op::ListLabels => "List labels", | |
| 401 | + | Op::CreateLabel => "Create a label", | |
| 402 | + | Op::UpdateLabel => "Update a label", | |
| 403 | + | Op::DeleteLabel => "Delete a label", | |
| 404 | + | Op::AddDefaultLabels => "Add the default labels", | |
| 405 | + | Op::ListIssueLabels => "List an issue's labels", | |
| 406 | + | Op::AddIssueLabels => "Add labels to an issue", | |
| 407 | + | Op::SetIssueLabels => "Set an issue's labels", | |
| 408 | + | Op::RemoveIssueLabels => "Remove labels from an issue", | |
| 409 | + | Op::ListMilestones => "List milestones", | |
| 410 | + | Op::GetMilestone => "Get a milestone", | |
| 411 | + | Op::CreateMilestone => "Create a milestone", | |
| 412 | + | Op::UpdateMilestone => "Update a milestone", | |
| 413 | + | Op::DeleteMilestone => "Delete a milestone", | |
| 414 | + | Op::UpdatePullRequest => "Update a pull request", | |
| 302 | 415 | Op::AddComment => "Add a comment", | |
| 303 | 416 | Op::ReviewPullRequest => "Review a pull request", | |
| 304 | 417 | Op::ListPullRequests => "List pull requests", | |
| ⋯ | |||
| 382 | 495 | Op::PinProject => "Pin a project", | |
| 383 | 496 | Op::UnpinProject => "Unpin a project", | |
| 384 | 497 | Op::ReorderPinnedProjects => "Reorder your pinned projects", | |
| 498 | + | Op::ListTeams => "List teams", | |
| 499 | + | Op::GetTeam => "Get a team", | |
| 500 | + | Op::CreateTeam => "Create a team", | |
| 501 | + | Op::UpdateTeam => "Update a team", | |
| 502 | + | Op::DeleteTeam => "Delete a team", | |
| 503 | + | Op::ListTeamMembers => "List a team's members", | |
| 504 | + | Op::SetTeamMember => "Add or change a team member", | |
| 505 | + | Op::RemoveTeamMember => "Remove a team member", | |
| 506 | + | Op::ListChildTeams => "List child teams", | |
| 507 | + | Op::ListTeamRepos => "List a team's repositories", | |
| 508 | + | Op::SetTeamRepo => "Give a team a role on a repository", | |
| 509 | + | Op::RemoveTeamRepo => "Remove a team from a repository", | |
| 510 | + | Op::SetTeamReviewAssignment => "Set a team's review assignment", | |
| 511 | + | Op::ListUserTeams => "List someone's teams", | |
| 512 | + | Op::RequestReviewers => "Request reviewers", | |
| 513 | + | Op::RemoveRequestedReviewers => "Remove requested reviewers", | |
| 514 | + | Op::GetCodeownersErrors => "List CODEOWNERS errors", | |
| 515 | + | Op::Security(op) => op.title(), | |
| 385 | 516 | } | |
| 386 | 517 | } | |
| 387 | 518 | ||
| ⋯ | |||
| 476 | 607 | "PUT" => "set_issue_subscription".to_owned(), | |
| 477 | 608 | _ => "delete_issue_subscription".to_owned(), | |
| 478 | 609 | }, | |
| 610 | + | // One label off an issue, by its name in the path. | |
| 611 | + | ("DELETE", ":label") if route.path.contains("/issues/:number/") => "remove_issue_label".to_owned(), | |
| 479 | 612 | ("DELETE", "saved") => "unsave_thread".to_owned(), | |
| 480 | 613 | ("DELETE", "snooze") => "unsnooze_thread".to_owned(), | |
| 481 | 614 | _ => op.name().to_owned(), | |
| 9 | 9 | OutsideCollaboratorsArgs, RemoveCollaboratorArgs, RepoAccess, RepoAccessArgs, RepoInvitation, RepoRole, | |
| 10 | 10 | RespondRepoInvitationArgs, RevokeRepoInvitationArgs, SetBasePermissionArgs, SetCollaboratorRoleArgs, | |
| 11 | 11 | }; | |
| 12 | + | use g1t_contracts::codeowners::CodeOwnersErrorsArgs; | |
| 12 | 13 | use g1t_contracts::identity::AgentScope; | |
| 13 | 14 | use g1t_contracts::events::{Event, ListArgs as ListEventsArgs}; | |
| 14 | 15 | use g1t_contracts::identity::{CreateWorkspaceArgs, UpdateWorkspaceArgs, Workspace}; | |
| 15 | 16 | use g1t_contracts::repos::{CreateArgs, GetArgs, ListArgs as ListReposArgs, Repo, RepoPath}; | |
| 17 | + | use g1t_contracts::teams::{ | |
| 18 | + | CreateTeamArgs, DeleteTeamArgs, ListTeamsArgs, RemoveTeamMemberArgs, RemoveTeamRepoArgs, ReviewAlgorithm, | |
| 19 | + | ReviewAssignment, SetTeamMemberArgs, SetTeamRepoArgs, Team, TeamArgs, TeamRole, TeamVisibility, UpdateTeamArgs, | |
| 20 | + | UserTeamsArgs, | |
| 21 | + | }; | |
| 16 | 22 | use g1t_contracts::security::{ | |
| 17 | 23 | AlertChange, AlertState, DismissArgs, DismissReason, OverviewArgs as SecurityOverviewArgs, ReopenArgs, | |
| 18 | 24 | SecurityOverview, | |
| 19 | 25 | }; | |
| 20 | 26 | ||
| 21 | 27 | use crate::alerts::{AlertKind, SecurityAlert}; | |
| 28 | + | use crate::security::SecurityOp; | |
| 22 | 29 | use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel}; | |
| 23 | 30 | use g1t_contracts::work::*; | |
| 24 | 31 | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| ⋯ | |||
| 131 | 138 | GetPlan, | |
| 132 | 139 | ApplyPlan, | |
| 133 | 140 | ListLabels, | |
| 141 | + | CreateLabel, | |
| 142 | + | UpdateLabel, | |
| 143 | + | DeleteLabel, | |
| 144 | + | AddDefaultLabels, | |
| 145 | + | ListIssueLabels, | |
| 146 | + | AddIssueLabels, | |
| 147 | + | SetIssueLabels, | |
| 148 | + | RemoveIssueLabels, | |
| 149 | + | ListMilestones, | |
| 150 | + | GetMilestone, | |
| 151 | + | CreateMilestone, | |
| 152 | + | UpdateMilestone, | |
| 153 | + | DeleteMilestone, | |
| 134 | 154 | AddComment, | |
| 135 | 155 | ReviewPullRequest, | |
| 136 | 156 | ListPullRequests, | |
| 137 | 157 | GetPullRequest, | |
| 138 | 158 | CreatePullRequest, | |
| 159 | + | UpdatePullRequest, | |
| 139 | 160 | RecordSession, | |
| 140 | 161 | ReadSession, | |
| 141 | 162 | MarkPullRequestReady, | |
| ⋯ | |||
| 214 | 235 | PinProject, | |
| 215 | 236 | UnpinProject, | |
| 216 | 237 | ReorderPinnedProjects, | |
| 238 | + | ListTeams, | |
| 239 | + | GetTeam, | |
| 240 | + | CreateTeam, | |
| 241 | + | UpdateTeam, | |
| 242 | + | DeleteTeam, | |
| 243 | + | ListTeamMembers, | |
| 244 | + | SetTeamMember, | |
| 245 | + | RemoveTeamMember, | |
| 246 | + | ListChildTeams, | |
| 247 | + | ListTeamRepos, | |
| 248 | + | SetTeamRepo, | |
| 249 | + | RemoveTeamRepo, | |
| 250 | + | SetTeamReviewAssignment, | |
| 251 | + | ListUserTeams, | |
| 252 | + | RequestReviewers, | |
| 253 | + | RemoveRequestedReviewers, | |
| 254 | + | GetCodeownersErrors, | |
| 255 | + | /// The security suite's operations: see [`crate::security`]. | |
| 256 | + | Security(SecurityOp), | |
| 217 | 257 | } | |
| 218 | 258 | ||
| 219 | 259 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| ⋯ | |||
| 415 | 455 | g1t_contracts::webhooks::EVENT_TYPES.to_vec() | |
| 416 | 456 | } | |
| 417 | 457 | ||
| 458 | + | fn label_schema() -> Value { | |
| 459 | + | json!({ "type": "string", "description": "The label's name, e.g. \"good first issue\". URL-encode spaces in the path." }) | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | fn milestone_schema() -> Value { | |
| 463 | + | json!({ "type": "integer", "description": "The milestone's number, from list_milestones." }) | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | /// A milestone given as a number, or as null or 0 for none: `Some(0)` for | |
| 467 | + | /// none, `None` when it was not given. | |
| 468 | + | fn milestone_input(input: &Value) -> Option<u32> { | |
| 469 | + | match input.get("milestone") { | |
| 470 | + | None => None, | |
| 471 | + | Some(Value::Null) => Some(0), | |
| 472 | + | Some(_) => integer(input, "milestone"), | |
| 473 | + | } | |
| 474 | + | } | |
| 475 | + | ||
| 418 | 476 | fn repo_schema() -> Value { | |
| 419 | 477 | json!({ | |
| 420 | 478 | "type": "string", | |
| ⋯ | |||
| 435 | 493 | }) | |
| 436 | 494 | } | |
| 437 | 495 | ||
| 496 | + | fn team_schema() -> Value { | |
| 497 | + | json!({ | |
| 498 | + | "type": "string", | |
| 499 | + | "description": "The team's slug, as in its mention @workspace/slug, e.g. \"backend\".", | |
| 500 | + | }) | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | /// A person's place in a team. | |
| 504 | + | fn team_role_schema() -> Value { | |
| 505 | + | json!({ | |
| 506 | + | "type": "string", | |
| 507 | + | "enum": [TeamRole::Member.as_str(), TeamRole::Maintainer.as_str()], | |
| 508 | + | "description": "member, or maintainer: also manages the team's people and settings. Defaults to member.", | |
| 509 | + | }) | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | fn team_visibility_schema() -> Value { | |
| 513 | + | json!({ | |
| 514 | + | "type": "string", | |
| 515 | + | "enum": [TeamVisibility::Visible.as_str(), TeamVisibility::Secret.as_str()], | |
| 516 | + | "description": "visible: every member of the workspace sees it. secret: only its own people and the workspace's owners.", | |
| 517 | + | }) | |
| 518 | + | } | |
| 519 | + | ||
| 520 | + | fn include_child_teams_schema() -> Value { | |
| 521 | + | json!({ | |
| 522 | + | "type": "boolean", | |
| 523 | + | "description": "Also the people of its child teams: listed with list_members, picked from with review assignment.", | |
| 524 | + | }) | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | /// The fields of a team's review assignment, each optional. | |
| 528 | + | fn review_assignment_properties() -> Value { | |
| 529 | + | json!({ | |
| 530 | + | "enabled": { | |
| 531 | + | "type": "boolean", | |
| 532 | + | "description": "On: g1t picks count people from the team to ask. Off: everyone in it is asked.", | |
| 533 | + | }, | |
| 534 | + | "algorithm": { | |
| 535 | + | "type": "string", | |
| 536 | + | "enum": [ReviewAlgorithm::RoundRobin.as_str(), ReviewAlgorithm::LoadBalance.as_str()], | |
| 537 | + | "description": "round_robin: whoever this team asked least recently. load_balance: whoever has the fewest pull requests waiting on their review.", | |
| 538 | + | }, | |
| 539 | + | "count": { | |
| 540 | + | "type": "integer", | |
| 541 | + | "minimum": 1, | |
| 542 | + | "maximum": g1t_contracts::teams::MAX_ASSIGNED, | |
| 543 | + | "description": "How many people to pick, 1 to 10. People from the team already asked count towards it.", | |
| 544 | + | }, | |
| 545 | + | "skip_busy": { | |
| 546 | + | "type": "boolean", | |
| 547 | + | "description": "Leave out anyone with busy_at or more pull requests waiting on their review.", | |
| 548 | + | }, | |
| 549 | + | "busy_at": { | |
| 550 | + | "type": "integer", | |
| 551 | + | "minimum": 1, | |
| 552 | + | "maximum": 100, | |
| 553 | + | "description": "With skip_busy: how many waiting reviews make someone busy, 1 to 100.", | |
| 554 | + | }, | |
| 555 | + | "include_child_teams": include_child_teams_schema(), | |
| 556 | + | "excluded": { | |
| 557 | + | "type": "array", | |
| 558 | + | "items": { "type": "string" }, | |
| 559 | + | "description": "Usernames never picked. Replaces the whole list.", | |
| 560 | + | }, | |
| 561 | + | "notify_team": { | |
| 562 | + | "type": "boolean", | |
| 563 | + | "description": "Also tell the rest of the team when people are picked.", | |
| 564 | + | }, | |
| 565 | + | }) | |
| 566 | + | } | |
| 567 | + | ||
| 568 | + | /// The inputs naming a team, with `more` added. | |
| 569 | + | fn team_target(more: Value) -> Value { | |
| 570 | + | let mut properties = json!({ "workspace": workspace_schema(), "team": team_schema() }); | |
| 571 | + | if let (Some(all), Value::Object(more)) = (properties.as_object_mut(), more) { | |
| 572 | + | all.extend(more); | |
| 573 | + | } | |
| 574 | + | properties | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | /// The people and teams to ask, or stop asking, to review a pull request. | |
| 578 | + | fn requested_reviewers_properties() -> Value { | |
| 579 | + | numbered(json!({ | |
| 580 | + | "reviewers": { | |
| 581 | + | "type": "array", | |
| 582 | + | "items": { "type": "string" }, | |
| 583 | + | "description": "Usernames. g1t asks a g1t agent.", | |
| 584 | + | }, | |
| 585 | + | "team_reviewers": { | |
| 586 | + | "type": "array", | |
| 587 | + | "items": { "type": "string" }, | |
| 588 | + | "description": "Teams, as \"workspace/team\", or the team's slug in the repository's workspace.", | |
| 589 | + | }, | |
| 590 | + | })) | |
| 591 | + | } | |
| 592 | + | ||
| 438 | 593 | fn thread_id_schema() -> Value { | |
| 439 | 594 | json!({ "type": "string", "description": "The thread's id, from list_notifications." }) | |
| 440 | 595 | } | |
| ⋯ | |||
| 461 | 616 | } | |
| 462 | 617 | ||
| 463 | 618 | impl Op { | |
| 464 | − | pub const ALL: [Op; 135] = [ | |
| 619 | + | pub const ALL: [Op; 197] = [ | |
| 465 | 620 | Op::Whoami, | |
| 466 | 621 | Op::CreateWorkspace, | |
| 467 | 622 | Op::DeleteWorkspace, | |
| ⋯ | |||
| 514 | 669 | Op::GetPlan, | |
| 515 | 670 | Op::ApplyPlan, | |
| 516 | 671 | Op::ListLabels, | |
| 672 | + | Op::CreateLabel, | |
| 673 | + | Op::UpdateLabel, | |
| 674 | + | Op::DeleteLabel, | |
| 675 | + | Op::AddDefaultLabels, | |
| 676 | + | Op::ListIssueLabels, | |
| 677 | + | Op::AddIssueLabels, | |
| 678 | + | Op::SetIssueLabels, | |
| 679 | + | Op::RemoveIssueLabels, | |
| 680 | + | Op::ListMilestones, | |
| 681 | + | Op::GetMilestone, | |
| 682 | + | Op::CreateMilestone, | |
| 683 | + | Op::UpdateMilestone, | |
| 684 | + | Op::DeleteMilestone, | |
| 517 | 685 | Op::AddComment, | |
| 518 | 686 | Op::ReviewPullRequest, | |
| 519 | 687 | Op::ListPullRequests, | |
| 520 | 688 | Op::GetPullRequest, | |
| 521 | 689 | Op::CreatePullRequest, | |
| 690 | + | Op::UpdatePullRequest, | |
| 522 | 691 | Op::RecordSession, | |
| 523 | 692 | Op::ReadSession, | |
| 524 | 693 | Op::MarkPullRequestReady, | |
| ⋯ | |||
| 597 | 766 | Op::PinProject, | |
| 598 | 767 | Op::UnpinProject, | |
| 599 | 768 | Op::ReorderPinnedProjects, | |
| 769 | + | Op::ListTeams, | |
| 770 | + | Op::GetTeam, | |
| 771 | + | Op::CreateTeam, | |
| 772 | + | Op::UpdateTeam, | |
| 773 | + | Op::DeleteTeam, | |
| 774 | + | Op::ListTeamMembers, | |
| 775 | + | Op::SetTeamMember, | |
| 776 | + | Op::RemoveTeamMember, | |
| 777 | + | Op::ListChildTeams, | |
| 778 | + | Op::ListTeamRepos, | |
| 779 | + | Op::SetTeamRepo, | |
| 780 | + | Op::RemoveTeamRepo, | |
| 781 | + | Op::SetTeamReviewAssignment, | |
| 782 | + | Op::ListUserTeams, | |
| 783 | + | Op::RequestReviewers, | |
| 784 | + | Op::RemoveRequestedReviewers, | |
| 785 | + | Op::GetCodeownersErrors, | |
| 786 | + | Op::Security(SecurityOp::ListSecretAlerts), | |
| 787 | + | Op::Security(SecurityOp::GetSecretAlert), | |
| 788 | + | Op::Security(SecurityOp::UpdateSecretAlert), | |
| 789 | + | Op::Security(SecurityOp::ListSecretLocations), | |
| 790 | + | Op::Security(SecurityOp::BypassPushProtection), | |
| 791 | + | Op::Security(SecurityOp::CheckSecretValidity), | |
| 792 | + | Op::Security(SecurityOp::ListBypassRequests), | |
| 793 | + | Op::Security(SecurityOp::ReviewBypassRequest), | |
| 794 | + | Op::Security(SecurityOp::ListCustomPatterns), | |
| 795 | + | Op::Security(SecurityOp::CreateCustomPattern), | |
| 796 | + | Op::Security(SecurityOp::UpdateCustomPattern), | |
| 797 | + | Op::Security(SecurityOp::DeleteCustomPattern), | |
| 798 | + | Op::Security(SecurityOp::DryRunCustomPattern), | |
| 799 | + | Op::Security(SecurityOp::ListCodeAlerts), | |
| 800 | + | Op::Security(SecurityOp::GetCodeAlert), | |
| 801 | + | Op::Security(SecurityOp::UpdateCodeAlert), | |
| 802 | + | Op::Security(SecurityOp::ListAnalyses), | |
| 803 | + | Op::Security(SecurityOp::UploadSarif), | |
| 804 | + | Op::Security(SecurityOp::GetSarifUpload), | |
| 805 | + | Op::Security(SecurityOp::ListVulnerabilityAlerts), | |
| 806 | + | Op::Security(SecurityOp::GetVulnerabilityAlert), | |
| 807 | + | Op::Security(SecurityOp::UpdateVulnerabilityAlert), | |
| 808 | + | Op::Security(SecurityOp::FixAlert), | |
| 809 | + | Op::Security(SecurityOp::GetDependencyGraph), | |
| 810 | + | Op::Security(SecurityOp::GetSbom), | |
| 811 | + | Op::Security(SecurityOp::CompareDependencies), | |
| 812 | + | Op::Security(SecurityOp::GetSettings), | |
| 813 | + | Op::Security(SecurityOp::UpdateSettings), | |
| 814 | + | Op::Security(SecurityOp::GetWorkspaceSettings), | |
| 815 | + | Op::Security(SecurityOp::UpdateWorkspaceSettings), | |
| 816 | + | Op::Security(SecurityOp::GetOverview), | |
| 600 | 817 | ]; | |
| 601 | 818 | ||
| 602 | 819 | pub fn by_name(name: &str) -> Option<Op> { | |
| ⋯ | |||
| 658 | 875 | Op::GetPlan => "get_plan", | |
| 659 | 876 | Op::ApplyPlan => "apply_plan", | |
| 660 | 877 | Op::ListLabels => "list_labels", | |
| 878 | + | Op::CreateLabel => "create_label", | |
| 879 | + | Op::UpdateLabel => "update_label", | |
| 880 | + | Op::DeleteLabel => "delete_label", | |
| 881 | + | Op::AddDefaultLabels => "add_default_labels", | |
| 882 | + | Op::ListIssueLabels => "list_issue_labels", | |
| 883 | + | Op::AddIssueLabels => "add_issue_labels", | |
| 884 | + | Op::SetIssueLabels => "set_issue_labels", | |
| 885 | + | Op::RemoveIssueLabels => "remove_issue_labels", | |
| 886 | + | Op::ListMilestones => "list_milestones", | |
| 887 | + | Op::GetMilestone => "get_milestone", | |
| 888 | + | Op::CreateMilestone => "create_milestone", | |
| 889 | + | Op::UpdateMilestone => "update_milestone", | |
| 890 | + | Op::DeleteMilestone => "delete_milestone", | |
| 661 | 891 | Op::AddComment => "add_comment", | |
| 662 | 892 | Op::ReviewPullRequest => "review_pull_request", | |
| 663 | 893 | Op::ListPullRequests => "list_pull_requests", | |
| 664 | 894 | Op::GetPullRequest => "get_pull_request", | |
| 665 | 895 | Op::CreatePullRequest => "create_pull_request", | |
| 896 | + | Op::UpdatePullRequest => "update_pull_request", | |
| 666 | 897 | Op::RecordSession => "record_session", | |
| 667 | 898 | Op::ReadSession => "read_session", | |
| 668 | 899 | Op::MarkPullRequestReady => "mark_pull_request_ready", | |
| ⋯ | |||
| 741 | 972 | Op::PinProject => "pin_project", | |
| 742 | 973 | Op::UnpinProject => "unpin_project", | |
| 743 | 974 | Op::ReorderPinnedProjects => "reorder_pinned_projects", | |
| 975 | + | Op::ListTeams => "list_teams", | |
| 976 | + | Op::GetTeam => "get_team", | |
| 977 | + | Op::CreateTeam => "create_team", | |
| 978 | + | Op::UpdateTeam => "update_team", | |
| 979 | + | Op::DeleteTeam => "delete_team", | |
| 980 | + | Op::ListTeamMembers => "list_team_members", | |
| 981 | + | Op::SetTeamMember => "set_team_member", | |
| 982 | + | Op::RemoveTeamMember => "remove_team_member", | |
| 983 | + | Op::ListChildTeams => "list_child_teams", | |
| 984 | + | Op::ListTeamRepos => "list_team_repos", | |
| 985 | + | Op::SetTeamRepo => "set_team_repo", | |
| 986 | + | Op::RemoveTeamRepo => "remove_team_repo", | |
| 987 | + | Op::SetTeamReviewAssignment => "set_team_review_assignment", | |
| 988 | + | Op::ListUserTeams => "list_user_teams", | |
| 989 | + | Op::RequestReviewers => "request_reviewers", | |
| 990 | + | Op::RemoveRequestedReviewers => "remove_requested_reviewers", | |
| 991 | + | Op::GetCodeownersErrors => "get_codeowners_errors", | |
| 992 | + | Op::Security(op) => op.name(), | |
| 744 | 993 | } | |
| 745 | 994 | } | |
| 746 | 995 | ||
| ⋯ | |||
| 822 | 1071 | "Move a repository to another workspace, keeping its name. You must own both workspaces, and the destination must not already have a repository of that name; a free destination takes a private repository only if its private storage has room. Everything moves with it: git data, issues, pull requests, comments, labels, workflow runs, deployments, its project, and its own secrets, variables and webhooks. Its old address keeps working: web pages, git remotes and API calls redirect to the new one until a repository is made at the old address. Usage from now on is charged to the new workspace." | |
| 823 | 1072 | } | |
| 824 | 1073 | Op::GetRepoSettings => { | |
| 825 | − | "How a repository handles pull requests, as its default branch's protection: the checks that must pass (required_checks), the approvals a merge needs, whether required checks can be bypassed, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged. The same rules hold for a person's pull request and an agent's." | |
| 1074 | + | "How a repository handles pull requests, as its default branch's protection: the checks that must pass (required_checks), the approvals a merge needs, whether its code owners must approve (`require_code_owner_review`), whether required checks can be bypassed, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged. The same rules hold for a person's pull request and an agent's." | |
| 826 | 1075 | } | |
| 827 | 1076 | Op::UpdateRepoSettings => { | |
| 828 | − | "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. Needs the Maintain role or higher." | |
| 1077 | + | "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. With `require_code_owner_review`, a pull request merges only once the code owners of every file it changes, as the CODEOWNERS file of the branch it merges into names them, have approved it. Needs the Maintain role or higher." | |
| 829 | 1078 | } | |
| 830 | 1079 | Op::ListCheckNames => { | |
| 831 | 1080 | "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)." | |
| ⋯ | |||
| 861 | 1110 | "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere." | |
| 862 | 1111 | } | |
| 863 | 1112 | Op::ListIssues => { | |
| 864 | − | "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it." | |
| 1113 | + | "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it. Filter by state, by a label's name, or by a milestone's number." | |
| 865 | 1114 | } | |
| 866 | 1115 | Op::GetIssue => { | |
| 867 | 1116 | "An issue: its description (which may say what done means, under \"Definition of done\"), labels, its comments, and every pull request made against it with its status. If the issue is closed, resolved_by is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried." | |
| 868 | 1117 | } | |
| 869 | 1118 | Op::CreateIssue => { | |
| 870 | − | "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request." | |
| 1119 | + | "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request. labels are the repository's labels by name; a name it does not have yet is created when you have the Triage role or higher, and refused otherwise. milestone, a milestone's number, needs the Triage role." | |
| 871 | 1120 | } | |
| 872 | 1121 | Op::UpdateIssue => { | |
| 873 | − | "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher." | |
| 1122 | + | "Change an issue's title, body, labels, milestone or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set, and milestone null or 0 takes it out of its milestone. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher, and so does the milestone. Each label added or removed is an issue.labeled or issue.unlabeled event." | |
| 874 | 1123 | } | |
| 875 | 1124 | Op::CloseIssue => { | |
| 876 | 1125 | "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher." | |
| ⋯ | |||
| 891 | 1140 | Op::Delegate => { | |
| 892 | 1141 | "Put an agent on something in one step: open an issue and assign it to g1t at once. Say what you want done in plain words, with what done means if you know it. What must pass before its pull request merges is the default branch's required checks. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose." | |
| 893 | 1142 | } | |
| 894 | − | Op::ListLabels => "The labels available on a repository's issues.", | |
| 1143 | + | Op::ListLabels => { | |
| 1144 | + | "A repository's labels, by name: each one's color (six hex digits), description, and how many issues and pull requests carry it. A new repository starts with bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security." | |
| 1145 | + | } | |
| 1146 | + | Op::CreateLabel => { | |
| 1147 | + | "Create a label, named by label. Names are lowercase and unique in a repository, at most 50 characters; color is six hex digits (one is chosen from the name when left out), description at most 100 characters. Needs the Triage role or higher." | |
| 1148 | + | } | |
| 1149 | + | Op::UpdateLabel => { | |
| 1150 | + | "Change a label's name, color or description; only the fields given change. Renaming it renames it on every issue and pull request that carries it. Needs the Triage role or higher." | |
| 1151 | + | } | |
| 1152 | + | Op::DeleteLabel => { | |
| 1153 | + | "Delete a label. It is taken off every issue and pull request that carries it, without events for each. Needs the Triage role or higher." | |
| 1154 | + | } | |
| 1155 | + | Op::AddDefaultLabels => { | |
| 1156 | + | "Add the default labels a repository does not have yet: bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security. Labels it has already are left as they are. Returns every label it has now. Needs the Triage role or higher." | |
| 1157 | + | } | |
| 1158 | + | Op::ListIssueLabels => { | |
| 1159 | + | "The labels an issue or a pull request carries, with their colors and descriptions. Issues and pull requests share numbers." | |
| 1160 | + | } | |
| 1161 | + | Op::AddIssueLabels => { | |
| 1162 | + | "Add labels to an issue or a pull request, keeping the ones it has. A name the repository does not have yet is created when you have the Triage role or higher; without it, you may use the repository's labels on what you opened. Each label added is an issue.labeled or pull.labeled event. Returns its labels now, at most 20." | |
| 1163 | + | } | |
| 1164 | + | Op::SetIssueLabels => { | |
| 1165 | + | "Replace the labels of an issue or a pull request with these; an empty list takes them all off. The same rules as add_issue_labels. Returns its labels now." | |
| 1166 | + | } | |
| 1167 | + | Op::RemoveIssueLabels => { | |
| 1168 | + | "Take labels off an issue or a pull request: label for one, labels for several, or neither for all of them. The labels stay on the repository. Returns its labels now." | |
| 1169 | + | } | |
| 1170 | + | Op::ListMilestones => { | |
| 1171 | + | "A repository's milestones: open ones soonest due first (those without a due date after), then closed ones, most recently closed first. Each has its number, title, description, due_on (YYYY-MM-DD), state, and open_items and closed_items: its issues and pull requests, a merged pull request counting as closed." | |
| 1172 | + | } | |
| 1173 | + | Op::GetMilestone => "A milestone, with every issue and pull request in it, newest first.", | |
| 1174 | + | Op::CreateMilestone => { | |
| 1175 | + | "Create a milestone: a title, unique in the repository, at most 100 characters; a description in Markdown; and a due_on day (YYYY-MM-DD). Milestones are numbered from 1 in each repository, apart from issues. Needs the Triage role or higher." | |
| 1176 | + | } | |
| 1177 | + | Op::UpdateMilestone => { | |
| 1178 | + | "Change a milestone's title, description, due date or state (open or closed); only the fields given change, and due_on \"\" clears its due date. Needs the Triage role or higher." | |
| 1179 | + | } | |
| 1180 | + | Op::DeleteMilestone => { | |
| 1181 | + | "Delete a milestone. The issues and pull requests in it are in no milestone afterwards. Needs the Triage role or higher." | |
| 1182 | + | } | |
| 895 | 1183 | Op::AddComment => { | |
| 896 | 1184 | "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change." | |
| 897 | 1185 | } | |
| ⋯ | |||
| 899 | 1187 | "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened, or one g1t made for you (you are its requested_by)." | |
| 900 | 1188 | } | |
| 901 | 1189 | Op::ListPullRequests => { | |
| 902 | − | "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed." | |
| 1190 | + | "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed. Filter by a label's name, a milestone's number, or base, the branch they merge into." | |
| 903 | 1191 | } | |
| 904 | 1192 | Op::GetPullRequest => { | |
| 905 | − | "A pull request's status, head commit, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the default branch requires, as success, failure, pending or expected when nothing has reported it yet), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files." | |
| 1193 | + | "A pull request's status, base (the branch it merges into), head commit, labels, milestone, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the default branch requires, as success, failure, pending or expected when nothing has reported it yet; empty for a pull request into another branch, which the default branch's protection does not cover), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files. `pull.reviewers` lists the people asked to review it and `pull.team_reviewers` the teams, as `workspace/team`. `code_owners` is there when the branch it merges into has a CODEOWNERS file: its `path`, whether code owners' approval is `required`, `reviews` (one per section and rule that owns a changed file, with its `section`, `line`, `pattern`, `owners`, `files`, whether it is `optional`, the approvals `required`, who it was `approved_by` and `changes_requested_by`, and whether it is `satisfied`), what is still `missing`, and how many `errors` the file has (get_codeowners_errors lists them)." | |
| 906 | 1194 | } | |
| 907 | 1195 | Op::CreatePullRequest => { | |
| 908 | − | "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once." | |
| 1196 | + | "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once. It merges into the default branch unless base names another existing branch; leave base out unless you were asked for another." | |
| 1197 | + | } | |
| 1198 | + | Op::UpdatePullRequest => { | |
| 1199 | + | "Change an open pull request: base, the branch it merges into (an existing branch; needs the Write role or higher); its labels (replacing the set, as set_issue_labels does); its milestone (a number, or null or 0 for none; needs the Triage role); and assignees and reviewers (each replacing the set). Only the fields given change. Its author, or whoever asked g1t for it, may change it; anyone else needs the Triage role or higher. A new base is a pull.base_changed event: it leaves the merge queue, and whether it is behind, merges cleanly and has the checks it needs is worked out against the new base." | |
| 909 | 1200 | } | |
| 910 | 1201 | Op::RecordSession => { | |
| 911 | 1202 | "Append entries to a pull request's session: the prompt you were given, your reasoning, the tools you ran. This is how people later see why a change was made, so record as you work, not only at the end." | |
| ⋯ | |||
| 919 | 1210 | "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue." | |
| 920 | 1211 | } | |
| 921 | 1212 | Op::MergePullRequest => { | |
| 922 | − | "Land a pull request on the repository's main branch. Merging needs the Write role or higher, and only once it is marked ready and every check the default branch requires has passed on its head (see required_checks on get_pull_request); with ignore_checks, someone who may merge can bypass them where the repository allows it. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed." | |
| 1213 | + | "Land a pull request on its base, the branch it merges into (the default branch unless it names another). Merging needs the Write role or higher, and only once it is marked ready and, into the default branch, every check the default branch requires has passed on its head (see required_checks on get_pull_request); with ignore_checks, someone who may merge can bypass them where the repository allows it. Merging into the default branch resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded; merging into another branch leaves the issue open. Where the repository has a merge queue, a pull request into the default branch joins the queue instead of landing at once. If its base has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests into its default branch to be up to date refuses instead, so pull the base into its fork or branch, push, and merge again. Check status in the result to see whether it has landed." | |
| 923 | 1214 | } | |
| 924 | 1215 | Op::ListEvents => { | |
| 925 | 1216 | "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first." | |
| ⋯ | |||
| 1117 | 1408 | Op::ReorderPinnedProjects => { | |
| 1118 | 1409 | "Put your pins in a workspace in a new order: `projects` names every pinned project's slug, once, in the order you want them. Returns your pins, in order." | |
| 1119 | 1410 | } | |
| 1411 | + | Op::ListTeams => { | |
| 1412 | + | "A workspace's teams that you can see, yours first, then by name. A team is a group of the workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. A secret team is seen only by its own people and the workspace's owners. Each team has its `slug`, `name`, `description`, `visibility` (`visible` or `secret`), `parent`, whether its people are notified when it is mentioned (`notify`), its `review_assignment`, how many people, repositories and child teams it has (`members_count`, `repos_count`, `child_teams_count`), your own `viewer_role` in it, and whether you may change it (`can_manage`). `query` narrows them by name or slug. Members of the workspace only." | |
| 1413 | + | } | |
| 1414 | + | Op::GetTeam => { | |
| 1415 | + | "One team, by its slug, as list_teams describes it. A secret team is found only by its own people and the workspace's owners; anyone else is told it does not exist. Members of the workspace only." | |
| 1416 | + | } | |
| 1417 | + | Op::CreateTeam => { | |
| 1418 | + | "Create a team in a workspace. Any member may create one, and becomes its first maintainer; `members` adds more people by username, each a member of the workspace. `slug` is made from the name unless you give one: lowercase letters, digits and single hyphens. `visibility` is `visible` (the default: every member sees it) or `secret` (only its people and the owners). A team under a `parent` inherits the parent's roles on repositories, and a mention or review request for the parent reaches it too; giving it a parent needs an owner, or a maintainer of the parent. Secret teams cannot be nested. People only, signed in or with a personal access token. Returns the team." | |
| 1419 | + | } | |
| 1420 | + | Op::UpdateTeam => { | |
| 1421 | + | "Change a team's `name`, `slug`, `description`, `visibility`, `parent` (an empty string takes it out from under its parent), `notify` or `review_assignment`. Only the fields given change; give at least one. A new slug changes how it is mentioned, @workspace/slug. Owners of the workspace and the team's maintainers. People only. Returns the team as it is now." | |
| 1422 | + | } | |
| 1423 | + | Op::DeleteTeam => { | |
| 1424 | + | "Delete a team. Its child teams move up to its parent, and the roles it gave on repositories go with it: its people keep only what they have otherwise. Owners of the workspace and the team's maintainers. People only. Returns true." | |
| 1425 | + | } | |
| 1426 | + | Op::ListTeamMembers => { | |
| 1427 | + | "The people in a team, each with their `username`, `name`, `avatar` and `role` in it (`member` or `maintainer`). With `include_child_teams`, the people of its child teams are listed too, each with `via`, the child team they are in. Anyone who can see the team." | |
| 1428 | + | } | |
| 1429 | + | Op::SetTeamMember => { | |
| 1430 | + | "Add a member of the workspace to a team, or change their role in it: `member` (the default) or `maintainer`, who manages the team's people and settings. Someone who is not a member of the workspace must join it first. Owners of the workspace and the team's maintainers. People only. Returns the person as list_team_members lists them." | |
| 1431 | + | } | |
| 1432 | + | Op::RemoveTeamMember => { | |
| 1433 | + | "Take someone out of a team. They lose the roles the team gave them on repositories, unless they have them otherwise. Owners of the workspace and the team's maintainers; anyone may leave a team themselves. People only. Returns true." | |
| 1434 | + | } | |
| 1435 | + | Op::ListChildTeams => { | |
| 1436 | + | "The teams nested directly under a team, as list_teams describes them. Anyone who can see the team." | |
| 1437 | + | } | |
| 1438 | + | Op::ListTeamRepos => { | |
| 1439 | + | "The repositories a team has a role on: each one's `repo` (`workspace/name`), the team's `role` there (read, triage, write, maintain or admin), and `inherited_from`, the parent team it comes from when the team inherits it, or null for its own. Everyone in the team gets the role; where someone has a higher one otherwise, the higher one counts. Anyone who can see the team." | |
| 1440 | + | } | |
| 1441 | + | Op::SetTeamRepo => { | |
| 1442 | + | "Give a team a role on a repository in its workspace, or change it: read, triage, write, maintain or admin. Everyone in the team and in its child teams gets the role. Needs the Admin role on the repository. People only. Returns the repository as list_team_repos lists it." | |
| 1443 | + | } | |
| 1444 | + | Op::RemoveTeamRepo => { | |
| 1445 | + | "Take a team's role on a repository away. Its people keep only the roles they have otherwise. Needs the Admin role on the repository, or to be an owner or one of the team's maintainers. People only. Returns true." | |
| 1446 | + | } | |
| 1447 | + | Op::SetTeamReviewAssignment => { | |
| 1448 | + | "Choose what happens when a team is asked to review a pull request. Off, everyone in it is asked. On (`enabled`), g1t picks `count` people from it (1 to 10, never the pull request's author) and asks them, and the team stays shown as asked beside them: `round_robin` picks whoever this team asked least recently, `load_balance` whoever has the fewest pull requests waiting on their review. `skip_busy` leaves out anyone with `busy_at` or more waiting; `include_child_teams` also picks from its child teams' people; `excluded` lists usernames never picked; `notify_team` also tells the rest of the team. Fields left out keep their current value. Owners of the workspace and the team's maintainers. People only. Returns the team." | |
| 1449 | + | } | |
| 1450 | + | Op::ListUserTeams => { | |
| 1451 | + | "The teams someone is in within a workspace, as list_teams describes them, leaving out secret teams you cannot see. Members of the workspace only." | |
| 1452 | + | } | |
| 1453 | + | Op::RequestReviewers => { | |
| 1454 | + | "Ask more people or teams to review a pull request. `reviewers` are usernames, and may include `g1t` to ask a g1t agent; `team_reviewers` are teams, as `workspace/team` or the team's slug in the repository's workspace. They are added to whoever is asked already. Asking a team asks everyone in it, or with its review assignment on, the people it picks. Nobody is asked to review their own pull request, and a team must be one you can see. Whoever opened the pull request, or anyone with the Triage role or higher, while it is open. Returns the pull request, with `reviewers` and `team_reviewers` as they are now." | |
| 1455 | + | } | |
| 1456 | + | Op::RemoveRequestedReviewers => { | |
| 1457 | + | "Stop asking people or teams to review a pull request: `reviewers` by username and `team_reviewers` as `workspace/team` or the team's slug. Reviews they already gave stay. The same people may do this as may ask. Returns the pull request, with `reviewers` and `team_reviewers` as they are now." | |
| 1458 | + | } | |
| 1459 | + | Op::GetCodeownersErrors => { | |
| 1460 | + | "Check a repository's CODEOWNERS file as a linter would. g1t reads it from one branch (`ref`, the default branch unless you say): the first of `.g1t/CODEOWNERS`, `.github/CODEOWNERS`, `CODEOWNERS`, `docs/CODEOWNERS` and `.gitlab/CODEOWNERS` that exists. Returns its `path` (null when there is none), the `ref` read, its `size`, how many `rules` it has, its `sections`, and `errors`: each with its `line` (0 for the file as a whole), `kind`, the `token` at fault and a `message` saying how to fix it. `kind` is `too_large`, `negation`, `character_range`, `bad_pattern`, `bad_owner`, `bad_section`, `unknown_user`, `unknown_team`, `unknown_email`, `no_write_access` or `team_no_access`. Needs the Read role; a public repository's is open to anyone." | |
| 1461 | + | } | |
| 1462 | + | Op::Security(op) => op.description(), | |
| 1120 | 1463 | } | |
| 1121 | 1464 | } | |
| 1122 | 1465 | ||
| ⋯ | |||
| 1249 | 1592 | }), | |
| 1250 | 1593 | &["repo", "to"], | |
| 1251 | 1594 | ), | |
| 1252 | − | Op::GetRepo | Op::ListLabels => repo_only(), | |
| 1595 | + | Op::GetRepo | Op::ListLabels | Op::AddDefaultLabels => repo_only(), | |
| 1596 | + | Op::CreateLabel => object( | |
| 1597 | + | json!({ | |
| 1598 | + | "repo": repo_schema(), | |
| 1599 | + | "label": { "type": "string", "description": "Its name: lowercase, at most 50 characters, e.g. \"good first issue\"." }, | |
| 1600 | + | "color": { "type": "string", "description": "Six hex digits, with or without #, e.g. \"d73a4a\". Chosen from the name when left out." }, | |
| 1601 | + | "description": { "type": "string", "description": "What it means, at most 100 characters." }, | |
| 1602 | + | }), | |
| 1603 | + | &["repo", "label"], | |
| 1604 | + | ), | |
| 1605 | + | Op::UpdateLabel => object( | |
| 1606 | + | json!({ | |
| 1607 | + | "repo": repo_schema(), | |
| 1608 | + | "label": label_schema(), | |
| 1609 | + | "new_name": { "type": "string", "description": "Rename it, on everything that carries it." }, | |
| 1610 | + | "color": { "type": "string", "description": "Six hex digits." }, | |
| 1611 | + | "description": { "type": "string", "description": "An empty string clears it." }, | |
| 1612 | + | }), | |
| 1613 | + | &["repo", "label"], | |
| 1614 | + | ), | |
| 1615 | + | Op::DeleteLabel => object(json!({ "repo": repo_schema(), "label": label_schema() }), &["repo", "label"]), | |
| 1616 | + | Op::ListIssueLabels => just_numbered(), | |
| 1617 | + | Op::AddIssueLabels | Op::SetIssueLabels => object( | |
| 1618 | + | numbered(json!({ | |
| 1619 | + | "labels": { | |
| 1620 | + | "type": "array", | |
| 1621 | + | "items": { "type": "string" }, | |
| 1622 | + | "description": "Label names, e.g. [\"bug\", \"help wanted\"]. Names the repository does not have yet are created for someone with the Triage role.", | |
| 1623 | + | }, | |
| 1624 | + | })), | |
| 1625 | + | &["repo", "number", "labels"], | |
| 1626 | + | ), | |
| 1627 | + | Op::RemoveIssueLabels => object( | |
| 1628 | + | numbered(json!({ | |
| 1629 | + | "label": label_schema(), | |
| 1630 | + | "labels": { | |
| 1631 | + | "type": "array", | |
| 1632 | + | "items": { "type": "string" }, | |
| 1633 | + | "description": "Instead of label: several to take off. With neither, all of them.", | |
| 1634 | + | }, | |
| 1635 | + | })), | |
| 1636 | + | &["repo", "number"], | |
| 1637 | + | ), | |
| 1638 | + | Op::ListMilestones => object( | |
| 1639 | + | json!({ "repo": repo_schema(), "state": states }), | |
| 1640 | + | &["repo"], | |
| 1641 | + | ), | |
| 1642 | + | Op::GetMilestone | Op::DeleteMilestone => { | |
| 1643 | + | object(json!({ "repo": repo_schema(), "milestone": milestone_schema() }), &["repo", "milestone"]) | |
| 1644 | + | } | |
| 1645 | + | Op::CreateMilestone | Op::UpdateMilestone => { | |
| 1646 | + | let mut properties = json!({ | |
| 1647 | + | "repo": repo_schema(), | |
| 1648 | + | "title": { "type": "string", "description": "Unique in the repository, at most 100 characters." }, | |
| 1649 | + | "description": { "type": "string", "description": "Markdown." }, | |
| 1650 | + | "due_on": { "type": "string", "description": "The day it is due, YYYY-MM-DD. On update, \"\" clears it." }, | |
| 1651 | + | "state": states, | |
| 1652 | + | }); | |
| 1653 | + | if self == Op::UpdateMilestone { | |
| 1654 | + | properties["milestone"] = milestone_schema(); | |
| 1655 | + | object(properties, &["repo", "milestone"]) | |
| 1656 | + | } else { | |
| 1657 | + | object(properties, &["repo", "title"]) | |
| 1658 | + | } | |
| 1659 | + | } | |
| 1253 | 1660 | Op::UpdateRepo => object( | |
| 1254 | 1661 | json!({ | |
| 1255 | 1662 | "repo": repo_schema(), | |
| ⋯ | |||
| 1465 | 1872 | "type": "boolean", | |
| 1466 | 1873 | "description": "Ask a person before merging a g1t agent's change whose confidence is low: auto-merge and the merge queue leave it until a person approves it. On by default.", | |
| 1467 | 1874 | }, | |
| 1875 | + | "require_code_owner_review": { | |
| 1876 | + | "type": "boolean", | |
| 1877 | + | "description": "Refuse to merge until the code owners of every file a pull request changes, as the CODEOWNERS file of the branch it merges into names them, have approved it, as many as each section asks. Only people's approvals count, and g1t's only where the file names @g1t.", | |
| 1878 | + | }, | |
| 1468 | 1879 | }), | |
| 1469 | 1880 | &["repo"], | |
| 1470 | 1881 | ), | |
| ⋯ | |||
| 1489 | 1900 | "repo": repo_schema(), | |
| 1490 | 1901 | "state": states, | |
| 1491 | 1902 | "label": { "type": "string", "description": "Only issues carrying this label." }, | |
| 1903 | + | "milestone": { "type": "integer", "description": "Only issues in the milestone of this number." }, | |
| 1492 | 1904 | }), | |
| 1493 | 1905 | &["repo"], | |
| 1494 | 1906 | ), | |
| ⋯ | |||
| 1508 | 1920 | "labels": { | |
| 1509 | 1921 | "type": "array", | |
| 1510 | 1922 | "items": { "type": "string" }, | |
| 1511 | − | "description": "What kind of issue this is, e.g. \"bug\" or \"feature\". list_labels shows the labels in use; a new name creates a new label.", | |
| 1923 | + | "description": "What kind of issue this is, e.g. \"bug\" or \"enhancement\": the repository's labels, as list_labels gives them. A name it does not have yet is created for someone with the Triage role.", | |
| 1512 | 1924 | }, | |
| 1513 | 1925 | "checks": { | |
| 1514 | 1926 | "type": "array", | |
| ⋯ | |||
| 1516 | 1928 | "deprecated": true, | |
| 1517 | 1929 | "description": "Deprecated. Commands are added to the body under \"Definition of done\", and the response says so in deprecation. What must pass before a pull request merges is the default branch's required checks.", | |
| 1518 | 1930 | }, | |
| 1931 | + | "milestone": { "type": "integer", "description": "The number of the milestone to put it in. Needs the Triage role." }, | |
| 1519 | 1932 | }), | |
| 1520 | 1933 | &["repo", "title"], | |
| 1521 | 1934 | ), | |
| ⋯ | |||
| 1523 | 1936 | numbered(json!({ | |
| 1524 | 1937 | "title": { "type": "string" }, | |
| 1525 | 1938 | "body": { "type": "string" }, | |
| 1526 | − | "labels": { "type": "array", "items": { "type": "string" } }, | |
| 1939 | + | "labels": { | |
| 1940 | + | "type": "array", | |
| 1941 | + | "items": { "type": "string" }, | |
| 1942 | + | "description": "Replaces the whole set. Names the repository does not have yet are created for someone with the Triage role.", | |
| 1943 | + | }, | |
| 1944 | + | "milestone": { | |
| 1945 | + | "type": ["integer", "null"], | |
| 1946 | + | "description": "The number of the milestone to put it in; null or 0 takes it out. Needs the Triage role.", | |
| 1947 | + | }, | |
| 1527 | 1948 | "assignees": { | |
| 1528 | 1949 | "type": "array", | |
| 1529 | 1950 | "items": { "type": "string" }, | |
| ⋯ | |||
| 1630 | 2051 | })), | |
| 1631 | 2052 | &["repo", "number", "verdict"], | |
| 1632 | 2053 | ), | |
| 1633 | − | Op::ListPullRequests => { | |
| 1634 | − | object(json!({ "repo": repo_schema(), "state": states }), &["repo"]) | |
| 1635 | − | } | |
| 2054 | + | Op::ListPullRequests => object( | |
| 2055 | + | json!({ | |
| 2056 | + | "repo": repo_schema(), | |
| 2057 | + | "state": states, | |
| 2058 | + | "label": { "type": "string", "description": "Only pull requests carrying this label." }, | |
| 2059 | + | "milestone": { "type": "integer", "description": "Only pull requests in the milestone of this number." }, | |
| 2060 | + | "base": { "type": "string", "description": "Only pull requests into this branch." }, | |
| 2061 | + | }), | |
| 2062 | + | &["repo"], | |
| 2063 | + | ), | |
| 2064 | + | Op::UpdatePullRequest => object( | |
| 2065 | + | numbered(json!({ | |
| 2066 | + | "base": { | |
| 2067 | + | "type": "string", | |
| 2068 | + | "description": "The branch it merges into: an existing branch other than its own. Needs the Write role.", | |
| 2069 | + | }, | |
| 2070 | + | "labels": { | |
| 2071 | + | "type": "array", | |
| 2072 | + | "items": { "type": "string" }, | |
| 2073 | + | "description": "Replaces the whole set.", | |
| 2074 | + | }, | |
| 2075 | + | "milestone": { | |
| 2076 | + | "type": ["integer", "null"], | |
| 2077 | + | "description": "The number of the milestone to put it in; null or 0 takes it out. Needs the Triage role.", | |
| 2078 | + | }, | |
| 2079 | + | "assignees": { | |
| 2080 | + | "type": "array", | |
| 2081 | + | "items": { "type": "string" }, | |
| 2082 | + | "description": "Usernames; replaces the whole set.", | |
| 2083 | + | }, | |
| 2084 | + | "reviewers": { | |
| 2085 | + | "type": "array", | |
| 2086 | + | "items": { "type": "string" }, | |
| 2087 | + | "description": "Usernames whose review is asked for, and g1t for a g1t agent's; replaces the whole set.", | |
| 2088 | + | }, | |
| 2089 | + | })), | |
| 2090 | + | &["repo", "number"], | |
| 2091 | + | ), | |
| 1636 | 2092 | Op::CreatePullRequest => object( | |
| 1637 | 2093 | json!({ | |
| 1638 | 2094 | "repo": repo_schema(), | |
| ⋯ | |||
| 1653 | 2109 | "type": "string", | |
| 1654 | 2110 | "description": "A label for the agent doing the work, e.g. \"claude-code\".", | |
| 1655 | 2111 | }, | |
| 2112 | + | "base": { | |
| 2113 | + | "type": "string", | |
| 2114 | + | "description": "The branch it merges into: the default branch when left out. Name another existing branch only when asked to.", | |
| 2115 | + | }, | |
| 1656 | 2116 | }), | |
| 1657 | 2117 | &["repo"], | |
| 1658 | 2118 | ), | |
| ⋯ | |||
| 2134 | 2594 | }), | |
| 2135 | 2595 | &["workspace", "projects"], | |
| 2136 | 2596 | ), | |
| 2597 | + | Op::ListTeams => object( | |
| 2598 | + | json!({ | |
| 2599 | + | "workspace": workspace_schema(), | |
| 2600 | + | "query": { "type": "string", "description": "Only teams whose name or slug has these letters." }, | |
| 2601 | + | }), | |
| 2602 | + | &["workspace"], | |
| 2603 | + | ), | |
| 2604 | + | Op::GetTeam | Op::DeleteTeam | Op::ListChildTeams | Op::ListTeamRepos => { | |
| 2605 | + | object(team_target(json!({})), &["workspace", "team"]) | |
| 2606 | + | } | |
| 2607 | + | Op::CreateTeam => object( | |
| 2608 | + | json!({ | |
| 2609 | + | "workspace": workspace_schema(), | |
| 2610 | + | "name": { "type": "string", "description": "Its display name, at most 80 characters." }, | |
| 2611 | + | "slug": { | |
| 2612 | + | "type": "string", | |
| 2613 | + | "description": "Its name in mentions and URLs: lowercase letters, digits and single hyphens. Made from the name if left out.", | |
| 2614 | + | }, | |
| 2615 | + | "description": { "type": "string", "description": "What it is for, at most 280 characters." }, | |
| 2616 | + | "visibility": team_visibility_schema(), | |
| 2617 | + | "parent": { "type": "string", "description": "The slug of the team to nest it under." }, | |
| 2618 | + | "notify": { | |
| 2619 | + | "type": "boolean", | |
| 2620 | + | "description": "Whether its people are notified when it is mentioned. On unless you say.", | |
| 2621 | + | }, | |
| 2622 | + | "members": { | |
| 2623 | + | "type": "array", | |
| 2624 | + | "items": { "type": "string" }, | |
| 2625 | + | "description": "Usernames of members of the workspace to add, besides you.", | |
| 2626 | + | }, | |
| 2627 | + | }), | |
| 2628 | + | &["workspace", "name"], | |
| 2629 | + | ), | |
| 2630 | + | Op::UpdateTeam => object( | |
| 2631 | + | team_target(json!({ | |
| 2632 | + | "name": { "type": "string", "description": "A new display name." }, | |
| 2633 | + | "slug": { "type": "string", "description": "A new slug, which changes its mention." }, | |
| 2634 | + | "description": { "type": "string", "description": "A new description; an empty string clears it." }, | |
| 2635 | + | "visibility": team_visibility_schema(), | |
| 2636 | + | "parent": { | |
| 2637 | + | "type": "string", | |
| 2638 | + | "description": "The slug of the team to nest it under; an empty string for none.", | |
| 2639 | + | }, | |
| 2640 | + | "notify": { "type": "boolean", "description": "Whether its people are notified when it is mentioned." }, | |
| 2641 | + | "review_assignment": { | |
| 2642 | + | "type": "object", | |
| 2643 | + | "properties": review_assignment_properties(), | |
| 2644 | + | "description": "What happens when it is asked to review; fields left out keep their value. See set_team_review_assignment.", | |
| 2645 | + | }, | |
| 2646 | + | })), | |
| 2647 | + | &["workspace", "team"], | |
| 2648 | + | ), | |
| 2649 | + | Op::ListTeamMembers => object( | |
| 2650 | + | team_target(json!({ "include_child_teams": include_child_teams_schema() })), | |
| 2651 | + | &["workspace", "team"], | |
| 2652 | + | ), | |
| 2653 | + | Op::SetTeamMember => object( | |
| 2654 | + | team_target(json!({ "username": username_schema(), "role": team_role_schema() })), | |
| 2655 | + | &["workspace", "team", "username"], | |
| 2656 | + | ), | |
| 2657 | + | Op::RemoveTeamMember => object( | |
| 2658 | + | team_target(json!({ "username": username_schema() })), | |
| 2659 | + | &["workspace", "team", "username"], | |
| 2660 | + | ), | |
| 2661 | + | Op::SetTeamRepo | Op::RemoveTeamRepo => { | |
| 2662 | + | let mut properties = team_target(json!({ | |
| 2663 | + | "repo": { | |
| 2664 | + | "type": "string", | |
| 2665 | + | "description": "The repository, in the team's workspace: its name, or \"owner/name\".", | |
| 2666 | + | }, | |
| 2667 | + | })); | |
| 2668 | + | let mut required = vec!["workspace", "team", "repo"]; | |
| 2669 | + | if self == Op::SetTeamRepo { | |
| 2670 | + | properties["role"] = role_schema(); | |
| 2671 | + | required.push("role"); | |
| 2672 | + | } | |
| 2673 | + | object(properties, &required) | |
| 2674 | + | } | |
| 2675 | + | Op::SetTeamReviewAssignment => object(team_target(review_assignment_properties()), &["workspace", "team"]), | |
| 2676 | + | Op::ListUserTeams => object( | |
| 2677 | + | json!({ "workspace": workspace_schema(), "username": username_schema() }), | |
| 2678 | + | &["workspace", "username"], | |
| 2679 | + | ), | |
| 2680 | + | Op::RequestReviewers | Op::RemoveRequestedReviewers => { | |
| 2681 | + | object(requested_reviewers_properties(), &["repo", "number"]) | |
| 2682 | + | } | |
| 2683 | + | Op::GetCodeownersErrors => object( | |
| 2684 | + | json!({ | |
| 2685 | + | "repo": repo_schema(), | |
| 2686 | + | "ref": { | |
| 2687 | + | "type": "string", | |
| 2688 | + | "description": "The branch, tag or commit to read the file from. The default branch if left out.", | |
| 2689 | + | }, | |
| 2690 | + | }), | |
| 2691 | + | &["repo"], | |
| 2692 | + | ), | |
| 2693 | + | Op::Security(op) => op.input(), | |
| 2137 | 2694 | } | |
| 2138 | 2695 | } | |
| 2139 | 2696 | ||
| ⋯ | |||
| 2147 | 2704 | | Op::ListIssues | |
| 2148 | 2705 | | Op::GetIssue | |
| 2149 | 2706 | | Op::ListLabels | |
| 2707 | + | | Op::ListIssueLabels | |
| 2708 | + | | Op::ListMilestones | |
| 2709 | + | | Op::GetMilestone | |
| 2150 | 2710 | | Op::ListPullRequests | |
| 2151 | 2711 | | Op::GetPullRequest | |
| 2152 | 2712 | | Op::ReadSession | |
| ⋯ | |||
| 2155 | 2715 | | Op::GetRepoSettings | |
| 2156 | 2716 | | Op::ListCheckNames | |
| 2157 | 2717 | | Op::GetMergeQueue | |
| 2718 | + | | Op::GetCodeownersErrors | |
| 2158 | 2719 | ) | |
| 2159 | 2720 | } | |
| 2160 | 2721 | ||
| ⋯ | |||
| 2165 | 2726 | ||
| 2166 | 2727 | /// Whether the operation is about one repository, named by `repo`. | |
| 2167 | 2728 | pub(crate) fn needs_repo(self) -> bool { | |
| 2729 | + | if let Op::Security(op) = self { | |
| 2730 | + | return op.needs_repo(); | |
| 2731 | + | } | |
| 2168 | 2732 | !matches!( | |
| 2169 | 2733 | self, | |
| 2170 | 2734 | Op::Whoami | |
| ⋯ | |||
| 2235 | 2799 | | Op::PinProject | |
| 2236 | 2800 | | Op::UnpinProject | |
| 2237 | 2801 | | Op::ReorderPinnedProjects | |
| 2802 | + | | Op::ListTeams | |
| 2803 | + | | Op::GetTeam | |
| 2804 | + | | Op::CreateTeam | |
| 2805 | + | | Op::UpdateTeam | |
| 2806 | + | | Op::DeleteTeam | |
| 2807 | + | | Op::ListTeamMembers | |
| 2808 | + | | Op::SetTeamMember | |
| 2809 | + | | Op::RemoveTeamMember | |
| 2810 | + | | Op::ListChildTeams | |
| 2811 | + | | Op::ListTeamRepos | |
| 2812 | + | | Op::SetTeamRepo | |
| 2813 | + | | Op::RemoveTeamRepo | |
| 2814 | + | | Op::SetTeamReviewAssignment | |
| 2815 | + | | Op::ListUserTeams | |
| 2238 | 2816 | ) | |
| 2239 | 2817 | } | |
| 2240 | 2818 | ||
| ⋯ | |||
| 2902 | 3480 | max_revisions: integer(input, "max_revisions").unwrap_or(current.max_revisions), | |
| 2903 | 3481 | merge_queue: flag("merge_queue", current.merge_queue), | |
| 2904 | 3482 | hold_low_confidence: flag("hold_low_confidence", current.hold_low_confidence), | |
| 3483 | + | require_code_owner_review: flag( | |
| 3484 | + | "require_code_owner_review", | |
| 3485 | + | current.require_code_owner_review, | |
| 3486 | + | ), | |
| 2905 | 3487 | ..current | |
| 2906 | 3488 | }; | |
| 2907 | 3489 | pass( | |
| ⋯ | |||
| 2948 | 3530 | viewer: viewer.clone(), | |
| 2949 | 3531 | state: state(input), | |
| 2950 | 3532 | label: optional_text(input, "label"), | |
| 3533 | + | milestone: integer(input, "milestone"), | |
| 2951 | 3534 | }, | |
| 2952 | 3535 | ) | |
| 2953 | 3536 | .await | |
| ⋯ | |||
| 2965 | 3548 | body: text(input, "body"), | |
| 2966 | 3549 | labels: strings(input, "labels").unwrap_or_default(), | |
| 2967 | 3550 | checks: checks.clone(), | |
| 3551 | + | milestone: integer(input, "milestone"), | |
| 2968 | 3552 | }, | |
| 2969 | 3553 | ) | |
| 2970 | 3554 | .await?; | |
| ⋯ | |||
| 2982 | 3566 | body: input["body"].as_str().map(str::to_owned), | |
| 2983 | 3567 | labels: strings(input, "labels"), | |
| 2984 | 3568 | assignees: strings(input, "assignees"), | |
| 3569 | + | milestone: milestone_input(input), | |
| 2985 | 3570 | }, | |
| 2986 | 3571 | ) | |
| 2987 | 3572 | .await | |
| ⋯ | |||
| 3073 | 3658 | .await | |
| 3074 | 3659 | } | |
| 3075 | 3660 | Op::ListLabels => pass(work, "list_labels", &view()).await, | |
| 3661 | + | Op::CreateLabel | Op::UpdateLabel => { | |
| 3662 | + | let creating = self == Op::CreateLabel; | |
| 3663 | + | pass( | |
| 3664 | + | work, | |
| 3665 | + | "save_label", | |
| 3666 | + | &SaveLabelArgs { | |
| 3667 | + | actor: actor(), | |
| 3668 | + | repo, | |
| 3669 | + | name: (!creating).then(|| text(input, "label")), | |
| 3670 | + | new_name: if creating { Some(text(input, "label")) } else { optional_text(input, "new_name") }, | |
| 3671 | + | color: optional_text(input, "color"), | |
| 3672 | + | description: input["description"].as_str().map(str::to_owned), | |
| 3673 | + | }, | |
| 3674 | + | ) | |
| 3675 | + | .await | |
| 3676 | + | } | |
| 3677 | + | Op::DeleteLabel => { | |
| 3678 | + | pass(work, "delete_label", &DeleteLabelArgs { actor: actor(), repo, name: text(input, "label") }).await | |
| 3679 | + | } | |
| 3680 | + | Op::AddDefaultLabels => pass(work, "add_default_labels", &RepoActorArgs { actor: actor(), repo }).await, | |
| 3681 | + | Op::ListIssueLabels => { | |
| 3682 | + | // The item's names, with each label's color and description. | |
| 3683 | + | let labels = call::<_, Vec<Label>>(work, "list_labels", &view()).await?; | |
| 3684 | + | let item = call::<_, IssueDetail>(work, "get_issue", &view()).await?; | |
| 3685 | + | let names = match item { | |
| 3686 | + | Outcome::Ok(detail) => detail.issue.labels, | |
| 3687 | + | Outcome::Fail(_) => match call::<_, PullDetail>(work, "get_pull", &view()).await? { | |
| 3688 | + | Outcome::Ok(detail) => detail.pull.labels, | |
| 3689 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 3690 | + | }, | |
| 3691 | + | }; | |
| 3692 | + | let labels = match labels { | |
| 3693 | + | Outcome::Ok(labels) => labels, | |
| 3694 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 3695 | + | }; | |
| 3696 | + | ok(&names | |
| 3697 | + | .iter() | |
| 3698 | + | .filter_map(|name| labels.iter().find(|label| label.name == *name)) | |
| 3699 | + | .collect::<Vec<_>>()) | |
| 3700 | + | } | |
| 3701 | + | Op::AddIssueLabels | Op::SetIssueLabels | Op::RemoveIssueLabels => { | |
| 3702 | + | let (change, labels) = match self { | |
| 3703 | + | Op::AddIssueLabels => (LabelChange::Add, strings(input, "labels").unwrap_or_default()), | |
| 3704 | + | Op::SetIssueLabels => (LabelChange::Set, strings(input, "labels").unwrap_or_default()), | |
| 3705 | + | // One, several, or with neither, all of them. | |
| 3706 | + | _ => match (optional_text(input, "label"), strings(input, "labels")) { | |
| 3707 | + | (Some(one), _) => (LabelChange::Remove, vec![one]), | |
| 3708 | + | (None, Some(several)) => (LabelChange::Remove, several), | |
| 3709 | + | (None, None) => (LabelChange::Set, Vec::new()), | |
| 3710 | + | }, | |
| 3711 | + | }; | |
| 3712 | + | pass(work, "set_labels", &SetLabelsArgs { actor: actor(), repo, number, labels, change }).await | |
| 3713 | + | } | |
| 3714 | + | Op::ListMilestones => { | |
| 3715 | + | pass(work, "list_milestones", &ListMilestonesArgs { repo, viewer: viewer.clone(), state: state(input) }).await | |
| 3716 | + | } | |
| 3717 | + | Op::GetMilestone => { | |
| 3718 | + | let asked = ViewArgs { number: integer(input, "milestone").unwrap_or_default(), ..view() }; | |
| 3719 | + | pass(work, "get_milestone", &asked).await | |
| 3720 | + | } | |
| 3721 | + | Op::CreateMilestone | Op::UpdateMilestone => { | |
| 3722 | + | pass( | |
| 3723 | + | work, | |
| 3724 | + | "save_milestone", | |
| 3725 | + | &SaveMilestoneArgs { | |
| 3726 | + | actor: actor(), | |
| 3727 | + | repo, | |
| 3728 | + | number: (self == Op::UpdateMilestone).then(|| integer(input, "milestone").unwrap_or_default()), | |
| 3729 | + | title: input["title"].as_str().map(str::to_owned), | |
| 3730 | + | description: input["description"].as_str().map(str::to_owned), | |
| 3731 | + | due_on: input["due_on"].as_str().map(str::to_owned), | |
| 3732 | + | state: state(input), | |
| 3733 | + | }, | |
| 3734 | + | ) | |
| 3735 | + | .await | |
| 3736 | + | } | |
| 3737 | + | Op::DeleteMilestone => { | |
| 3738 | + | pass( | |
| 3739 | + | work, | |
| 3740 | + | "delete_milestone", | |
| 3741 | + | &DeleteMilestoneArgs { actor: actor(), repo, number: integer(input, "milestone").unwrap_or_default() }, | |
| 3742 | + | ) | |
| 3743 | + | .await | |
| 3744 | + | } | |
| 3745 | + | Op::UpdatePullRequest => { | |
| 3746 | + | pass( | |
| 3747 | + | work, | |
| 3748 | + | "update_pull", | |
| 3749 | + | &UpdatePullArgs { | |
| 3750 | + | actor: actor(), | |
| 3751 | + | repo, | |
| 3752 | + | number, | |
| 3753 | + | assignees: strings(input, "assignees"), | |
| 3754 | + | reviewers: strings(input, "reviewers"), | |
| 3755 | + | labels: strings(input, "labels"), | |
| 3756 | + | milestone: milestone_input(input), | |
| 3757 | + | base: optional_text(input, "base"), | |
| 3758 | + | }, | |
| 3759 | + | ) | |
| 3760 | + | .await | |
| 3761 | + | } | |
| 3076 | 3762 | Op::AddComment | Op::ReviewPullRequest => { | |
| 3077 | 3763 | let verdict = match (self, input["verdict"].as_str()) { | |
| 3078 | 3764 | (Op::AddComment, _) => None, | |
| ⋯ | |||
| 3108 | 3794 | repo, | |
| 3109 | 3795 | viewer: viewer.clone(), | |
| 3110 | 3796 | state: state(input), | |
| 3797 | + | label: optional_text(input, "label"), | |
| 3798 | + | milestone: integer(input, "milestone"), | |
| 3799 | + | base: optional_text(input, "base"), | |
| 3111 | 3800 | }, | |
| 3112 | 3801 | ) | |
| 3113 | 3802 | .await | |
| ⋯ | |||
| 3127 | 3816 | branch: optional_text(input, "branch"), | |
| 3128 | 3817 | agent: optional_text(input, "agent").unwrap_or_else(|| "agent".into()), | |
| 3129 | 3818 | runtime: Runtime::External, | |
| 3819 | + | base: optional_text(input, "base"), | |
| 3130 | 3820 | }, | |
| 3131 | 3821 | ) | |
| 3132 | 3822 | .await?; | |
| ⋯ | |||
| 3665 | 4355 | .await?; | |
| 3666 | 4356 | changed_alert(changed) | |
| 3667 | 4357 | } | |
| 4358 | + | // Teams: identity decides who may see and change each, and | |
| 4359 | + | // refuses every token but a person's for changes. See | |
| 4360 | + | // g1t_contracts::teams. | |
| 4361 | + | Op::ListTeams => { | |
| 4362 | + | pass( | |
| 4363 | + | identity, | |
| 4364 | + | "list_teams", | |
| 4365 | + | &ListTeamsArgs { viewer: viewer.clone(), workspace: workspace(), query: optional_text(input, "query") }, | |
| 4366 | + | ) | |
| 4367 | + | .await | |
| 4368 | + | } | |
| 4369 | + | Op::GetTeam | Op::ListChildTeams | Op::ListTeamRepos | Op::ListTeamMembers => { | |
| 4370 | + | let method = match self { | |
| 4371 | + | Op::GetTeam => "get_team", | |
| 4372 | + | Op::ListChildTeams => "child_teams", | |
| 4373 | + | Op::ListTeamRepos => "team_repos", | |
| 4374 | + | _ => "team_members", | |
| 4375 | + | }; | |
| 4376 | + | pass( | |
| 4377 | + | identity, | |
| 4378 | + | method, | |
| 4379 | + | &TeamArgs { | |
| 4380 | + | viewer: viewer.clone(), | |
| 4381 | + | workspace: workspace(), | |
| 4382 | + | team: team_slug(input), | |
| 4383 | + | include_child_teams: self == Op::ListTeamMembers && yes(input, "include_child_teams") == Some(true), | |
| 4384 | + | }, | |
| 4385 | + | ) | |
| 4386 | + | .await | |
| 4387 | + | } | |
| 4388 | + | Op::CreateTeam => { | |
| 4389 | + | let visibility = match team_visibility(input) { | |
| 4390 | + | Ok(visibility) => visibility, | |
| 4391 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 4392 | + | }; | |
| 4393 | + | pass( | |
| 4394 | + | identity, | |
| 4395 | + | "create_team", | |
| 4396 | + | &CreateTeamArgs { | |
| 4397 | + | actor: actor(), | |
| 4398 | + | workspace: workspace(), | |
| 4399 | + | name: text(input, "name").trim().to_owned(), | |
| 4400 | + | slug: optional_text(input, "slug"), | |
| 4401 | + | description: optional_text(input, "description"), | |
| 4402 | + | visibility, | |
| 4403 | + | parent: optional_text(input, "parent"), | |
| 4404 | + | notify: yes(input, "notify"), | |
| 4405 | + | members: strings(input, "members").unwrap_or_default(), | |
| 4406 | + | surface: Some(services.audit.surface), | |
| 4407 | + | }, | |
| 4408 | + | ) | |
| 4409 | + | .await | |
| 4410 | + | } | |
| 4411 | + | Op::UpdateTeam | Op::SetTeamReviewAssignment => { | |
| 4412 | + | let visibility = match team_visibility(input) { | |
| 4413 | + | Ok(visibility) if self == Op::UpdateTeam => visibility, | |
| 4414 | + | Ok(_) => None, | |
| 4415 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 4416 | + | }; | |
| 4417 | + | // The review assignment's fields: in `review_assignment` to | |
| 4418 | + | // update a team, or at the top level to set it. | |
| 4419 | + | let given = match self { | |
| 4420 | + | Op::UpdateTeam => input.get("review_assignment").filter(|value| !value.is_null()), | |
| 4421 | + | _ => Some(input), | |
| 4422 | + | }; | |
| 4423 | + | if given.is_some_and(|given| !given.is_object()) { | |
| 4424 | + | return failed(FailureCode::Invalid, "review_assignment is an object, such as {\"enabled\": true, \"count\": 2}."); | |
| 4425 | + | } | |
| 4426 | + | let review = match given { | |
| 4427 | + | None => None, | |
| 4428 | + | Some(given) => { | |
| 4429 | + | if !REVIEW_ASSIGNMENT_FIELDS.iter().any(|key| given.get(*key).is_some_and(|value| !value.is_null())) { | |
| 4430 | + | return failed( | |
| 4431 | + | FailureCode::Invalid, | |
| 4432 | + | &format!("Give the review assignment to change: {}.", REVIEW_ASSIGNMENT_FIELDS.join(", ")), | |
| 4433 | + | ); | |
| 4434 | + | } | |
| 4435 | + | // What is not given stays as it is. | |
| 4436 | + | let current: Outcome<Team> = call( | |
| 4437 | + | identity, | |
| 4438 | + | "get_team", | |
| 4439 | + | &TeamArgs { viewer: viewer.clone(), workspace: workspace(), team: team_slug(input), include_child_teams: false }, | |
| 4440 | + | ) | |
| 4441 | + | .await?; | |
| 4442 | + | let current = match current { | |
| 4443 | + | Outcome::Ok(team) => team.review_assignment, | |
| 4444 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 4445 | + | }; | |
| 4446 | + | match review_assignment(given, current) { | |
| 4447 | + | Ok(review) => Some(review), | |
| 4448 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 4449 | + | } | |
| 4450 | + | } | |
| 4451 | + | }; | |
| 4452 | + | let words = |key: &str| match self { | |
| 4453 | + | Op::UpdateTeam => input[key].as_str().map(str::to_owned), | |
| 4454 | + | _ => None, | |
| 4455 | + | }; | |
| 4456 | + | let args = UpdateTeamArgs { | |
| 4457 | + | actor: actor(), | |
| 4458 | + | workspace: workspace(), | |
| 4459 | + | team: team_slug(input), | |
| 4460 | + | name: words("name"), | |
| 4461 | + | slug: words("slug"), | |
| 4462 | + | description: words("description"), | |
| 4463 | + | visibility, | |
| 4464 | + | parent: words("parent"), | |
| 4465 | + | notify: if self == Op::UpdateTeam { yes(input, "notify") } else { None }, | |
| 4466 | + | review_assignment: review, | |
| 4467 | + | surface: Some(services.audit.surface), | |
| 4468 | + | }; | |
| 4469 | + | if args.name.is_none() | |
| 4470 | + | && args.slug.is_none() | |
| 4471 | + | && args.description.is_none() | |
| 4472 | + | && args.visibility.is_none() | |
| 4473 | + | && args.parent.is_none() | |
| 4474 | + | && args.notify.is_none() | |
| 4475 | + | && args.review_assignment.is_none() | |
| 4476 | + | { | |
| 4477 | + | return failed( | |
| 4478 | + | FailureCode::Invalid, | |
| 4479 | + | "Give name, slug, description, visibility, parent, notify or review_assignment to change.", | |
| 4480 | + | ); | |
| 4481 | + | } | |
| 4482 | + | pass(identity, "update_team", &args).await | |
| 4483 | + | } | |
| 4484 | + | Op::DeleteTeam => { | |
| 4485 | + | pass( | |
| 4486 | + | identity, | |
| 4487 | + | "delete_team", | |
| 4488 | + | &DeleteTeamArgs { | |
| 4489 | + | actor: actor(), | |
| 4490 | + | workspace: workspace(), | |
| 4491 | + | team: team_slug(input), | |
| 4492 | + | surface: Some(services.audit.surface), | |
| 4493 | + | }, | |
| 4494 | + | ) | |
| 4495 | + | .await | |
| 4496 | + | } | |
| 4497 | + | Op::SetTeamMember => { | |
| 4498 | + | let role = match team_role(input) { | |
| 4499 | + | Ok(role) => role, | |
| 4500 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 4501 | + | }; | |
| 4502 | + | pass( | |
| 4503 | + | identity, | |
| 4504 | + | "set_team_member", | |
| 4505 | + | &SetTeamMemberArgs { | |
| 4506 | + | actor: actor(), | |
| 4507 | + | workspace: workspace(), | |
| 4508 | + | team: team_slug(input), | |
| 4509 | + | username: text(input, "username").trim().trim_start_matches('@').to_owned(), | |
| 4510 | + | role, | |
| 4511 | + | surface: Some(services.audit.surface), | |
| 4512 | + | }, | |
| 4513 | + | ) | |
| 4514 | + | .await | |
| 4515 | + | } | |
| 4516 | + | Op::RemoveTeamMember => { | |
| 4517 | + | pass( | |
| 4518 | + | identity, | |
| 4519 | + | "remove_team_member", | |
| 4520 | + | &RemoveTeamMemberArgs { | |
| 4521 | + | actor: actor(), | |
| 4522 | + | workspace: workspace(), | |
| 4523 | + | team: team_slug(input), | |
| 4524 | + | username: text(input, "username").trim().trim_start_matches('@').to_owned(), | |
| 4525 | + | surface: Some(services.audit.surface), | |
| 4526 | + | }, | |
| 4527 | + | ) | |
| 4528 | + | .await | |
| 4529 | + | } | |
| 4530 | + | Op::SetTeamRepo | Op::RemoveTeamRepo => { | |
| 4531 | + | let Some(path) = team_repo(input, &workspace()) else { | |
| 4532 | + | return failed( | |
| 4533 | + | FailureCode::Invalid, | |
| 4534 | + | "Give the repository: its name in the team's workspace, or \"owner/name\".", | |
| 4535 | + | ); | |
| 4536 | + | }; | |
| 4537 | + | if self == Op::RemoveTeamRepo { | |
| 4538 | + | return pass( | |
| 4539 | + | identity, | |
| 4540 | + | "remove_team_repo", | |
| 4541 | + | &RemoveTeamRepoArgs { | |
| 4542 | + | actor: actor(), | |
| 4543 | + | workspace: workspace(), | |
| 4544 | + | team: team_slug(input), | |
| 4545 | + | repo: path, | |
| 4546 | + | surface: Some(services.audit.surface), | |
| 4547 | + | }, | |
| 4548 | + | ) | |
| 4549 | + | .await; | |
| 4550 | + | } | |
| 4551 | + | let Some(role) = repo_role(input) else { | |
| 4552 | + | return failed(FailureCode::Invalid, ROLE_NEEDED); | |
| 4553 | + | }; | |
| 4554 | + | pass( | |
| 4555 | + | identity, | |
| 4556 | + | "set_team_repo", | |
| 4557 | + | &SetTeamRepoArgs { | |
| 4558 | + | actor: actor(), | |
| 4559 | + | workspace: workspace(), | |
| 4560 | + | team: team_slug(input), | |
| 4561 | + | repo: path, | |
| 4562 | + | role, | |
| 4563 | + | surface: Some(services.audit.surface), | |
| 4564 | + | }, | |
| 4565 | + | ) | |
| 4566 | + | .await | |
| 4567 | + | } | |
| 4568 | + | Op::ListUserTeams => { | |
| 4569 | + | pass( | |
| 4570 | + | identity, | |
| 4571 | + | "user_teams", | |
| 4572 | + | &UserTeamsArgs { | |
| 4573 | + | viewer: viewer.clone(), | |
| 4574 | + | workspace: workspace(), | |
| 4575 | + | username: text(input, "username").trim().trim_start_matches('@').to_owned(), | |
| 4576 | + | }, | |
| 4577 | + | ) | |
| 4578 | + | .await | |
| 4579 | + | } | |
| 4580 | + | // Who is asked to review: the whole list, people and teams, | |
| 4581 | + | // replaces who is asked, so read it and change it. | |
| 4582 | + | Op::RequestReviewers | Op::RemoveRequestedReviewers => { | |
| 4583 | + | let (people, teams) = reviewer_names(input, &repo.namespace); | |
| 4584 | + | if people.is_empty() && teams.is_empty() { | |
| 4585 | + | return failed( | |
| 4586 | + | FailureCode::Invalid, | |
| 4587 | + | "Give reviewers (usernames) or team_reviewers (\"workspace/team\").", | |
| 4588 | + | ); | |
| 4589 | + | } | |
| 4590 | + | let found: Outcome<PullDetail> = call(work, "get_pull", &view()).await?; | |
| 4591 | + | let pull = match found { | |
| 4592 | + | Outcome::Ok(detail) => detail.pull, | |
| 4593 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 4594 | + | }; | |
| 4595 | + | let reviewers = reviewers_after( | |
| 4596 | + | &pull.reviewers, | |
| 4597 | + | &pull.team_reviewers, | |
| 4598 | + | &people, | |
| 4599 | + | &teams, | |
| 4600 | + | self == Op::RequestReviewers, | |
| 4601 | + | ); | |
| 4602 | + | pass( | |
| 4603 | + | work, | |
| 4604 | + | "update_pull", | |
| 4605 | + | &UpdatePullArgs { actor: actor(), repo: repo.clone(), number, assignees: None, reviewers: Some(reviewers), labels: None, milestone: None, base: None }, | |
| 4606 | + | ) | |
| 4607 | + | .await | |
| 4608 | + | } | |
| 4609 | + | Op::GetCodeownersErrors => { | |
| 4610 | + | pass( | |
| 4611 | + | work, | |
| 4612 | + | "codeowners_errors", | |
| 4613 | + | &CodeOwnersErrorsArgs { viewer: viewer.clone(), repo, git_ref: optional_text(input, "ref") }, | |
| 4614 | + | ) | |
| 4615 | + | .await | |
| 4616 | + | } | |
| 3668 | 4617 | // A person's own inbox: the events service keeps it. | |
| 3669 | 4618 | Op::ListNotifications | |
| 3670 | 4619 | | Op::MarkNotificationsRead | |
| ⋯ | |||
| 3684 | 4633 | Op::ListPinnedProjects | Op::PinProject | Op::UnpinProject | Op::ReorderPinnedProjects => { | |
| 3685 | 4634 | crate::pins::run(self, services, viewer, input).await | |
| 3686 | 4635 | } | |
| 4636 | + | // The security suite: the security service decides, this gives | |
| 4637 | + | // each answer its public shape. | |
| 4638 | + | Op::Security(op) => crate::security::run(op, services, viewer, input).await, | |
| 3687 | 4639 | Op::ReopenSecurityAlert => { | |
| 3688 | 4640 | let changed: Outcome<AlertChange> = call( | |
| 3689 | 4641 | &services.security, | |
| ⋯ | |||
| 3757 | 4709 | input["role"].as_str().and_then(RepoRole::parse) | |
| 3758 | 4710 | } | |
| 3759 | 4711 | ||
| 4712 | + | /// A yes or no, given as a boolean or, in a URL, as text. | |
| 4713 | + | fn yes(input: &Value, key: &str) -> Option<bool> { | |
| 4714 | + | match &input[key] { | |
| 4715 | + | Value::Bool(value) => Some(*value), | |
| 4716 | + | Value::String(text) => match text.trim().to_ascii_lowercase().as_str() { | |
| 4717 | + | "true" | "1" | "yes" => Some(true), | |
| 4718 | + | "false" | "0" | "no" => Some(false), | |
| 4719 | + | _ => None, | |
| 4720 | + | }, | |
| 4721 | + | _ => None, | |
| 4722 | + | } | |
| 4723 | + | } | |
| 4724 | + | ||
| 4725 | + | /// The team named by `team`, by its slug. | |
| 4726 | + | fn team_slug(input: &Value) -> String { | |
| 4727 | + | text(input, "team").trim().trim_start_matches('@').to_lowercase() | |
| 4728 | + | } | |
| 4729 | + | ||
| 4730 | + | /// `visibility`, when it is given. | |
| 4731 | + | fn team_visibility(input: &Value) -> std::result::Result<Option<TeamVisibility>, String> { | |
| 4732 | + | match input.get("visibility").filter(|value| !value.is_null()) { | |
| 4733 | + | None => Ok(None), | |
| 4734 | + | Some(value) => value | |
| 4735 | + | .as_str() | |
| 4736 | + | .and_then(TeamVisibility::parse) | |
| 4737 | + | .map(Some) | |
| 4738 | + | .ok_or_else(|| "visibility is visible or secret.".to_owned()), | |
| 4739 | + | } | |
| 4740 | + | } | |
| 4741 | + | ||
| 4742 | + | /// A person's `role` in a team: member when it is left out. | |
| 4743 | + | fn team_role(input: &Value) -> std::result::Result<TeamRole, String> { | |
| 4744 | + | match input.get("role").filter(|value| !value.is_null()) { | |
| 4745 | + | None => Ok(TeamRole::Member), | |
| 4746 | + | Some(value) => value | |
| 4747 | + | .as_str() | |
| 4748 | + | .and_then(TeamRole::parse) | |
| 4749 | + | .ok_or_else(|| "role is member or maintainer.".to_owned()), | |
| 4750 | + | } | |
| 4751 | + | } | |
| 4752 | + | ||
| 4753 | + | /// The fields of a team's review assignment, as inputs name them. | |
| 4754 | + | const REVIEW_ASSIGNMENT_FIELDS: [&str; 8] = | |
| 4755 | + | ["enabled", "algorithm", "count", "skip_busy", "busy_at", "include_child_teams", "excluded", "notify_team"]; | |
| 4756 | + | ||
| 4757 | + | /// `current` with the fields `given` has changed, each checked. | |
| 4758 | + | fn review_assignment(given: &Value, current: ReviewAssignment) -> std::result::Result<ReviewAssignment, String> { | |
| 4759 | + | let mut next = current; | |
| 4760 | + | let present = |key: &str| given.get(key).is_some_and(|value| !value.is_null()); | |
| 4761 | + | let boolean = |key: &str, now: bool| -> std::result::Result<bool, String> { | |
| 4762 | + | if !present(key) { | |
| 4763 | + | return Ok(now); | |
| 4764 | + | } | |
| 4765 | + | yes(given, key).ok_or_else(|| format!("{key} is true or false.")) | |
| 4766 | + | }; | |
| 4767 | + | let within = |key: &str, now: u32, most: u32| -> std::result::Result<u32, String> { | |
| 4768 | + | if !present(key) { | |
| 4769 | + | return Ok(now); | |
| 4770 | + | } | |
| 4771 | + | integer(given, key) | |
| 4772 | + | .filter(|n| (1..=most).contains(n)) | |
| 4773 | + | .ok_or_else(|| format!("{key} is a whole number from 1 to {most}.")) | |
| 4774 | + | }; | |
| 4775 | + | next.enabled = boolean("enabled", next.enabled)?; | |
| 4776 | + | if present("algorithm") { | |
| 4777 | + | next.algorithm = given["algorithm"] | |
| 4778 | + | .as_str() | |
| 4779 | + | .and_then(ReviewAlgorithm::parse) | |
| 4780 | + | .ok_or_else(|| "algorithm is round_robin or load_balance.".to_owned())?; | |
| 4781 | + | } | |
| 4782 | + | next.count = within("count", next.count, g1t_contracts::teams::MAX_ASSIGNED)?; | |
| 4783 | + | next.skip_busy = boolean("skip_busy", next.skip_busy)?; | |
| 4784 | + | next.busy_at = within("busy_at", next.busy_at, 100)?; | |
| 4785 | + | next.include_child_teams = boolean("include_child_teams", next.include_child_teams)?; | |
| 4786 | + | if present("excluded") { | |
| 4787 | + | next.excluded = strings(given, "excluded").ok_or_else(|| "excluded is a list of usernames.".to_owned())?; | |
| 4788 | + | } | |
| 4789 | + | next.notify_team = boolean("notify_team", next.notify_team)?; | |
| 4790 | + | Ok(next) | |
| 4791 | + | } | |
| 4792 | + | ||
| 4793 | + | /// The repository `repo` names for a team of `workspace`: `owner/name`, or | |
| 4794 | + | /// a name in the workspace. | |
| 4795 | + | fn team_repo(input: &Value, workspace: &str) -> Option<RepoPath> { | |
| 4796 | + | repo_path(input).or_else(|| { | |
| 4797 | + | let name = input["repo"].as_str()?.trim(); | |
| 4798 | + | (!name.is_empty() && !name.contains('/')).then(|| RepoPath { | |
| 4799 | + | namespace: workspace.to_owned(), | |
| 4800 | + | name: name.to_owned(), | |
| 4801 | + | }) | |
| 4802 | + | }) | |
| 4803 | + | } | |
| 4804 | + | ||
| 4805 | + | /// The people (`reviewers`) and teams (`team_reviewers`) a call names, each | |
| 4806 | + | /// once, lowercase; a team as `workspace/team`, a bare slug being one of | |
| 4807 | + | /// `workspace`'s. A name in `reviewers` with a `/` is a team too. | |
| 4808 | + | fn reviewer_names(input: &Value, workspace: &str) -> (Vec<String>, Vec<String>) { | |
| 4809 | + | let (mut people, mut teams): (Vec<String>, Vec<String>) = (Vec::new(), Vec::new()); | |
| 4810 | + | let clean = |name: &str| name.trim().trim_start_matches('@').to_lowercase(); | |
| 4811 | + | for name in strings(input, "reviewers").unwrap_or_default() { | |
| 4812 | + | let name = clean(&name); | |
| 4813 | + | let list = if name.contains('/') { &mut teams } else { &mut people }; | |
| 4814 | + | if !name.is_empty() && !list.contains(&name) { | |
| 4815 | + | list.push(name); | |
| 4816 | + | } | |
| 4817 | + | } | |
| 4818 | + | for name in strings(input, "team_reviewers").unwrap_or_default() { | |
| 4819 | + | let name = clean(&name); | |
| 4820 | + | if name.is_empty() { | |
| 4821 | + | continue; | |
| 4822 | + | } | |
| 4823 | + | let name = if name.contains('/') { name } else { format!("{}/{name}", workspace.to_lowercase()) }; | |
| 4824 | + | if !teams.contains(&name) { | |
| 4825 | + | teams.push(name); | |
| 4826 | + | } | |
| 4827 | + | } | |
| 4828 | + | (people, teams) | |
| 4829 | + | } | |
| 4830 | + | ||
| 4831 | + | /// Who is asked to review once `people` and `teams` are added (or, with | |
| 4832 | + | /// `add` false, taken away), as update_pull takes it: people, then teams. | |
| 4833 | + | fn reviewers_after( | |
| 4834 | + | current_people: &[String], | |
| 4835 | + | current_teams: &[String], | |
| 4836 | + | people: &[String], | |
| 4837 | + | teams: &[String], | |
| 4838 | + | add: bool, | |
| 4839 | + | ) -> Vec<String> { | |
| 4840 | + | let has = |list: &[String], name: &str| list.iter().any(|item| item.eq_ignore_ascii_case(name)); | |
| 4841 | + | let mut out = Vec::new(); | |
| 4842 | + | for (current, change) in [(current_people, people), (current_teams, teams)] { | |
| 4843 | + | let mut kept: Vec<String> = current.iter().filter(|name| add || !has(change, name)).cloned().collect(); | |
| 4844 | + | if add { | |
| 4845 | + | for name in change { | |
| 4846 | + | if !has(&kept, name) { | |
| 4847 | + | kept.push(name.clone()); | |
| 4848 | + | } | |
| 4849 | + | } | |
| 4850 | + | } | |
| 4851 | + | out.extend(kept); | |
| 4852 | + | } | |
| 4853 | + | out | |
| 4854 | + | } | |
| 4855 | + | ||
| 3760 | 4856 | impl Op { | |
| 3761 | 4857 | /// The properties of the operation's input schema. | |
| 3762 | 4858 | pub fn properties(self) -> Map<String, Value> { | |
| ⋯ | |||
| 3920 | 5016 | } | |
| 3921 | 5017 | assert!(!NEVER.contains(&Op::ListSecurityAlerts.name())); | |
| 3922 | 5018 | } | |
| 5019 | + | ||
| 5020 | + | const TEAMS: [Op; 14] = [ | |
| 5021 | + | Op::ListTeams, | |
| 5022 | + | Op::GetTeam, | |
| 5023 | + | Op::CreateTeam, | |
| 5024 | + | Op::UpdateTeam, | |
| 5025 | + | Op::DeleteTeam, | |
| 5026 | + | Op::ListTeamMembers, | |
| 5027 | + | Op::SetTeamMember, | |
| 5028 | + | Op::RemoveTeamMember, | |
| 5029 | + | Op::ListChildTeams, | |
| 5030 | + | Op::ListTeamRepos, | |
| 5031 | + | Op::SetTeamRepo, | |
| 5032 | + | Op::RemoveTeamRepo, | |
| 5033 | + | Op::SetTeamReviewAssignment, | |
| 5034 | + | Op::ListUserTeams, | |
| 5035 | + | ]; | |
| 5036 | + | ||
| 5037 | + | /// A team belongs to a workspace: its operations name the workspace, | |
| 5038 | + | /// never need a repository, and need someone signed in. | |
| 5039 | + | #[test] | |
| 5040 | + | fn team_operations_name_a_workspace() { | |
| 5041 | + | for op in TEAMS { | |
| 5042 | + | assert!(!op.needs_repo(), "{}", op.name()); | |
| 5043 | + | assert!(op.needs_user(), "{}", op.name()); | |
| 5044 | + | assert!(op.required().contains(&"workspace".to_owned()), "{}", op.name()); | |
| 5045 | + | } | |
| 5046 | + | for op in [Op::RequestReviewers, Op::RemoveRequestedReviewers, Op::GetCodeownersErrors] { | |
| 5047 | + | assert!(op.needs_repo(), "{}", op.name()); | |
| 5048 | + | } | |
| 5049 | + | // A public repository's CODEOWNERS file is anyone's to check. | |
| 5050 | + | assert!(!Op::GetCodeownersErrors.needs_user()); | |
| 5051 | + | } | |
| 5052 | + | ||
| 5053 | + | #[test] | |
| 5054 | + | fn team_words_are_checked() { | |
| 5055 | + | assert_eq!(team_visibility(&json!({})), Ok(None)); | |
| 5056 | + | assert_eq!(team_visibility(&json!({ "visibility": "Secret" })), Ok(Some(TeamVisibility::Secret))); | |
| 5057 | + | assert!(team_visibility(&json!({ "visibility": "hidden" })).is_err()); | |
| 5058 | + | assert_eq!(team_role(&json!({})), Ok(TeamRole::Member)); | |
| 5059 | + | assert_eq!(team_role(&json!({ "role": "maintainer" })), Ok(TeamRole::Maintainer)); | |
| 5060 | + | assert!(team_role(&json!({ "role": "admin" })).is_err()); | |
| 5061 | + | assert_eq!(Op::SetTeamMember.input()["properties"]["role"]["enum"], json!(["member", "maintainer"])); | |
| 5062 | + | assert_eq!(Op::CreateTeam.input()["properties"]["visibility"]["enum"], json!(["visible", "secret"])); | |
| 5063 | + | assert_eq!( | |
| 5064 | + | Op::SetTeamRepo.input()["properties"]["role"]["enum"], | |
| 5065 | + | json!(["read", "triage", "write", "maintain", "admin"]) | |
| 5066 | + | ); | |
| 5067 | + | assert_eq!( | |
| 5068 | + | Op::SetTeamReviewAssignment.input()["properties"]["algorithm"]["enum"], | |
| 5069 | + | json!(["round_robin", "load_balance"]) | |
| 5070 | + | ); | |
| 5071 | + | assert_eq!(yes(&json!({ "a": "true" }), "a"), Some(true)); | |
| 5072 | + | assert_eq!(yes(&json!({ "a": false }), "a"), Some(false)); | |
| 5073 | + | assert_eq!(yes(&json!({ "a": "maybe" }), "a"), None); | |
| 5074 | + | assert_eq!(team_slug(&json!({ "team": " @Backend " })), "backend"); | |
| 5075 | + | } | |
| 5076 | + | ||
| 5077 | + | /// Fields left out keep their value; a bad one is refused before | |
| 5078 | + | /// identity is asked. | |
| 5079 | + | #[test] | |
| 5080 | + | fn review_assignment_changes_only_what_is_given() { | |
| 5081 | + | let current = ReviewAssignment { count: 2, excluded: vec!["bo".into()], ..ReviewAssignment::default() }; | |
| 5082 | + | let next = review_assignment(&json!({ "enabled": true, "algorithm": "load_balance" }), current.clone()).unwrap(); | |
| 5083 | + | assert!(next.enabled); | |
| 5084 | + | assert_eq!(next.algorithm, ReviewAlgorithm::LoadBalance); | |
| 5085 | + | assert_eq!((next.count, next.excluded.clone()), (2, vec!["bo".to_owned()])); | |
| 5086 | + | let next = review_assignment(&json!({ "count": "3", "excluded": [], "skip_busy": "true", "busy_at": 4 }), current.clone()).unwrap(); | |
| 5087 | + | assert_eq!((next.count, next.busy_at, next.skip_busy), (3, 4, true)); | |
| 5088 | + | assert!(next.excluded.is_empty()); | |
| 5089 | + | for bad in [ | |
| 5090 | + | json!({ "algorithm": "random" }), | |
| 5091 | + | json!({ "count": 0 }), | |
| 5092 | + | json!({ "count": 11 }), | |
| 5093 | + | json!({ "busy_at": 101 }), | |
| 5094 | + | json!({ "enabled": "sometimes" }), | |
| 5095 | + | json!({ "excluded": "ana" }), | |
| 5096 | + | ] { | |
| 5097 | + | assert!(review_assignment(&bad, current.clone()).is_err(), "{bad}"); | |
| 5098 | + | } | |
| 5099 | + | } | |
| 5100 | + | ||
| 5101 | + | #[test] | |
| 5102 | + | fn a_team_names_a_repository_by_itself_or_in_full() { | |
| 5103 | + | let path = team_repo(&json!({ "repo": "rocket" }), "acme").unwrap(); | |
| 5104 | + | assert_eq!((path.namespace.as_str(), path.name.as_str()), ("acme", "rocket")); | |
| 5105 | + | let path = team_repo(&json!({ "repo": "acme/rocket" }), "other").unwrap(); | |
| 5106 | + | assert_eq!((path.namespace.as_str(), path.name.as_str()), ("acme", "rocket")); | |
| 5107 | + | assert!(team_repo(&json!({ "repo": "" }), "acme").is_none()); | |
| 5108 | + | assert!(team_repo(&json!({}), "acme").is_none()); | |
| 5109 | + | } | |
| 5110 | + | ||
| 5111 | + | /// Requested reviewers are added to, or taken from, who is asked; a | |
| 5112 | + | /// team's bare slug is one of the repository's workspace. | |
| 5113 | + | #[test] | |
| 5114 | + | fn requested_reviewers_change_the_whole_list() { | |
| 5115 | + | let input = json!({ "reviewers": ["@Ana", "g1t", "acme/web"], "team_reviewers": ["Backend", "acme/web"] }); | |
| 5116 | + | let (people, teams) = reviewer_names(&input, "Acme"); | |
| 5117 | + | assert_eq!(people, vec!["ana", "g1t"]); | |
| 5118 | + | assert_eq!(teams, vec!["acme/web", "acme/backend"]); | |
| 5119 | + | let current_people = vec!["bo".to_owned(), "ana".to_owned()]; | |
| 5120 | + | let current_teams = vec!["acme/web".to_owned()]; | |
| 5121 | + | assert_eq!( | |
| 5122 | + | reviewers_after(¤t_people, ¤t_teams, &people, &teams, true), | |
| 5123 | + | vec!["bo", "ana", "g1t", "acme/web", "acme/backend"] | |
| 5124 | + | ); | |
| 5125 | + | assert_eq!( | |
| 5126 | + | reviewers_after(¤t_people, ¤t_teams, &["ANA".to_owned()], &["acme/web".to_owned()], false), | |
| 5127 | + | vec!["bo"] | |
| 5128 | + | ); | |
| 5129 | + | assert_eq!(reviewer_names(&json!({}), "acme"), (vec![], vec![])); | |
| 5130 | + | } | |
| 3923 | 5131 | } | |
| 726 | 726 | "max_revisions": 2, | |
| 727 | 727 | "merge_queue": false, | |
| 728 | 728 | "hold_low_confidence": true, | |
| 729 | + | "require_code_owner_review": false, | |
| 729 | 730 | "updated_by": null, | |
| 730 | 731 | "updated_at": null | |
| 731 | 732 | } | |
| ⋯ | |||
| 738 | 739 | ], | |
| 739 | 740 | "required_approvals": 1, | |
| 740 | 741 | "count_agent_approvals": false, | |
| 741 | − | "merge_queue": true | |
| 742 | + | "merge_queue": true, | |
| 743 | + | "require_code_owner_review": true | |
| 742 | 744 | }, | |
| 743 | 745 | "response": { | |
| 744 | 746 | "auto_merge": false, | |
| ⋯ | |||
| 754 | 756 | "max_revisions": 2, | |
| 755 | 757 | "merge_queue": true, | |
| 756 | 758 | "hold_low_confidence": true, | |
| 759 | + | "require_code_owner_review": true, | |
| 757 | 760 | "updated_by": "syntaqx", | |
| 758 | 761 | "updated_at": "2026-10-04T16:20:37.508Z" | |
| 759 | 762 | }, | |
| 760 | − | "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](/reference/api/repositories/list-check-names/) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/working-with-g1t/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/working-with-g1t/#how-sure-the-agent-is)." | |
| 763 | + | "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](/reference/api/repositories/list-check-names/) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/working-with-g1t/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/working-with-g1t/#how-sure-the-agent-is). With `require_code_owner_review`, a pull request into the default branch merges only once the code owners of every file it changes have approved it, as many as each section of its CODEOWNERS file asks: see [Code owners](/guides/codeowners/)." | |
| 761 | 764 | }, | |
| 762 | 765 | "list_check_names": { | |
| 763 | 766 | "response": [ | |
| ⋯ | |||
| 778 | 781 | ], | |
| 779 | 782 | "notes": "The names reported on the repository's commits in the last 30 days, most recent first. A workflow reports a check named after it (`CI`), for each event it ran for; another tool, such as a deployment, reports its own name (`g1t / deploy`). Empty until something has reported on a commit: add a workflow in `.g1t/workflows` first." | |
| 780 | 783 | }, | |
| 784 | + | "get_codeowners_errors": { | |
| 785 | + | "params": { | |
| 786 | + | "owner": "flagon-io", | |
| 787 | + | "name": "hello" | |
| 788 | + | }, | |
| 789 | + | "query": { | |
| 790 | + | "ref": "main" | |
| 791 | + | }, | |
| 792 | + | "response": { | |
| 793 | + | "path": ".github/CODEOWNERS", | |
| 794 | + | "ref": "main", | |
| 795 | + | "size": 412, | |
| 796 | + | "rules": 9, | |
| 797 | + | "sections": [ | |
| 798 | + | "Docs" | |
| 799 | + | ], | |
| 800 | + | "errors": [ | |
| 801 | + | { | |
| 802 | + | "line": 4, | |
| 803 | + | "kind": "unknown_team", | |
| 804 | + | "token": "@flagon-io/platform", | |
| 805 | + | "message": "@flagon-io/platform is not a team of flagon-io." | |
| 806 | + | }, | |
| 807 | + | { | |
| 808 | + | "line": 7, | |
| 809 | + | "kind": "negation", | |
| 810 | + | "token": "!docs/internal/", | |
| 811 | + | "message": "!docs/internal/ starts with !, and negation is not supported; this line is skipped. Give the path a later rule with no owners instead." | |
| 812 | + | } | |
| 813 | + | ] | |
| 814 | + | }, | |
| 815 | + | "notes": "The file is the first of `.g1t/CODEOWNERS`, `.github/CODEOWNERS`, `CODEOWNERS`, `docs/CODEOWNERS` and `.gitlab/CODEOWNERS` found on `ref` (the default branch when left out); `path` is null when there is none, with no errors. Each error has its `line` (0 for the file as a whole), the `token` at fault, a `message`, and a `kind`:\n\n| `kind` | |\n| --- | --- |\n| `too_large` | The file is over 3 MB and was ignored. |\n| `negation` | A pattern starting with `!`; the line was skipped. |\n| `character_range` | A pattern with `[` or `]`; the line was skipped. |\n| `bad_pattern` | A pattern that names no path; the line was skipped. |\n| `bad_owner` | An owner that is not `@user`, `@workspace/team` or an email address. |\n| `bad_section` | A section header that could not be read. |\n| `unknown_user` | No account has that username. |\n| `unknown_team` | The workspace has no team of that slug. |\n| `unknown_email` | No account has confirmed that address. |\n| `no_write_access` | The person cannot write to the repository. |\n| `team_no_access` | The team has no write access to the repository. |\n\nSee [Code owners](/guides/codeowners/)." | |
| 816 | + | }, | |
| 781 | 817 | "list_events": { | |
| 782 | 818 | "query": { | |
| 783 | 819 | "before": "evt_01m43t2a6c9e3g7j1m5q9t3x7b" | |
| ⋯ | |||
| 849 | 885 | "assignees": [], | |
| 850 | 886 | "blocked_by": [], | |
| 851 | 887 | "queued": false, | |
| 852 | − | "agent": "claude-code" | |
| 888 | + | "agent": "claude-code", | |
| 889 | + | "milestone": null | |
| 853 | 890 | } | |
| 854 | 891 | ], | |
| 855 | 892 | "notes": "Returns at most 100 issues, newest first. `comment_count` counts comments, not events such as \"opened #14 for this\"." | |
| ⋯ | |||
| 890 | 927 | "assignees": [], | |
| 891 | 928 | "blocked_by": [], | |
| 892 | 929 | "queued": false, | |
| 893 | − | "agent": null | |
| 930 | + | "agent": null, | |
| 931 | + | "milestone": null | |
| 894 | 932 | }, | |
| 895 | 933 | "notes": "Labels are lowercased. A label not used before is created." | |
| 896 | 934 | }, | |
| ⋯ | |||
| 924 | 962 | "assignees": [], | |
| 925 | 963 | "blocked_by": [], | |
| 926 | 964 | "queued": false, | |
| 927 | − | "agent": null | |
| 965 | + | "agent": null, | |
| 966 | + | "milestone": null | |
| 928 | 967 | }, | |
| 929 | 968 | "pulls": [ | |
| 930 | 969 | { | |
| ⋯ | |||
| 952 | 991 | "files": [], | |
| 953 | 992 | "assignees": [], | |
| 954 | 993 | "reviewers": [], | |
| 994 | + | "labels": [], | |
| 995 | + | "milestone": null, | |
| 996 | + | "base": "main", | |
| 955 | 997 | "author": { | |
| 956 | 998 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 957 | 999 | "username": "syntaqx", | |
| ⋯ | |||
| 996 | 1038 | "reviewers": [ | |
| 997 | 1039 | "ana" | |
| 998 | 1040 | ], | |
| 1041 | + | "labels": [], | |
| 1042 | + | "milestone": null, | |
| 1043 | + | "base": "main", | |
| 999 | 1044 | "author": { | |
| 1000 | 1045 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1001 | 1046 | "username": "syntaqx", | |
| ⋯ | |||
| 1037 | 1082 | ], | |
| 1038 | 1083 | "assignees": [ | |
| 1039 | 1084 | "syntaqx" | |
| 1040 | − | ] | |
| 1085 | + | ], | |
| 1086 | + | "milestone": 3 | |
| 1041 | 1087 | }, | |
| 1042 | 1088 | "response": { | |
| 1043 | 1089 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| ⋯ | |||
| 1070 | 1116 | ], | |
| 1071 | 1117 | "blocked_by": [], | |
| 1072 | 1118 | "queued": false, | |
| 1073 | − | "agent": null | |
| 1119 | + | "agent": null, | |
| 1120 | + | "milestone": { | |
| 1121 | + | "number": 3, | |
| 1122 | + | "title": "Launch" | |
| 1123 | + | } | |
| 1074 | 1124 | } | |
| 1075 | 1125 | }, | |
| 1076 | 1126 | "close_issue": { | |
| ⋯ | |||
| 1105 | 1155 | "assignees": [], | |
| 1106 | 1156 | "blocked_by": [], | |
| 1107 | 1157 | "queued": false, | |
| 1108 | − | "agent": null | |
| 1158 | + | "agent": null, | |
| 1159 | + | "milestone": null | |
| 1109 | 1160 | } | |
| 1110 | 1161 | }, | |
| 1111 | 1162 | "reopen_issue": { | |
| ⋯ | |||
| 1137 | 1188 | "assignees": [], | |
| 1138 | 1189 | "blocked_by": [], | |
| 1139 | 1190 | "queued": false, | |
| 1140 | − | "agent": null | |
| 1191 | + | "agent": null, | |
| 1192 | + | "milestone": null | |
| 1141 | 1193 | } | |
| 1142 | 1194 | }, | |
| 1143 | 1195 | "assign_issue": { | |
| ⋯ | |||
| 1169 | 1221 | "files": [], | |
| 1170 | 1222 | "assignees": [], | |
| 1171 | 1223 | "reviewers": [], | |
| 1224 | + | "labels": [], | |
| 1225 | + | "milestone": null, | |
| 1226 | + | "base": "main", | |
| 1172 | 1227 | "author": { | |
| 1173 | 1228 | "id": "usr_g1t_agent", | |
| 1174 | 1229 | "username": "g1t", | |
| ⋯ | |||
| 1221 | 1276 | "assignees": [], | |
| 1222 | 1277 | "blocked_by": [], | |
| 1223 | 1278 | "queued": false, | |
| 1224 | − | "agent": "g1t" | |
| 1279 | + | "agent": "g1t", | |
| 1280 | + | "milestone": null | |
| 1225 | 1281 | }, | |
| 1226 | 1282 | "pull": { | |
| 1227 | 1283 | "id": "pr_01m4a2c9b6e0h4m8q2t6x0a4d8", | |
| ⋯ | |||
| 1248 | 1304 | "files": [], | |
| 1249 | 1305 | "assignees": [], | |
| 1250 | 1306 | "reviewers": [], | |
| 1307 | + | "labels": [], | |
| 1308 | + | "milestone": null, | |
| 1309 | + | "base": "main", | |
| 1251 | 1310 | "author": { | |
| 1252 | 1311 | "id": "usr_g1t_agent", | |
| 1253 | 1312 | "username": "g1t", | |
| ⋯ | |||
| 1303 | 1362 | }, | |
| 1304 | 1363 | "list_labels": { | |
| 1305 | 1364 | "response": [ | |
| 1306 | − | "bug", | |
| 1307 | − | "feature", | |
| 1308 | − | "docs", | |
| 1309 | − | "chore", | |
| 1310 | − | "question", | |
| 1311 | − | "good first issue" | |
| 1365 | + | { | |
| 1366 | + | "name": "bug", | |
| 1367 | + | "color": "d73a4a", | |
| 1368 | + | "description": "Something isn't working", | |
| 1369 | + | "issues": 4, | |
| 1370 | + | "pulls": 1 | |
| 1371 | + | }, | |
| 1372 | + | { | |
| 1373 | + | "name": "dependencies", | |
| 1374 | + | "color": "0366d6", | |
| 1375 | + | "description": "Updates a dependency", | |
| 1376 | + | "issues": 0, | |
| 1377 | + | "pulls": 6 | |
| 1378 | + | }, | |
| 1379 | + | { | |
| 1380 | + | "name": "good first issue", | |
| 1381 | + | "color": "7057ff", | |
| 1382 | + | "description": "Good for newcomers", | |
| 1383 | + | "issues": 2, | |
| 1384 | + | "pulls": 0 | |
| 1385 | + | } | |
| 1312 | 1386 | ], | |
| 1313 | − | "notes": "The five default labels come first, then the others used on the repository's issues, alphabetically." | |
| 1387 | + | "notes": "By name. A new repository starts with the default labels; add_default_labels adds those an older one is missing." | |
| 1314 | 1388 | }, | |
| 1315 | 1389 | "plan_work": { | |
| 1316 | 1390 | "request": { | |
| ⋯ | |||
| 1479 | 1553 | "reviewers": [ | |
| 1480 | 1554 | "ana" | |
| 1481 | 1555 | ], | |
| 1556 | + | "team_reviewers": [], | |
| 1482 | 1557 | "author": { | |
| 1483 | 1558 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1484 | 1559 | "username": "syntaqx", | |
| ⋯ | |||
| 1488 | 1563 | }, | |
| 1489 | 1564 | "requested_by": null, | |
| 1490 | 1565 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1491 | − | "updated_at": "2026-10-01T18:35:44.902Z" | |
| 1566 | + | "updated_at": "2026-10-01T18:35:44.902Z", | |
| 1567 | + | "labels": [], | |
| 1568 | + | "milestone": null, | |
| 1569 | + | "base": "main" | |
| 1492 | 1570 | } | |
| 1493 | 1571 | ], | |
| 1494 | 1572 | "notes": "Returns at most 100 pull requests, newest first. `check_status` is `queued`, `running`, `passed`, `failed`, `errored`, or `null` when no checks have run against the head." | |
| ⋯ | |||
| 1524 | 1602 | "files": [], | |
| 1525 | 1603 | "assignees": [], | |
| 1526 | 1604 | "reviewers": [], | |
| 1605 | + | "labels": [], | |
| 1606 | + | "milestone": null, | |
| 1607 | + | "base": "main", | |
| 1527 | 1608 | "author": { | |
| 1528 | 1609 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1529 | 1610 | "username": "syntaqx", | |
| ⋯ | |||
| 1578 | 1659 | "reviewers": [ | |
| 1579 | 1660 | "ana" | |
| 1580 | 1661 | ], | |
| 1662 | + | "team_reviewers": [ | |
| 1663 | + | "flagon-io/backend" | |
| 1664 | + | ], | |
| 1581 | 1665 | "author": { | |
| 1582 | 1666 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1583 | 1667 | "username": "syntaqx", | |
| ⋯ | |||
| 1588 | 1672 | "requested_by": null, | |
| 1589 | 1673 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1590 | 1674 | "updated_at": "2026-10-01T18:35:44.902Z", | |
| 1591 | − | "confidence": null | |
| 1675 | + | "confidence": null, | |
| 1676 | + | "labels": [], | |
| 1677 | + | "milestone": null, | |
| 1678 | + | "base": "main" | |
| 1592 | 1679 | }, | |
| 1593 | 1680 | "issue": { | |
| 1594 | 1681 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| ⋯ | |||
| 1618 | 1705 | "assignees": [], | |
| 1619 | 1706 | "blocked_by": [], | |
| 1620 | 1707 | "queued": false, | |
| 1621 | − | "agent": "claude-code" | |
| 1708 | + | "agent": "claude-code", | |
| 1709 | + | "milestone": null | |
| 1622 | 1710 | }, | |
| 1623 | 1711 | "comments": [ | |
| 1624 | 1712 | { | |
| ⋯ | |||
| 1675 | 1763 | "description": "CI passed", | |
| 1676 | 1764 | "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4" | |
| 1677 | 1765 | } | |
| 1678 | − | ] | |
| 1766 | + | ], | |
| 1767 | + | "code_owners": { | |
| 1768 | + | "path": ".github/CODEOWNERS", | |
| 1769 | + | "required": true, | |
| 1770 | + | "reviews": [ | |
| 1771 | + | { | |
| 1772 | + | "section": null, | |
| 1773 | + | "line": 3, | |
| 1774 | + | "pattern": "/src/", | |
| 1775 | + | "owners": [ | |
| 1776 | + | "@flagon-io/backend" | |
| 1777 | + | ], | |
| 1778 | + | "files": [ | |
| 1779 | + | "src/main.rs" | |
| 1780 | + | ], | |
| 1781 | + | "optional": false, | |
| 1782 | + | "required": 1, | |
| 1783 | + | "approved_by": [], | |
| 1784 | + | "changes_requested_by": [], | |
| 1785 | + | "satisfied": false | |
| 1786 | + | } | |
| 1787 | + | ], | |
| 1788 | + | "missing": "Code owners have not approved: @flagon-io/backend for /src/.", | |
| 1789 | + | "errors": 0 | |
| 1790 | + | } | |
| 1679 | 1791 | }, | |
| 1680 | − | "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](/reference/api/repositories/update-repo-settings/)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request g1t is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have g1t do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](/reference/api/actions/get-workflow-run/) and [`get_job_logs`](/reference/api/actions/get-job-logs/)." | |
| 1792 | + | "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](/reference/api/repositories/update-repo-settings/)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request g1t is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n| `pull.reviewers`, `pull.team_reviewers` | The people asked to review it, `g1t` among them when a g1t agent was, and the teams, as `workspace/team`. Change them with [`request_reviewers`](/reference/api/pull-requests/request-reviewers/). |\n| `code_owners` | Present when the branch it merges into has a CODEOWNERS file: its `path`, whether code owners' approval is `required` there, `reviews` (one per section and rule that owns a changed file: `section`, `line`, `pattern`, `owners`, `files`, `optional`, the approvals `required`, `approved_by`, `changes_requested_by` and `satisfied`), what is still `missing`, as the merge box says it, and how many `errors` the file has; [`get_codeowners_errors`](/reference/api/repositories/get-codeowners-errors/) lists them. |\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have g1t do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](/reference/api/actions/get-workflow-run/) and [`get_job_logs`](/reference/api/actions/get-job-logs/)." | |
| 1681 | 1793 | }, | |
| 1682 | 1794 | "get_pull_request_changes": { | |
| 1683 | 1795 | "response": { | |
| ⋯ | |||
| 1761 | 1873 | ], | |
| 1762 | 1874 | "assignees": [], | |
| 1763 | 1875 | "reviewers": [], | |
| 1876 | + | "labels": [], | |
| 1877 | + | "milestone": null, | |
| 1878 | + | "base": "main", | |
| 1764 | 1879 | "author": { | |
| 1765 | 1880 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1766 | 1881 | "username": "syntaqx", | |
| ⋯ | |||
| 1800 | 1915 | "created_at": "2026-10-01T18:40:05.019Z" | |
| 1801 | 1916 | } | |
| 1802 | 1917 | }, | |
| 1918 | + | "request_reviewers": { | |
| 1919 | + | "params": { | |
| 1920 | + | "owner": "flagon-io", | |
| 1921 | + | "name": "hello", | |
| 1922 | + | "number": 14 | |
| 1923 | + | }, | |
| 1924 | + | "request": { | |
| 1925 | + | "reviewers": [ | |
| 1926 | + | "bo" | |
| 1927 | + | ], | |
| 1928 | + | "team_reviewers": [ | |
| 1929 | + | "backend" | |
| 1930 | + | ] | |
| 1931 | + | }, | |
| 1932 | + | "response": { | |
| 1933 | + | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 1934 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1935 | + | "number": 14, | |
| 1936 | + | "issue": 12, | |
| 1937 | + | "title": "Greeting should name the caller", | |
| 1938 | + | "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".", | |
| 1939 | + | "agent": "claude-code", | |
| 1940 | + | "runtime": "external", | |
| 1941 | + | "status": "open", | |
| 1942 | + | "fork": { | |
| 1943 | + | "namespace": "pulls", | |
| 1944 | + | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 1945 | + | }, | |
| 1946 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1947 | + | "branch": null, | |
| 1948 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 1949 | + | "merge_base": null, | |
| 1950 | + | "merged_by": null, | |
| 1951 | + | "merged_at": null, | |
| 1952 | + | "superseded_by": null, | |
| 1953 | + | "check_status": "passed", | |
| 1954 | + | "files": [ | |
| 1955 | + | { | |
| 1956 | + | "path": "src/main.rs", | |
| 1957 | + | "additions": 6, | |
| 1958 | + | "deletions": 2 | |
| 1959 | + | } | |
| 1960 | + | ], | |
| 1961 | + | "assignees": [], | |
| 1962 | + | "reviewers": [ | |
| 1963 | + | "ana", | |
| 1964 | + | "bo" | |
| 1965 | + | ], | |
| 1966 | + | "team_reviewers": [ | |
| 1967 | + | "flagon-io/backend" | |
| 1968 | + | ], | |
| 1969 | + | "author": { | |
| 1970 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1971 | + | "username": "syntaqx", | |
| 1972 | + | "kind": "user", | |
| 1973 | + | "verified": false, | |
| 1974 | + | "workspaces": [] | |
| 1975 | + | }, | |
| 1976 | + | "requested_by": null, | |
| 1977 | + | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1978 | + | "updated_at": "2026-10-01T18:40:12.331Z" | |
| 1979 | + | }, | |
| 1980 | + | "notes": "Adds to who is asked: `reviewers` by username (`g1t` asks a g1t agent), `team_reviewers` as `workspace/team`, or a team's slug in the repository's workspace. A team with review assignment on has the people it picks added to `reviewers`, and stays in `team_reviewers`. Each is told in their inbox. Nobody is asked to review their own pull request. `422` for someone who is not an account, or a team that does not exist or that you cannot see. Whoever opened it, or the Triage role or higher, while it is open. See [Pull requests](/guides/pull-requests/) and [Teams](/guides/teams/)." | |
| 1981 | + | }, | |
| 1982 | + | "remove_requested_reviewers": { | |
| 1983 | + | "params": { | |
| 1984 | + | "owner": "flagon-io", | |
| 1985 | + | "name": "hello", | |
| 1986 | + | "number": 14 | |
| 1987 | + | }, | |
| 1988 | + | "request": { | |
| 1989 | + | "reviewers": [ | |
| 1990 | + | "bo" | |
| 1991 | + | ], | |
| 1992 | + | "team_reviewers": [ | |
| 1993 | + | "flagon-io/backend" | |
| 1994 | + | ] | |
| 1995 | + | }, | |
| 1996 | + | "response": { | |
| 1997 | + | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 1998 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1999 | + | "number": 14, | |
| 2000 | + | "issue": 12, | |
| 2001 | + | "title": "Greeting should name the caller", | |
| 2002 | + | "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".", | |
| 2003 | + | "agent": "claude-code", | |
| 2004 | + | "runtime": "external", | |
| 2005 | + | "status": "open", | |
| 2006 | + | "fork": { | |
| 2007 | + | "namespace": "pulls", | |
| 2008 | + | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 2009 | + | }, | |
| 2010 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2011 | + | "branch": null, | |
| 2012 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2013 | + | "merge_base": null, | |
| 2014 | + | "merged_by": null, | |
| 2015 | + | "merged_at": null, | |
| 2016 | + | "superseded_by": null, | |
| 2017 | + | "check_status": "passed", | |
| 2018 | + | "files": [ | |
| 2019 | + | { | |
| 2020 | + | "path": "src/main.rs", | |
| 2021 | + | "additions": 6, | |
| 2022 | + | "deletions": 2 | |
| 2023 | + | } | |
| 2024 | + | ], | |
| 2025 | + | "assignees": [], | |
| 2026 | + | "reviewers": [ | |
| 2027 | + | "ana" | |
| 2028 | + | ], | |
| 2029 | + | "team_reviewers": [], | |
| 2030 | + | "author": { | |
| 2031 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2032 | + | "username": "syntaqx", | |
| 2033 | + | "kind": "user", | |
| 2034 | + | "verified": false, | |
| 2035 | + | "workspaces": [] | |
| 2036 | + | }, | |
| 2037 | + | "requested_by": null, | |
| 2038 | + | "created_at": "2026-10-01T18:20:02.117Z", | |
| 2039 | + | "updated_at": "2026-10-01T18:41:30.904Z" | |
| 2040 | + | }, | |
| 2041 | + | "notes": "Takes them off who is asked; anyone not asked is ignored. Reviews they already gave stay, as do the people a team's review assignment picked: remove them by username." | |
| 2042 | + | }, | |
| 1803 | 2043 | "merge_pull_request": { | |
| 1804 | 2044 | "request": { | |
| 1805 | 2045 | "keep_issue_open": false | |
| ⋯ | |||
| 1837 | 2077 | "reviewers": [ | |
| 1838 | 2078 | "ana" | |
| 1839 | 2079 | ], | |
| 2080 | + | "labels": [], | |
| 2081 | + | "milestone": null, | |
| 2082 | + | "base": "main", | |
| 1840 | 2083 | "author": { | |
| 1841 | 2084 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1842 | 2085 | "username": "syntaqx", | |
| ⋯ | |||
| 1882 | 2125 | ], | |
| 1883 | 2126 | "assignees": [], | |
| 1884 | 2127 | "reviewers": [], | |
| 2128 | + | "labels": [], | |
| 2129 | + | "milestone": null, | |
| 2130 | + | "base": "main", | |
| 1885 | 2131 | "author": { | |
| 1886 | 2132 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1887 | 2133 | "username": "syntaqx", | |
| ⋯ | |||
| 4111 | 4357 | ], | |
| 4112 | 4358 | "notes": "By username, each with the repositories they have a role on. Refused with `403` for anyone but an owner. See [Access and roles](/guides/access-and-roles/)." | |
| 4113 | 4359 | }, | |
| 4360 | + | "list_teams": { | |
| 4361 | + | "params": { | |
| 4362 | + | "workspace": "flagon-io" | |
| 4363 | + | }, | |
| 4364 | + | "query": { | |
| 4365 | + | "q": "back" | |
| 4366 | + | }, | |
| 4367 | + | "response": [ | |
| 4368 | + | { | |
| 4369 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4370 | + | "workspace": "flagon-io", | |
| 4371 | + | "slug": "backend", | |
| 4372 | + | "name": "Backend", | |
| 4373 | + | "description": "The API and the services behind it.", | |
| 4374 | + | "visibility": "visible", | |
| 4375 | + | "parent": { | |
| 4376 | + | "slug": "engineering", | |
| 4377 | + | "name": "Engineering" | |
| 4378 | + | }, | |
| 4379 | + | "notify": true, | |
| 4380 | + | "review_assignment": { | |
| 4381 | + | "enabled": false, | |
| 4382 | + | "algorithm": "round_robin", | |
| 4383 | + | "count": 1, | |
| 4384 | + | "skip_busy": false, | |
| 4385 | + | "busy_at": 5, | |
| 4386 | + | "include_child_teams": false, | |
| 4387 | + | "excluded": [], | |
| 4388 | + | "notify_team": false | |
| 4389 | + | }, | |
| 4390 | + | "members_count": 4, | |
| 4391 | + | "repos_count": 2, | |
| 4392 | + | "child_teams_count": 1, | |
| 4393 | + | "viewer_role": "maintainer", | |
| 4394 | + | "can_manage": true, | |
| 4395 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4396 | + | "updated_at": "2026-10-06T15:02:11.480Z" | |
| 4397 | + | } | |
| 4398 | + | ], | |
| 4399 | + | "notes": "Teams you are in come first, then the rest by name. A secret team is listed only for its own people and the workspace's owners. `review_assignment` is what happens when the team is asked to review: see [`set_team_review_assignment`](/reference/api/teams/set-team-review-assignment/). `403` for anyone who is not a member of the workspace. See [Teams](/guides/teams/)." | |
| 4400 | + | }, | |
| 4401 | + | "create_team": { | |
| 4402 | + | "params": { | |
| 4403 | + | "workspace": "flagon-io" | |
| 4404 | + | }, | |
| 4405 | + | "request": { | |
| 4406 | + | "name": "Backend", | |
| 4407 | + | "description": "The API and the services behind it.", | |
| 4408 | + | "visibility": "visible", | |
| 4409 | + | "parent": "engineering", | |
| 4410 | + | "members": [ | |
| 4411 | + | "ana" | |
| 4412 | + | ] | |
| 4413 | + | }, | |
| 4414 | + | "response": { | |
| 4415 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4416 | + | "workspace": "flagon-io", | |
| 4417 | + | "slug": "backend", | |
| 4418 | + | "name": "Backend", | |
| 4419 | + | "description": "The API and the services behind it.", | |
| 4420 | + | "visibility": "visible", | |
| 4421 | + | "parent": { | |
| 4422 | + | "slug": "engineering", | |
| 4423 | + | "name": "Engineering" | |
| 4424 | + | }, | |
| 4425 | + | "notify": true, | |
| 4426 | + | "review_assignment": { | |
| 4427 | + | "enabled": false, | |
| 4428 | + | "algorithm": "round_robin", | |
| 4429 | + | "count": 1, | |
| 4430 | + | "skip_busy": false, | |
| 4431 | + | "busy_at": 5, | |
| 4432 | + | "include_child_teams": false, | |
| 4433 | + | "excluded": [], | |
| 4434 | + | "notify_team": false | |
| 4435 | + | }, | |
| 4436 | + | "members_count": 2, | |
| 4437 | + | "repos_count": 0, | |
| 4438 | + | "child_teams_count": 0, | |
| 4439 | + | "viewer_role": "maintainer", | |
| 4440 | + | "can_manage": true, | |
| 4441 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4442 | + | "updated_at": "2026-10-06T15:02:11.480Z" | |
| 4443 | + | }, | |
| 4444 | + | "notes": "You become the team's maintainer. `slug` is made from `name` when left out (`Web & Mobile` becomes `web-mobile`), and `409` says one is taken. A workspace has at most 500 teams, nested at most 8 deep; `name` is at most 80 characters and `description` 280. A `secret` team cannot have a parent or child teams. Refused with `403` for an agent's or a workspace's token. See [Teams](/guides/teams/)." | |
| 4445 | + | }, | |
| 4446 | + | "get_team": { | |
| 4447 | + | "params": { | |
| 4448 | + | "workspace": "flagon-io", | |
| 4449 | + | "team": "backend" | |
| 4450 | + | }, | |
| 4451 | + | "response": { | |
| 4452 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4453 | + | "workspace": "flagon-io", | |
| 4454 | + | "slug": "backend", | |
| 4455 | + | "name": "Backend", | |
| 4456 | + | "description": "The API and the services behind it.", | |
| 4457 | + | "visibility": "visible", | |
| 4458 | + | "parent": { | |
| 4459 | + | "slug": "engineering", | |
| 4460 | + | "name": "Engineering" | |
| 4461 | + | }, | |
| 4462 | + | "notify": true, | |
| 4463 | + | "review_assignment": { | |
| 4464 | + | "enabled": false, | |
| 4465 | + | "algorithm": "round_robin", | |
| 4466 | + | "count": 1, | |
| 4467 | + | "skip_busy": false, | |
| 4468 | + | "busy_at": 5, | |
| 4469 | + | "include_child_teams": false, | |
| 4470 | + | "excluded": [], | |
| 4471 | + | "notify_team": false | |
| 4472 | + | }, | |
| 4473 | + | "members_count": 4, | |
| 4474 | + | "repos_count": 2, | |
| 4475 | + | "child_teams_count": 1, | |
| 4476 | + | "viewer_role": "maintainer", | |
| 4477 | + | "can_manage": true, | |
| 4478 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4479 | + | "updated_at": "2026-10-06T15:02:11.480Z" | |
| 4480 | + | }, | |
| 4481 | + | "notes": "`404` for a team that does not exist, or a secret one you are not in, unless you are an owner." | |
| 4482 | + | }, | |
| 4483 | + | "update_team": { | |
| 4484 | + | "params": { | |
| 4485 | + | "workspace": "flagon-io", | |
| 4486 | + | "team": "backend" | |
| 4487 | + | }, | |
| 4488 | + | "request": { | |
| 4489 | + | "description": "The API, the services behind it, and their on-call.", | |
| 4490 | + | "visibility": "secret", | |
| 4491 | + | "parent": "" | |
| 4492 | + | }, | |
| 4493 | + | "response": { | |
| 4494 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4495 | + | "workspace": "flagon-io", | |
| 4496 | + | "slug": "backend", | |
| 4497 | + | "name": "Backend", | |
| 4498 | + | "description": "The API, the services behind it, and their on-call.", | |
| 4499 | + | "visibility": "secret", | |
| 4500 | + | "parent": null, | |
| 4501 | + | "notify": true, | |
| 4502 | + | "review_assignment": { | |
| 4503 | + | "enabled": false, | |
| 4504 | + | "algorithm": "round_robin", | |
| 4505 | + | "count": 1, | |
| 4506 | + | "skip_busy": false, | |
| 4507 | + | "busy_at": 5, | |
| 4508 | + | "include_child_teams": false, | |
| 4509 | + | "excluded": [], | |
| 4510 | + | "notify_team": false | |
| 4511 | + | }, | |
| 4512 | + | "members_count": 4, | |
| 4513 | + | "repos_count": 2, | |
| 4514 | + | "child_teams_count": 1, | |
| 4515 | + | "viewer_role": "maintainer", | |
| 4516 | + | "can_manage": true, | |
| 4517 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4518 | + | "updated_at": "2026-10-07T09:41:52.006Z" | |
| 4519 | + | }, | |
| 4520 | + | "notes": "Only the fields given change. `parent` set to `\"\"` takes the team out from under its parent; a team cannot be nested under itself or one of its own child teams. A new `slug` changes how it is mentioned: `@flagon-io/backend` no longer reaches it. `review_assignment` takes the fields [`set_team_review_assignment`](/reference/api/teams/set-team-review-assignment/) does. Owners of the workspace and the team's maintainers (`403` otherwise)." | |
| 4521 | + | }, | |
| 4522 | + | "delete_team": { | |
| 4523 | + | "params": { | |
| 4524 | + | "workspace": "flagon-io", | |
| 4525 | + | "team": "backend" | |
| 4526 | + | }, | |
| 4527 | + | "response": true, | |
| 4528 | + | "notes": "Its child teams move up to its parent, or to the top when it has none. The roles it gave on repositories are taken away, and it is no longer asked to review. Owners of the workspace and the team's maintainers (`403` otherwise)." | |
| 4529 | + | }, | |
| 4530 | + | "list_team_members": { | |
| 4531 | + | "params": { | |
| 4532 | + | "workspace": "flagon-io", | |
| 4533 | + | "team": "backend" | |
| 4534 | + | }, | |
| 4535 | + | "query": { | |
| 4536 | + | "include_child_teams": "true" | |
| 4537 | + | }, | |
| 4538 | + | "response": [ | |
| 4539 | + | { | |
| 4540 | + | "username": "syntaqx", | |
| 4541 | + | "name": "Chase Pierce", | |
| 4542 | + | "avatar": null, | |
| 4543 | + | "role": "maintainer", | |
| 4544 | + | "via": null | |
| 4545 | + | }, | |
| 4546 | + | { | |
| 4547 | + | "username": "ana", | |
| 4548 | + | "name": "Ana Lima", | |
| 4549 | + | "avatar": null, | |
| 4550 | + | "role": "member", | |
| 4551 | + | "via": null | |
| 4552 | + | }, | |
| 4553 | + | { | |
| 4554 | + | "username": "bo", | |
| 4555 | + | "name": null, | |
| 4556 | + | "avatar": null, | |
| 4557 | + | "role": "member", | |
| 4558 | + | "via": "payments" | |
| 4559 | + | } | |
| 4560 | + | ], | |
| 4561 | + | "notes": "Maintainers come first, then members, each by username. With `include_child_teams`, the people of its child teams (and theirs) follow, each with `via`, the child team they are in; someone in both is listed once, as the team's own." | |
| 4562 | + | }, | |
| 4563 | + | "set_team_member": { | |
| 4564 | + | "params": { | |
| 4565 | + | "workspace": "flagon-io", | |
| 4566 | + | "team": "backend", | |
| 4567 | + | "username": "ana" | |
| 4568 | + | }, | |
| 4569 | + | "request": { | |
| 4570 | + | "role": "maintainer" | |
| 4571 | + | }, | |
| 4572 | + | "response": { | |
| 4573 | + | "username": "ana", | |
| 4574 | + | "name": "Ana Lima", | |
| 4575 | + | "avatar": null, | |
| 4576 | + | "role": "maintainer", | |
| 4577 | + | "via": null | |
| 4578 | + | }, | |
| 4579 | + | "notes": "`role` is `member` (the default) or `maintainer`. `422` when they are not a member of the workspace: add them to it first. Owners of the workspace and the team's maintainers (`403` otherwise)." | |
| 4580 | + | }, | |
| 4581 | + | "remove_team_member": { | |
| 4582 | + | "params": { | |
| 4583 | + | "workspace": "flagon-io", | |
| 4584 | + | "team": "backend", | |
| 4585 | + | "username": "ana" | |
| 4586 | + | }, | |
| 4587 | + | "response": true, | |
| 4588 | + | "notes": "Anyone may take themselves out of a team. Leaving the workspace takes a person out of all its teams." | |
| 4589 | + | }, | |
| 4590 | + | "list_child_teams": { | |
| 4591 | + | "params": { | |
| 4592 | + | "workspace": "flagon-io", | |
| 4593 | + | "team": "engineering" | |
| 4594 | + | }, | |
| 4595 | + | "response": [ | |
| 4596 | + | { | |
| 4597 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4598 | + | "workspace": "flagon-io", | |
| 4599 | + | "slug": "backend", | |
| 4600 | + | "name": "Backend", | |
| 4601 | + | "description": "The API and the services behind it.", | |
| 4602 | + | "visibility": "visible", | |
| 4603 | + | "parent": { | |
| 4604 | + | "slug": "engineering", | |
| 4605 | + | "name": "Engineering" | |
| 4606 | + | }, | |
| 4607 | + | "notify": true, | |
| 4608 | + | "review_assignment": { | |
| 4609 | + | "enabled": false, | |
| 4610 | + | "algorithm": "round_robin", | |
| 4611 | + | "count": 1, | |
| 4612 | + | "skip_busy": false, | |
| 4613 | + | "busy_at": 5, | |
| 4614 | + | "include_child_teams": false, | |
| 4615 | + | "excluded": [], | |
| 4616 | + | "notify_team": false | |
| 4617 | + | }, | |
| 4618 | + | "members_count": 4, | |
| 4619 | + | "repos_count": 2, | |
| 4620 | + | "child_teams_count": 0, | |
| 4621 | + | "viewer_role": "maintainer", | |
| 4622 | + | "can_manage": true, | |
| 4623 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4624 | + | "updated_at": "2026-10-06T15:02:11.480Z" | |
| 4625 | + | } | |
| 4626 | + | ], | |
| 4627 | + | "notes": "Only the teams directly under it; read each one's own with this again. A child team inherits its parent's roles on repositories, and a mention or review request for the parent reaches its people too." | |
| 4628 | + | }, | |
| 4629 | + | "list_team_repos": { | |
| 4630 | + | "params": { | |
| 4631 | + | "workspace": "flagon-io", | |
| 4632 | + | "team": "backend" | |
| 4633 | + | }, | |
| 4634 | + | "response": [ | |
| 4635 | + | { | |
| 4636 | + | "repo": "flagon-io/hello", | |
| 4637 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 4638 | + | "role": "write", | |
| 4639 | + | "inherited_from": null | |
| 4640 | + | }, | |
| 4641 | + | { | |
| 4642 | + | "repo": "flagon-io/docs", | |
| 4643 | + | "repo_id": "rep_01m3m5r2a8c4e6g8j0m2p4r6t8", | |
| 4644 | + | "role": "read", | |
| 4645 | + | "inherited_from": "engineering" | |
| 4646 | + | } | |
| 4647 | + | ], | |
| 4648 | + | "notes": "A role inherited from a parent team names it in `inherited_from`. Where the team has a role of its own on the same repository, the higher one is listed. Only repositories you can see are listed." | |
| 4649 | + | }, | |
| 4650 | + | "set_team_repo": { | |
| 4651 | + | "params": { | |
| 4652 | + | "workspace": "flagon-io", | |
| 4653 | + | "team": "backend", | |
| 4654 | + | "repo": "hello" | |
| 4655 | + | }, | |
| 4656 | + | "request": { | |
| 4657 | + | "role": "maintain" | |
| 4658 | + | }, | |
| 4659 | + | "response": { | |
| 4660 | + | "repo": "flagon-io/hello", | |
| 4661 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 4662 | + | "role": "maintain", | |
| 4663 | + | "inherited_from": null | |
| 4664 | + | }, | |
| 4665 | + | "notes": "`repo` in the path is the repository's name in the team's workspace; a team has roles only on its own workspace's repositories. `role` is `read`, `triage`, `write`, `maintain` or `admin`. Everyone in the team and its child teams gets it; someone with a higher role otherwise keeps that. Needs the Admin role on the repository (`403` otherwise), and the `access:admin` scope. See [Access and roles](/guides/access-and-roles/)." | |
| 4666 | + | }, | |
| 4667 | + | "remove_team_repo": { | |
| 4668 | + | "params": { | |
| 4669 | + | "workspace": "flagon-io", | |
| 4670 | + | "team": "backend", | |
| 4671 | + | "repo": "hello" | |
| 4672 | + | }, | |
| 4673 | + | "response": true, | |
| 4674 | + | "notes": "A role the team inherits from a parent is taken away on the parent. Needs the Admin role on the repository, or to be an owner or one of the team's maintainers." | |
| 4675 | + | }, | |
| 4676 | + | "set_team_review_assignment": { | |
| 4677 | + | "params": { | |
| 4678 | + | "workspace": "flagon-io", | |
| 4679 | + | "team": "backend" | |
| 4680 | + | }, | |
| 4681 | + | "request": { | |
| 4682 | + | "enabled": true, | |
| 4683 | + | "algorithm": "load_balance", | |
| 4684 | + | "count": 2, | |
| 4685 | + | "skip_busy": true, | |
| 4686 | + | "busy_at": 5, | |
| 4687 | + | "excluded": [ | |
| 4688 | + | "syntaqx" | |
| 4689 | + | ] | |
| 4690 | + | }, | |
| 4691 | + | "response": { | |
| 4692 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4693 | + | "workspace": "flagon-io", | |
| 4694 | + | "slug": "backend", | |
| 4695 | + | "name": "Backend", | |
| 4696 | + | "description": "The API and the services behind it.", | |
| 4697 | + | "visibility": "visible", | |
| 4698 | + | "parent": { | |
| 4699 | + | "slug": "engineering", | |
| 4700 | + | "name": "Engineering" | |
| 4701 | + | }, | |
| 4702 | + | "notify": true, | |
| 4703 | + | "review_assignment": { | |
| 4704 | + | "enabled": true, | |
| 4705 | + | "algorithm": "load_balance", | |
| 4706 | + | "count": 2, | |
| 4707 | + | "skip_busy": true, | |
| 4708 | + | "busy_at": 5, | |
| 4709 | + | "include_child_teams": false, | |
| 4710 | + | "excluded": [ | |
| 4711 | + | "syntaqx" | |
| 4712 | + | ], | |
| 4713 | + | "notify_team": false | |
| 4714 | + | }, | |
| 4715 | + | "members_count": 4, | |
| 4716 | + | "repos_count": 2, | |
| 4717 | + | "child_teams_count": 1, | |
| 4718 | + | "viewer_role": "maintainer", | |
| 4719 | + | "can_manage": true, | |
| 4720 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4721 | + | "updated_at": "2026-10-07T09:44:03.512Z" | |
| 4722 | + | }, | |
| 4723 | + | "notes": "| Field | |\n| --- | --- |\n| `enabled` | Off, everyone in the team is asked. On, `count` people are picked and asked, and the team stays shown as asked beside them. |\n| `algorithm` | `round_robin`: whoever this team asked least recently. `load_balance`: whoever has the fewest pull requests waiting on their review. |\n| `count` | How many to pick, 1 to 10. People from the team already asked count towards it. |\n| `skip_busy`, `busy_at` | Leave out anyone with `busy_at` (1 to 100) or more pull requests waiting on their review. |\n| `include_child_teams` | Also pick from its child teams' people. |\n| `excluded` | Usernames never picked. Replaces the whole list. |\n| `notify_team` | Also tell the rest of the team when people are picked. |\n\nFields left out keep their value. The pull request's author is never picked. See [Teams](/guides/teams/)." | |
| 4724 | + | }, | |
| 4725 | + | "list_user_teams": { | |
| 4726 | + | "params": { | |
| 4727 | + | "workspace": "flagon-io", | |
| 4728 | + | "username": "ana" | |
| 4729 | + | }, | |
| 4730 | + | "response": [ | |
| 4731 | + | { | |
| 4732 | + | "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g", | |
| 4733 | + | "workspace": "flagon-io", | |
| 4734 | + | "slug": "backend", | |
| 4735 | + | "name": "Backend", | |
| 4736 | + | "description": "The API and the services behind it.", | |
| 4737 | + | "visibility": "visible", | |
| 4738 | + | "parent": { | |
| 4739 | + | "slug": "engineering", | |
| 4740 | + | "name": "Engineering" | |
| 4741 | + | }, | |
| 4742 | + | "notify": true, | |
| 4743 | + | "review_assignment": { | |
| 4744 | + | "enabled": false, | |
| 4745 | + | "algorithm": "round_robin", | |
| 4746 | + | "count": 1, | |
| 4747 | + | "skip_busy": false, | |
| 4748 | + | "busy_at": 5, | |
| 4749 | + | "include_child_teams": false, | |
| 4750 | + | "excluded": [], | |
| 4751 | + | "notify_team": false | |
| 4752 | + | }, | |
| 4753 | + | "members_count": 4, | |
| 4754 | + | "repos_count": 2, | |
| 4755 | + | "child_teams_count": 1, | |
| 4756 | + | "viewer_role": null, | |
| 4757 | + | "can_manage": false, | |
| 4758 | + | "created_at": "2026-10-06T15:02:11.480Z", | |
| 4759 | + | "updated_at": "2026-10-06T15:02:11.480Z" | |
| 4760 | + | } | |
| 4761 | + | ], | |
| 4762 | + | "notes": "Only the teams they are in themselves, not the parents those teams are under. Secret teams you are not in are left out unless you are an owner. `403` for anyone who is not a member of the workspace." | |
| 4763 | + | }, | |
| 4114 | 4764 | "list_security_alerts": { | |
| 4115 | 4765 | "params": { | |
| 4116 | 4766 | "owner": "flagon-io", | |
| ⋯ | |||
| 5180 | 5830 | } | |
| 5181 | 5831 | ], | |
| 5182 | 5832 | "notes": "Name every pinned project once; anything else is refused with `422 invalid`." | |
| 5833 | + | }, | |
| 5834 | + | "list_secret_scanning_alerts": { | |
| 5835 | + | "params": { | |
| 5836 | + | "owner": "flagon-io", | |
| 5837 | + | "name": "hello" | |
| 5838 | + | }, | |
| 5839 | + | "query": { | |
| 5840 | + | "state": "open" | |
| 5841 | + | }, | |
| 5842 | + | "response": [ | |
| 5843 | + | { | |
| 5844 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 5845 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 5846 | + | "kind": "github_token", | |
| 5847 | + | "label": "a GitHub token", | |
| 5848 | + | "path": "scripts/release.sh", | |
| 5849 | + | "line": 12, | |
| 5850 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 5851 | + | "preview": "ghp_X7…", | |
| 5852 | + | "status": "open", | |
| 5853 | + | "source": "push", | |
| 5854 | + | "found_by": "syntaqx", | |
| 5855 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 5856 | + | "decided_by": null, | |
| 5857 | + | "reason": null, | |
| 5858 | + | "decided_at": null, | |
| 5859 | + | "dismissed_reason": null, | |
| 5860 | + | "test_value": null, | |
| 5861 | + | "state": "open", | |
| 5862 | + | "validity": "active", | |
| 5863 | + | "validity_checked_at": "2026-10-06T09:20:41.502Z", | |
| 5864 | + | "bypass": { | |
| 5865 | + | "reason": "will_fix_later", | |
| 5866 | + | "comment": "Rotating it this afternoon.", | |
| 5867 | + | "by": "syntaqx", | |
| 5868 | + | "at": "2026-10-06T09:14:02.118Z", | |
| 5869 | + | "approved_by": null | |
| 5870 | + | }, | |
| 5871 | + | "pattern_id": null, | |
| 5872 | + | "pattern_name": null, | |
| 5873 | + | "locations": 1 | |
| 5874 | + | } | |
| 5875 | + | ], | |
| 5876 | + | "notes": "| Filter | Values |\n| --- | --- |\n| `state` | `open` (in the history, or blocked at a push), `dismissed`, `fixed` |\n| `secret_type` | `aws_access_key`, `github_token`, `custom_pattern`, … |\n| `validity` | `active`, `inactive`, `unknown`, `unsupported` |\n| `bypassed` | `true` or `false` |\n\nThe secret itself is never returned: `preview` is enough to recognise it. `status` says where it stands: `open`, `blocked` (stopped at a push, never landed), `allowed` or `resolved`." | |
| 5877 | + | }, | |
| 5878 | + | "list_secret_scanning_alerts_for_workspace": { | |
| 5879 | + | "params": { | |
| 5880 | + | "workspace": "flagon-io" | |
| 5881 | + | }, | |
| 5882 | + | "query": { | |
| 5883 | + | "state": "open" | |
| 5884 | + | }, | |
| 5885 | + | "response": [ | |
| 5886 | + | { | |
| 5887 | + | "repo": "hello", | |
| 5888 | + | "secret": { | |
| 5889 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 5890 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 5891 | + | "kind": "github_token", | |
| 5892 | + | "label": "a GitHub token", | |
| 5893 | + | "path": "scripts/release.sh", | |
| 5894 | + | "line": 12, | |
| 5895 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 5896 | + | "preview": "ghp_X7…", | |
| 5897 | + | "status": "open", | |
| 5898 | + | "source": "push", | |
| 5899 | + | "found_by": "syntaqx", | |
| 5900 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 5901 | + | "decided_by": null, | |
| 5902 | + | "reason": null, | |
| 5903 | + | "decided_at": null, | |
| 5904 | + | "dismissed_reason": null, | |
| 5905 | + | "test_value": null, | |
| 5906 | + | "state": "open", | |
| 5907 | + | "validity": "active", | |
| 5908 | + | "validity_checked_at": "2026-10-06T09:20:41.502Z", | |
| 5909 | + | "bypass": { | |
| 5910 | + | "reason": "will_fix_later", | |
| 5911 | + | "comment": "Rotating it this afternoon.", | |
| 5912 | + | "by": "syntaqx", | |
| 5913 | + | "at": "2026-10-06T09:14:02.118Z", | |
| 5914 | + | "approved_by": null | |
| 5915 | + | }, | |
| 5916 | + | "pattern_id": null, | |
| 5917 | + | "pattern_name": null, | |
| 5918 | + | "locations": 1 | |
| 5919 | + | } | |
| 5920 | + | } | |
| 5921 | + | ], | |
| 5922 | + | "notes": "Every repository whose findings you may see, each alert with its repository's name." | |
| 5923 | + | }, | |
| 5924 | + | "get_secret_scanning_alert": { | |
| 5925 | + | "params": { | |
| 5926 | + | "owner": "flagon-io", | |
| 5927 | + | "name": "hello", | |
| 5928 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p" | |
| 5929 | + | }, | |
| 5930 | + | "response": { | |
| 5931 | + | "secret": { | |
| 5932 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 5933 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 5934 | + | "kind": "github_token", | |
| 5935 | + | "label": "a GitHub token", | |
| 5936 | + | "path": "scripts/release.sh", | |
| 5937 | + | "line": 12, | |
| 5938 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 5939 | + | "preview": "ghp_X7…", | |
| 5940 | + | "status": "blocked", | |
| 5941 | + | "source": "push", | |
| 5942 | + | "found_by": "syntaqx", | |
| 5943 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 5944 | + | "decided_by": null, | |
| 5945 | + | "reason": null, | |
| 5946 | + | "decided_at": null, | |
| 5947 | + | "dismissed_reason": null, | |
| 5948 | + | "test_value": null, | |
| 5949 | + | "state": "open", | |
| 5950 | + | "validity": null, | |
| 5951 | + | "validity_checked_at": null, | |
| 5952 | + | "bypass": null, | |
| 5953 | + | "pattern_id": null, | |
| 5954 | + | "pattern_name": null, | |
| 5955 | + | "locations": 1 | |
| 5956 | + | }, | |
| 5957 | + | "locations": [ | |
| 5958 | + | { | |
| 5959 | + | "path": "scripts/release.sh", | |
| 5960 | + | "line": 12, | |
| 5961 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 5962 | + | "source": "push", | |
| 5963 | + | "found_at": "2026-10-06T09:14:02.118Z" | |
| 5964 | + | } | |
| 5965 | + | ], | |
| 5966 | + | "activity": [], | |
| 5967 | + | "requests": [], | |
| 5968 | + | "checkable": true, | |
| 5969 | + | "can_bypass": true, | |
| 5970 | + | "can_request_bypass": false | |
| 5971 | + | } | |
| 5972 | + | }, | |
| 5973 | + | "update_secret_scanning_alert": { | |
| 5974 | + | "params": { | |
| 5975 | + | "owner": "flagon-io", | |
| 5976 | + | "name": "hello", | |
| 5977 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p" | |
| 5978 | + | }, | |
| 5979 | + | "request": { | |
| 5980 | + | "state": "dismissed", | |
| 5981 | + | "reason": "revoked", | |
| 5982 | + | "comment": "Rotated in the issuer's settings." | |
| 5983 | + | }, | |
| 5984 | + | "response": { | |
| 5985 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 5986 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 5987 | + | "kind": "github_token", | |
| 5988 | + | "label": "a GitHub token", | |
| 5989 | + | "path": "scripts/release.sh", | |
| 5990 | + | "line": 12, | |
| 5991 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 5992 | + | "preview": "ghp_X7…", | |
| 5993 | + | "status": "resolved", | |
| 5994 | + | "source": "push", | |
| 5995 | + | "found_by": "syntaqx", | |
| 5996 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 5997 | + | "decided_by": "syntaqx", | |
| 5998 | + | "reason": "Rotated in the issuer's settings.", | |
| 5999 | + | "decided_at": "2026-10-06T09:20:41.502Z", | |
| 6000 | + | "dismissed_reason": "revoked", | |
| 6001 | + | "test_value": null, | |
| 6002 | + | "state": "fixed", | |
| 6003 | + | "validity": "active", | |
| 6004 | + | "validity_checked_at": "2026-10-06T09:20:41.502Z", | |
| 6005 | + | "bypass": { | |
| 6006 | + | "reason": "will_fix_later", | |
| 6007 | + | "comment": "Rotating it this afternoon.", | |
| 6008 | + | "by": "syntaqx", | |
| 6009 | + | "at": "2026-10-06T09:14:02.118Z", | |
| 6010 | + | "approved_by": null | |
| 6011 | + | }, | |
| 6012 | + | "pattern_id": null, | |
| 6013 | + | "pattern_name": null, | |
| 6014 | + | "locations": 1 | |
| 6015 | + | }, | |
| 6016 | + | "notes": "Takes the Admin role: a dismissed secret is let through push protection, unless it was `revoked`, which marks it fixed. `state` `open` reopens it." | |
| 6017 | + | }, | |
| 6018 | + | "list_secret_scanning_locations": { | |
| 6019 | + | "params": { | |
| 6020 | + | "owner": "flagon-io", | |
| 6021 | + | "name": "hello", | |
| 6022 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p" | |
| 6023 | + | }, | |
| 6024 | + | "response": [ | |
| 6025 | + | { | |
| 6026 | + | "path": "scripts/release.sh", | |
| 6027 | + | "line": 12, | |
| 6028 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6029 | + | "source": "push", | |
| 6030 | + | "found_at": "2026-10-06T09:14:02.118Z" | |
| 6031 | + | } | |
| 6032 | + | ] | |
| 6033 | + | }, | |
| 6034 | + | "bypass_push_protection": { | |
| 6035 | + | "params": { | |
| 6036 | + | "owner": "flagon-io", | |
| 6037 | + | "name": "hello", | |
| 6038 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p" | |
| 6039 | + | }, | |
| 6040 | + | "request": { | |
| 6041 | + | "reason": "will_fix_later", | |
| 6042 | + | "comment": "Rotating it this afternoon." | |
| 6043 | + | }, | |
| 6044 | + | "response": { | |
| 6045 | + | "secret": { | |
| 6046 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 6047 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6048 | + | "kind": "github_token", | |
| 6049 | + | "label": "a GitHub token", | |
| 6050 | + | "path": "scripts/release.sh", | |
| 6051 | + | "line": 12, | |
| 6052 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6053 | + | "preview": "ghp_X7…", | |
| 6054 | + | "status": "open", | |
| 6055 | + | "source": "push", | |
| 6056 | + | "found_by": "syntaqx", | |
| 6057 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 6058 | + | "decided_by": null, | |
| 6059 | + | "reason": null, | |
| 6060 | + | "decided_at": null, | |
| 6061 | + | "dismissed_reason": null, | |
| 6062 | + | "test_value": null, | |
| 6063 | + | "state": "open", | |
| 6064 | + | "validity": null, | |
| 6065 | + | "validity_checked_at": null, | |
| 6066 | + | "bypass": { | |
| 6067 | + | "reason": "will_fix_later", | |
| 6068 | + | "comment": "Rotating it this afternoon.", | |
| 6069 | + | "by": "syntaqx", | |
| 6070 | + | "at": "2026-10-06T09:14:02.118Z", | |
| 6071 | + | "approved_by": null | |
| 6072 | + | }, | |
| 6073 | + | "pattern_id": null, | |
| 6074 | + | "pattern_name": null, | |
| 6075 | + | "locations": 1 | |
| 6076 | + | }, | |
| 6077 | + | "request": null | |
| 6078 | + | }, | |
| 6079 | + | "notes": "| Reason | The alert |\n| --- | --- |\n| `false_positive` | Closed as a false positive |\n| `used_in_tests` | Closed as used in tests |\n| `will_fix_later` | Stays open, to be rotated |\n\nWith delegated bypass on, a call from someone who does not review bypasses makes a request instead: `request` is set and the secret is still blocked until an owner or admin approves it. Push again once it is bypassed." | |
| 6080 | + | }, | |
| 6081 | + | "check_secret_validity": { | |
| 6082 | + | "params": { | |
| 6083 | + | "owner": "flagon-io", | |
| 6084 | + | "name": "hello", | |
| 6085 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p" | |
| 6086 | + | }, | |
| 6087 | + | "response": { | |
| 6088 | + | "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 6089 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6090 | + | "kind": "github_token", | |
| 6091 | + | "label": "a GitHub token", | |
| 6092 | + | "path": "scripts/release.sh", | |
| 6093 | + | "line": 12, | |
| 6094 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6095 | + | "preview": "ghp_X7…", | |
| 6096 | + | "status": "open", | |
| 6097 | + | "source": "push", | |
| 6098 | + | "found_by": "syntaqx", | |
| 6099 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 6100 | + | "decided_by": null, | |
| 6101 | + | "reason": null, | |
| 6102 | + | "decided_at": null, | |
| 6103 | + | "dismissed_reason": null, | |
| 6104 | + | "test_value": null, | |
| 6105 | + | "state": "open", | |
| 6106 | + | "validity": "active", | |
| 6107 | + | "validity_checked_at": "2026-10-06T09:20:41.502Z", | |
| 6108 | + | "bypass": { | |
| 6109 | + | "reason": "will_fix_later", | |
| 6110 | + | "comment": "Rotating it this afternoon.", | |
| 6111 | + | "by": "syntaqx", | |
| 6112 | + | "at": "2026-10-06T09:14:02.118Z", | |
| 6113 | + | "approved_by": null | |
| 6114 | + | }, | |
| 6115 | + | "pattern_id": null, | |
| 6116 | + | "pattern_name": null, | |
| 6117 | + | "locations": 1 | |
| 6118 | + | }, | |
| 6119 | + | "notes": "Made for GitHub, GitLab, Stripe, Slack, npm, OpenAI, Anthropic and SendGrid tokens, with the issuer's own read-only call. Other formats answer `unsupported`; a secret that never landed answers `unknown`." | |
| 6120 | + | }, | |
| 6121 | + | "list_bypass_requests": { | |
| 6122 | + | "params": { | |
| 6123 | + | "workspace": "flagon-io" | |
| 6124 | + | }, | |
| 6125 | + | "query": { | |
| 6126 | + | "state": "pending" | |
| 6127 | + | }, | |
| 6128 | + | "response": [ | |
| 6129 | + | { | |
| 6130 | + | "id": "byp_01kq2n8e5f6g7h8j9k0m1n2p3q", | |
| 6131 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6132 | + | "workspace": "flagon-io", | |
| 6133 | + | "repo": "hello", | |
| 6134 | + | "secret_id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 6135 | + | "label": "a GitHub token", | |
| 6136 | + | "path": "scripts/release.sh", | |
| 6137 | + | "line": 12, | |
| 6138 | + | "preview": "ghp_X7…", | |
| 6139 | + | "requester": "ana", | |
| 6140 | + | "reason": "used_in_tests", | |
| 6141 | + | "comment": "A token from the test fixtures, never issued.", | |
| 6142 | + | "state": "pending", | |
| 6143 | + | "reviewer": null, | |
| 6144 | + | "review_comment": null, | |
| 6145 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6146 | + | "reviewed_at": null | |
| 6147 | + | } | |
| 6148 | + | ] | |
| 6149 | + | }, | |
| 6150 | + | "review_bypass_request": { | |
| 6151 | + | "params": { | |
| 6152 | + | "workspace": "flagon-io", | |
| 6153 | + | "id": "byp_01kq2n8e5f6g7h8j9k0m1n2p3q" | |
| 6154 | + | }, | |
| 6155 | + | "request": { | |
| 6156 | + | "decision": "approve", | |
| 6157 | + | "comment": "A fixture." | |
| 6158 | + | }, | |
| 6159 | + | "response": { | |
| 6160 | + | "id": "byp_01kq2n8e5f6g7h8j9k0m1n2p3q", | |
| 6161 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6162 | + | "workspace": "flagon-io", | |
| 6163 | + | "repo": "hello", | |
| 6164 | + | "secret_id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p", | |
| 6165 | + | "label": "a GitHub token", | |
| 6166 | + | "path": "scripts/release.sh", | |
| 6167 | + | "line": 12, | |
| 6168 | + | "preview": "ghp_X7…", | |
| 6169 | + | "requester": "ana", | |
| 6170 | + | "reason": "used_in_tests", | |
| 6171 | + | "comment": "A token from the test fixtures, never issued.", | |
| 6172 | + | "state": "approved", | |
| 6173 | + | "reviewer": "syntaqx", | |
| 6174 | + | "review_comment": "A fixture.", | |
| 6175 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6176 | + | "reviewed_at": "2026-10-06T09:20:41.502Z" | |
| 6177 | + | } | |
| 6178 | + | }, | |
| 6179 | + | "list_custom_patterns": { | |
| 6180 | + | "params": { | |
| 6181 | + | "owner": "flagon-io", | |
| 6182 | + | "name": "hello" | |
| 6183 | + | }, | |
| 6184 | + | "response": { | |
| 6185 | + | "patterns": [ | |
| 6186 | + | { | |
| 6187 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r", | |
| 6188 | + | "scope": "repository", | |
| 6189 | + | "workspace": "flagon-io", | |
| 6190 | + | "repo": "hello", | |
| 6191 | + | "name": "Acme API key", | |
| 6192 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6193 | + | "before": null, | |
| 6194 | + | "after": null, | |
| 6195 | + | "test_strings": [ | |
| 6196 | + | "ACME_KEY=acme_0123456789abcdef0123456789abcdef" | |
| 6197 | + | ], | |
| 6198 | + | "state": "published", | |
| 6199 | + | "created_by": "syntaqx", | |
| 6200 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6201 | + | "updated_by": "syntaqx", | |
| 6202 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6203 | + | "open_alerts": 0 | |
| 6204 | + | } | |
| 6205 | + | ], | |
| 6206 | + | "entitled": true | |
| 6207 | + | }, | |
| 6208 | + | "notes": "A repository's list includes its workspace's patterns, with `scope` `workspace`. `entitled` says whether they run here: always on a public repository, and on a private one with the Security and quality activation." | |
| 6209 | + | }, | |
| 6210 | + | "list_custom_patterns_for_workspace": { | |
| 6211 | + | "params": { | |
| 6212 | + | "workspace": "flagon-io" | |
| 6213 | + | }, | |
| 6214 | + | "response": { | |
| 6215 | + | "patterns": [ | |
| 6216 | + | { | |
| 6217 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r", | |
| 6218 | + | "scope": "workspace", | |
| 6219 | + | "workspace": "flagon-io", | |
| 6220 | + | "repo": null, | |
| 6221 | + | "name": "Acme API key", | |
| 6222 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6223 | + | "before": null, | |
| 6224 | + | "after": null, | |
| 6225 | + | "test_strings": [ | |
| 6226 | + | "ACME_KEY=acme_0123456789abcdef0123456789abcdef" | |
| 6227 | + | ], | |
| 6228 | + | "state": "published", | |
| 6229 | + | "created_by": "syntaqx", | |
| 6230 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6231 | + | "updated_by": "syntaqx", | |
| 6232 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6233 | + | "open_alerts": 0 | |
| 6234 | + | } | |
| 6235 | + | ], | |
| 6236 | + | "entitled": true | |
| 6237 | + | } | |
| 6238 | + | }, | |
| 6239 | + | "create_custom_pattern": { | |
| 6240 | + | "params": { | |
| 6241 | + | "owner": "flagon-io", | |
| 6242 | + | "name": "hello" | |
| 6243 | + | }, | |
| 6244 | + | "request": { | |
| 6245 | + | "pattern_name": "Acme API key", | |
| 6246 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6247 | + | "test_strings": [ | |
| 6248 | + | "ACME_KEY=acme_0123456789abcdef0123456789abcdef" | |
| 6249 | + | ], | |
| 6250 | + | "publish": true | |
| 6251 | + | }, | |
| 6252 | + | "response": { | |
| 6253 | + | "pattern": { | |
| 6254 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r", | |
| 6255 | + | "scope": "repository", | |
| 6256 | + | "workspace": "flagon-io", | |
| 6257 | + | "repo": "hello", | |
| 6258 | + | "name": "Acme API key", | |
| 6259 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6260 | + | "before": null, | |
| 6261 | + | "after": null, | |
| 6262 | + | "test_strings": [ | |
| 6263 | + | "ACME_KEY=acme_0123456789abcdef0123456789abcdef" | |
| 6264 | + | ], | |
| 6265 | + | "state": "published", | |
| 6266 | + | "created_by": "syntaqx", | |
| 6267 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6268 | + | "updated_by": "syntaqx", | |
| 6269 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6270 | + | "open_alerts": 0 | |
| 6271 | + | }, | |
| 6272 | + | "tests": [ | |
| 6273 | + | [ | |
| 6274 | + | 9, | |
| 6275 | + | 46 | |
| 6276 | + | ] | |
| 6277 | + | ] | |
| 6278 | + | }, | |
| 6279 | + | "notes": "`tests` gives, for each test string, where the pattern matched (start and end, in characters), or `null`. A pattern that does not compile, matches an empty string, or is too complex is refused with `422` and says why." | |
| 6280 | + | }, | |
| 6281 | + | "create_custom_pattern_for_workspace": { | |
| 6282 | + | "params": { | |
| 6283 | + | "workspace": "flagon-io" | |
| 6284 | + | }, | |
| 6285 | + | "request": { | |
| 6286 | + | "pattern_name": "Acme API key", | |
| 6287 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6288 | + | "publish": false | |
| 6289 | + | }, | |
| 6290 | + | "response": { | |
| 6291 | + | "pattern": { | |
| 6292 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r", | |
| 6293 | + | "scope": "workspace", | |
| 6294 | + | "workspace": "flagon-io", | |
| 6295 | + | "repo": null, | |
| 6296 | + | "name": "Acme API key", | |
| 6297 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6298 | + | "before": null, | |
| 6299 | + | "after": null, | |
| 6300 | + | "test_strings": [], | |
| 6301 | + | "state": "draft", | |
| 6302 | + | "created_by": "syntaqx", | |
| 6303 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6304 | + | "updated_by": "syntaqx", | |
| 6305 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6306 | + | "open_alerts": 0 | |
| 6307 | + | }, | |
| 6308 | + | "tests": [] | |
| 6309 | + | } | |
| 6310 | + | }, | |
| 6311 | + | "update_custom_pattern": { | |
| 6312 | + | "params": { | |
| 6313 | + | "owner": "flagon-io", | |
| 6314 | + | "name": "hello", | |
| 6315 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r" | |
| 6316 | + | }, | |
| 6317 | + | "request": { | |
| 6318 | + | "pattern_name": "Acme API key", | |
| 6319 | + | "pattern": "acme_[a-z0-9]{32,40}", | |
| 6320 | + | "publish": true | |
| 6321 | + | }, | |
| 6322 | + | "response": { | |
| 6323 | + | "pattern": { | |
| 6324 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r", | |
| 6325 | + | "scope": "repository", | |
| 6326 | + | "workspace": "flagon-io", | |
| 6327 | + | "repo": "hello", | |
| 6328 | + | "name": "Acme API key", | |
| 6329 | + | "pattern": "acme_[a-z0-9]{32,40}", | |
| 6330 | + | "before": null, | |
| 6331 | + | "after": null, | |
| 6332 | + | "test_strings": [ | |
| 6333 | + | "ACME_KEY=acme_0123456789abcdef0123456789abcdef" | |
| 6334 | + | ], | |
| 6335 | + | "state": "published", | |
| 6336 | + | "created_by": "syntaqx", | |
| 6337 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6338 | + | "updated_by": "syntaqx", | |
| 6339 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6340 | + | "open_alerts": 0 | |
| 6341 | + | }, | |
| 6342 | + | "tests": [ | |
| 6343 | + | [ | |
| 6344 | + | 9, | |
| 6345 | + | 46 | |
| 6346 | + | ] | |
| 6347 | + | ] | |
| 6348 | + | } | |
| 6349 | + | }, | |
| 6350 | + | "update_custom_pattern_for_workspace": { | |
| 6351 | + | "params": { | |
| 6352 | + | "workspace": "flagon-io", | |
| 6353 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r" | |
| 6354 | + | }, | |
| 6355 | + | "request": { | |
| 6356 | + | "pattern_name": "Acme API key", | |
| 6357 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6358 | + | "publish": true | |
| 6359 | + | }, | |
| 6360 | + | "response": { | |
| 6361 | + | "pattern": { | |
| 6362 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r", | |
| 6363 | + | "scope": "workspace", | |
| 6364 | + | "workspace": "flagon-io", | |
| 6365 | + | "repo": null, | |
| 6366 | + | "name": "Acme API key", | |
| 6367 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6368 | + | "before": null, | |
| 6369 | + | "after": null, | |
| 6370 | + | "test_strings": [ | |
| 6371 | + | "ACME_KEY=acme_0123456789abcdef0123456789abcdef" | |
| 6372 | + | ], | |
| 6373 | + | "state": "published", | |
| 6374 | + | "created_by": "syntaqx", | |
| 6375 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6376 | + | "updated_by": "syntaqx", | |
| 6377 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6378 | + | "open_alerts": 0 | |
| 6379 | + | }, | |
| 6380 | + | "tests": [ | |
| 6381 | + | [ | |
| 6382 | + | 9, | |
| 6383 | + | 46 | |
| 6384 | + | ] | |
| 6385 | + | ] | |
| 6386 | + | } | |
| 6387 | + | }, | |
| 6388 | + | "delete_custom_pattern": { | |
| 6389 | + | "params": { | |
| 6390 | + | "owner": "flagon-io", | |
| 6391 | + | "name": "hello", | |
| 6392 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r" | |
| 6393 | + | }, | |
| 6394 | + | "response": { | |
| 6395 | + | "deleted": true | |
| 6396 | + | } | |
| 6397 | + | }, | |
| 6398 | + | "delete_custom_pattern_for_workspace": { | |
| 6399 | + | "params": { | |
| 6400 | + | "workspace": "flagon-io", | |
| 6401 | + | "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r" | |
| 6402 | + | }, | |
| 6403 | + | "response": { | |
| 6404 | + | "deleted": true | |
| 6405 | + | } | |
| 6406 | + | }, | |
| 6407 | + | "dry_run_custom_pattern": { | |
| 6408 | + | "params": { | |
| 6409 | + | "owner": "flagon-io", | |
| 6410 | + | "name": "hello" | |
| 6411 | + | }, | |
| 6412 | + | "request": { | |
| 6413 | + | "pattern": "acme_[a-z0-9]{32}" | |
| 6414 | + | }, | |
| 6415 | + | "response": { | |
| 6416 | + | "repos": [ | |
| 6417 | + | { | |
| 6418 | + | "name": "hello", | |
| 6419 | + | "files_scanned": 214, | |
| 6420 | + | "matches": [ | |
| 6421 | + | { | |
| 6422 | + | "path": "config/dev.env", | |
| 6423 | + | "line": 3, | |
| 6424 | + | "preview": "ACME_KEY=acme_01••••••••••••••••••••••••" | |
| 6425 | + | } | |
| 6426 | + | ], | |
| 6427 | + | "truncated": false, | |
| 6428 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291" | |
| 6429 | + | } | |
| 6430 | + | ] | |
| 6431 | + | }, | |
| 6432 | + | "notes": "Reads up to 2,000 files and 20 MB of the default branch, skipping what secret scanning skips. Nothing is recorded." | |
| 6433 | + | }, | |
| 6434 | + | "dry_run_custom_pattern_for_workspace": { | |
| 6435 | + | "params": { | |
| 6436 | + | "workspace": "flagon-io" | |
| 6437 | + | }, | |
| 6438 | + | "request": { | |
| 6439 | + | "pattern": "acme_[a-z0-9]{32}", | |
| 6440 | + | "repos": [ | |
| 6441 | + | "hello" | |
| 6442 | + | ] | |
| 6443 | + | }, | |
| 6444 | + | "response": { | |
| 6445 | + | "repos": [ | |
| 6446 | + | { | |
| 6447 | + | "name": "hello", | |
| 6448 | + | "files_scanned": 214, | |
| 6449 | + | "matches": [], | |
| 6450 | + | "truncated": false, | |
| 6451 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291" | |
| 6452 | + | } | |
| 6453 | + | ] | |
| 6454 | + | } | |
| 6455 | + | }, | |
| 6456 | + | "list_code_scanning_alerts": { | |
| 6457 | + | "params": { | |
| 6458 | + | "owner": "flagon-io", | |
| 6459 | + | "name": "hello" | |
| 6460 | + | }, | |
| 6461 | + | "query": { | |
| 6462 | + | "state": "open", | |
| 6463 | + | "severity": "high" | |
| 6464 | + | }, | |
| 6465 | + | "response": [ | |
| 6466 | + | { | |
| 6467 | + | "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s", | |
| 6468 | + | "number": 4, | |
| 6469 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6470 | + | "tool": "Semgrep OSS", | |
| 6471 | + | "category": "Semgrep OSS", | |
| 6472 | + | "rule_id": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6473 | + | "rule_name": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6474 | + | "rule_description": "Detected calls to child_process from a function argument.", | |
| 6475 | + | "help": null, | |
| 6476 | + | "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process", | |
| 6477 | + | "tags": [ | |
| 6478 | + | "security", | |
| 6479 | + | "CWE-78" | |
| 6480 | + | ], | |
| 6481 | + | "level": "error", | |
| 6482 | + | "security_severity": null, | |
| 6483 | + | "severity": "high", | |
| 6484 | + | "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.", | |
| 6485 | + | "path": "src/server.js", | |
| 6486 | + | "start_line": 6, | |
| 6487 | + | "end_line": 6, | |
| 6488 | + | "start_column": 3, | |
| 6489 | + | "end_column": 60, | |
| 6490 | + | "state": "open", | |
| 6491 | + | "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6", | |
| 6492 | + | "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6493 | + | "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6494 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6495 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6496 | + | "fixed_at": null, | |
| 6497 | + | "dismissed_by": null, | |
| 6498 | + | "dismissed_reason": null, | |
| 6499 | + | "dismissed_comment": null, | |
| 6500 | + | "dismissed_at": null, | |
| 6501 | + | "issue": null | |
| 6502 | + | } | |
| 6503 | + | ], | |
| 6504 | + | "notes": "`severity` is the rule's security severity when it has one (from its `security-severity` score), else from the result's level: `error` high, `warning` medium, `note` low." | |
| 6505 | + | }, | |
| 6506 | + | "list_code_scanning_alerts_for_workspace": { | |
| 6507 | + | "params": { | |
| 6508 | + | "workspace": "flagon-io" | |
| 6509 | + | }, | |
| 6510 | + | "query": { | |
| 6511 | + | "state": "open" | |
| 6512 | + | }, | |
| 6513 | + | "response": [ | |
| 6514 | + | { | |
| 6515 | + | "repo": "hello", | |
| 6516 | + | "code": { | |
| 6517 | + | "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s", | |
| 6518 | + | "number": 4, | |
| 6519 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6520 | + | "tool": "Semgrep OSS", | |
| 6521 | + | "category": "Semgrep OSS", | |
| 6522 | + | "rule_id": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6523 | + | "rule_name": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6524 | + | "rule_description": "Detected calls to child_process from a function argument.", | |
| 6525 | + | "help": null, | |
| 6526 | + | "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process", | |
| 6527 | + | "tags": [ | |
| 6528 | + | "security", | |
| 6529 | + | "CWE-78" | |
| 6530 | + | ], | |
| 6531 | + | "level": "error", | |
| 6532 | + | "security_severity": null, | |
| 6533 | + | "severity": "high", | |
| 6534 | + | "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.", | |
| 6535 | + | "path": "src/server.js", | |
| 6536 | + | "start_line": 6, | |
| 6537 | + | "end_line": 6, | |
| 6538 | + | "start_column": 3, | |
| 6539 | + | "end_column": 60, | |
| 6540 | + | "state": "open", | |
| 6541 | + | "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6", | |
| 6542 | + | "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6543 | + | "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6544 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6545 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6546 | + | "fixed_at": null, | |
| 6547 | + | "dismissed_by": null, | |
| 6548 | + | "dismissed_reason": null, | |
| 6549 | + | "dismissed_comment": null, | |
| 6550 | + | "dismissed_at": null, | |
| 6551 | + | "issue": null | |
| 6552 | + | } | |
| 6553 | + | } | |
| 6554 | + | ] | |
| 6555 | + | }, | |
| 6556 | + | "get_code_scanning_alert": { | |
| 6557 | + | "params": { | |
| 6558 | + | "owner": "flagon-io", | |
| 6559 | + | "name": "hello", | |
| 6560 | + | "number": 4 | |
| 6561 | + | }, | |
| 6562 | + | "response": { | |
| 6563 | + | "alert": { | |
| 6564 | + | "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s", | |
| 6565 | + | "number": 4, | |
| 6566 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6567 | + | "tool": "Semgrep OSS", | |
| 6568 | + | "category": "Semgrep OSS", | |
| 6569 | + | "rule_id": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6570 | + | "rule_name": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6571 | + | "rule_description": "Detected calls to child_process from a function argument.", | |
| 6572 | + | "help": null, | |
| 6573 | + | "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process", | |
| 6574 | + | "tags": [ | |
| 6575 | + | "security", | |
| 6576 | + | "CWE-78" | |
| 6577 | + | ], | |
| 6578 | + | "level": "error", | |
| 6579 | + | "security_severity": null, | |
| 6580 | + | "severity": "high", | |
| 6581 | + | "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.", | |
| 6582 | + | "path": "src/server.js", | |
| 6583 | + | "start_line": 6, | |
| 6584 | + | "end_line": 6, | |
| 6585 | + | "start_column": 3, | |
| 6586 | + | "end_column": 60, | |
| 6587 | + | "state": "open", | |
| 6588 | + | "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6", | |
| 6589 | + | "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6590 | + | "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6591 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6592 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6593 | + | "fixed_at": null, | |
| 6594 | + | "dismissed_by": null, | |
| 6595 | + | "dismissed_reason": null, | |
| 6596 | + | "dismissed_comment": null, | |
| 6597 | + | "dismissed_at": null, | |
| 6598 | + | "issue": null | |
| 6599 | + | }, | |
| 6600 | + | "activity": [], | |
| 6601 | + | "analyses": [ | |
| 6602 | + | { | |
| 6603 | + | "id": "ana_01kq2rbh8j9k0m1n2p3q4r5s6t", | |
| 6604 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6605 | + | "sarif_id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v", | |
| 6606 | + | "tool": "Semgrep OSS", | |
| 6607 | + | "tool_version": "1.140.0", | |
| 6608 | + | "category": "Semgrep OSS", | |
| 6609 | + | "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6610 | + | "git_ref": "refs/heads/main", | |
| 6611 | + | "pull": null, | |
| 6612 | + | "results": 7, | |
| 6613 | + | "new_alerts": 2, | |
| 6614 | + | "fixed_alerts": 1, | |
| 6615 | + | "dropped": 0, | |
| 6616 | + | "created_at": "2026-10-06T09:14:02.118Z" | |
| 6617 | + | } | |
| 6618 | + | ] | |
| 6619 | + | } | |
| 6620 | + | }, | |
| 6621 | + | "update_code_scanning_alert": { | |
| 6622 | + | "params": { | |
| 6623 | + | "owner": "flagon-io", | |
| 6624 | + | "name": "hello", | |
| 6625 | + | "number": 4 | |
| 6626 | + | }, | |
| 6627 | + | "request": { | |
| 6628 | + | "state": "dismissed", | |
| 6629 | + | "dismissed_reason": "false_positive", | |
| 6630 | + | "dismissed_comment": "The argument is a constant." | |
| 6631 | + | }, | |
| 6632 | + | "response": { | |
| 6633 | + | "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s", | |
| 6634 | + | "number": 4, | |
| 6635 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6636 | + | "tool": "Semgrep OSS", | |
| 6637 | + | "category": "Semgrep OSS", | |
| 6638 | + | "rule_id": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6639 | + | "rule_name": "javascript.lang.security.detect-child-process.detect-child-process", | |
| 6640 | + | "rule_description": "Detected calls to child_process from a function argument.", | |
| 6641 | + | "help": null, | |
| 6642 | + | "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process", | |
| 6643 | + | "tags": [ | |
| 6644 | + | "security", | |
| 6645 | + | "CWE-78" | |
| 6646 | + | ], | |
| 6647 | + | "level": "error", | |
| 6648 | + | "security_severity": null, | |
| 6649 | + | "severity": "high", | |
| 6650 | + | "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.", | |
| 6651 | + | "path": "src/server.js", | |
| 6652 | + | "start_line": 6, | |
| 6653 | + | "end_line": 6, | |
| 6654 | + | "start_column": 3, | |
| 6655 | + | "end_column": 60, | |
| 6656 | + | "state": "dismissed", | |
| 6657 | + | "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6", | |
| 6658 | + | "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6659 | + | "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6660 | + | "created_at": "2026-10-06T09:14:02.118Z", | |
| 6661 | + | "updated_at": "2026-10-06T09:14:02.118Z", | |
| 6662 | + | "fixed_at": null, | |
| 6663 | + | "dismissed_by": "syntaqx", | |
| 6664 | + | "dismissed_reason": "false_positive", | |
| 6665 | + | "dismissed_comment": "The argument is a constant.", | |
| 6666 | + | "dismissed_at": "2026-10-06T09:20:41.502Z", | |
| 6667 | + | "issue": null | |
| 6668 | + | } | |
| 6669 | + | }, | |
| 6670 | + | "list_code_scanning_analyses": { | |
| 6671 | + | "params": { | |
| 6672 | + | "owner": "flagon-io", | |
| 6673 | + | "name": "hello" | |
| 6674 | + | }, | |
| 6675 | + | "response": [ | |
| 6676 | + | { | |
| 6677 | + | "id": "ana_01kq2rbh8j9k0m1n2p3q4r5s6t", | |
| 6678 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6679 | + | "sarif_id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v", | |
| 6680 | + | "tool": "Semgrep OSS", | |
| 6681 | + | "tool_version": "1.140.0", | |
| 6682 | + | "category": "Semgrep OSS", | |
| 6683 | + | "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6684 | + | "git_ref": "refs/heads/main", | |
| 6685 | + | "pull": null, | |
| 6686 | + | "results": 7, | |
| 6687 | + | "new_alerts": 2, | |
| 6688 | + | "fixed_alerts": 1, | |
| 6689 | + | "dropped": 0, | |
| 6690 | + | "created_at": "2026-10-06T09:14:02.118Z" | |
| 6691 | + | } | |
| 6692 | + | ] | |
| 6693 | + | }, | |
| 6694 | + | "upload_sarif": { | |
| 6695 | + | "params": { | |
| 6696 | + | "owner": "flagon-io", | |
| 6697 | + | "name": "hello" | |
| 6698 | + | }, | |
| 6699 | + | "request": { | |
| 6700 | + | "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6701 | + | "ref": "refs/heads/main", | |
| 6702 | + | "sarif": "H4sIAAAAAAAA…", | |
| 6703 | + | "checkout_uri": "file:///home/runner/work/repo" | |
| 6704 | + | }, | |
| 6705 | + | "response": { | |
| 6706 | + | "id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v", | |
| 6707 | + | "processing_status": "complete", | |
| 6708 | + | "analyses": [ | |
| 6709 | + | "ana_01kq2rbh8j9k0m1n2p3q4r5s6t" | |
| 6710 | + | ], | |
| 6711 | + | "errors": [], | |
| 6712 | + | "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6713 | + | "git_ref": "refs/heads/main", | |
| 6714 | + | "created_at": "2026-10-06T09:14:02.118Z" | |
| 6715 | + | }, | |
| 6716 | + | "notes": "`sarif` is the SARIF 2.1.0 file gzipped, then base64-encoded: `gzip -c results.sarif | base64 -w0`. The upload is read at once: `processing_status` is `complete` or `failed`, with `errors` saying why. For `refs/pull/<number>/head`, the results become the pull request's `Code scanning` check instead of alerts." | |
| 6717 | + | }, | |
| 6718 | + | "get_sarif_upload": { | |
| 6719 | + | "params": { | |
| 6720 | + | "owner": "flagon-io", | |
| 6721 | + | "name": "hello", | |
| 6722 | + | "id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v" | |
| 6723 | + | }, | |
| 6724 | + | "response": { | |
| 6725 | + | "id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v", | |
| 6726 | + | "processing_status": "complete", | |
| 6727 | + | "analyses": [ | |
| 6728 | + | "ana_01kq2rbh8j9k0m1n2p3q4r5s6t" | |
| 6729 | + | ], | |
| 6730 | + | "errors": [], | |
| 6731 | + | "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6732 | + | "git_ref": "refs/heads/main", | |
| 6733 | + | "created_at": "2026-10-06T09:14:02.118Z" | |
| 6734 | + | } | |
| 6735 | + | }, | |
| 6736 | + | "list_vulnerability_alerts": { | |
| 6737 | + | "params": { | |
| 6738 | + | "owner": "flagon-io", | |
| 6739 | + | "name": "hello" | |
| 6740 | + | }, | |
| 6741 | + | "query": { | |
| 6742 | + | "state": "open" | |
| 6743 | + | }, | |
| 6744 | + | "response": [ | |
| 6745 | + | { | |
| 6746 | + | "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n", | |
| 6747 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6748 | + | "ecosystem": "npm", | |
| 6749 | + | "package": "lodash", | |
| 6750 | + | "version": "4.17.20", | |
| 6751 | + | "manifest": "package-lock.json", | |
| 6752 | + | "advisory": "GHSA-35jh-r3h4-6jhm", | |
| 6753 | + | "osv_id": "GHSA-35jh-r3h4-6jhm", | |
| 6754 | + | "summary": "Command Injection in lodash", | |
| 6755 | + | "severity": "high", | |
| 6756 | + | "fixed_version": "4.17.21", | |
| 6757 | + | "status": "open", | |
| 6758 | + | "issue": null, | |
| 6759 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 6760 | + | "fixed_at": null, | |
| 6761 | + | "state": "open", | |
| 6762 | + | "dismissed_by": null, | |
| 6763 | + | "dismissed_reason": null, | |
| 6764 | + | "dismissed_comment": null, | |
| 6765 | + | "dismissed_at": null, | |
| 6766 | + | "update": null | |
| 6767 | + | } | |
| 6768 | + | ] | |
| 6769 | + | }, | |
| 6770 | + | "list_vulnerability_alerts_for_workspace": { | |
| 6771 | + | "params": { | |
| 6772 | + | "workspace": "flagon-io" | |
| 6773 | + | }, | |
| 6774 | + | "query": { | |
| 6775 | + | "severity": "critical" | |
| 6776 | + | }, | |
| 6777 | + | "response": [ | |
| 6778 | + | { | |
| 6779 | + | "repo": "hello", | |
| 6780 | + | "vulnerability": { | |
| 6781 | + | "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n", | |
| 6782 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6783 | + | "ecosystem": "npm", | |
| 6784 | + | "package": "lodash", | |
| 6785 | + | "version": "4.17.20", | |
| 6786 | + | "manifest": "package-lock.json", | |
| 6787 | + | "advisory": "GHSA-35jh-r3h4-6jhm", | |
| 6788 | + | "osv_id": "GHSA-35jh-r3h4-6jhm", | |
| 6789 | + | "summary": "Command Injection in lodash", | |
| 6790 | + | "severity": "high", | |
| 6791 | + | "fixed_version": "4.17.21", | |
| 6792 | + | "status": "open", | |
| 6793 | + | "issue": null, | |
| 6794 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 6795 | + | "fixed_at": null, | |
| 6796 | + | "state": "open", | |
| 6797 | + | "dismissed_by": null, | |
| 6798 | + | "dismissed_reason": null, | |
| 6799 | + | "dismissed_comment": null, | |
| 6800 | + | "dismissed_at": null, | |
| 6801 | + | "update": null | |
| 6802 | + | } | |
| 6803 | + | } | |
| 6804 | + | ] | |
| 6805 | + | }, | |
| 6806 | + | "get_vulnerability_alert": { | |
| 6807 | + | "params": { | |
| 6808 | + | "owner": "flagon-io", | |
| 6809 | + | "name": "hello", | |
| 6810 | + | "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n" | |
| 6811 | + | }, | |
| 6812 | + | "response": { | |
| 6813 | + | "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n", | |
| 6814 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6815 | + | "ecosystem": "npm", | |
| 6816 | + | "package": "lodash", | |
| 6817 | + | "version": "4.17.20", | |
| 6818 | + | "manifest": "package-lock.json", | |
| 6819 | + | "advisory": "GHSA-35jh-r3h4-6jhm", | |
| 6820 | + | "osv_id": "GHSA-35jh-r3h4-6jhm", | |
| 6821 | + | "summary": "Command Injection in lodash", | |
| 6822 | + | "severity": "high", | |
| 6823 | + | "fixed_version": "4.17.21", | |
| 6824 | + | "status": "open", | |
| 6825 | + | "issue": null, | |
| 6826 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 6827 | + | "fixed_at": null, | |
| 6828 | + | "state": "open", | |
| 6829 | + | "dismissed_by": null, | |
| 6830 | + | "dismissed_reason": null, | |
| 6831 | + | "dismissed_comment": null, | |
| 6832 | + | "dismissed_at": null, | |
| 6833 | + | "update": null | |
| 6834 | + | } | |
| 6835 | + | }, | |
| 6836 | + | "update_vulnerability_alert": { | |
| 6837 | + | "params": { | |
| 6838 | + | "owner": "flagon-io", | |
| 6839 | + | "name": "hello", | |
| 6840 | + | "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n" | |
| 6841 | + | }, | |
| 6842 | + | "request": { | |
| 6843 | + | "state": "dismissed", | |
| 6844 | + | "reason": "tolerable_risk", | |
| 6845 | + | "comment": "Only the build uses it." | |
| 6846 | + | }, | |
| 6847 | + | "response": { | |
| 6848 | + | "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n", | |
| 6849 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 6850 | + | "ecosystem": "npm", | |
| 6851 | + | "package": "lodash", | |
| 6852 | + | "version": "4.17.20", | |
| 6853 | + | "manifest": "package-lock.json", | |
| 6854 | + | "advisory": "GHSA-35jh-r3h4-6jhm", | |
| 6855 | + | "osv_id": "GHSA-35jh-r3h4-6jhm", | |
| 6856 | + | "summary": "Command Injection in lodash", | |
| 6857 | + | "severity": "high", | |
| 6858 | + | "fixed_version": "4.17.21", | |
| 6859 | + | "status": "dismissed", | |
| 6860 | + | "issue": null, | |
| 6861 | + | "found_at": "2026-10-06T09:14:02.118Z", | |
| 6862 | + | "fixed_at": null, | |
| 6863 | + | "state": "dismissed", | |
| 6864 | + | "dismissed_by": "syntaqx", | |
| 6865 | + | "dismissed_reason": "tolerable_risk", | |
| 6866 | + | "dismissed_comment": "Only the build uses it.", | |
| 6867 | + | "dismissed_at": "2026-10-06T09:20:41.502Z", | |
| 6868 | + | "update": null | |
| 6869 | + | } | |
| 6870 | + | }, | |
| 6871 | + | "fix_security_alert": { | |
| 6872 | + | "params": { | |
| 6873 | + | "owner": "flagon-io", | |
| 6874 | + | "name": "hello", | |
| 6875 | + | "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s" | |
| 6876 | + | }, | |
| 6877 | + | "response": { | |
| 6878 | + | "issue": 57, | |
| 6879 | + | "started": true, | |
| 6880 | + | "message": null | |
| 6881 | + | }, | |
| 6882 | + | "notes": "Opens an issue with what is wrong and what done means, and puts g1t on it as you. Its run is charged as agent usage, and its pull request lands through the repository's required checks. Asking again while the issue is open returns it." | |
| 6883 | + | }, | |
| 6884 | + | "get_dependency_graph": { | |
| 6885 | + | "params": { | |
| 6886 | + | "owner": "flagon-io", | |
| 6887 | + | "name": "hello" | |
| 6888 | + | }, | |
| 6889 | + | "response": { | |
| 6890 | + | "commit": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6891 | + | "manifests": [ | |
| 6892 | + | { | |
| 6893 | + | "path": "package-lock.json", | |
| 6894 | + | "ecosystem": "npm", | |
| 6895 | + | "dependencies": 2, | |
| 6896 | + | "direct": 1 | |
| 6897 | + | } | |
| 6898 | + | ], | |
| 6899 | + | "dependencies": [ | |
| 6900 | + | { | |
| 6901 | + | "ecosystem": "npm", | |
| 6902 | + | "name": "lodash", | |
| 6903 | + | "version": "4.17.20", | |
| 6904 | + | "manifest": "package-lock.json", | |
| 6905 | + | "relationship": "direct", | |
| 6906 | + | "development": false, | |
| 6907 | + | "license": "MIT", | |
| 6908 | + | "purl": "pkg:npm/lodash@4.17.20", | |
| 6909 | + | "vulnerabilities": 1 | |
| 6910 | + | }, | |
| 6911 | + | { | |
| 6912 | + | "ecosystem": "npm", | |
| 6913 | + | "name": "ms", | |
| 6914 | + | "version": "2.1.3", | |
| 6915 | + | "manifest": "package-lock.json", | |
| 6916 | + | "relationship": "transitive", | |
| 6917 | + | "development": false, | |
| 6918 | + | "license": "MIT", | |
| 6919 | + | "purl": "pkg:npm/ms@2.1.3", | |
| 6920 | + | "vulnerabilities": 0 | |
| 6921 | + | } | |
| 6922 | + | ] | |
| 6923 | + | } | |
| 6924 | + | }, | |
| 6925 | + | "get_sbom": { | |
| 6926 | + | "params": { | |
| 6927 | + | "owner": "flagon-io", | |
| 6928 | + | "name": "hello" | |
| 6929 | + | }, | |
| 6930 | + | "response": { | |
| 6931 | + | "sbom": { | |
| 6932 | + | "spdxVersion": "SPDX-2.3", | |
| 6933 | + | "dataLicense": "CC0-1.0", | |
| 6934 | + | "SPDXID": "SPDXRef-DOCUMENT", | |
| 6935 | + | "name": "flagon-io/hello dependency graph", | |
| 6936 | + | "documentNamespace": "https://g1t.sh/flagon-io/hello/sbom/sbom_01kq2tdk0m1n2p3q4r5s6t7v8w", | |
| 6937 | + | "creationInfo": { | |
| 6938 | + | "created": "2026-10-06T09:14:02Z", | |
| 6939 | + | "creators": [ | |
| 6940 | + | "Tool: g1t", | |
| 6941 | + | "Organization: g1t" | |
| 6942 | + | ], | |
| 6943 | + | "comment": "Read from the repository's lockfiles on its default branch." | |
| 6944 | + | }, | |
| 6945 | + | "documentDescribes": [ | |
| 6946 | + | "SPDXRef-Repository-flagon-io-hello" | |
| 6947 | + | ], | |
| 6948 | + | "packages": [ | |
| 6949 | + | { | |
| 6950 | + | "SPDXID": "SPDXRef-Repository-flagon-io-hello", | |
| 6951 | + | "name": "flagon-io/hello", | |
| 6952 | + | "versionInfo": "4807077b296e6edbf410d55e72749d3e1170c291", | |
| 6953 | + | "downloadLocation": "git+https://g1t.sh/flagon-io/hello.git", | |
| 6954 | + | "filesAnalyzed": false, | |
| 6955 | + | "licenseConcluded": "NOASSERTION", | |
| 6956 | + | "licenseDeclared": "NOASSERTION", | |
| 6957 | + | "copyrightText": "NOASSERTION", | |
| 6958 | + | "primaryPackagePurpose": "SOURCE", | |
| 6959 | + | "externalRefs": [] | |
| 6960 | + | }, | |
| 6961 | + | { | |
| 6962 | + | "SPDXID": "SPDXRef-Package-npm-lodash-4.17.20", | |
| 6963 | + | "name": "lodash", | |
| 6964 | + | "versionInfo": "4.17.20", | |
| 6965 | + | "downloadLocation": "NOASSERTION", | |
| 6966 | + | "filesAnalyzed": false, | |
| 6967 | + | "licenseConcluded": "NOASSERTION", | |
| 6968 | + | "licenseDeclared": "MIT", | |
| 6969 | + | "copyrightText": "NOASSERTION", | |
| 6970 | + | "primaryPackagePurpose": "LIBRARY", | |
| 6971 | + | "comment": "Resolved by package-lock.json (direct dependency).", | |
| 6972 | + | "externalRefs": [ | |
| 6973 | + | { | |
| 6974 | + | "referenceCategory": "PACKAGE-MANAGER", | |
| 6975 | + | "referenceType": "purl", | |
| 6976 | + | "referenceLocator": "pkg:npm/lodash@4.17.20" | |
| 6977 | + | } | |
| 6978 | + | ] | |
| 6979 | + | } | |
| 6980 | + | ], | |
| 6981 | + | "relationships": [ | |
| 6982 | + | { | |
| 6983 | + | "spdxElementId": "SPDXRef-DOCUMENT", | |
| 6984 | + | "relationshipType": "DESCRIBES", | |
| 6985 | + | "relatedSpdxElement": "SPDXRef-Repository-flagon-io-hello" | |
| 6986 | + | }, | |
| 6987 | + | { | |
| 6988 | + | "spdxElementId": "SPDXRef-Repository-flagon-io-hello", | |
| 6989 | + | "relationshipType": "DEPENDS_ON", | |
| 6990 | + | "relatedSpdxElement": "SPDXRef-Package-npm-lodash-4.17.20" | |
| 6991 | + | } | |
| 6992 | + | ] | |
| 6993 | + | } | |
| 6994 | + | }, | |
| 6995 | + | "notes": "`sbom` is an SPDX 2.3 JSON document, sent as SPDX spells it. Save it with `jq .sbom`." | |
| 6996 | + | }, | |
| 6997 | + | "compare_dependencies": { | |
| 6998 | + | "params": { | |
| 6999 | + | "owner": "flagon-io", | |
| 7000 | + | "name": "hello", | |
| 7001 | + | "basehead": "main...upgrade-deps" | |
| 7002 | + | }, | |
| 7003 | + | "response": { | |
| 7004 | + | "base": "main", | |
| 7005 | + | "head": "upgrade-deps", | |
| 7006 | + | "changes": [ | |
| 7007 | + | { | |
| 7008 | + | "change_type": "added", | |
| 7009 | + | "manifest": "package-lock.json", | |
| 7010 | + | "ecosystem": "npm", | |
| 7011 | + | "name": "lodash", | |
| 7012 | + | "version": "4.17.20", | |
| 7013 | + | "relationship": "direct", | |
| 7014 | + | "development": false, | |
| 7015 | + | "license": "MIT", | |
| 7016 | + | "purl": "pkg:npm/lodash@4.17.20", | |
| 7017 | + | "vulnerabilities": [ | |
| 7018 | + | { | |
| 7019 | + | "advisory": "GHSA-35jh-r3h4-6jhm", | |
| 7020 | + | "osv_id": "GHSA-35jh-r3h4-6jhm", | |
| 7021 | + | "summary": "Command Injection in lodash", | |
| 7022 | + | "severity": "high", | |
| 7023 | + | "fixed_version": "4.17.21", | |
| 7024 | + | "url": "https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm" | |
| 7025 | + | } | |
| 7026 | + | ], | |
| 7027 | + | "denied_license": false, | |
| 7028 | + | "failing": true | |
| 7029 | + | }, | |
| 7030 | + | { | |
| 7031 | + | "change_type": "removed", | |
| 7032 | + | "manifest": "package-lock.json", | |
| 7033 | + | "ecosystem": "npm", | |
| 7034 | + | "name": "lodash", | |
| 7035 | + | "version": "4.17.21", | |
| 7036 | + | "relationship": "direct", | |
| 7037 | + | "development": false, | |
| 7038 | + | "license": "MIT", | |
| 7039 | + | "purl": "pkg:npm/lodash@4.17.21", | |
| 7040 | + | "vulnerabilities": [], | |
| 7041 | + | "denied_license": false, | |
| 7042 | + | "failing": false | |
| 7043 | + | } | |
| 7044 | + | ], | |
| 7045 | + | "passed": false, | |
| 7046 | + | "headline": "Adds 1 vulnerable package", | |
| 7047 | + | "fail_on": "high", | |
| 7048 | + | "deny_licenses": [] | |
| 7049 | + | } | |
| 7050 | + | }, | |
| 7051 | + | "get_security_settings": { | |
| 7052 | + | "params": { | |
| 7053 | + | "owner": "flagon-io", | |
| 7054 | + | "name": "hello" | |
| 7055 | + | }, | |
| 7056 | + | "response": { | |
| 7057 | + | "settings": { | |
| 7058 | + | "code_scanning_gate": "high", | |
| 7059 | + | "dependency_review": true, | |
| 7060 | + | "review_fail_on": "high", | |
| 7061 | + | "review_deny_licenses": [ | |
| 7062 | + | "AGPL-3.0-only" | |
| 7063 | + | ], | |
| 7064 | + | "review_comment": true | |
| 7065 | + | }, | |
| 7066 | + | "workspace": { | |
| 7067 | + | "delegated_bypass": true, | |
| 7068 | + | "validity_checks": true | |
| 7069 | + | }, | |
| 7070 | + | "private": true, | |
| 7071 | + | "entitled": true, | |
| 7072 | + | "upkeep": true | |
| 7073 | + | } | |
| 7074 | + | }, | |
| 7075 | + | "update_security_settings": { | |
| 7076 | + | "params": { | |
| 7077 | + | "owner": "flagon-io", | |
| 7078 | + | "name": "hello" | |
| 7079 | + | }, | |
| 7080 | + | "request": { | |
| 7081 | + | "code_scanning_gate": "high", | |
| 7082 | + | "review_deny_licenses": [ | |
| 7083 | + | "AGPL-3.0-only" | |
| 7084 | + | ] | |
| 7085 | + | }, | |
| 7086 | + | "response": { | |
| 7087 | + | "settings": { | |
| 7088 | + | "code_scanning_gate": "high", | |
| 7089 | + | "dependency_review": true, | |
| 7090 | + | "review_fail_on": "high", | |
| 7091 | + | "review_deny_licenses": [ | |
| 7092 | + | "AGPL-3.0-only" | |
| 7093 | + | ], | |
| 7094 | + | "review_comment": true | |
| 7095 | + | }, | |
| 7096 | + | "workspace": { | |
| 7097 | + | "delegated_bypass": true, | |
| 7098 | + | "validity_checks": true | |
| 7099 | + | }, | |
| 7100 | + | "private": true, | |
| 7101 | + | "entitled": true, | |
| 7102 | + | "upkeep": true | |
| 7103 | + | }, | |
| 7104 | + | "notes": "Only what you send changes. The `Code scanning` and `Dependency review` checks gate merges once you require them in branch protection." | |
| 7105 | + | }, | |
| 7106 | + | "get_workspace_security_settings": { | |
| 7107 | + | "params": { | |
| 7108 | + | "workspace": "flagon-io" | |
| 7109 | + | }, | |
| 7110 | + | "response": { | |
| 7111 | + | "settings": { | |
| 7112 | + | "delegated_bypass": true, | |
| 7113 | + | "validity_checks": true | |
| 7114 | + | }, | |
| 7115 | + | "activated": true | |
| 7116 | + | } | |
| 7117 | + | }, | |
| 7118 | + | "update_workspace_security_settings": { | |
| 7119 | + | "params": { | |
| 7120 | + | "workspace": "flagon-io" | |
| 7121 | + | }, | |
| 7122 | + | "request": { | |
| 7123 | + | "delegated_bypass": true | |
| 7124 | + | }, | |
| 7125 | + | "response": { | |
| 7126 | + | "settings": { | |
| 7127 | + | "delegated_bypass": true, | |
| 7128 | + | "validity_checks": true | |
| 7129 | + | }, | |
| 7130 | + | "activated": true | |
| 7131 | + | } | |
| 7132 | + | }, | |
| 7133 | + | "get_security_overview": { | |
| 7134 | + | "params": { | |
| 7135 | + | "workspace": "flagon-io" | |
| 7136 | + | }, | |
| 7137 | + | "query": { | |
| 7138 | + | "days": "30" | |
| 7139 | + | }, | |
| 7140 | + | "response": { | |
| 7141 | + | "activated": true, | |
| 7142 | + | "private_hidden": 0, | |
| 7143 | + | "totals": [ | |
| 7144 | + | { | |
| 7145 | + | "alert_type": "secret_scanning", | |
| 7146 | + | "open": { | |
| 7147 | + | "critical": 1, | |
| 7148 | + | "high": 0, | |
| 7149 | + | "medium": 0, | |
| 7150 | + | "low": 0, | |
| 7151 | + | "unknown": 0 | |
| 7152 | + | }, | |
| 7153 | + | "opened": 2, | |
| 7154 | + | "closed": 1 | |
| 7155 | + | }, | |
| 7156 | + | { | |
| 7157 | + | "alert_type": "code_scanning", | |
| 7158 | + | "open": { | |
| 7159 | + | "critical": 0, | |
| 7160 | + | "high": 3, | |
| 7161 | + | "medium": 4, | |
| 7162 | + | "low": 0, | |
| 7163 | + | "unknown": 0 | |
| 7164 | + | }, | |
| 7165 | + | "opened": 7, | |
| 7166 | + | "closed": 2 | |
| 7167 | + | }, | |
| 7168 | + | { | |
| 7169 | + | "alert_type": "vulnerability", | |
| 7170 | + | "open": { | |
| 7171 | + | "critical": 0, | |
| 7172 | + | "high": 1, | |
| 7173 | + | "medium": 2, | |
| 7174 | + | "low": 1, | |
| 7175 | + | "unknown": 0 | |
| 7176 | + | }, | |
| 7177 | + | "opened": 3, | |
| 7178 | + | "closed": 5 | |
| 7179 | + | } | |
| 7180 | + | ], | |
| 7181 | + | "trend": [ | |
| 7182 | + | { | |
| 7183 | + | "day": "2026-10-05", | |
| 7184 | + | "secret_scanning": 1, | |
| 7185 | + | "code_scanning": 8, | |
| 7186 | + | "vulnerability": 6 | |
| 7187 | + | }, | |
| 7188 | + | { | |
| 7189 | + | "day": "2026-10-06", | |
| 7190 | + | "secret_scanning": 1, | |
| 7191 | + | "code_scanning": 7, | |
| 7192 | + | "vulnerability": 4 | |
| 7193 | + | } | |
| 7194 | + | ], | |
| 7195 | + | "repos": [ | |
| 7196 | + | { | |
| 7197 | + | "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m", | |
| 7198 | + | "name": "hello", | |
| 7199 | + | "private": true, | |
| 7200 | + | "custom_patterns": 1, | |
| 7201 | + | "validity_checks": true, | |
| 7202 | + | "code_scanning_at": "2026-10-06T09:14:02.118Z", | |
| 7203 | + | "dependency_review": true, | |
| 7204 | + | "security_updates": true, | |
| 7205 | + | "lockfiles": 1, | |
| 7206 | + | "secrets": { | |
| 7207 | + | "critical": 1, | |
| 7208 | + | "high": 0, | |
| 7209 | + | "medium": 0, | |
| 7210 | + | "low": 0, | |
| 7211 | + | "unknown": 0 | |
| 7212 | + | }, | |
| 7213 | + | "code": { | |
| 7214 | + | "critical": 0, | |
| 7215 | + | "high": 3, | |
| 7216 | + | "medium": 4, | |
| 7217 | + | "low": 0, | |
| 7218 | + | "unknown": 0 | |
| 7219 | + | }, | |
| 7220 | + | "vulnerabilities": { | |
| 7221 | + | "critical": 0, | |
| 7222 | + | "high": 1, | |
| 7223 | + | "medium": 2, | |
| 7224 | + | "low": 1, | |
| 7225 | + | "unknown": 0 | |
| 7226 | + | } | |
| 7227 | + | } | |
| 7228 | + | ] | |
| 7229 | + | } | |
| 7230 | + | }, | |
| 7231 | + | "create_label": { | |
| 7232 | + | "request": { | |
| 7233 | + | "label": "area: cli", | |
| 7234 | + | "color": "1d76db", | |
| 7235 | + | "description": "The command-line tool" | |
| 7236 | + | }, | |
| 7237 | + | "response": { | |
| 7238 | + | "name": "area: cli", | |
| 7239 | + | "color": "1d76db", | |
| 7240 | + | "description": "The command-line tool", | |
| 7241 | + | "issues": 0, | |
| 7242 | + | "pulls": 0 | |
| 7243 | + | } | |
| 7244 | + | }, | |
| 7245 | + | "update_label": { | |
| 7246 | + | "params": { | |
| 7247 | + | "label": "area: cli" | |
| 7248 | + | }, | |
| 7249 | + | "request": { | |
| 7250 | + | "new_name": "cli", | |
| 7251 | + | "color": "0052cc" | |
| 7252 | + | }, | |
| 7253 | + | "response": { | |
| 7254 | + | "name": "cli", | |
| 7255 | + | "color": "0052cc", | |
| 7256 | + | "description": "The command-line tool", | |
| 7257 | + | "issues": 3, | |
| 7258 | + | "pulls": 1 | |
| 7259 | + | }, | |
| 7260 | + | "notes": "Renaming a label renames it on every issue and pull request that carries it." | |
| 7261 | + | }, | |
| 7262 | + | "delete_label": { | |
| 7263 | + | "params": { | |
| 7264 | + | "label": "wontfix" | |
| 7265 | + | }, | |
| 7266 | + | "response": true | |
| 7267 | + | }, | |
| 7268 | + | "add_default_labels": { | |
| 7269 | + | "response": [ | |
| 7270 | + | { | |
| 7271 | + | "name": "bug", | |
| 7272 | + | "color": "d73a4a", | |
| 7273 | + | "description": "Something isn't working", | |
| 7274 | + | "issues": 4, | |
| 7275 | + | "pulls": 1 | |
| 7276 | + | }, | |
| 7277 | + | { | |
| 7278 | + | "name": "documentation", | |
| 7279 | + | "color": "0075ca", | |
| 7280 | + | "description": "Improvements or additions to documentation", | |
| 7281 | + | "issues": 0, | |
| 7282 | + | "pulls": 0 | |
| 7283 | + | } | |
| 7284 | + | ], | |
| 7285 | + | "notes": "Every label the repository has afterwards, shortened here. Labels it already had are left as they were." | |
| 7286 | + | }, | |
| 7287 | + | "list_issue_labels": { | |
| 7288 | + | "response": [ | |
| 7289 | + | { | |
| 7290 | + | "name": "bug", | |
| 7291 | + | "color": "d73a4a", | |
| 7292 | + | "description": "Something isn't working", | |
| 7293 | + | "issues": 4, | |
| 7294 | + | "pulls": 1 | |
| 7295 | + | }, | |
| 7296 | + | { | |
| 7297 | + | "name": "help wanted", | |
| 7298 | + | "color": "008672", | |
| 7299 | + | "description": "Extra attention is needed", | |
| 7300 | + | "issues": 1, | |
| 7301 | + | "pulls": 0 | |
| 7302 | + | } | |
| 7303 | + | ] | |
| 7304 | + | }, | |
| 7305 | + | "add_issue_labels": { | |
| 7306 | + | "request": { | |
| 7307 | + | "labels": [ | |
| 7308 | + | "help wanted" | |
| 7309 | + | ] | |
| 7310 | + | }, | |
| 7311 | + | "response": [ | |
| 7312 | + | "bug", | |
| 7313 | + | "help wanted" | |
| 7314 | + | ], | |
| 7315 | + | "notes": "Returns its labels now, by name." | |
| 7316 | + | }, | |
| 7317 | + | "set_issue_labels": { | |
| 7318 | + | "request": { | |
| 7319 | + | "labels": [ | |
| 7320 | + | "bug" | |
| 7321 | + | ] | |
| 7322 | + | }, | |
| 7323 | + | "response": [ | |
| 7324 | + | "bug" | |
| 7325 | + | ] | |
| 7326 | + | }, | |
| 7327 | + | "remove_issue_labels": { | |
| 7328 | + | "response": [] | |
| 7329 | + | }, | |
| 7330 | + | "remove_issue_label": { | |
| 7331 | + | "params": { | |
| 7332 | + | "label": "help wanted" | |
| 7333 | + | }, | |
| 7334 | + | "response": [ | |
| 7335 | + | "bug" | |
| 7336 | + | ] | |
| 7337 | + | }, | |
| 7338 | + | "list_milestones": { | |
| 7339 | + | "response": [ | |
| 7340 | + | { | |
| 7341 | + | "number": 3, | |
| 7342 | + | "title": "Launch", | |
| 7343 | + | "description": "Everything that has to land before the launch on October 14.", | |
| 7344 | + | "due_on": "2026-10-14", | |
| 7345 | + | "state": "open", | |
| 7346 | + | "open_items": 5, | |
| 7347 | + | "closed_items": 12, | |
| 7348 | + | "created_at": "2026-09-20T09:00:00.000Z", | |
| 7349 | + | "updated_at": "2026-10-06T16:12:40.118Z", | |
| 7350 | + | "closed_at": null | |
| 7351 | + | } | |
| 7352 | + | ], | |
| 7353 | + | "notes": "Open milestones soonest due first, then closed ones. Progress is closed_items out of open_items plus closed_items." | |
| 7354 | + | }, | |
| 7355 | + | "create_milestone": { | |
| 7356 | + | "request": { | |
| 7357 | + | "title": "Launch", | |
| 7358 | + | "description": "Everything that has to land before the launch on October 14.", | |
| 7359 | + | "due_on": "2026-10-14" | |
| 7360 | + | }, | |
| 7361 | + | "response": { | |
| 7362 | + | "number": 3, | |
| 7363 | + | "title": "Launch", | |
| 7364 | + | "description": "Everything that has to land before the launch on October 14.", | |
| 7365 | + | "due_on": "2026-10-14", | |
| 7366 | + | "state": "open", | |
| 7367 | + | "open_items": 0, | |
| 7368 | + | "closed_items": 0, | |
| 7369 | + | "created_at": "2026-09-20T09:00:00.000Z", | |
| 7370 | + | "updated_at": "2026-09-20T09:00:00.000Z", | |
| 7371 | + | "closed_at": null | |
| 7372 | + | } | |
| 7373 | + | }, | |
| 7374 | + | "update_milestone": { | |
| 7375 | + | "params": { | |
| 7376 | + | "milestone": 3 | |
| 7377 | + | }, | |
| 7378 | + | "request": { | |
| 7379 | + | "state": "closed" | |
| 7380 | + | }, | |
| 7381 | + | "response": { | |
| 7382 | + | "number": 3, | |
| 7383 | + | "title": "Launch", | |
| 7384 | + | "description": "Everything that has to land before the launch on October 14.", | |
| 7385 | + | "due_on": "2026-10-14", | |
| 7386 | + | "state": "closed", | |
| 7387 | + | "open_items": 0, | |
| 7388 | + | "closed_items": 17, | |
| 7389 | + | "created_at": "2026-09-20T09:00:00.000Z", | |
| 7390 | + | "updated_at": "2026-10-14T18:00:00.000Z", | |
| 7391 | + | "closed_at": "2026-10-14T18:00:00.000Z" | |
| 7392 | + | } | |
| 7393 | + | }, | |
| 7394 | + | "delete_milestone": { | |
| 7395 | + | "params": { | |
| 7396 | + | "milestone": 3 | |
| 7397 | + | }, | |
| 7398 | + | "response": true | |
| 7399 | + | }, | |
| 7400 | + | "get_milestone": { | |
| 7401 | + | "params": { | |
| 7402 | + | "milestone": 3 | |
| 7403 | + | }, | |
| 7404 | + | "response": { | |
| 7405 | + | "milestone": { | |
| 7406 | + | "number": 3, | |
| 7407 | + | "title": "Launch", | |
| 7408 | + | "description": "Everything that has to land before the launch on October 14.", | |
| 7409 | + | "due_on": "2026-10-14", | |
| 7410 | + | "state": "open", | |
| 7411 | + | "open_items": 5, | |
| 7412 | + | "closed_items": 12, | |
| 7413 | + | "created_at": "2026-09-20T09:00:00.000Z", | |
| 7414 | + | "updated_at": "2026-10-06T16:12:40.118Z", | |
| 7415 | + | "closed_at": null | |
| 7416 | + | }, | |
| 7417 | + | "issues": [ | |
| 7418 | + | { | |
| 7419 | + | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 7420 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 7421 | + | "number": 12, | |
| 7422 | + | "title": "Greeting should name the caller", | |
| 7423 | + | "body": "Take a name from the first argument; fall back to world.", | |
| 7424 | + | "labels": [ | |
| 7425 | + | "feature", | |
| 7426 | + | "good first issue" | |
| 7427 | + | ], | |
| 7428 | + | "state": "open", | |
| 7429 | + | "reason": null, | |
| 7430 | + | "resolved_by": null, | |
| 7431 | + | "author": { | |
| 7432 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 7433 | + | "username": "syntaqx", | |
| 7434 | + | "kind": "user", | |
| 7435 | + | "verified": false, | |
| 7436 | + | "workspaces": [] | |
| 7437 | + | }, | |
| 7438 | + | "requested_by": null, | |
| 7439 | + | "created_at": "2026-10-01T18:04:11.482Z", | |
| 7440 | + | "updated_at": "2026-10-01T18:09:47.305Z", | |
| 7441 | + | "closed_at": null, | |
| 7442 | + | "pull_count": 0, | |
| 7443 | + | "comment_count": 0, | |
| 7444 | + | "assignees": [ | |
| 7445 | + | "syntaqx" | |
| 7446 | + | ], | |
| 7447 | + | "blocked_by": [], | |
| 7448 | + | "queued": false, | |
| 7449 | + | "agent": null, | |
| 7450 | + | "milestone": { | |
| 7451 | + | "number": 3, | |
| 7452 | + | "title": "Launch" | |
| 7453 | + | } | |
| 7454 | + | } | |
| 7455 | + | ], | |
| 7456 | + | "pulls": [ | |
| 7457 | + | { | |
| 7458 | + | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 7459 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 7460 | + | "number": 14, | |
| 7461 | + | "issue": 12, | |
| 7462 | + | "title": "Greeting should name the caller", | |
| 7463 | + | "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".", | |
| 7464 | + | "agent": "claude-code", | |
| 7465 | + | "runtime": "external", | |
| 7466 | + | "status": "open", | |
| 7467 | + | "fork": { | |
| 7468 | + | "namespace": "pulls", | |
| 7469 | + | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 7470 | + | }, | |
| 7471 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 7472 | + | "branch": null, | |
| 7473 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 7474 | + | "merge_base": null, | |
| 7475 | + | "merged_by": null, | |
| 7476 | + | "merged_at": null, | |
| 7477 | + | "superseded_by": null, | |
| 7478 | + | "check_status": "passed", | |
| 7479 | + | "files": [ | |
| 7480 | + | { | |
| 7481 | + | "path": "src/main.rs", | |
| 7482 | + | "additions": 6, | |
| 7483 | + | "deletions": 2 | |
| 7484 | + | } | |
| 7485 | + | ], | |
| 7486 | + | "assignees": [], | |
| 7487 | + | "reviewers": [ | |
| 7488 | + | "ana" | |
| 7489 | + | ], | |
| 7490 | + | "labels": [], | |
| 7491 | + | "milestone": { | |
| 7492 | + | "number": 3, | |
| 7493 | + | "title": "Launch" | |
| 7494 | + | }, | |
| 7495 | + | "base": "main", | |
| 7496 | + | "author": { | |
| 7497 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 7498 | + | "username": "syntaqx", | |
| 7499 | + | "kind": "user", | |
| 7500 | + | "verified": false, | |
| 7501 | + | "workspaces": [] | |
| 7502 | + | }, | |
| 7503 | + | "requested_by": null, | |
| 7504 | + | "created_at": "2026-10-01T18:20:02.117Z", | |
| 7505 | + | "updated_at": "2026-10-01T18:35:44.902Z", | |
| 7506 | + | "confidence": null | |
| 7507 | + | } | |
| 7508 | + | ] | |
| 7509 | + | } | |
| 7510 | + | }, | |
| 7511 | + | "update_pull_request": { | |
| 7512 | + | "request": { | |
| 7513 | + | "base": "release/1.x", | |
| 7514 | + | "labels": [ | |
| 7515 | + | "bug" | |
| 7516 | + | ] | |
| 7517 | + | }, | |
| 7518 | + | "response": { | |
| 7519 | + | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 7520 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 7521 | + | "number": 14, | |
| 7522 | + | "issue": 12, | |
| 7523 | + | "title": "Greeting should name the caller", | |
| 7524 | + | "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".", | |
| 7525 | + | "agent": "claude-code", | |
| 7526 | + | "runtime": "external", | |
| 7527 | + | "status": "open", | |
| 7528 | + | "fork": { | |
| 7529 | + | "namespace": "pulls", | |
| 7530 | + | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 7531 | + | }, | |
| 7532 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 7533 | + | "branch": null, | |
| 7534 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 7535 | + | "merge_base": null, | |
| 7536 | + | "merged_by": null, | |
| 7537 | + | "merged_at": null, | |
| 7538 | + | "superseded_by": null, | |
| 7539 | + | "check_status": "passed", | |
| 7540 | + | "files": [ | |
| 7541 | + | { | |
| 7542 | + | "path": "src/main.rs", | |
| 7543 | + | "additions": 6, | |
| 7544 | + | "deletions": 2 | |
| 7545 | + | } | |
| 7546 | + | ], | |
| 7547 | + | "assignees": [], | |
| 7548 | + | "reviewers": [ | |
| 7549 | + | "ana" | |
| 7550 | + | ], | |
| 7551 | + | "labels": [ | |
| 7552 | + | "bug" | |
| 7553 | + | ], | |
| 7554 | + | "milestone": null, | |
| 7555 | + | "base": "release/1.x", | |
| 7556 | + | "author": { | |
| 7557 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 7558 | + | "username": "syntaqx", | |
| 7559 | + | "kind": "user", | |
| 7560 | + | "verified": false, | |
| 7561 | + | "workspaces": [] | |
| 7562 | + | }, | |
| 7563 | + | "requested_by": null, | |
| 7564 | + | "created_at": "2026-10-01T18:20:02.117Z", | |
| 7565 | + | "updated_at": "2026-10-01T18:35:44.902Z", | |
| 7566 | + | "confidence": null | |
| 7567 | + | }, | |
| 7568 | + | "notes": "A new base takes it out of the merge queue, and whether it is behind, merges cleanly and has the checks it needs is worked out against the new base." | |
| 5183 | 7569 | } | |
| 5184 | 7570 | } | |
| 7 | 7 | //! encoded again, so that every field the type has is sent, not only the | |
| 8 | 8 | //! ones an example shows. | |
| 9 | 9 | ||
| 10 | − | use g1t_contracts::{access, actions, integrations, repos, search, webhooks, work}; | |
| 10 | + | use g1t_contracts::{access, actions, codeowners, integrations, repos, search, teams, webhooks, work}; | |
| 11 | 11 | use g1t_kit::wire::{self, USER_KEYED}; | |
| 12 | 12 | use serde::Serialize; | |
| 13 | 13 | use serde::de::DeserializeOwned; | |
| ⋯ | |||
| 79 | 79 | return through::<access::RepoInvitation>(op, as_is); | |
| 80 | 80 | } | |
| 81 | 81 | Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is), | |
| 82 | + | // Teams and code owners, also `snake_case`. | |
| 83 | + | Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is), | |
| 84 | + | Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => { | |
| 85 | + | return through::<teams::Team>(op, as_is); | |
| 86 | + | } | |
| 87 | + | Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is), | |
| 88 | + | Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is), | |
| 89 | + | Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is), | |
| 90 | + | Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is), | |
| 91 | + | Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is), | |
| 92 | + | Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is), | |
| 82 | 93 | // Built by the API itself, in `snake_case`. | |
| 83 | 94 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 84 | 95 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { | |
| ⋯ | |||
| 86 | 97 | } | |
| 87 | 98 | _ => {} | |
| 88 | 99 | } | |
| 89 | − | let sent = as_services_send(example); | |
| 100 | + | let mut sent = as_services_send(example); | |
| 101 | + | // A pull request's code owners are `snake_case` inside it. | |
| 102 | + | if op == Op::GetPullRequest | |
| 103 | + | && let Some(code_owners) = example.get("code_owners") | |
| 104 | + | { | |
| 105 | + | sent["codeOwners"] = code_owners.clone(); | |
| 106 | + | } | |
| 90 | 107 | match op { | |
| 91 | 108 | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent), | |
| 92 | 109 | Op::ListRepos => through::<Vec<repos::Repo>>(op, sent), | |
| ⋯ | |||
| 113 | 130 | Op::GetIssue => through::<work::IssueDetail>(op, sent), | |
| 114 | 131 | Op::Delegate => through::<work::Delegated>(op, sent), | |
| 115 | 132 | Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent), | |
| 133 | + | Op::UpdatePullRequest => through::<work::Pull>(op, sent), | |
| 134 | + | Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent), | |
| 135 | + | Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent), | |
| 136 | + | Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent), | |
| 137 | + | Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent), | |
| 138 | + | Op::GetMilestone => through::<work::MilestoneDetail>(op, sent), | |
| 116 | 139 | Op::GetPullRequest => through::<work::PullDetail>(op, sent), | |
| 117 | − | Op::MarkPullRequestReady | Op::ClosePullRequest | Op::MergePullRequest | Op::AssignIssue => { | |
| 140 | + | Op::MarkPullRequestReady | |
| 141 | + | | Op::ClosePullRequest | |
| 142 | + | | Op::MergePullRequest | |
| 143 | + | | Op::AssignIssue | |
| 144 | + | | Op::RequestReviewers | |
| 145 | + | | Op::RemoveRequestedReviewers => { | |
| 118 | 146 | through::<work::Pull>(op, sent) | |
| 119 | 147 | } | |
| 120 | 148 | Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent), | |
| 3 | 3 | use serde_json::{Map, Value}; | |
| 4 | 4 | ||
| 5 | 5 | use crate::operations::Op; | |
| 6 | + | use crate::security::SecurityOp; | |
| 6 | 7 | ||
| 7 | 8 | pub struct Route { | |
| 8 | 9 | pub method: &'static str, | |
| ⋯ | |||
| 94 | 95 | Op::ListOutsideCollaborators, | |
| 95 | 96 | &[], | |
| 96 | 97 | ), | |
| 98 | + | // Teams: a workspace's groups of members, with roles on repositories. | |
| 99 | + | route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]), | |
| 100 | + | route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]), | |
| 101 | + | route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]), | |
| 102 | + | route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]), | |
| 103 | + | route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]), | |
| 104 | + | route( | |
| 105 | + | "GET", | |
| 106 | + | "/workspaces/:workspace/teams/:team/members", | |
| 107 | + | Op::ListTeamMembers, | |
| 108 | + | &[("include_child_teams", "include_child_teams")], | |
| 109 | + | ), | |
| 110 | + | route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]), | |
| 111 | + | route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]), | |
| 112 | + | route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]), | |
| 113 | + | route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]), | |
| 114 | + | route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]), | |
| 115 | + | route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]), | |
| 116 | + | route( | |
| 117 | + | "PUT", | |
| 118 | + | "/workspaces/:workspace/teams/:team/review_assignment", | |
| 119 | + | Op::SetTeamReviewAssignment, | |
| 120 | + | &[], | |
| 121 | + | ), | |
| 122 | + | route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]), | |
| 123 | + | // Code owners: the CODEOWNERS file, checked. | |
| 124 | + | route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]), | |
| 97 | 125 | // Security alerts: secrets and vulnerable dependencies. | |
| 98 | 126 | route( | |
| 99 | 127 | "GET", | |
| ⋯ | |||
| 103 | 131 | ), | |
| 104 | 132 | route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]), | |
| 105 | 133 | route("POST", "/repos/:owner/:name/security/alerts/:id/reopen", Op::ReopenSecurityAlert, &[]), | |
| 134 | + | // The security suite: secret scanning, code scanning, vulnerability | |
| 135 | + | // alerts and the supply chain, at the common addresses. | |
| 136 | + | route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]), | |
| 137 | + | route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]), | |
| 138 | + | route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]), | |
| 139 | + | route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]), | |
| 140 | + | route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]), | |
| 141 | + | route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]), | |
| 142 | + | route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]), | |
| 143 | + | route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]), | |
| 144 | + | route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]), | |
| 145 | + | route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]), | |
| 146 | + | route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]), | |
| 147 | + | route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]), | |
| 148 | + | route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]), | |
| 149 | + | route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]), | |
| 150 | + | route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]), | |
| 151 | + | route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]), | |
| 152 | + | route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]), | |
| 153 | + | route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]), | |
| 154 | + | route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]), | |
| 155 | + | route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]), | |
| 156 | + | route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]), | |
| 157 | + | route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]), | |
| 158 | + | route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]), | |
| 159 | + | route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]), | |
| 160 | + | route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]), | |
| 161 | + | route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]), | |
| 162 | + | route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]), | |
| 163 | + | route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]), | |
| 164 | + | route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]), | |
| 165 | + | route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]), | |
| 166 | + | route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]), | |
| 167 | + | route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]), | |
| 168 | + | route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]), | |
| 169 | + | route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]), | |
| 170 | + | route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]), | |
| 171 | + | route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]), | |
| 172 | + | route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]), | |
| 173 | + | route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]), | |
| 174 | + | route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]), | |
| 106 | 175 | route("GET", "/repos", Op::ListRepos, &[("q", "query")]), | |
| 107 | 176 | route( | |
| 108 | 177 | "GET", | |
| ⋯ | |||
| 184 | 253 | &[("before", "before")], | |
| 185 | 254 | ), | |
| 186 | 255 | route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]), | |
| 256 | + | route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]), | |
| 257 | + | route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]), | |
| 258 | + | route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]), | |
| 259 | + | route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]), | |
| 260 | + | route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]), | |
| 261 | + | route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]), | |
| 262 | + | route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]), | |
| 263 | + | route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]), | |
| 264 | + | route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]), | |
| 265 | + | route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]), | |
| 266 | + | route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]), | |
| 267 | + | route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]), | |
| 268 | + | route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]), | |
| 269 | + | route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]), | |
| 187 | 270 | route( | |
| 188 | 271 | "GET", | |
| 189 | 272 | "/repos/:owner/:name/issues", | |
| 190 | 273 | Op::ListIssues, | |
| 191 | − | &[("state", "state"), ("label", "label")], | |
| 274 | + | &[("state", "state"), ("label", "label"), ("milestone", "milestone")], | |
| 192 | 275 | ), | |
| 193 | 276 | route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]), | |
| 194 | 277 | route( | |
| ⋯ | |||
| 560 | 643 | "GET", | |
| 561 | 644 | "/repos/:owner/:name/pulls", | |
| 562 | 645 | Op::ListPullRequests, | |
| 563 | − | &[("state", "state")], | |
| 646 | + | &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")], | |
| 564 | 647 | ), | |
| 565 | 648 | route( | |
| 566 | 649 | "POST", | |
| ⋯ | |||
| 575 | 658 | &[], | |
| 576 | 659 | ), | |
| 577 | 660 | route( | |
| 661 | + | "PATCH", | |
| 662 | + | "/repos/:owner/:name/pulls/:number", | |
| 663 | + | Op::UpdatePullRequest, | |
| 664 | + | &[], | |
| 665 | + | ), | |
| 666 | + | route( | |
| 578 | 667 | "GET", | |
| 579 | 668 | "/repos/:owner/:name/pulls/:number/changes", | |
| 580 | 669 | Op::GetPullRequestChanges, | |
| ⋯ | |||
| 587 | 676 | &[], | |
| 588 | 677 | ), | |
| 589 | 678 | route( | |
| 679 | + | "POST", | |
| 680 | + | "/repos/:owner/:name/pulls/:number/requested_reviewers", | |
| 681 | + | Op::RequestReviewers, | |
| 682 | + | &[], | |
| 683 | + | ), | |
| 684 | + | route( | |
| 685 | + | "DELETE", | |
| 686 | + | "/repos/:owner/:name/pulls/:number/requested_reviewers", | |
| 687 | + | Op::RemoveRequestedReviewers, | |
| 688 | + | &[], | |
| 689 | + | ), | |
| 690 | + | route( | |
| 590 | 691 | "GET", | |
| 591 | 692 | "/repos/:owner/:name/pulls/:number/session", | |
| 592 | 693 | Op::ReadSession, | |
| ⋯ | |||
| 673 | 774 | /// | |
| 674 | 775 | /// The input is the JSON body, overlaid with the query parameters the route | |
| 675 | 776 | /// reads and then with what the path names: `owner` and `name` become | |
| 676 | − | /// `repo`, and `number` becomes an integer. | |
| 777 | + | /// `repo`, as does a team's `repo` with its `workspace`, and `number` | |
| 778 | + | /// becomes an integer. | |
| 677 | 779 | pub fn resolve( | |
| 678 | 780 | method: &str, | |
| 679 | 781 | path: &str, | |
| ⋯ | |||
| 703 | 805 | if let (Some(owner), Some(name)) = (param("owner"), param("name")) { | |
| 704 | 806 | input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}"))); | |
| 705 | 807 | } | |
| 706 | − | for key in ["plan", "id", "workspace", "delivery", "workflow", "job", "setting", "username"] { | |
| 808 | + | for key in ["plan", "id", "workspace", "delivery", "workflow", "job", "setting", "username", "team", "basehead"] { | |
| 707 | 809 | if let Some(value) = param(key) { | |
| 708 | 810 | input.insert(key.to_owned(), Value::String(value.to_owned())); | |
| 709 | 811 | } | |
| 710 | 812 | } | |
| 711 | − | // A branch name may hold slashes, sent URL-encoded as one segment. | |
| 813 | + | // A repository of a team's workspace, named by itself. | |
| 814 | + | if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) { | |
| 815 | + | input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}"))); | |
| 816 | + | } | |
| 817 | + | // A branch name may hold slashes, sent URL-encoded as one segment, and | |
| 818 | + | // a label's name spaces. | |
| 712 | 819 | if let Some(branch) = param("branch") { | |
| 713 | 820 | input.insert("branch".to_owned(), Value::String(percent_decoded(branch))); | |
| 714 | 821 | } | |
| 822 | + | if let Some(label) = param("label") { | |
| 823 | + | input.insert("label".to_owned(), Value::String(percent_decoded(label))); | |
| 824 | + | } | |
| 825 | + | if let Some(milestone) = param("milestone") { | |
| 826 | + | // Not a number: zero, which no milestone has. | |
| 827 | + | input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into()); | |
| 828 | + | } | |
| 715 | 829 | // GitHub says some things with the path alone. | |
| 716 | 830 | if route.path.ends_with("/enable") || route.path.ends_with("/disable") { | |
| 717 | 831 | input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable"))); | |
| ⋯ | |||
| 794 | 908 | } | |
| 795 | 909 | ||
| 796 | 910 | #[test] | |
| 911 | + | fn teams_are_addressed_by_workspace_and_slug() { | |
| 912 | + | let query = [("q".to_owned(), "back".to_owned())]; | |
| 913 | + | let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap(); | |
| 914 | + | assert_eq!(route.op, Op::ListTeams); | |
| 915 | + | assert_eq!(input, json!({ "query": "back", "workspace": "acme" })); | |
| 916 | + | let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap(); | |
| 917 | + | assert_eq!(route.op, Op::UpdateTeam); | |
| 918 | + | assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" })); | |
| 919 | + | let (route, input) = | |
| 920 | + | resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap(); | |
| 921 | + | assert_eq!(route.op, Op::SetTeamMember); | |
| 922 | + | assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" })); | |
| 923 | + | let query = [("include_child_teams".to_owned(), "true".to_owned())]; | |
| 924 | + | let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap(); | |
| 925 | + | assert_eq!(route.op, Op::ListTeamMembers); | |
| 926 | + | assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" })); | |
| 927 | + | // A repository is named by itself, in the team's workspace. | |
| 928 | + | let (route, input) = | |
| 929 | + | resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap(); | |
| 930 | + | assert_eq!(route.op, Op::SetTeamRepo); | |
| 931 | + | assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" })); | |
| 932 | + | let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op; | |
| 933 | + | assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo); | |
| 934 | + | assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams); | |
| 935 | + | assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos); | |
| 936 | + | assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment); | |
| 937 | + | assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam); | |
| 938 | + | assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam); | |
| 939 | + | assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam); | |
| 940 | + | assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember); | |
| 941 | + | let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap(); | |
| 942 | + | assert_eq!(route.op, Op::ListUserTeams); | |
| 943 | + | assert_eq!(input, json!({ "workspace": "acme", "username": "ana" })); | |
| 944 | + | } | |
| 945 | + | ||
| 946 | + | #[test] | |
| 947 | + | fn reviewers_are_requested_and_code_owners_checked_on_a_repository() { | |
| 948 | + | let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] }); | |
| 949 | + | let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap(); | |
| 950 | + | assert_eq!(route.op, Op::RequestReviewers); | |
| 951 | + | assert_eq!( | |
| 952 | + | input, | |
| 953 | + | json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 }) | |
| 954 | + | ); | |
| 955 | + | let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap(); | |
| 956 | + | assert_eq!(route.op, Op::RemoveRequestedReviewers); | |
| 957 | + | let query = [("ref".to_owned(), "main".to_owned())]; | |
| 958 | + | let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap(); | |
| 959 | + | assert_eq!(route.op, Op::GetCodeownersErrors); | |
| 960 | + | assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" })); | |
| 961 | + | } | |
| 962 | + | ||
| 963 | + | #[test] | |
| 797 | 964 | fn notifications_are_addressed_as_threads_and_by_issue() { | |
| 798 | 965 | let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap(); | |
| 799 | 966 | assert_eq!(route.op, Op::MarkThreadDone); | |
| ⋯ | |||
| 815 | 982 | } | |
| 816 | 983 | ||
| 817 | 984 | #[test] | |
| 985 | + | fn labels_and_milestones_are_named_in_the_path() { | |
| 986 | + | let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap(); | |
| 987 | + | assert_eq!(route.op, Op::UpdateLabel); | |
| 988 | + | assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" })); | |
| 989 | + | let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap(); | |
| 990 | + | assert_eq!(route.op, Op::RemoveIssueLabels); | |
| 991 | + | assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" })); | |
| 992 | + | let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap(); | |
| 993 | + | assert_eq!(route.op, Op::RemoveIssueLabels); | |
| 994 | + | assert_eq!(input, json!({ "number": 7, "repo": "acme/web" })); | |
| 995 | + | let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap(); | |
| 996 | + | assert_eq!(route.op, Op::AddDefaultLabels); | |
| 997 | + | let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap(); | |
| 998 | + | assert_eq!(route.op, Op::UpdateMilestone); | |
| 999 | + | assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" })); | |
| 1000 | + | let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap(); | |
| 1001 | + | assert_eq!(route.op, Op::UpdatePullRequest); | |
| 1002 | + | assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" })); | |
| 1003 | + | } | |
| 1004 | + | ||
| 1005 | + | #[test] | |
| 818 | 1006 | fn query_parameters_are_renamed() { | |
| 819 | 1007 | let query = [ | |
| 820 | 1008 | ("q".to_owned(), "parser".to_owned()), | |
| 1 | + | //! The security suite over REST and MCP: secret scanning (alerts, where | |
| 2 | + | //! each secret is, push protection bypasses and their review, validity | |
| 3 | + | //! checks, custom patterns), code scanning (alerts, analyses, SARIF | |
| 4 | + | //! uploads), vulnerability alerts, the dependency graph with its SBOM and | |
| 5 | + | //! dependency review, "Fix with g1t", settings, and the workspace's | |
| 6 | + | //! overview. | |
| 7 | + | //! | |
| 8 | + | //! The addresses follow the common ones (`/repos/{owner}/{name}/secret- | |
| 9 | + | //! scanning/alerts`, `/code-scanning/sarifs`, `/dependency-graph/sbom`), | |
| 10 | + | //! in g1t's spelling: no version prefix, `snake_case` throughout. The | |
| 11 | + | //! security service decides who may see and change what, and which parts | |
| 12 | + | //! need the Security and quality activation (a 402 says so); this module | |
| 13 | + | //! reads the input and gives each answer its public shape. | |
| 14 | + | ||
| 15 | + | use g1t_contracts::repos::RepoPath; | |
| 16 | + | use g1t_contracts::security::{AlertChange, AlertState, DismissArgs, DismissReason, ReopenArgs, SecretFinding, SecurityOverview, Vulnerability}; | |
| 17 | + | use g1t_contracts::security_suite::*; | |
| 18 | + | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 19 | + | use serde::Serialize; | |
| 20 | + | use serde::de::DeserializeOwned; | |
| 21 | + | use serde_json::{Value, json}; | |
| 22 | + | use worker::Result; | |
| 23 | + | ||
| 24 | + | use crate::operations::Services; | |
| 25 | + | ||
| 26 | + | /// One operation of the suite. | |
| 27 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 28 | + | pub enum SecurityOp { | |
| 29 | + | ListSecretAlerts, | |
| 30 | + | GetSecretAlert, | |
| 31 | + | UpdateSecretAlert, | |
| 32 | + | ListSecretLocations, | |
| 33 | + | BypassPushProtection, | |
| 34 | + | CheckSecretValidity, | |
| 35 | + | ListBypassRequests, | |
| 36 | + | ReviewBypassRequest, | |
| 37 | + | ListCustomPatterns, | |
| 38 | + | CreateCustomPattern, | |
| 39 | + | UpdateCustomPattern, | |
| 40 | + | DeleteCustomPattern, | |
| 41 | + | DryRunCustomPattern, | |
| 42 | + | ListCodeAlerts, | |
| 43 | + | GetCodeAlert, | |
| 44 | + | UpdateCodeAlert, | |
| 45 | + | ListAnalyses, | |
| 46 | + | UploadSarif, | |
| 47 | + | GetSarifUpload, | |
| 48 | + | ListVulnerabilityAlerts, | |
| 49 | + | GetVulnerabilityAlert, | |
| 50 | + | UpdateVulnerabilityAlert, | |
| 51 | + | FixAlert, | |
| 52 | + | GetDependencyGraph, | |
| 53 | + | GetSbom, | |
| 54 | + | CompareDependencies, | |
| 55 | + | GetSettings, | |
| 56 | + | UpdateSettings, | |
| 57 | + | GetWorkspaceSettings, | |
| 58 | + | UpdateWorkspaceSettings, | |
| 59 | + | GetOverview, | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | impl SecurityOp { | |
| 63 | + | /// Every one: `Op::ALL` lists each as `Op::Security(…)`, which a test | |
| 64 | + | /// checks against this. | |
| 65 | + | #[cfg(test)] | |
| 66 | + | pub const ALL: [SecurityOp; 31] = [ | |
| 67 | + | SecurityOp::ListSecretAlerts, | |
| 68 | + | SecurityOp::GetSecretAlert, | |
| 69 | + | SecurityOp::UpdateSecretAlert, | |
| 70 | + | SecurityOp::ListSecretLocations, | |
| 71 | + | SecurityOp::BypassPushProtection, | |
| 72 | + | SecurityOp::CheckSecretValidity, | |
| 73 | + | SecurityOp::ListBypassRequests, | |
| 74 | + | SecurityOp::ReviewBypassRequest, | |
| 75 | + | SecurityOp::ListCustomPatterns, | |
| 76 | + | SecurityOp::CreateCustomPattern, | |
| 77 | + | SecurityOp::UpdateCustomPattern, | |
| 78 | + | SecurityOp::DeleteCustomPattern, | |
| 79 | + | SecurityOp::DryRunCustomPattern, | |
| 80 | + | SecurityOp::ListCodeAlerts, | |
| 81 | + | SecurityOp::GetCodeAlert, | |
| 82 | + | SecurityOp::UpdateCodeAlert, | |
| 83 | + | SecurityOp::ListAnalyses, | |
| 84 | + | SecurityOp::UploadSarif, | |
| 85 | + | SecurityOp::GetSarifUpload, | |
| 86 | + | SecurityOp::ListVulnerabilityAlerts, | |
| 87 | + | SecurityOp::GetVulnerabilityAlert, | |
| 88 | + | SecurityOp::UpdateVulnerabilityAlert, | |
| 89 | + | SecurityOp::FixAlert, | |
| 90 | + | SecurityOp::GetDependencyGraph, | |
| 91 | + | SecurityOp::GetSbom, | |
| 92 | + | SecurityOp::CompareDependencies, | |
| 93 | + | SecurityOp::GetSettings, | |
| 94 | + | SecurityOp::UpdateSettings, | |
| 95 | + | SecurityOp::GetWorkspaceSettings, | |
| 96 | + | SecurityOp::UpdateWorkspaceSettings, | |
| 97 | + | SecurityOp::GetOverview, | |
| 98 | + | ]; | |
| 99 | + | ||
| 100 | + | pub fn name(self) -> &'static str { | |
| 101 | + | match self { | |
| 102 | + | SecurityOp::ListSecretAlerts => "list_secret_scanning_alerts", | |
| 103 | + | SecurityOp::GetSecretAlert => "get_secret_scanning_alert", | |
| 104 | + | SecurityOp::UpdateSecretAlert => "update_secret_scanning_alert", | |
| 105 | + | SecurityOp::ListSecretLocations => "list_secret_scanning_locations", | |
| 106 | + | SecurityOp::BypassPushProtection => "bypass_push_protection", | |
| 107 | + | SecurityOp::CheckSecretValidity => "check_secret_validity", | |
| 108 | + | SecurityOp::ListBypassRequests => "list_bypass_requests", | |
| 109 | + | SecurityOp::ReviewBypassRequest => "review_bypass_request", | |
| 110 | + | SecurityOp::ListCustomPatterns => "list_custom_patterns", | |
| 111 | + | SecurityOp::CreateCustomPattern => "create_custom_pattern", | |
| 112 | + | SecurityOp::UpdateCustomPattern => "update_custom_pattern", | |
| 113 | + | SecurityOp::DeleteCustomPattern => "delete_custom_pattern", | |
| 114 | + | SecurityOp::DryRunCustomPattern => "dry_run_custom_pattern", | |
| 115 | + | SecurityOp::ListCodeAlerts => "list_code_scanning_alerts", | |
| 116 | + | SecurityOp::GetCodeAlert => "get_code_scanning_alert", | |
| 117 | + | SecurityOp::UpdateCodeAlert => "update_code_scanning_alert", | |
| 118 | + | SecurityOp::ListAnalyses => "list_code_scanning_analyses", | |
| 119 | + | SecurityOp::UploadSarif => "upload_sarif", | |
| 120 | + | SecurityOp::GetSarifUpload => "get_sarif_upload", | |
| 121 | + | SecurityOp::ListVulnerabilityAlerts => "list_vulnerability_alerts", | |
| 122 | + | SecurityOp::GetVulnerabilityAlert => "get_vulnerability_alert", | |
| 123 | + | SecurityOp::UpdateVulnerabilityAlert => "update_vulnerability_alert", | |
| 124 | + | SecurityOp::FixAlert => "fix_security_alert", | |
| 125 | + | SecurityOp::GetDependencyGraph => "get_dependency_graph", | |
| 126 | + | SecurityOp::GetSbom => "get_sbom", | |
| 127 | + | SecurityOp::CompareDependencies => "compare_dependencies", | |
| 128 | + | SecurityOp::GetSettings => "get_security_settings", | |
| 129 | + | SecurityOp::UpdateSettings => "update_security_settings", | |
| 130 | + | SecurityOp::GetWorkspaceSettings => "get_workspace_security_settings", | |
| 131 | + | SecurityOp::UpdateWorkspaceSettings => "update_workspace_security_settings", | |
| 132 | + | SecurityOp::GetOverview => "get_security_overview", | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | /// For the API reference: "List secret scanning alerts". | |
| 137 | + | pub fn title(self) -> &'static str { | |
| 138 | + | match self { | |
| 139 | + | SecurityOp::ListSecretAlerts => "List secret scanning alerts", | |
| 140 | + | SecurityOp::GetSecretAlert => "Get a secret scanning alert", | |
| 141 | + | SecurityOp::UpdateSecretAlert => "Dismiss or reopen a secret scanning alert", | |
| 142 | + | SecurityOp::ListSecretLocations => "List where a secret was found", | |
| 143 | + | SecurityOp::BypassPushProtection => "Bypass push protection", | |
| 144 | + | SecurityOp::CheckSecretValidity => "Check whether a secret still works", | |
| 145 | + | SecurityOp::ListBypassRequests => "List push protection bypass requests", | |
| 146 | + | SecurityOp::ReviewBypassRequest => "Review a bypass request", | |
| 147 | + | SecurityOp::ListCustomPatterns => "List custom patterns", | |
| 148 | + | SecurityOp::CreateCustomPattern => "Create a custom pattern", | |
| 149 | + | SecurityOp::UpdateCustomPattern => "Update a custom pattern", | |
| 150 | + | SecurityOp::DeleteCustomPattern => "Delete a custom pattern", | |
| 151 | + | SecurityOp::DryRunCustomPattern => "Dry-run a custom pattern", | |
| 152 | + | SecurityOp::ListCodeAlerts => "List code scanning alerts", | |
| 153 | + | SecurityOp::GetCodeAlert => "Get a code scanning alert", | |
| 154 | + | SecurityOp::UpdateCodeAlert => "Dismiss or reopen a code scanning alert", | |
| 155 | + | SecurityOp::ListAnalyses => "List code scanning analyses", | |
| 156 | + | SecurityOp::UploadSarif => "Upload a SARIF file", | |
| 157 | + | SecurityOp::GetSarifUpload => "Get a SARIF upload", | |
| 158 | + | SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts", | |
| 159 | + | SecurityOp::GetVulnerabilityAlert => "Get a vulnerability alert", | |
| 160 | + | SecurityOp::UpdateVulnerabilityAlert => "Dismiss or reopen a vulnerability alert", | |
| 161 | + | SecurityOp::FixAlert => "Fix an alert with g1t", | |
| 162 | + | SecurityOp::GetDependencyGraph => "Get the dependency graph", | |
| 163 | + | SecurityOp::GetSbom => "Export an SBOM", | |
| 164 | + | SecurityOp::CompareDependencies => "Compare dependencies", | |
| 165 | + | SecurityOp::GetSettings => "Get a repository's security settings", | |
| 166 | + | SecurityOp::UpdateSettings => "Update a repository's security settings", | |
| 167 | + | SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings", | |
| 168 | + | SecurityOp::UpdateWorkspaceSettings => "Update a workspace's security settings", | |
| 169 | + | SecurityOp::GetOverview => "Get the security overview", | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | pub fn description(self) -> &'static str { | |
| 174 | + | match self { | |
| 175 | + | SecurityOp::ListSecretAlerts => "List secret scanning alerts: secrets found in pushes (blocked) and in history (open), newest first, in a repository or (with workspace) across a workspace. Filter by state (open, dismissed, fixed), secret_type, validity (active, inactive, unknown, unsupported) and bypassed. The secret itself is never returned: a preview and a fingerprint-based id only.", | |
| 176 | + | SecurityOp::GetSecretAlert => "Get one secret scanning alert by id (sec_…), with every place it was found, its activity, its bypass requests, and whether you may bypass it or only ask to.", | |
| 177 | + | SecurityOp::UpdateSecretAlert => "Dismiss a secret scanning alert (state dismissed, with a reason: false_positive, used_in_tests, revoked or wont_fix, and an optional comment) or reopen it (state open). Revoked marks it fixed; the others let pushes carrying it through. Takes the Admin role.", | |
| 178 | + | SecurityOp::ListSecretLocations => "List every place a secret was found: file, line, commit and whether a push or the history scan found it.", | |
| 179 | + | SecurityOp::BypassPushProtection => "Push past push protection for a blocked secret, with a reason: false_positive or used_in_tests (the alert is closed with that reason) or will_fix_later (it stays open, to be rotated). Recorded on the alert and in the audit log. With delegated bypass on, someone who does not review bypasses makes a request instead, which owners and the repository's admins approve or deny; the answer says which happened. Push again once it is bypassed or approved.", | |
| 180 | + | SecurityOp::CheckSecretValidity => "Ask a landed secret's issuer whether it still works, and mark the alert active or inactive. The check is the issuer's own read-only identity call over HTTPS; the secret goes nowhere else. Needs validity checks on for the workspace (and the Security and quality activation on a private repository). Formats with no safe check answer unsupported.", | |
| 181 | + | SecurityOp::ListBypassRequests => "List a workspace's push protection bypass requests, pending first. Owners and repository admins see every request; anyone else their own. Filter by state (pending, approved, denied, cancelled) or repo.", | |
| 182 | + | SecurityOp::ReviewBypassRequest => "Approve or deny a bypass request (owners and the repository's admins, never your own), or cancel your own. An approved request bypasses push protection for that secret, as its requester asked.", | |
| 183 | + | SecurityOp::ListCustomPatterns => "List custom secret patterns: a repository's own and the ones it inherits from its workspace (with repo), or a workspace's (with workspace).", | |
| 184 | + | SecurityOp::CreateCustomPattern => "Create a custom secret pattern: a name, a regular expression for the secret, optional regular expressions for what comes right before and after it, and test strings. Patterns run in linear time (no look-around or back-references) and within size limits. With publish true, push protection and scans use it at once and the history is scanned again for it; otherwise it is a draft. A repository's takes Admin; a workspace's, an owner. On a private repository it needs the Security and quality activation.", | |
| 185 | + | SecurityOp::UpdateCustomPattern => "Change a custom pattern, publish it, or turn it back into a draft (publish false). Returns where it matched each test string.", | |
| 186 | + | SecurityOp::DeleteCustomPattern => "Delete a custom pattern. Alerts it found stay.", | |
| 187 | + | SecurityOp::DryRunCustomPattern => "Run a pattern over the default branch without saving it: of the repository, or (with workspace) of up to ten of its repositories, or those named in repos. Returns the files read and up to fifty matches each, masked.", | |
| 188 | + | SecurityOp::ListCodeAlerts => "List code scanning alerts: problems a tool reported on the default branch, one per tool, category and fingerprint, open first and worst first. In a repository, or (with workspace) across a workspace. Filter by state, severity, tool and rule_id.", | |
| 189 | + | SecurityOp::GetCodeAlert => "Get one code scanning alert by number, with its rule, location, activity and the analyses that reported it.", | |
| 190 | + | SecurityOp::UpdateCodeAlert => "Dismiss a code scanning alert (state dismissed, dismissed_reason false_positive, wont_fix or used_in_tests, optional dismissed_comment) or reopen it (state open). A fixed alert reopens by itself when an analysis reports it again.", | |
| 191 | + | SecurityOp::ListAnalyses => "List code scanning analyses, newest first: each upload's run of one tool on one commit, with how many results it had and the alerts it opened and fixed.", | |
| 192 | + | SecurityOp::UploadSarif => "Upload a SARIF 2.1.0 file: sarif is the file gzipped and base64-encoded; commit_sha the full commit; ref refs/heads/<branch> or refs/pull/<number>/head. For the default branch, new results open alerts and results no longer reported fix theirs. For a pull request, its results new to it on lines it changes become review comments and the Code scanning check, which fails at the repository's threshold. Read at once; the answer says complete or failed and why. Needs the Security and quality activation on a private repository.", | |
| 193 | + | SecurityOp::GetSarifUpload => "Get a SARIF upload by id (sar_…): whether it was read, the analyses it made, and what was wrong.", | |
| 194 | + | SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts: a package a lockfile resolves with a known advisory, open first and worst first, with the security update g1t opened for it. In a repository, or (with workspace) across a workspace. Filter by state, severity, ecosystem and package.", | |
| 195 | + | SecurityOp::GetVulnerabilityAlert => "Get one vulnerability alert by id (vul_…).", | |
| 196 | + | SecurityOp::UpdateVulnerabilityAlert => "Dismiss a vulnerability alert (state dismissed, with a reason: fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used, and an optional comment) or reopen it (state open).", | |
| 197 | + | SecurityOp::FixAlert => "Put g1t on an issue to fix an alert: a code scanning alert (cod_…), a vulnerable dependency (vul_…) or a secret in the code (sec_…; rotating it stays with you). Its pull request lands through the repository's required checks. The agent's run is charged as agent usage. Returns the issue, and whether the agent started.", | |
| 198 | + | SecurityOp::GetDependencyGraph => "Get the dependency graph: every package the lockfiles on the default branch resolve, per lockfile, with whether it is direct or transitive (where the lockfile says), for development, its license when recorded, its package URL and its open vulnerability alerts.", | |
| 199 | + | SecurityOp::GetSbom => "Export the dependency graph as an SPDX 2.3 JSON document, in sbom. Every package is named by its package URL.", | |
| 200 | + | SecurityOp::CompareDependencies => "Compare the dependencies at two commits, branches or tags (basehead, as base...head): what was added and removed per lockfile, with the known vulnerabilities of what was added and whether it passes the repository's dependency review policy. Needs the Security and quality activation on a private repository.", | |
| 201 | + | SecurityOp::GetSettings => "Get a repository's security settings: when the Code scanning check fails, dependency review and its policy, its workspace's settings, and whether the paid features are on for it.", | |
| 202 | + | SecurityOp::UpdateSettings => "Change a repository's security settings: code_scanning_gate (none, errors, critical, high, medium or any), dependency_review, review_fail_on (critical, high, medium, low or none), review_deny_licenses (SPDX ids) and review_comment. Takes the Maintain role. Require the Code scanning and Dependency review checks in branch protection to gate merges on them.", | |
| 203 | + | SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings (delegated bypass, validity checks) and whether it has the Security and quality activation.", | |
| 204 | + | SecurityOp::UpdateWorkspaceSettings => "Turn delegated bypass and validity checks on or off for a workspace. Owners only.", | |
| 205 | + | SecurityOp::GetOverview => "Get a workspace's security overview: open alerts by type and severity, how many opened and closed in the last days (7 to 90, 30 by default), a daily trend, and for each repository which features are on and what is open, most in need first. Private repositories count with the Security and quality activation only.", | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | /// Whether the operation is about one repository named by `repo` | |
| 210 | + | /// (rather than a workspace, or either). | |
| 211 | + | pub fn needs_repo(self) -> bool { | |
| 212 | + | !matches!( | |
| 213 | + | self, | |
| 214 | + | SecurityOp::ListSecretAlerts | |
| 215 | + | | SecurityOp::ListCodeAlerts | |
| 216 | + | | SecurityOp::ListVulnerabilityAlerts | |
| 217 | + | | SecurityOp::ListBypassRequests | |
| 218 | + | | SecurityOp::ReviewBypassRequest | |
| 219 | + | | SecurityOp::ListCustomPatterns | |
| 220 | + | | SecurityOp::CreateCustomPattern | |
| 221 | + | | SecurityOp::UpdateCustomPattern | |
| 222 | + | | SecurityOp::DeleteCustomPattern | |
| 223 | + | | SecurityOp::DryRunCustomPattern | |
| 224 | + | | SecurityOp::GetWorkspaceSettings | |
| 225 | + | | SecurityOp::UpdateWorkspaceSettings | |
| 226 | + | | SecurityOp::GetOverview | |
| 227 | + | ) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | pub fn input(self) -> Value { | |
| 231 | + | let repo = || json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 232 | + | let either = |mut properties: Value| { | |
| 233 | + | properties["repo"] = json!({ "type": "string", "description": "Repository as \"owner/name\". Or give workspace." }); | |
| 234 | + | properties["workspace"] = json!({ "type": "string", "description": "Instead of repo: the workspace's slug, for all of it (or its own, for patterns)." }); | |
| 235 | + | properties | |
| 236 | + | }; | |
| 237 | + | let secret_id = || json!({ "type": "string", "description": "The alert's id: sec_…" }); | |
| 238 | + | let number = || json!({ "type": "integer", "description": "The code scanning alert's number." }); | |
| 239 | + | let state = || json!({ "type": "string", "enum": ["open", "dismissed", "fixed"], "description": "Only alerts in this state." }); | |
| 240 | + | let severity = || json!({ "type": "string", "enum": ["critical", "high", "medium", "low", "unknown"], "description": "Only alerts of this severity." }); | |
| 241 | + | let set_state = || json!({ "type": "string", "enum": ["open", "dismissed"], "description": "dismissed, with a reason, or open to reopen." }); | |
| 242 | + | let comment = || json!({ "type": "string", "description": "Why, in a sentence; kept with the alert. At most 500 characters." }); | |
| 243 | + | let pattern_fields = |mut properties: Value| { | |
| 244 | + | properties["pattern_name"] = json!({ "type": "string", "description": "What people call it: \"Acme API key\"." }); | |
| 245 | + | properties["pattern"] = json!({ "type": "string", "description": "The secret's format, as a regular expression (the regex crate's syntax: no look-around or back-references). At most 1,000 characters; it may not match an empty string." }); | |
| 246 | + | properties["before"] = json!({ "type": "string", "description": "What must come right before the secret, as a regular expression. Default: the start of the line or a character that is not a letter or digit." }); | |
| 247 | + | properties["after"] = json!({ "type": "string", "description": "What must come right after it. Default: the end of the line or a character that is not a letter or digit." }); | |
| 248 | + | properties | |
| 249 | + | }; | |
| 250 | + | let workspace = || json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." }); | |
| 251 | + | let (properties, required): (Value, &[&str]) = match self { | |
| 252 | + | SecurityOp::ListSecretAlerts => ( | |
| 253 | + | either(json!({ | |
| 254 | + | "state": state(), | |
| 255 | + | "secret_type": { "type": "string", "description": "Only this kind of secret: aws_access_key, github_token, custom_pattern, …" }, | |
| 256 | + | "validity": { "type": "string", "enum": ["active", "inactive", "unknown", "unsupported"], "description": "Only alerts whose issuer said this when last asked." }, | |
| 257 | + | "bypassed": { "type": "boolean", "description": "Only alerts someone bypassed push protection for (true), or not (false)." }, | |
| 258 | + | })), | |
| 259 | + | &[], | |
| 260 | + | ), | |
| 261 | + | SecurityOp::GetSecretAlert | SecurityOp::ListSecretLocations | SecurityOp::CheckSecretValidity => { | |
| 262 | + | (json!({ "repo": repo(), "id": secret_id() }), &["repo", "id"]) | |
| 263 | + | } | |
| 264 | + | SecurityOp::UpdateSecretAlert => ( | |
| 265 | + | json!({ | |
| 266 | + | "repo": repo(), | |
| 267 | + | "id": secret_id(), | |
| 268 | + | "state": set_state(), | |
| 269 | + | "reason": { "type": "string", "enum": ["false_positive", "used_in_tests", "revoked", "wont_fix"], "description": "Why it is dismissed. revoked marks it fixed." }, | |
| 270 | + | "comment": comment(), | |
| 271 | + | }), | |
| 272 | + | &["repo", "id", "state"], | |
| 273 | + | ), | |
| 274 | + | SecurityOp::BypassPushProtection => ( | |
| 275 | + | json!({ | |
| 276 | + | "repo": repo(), | |
| 277 | + | "id": secret_id(), | |
| 278 | + | "reason": { "type": "string", "enum": BypassReason::ALL.map(BypassReason::as_str), "description": "false_positive: not a secret. used_in_tests: a value for tests. will_fix_later: real, to be rotated (the alert stays open)." }, | |
| 279 | + | "comment": comment(), | |
| 280 | + | }), | |
| 281 | + | &["repo", "id", "reason"], | |
| 282 | + | ), | |
| 283 | + | SecurityOp::ListBypassRequests => ( | |
| 284 | + | json!({ | |
| 285 | + | "workspace": workspace(), | |
| 286 | + | "repo": { "type": "string", "description": "Only this repository's, as \"owner/name\"." }, | |
| 287 | + | "state": { "type": "string", "enum": ["pending", "approved", "denied", "cancelled"], "description": "Only requests in this state." }, | |
| 288 | + | }), | |
| 289 | + | &["workspace"], | |
| 290 | + | ), | |
| 291 | + | SecurityOp::ReviewBypassRequest => ( | |
| 292 | + | json!({ | |
| 293 | + | "workspace": workspace(), | |
| 294 | + | "id": { "type": "string", "description": "The request's id: byp_…" }, | |
| 295 | + | "decision": { "type": "string", "enum": ["approve", "deny", "cancel"], "description": "approve or deny (reviewers), or cancel (your own)." }, | |
| 296 | + | "comment": comment(), | |
| 297 | + | }), | |
| 298 | + | &["workspace", "id", "decision"], | |
| 299 | + | ), | |
| 300 | + | SecurityOp::ListCustomPatterns => (either(json!({})), &[]), | |
| 301 | + | SecurityOp::CreateCustomPattern => ( | |
| 302 | + | either(pattern_fields(json!({ | |
| 303 | + | "test_strings": { "type": "array", "items": { "type": "string" }, "description": "Up to 20 strings to show the pattern working on." }, | |
| 304 | + | "publish": { "type": "boolean", "description": "Use it in push protection and scans now (true), or keep a draft (false, the default)." }, | |
| 305 | + | }))), | |
| 306 | + | &["pattern_name", "pattern"], | |
| 307 | + | ), | |
| 308 | + | SecurityOp::UpdateCustomPattern => ( | |
| 309 | + | either(pattern_fields(json!({ | |
| 310 | + | "id": { "type": "string", "description": "The pattern's id: pat_…" }, | |
| 311 | + | "test_strings": { "type": "array", "items": { "type": "string" }, "description": "Up to 20 strings to show the pattern working on." }, | |
| 312 | + | "publish": { "type": "boolean", "description": "Published (true) or a draft (false)." }, | |
| 313 | + | }))), | |
| 314 | + | &["id", "pattern_name", "pattern"], | |
| 315 | + | ), | |
| 316 | + | SecurityOp::DeleteCustomPattern => (either(json!({ "id": { "type": "string", "description": "The pattern's id: pat_…" } })), &["id"]), | |
| 317 | + | SecurityOp::DryRunCustomPattern => ( | |
| 318 | + | either(pattern_fields(json!({ | |
| 319 | + | "repos": { "type": "array", "items": { "type": "string" }, "description": "With workspace: repository names to run it on; the first ten when empty." }, | |
| 320 | + | }))), | |
| 321 | + | &["pattern"], | |
| 322 | + | ), | |
| 323 | + | SecurityOp::ListCodeAlerts => ( | |
| 324 | + | either(json!({ | |
| 325 | + | "state": state(), | |
| 326 | + | "severity": severity(), | |
| 327 | + | "tool": { "type": "string", "description": "Only this tool's: \"ESLint\"." }, | |
| 328 | + | "rule_id": { "type": "string", "description": "Only this rule's." }, | |
| 329 | + | })), | |
| 330 | + | &[], | |
| 331 | + | ), | |
| 332 | + | SecurityOp::GetCodeAlert => (json!({ "repo": repo(), "number": number() }), &["repo", "number"]), | |
| 333 | + | SecurityOp::UpdateCodeAlert => ( | |
| 334 | + | json!({ | |
| 335 | + | "repo": repo(), | |
| 336 | + | "number": number(), | |
| 337 | + | "state": set_state(), | |
| 338 | + | "dismissed_reason": { "type": "string", "enum": ["false_positive", "wont_fix", "used_in_tests"], "description": "Why it is dismissed." }, | |
| 339 | + | "dismissed_comment": comment(), | |
| 340 | + | }), | |
| 341 | + | &["repo", "number", "state"], | |
| 342 | + | ), | |
| 343 | + | SecurityOp::ListAnalyses => (json!({ "repo": repo() }), &["repo"]), | |
| 344 | + | SecurityOp::UploadSarif => ( | |
| 345 | + | json!({ | |
| 346 | + | "repo": repo(), | |
| 347 | + | "commit_sha": { "type": "string", "description": "The full hash of the commit analysed." }, | |
| 348 | + | "ref": { "type": "string", "description": "refs/heads/<branch>, or refs/pull/<number>/head (or /merge) for a pull request." }, | |
| 349 | + | "sarif": { "type": "string", "description": "The SARIF 2.1.0 file, gzipped, then base64-encoded. At most 10 MB encoded and 40 MB unzipped." }, | |
| 350 | + | "tool_name": { "type": "string", "description": "The tool's name, when the file has one run and you want another name for it." }, | |
| 351 | + | "category": { "type": "string", "description": "Which analysis this is, when a repository runs several of one tool. Default: the run's automationDetails.id, or the tool's name." }, | |
| 352 | + | "checkout_uri": { "type": "string", "description": "Where the files were checked out (file:///home/runner/work/repo), so absolute paths become repository paths." }, | |
| 353 | + | }), | |
| 354 | + | &["repo", "commit_sha", "ref", "sarif"], | |
| 355 | + | ), | |
| 356 | + | SecurityOp::GetSarifUpload => (json!({ "repo": repo(), "id": { "type": "string", "description": "The upload's id: sar_…" } }), &["repo", "id"]), | |
| 357 | + | SecurityOp::ListVulnerabilityAlerts => ( | |
| 358 | + | either(json!({ | |
| 359 | + | "state": state(), | |
| 360 | + | "severity": severity(), | |
| 361 | + | "ecosystem": { "type": "string", "description": "Only this ecosystem's: npm, crates.io, Go or PyPI." }, | |
| 362 | + | "package": { "type": "string", "description": "Only this package's." }, | |
| 363 | + | })), | |
| 364 | + | &[], | |
| 365 | + | ), | |
| 366 | + | SecurityOp::GetVulnerabilityAlert => (json!({ "repo": repo(), "id": { "type": "string", "description": "The alert's id: vul_…" } }), &["repo", "id"]), | |
| 367 | + | SecurityOp::UpdateVulnerabilityAlert => ( | |
| 368 | + | json!({ | |
| 369 | + | "repo": repo(), | |
| 370 | + | "id": { "type": "string", "description": "The alert's id: vul_…" }, | |
| 371 | + | "state": set_state(), | |
| 372 | + | "reason": { "type": "string", "enum": ["fix_started", "no_bandwidth", "tolerable_risk", "inaccurate", "not_used"], "description": "Why it is dismissed." }, | |
| 373 | + | "comment": comment(), | |
| 374 | + | }), | |
| 375 | + | &["repo", "id", "state"], | |
| 376 | + | ), | |
| 377 | + | SecurityOp::FixAlert => ( | |
| 378 | + | json!({ "repo": repo(), "id": { "type": "string", "description": "The alert's id: cod_…, vul_… or sec_…" } }), | |
| 379 | + | &["repo", "id"], | |
| 380 | + | ), | |
| 381 | + | SecurityOp::GetDependencyGraph | SecurityOp::GetSbom | SecurityOp::GetSettings => (json!({ "repo": repo() }), &["repo"]), | |
| 382 | + | SecurityOp::CompareDependencies => ( | |
| 383 | + | json!({ | |
| 384 | + | "repo": repo(), | |
| 385 | + | "basehead": { "type": "string", "description": "base...head: two commits, branches or tags, e.g. main...my-branch." }, | |
| 386 | + | }), | |
| 387 | + | &["repo", "basehead"], | |
| 388 | + | ), | |
| 389 | + | SecurityOp::UpdateSettings => ( | |
| 390 | + | json!({ | |
| 391 | + | "repo": repo(), | |
| 392 | + | "code_scanning_gate": { "type": "string", "enum": ["none", "errors", "critical", "high", "medium", "any"], "description": "When a pull request's Code scanning check fails: never, on errors, or on new results of this security severity or worse (and errors)." }, | |
| 393 | + | "dependency_review": { "type": "boolean", "description": "Whether pull requests get the Dependency review check." }, | |
| 394 | + | "review_fail_on": { "type": "string", "enum": ["critical", "high", "medium", "low", "none"], "description": "The lowest severity of a known vulnerability in an added package that fails the review." }, | |
| 395 | + | "review_deny_licenses": { "type": "array", "items": { "type": "string" }, "description": "SPDX license ids an added package may not have." }, | |
| 396 | + | "review_comment": { "type": "boolean", "description": "Whether the review comments its summary on the pull request." }, | |
| 397 | + | }), | |
| 398 | + | &["repo"], | |
| 399 | + | ), | |
| 400 | + | SecurityOp::GetWorkspaceSettings => (json!({ "workspace": workspace() }), &["workspace"]), | |
| 401 | + | SecurityOp::UpdateWorkspaceSettings => ( | |
| 402 | + | json!({ | |
| 403 | + | "workspace": workspace(), | |
| 404 | + | "delegated_bypass": { "type": "boolean", "description": "Bypasses need an owner's or the repository's admins' approval." }, | |
| 405 | + | "validity_checks": { "type": "boolean", "description": "Ask issuers whether secrets still work, where that can be done safely." }, | |
| 406 | + | }), | |
| 407 | + | &["workspace"], | |
| 408 | + | ), | |
| 409 | + | SecurityOp::GetOverview => ( | |
| 410 | + | json!({ "workspace": workspace(), "days": { "type": "integer", "description": "Days of trend, 7 to 90. Default 30." } }), | |
| 411 | + | &["workspace"], | |
| 412 | + | ), | |
| 413 | + | }; | |
| 414 | + | let mut schema = json!({ "type": "object", "properties": properties }); | |
| 415 | + | if !required.is_empty() { | |
| 416 | + | schema["required"] = json!(required); | |
| 417 | + | } | |
| 418 | + | schema | |
| 419 | + | } | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| 423 | + | Ok(Outcome::fail(code, message)) | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | fn ok<T: Serialize>(value: &T) -> Result<Outcome<Value>> { | |
| 427 | + | Ok(Outcome::Ok(serde_json::to_value(value)?)) | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | fn text(input: &Value, key: &str) -> Option<String> { | |
| 431 | + | input[key].as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_owned) | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | fn flag(input: &Value, key: &str) -> Option<bool> { | |
| 435 | + | match &input[key] { | |
| 436 | + | Value::Bool(value) => Some(*value), | |
| 437 | + | Value::String(text) => match text.trim() { | |
| 438 | + | "true" | "1" => Some(true), | |
| 439 | + | "false" | "0" => Some(false), | |
| 440 | + | _ => None, | |
| 441 | + | }, | |
| 442 | + | _ => None, | |
| 443 | + | } | |
| 444 | + | } | |
| 445 | + | ||
| 446 | + | fn whole(input: &Value, key: &str) -> Option<u32> { | |
| 447 | + | match &input[key] { | |
| 448 | + | Value::Number(number) => number.as_u64().and_then(|n| u32::try_from(n).ok()), | |
| 449 | + | Value::String(digits) => digits.trim().parse().ok(), | |
| 450 | + | _ => None, | |
| 451 | + | } | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | fn strings(input: &Value, key: &str) -> Vec<String> { | |
| 455 | + | input[key].as_array().map(|items| items.iter().filter_map(|item| item.as_str().map(str::to_owned)).collect()).unwrap_or_default() | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | /// One of `allowed`, or why not. | |
| 459 | + | fn one_of(input: &Value, key: &str, allowed: &[&str]) -> std::result::Result<Option<String>, String> { | |
| 460 | + | match text(input, key) { | |
| 461 | + | None => Ok(None), | |
| 462 | + | Some(given) => { | |
| 463 | + | let lower = given.to_lowercase(); | |
| 464 | + | if allowed.contains(&lower.as_str()) { | |
| 465 | + | Ok(Some(lower)) | |
| 466 | + | } else { | |
| 467 | + | Err(format!("{key} is {}, not {given}.", allowed.join(", "))) | |
| 468 | + | } | |
| 469 | + | } | |
| 470 | + | } | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | /// Filters for secret scanning alerts. | |
| 474 | + | #[derive(Debug, Default, PartialEq)] | |
| 475 | + | pub(crate) struct SecretFilters { | |
| 476 | + | pub state: Option<AlertState>, | |
| 477 | + | pub secret_type: Option<String>, | |
| 478 | + | pub validity: Option<String>, | |
| 479 | + | pub bypassed: Option<bool>, | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | pub(crate) fn secret_filters(input: &Value) -> std::result::Result<SecretFilters, String> { | |
| 483 | + | Ok(SecretFilters { | |
| 484 | + | state: one_of(input, "state", &["open", "dismissed", "fixed"])?.and_then(|state| AlertState::parse(&state)), | |
| 485 | + | secret_type: text(input, "secret_type"), | |
| 486 | + | validity: one_of(input, "validity", &["active", "inactive", "unknown", "unsupported"])?, | |
| 487 | + | bypassed: match &input["bypassed"] { | |
| 488 | + | Value::Null => None, | |
| 489 | + | _ => Some(flag(input, "bypassed").ok_or("bypassed is true or false.")?), | |
| 490 | + | }, | |
| 491 | + | }) | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | impl SecretFilters { | |
| 495 | + | pub(crate) fn keeps(&self, secret: &SecretFinding) -> bool { | |
| 496 | + | self.state.is_none_or(|state| secret.state == state) | |
| 497 | + | && self.secret_type.as_deref().is_none_or(|kind| secret.kind == kind) | |
| 498 | + | && self.validity.as_deref().is_none_or(|validity| secret.validity.as_deref().unwrap_or("unknown") == validity) | |
| 499 | + | && self.bypassed.is_none_or(|bypassed| secret.bypass.is_some() == bypassed) | |
| 500 | + | } | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | /// Filters for code scanning alerts. | |
| 504 | + | #[derive(Debug, Default, PartialEq)] | |
| 505 | + | pub(crate) struct CodeFilters { | |
| 506 | + | pub state: Option<AlertState>, | |
| 507 | + | pub severity: Option<String>, | |
| 508 | + | pub tool: Option<String>, | |
| 509 | + | pub rule_id: Option<String>, | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | pub(crate) fn code_filters(input: &Value) -> std::result::Result<CodeFilters, String> { | |
| 513 | + | Ok(CodeFilters { | |
| 514 | + | state: one_of(input, "state", &["open", "dismissed", "fixed"])?.and_then(|state| AlertState::parse(&state)), | |
| 515 | + | severity: one_of(input, "severity", &["critical", "high", "medium", "low", "unknown"])?, | |
| 516 | + | tool: text(input, "tool"), | |
| 517 | + | rule_id: text(input, "rule_id"), | |
| 518 | + | }) | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | impl CodeFilters { | |
| 522 | + | pub(crate) fn keeps(&self, alert: &CodeAlert) -> bool { | |
| 523 | + | self.state.is_none_or(|state| alert.state == state) | |
| 524 | + | && self.severity.as_deref().is_none_or(|severity| alert.severity == severity) | |
| 525 | + | && self.tool.as_deref().is_none_or(|tool| alert.tool.eq_ignore_ascii_case(tool)) | |
| 526 | + | && self.rule_id.as_deref().is_none_or(|rule| alert.rule_id == rule) | |
| 527 | + | } | |
| 528 | + | } | |
| 529 | + | ||
| 530 | + | /// Filters for vulnerability alerts. | |
| 531 | + | #[derive(Debug, Default, PartialEq)] | |
| 532 | + | pub(crate) struct VulnerabilityFilters { | |
| 533 | + | pub state: Option<AlertState>, | |
| 534 | + | pub severity: Option<String>, | |
| 535 | + | pub ecosystem: Option<String>, | |
| 536 | + | pub package: Option<String>, | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | pub(crate) fn vulnerability_filters(input: &Value) -> std::result::Result<VulnerabilityFilters, String> { | |
| 540 | + | Ok(VulnerabilityFilters { | |
| 541 | + | state: one_of(input, "state", &["open", "dismissed", "fixed"])?.and_then(|state| AlertState::parse(&state)), | |
| 542 | + | severity: one_of(input, "severity", &["critical", "high", "medium", "low", "unknown"])?, | |
| 543 | + | ecosystem: text(input, "ecosystem"), | |
| 544 | + | package: text(input, "package"), | |
| 545 | + | }) | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | impl VulnerabilityFilters { | |
| 549 | + | pub(crate) fn keeps(&self, vuln: &Vulnerability) -> bool { | |
| 550 | + | self.state.is_none_or(|state| vuln.state == state) | |
| 551 | + | && self.severity.as_deref().is_none_or(|severity| vuln.severity == severity) | |
| 552 | + | && self.ecosystem.as_deref().is_none_or(|ecosystem| vuln.ecosystem.eq_ignore_ascii_case(ecosystem)) | |
| 553 | + | && self.package.as_deref().is_none_or(|package| vuln.package == package) | |
| 554 | + | } | |
| 555 | + | } | |
| 556 | + | ||
| 557 | + | /// `base...head` (or `base..head`), as compare_dependencies reads it. | |
| 558 | + | pub(crate) fn base_head(text: &str) -> Option<(String, String)> { | |
| 559 | + | let (base, head) = text.split_once("...").or_else(|| text.split_once(".."))?; | |
| 560 | + | let (base, head) = (base.trim(), head.trim()); | |
| 561 | + | (!base.is_empty() && !head.is_empty()).then(|| (base.to_owned(), head.to_owned())) | |
| 562 | + | } | |
| 563 | + | ||
| 564 | + | /// What dismissing or reopening an alert of `kind` asks: the reason, if | |
| 565 | + | /// dismissing, checked against the reasons that kind takes. | |
| 566 | + | pub(crate) fn state_change(input: &Value, reason_key: &str, reasons: &[DismissReason]) -> std::result::Result<Option<DismissReason>, String> { | |
| 567 | + | match text(input, "state").as_deref() { | |
| 568 | + | Some("open") => Ok(None), | |
| 569 | + | Some("dismissed") => { | |
| 570 | + | let names: Vec<&str> = reasons.iter().map(|reason| reason.as_str()).collect(); | |
| 571 | + | let given = text(input, reason_key).ok_or_else(|| format!("Give {reason_key}: one of {}.", names.join(", ")))?; | |
| 572 | + | DismissReason::parse(&given) | |
| 573 | + | .filter(|reason| reasons.contains(reason)) | |
| 574 | + | .map(Some) | |
| 575 | + | .ok_or_else(|| format!("{reason_key} is {}, not {given}.", names.join(", "))) | |
| 576 | + | } | |
| 577 | + | _ => Err("state is open or dismissed.".to_owned()), | |
| 578 | + | } | |
| 579 | + | } | |
| 580 | + | ||
| 581 | + | const SECRET_REASONS: [DismissReason; 4] = [DismissReason::FalsePositive, DismissReason::UsedInTests, DismissReason::Revoked, DismissReason::WontFix]; | |
| 582 | + | const CODE_REASONS: [DismissReason; 3] = [DismissReason::FalsePositive, DismissReason::WontFix, DismissReason::UsedInTests]; | |
| 583 | + | const DEPENDENCY_REASONS: [DismissReason; 5] = [ | |
| 584 | + | DismissReason::FixStarted, | |
| 585 | + | DismissReason::NoBandwidth, | |
| 586 | + | DismissReason::TolerableRisk, | |
| 587 | + | DismissReason::Inaccurate, | |
| 588 | + | DismissReason::NotUsed, | |
| 589 | + | ]; | |
| 590 | + | ||
| 591 | + | async fn call<A: Serialize, T: DeserializeOwned>(services: &Services, method: &str, args: &A) -> Result<Outcome<T>> { | |
| 592 | + | g1t_kit::call(&services.security, method, args).await | |
| 593 | + | } | |
| 594 | + | ||
| 595 | + | /// Passes a service's outcome through as it is. | |
| 596 | + | async fn pass<A: Serialize>(services: &Services, method: &str, args: &A) -> Result<Outcome<Value>> { | |
| 597 | + | call(services, method, args).await | |
| 598 | + | } | |
| 599 | + | ||
| 600 | + | fn path_of(input: &Value) -> Option<RepoPath> { | |
| 601 | + | crate::operations::repo_path(input) | |
| 602 | + | } | |
| 603 | + | ||
| 604 | + | pub async fn run(op: SecurityOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 605 | + | let actor = || viewer.clone().unwrap_or_default(); | |
| 606 | + | let repo = path_of(input); | |
| 607 | + | let workspace = text(input, "workspace").map(|slug| slug.to_lowercase()); | |
| 608 | + | let need_repo = || "Give the repository as \"owner/name\".".to_owned(); | |
| 609 | + | // Operations on a repository or a workspace: which. | |
| 610 | + | let scope_repo = repo.clone(); | |
| 611 | + | let scope_workspace = || workspace.clone().or_else(|| repo.as_ref().map(|repo| repo.namespace.to_lowercase())); | |
| 612 | + | match op { | |
| 613 | + | SecurityOp::ListSecretAlerts => { | |
| 614 | + | let filters = match secret_filters(input) { | |
| 615 | + | Ok(filters) => filters, | |
| 616 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 617 | + | }; | |
| 618 | + | match (scope_repo, workspace) { | |
| 619 | + | (Some(repo), _) => { | |
| 620 | + | let overview: Outcome<SecurityOverview> = | |
| 621 | + | call(services, "overview", &g1t_contracts::security::OverviewArgs { repo, viewer: viewer.clone() }).await?; | |
| 622 | + | match overview { | |
| 623 | + | Outcome::Ok(overview) => { | |
| 624 | + | let alerts: Vec<SecretFinding> = overview.secrets.into_iter().filter(|secret| filters.keeps(secret)).collect(); | |
| 625 | + | ok(&alerts) | |
| 626 | + | } | |
| 627 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 628 | + | } | |
| 629 | + | } | |
| 630 | + | (None, Some(workspace)) => { | |
| 631 | + | let found: Outcome<Vec<WorkspaceAlert>> = call( | |
| 632 | + | services, | |
| 633 | + | "workspace_alerts", | |
| 634 | + | &WorkspaceAlertsArgs { viewer: viewer.clone(), workspace, alert_type: AlertType::SecretScanning }, | |
| 635 | + | ) | |
| 636 | + | .await?; | |
| 637 | + | match found { | |
| 638 | + | Outcome::Ok(found) => { | |
| 639 | + | let alerts: Vec<WorkspaceAlert> = | |
| 640 | + | found.into_iter().filter(|alert| alert.secret.as_ref().is_some_and(|secret| filters.keeps(secret))).collect(); | |
| 641 | + | ok(&alerts) | |
| 642 | + | } | |
| 643 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 644 | + | } | |
| 645 | + | } | |
| 646 | + | (None, None) => failed(FailureCode::Invalid, "Give repo, or workspace for all of one."), | |
| 647 | + | } | |
| 648 | + | } | |
| 649 | + | SecurityOp::GetSecretAlert | SecurityOp::ListSecretLocations => { | |
| 650 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 651 | + | let id = text(input, "id").unwrap_or_default(); | |
| 652 | + | let detail: Outcome<SecretAlertDetail> = call(services, "secret_alert", &SecretAlertArgs { viewer: viewer.clone(), repo, id }).await?; | |
| 653 | + | match (detail, op) { | |
| 654 | + | (Outcome::Ok(detail), SecurityOp::ListSecretLocations) => ok(&detail.locations), | |
| 655 | + | (Outcome::Ok(detail), _) => ok(&detail), | |
| 656 | + | (Outcome::Fail(failure), _) => Ok(Outcome::Fail(failure)), | |
| 657 | + | } | |
| 658 | + | } | |
| 659 | + | SecurityOp::UpdateSecretAlert | SecurityOp::UpdateVulnerabilityAlert => { | |
| 660 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 661 | + | let id = text(input, "id").unwrap_or_default(); | |
| 662 | + | let (reasons, wants): (&[DismissReason], &str) = match op { | |
| 663 | + | SecurityOp::UpdateSecretAlert => (&SECRET_REASONS, "sec_"), | |
| 664 | + | _ => (&DEPENDENCY_REASONS, "vul_"), | |
| 665 | + | }; | |
| 666 | + | if !id.starts_with(wants) { | |
| 667 | + | return failed(FailureCode::NotFound, "No such alert."); | |
| 668 | + | } | |
| 669 | + | let reason = match state_change(input, "reason", reasons) { | |
| 670 | + | Ok(reason) => reason, | |
| 671 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 672 | + | }; | |
| 673 | + | let changed: Outcome<AlertChange> = match reason { | |
| 674 | + | Some(reason) => { | |
| 675 | + | let comment = text(input, "comment").unwrap_or_default(); | |
| 676 | + | call(services, "dismiss", &DismissArgs { actor: actor(), repo, id, reason, comment }).await? | |
| 677 | + | } | |
| 678 | + | None => call(services, "reopen", &ReopenArgs { actor: actor(), repo, id }).await?, | |
| 679 | + | }; | |
| 680 | + | match changed { | |
| 681 | + | Outcome::Ok(AlertChange { secret: Some(secret), .. }) => ok(&secret), | |
| 682 | + | Outcome::Ok(AlertChange { vulnerability: Some(vuln), .. }) => ok(&vuln), | |
| 683 | + | Outcome::Ok(_) => failed(FailureCode::NotFound, "No such alert."), | |
| 684 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 685 | + | } | |
| 686 | + | } | |
| 687 | + | SecurityOp::BypassPushProtection => { | |
| 688 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 689 | + | let Some(reason) = text(input, "reason").as_deref().and_then(BypassReason::parse) else { | |
| 690 | + | return failed(FailureCode::Invalid, "reason is false_positive, used_in_tests or will_fix_later."); | |
| 691 | + | }; | |
| 692 | + | let args = BypassArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default(), reason, comment: text(input, "comment").unwrap_or_default() }; | |
| 693 | + | pass(services, "bypass", &args).await | |
| 694 | + | } | |
| 695 | + | SecurityOp::CheckSecretValidity => { | |
| 696 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 697 | + | pass(services, "check_validity", &CheckValidityArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default() }).await | |
| 698 | + | } | |
| 699 | + | SecurityOp::ListBypassRequests => { | |
| 700 | + | let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") }; | |
| 701 | + | let state = match one_of(input, "state", &["pending", "approved", "denied", "cancelled"]) { | |
| 702 | + | Ok(state) => state, | |
| 703 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 704 | + | }; | |
| 705 | + | pass(services, "bypass_requests", &BypassRequestsArgs { viewer: viewer.clone(), workspace, repo, state }).await | |
| 706 | + | } | |
| 707 | + | SecurityOp::ReviewBypassRequest => { | |
| 708 | + | let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") }; | |
| 709 | + | let decision = match one_of(input, "decision", &["approve", "deny", "cancel"]) { | |
| 710 | + | Ok(Some(decision)) => decision, | |
| 711 | + | Ok(None) => return failed(FailureCode::Invalid, "decision is approve, deny or cancel."), | |
| 712 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 713 | + | }; | |
| 714 | + | let args = ReviewBypassArgs { | |
| 715 | + | actor: actor(), | |
| 716 | + | workspace, | |
| 717 | + | id: text(input, "id").unwrap_or_default(), | |
| 718 | + | decision, | |
| 719 | + | comment: text(input, "comment").unwrap_or_default(), | |
| 720 | + | }; | |
| 721 | + | pass(services, "review_bypass", &args).await | |
| 722 | + | } | |
| 723 | + | SecurityOp::ListCustomPatterns => { | |
| 724 | + | let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") }; | |
| 725 | + | pass(services, "custom_patterns", &CustomPatternsArgs { viewer: viewer.clone(), workspace, repo: scope_repo }).await | |
| 726 | + | } | |
| 727 | + | SecurityOp::CreateCustomPattern | SecurityOp::UpdateCustomPattern => { | |
| 728 | + | let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") }; | |
| 729 | + | let args = SaveCustomPatternArgs { | |
| 730 | + | actor: actor(), | |
| 731 | + | workspace, | |
| 732 | + | repo: scope_repo, | |
| 733 | + | id: if op == SecurityOp::UpdateCustomPattern { text(input, "id") } else { None }, | |
| 734 | + | name: text(input, "pattern_name").unwrap_or_default(), | |
| 735 | + | pattern: input["pattern"].as_str().unwrap_or_default().to_owned(), | |
| 736 | + | before: text(input, "before"), | |
| 737 | + | after: text(input, "after"), | |
| 738 | + | test_strings: strings(input, "test_strings"), | |
| 739 | + | publish: flag(input, "publish").unwrap_or(false), | |
| 740 | + | }; | |
| 741 | + | pass(services, "save_custom_pattern", &args).await | |
| 742 | + | } | |
| 743 | + | SecurityOp::DeleteCustomPattern => { | |
| 744 | + | let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") }; | |
| 745 | + | let args = DeleteCustomPatternArgs { actor: actor(), workspace, repo: scope_repo, id: text(input, "id").unwrap_or_default() }; | |
| 746 | + | match call::<_, bool>(services, "delete_custom_pattern", &args).await? { | |
| 747 | + | Outcome::Ok(_) => ok(&json!({ "deleted": true })), | |
| 748 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 749 | + | } | |
| 750 | + | } | |
| 751 | + | SecurityOp::DryRunCustomPattern => { | |
| 752 | + | let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") }; | |
| 753 | + | let args = DryRunPatternArgs { | |
| 754 | + | actor: actor(), | |
| 755 | + | workspace, | |
| 756 | + | repo: scope_repo, | |
| 757 | + | repos: strings(input, "repos"), | |
| 758 | + | pattern: input["pattern"].as_str().unwrap_or_default().to_owned(), | |
| 759 | + | before: text(input, "before"), | |
| 760 | + | after: text(input, "after"), | |
| 761 | + | }; | |
| 762 | + | pass(services, "dry_run_pattern", &args).await | |
| 763 | + | } | |
| 764 | + | SecurityOp::ListCodeAlerts => { | |
| 765 | + | let filters = match code_filters(input) { | |
| 766 | + | Ok(filters) => filters, | |
| 767 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 768 | + | }; | |
| 769 | + | match (scope_repo, workspace) { | |
| 770 | + | (Some(repo), _) => match call::<_, CodeScanning>(services, "code_scanning", &CodeScanningArgs { viewer: viewer.clone(), repo }).await? { | |
| 771 | + | Outcome::Ok(scanning) => { | |
| 772 | + | let alerts: Vec<CodeAlert> = scanning.alerts.into_iter().filter(|alert| filters.keeps(alert)).collect(); | |
| 773 | + | ok(&alerts) | |
| 774 | + | } | |
| 775 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 776 | + | }, | |
| 777 | + | (None, Some(workspace)) => { | |
| 778 | + | let found: Outcome<Vec<WorkspaceAlert>> = call( | |
| 779 | + | services, | |
| 780 | + | "workspace_alerts", | |
| 781 | + | &WorkspaceAlertsArgs { viewer: viewer.clone(), workspace, alert_type: AlertType::CodeScanning }, | |
| 782 | + | ) | |
| 783 | + | .await?; | |
| 784 | + | match found { | |
| 785 | + | Outcome::Ok(found) => { | |
| 786 | + | let alerts: Vec<WorkspaceAlert> = | |
| 787 | + | found.into_iter().filter(|alert| alert.code.as_ref().is_some_and(|code| filters.keeps(code))).collect(); | |
| 788 | + | ok(&alerts) | |
| 789 | + | } | |
| 790 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 791 | + | } | |
| 792 | + | } | |
| 793 | + | (None, None) => failed(FailureCode::Invalid, "Give repo, or workspace for all of one."), | |
| 794 | + | } | |
| 795 | + | } | |
| 796 | + | SecurityOp::GetCodeAlert => { | |
| 797 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 798 | + | pass(services, "code_alert", &CodeAlertArgs { viewer: viewer.clone(), repo, number: whole(input, "number").unwrap_or(0) }).await | |
| 799 | + | } | |
| 800 | + | SecurityOp::UpdateCodeAlert => { | |
| 801 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 802 | + | let reason = match state_change(input, "dismissed_reason", &CODE_REASONS) { | |
| 803 | + | Ok(reason) => reason, | |
| 804 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 805 | + | }; | |
| 806 | + | let args = SetCodeAlertStateArgs { | |
| 807 | + | actor: actor(), | |
| 808 | + | repo, | |
| 809 | + | number: whole(input, "number").unwrap_or(0), | |
| 810 | + | state: if reason.is_some() { AlertState::Dismissed } else { AlertState::Open }, | |
| 811 | + | reason, | |
| 812 | + | comment: text(input, "dismissed_comment").unwrap_or_default(), | |
| 813 | + | }; | |
| 814 | + | pass(services, "set_code_alert_state", &args).await | |
| 815 | + | } | |
| 816 | + | SecurityOp::ListAnalyses => { | |
| 817 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 818 | + | match call::<_, CodeScanning>(services, "code_scanning", &CodeScanningArgs { viewer: viewer.clone(), repo }).await? { | |
| 819 | + | Outcome::Ok(scanning) => ok(&scanning.analyses), | |
| 820 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 821 | + | } | |
| 822 | + | } | |
| 823 | + | SecurityOp::UploadSarif => { | |
| 824 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 825 | + | let (Some(commit_sha), Some(git_ref), Some(sarif)) = (text(input, "commit_sha"), text(input, "ref"), text(input, "sarif")) else { | |
| 826 | + | return failed(FailureCode::Invalid, "Give commit_sha, ref and sarif (the file gzipped and base64-encoded)."); | |
| 827 | + | }; | |
| 828 | + | if sarif.len() > g1t_scan_limits::MAX_UPLOAD_BYTES { | |
| 829 | + | return failed(FailureCode::Invalid, "The upload is larger than 10 MB."); | |
| 830 | + | } | |
| 831 | + | let args = UploadSarifArgs { | |
| 832 | + | actor: actor(), | |
| 833 | + | repo, | |
| 834 | + | commit_sha, | |
| 835 | + | git_ref, | |
| 836 | + | sarif, | |
| 837 | + | tool_name: text(input, "tool_name"), | |
| 838 | + | category: text(input, "category"), | |
| 839 | + | checkout_uri: text(input, "checkout_uri"), | |
| 840 | + | }; | |
| 841 | + | pass(services, "upload_sarif", &args).await | |
| 842 | + | } | |
| 843 | + | SecurityOp::GetSarifUpload => { | |
| 844 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 845 | + | pass(services, "sarif_status", &SarifStatusArgs { viewer: viewer.clone(), repo, id: text(input, "id").unwrap_or_default() }).await | |
| 846 | + | } | |
| 847 | + | SecurityOp::ListVulnerabilityAlerts => { | |
| 848 | + | let filters = match vulnerability_filters(input) { | |
| 849 | + | Ok(filters) => filters, | |
| 850 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 851 | + | }; | |
| 852 | + | match (scope_repo, workspace) { | |
| 853 | + | (Some(repo), _) => { | |
| 854 | + | let overview: Outcome<SecurityOverview> = | |
| 855 | + | call(services, "overview", &g1t_contracts::security::OverviewArgs { repo, viewer: viewer.clone() }).await?; | |
| 856 | + | match overview { | |
| 857 | + | Outcome::Ok(overview) => { | |
| 858 | + | let alerts: Vec<Vulnerability> = overview.vulnerabilities.into_iter().filter(|vuln| filters.keeps(vuln)).collect(); | |
| 859 | + | ok(&alerts) | |
| 860 | + | } | |
| 861 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 862 | + | } | |
| 863 | + | } | |
| 864 | + | (None, Some(workspace)) => { | |
| 865 | + | let found: Outcome<Vec<WorkspaceAlert>> = call( | |
| 866 | + | services, | |
| 867 | + | "workspace_alerts", | |
| 868 | + | &WorkspaceAlertsArgs { viewer: viewer.clone(), workspace, alert_type: AlertType::Vulnerability }, | |
| 869 | + | ) | |
| 870 | + | .await?; | |
| 871 | + | match found { | |
| 872 | + | Outcome::Ok(found) => { | |
| 873 | + | let alerts: Vec<WorkspaceAlert> = | |
| 874 | + | found.into_iter().filter(|alert| alert.vulnerability.as_ref().is_some_and(|vuln| filters.keeps(vuln))).collect(); | |
| 875 | + | ok(&alerts) | |
| 876 | + | } | |
| 877 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 878 | + | } | |
| 879 | + | } | |
| 880 | + | (None, None) => failed(FailureCode::Invalid, "Give repo, or workspace for all of one."), | |
| 881 | + | } | |
| 882 | + | } | |
| 883 | + | SecurityOp::GetVulnerabilityAlert => { | |
| 884 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 885 | + | let id = text(input, "id").unwrap_or_default(); | |
| 886 | + | let overview: Outcome<SecurityOverview> = | |
| 887 | + | call(services, "overview", &g1t_contracts::security::OverviewArgs { repo, viewer: viewer.clone() }).await?; | |
| 888 | + | match overview { | |
| 889 | + | Outcome::Ok(overview) => match overview.vulnerabilities.into_iter().find(|vuln| vuln.id == id) { | |
| 890 | + | Some(vuln) => ok(&vuln), | |
| 891 | + | None => failed(FailureCode::NotFound, "No such alert."), | |
| 892 | + | }, | |
| 893 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 894 | + | } | |
| 895 | + | } | |
| 896 | + | SecurityOp::FixAlert => { | |
| 897 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 898 | + | pass(services, "fix_alert", &FixAlertArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default() }).await | |
| 899 | + | } | |
| 900 | + | SecurityOp::GetDependencyGraph => { | |
| 901 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 902 | + | pass(services, "dependency_graph", &DependencyGraphArgs { viewer: viewer.clone(), repo }).await | |
| 903 | + | } | |
| 904 | + | SecurityOp::GetSbom => { | |
| 905 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 906 | + | // The document goes out as SPDX spells it: `sbom` is passed | |
| 907 | + | // through untouched (g1t_kit::wire::USER_KEYED). | |
| 908 | + | match call::<_, Value>(services, "sbom", &SbomArgs { viewer: viewer.clone(), repo }).await? { | |
| 909 | + | Outcome::Ok(document) => ok(&json!({ "sbom": document })), | |
| 910 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 911 | + | } | |
| 912 | + | } | |
| 913 | + | SecurityOp::CompareDependencies => { | |
| 914 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 915 | + | let Some((base, head)) = text(input, "basehead").as_deref().and_then(base_head) else { | |
| 916 | + | return failed(FailureCode::Invalid, "basehead is base...head, e.g. main...my-branch."); | |
| 917 | + | }; | |
| 918 | + | pass(services, "dependency_review", &DependencyReviewArgs { viewer: viewer.clone(), repo, base, head }).await | |
| 919 | + | } | |
| 920 | + | SecurityOp::GetSettings => { | |
| 921 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 922 | + | pass(services, "security_settings", &SecuritySettingsArgs { viewer: viewer.clone(), repo }).await | |
| 923 | + | } | |
| 924 | + | SecurityOp::UpdateSettings => { | |
| 925 | + | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 926 | + | // What is not given stays as it is. | |
| 927 | + | let current: Outcome<SecuritySettingsView> = | |
| 928 | + | call(services, "security_settings", &SecuritySettingsArgs { viewer: viewer.clone(), repo: repo.clone() }).await?; | |
| 929 | + | let mut settings = match current { | |
| 930 | + | Outcome::Ok(view) => view.settings, | |
| 931 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 932 | + | }; | |
| 933 | + | if let Some(gate) = text(input, "code_scanning_gate") { | |
| 934 | + | settings.code_scanning_gate = gate.to_lowercase(); | |
| 935 | + | } | |
| 936 | + | if let Some(on) = flag(input, "dependency_review") { | |
| 937 | + | settings.dependency_review = on; | |
| 938 | + | } | |
| 939 | + | if let Some(fail_on) = text(input, "review_fail_on") { | |
| 940 | + | settings.review_fail_on = fail_on.to_lowercase(); | |
| 941 | + | } | |
| 942 | + | if input["review_deny_licenses"].is_array() { | |
| 943 | + | settings.review_deny_licenses = strings(input, "review_deny_licenses"); | |
| 944 | + | } | |
| 945 | + | if let Some(on) = flag(input, "review_comment") { | |
| 946 | + | settings.review_comment = on; | |
| 947 | + | } | |
| 948 | + | pass(services, "set_security_settings", &SetSecuritySettingsArgs { actor: actor(), repo, settings }).await | |
| 949 | + | } | |
| 950 | + | SecurityOp::GetWorkspaceSettings => { | |
| 951 | + | let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") }; | |
| 952 | + | pass(services, "workspace_security_settings", &WorkspaceSecuritySettingsArgs { viewer: viewer.clone(), workspace }).await | |
| 953 | + | } | |
| 954 | + | SecurityOp::UpdateWorkspaceSettings => { | |
| 955 | + | let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") }; | |
| 956 | + | let current: Outcome<WorkspaceSecurityView> = | |
| 957 | + | call(services, "workspace_security_settings", &WorkspaceSecuritySettingsArgs { viewer: viewer.clone(), workspace: workspace.clone() }).await?; | |
| 958 | + | let mut settings = match current { | |
| 959 | + | Outcome::Ok(view) => view.settings, | |
| 960 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 961 | + | }; | |
| 962 | + | if let Some(on) = flag(input, "delegated_bypass") { | |
| 963 | + | settings.delegated_bypass = on; | |
| 964 | + | } | |
| 965 | + | if let Some(on) = flag(input, "validity_checks") { | |
| 966 | + | settings.validity_checks = on; | |
| 967 | + | } | |
| 968 | + | pass(services, "set_workspace_security_settings", &SetWorkspaceSecuritySettingsArgs { actor: actor(), workspace, settings }).await | |
| 969 | + | } | |
| 970 | + | SecurityOp::GetOverview => { | |
| 971 | + | let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") }; | |
| 972 | + | pass(services, "security_overview", &WorkspaceOverviewArgs { viewer: viewer.clone(), workspace, days: whole(input, "days") }).await | |
| 973 | + | } | |
| 974 | + | } | |
| 975 | + | } | |
| 976 | + | ||
| 977 | + | /// Limits the API checks before passing an upload on. | |
| 978 | + | mod g1t_scan_limits { | |
| 979 | + | /// As the security service's: 10 MB gzipped and base64-encoded. | |
| 980 | + | pub const MAX_UPLOAD_BYTES: usize = 10 * 1024 * 1024; | |
| 981 | + | } | |
| 982 | + | ||
| 983 | + | #[cfg(test)] | |
| 984 | + | mod tests { | |
| 985 | + | use super::*; | |
| 986 | + | ||
| 987 | + | #[test] | |
| 988 | + | fn every_one_is_an_operation() { | |
| 989 | + | for op in SecurityOp::ALL { | |
| 990 | + | assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Security(op)), "{}", op.name()); | |
| 991 | + | } | |
| 992 | + | } | |
| 993 | + | ||
| 994 | + | #[test] | |
| 995 | + | fn names_are_unique_and_found_again() { | |
| 996 | + | let mut names: Vec<&str> = SecurityOp::ALL.iter().map(|op| op.name()).collect(); | |
| 997 | + | names.sort(); | |
| 998 | + | names.dedup(); | |
| 999 | + | assert_eq!(names.len(), SecurityOp::ALL.len()); | |
| 1000 | + | } | |
| 1001 | + | ||
| 1002 | + | #[test] | |
| 1003 | + | fn secret_filters_are_read_as_words() { | |
| 1004 | + | let filters = secret_filters(&json!({ "state": "OPEN", "validity": "active", "bypassed": "true", "secret_type": "github_token" })).unwrap(); | |
| 1005 | + | assert_eq!(filters.state, Some(AlertState::Open)); | |
| 1006 | + | assert_eq!(filters.validity.as_deref(), Some("active")); | |
| 1007 | + | assert_eq!(filters.bypassed, Some(true)); | |
| 1008 | + | assert!(secret_filters(&json!({ "validity": "maybe" })).unwrap_err().contains("validity is active, inactive")); | |
| 1009 | + | assert!(secret_filters(&json!({ "bypassed": "perhaps" })).is_err()); | |
| 1010 | + | assert_eq!(secret_filters(&json!({})).unwrap(), SecretFilters::default()); | |
| 1011 | + | } | |
| 1012 | + | ||
| 1013 | + | #[test] | |
| 1014 | + | fn a_state_change_needs_a_reason_its_kind_takes() { | |
| 1015 | + | assert_eq!(state_change(&json!({ "state": "open" }), "reason", &SECRET_REASONS), Ok(None)); | |
| 1016 | + | assert_eq!( | |
| 1017 | + | state_change(&json!({ "state": "dismissed", "reason": "revoked" }), "reason", &SECRET_REASONS), | |
| 1018 | + | Ok(Some(DismissReason::Revoked)) | |
| 1019 | + | ); | |
| 1020 | + | assert!(state_change(&json!({ "state": "dismissed", "reason": "not_used" }), "reason", &SECRET_REASONS).unwrap_err().contains("reason is false_positive")); | |
| 1021 | + | assert!(state_change(&json!({ "state": "dismissed" }), "dismissed_reason", &CODE_REASONS).unwrap_err().starts_with("Give dismissed_reason")); | |
| 1022 | + | assert!(state_change(&json!({ "state": "fixed" }), "reason", &CODE_REASONS).is_err()); | |
| 1023 | + | } | |
| 1024 | + | ||
| 1025 | + | #[test] | |
| 1026 | + | fn base_and_head_are_split_at_the_dots() { | |
| 1027 | + | assert_eq!(base_head("main...feature/x"), Some(("main".into(), "feature/x".into()))); | |
| 1028 | + | assert_eq!(base_head("v1.0..v1.1"), Some(("v1.0".into(), "v1.1".into()))); | |
| 1029 | + | assert_eq!(base_head("main"), None); | |
| 1030 | + | assert_eq!(base_head("...head"), None); | |
| 1031 | + | } | |
| 1032 | + | ||
| 1033 | + | #[test] | |
| 1034 | + | fn code_and_vulnerability_filters_check_their_words() { | |
| 1035 | + | assert!(code_filters(&json!({ "severity": "severe" })).unwrap_err().contains("severity is critical")); | |
| 1036 | + | let filters = vulnerability_filters(&json!({ "ecosystem": "npm", "state": "dismissed" })).unwrap(); | |
| 1037 | + | assert_eq!(filters.state, Some(AlertState::Dismissed)); | |
| 1038 | + | } | |
| 1039 | + | ||
| 1040 | + | #[test] | |
| 1041 | + | fn a_read_only_token_sees_only_the_security_reads() { | |
| 1042 | + | use g1t_contracts::scopes::{Scope, scope_for}; | |
| 1043 | + | for op in SecurityOp::ALL { | |
| 1044 | + | let scope = scope_for(op.name()).unwrap_or_else(|| panic!("{} has no scope", op.name())); | |
| 1045 | + | assert!(matches!(scope, Scope::SecurityRead | Scope::SecurityWrite), "{}", op.name()); | |
| 1046 | + | } | |
| 1047 | + | assert_eq!(scope_for("get_sbom"), Some(Scope::SecurityRead)); | |
| 1048 | + | assert_eq!(scope_for("upload_sarif"), Some(Scope::SecurityWrite)); | |
| 1049 | + | // Fixing an alert also opens an issue and spends agent time. | |
| 1050 | + | let needed = g1t_contracts::scopes::needed("fix_security_alert", &json!({})); | |
| 1051 | + | assert_eq!(needed, [Scope::SecurityWrite, Scope::IssuesWrite, Scope::AgentsRun]); | |
| 1052 | + | // No agent decides about security. | |
| 1053 | + | for name in ["bypass_push_protection", "review_bypass_request", "update_security_settings", "fix_security_alert"] { | |
| 1054 | + | assert!(g1t_contracts::credentials::NEVER.contains(&name), "{name}"); | |
| 1055 | + | } | |
| 1056 | + | } | |
| 1057 | + | ||
| 1058 | + | #[test] | |
| 1059 | + | fn workspace_wide_operations_do_not_need_a_repository() { | |
| 1060 | + | for op in [SecurityOp::GetOverview, SecurityOp::ListBypassRequests, SecurityOp::ListCustomPatterns] { | |
| 1061 | + | assert!(!op.needs_repo()); | |
| 1062 | + | } | |
| 1063 | + | assert!(SecurityOp::UploadSarif.needs_repo()); | |
| 1064 | + | let required = SecurityOp::UploadSarif.input()["required"].clone(); | |
| 1065 | + | assert_eq!(required, json!(["repo", "commit_sha", "ref", "sarif"])); | |
| 1066 | + | } | |
| 1067 | + | } |
| 19 | 19 | use serde_json::{Map, Value, json}; | |
| 20 | 20 | ||
| 21 | 21 | use crate::operations::Op; | |
| 22 | + | use crate::security::SecurityOp; | |
| 22 | 23 | ||
| 23 | 24 | pub struct Action { | |
| 24 | 25 | pub name: &'static str, | |
| ⋯ | |||
| 57 | 58 | Tool { | |
| 58 | 59 | name: "repository", | |
| 59 | 60 | title: "Repositories", | |
| 60 | − | description: "Repositories: find, read and create them, change their settings, and see and dismiss their security alerts (secrets and vulnerable dependencies). Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.", | |
| 61 | + | description: "Repositories: find, read and create them, change their settings, check their CODEOWNERS file, manage their labels and milestones, and see and dismiss their security alerts (secrets and vulnerable dependencies). Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.", | |
| 61 | 62 | default_action: None, | |
| 62 | 63 | actions: &[ | |
| 63 | 64 | a("list", Op::ListRepos, "Repositories you can see"), | |
| ⋯ | |||
| 67 | 68 | a("get_settings", Op::GetRepoSettings, "Branch protection: required checks, approvals, how pull requests merge"), | |
| 68 | 69 | a("update_settings", Op::UpdateRepoSettings, "Change branch protection and how pull requests merge"), | |
| 69 | 70 | a("check_names", Op::ListCheckNames, "Check names reported lately, to require on the default branch"), | |
| 70 | − | a("list_labels", Op::ListLabels, "Labels in use"), | |
| 71 | + | a("codeowners", Op::GetCodeownersErrors, "Problems in its CODEOWNERS file, by line"), | |
| 72 | + | a("list_labels", Op::ListLabels, "Labels, with colors and how many issues and pull requests carry each"), | |
| 73 | + | a("create_label", Op::CreateLabel, "Create a label"), | |
| 74 | + | a("update_label", Op::UpdateLabel, "Rename a label or change its color or description"), | |
| 75 | + | a("delete_label", Op::DeleteLabel, "Delete a label, from everything that carries it"), | |
| 76 | + | a("add_default_labels", Op::AddDefaultLabels, "Add the default labels it is missing"), | |
| 77 | + | a("list_milestones", Op::ListMilestones, "Milestones, with progress and due dates"), | |
| 78 | + | a("get_milestone", Op::GetMilestone, "One milestone with its issues and pull requests"), | |
| 79 | + | a("create_milestone", Op::CreateMilestone, "Create a milestone"), | |
| 80 | + | a("update_milestone", Op::UpdateMilestone, "Change a milestone's title, description, due date or state"), | |
| 81 | + | a("delete_milestone", Op::DeleteMilestone, "Delete a milestone"), | |
| 71 | 82 | a("list_events", Op::ListEvents, "Timeline: pushes, issues, pull requests, comments"), | |
| 72 | 83 | a("rename_branch", Op::RenameBranch, "Rename a branch"), | |
| 73 | 84 | a("rename", Op::RenameRepo, "Rename it; old addresses redirect"), | |
| ⋯ | |||
| 93 | 104 | a("list", Op::ListIssues, "Issues on a repository, newest first"), | |
| 94 | 105 | a("get", Op::GetIssue, "One issue with comments and its pull requests"), | |
| 95 | 106 | a("create", Op::CreateIssue, "Open an issue"), | |
| 96 | − | a("update", Op::UpdateIssue, "Change title, body, labels or assignees"), | |
| 107 | + | a("update", Op::UpdateIssue, "Change title, body, labels, milestone or assignees"), | |
| 108 | + | a("labels", Op::ListIssueLabels, "The labels an issue or pull request carries"), | |
| 109 | + | a("add_labels", Op::AddIssueLabels, "Add labels to an issue or pull request"), | |
| 110 | + | a("set_labels", Op::SetIssueLabels, "Replace the labels of an issue or pull request"), | |
| 111 | + | a("remove_labels", Op::RemoveIssueLabels, "Take labels off an issue or pull request"), | |
| 97 | 112 | a("close", Op::CloseIssue, "Close it without a pull request"), | |
| 98 | 113 | a("reopen", Op::ReopenIssue, "Reopen it"), | |
| 99 | 114 | a("comment", Op::AddComment, "Comment on an issue or pull request; path and line for one line of a change"), | |
| ⋯ | |||
| 103 | 118 | Tool { | |
| 104 | 119 | name: "pull_request", | |
| 105 | 120 | title: "Pull requests", | |
| 106 | − | description: "Pull requests: start a change for an issue, record your session, mark it ready, review and merge. Read `overlaps` and `behind` on `get` before going far.", | |
| 121 | + | description: "Pull requests: start a change for an issue, record your session, mark it ready, ask people and teams to review, review and merge. Read `overlaps` and `behind` on `get` before going far, and `code_owners` for whose approval it needs.", | |
| 107 | 122 | default_action: None, | |
| 108 | 123 | actions: &[ | |
| 109 | 124 | a("list", Op::ListPullRequests, "Pull requests on a repository, newest first"), | |
| 110 | 125 | a("get", Op::GetPullRequest, "Status, checks and required checks, reviews, overlaps, whether it is behind"), | |
| 111 | 126 | a("changes", Op::GetPullRequestChanges, "Files and line-by-line diff"), | |
| 112 | 127 | a("create", Op::CreatePullRequest, "Start a draft with its own fork to push to, or open one from a pushed branch"), | |
| 128 | + | a("update", Op::UpdatePullRequest, "Change its base branch, labels, milestone, assignees or reviewers"), | |
| 113 | 129 | a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"), | |
| 114 | 130 | a("read_session", Op::ReadSession, "Its recorded session"), | |
| 115 | 131 | a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"), | |
| 132 | + | a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"), | |
| 133 | + | a("remove_requested_reviewers", Op::RemoveRequestedReviewers, "Stop asking people or teams to review it"), | |
| 116 | 134 | a("review", Op::ReviewPullRequest, "Approve or request changes"), | |
| 117 | 135 | a("close", Op::ClosePullRequest, "Close without merging"), | |
| 118 | 136 | a("merge", Op::MergePullRequest, "Land it, or join the merge queue"), | |
| ⋯ | |||
| 225 | 243 | ], | |
| 226 | 244 | }, | |
| 227 | 245 | Tool { | |
| 246 | + | name: "team", | |
| 247 | + | title: "Teams", | |
| 248 | + | description: "Teams: groups of a workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. Name one by `workspace` and its slug (`team`). Any member may create a team; the workspace's owners and the team's maintainers manage it. A secret team is seen only by its people and the owners.", | |
| 249 | + | default_action: None, | |
| 250 | + | actions: &[ | |
| 251 | + | a("list", Op::ListTeams, "A workspace's teams you can see"), | |
| 252 | + | a("get", Op::GetTeam, "One team"), | |
| 253 | + | a("create", Op::CreateTeam, "Create a team; you become its maintainer"), | |
| 254 | + | a("update", Op::UpdateTeam, "Change its name, slug, description, visibility, parent or notifications"), | |
| 255 | + | a("delete", Op::DeleteTeam, "Delete it; its child teams move up"), | |
| 256 | + | a("list_members", Op::ListTeamMembers, "Its people and their roles, child teams' with include_child_teams"), | |
| 257 | + | a("set_member", Op::SetTeamMember, "Add a member of the workspace, or change their role"), | |
| 258 | + | a("remove_member", Op::RemoveTeamMember, "Take someone out of it"), | |
| 259 | + | a("list_child_teams", Op::ListChildTeams, "The teams nested under it"), | |
| 260 | + | a("list_repos", Op::ListTeamRepos, "The repositories it has a role on"), | |
| 261 | + | a("set_repo", Op::SetTeamRepo, "Give it a role on a repository"), | |
| 262 | + | a("remove_repo", Op::RemoveTeamRepo, "Take its role on a repository away"), | |
| 263 | + | a("set_review_assignment", Op::SetTeamReviewAssignment, "Whom it picks when asked to review"), | |
| 264 | + | a("list_user_teams", Op::ListUserTeams, "The teams someone is in"), | |
| 265 | + | ], | |
| 266 | + | }, | |
| 267 | + | Tool { | |
| 228 | 268 | name: "workspace", | |
| 229 | 269 | title: "Workspaces", | |
| 230 | 270 | description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, and keep your own pinned projects at the top of its sidebar.", | |
| ⋯ | |||
| 249 | 289 | ], | |
| 250 | 290 | }, | |
| 251 | 291 | Tool { | |
| 292 | + | name: "security", | |
| 293 | + | title: "Security", | |
| 294 | + | description: "A repository's security: secret scanning alerts and push protection bypasses, custom secret patterns, code scanning alerts and SARIF uploads, vulnerability alerts, the dependency graph and its SBOM, dependency review, settings, and a workspace's overview. Fix an alert with g1t. Findings are shown to those who can change the code only. Give `repo` (owner/name), or `workspace` for lists across one.", | |
| 295 | + | default_action: Some("secret_alerts"), | |
| 296 | + | actions: &[ | |
| 297 | + | a("secret_alerts", Op::Security(SecurityOp::ListSecretAlerts), "Secret scanning alerts; by state, secret_type, validity, bypassed"), | |
| 298 | + | a("secret_alert", Op::Security(SecurityOp::GetSecretAlert), "One secret alert, with where it was found and its bypass requests"), | |
| 299 | + | a("update_secret_alert", Op::Security(SecurityOp::UpdateSecretAlert), "Dismiss a secret alert with a reason, or reopen it"), | |
| 300 | + | a("secret_locations", Op::Security(SecurityOp::ListSecretLocations), "Every file, line and commit a secret is in"), | |
| 301 | + | a("bypass", Op::Security(SecurityOp::BypassPushProtection), "Push past push protection with a reason, or ask to"), | |
| 302 | + | a("check_validity", Op::Security(SecurityOp::CheckSecretValidity), "Ask a secret's issuer whether it still works"), | |
| 303 | + | a("bypass_requests", Op::Security(SecurityOp::ListBypassRequests), "A workspace's push protection bypass requests"), | |
| 304 | + | a("review_bypass", Op::Security(SecurityOp::ReviewBypassRequest), "Approve, deny or cancel a bypass request"), | |
| 305 | + | a("patterns", Op::Security(SecurityOp::ListCustomPatterns), "Custom secret patterns of a repository or workspace"), | |
| 306 | + | a("create_pattern", Op::Security(SecurityOp::CreateCustomPattern), "Create a custom secret pattern, as a draft or published"), | |
| 307 | + | a("update_pattern", Op::Security(SecurityOp::UpdateCustomPattern), "Change, publish or unpublish a custom pattern"), | |
| 308 | + | a("delete_pattern", Op::Security(SecurityOp::DeleteCustomPattern), "Delete a custom pattern"), | |
| 309 | + | a("dry_run_pattern", Op::Security(SecurityOp::DryRunCustomPattern), "Run a pattern over the default branch without saving it"), | |
| 310 | + | a("code_alerts", Op::Security(SecurityOp::ListCodeAlerts), "Code scanning alerts; by state, severity, tool, rule_id"), | |
| 311 | + | a("code_alert", Op::Security(SecurityOp::GetCodeAlert), "One code scanning alert by number"), | |
| 312 | + | a("update_code_alert", Op::Security(SecurityOp::UpdateCodeAlert), "Dismiss a code scanning alert with a reason, or reopen it"), | |
| 313 | + | a("analyses", Op::Security(SecurityOp::ListAnalyses), "Code scanning analyses, newest first"), | |
| 314 | + | a("upload_sarif", Op::Security(SecurityOp::UploadSarif), "Upload a SARIF file, gzipped and base64-encoded"), | |
| 315 | + | a("sarif_upload", Op::Security(SecurityOp::GetSarifUpload), "Whether a SARIF upload was read, and its analyses"), | |
| 316 | + | a("vulnerability_alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), "Vulnerable dependencies; by state, severity, ecosystem, package"), | |
| 317 | + | a("vulnerability_alert", Op::Security(SecurityOp::GetVulnerabilityAlert), "One vulnerability alert"), | |
| 318 | + | a("update_vulnerability_alert", Op::Security(SecurityOp::UpdateVulnerabilityAlert), "Dismiss a vulnerability alert with a reason, or reopen it"), | |
| 319 | + | a("fix", Op::Security(SecurityOp::FixAlert), "Put g1t on an issue to fix an alert"), | |
| 320 | + | a("dependency_graph", Op::Security(SecurityOp::GetDependencyGraph), "Every package the lockfiles resolve, direct or transitive"), | |
| 321 | + | a("sbom", Op::Security(SecurityOp::GetSbom), "The dependency graph as an SPDX 2.3 document"), | |
| 322 | + | a("compare_dependencies", Op::Security(SecurityOp::CompareDependencies), "What changes in dependencies between base...head"), | |
| 323 | + | a("settings", Op::Security(SecurityOp::GetSettings), "A repository's security settings"), | |
| 324 | + | a("update_settings", Op::Security(SecurityOp::UpdateSettings), "Change when checks fail and dependency review's policy"), | |
| 325 | + | a("workspace_settings", Op::Security(SecurityOp::GetWorkspaceSettings), "A workspace's delegated bypass and validity checks"), | |
| 326 | + | a("update_workspace_settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), "Turn delegated bypass or validity checks on or off"), | |
| 327 | + | a("overview", Op::Security(SecurityOp::GetOverview), "A workspace's alerts, trends and coverage"), | |
| 328 | + | ], | |
| 329 | + | }, | |
| 330 | + | Tool { | |
| 252 | 331 | name: "notifications", | |
| 253 | 332 | title: "Notifications", | |
| 254 | 333 | description: "Your inbox: what needs you, and what you follow. One thread per issue, pull request, workflow or deployment, with why you were told (`reason`): an agent waiting on you, a review asked of you, an assignment, a mention, your work's checks, or what you subscribe to and watch. Mark threads read or done once handled, and choose what you hear of with subscribe, unsubscribe and watch. Your own: a personal token.", | |
| ⋯ | |||
| 296 | 375 | fn destructive(op: Op) -> bool { | |
| 297 | 376 | matches!( | |
| 298 | 377 | op, | |
| 299 | − | Op::DeleteWorkspace | |
| 378 | + | Op::Security(SecurityOp::DeleteCustomPattern | SecurityOp::BypassPushProtection) | |
| 379 | + | | Op::DeleteWorkspace | |
| 300 | 380 | | Op::UpdateWorkspace | |
| 301 | 381 | | Op::DeleteRepo | |
| 302 | 382 | | Op::PurgeRepo | |
| ⋯ | |||
| 312 | 392 | | Op::SetActionsVariable | |
| 313 | 393 | | Op::SetModelRoutes | |
| 314 | 394 | | Op::SetBasePermission | |
| 395 | + | | Op::DeleteTeam | |
| 396 | + | | Op::RemoveTeamRepo | |
| 315 | 397 | | Op::MergePullRequest | |
| 316 | 398 | | Op::RemoveRunner | |
| 317 | 399 | | Op::DeleteRunnerGroup | |
| ⋯ | |||
| 629 | 711 | assert_eq!(tool["annotations"]["destructiveHint"], false); | |
| 630 | 712 | } | |
| 631 | 713 | let issue = tools.iter().find(|tool| tool["name"] == "issue").unwrap(); | |
| 632 | − | assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get"])); | |
| 714 | + | assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get", "labels"])); | |
| 633 | 715 | // Nothing of the agent tool is a read. | |
| 634 | 716 | assert!(!tools.iter().any(|tool| tool["name"] == "agent")); | |
| 635 | 717 | } | |
| ⋯ | |||
| 638 | 720 | fn a_narrow_token_sees_only_its_tools() { | |
| 639 | 721 | let access = token(Some(vec![Scope::IssuesWrite])); | |
| 640 | 722 | let names: Vec<Value> = listed(&Gate::Token(&access)).into_iter().map(|tool| tool["name"].clone()).collect(); | |
| 641 | − | assert_eq!(names, vec![json!("issue"), json!("plan"), json!("account")]); | |
| 723 | + | // Labels and milestones are the repository's, managed with issues:write. | |
| 724 | + | assert_eq!(names, vec![json!("repository"), json!("issue"), json!("plan"), json!("account")]); | |
| 642 | 725 | // Notifications are a resource of their own: reading them lists | |
| 643 | 726 | // only what reads. | |
| 644 | 727 | let reader = token(Some(vec![Scope::NotificationsRead])); | |
| ⋯ | |||
| 677 | 760 | assert_eq!(resolve(account, &json!({})), Ok(Op::Whoami)); | |
| 678 | 761 | } | |
| 679 | 762 | ||
| 763 | + | #[test] | |
| 764 | + | fn teams_are_one_tool_and_a_workspace_reader_sees_only_its_reads() { | |
| 765 | + | let team = Tool::by_name("team").unwrap(); | |
| 766 | + | let names: Vec<&str> = team.actions.iter().map(|action| action.name).collect(); | |
| 767 | + | assert_eq!( | |
| 768 | + | names, | |
| 769 | + | [ | |
| 770 | + | "list", | |
| 771 | + | "get", | |
| 772 | + | "create", | |
| 773 | + | "update", | |
| 774 | + | "delete", | |
| 775 | + | "list_members", | |
| 776 | + | "set_member", | |
| 777 | + | "remove_member", | |
| 778 | + | "list_child_teams", | |
| 779 | + | "list_repos", | |
| 780 | + | "set_repo", | |
| 781 | + | "remove_repo", | |
| 782 | + | "set_review_assignment", | |
| 783 | + | "list_user_teams", | |
| 784 | + | ] | |
| 785 | + | ); | |
| 786 | + | let reader = token(Some(vec![Scope::WorkspaceRead])); | |
| 787 | + | let tools = listed(&Gate::Token(&reader)); | |
| 788 | + | let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap(); | |
| 789 | + | assert_eq!( | |
| 790 | + | listed_team["inputSchema"]["properties"]["action"]["enum"], | |
| 791 | + | json!(["list", "get", "list_members", "list_child_teams", "list_repos", "list_user_teams"]) | |
| 792 | + | ); | |
| 793 | + | assert_eq!(listed_team["annotations"]["readOnlyHint"], true); | |
| 794 | + | // A team's role on a repository is who has access. | |
| 795 | + | let admin = token(Some(vec![Scope::WorkspaceAdmin])); | |
| 796 | + | let tools = listed(&Gate::Token(&admin)); | |
| 797 | + | let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap(); | |
| 798 | + | let actions = listed_team["inputSchema"]["properties"]["action"]["enum"].as_array().unwrap(); | |
| 799 | + | assert!(actions.contains(&json!("set_review_assignment")) && !actions.contains(&json!("set_repo"))); | |
| 800 | + | let access = token(Some(vec![Scope::AccessAdmin])); | |
| 801 | + | let tools = listed(&Gate::Token(&access)); | |
| 802 | + | let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap(); | |
| 803 | + | assert_eq!(listed_team["inputSchema"]["properties"]["action"]["enum"], json!(["set_repo", "remove_repo"])); | |
| 804 | + | // Both kinds of role a schema names are offered. | |
| 805 | + | let roles = &listed(&Gate::Everything).into_iter().find(|tool| tool["name"] == "team").unwrap()["inputSchema"] | |
| 806 | + | ["properties"]["role"]["enum"]; | |
| 807 | + | for role in ["member", "maintainer", "read", "admin"] { | |
| 808 | + | assert!(roles.as_array().unwrap().contains(&json!(role)), "{role}"); | |
| 809 | + | } | |
| 810 | + | assert_eq!( | |
| 811 | + | resolve(team, &json!({ "action": "set_repo", "workspace": "acme", "team": "backend", "repo": "rocket" })), | |
| 812 | + | Err("team.set_repo needs role.".to_owned()) | |
| 813 | + | ); | |
| 814 | + | } | |
| 815 | + | ||
| 816 | + | #[test] | |
| 817 | + | fn reviewers_and_code_owners_are_actions_of_their_tools() { | |
| 818 | + | let pull = Tool::by_name("pull_request").unwrap(); | |
| 819 | + | assert_eq!( | |
| 820 | + | resolve(pull, &json!({ "action": "request_reviewers", "repo": "a/b", "number": 1, "team_reviewers": ["backend"] })), | |
| 821 | + | Ok(Op::RequestReviewers) | |
| 822 | + | ); | |
| 823 | + | assert_eq!(pull.action("remove_requested_reviewers").map(|action| action.op), Some(Op::RemoveRequestedReviewers)); | |
| 824 | + | let repository = Tool::by_name("repository").unwrap(); | |
| 825 | + | assert_eq!(resolve(repository, &json!({ "action": "codeowners", "repo": "a/b" })), Ok(Op::GetCodeownersErrors)); | |
| 826 | + | assert!(reads_only(Op::GetCodeownersErrors)); | |
| 827 | + | assert!(!reads_only(Op::RequestReviewers)); | |
| 828 | + | } | |
| 829 | + | ||
| 680 | 830 | /// How much smaller `tools/list` is than one tool per operation. Run | |
| 681 | 831 | /// with `--nocapture` to see the numbers. | |
| 682 | 832 | #[test] | |
| 84 | 84 | { label: 'RubyGems', slug: 'guides/rubygems' }, | |
| 85 | 85 | { label: 'Go modules', slug: 'guides/go' }, | |
| 86 | 86 | { label: 'Secrets and variables', slug: 'guides/secrets-and-variables' }, | |
| 87 | + | ], | |
| 88 | + | }, | |
| 89 | + | { | |
| 90 | + | label: 'Security', | |
| 91 | + | items: [ | |
| 87 | 92 | { label: 'Security', slug: 'guides/security' }, | |
| 93 | + | { label: 'Dependency updates', slug: 'guides/dependency-updates' }, | |
| 94 | + | { label: 'Secret protection', slug: 'guides/security/secret-protection' }, | |
| 95 | + | { label: 'Code scanning', slug: 'guides/security/code-scanning' }, | |
| 96 | + | { label: 'Supply chain', slug: 'guides/security/supply-chain' }, | |
| 97 | + | { label: 'Security overview', slug: 'guides/security/security-overview' }, | |
| 98 | + | { label: "What's free and what's paid", slug: 'guides/security/pricing' }, | |
| 88 | 99 | ], | |
| 89 | 100 | }, | |
| 90 | 101 | { | |
| ⋯ | |||
| 113 | 124 | label: 'Landing changes', | |
| 114 | 125 | items: [ | |
| 115 | 126 | { label: 'Pull requests and checks', slug: 'guides/pull-requests' }, | |
| 127 | + | { label: 'Pull requests into other branches', slug: 'guides/base-branches' }, | |
| 128 | + | { label: 'Labels', slug: 'guides/labels' }, | |
| 129 | + | { label: 'Milestones', slug: 'guides/milestones' }, | |
| 116 | 130 | { label: 'The merge queue', slug: 'guides/merge-queue' }, | |
| 131 | + | { label: 'CODEOWNERS', slug: 'guides/codeowners' }, | |
| 117 | 132 | { label: 'Sessions and why-blame', slug: 'guides/why-blame' }, | |
| 118 | 133 | { label: 'Forks and branches', slug: 'concepts/forks' }, | |
| 119 | 134 | ], | |
| ⋯ | |||
| 125 | 140 | { label: 'GitHub', slug: 'guides/github' }, | |
| 126 | 141 | { label: 'Workspaces and tokens', slug: 'guides/workspaces' }, | |
| 127 | 142 | { label: 'Access and roles', slug: 'guides/access-and-roles' }, | |
| 143 | + | { label: 'Teams', slug: 'guides/teams' }, | |
| 128 | 144 | { label: 'Managing a repository', slug: 'guides/managing-repositories' }, | |
| 129 | 145 | { label: 'Transferring a repository', slug: 'guides/transferring-repositories' }, | |
| 130 | 146 | { label: 'Audit log', slug: 'guides/audit-log' }, | |
| 157 | 157 | to a branch. | |
| 158 | 158 | - **Status.** Clear fork storage rules depend on Cloudflare. | |
| 159 | 159 | ||
| 160 | + | ### Some dependency update options are not applied yet | |
| 161 | + | ||
| 162 | + | g1t reads and checks every option of a `dependabot.yml` file, but does not | |
| 163 | + | act on all of them yet: | |
| 164 | + | ||
| 165 | + | - Version update pull requests are opened for npm, Cargo, Go and pip only. | |
| 166 | + | Entries for other ecosystems are checked and listed, and open nothing. | |
| 167 | + | ||
| 168 | + | - A multi-ecosystem group opens one pull request per ecosystem, not one | |
| 169 | + | for the group. | |
| 170 | + | - Registries that sign in with OIDC are not used. | |
| 171 | + | ||
| 172 | + | - **Instead.** Keep a separate entry per ecosystem and directory, and | |
| 173 | + | check the Security page, which lists what each entry reads but does not | |
| 174 | + | act on. See [Dependency updates](/guides/dependency-updates/#options). | |
| 175 | + | - **Status.** Planned. | |
| 176 | + | ||
| 160 | 177 | ### No conflict resolution in the browser | |
| 161 | 178 | ||
| 162 | 179 | You can't resolve a merge conflict on the pull request's page. | |
| ⋯ | |||
| 349 | 366 | ||
| 350 | 367 | ### Not built yet | |
| 351 | 368 | ||
| 352 | − | - **Milestones.** Planned. | |
| 369 | + | ||
| 353 | 370 | - **Releases and package registries.** Planned. | |
| 354 | 371 | - **Wikis.** Not scheduled. Keep docs in the repository. | |
| 1 | 1 | --- | |
| 2 | 2 | title: Access and roles | |
| 3 | − | description: The five repository roles and what each can do, the base permission members get, outside collaborators and invitations, and what agents may do on a person's behalf. | |
| 3 | + | description: The five repository roles and what each can do, the base permission members get, roles through teams, outside collaborators and invitations, and what agents may do on a person's behalf. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | Everyone who can work in a repository has a role on it. The role says what | |
| 7 | 7 | they can do there, from reading it to managing who else has access. A | |
| 8 | − | workspace gives its members a role on every one of its repositories, and a | |
| 8 | + | workspace gives its members a role on every one of its repositories, a | |
| 9 | 9 | repository can give anyone a role of their own: a member who needs more | |
| 10 | − | there, or someone outside the workspace. | |
| 10 | + | there, or someone outside the workspace, and it can give a | |
| 11 | + | [team](/guides/teams/) a role that everyone in the team has. | |
| 11 | 12 | ||
| 12 | 13 | ## The roles | |
| 13 | 14 | ||
| ⋯ | |||
| 57 | 58 | in it. | |
| 58 | 59 | 3. **A role given to you on that repository.** See | |
| 59 | 60 | [add someone to a repository](#add-someone-to-a-repository). | |
| 60 | − | 4. **Public.** Anyone, signed in or not, can read a public repository. | |
| 61 | + | 4. **Your teams.** The role each [team](/guides/teams/) you are in has on | |
| 62 | + | that repository, and the roles of that team's parent teams, which child | |
| 63 | + | teams inherit. See [repository access](/guides/teams/#repository-access). | |
| 64 | + | 5. **Public.** Anyone, signed in or not, can read a public repository. | |
| 61 | 65 | ||
| 62 | 66 | The highest wins. A member whose base permission is Read and who is given | |
| 63 | 67 | Maintain on one repository has Maintain there and Read everywhere else. A | |
| 64 | − | role lower than what you already have changes nothing. | |
| 68 | + | role lower than what you already have changes nothing. When a role given to | |
| 69 | + | you and a team's role are the same, the one given to you is shown as where | |
| 70 | + | it comes from. | |
| 65 | 71 | ||
| 66 | 72 | A workspace's own [access token](/guides/workspaces/#workspace-access-tokens) | |
| 67 | 73 | has Admin on its workspace's repositories, and none on any other. | |
| ⋯ | |||
| 136 | 142 | 3. Pick their role and choose **Add**. | |
| 137 | 143 | ||
| 138 | 144 | Everyone with access is listed under **People with access**, with their | |
| 139 | − | role and where it comes from. People with Write or Maintain can see the | |
| 140 | − | list; changing it needs Admin. | |
| 145 | + | role and where it comes from: owner, the base permission, a role given to | |
| 146 | + | them, or **Through team** and the team's slug. **Teams with access** lists the | |
| 147 | + | teams given a role on it, with how many people each has. People with Write | |
| 148 | + | or Maintain can see the lists; changing them needs Admin. | |
| 149 | + | ||
| 150 | + | Someone with Admin can give a team a role under **Teams with access**: | |
| 151 | + | pick the team and its role, and add it. Only the workspace's own teams can | |
| 152 | + | be added. See [teams](/guides/teams/#repository-access). | |
| 141 | 153 | ||
| 142 | 154 | What happens depends on who they are: | |
| 143 | 155 | ||
| ⋯ | |||
| 153 | 165 | ||
| 154 | 166 | To change someone's role, pick another beside their name. To take it away, | |
| 155 | 167 | choose **Remove**. Removing takes away only the role given on this | |
| 156 | − | repository: an owner's Admin and a member's base permission stay. Anyone | |
| 168 | + | repository: an owner's Admin, a member's base permission and what their | |
| 169 | + | teams give them stay. Anyone | |
| 157 | 170 | can remove their own role from a repository. Each change is confirmed | |
| 158 | 171 | under the list; one that is refused says why on that person's row. | |
| 159 | 172 | ||
| ⋯ | |||
| 184 | 197 | roles they have stay, and the base permission adds to them. | |
| 185 | 198 | ||
| 186 | 199 | Removing a member from a workspace also removes the roles they were given | |
| 187 | − | on its repositories. | |
| 200 | + | on its repositories, and takes them out of its teams. | |
| 188 | 201 | ||
| 189 | 202 | ## Invitations | |
| 190 | 203 | ||
| ⋯ | |||
| 258 | 271 | } | |
| 259 | 272 | ``` | |
| 260 | 273 | ||
| 261 | − | `source` is `owner`, `base` or `direct`. | |
| 274 | + | `source` is `owner`, `base`, `direct` or `team`. In the list from | |
| 275 | + | `list_collaborators`, each person also has `direct`, the role given to them | |
| 276 | + | on the repository if any, and `team_role` and `team`: the highest role a | |
| 277 | + | team gives them there, and that team's slug. A team's own roles are managed | |
| 278 | + | through the [teams API](/guides/teams/#through-the-api). | |
| 262 | 279 | ||
| 263 | 280 | ## Webhooks and the audit log | |
| 264 | 281 | ||
| ⋯ | |||
| 276 | 293 | The workspace's [audit log](/guides/audit-log/) records the same changes | |
| 277 | 294 | under those names, and also `repo.invitation_created`, | |
| 278 | 295 | `repo.invitation_revoked` and `workspace.base_permission_changed`. | |
| 296 | + | ||
| 297 | + | A team's role on a repository changing is sent as `team.repo_added`, | |
| 298 | + | `team.repo_role_changed` or `team.repo_removed`; see | |
| 299 | + | [teams](/guides/teams/#webhooks-and-the-audit-log). | |
| 184 | 184 | marks it ready, which on g1t is when it first has code. Each head runs | |
| 185 | 185 | each workflow once. | |
| 186 | 186 | ||
| 187 | + | They also start on the activity types `labeled`, `unlabeled`, | |
| 188 | + | `milestoned`, `demilestoned`, `assigned`, `review_requested` and | |
| 189 | + | `closed`, and `edited` when the branch a pull request merges into | |
| 190 | + | changes; `issues` workflows on `labeled`, `unlabeled`, `milestoned` and | |
| 191 | + | `demilestoned` too. List them under `types:` to run on them. For | |
| 192 | + | `labeled` and `unlabeled`, `github.event.label` names the label. A pull | |
| 193 | + | request's `branches` filter, `github.base_ref` and | |
| 194 | + | `pull_request.base.ref` are the branch it merges into, which is not | |
| 195 | + | always the default branch: see | |
| 196 | + | [pull requests into other branches](/guides/base-branches/). | |
| 197 | + | ||
| 187 | 198 | `github.event.pull_request` reads as it does on GitHub. For a pull request | |
| 188 | 199 | g1t made, `pull_request.user` is g1t (`login` `g1t`, `type` `Bot`), and | |
| 189 | 200 | `pull_request.requested_by` names the person who asked for it; it is `null` |
| 29 | 29 | | `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had it changed, or lost it. See [access and roles](/guides/access-and-roles/). | | |
| 30 | 30 | | `repo.invitation_created`, `repo.invitation_revoked` | Someone was invited to it, or an invitation was withdrawn. | | |
| 31 | 31 | | `workspace.base_permission_changed` | An owner changed what members get on every repository. | | |
| 32 | + | | `team.created`, `team.edited`, `team.deleted` | A [team](/guides/teams/) was created, changed or deleted. | | |
| 33 | + | | `team.member_added`, `team.member_role_changed`, `team.member_removed` | Someone was added to a team, made its maintainer or a member, or taken out of it. | | |
| 34 | + | | `team.repo_added`, `team.repo_role_changed`, `team.repo_removed` | A team was given a role on a repository, had it changed, or lost it. | | |
| 32 | 35 | | `workspace.residency_changed` | An owner changed where the workspace's new repositories are stored. See [data residency](/guides/workspaces/#data-residency). | | |
| 33 | 36 | | `workspace.deleted`, `workspace.restored`, `workspace.purged` | An owner deleted the workspace, g1t's support restored it, or it was removed for good. See [deleting a workspace](/guides/workspaces/#delete-a-workspace). | | |
| 34 | 37 |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.