Rulesets over REST and MCP
- REST, no version prefix, snake_case: /repos/{owner}/{name}/rulesets (list with include_parents, create, get, update, delete), /repos/{owner}/{name}/rules/branches/{branch} (every rule that holds for a branch or, with target=tag, a tag, and where each comes from), /repos/{owner}/{name}/rules/evaluations (how the rules judged pushes and merges, with 30-day insights), and the same for a workspace under /workspaces/{workspace}/rulesets and /rules/evaluations. - MCP: rulesets are actions of the repository tool (list_rulesets, get_ruleset, create_ruleset, update_ruleset, delete_ruleset, branch_rules, rule_evaluations) and of the workspace tool, so the tool count stays as it is. Deleting one is marked destructive. - Scopes: reading rulesets is repo:read / workspace:read; changing them changes what everyone, agents included, may do, so it is repo:admin / workspace:admin. - merge_pull_request takes bypass_rules; get_pull_request carries rules. - Under a repository's address `name` is the repository's, so the body names the ruleset ruleset_name; an exported ruleset's name is read too. - Examples in the reference, and the docs' OpenAPI document refreshed.
| 19 | 19 | #[cfg(test)] | |
| 20 | 20 | mod responses; | |
| 21 | 21 | mod rest; | |
| 22 | + | mod rules; | |
| 22 | 23 | mod runners; | |
| 23 | 24 | mod security; | |
| 24 | 25 | mod tools; |
| 8 | 8 | use serde_json::{Map, Value, json}; | |
| 9 | 9 | ||
| 10 | 10 | use crate::operations::Op; | |
| 11 | + | use crate::rules::RulesOp; | |
| 11 | 12 | use crate::security::SecurityOp; | |
| 12 | 13 | use crate::rest::{ROUTES, Route}; | |
| 13 | 14 | ||
| ⋯ | |||
| 198 | 199 | ], | |
| 199 | 200 | ), | |
| 200 | 201 | ( | |
| 202 | + | "Rules", | |
| 203 | + | "Rulesets: what may happen to a repository's branches and tags and what a pull request needs before it merges, for a repository or across a workspace; the rules that hold for one branch; and how they judged each push and merge, with insights.", | |
| 204 | + | &[ | |
| 205 | + | Op::Rules(RulesOp::ListRepoRulesets), | |
| 206 | + | Op::Rules(RulesOp::CreateRepoRuleset), | |
| 207 | + | Op::Rules(RulesOp::GetRepoRuleset), | |
| 208 | + | Op::Rules(RulesOp::UpdateRepoRuleset), | |
| 209 | + | Op::Rules(RulesOp::DeleteRepoRuleset), | |
| 210 | + | Op::Rules(RulesOp::GetBranchRules), | |
| 211 | + | Op::Rules(RulesOp::ListRuleEvaluations), | |
| 212 | + | Op::Rules(RulesOp::ListWorkspaceRulesets), | |
| 213 | + | Op::Rules(RulesOp::CreateWorkspaceRuleset), | |
| 214 | + | Op::Rules(RulesOp::GetWorkspaceRuleset), | |
| 215 | + | Op::Rules(RulesOp::UpdateWorkspaceRuleset), | |
| 216 | + | Op::Rules(RulesOp::DeleteWorkspaceRuleset), | |
| 217 | + | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 218 | + | ], | |
| 219 | + | ), | |
| 220 | + | ( | |
| 201 | 221 | "Issues", | |
| 202 | 222 | "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.", | |
| 203 | 223 | &[ | |
| ⋯ | |||
| 533 | 553 | Op::RemoveRequestedReviewers => "Remove requested reviewers", | |
| 534 | 554 | Op::GetCodeownersErrors => "List CODEOWNERS errors", | |
| 535 | 555 | Op::Security(op) => op.title(), | |
| 556 | + | Op::Rules(op) => op.title(), | |
| 536 | 557 | } | |
| 537 | 558 | } | |
| 538 | 559 | ||
| 25 | 25 | }; | |
| 26 | 26 | ||
| 27 | 27 | use crate::alerts::{AlertKind, SecurityAlert}; | |
| 28 | + | use crate::rules::RulesOp; | |
| 28 | 29 | use crate::security::SecurityOp; | |
| 29 | 30 | use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel}; | |
| 30 | 31 | use g1t_contracts::work::*; | |
| ⋯ | |||
| 261 | 262 | GetCodeownersErrors, | |
| 262 | 263 | /// The security suite's operations: see [`crate::security`]. | |
| 263 | 264 | Security(SecurityOp), | |
| 265 | + | /// Rulesets: rules.rs. | |
| 266 | + | Rules(RulesOp), | |
| 264 | 267 | } | |
| 265 | 268 | ||
| 266 | 269 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| ⋯ | |||
| 623 | 626 | } | |
| 624 | 627 | ||
| 625 | 628 | impl Op { | |
| 626 | − | pub const ALL: [Op; 204] = [ | |
| 629 | + | pub const ALL: [Op; 217] = [ | |
| 627 | 630 | Op::Whoami, | |
| 628 | 631 | Op::CreateWorkspace, | |
| 629 | 632 | Op::DeleteWorkspace, | |
| ⋯ | |||
| 828 | 831 | Op::Security(SecurityOp::GetWorkspaceSettings), | |
| 829 | 832 | Op::Security(SecurityOp::UpdateWorkspaceSettings), | |
| 830 | 833 | Op::Security(SecurityOp::GetOverview), | |
| 834 | + | Op::Rules(RulesOp::ListRepoRulesets), | |
| 835 | + | Op::Rules(RulesOp::GetRepoRuleset), | |
| 836 | + | Op::Rules(RulesOp::CreateRepoRuleset), | |
| 837 | + | Op::Rules(RulesOp::UpdateRepoRuleset), | |
| 838 | + | Op::Rules(RulesOp::DeleteRepoRuleset), | |
| 839 | + | Op::Rules(RulesOp::GetBranchRules), | |
| 840 | + | Op::Rules(RulesOp::ListRuleEvaluations), | |
| 841 | + | Op::Rules(RulesOp::ListWorkspaceRulesets), | |
| 842 | + | Op::Rules(RulesOp::GetWorkspaceRuleset), | |
| 843 | + | Op::Rules(RulesOp::CreateWorkspaceRuleset), | |
| 844 | + | Op::Rules(RulesOp::UpdateWorkspaceRuleset), | |
| 845 | + | Op::Rules(RulesOp::DeleteWorkspaceRuleset), | |
| 846 | + | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 831 | 847 | ]; | |
| 832 | 848 | ||
| 833 | 849 | pub fn by_name(name: &str) -> Option<Op> { | |
| ⋯ | |||
| 1011 | 1027 | Op::RemoveRequestedReviewers => "remove_requested_reviewers", | |
| 1012 | 1028 | Op::GetCodeownersErrors => "get_codeowners_errors", | |
| 1013 | 1029 | Op::Security(op) => op.name(), | |
| 1030 | + | Op::Rules(op) => op.name(), | |
| 1014 | 1031 | } | |
| 1015 | 1032 | } | |
| 1016 | 1033 | ||
| ⋯ | |||
| 1092 | 1109 | "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." | |
| 1093 | 1110 | } | |
| 1094 | 1111 | Op::GetRepoSettings => { | |
| 1095 | − | "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." | |
| 1112 | + | "How a repository handles pull requests: how g1t's agents are reviewed, revised and merged, and its default branch's protection as the rules of its rulesets stack there: 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 the merge queue. The same rules hold for a person's pull request and an agent's. list_repo_rulesets and get_branch_rules show every rule." | |
| 1096 | 1113 | } | |
| 1097 | 1114 | Op::UpdateRepoSettings => { | |
| 1098 | − | "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." | |
| 1115 | + | "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. The branch protection fields (required_checks, require_up_to_date, required_approvals, count_agent_approvals, allow_ignoring_checks, merge_queue, require_code_owner_review) are written to the repository's \"Default branch protection\" ruleset, made when it has none; rules only rulesets have stay as they are. 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." | |
| 1099 | 1116 | } | |
| 1100 | 1117 | Op::ListCheckNames => { | |
| 1101 | 1118 | "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)." | |
| ⋯ | |||
| 1211 | 1228 | "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." | |
| 1212 | 1229 | } | |
| 1213 | 1230 | Op::GetPullRequest => { | |
| 1214 | − | "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)." | |
| 1231 | + | "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 rules of the branch it merges into require, as success, failure, pending or expected when nothing has reported it yet), rules (each rule of that branch it does not meet yet, with the ruleset it comes from, what is wrong and how to meet it, in `unmet`; those you may bypass in `bypassable`; those of rulesets in evaluate that would refuse it in `evaluate`; and whether merging joins the merge queue), 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)." | |
| 1215 | 1232 | } | |
| 1216 | 1233 | Op::CreatePullRequest => { | |
| 1217 | 1234 | "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." | |
| ⋯ | |||
| 1231 | 1248 | "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." | |
| 1232 | 1249 | } | |
| 1233 | 1250 | Op::MergePullRequest => { | |
| 1234 | − | "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." | |
| 1251 | + | "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 it meets every rule that holds for its base (see rules and required_checks on get_pull_request: approvals, checks, deployments, merge windows and the rest, from the repository's and its workspace's rulesets); the refusal names the first rule not met. With ignore_checks, someone who may merge can bypass required checks where the rule allows it; with bypass_rules, someone a ruleset lists as a bypass actor merges past its rules, and it is recorded. 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." | |
| 1235 | 1252 | } | |
| 1236 | 1253 | Op::ListEvents => { | |
| 1237 | 1254 | "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first." | |
| ⋯ | |||
| 1502 | 1519 | "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." | |
| 1503 | 1520 | } | |
| 1504 | 1521 | Op::Security(op) => op.description(), | |
| 1522 | + | Op::Rules(op) => op.description(), | |
| 1505 | 1523 | } | |
| 1506 | 1524 | } | |
| 1507 | 1525 | ||
| ⋯ | |||
| 2196 | 2214 | }, | |
| 2197 | 2215 | "ignore_checks": { | |
| 2198 | 2216 | "type": "boolean", | |
| 2199 | − | "description": "Merge although required checks have not passed, where the repository allows bypassing them (allow_ignoring_checks).", | |
| 2217 | + | "description": "Merge although required checks have not passed, where the rule requiring them allows it (allow_bypass_on_merge).", | |
| 2218 | + | }, | |
| 2219 | + | "bypass_rules": { | |
| 2220 | + | "type": "boolean", | |
| 2221 | + | "description": "Merge although rules are not met, where a ruleset lists you as one who may bypass it. Recorded as a bypass in its evaluations.", | |
| 2200 | 2222 | }, | |
| 2201 | 2223 | })), | |
| 2202 | 2224 | &["repo", "number"], | |
| ⋯ | |||
| 2793 | 2815 | &["repo"], | |
| 2794 | 2816 | ), | |
| 2795 | 2817 | Op::Security(op) => op.input(), | |
| 2818 | + | Op::Rules(op) => op.input(), | |
| 2796 | 2819 | } | |
| 2797 | 2820 | } | |
| 2798 | 2821 | ||
| ⋯ | |||
| 2818 | 2841 | | Op::ListCheckNames | |
| 2819 | 2842 | | Op::GetMergeQueue | |
| 2820 | 2843 | | Op::GetCodeownersErrors | |
| 2844 | + | | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::GetRepoRuleset | RulesOp::GetBranchRules) | |
| 2821 | 2845 | ) | |
| 2822 | 2846 | } | |
| 2823 | 2847 | ||
| ⋯ | |||
| 2828 | 2852 | ||
| 2829 | 2853 | /// Whether the operation is about one repository, named by `repo`. | |
| 2830 | 2854 | pub(crate) fn needs_repo(self) -> bool { | |
| 2855 | + | if let Op::Rules(op) = self { | |
| 2856 | + | return op.needs_repo(); | |
| 2857 | + | } | |
| 2831 | 2858 | if let Op::Security(op) = self { | |
| 2832 | 2859 | return op.needs_repo(); | |
| 2833 | 2860 | } | |
| ⋯ | |||
| 4755 | 4782 | // The security suite: the security service decides, this gives | |
| 4756 | 4783 | // each answer its public shape. | |
| 4757 | 4784 | Op::Security(op) => crate::security::run(op, services, viewer, input).await, | |
| 4785 | + | Op::Rules(op) => crate::rules::run(op, services, viewer, input).await, | |
| 4758 | 4786 | Op::ReopenSecurityAlert => { | |
| 4759 | 4787 | let changed: Outcome<AlertChange> = call( | |
| 4760 | 4788 | &services.security, | |
| 7936 | 7936 | "confidence": null | |
| 7937 | 7937 | }, | |
| 7938 | 7938 | "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." | |
| 7939 | + | }, | |
| 7940 | + | "list_repo_rulesets": { | |
| 7941 | + | "params": { | |
| 7942 | + | "owner": "flagon-io", | |
| 7943 | + | "name": "hello" | |
| 7944 | + | }, | |
| 7945 | + | "query": { | |
| 7946 | + | "include_parents": "true" | |
| 7947 | + | }, | |
| 7948 | + | "response": [ | |
| 7949 | + | { | |
| 7950 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 7951 | + | "level": "repository", | |
| 7952 | + | "workspace": "flagon-io", | |
| 7953 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 7954 | + | "repository": "flagon-io/hello", | |
| 7955 | + | "name": "Protect main", | |
| 7956 | + | "enforcement": "active", | |
| 7957 | + | "target": "branch", | |
| 7958 | + | "conditions": { | |
| 7959 | + | "ref_name": { | |
| 7960 | + | "include": [ | |
| 7961 | + | "~DEFAULT_BRANCH" | |
| 7962 | + | ], | |
| 7963 | + | "exclude": [] | |
| 7964 | + | } | |
| 7965 | + | }, | |
| 7966 | + | "bypass_actors": [ | |
| 7967 | + | { | |
| 7968 | + | "kind": "role", | |
| 7969 | + | "value": "admin", | |
| 7970 | + | "mode": "pull_requests" | |
| 7971 | + | } | |
| 7972 | + | ], | |
| 7973 | + | "rules": [ | |
| 7974 | + | { | |
| 7975 | + | "type": "deletion", | |
| 7976 | + | "parameters": {}, | |
| 7977 | + | "applies_to": "everyone" | |
| 7978 | + | }, | |
| 7979 | + | { | |
| 7980 | + | "type": "non_fast_forward", | |
| 7981 | + | "parameters": {}, | |
| 7982 | + | "applies_to": "everyone" | |
| 7983 | + | }, | |
| 7984 | + | { | |
| 7985 | + | "type": "pull_request", | |
| 7986 | + | "parameters": { | |
| 7987 | + | "required_approvals": 1, | |
| 7988 | + | "count_agent_approvals": true, | |
| 7989 | + | "dismiss_stale_reviews_on_push": true, | |
| 7990 | + | "require_code_owner_review": true, | |
| 7991 | + | "require_last_push_approval": false, | |
| 7992 | + | "allowed_merge_methods": [] | |
| 7993 | + | }, | |
| 7994 | + | "applies_to": "everyone" | |
| 7995 | + | }, | |
| 7996 | + | { | |
| 7997 | + | "type": "required_status_checks", | |
| 7998 | + | "parameters": { | |
| 7999 | + | "checks": [ | |
| 8000 | + | { | |
| 8001 | + | "context": "CI", | |
| 8002 | + | "integration": "actions" | |
| 8003 | + | } | |
| 8004 | + | ], | |
| 8005 | + | "strict": true, | |
| 8006 | + | "paths": [], | |
| 8007 | + | "allow_bypass_on_merge": false | |
| 8008 | + | }, | |
| 8009 | + | "applies_to": "everyone" | |
| 8010 | + | }, | |
| 8011 | + | { | |
| 8012 | + | "type": "pull_request", | |
| 8013 | + | "parameters": { | |
| 8014 | + | "required_approvals": 1, | |
| 8015 | + | "count_agent_approvals": false, | |
| 8016 | + | "dismiss_stale_reviews_on_push": false, | |
| 8017 | + | "require_code_owner_review": false, | |
| 8018 | + | "require_last_push_approval": false, | |
| 8019 | + | "allowed_merge_methods": [] | |
| 8020 | + | }, | |
| 8021 | + | "applies_to": "agents" | |
| 8022 | + | }, | |
| 8023 | + | { | |
| 8024 | + | "type": "file_path_restriction", | |
| 8025 | + | "parameters": { | |
| 8026 | + | "restricted_file_paths": [ | |
| 8027 | + | ".g1t/workflows/**", | |
| 8028 | + | "CODEOWNERS" | |
| 8029 | + | ] | |
| 8030 | + | }, | |
| 8031 | + | "applies_to": "agents" | |
| 8032 | + | } | |
| 8033 | + | ], | |
| 8034 | + | "created_by": "syntaqx", | |
| 8035 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8036 | + | "updated_by": "syntaqx", | |
| 8037 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8038 | + | }, | |
| 8039 | + | { | |
| 8040 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8041 | + | "level": "workspace", | |
| 8042 | + | "workspace": "flagon-io", | |
| 8043 | + | "name": "Release freeze", | |
| 8044 | + | "enforcement": "evaluate", | |
| 8045 | + | "target": "branch", | |
| 8046 | + | "conditions": { | |
| 8047 | + | "ref_name": { | |
| 8048 | + | "include": [ | |
| 8049 | + | "~DEFAULT_BRANCH", | |
| 8050 | + | "release/**" | |
| 8051 | + | ], | |
| 8052 | + | "exclude": [] | |
| 8053 | + | }, | |
| 8054 | + | "repository": { | |
| 8055 | + | "include": [ | |
| 8056 | + | "~ALL" | |
| 8057 | + | ], | |
| 8058 | + | "exclude": [ | |
| 8059 | + | "sandbox-*" | |
| 8060 | + | ], | |
| 8061 | + | "visibility": "any", | |
| 8062 | + | "topics": [] | |
| 8063 | + | } | |
| 8064 | + | }, | |
| 8065 | + | "bypass_actors": [ | |
| 8066 | + | { | |
| 8067 | + | "kind": "team", | |
| 8068 | + | "value": "flagon-io/release", | |
| 8069 | + | "mode": "always" | |
| 8070 | + | } | |
| 8071 | + | ], | |
| 8072 | + | "rules": [ | |
| 8073 | + | { | |
| 8074 | + | "type": "merge_window", | |
| 8075 | + | "parameters": { | |
| 8076 | + | "time_zone": "-05:00", | |
| 8077 | + | "windows": [ | |
| 8078 | + | { | |
| 8079 | + | "days": [ | |
| 8080 | + | "mon", | |
| 8081 | + | "tue", | |
| 8082 | + | "wed", | |
| 8083 | + | "thu" | |
| 8084 | + | ], | |
| 8085 | + | "start": "09:00", | |
| 8086 | + | "end": "17:00" | |
| 8087 | + | } | |
| 8088 | + | ], | |
| 8089 | + | "freezes": [ | |
| 8090 | + | { | |
| 8091 | + | "start": "2026-12-20T00:00:00Z", | |
| 8092 | + | "end": "2027-01-04T00:00:00Z", | |
| 8093 | + | "reason": "Holidays" | |
| 8094 | + | } | |
| 8095 | + | ], | |
| 8096 | + | "exceptions": [] | |
| 8097 | + | }, | |
| 8098 | + | "applies_to": "everyone" | |
| 8099 | + | }, | |
| 8100 | + | { | |
| 8101 | + | "type": "cost_cap", | |
| 8102 | + | "parameters": { | |
| 8103 | + | "max_usd": 25.0 | |
| 8104 | + | }, | |
| 8105 | + | "applies_to": "agents" | |
| 8106 | + | } | |
| 8107 | + | ], | |
| 8108 | + | "created_by": "syntaqx", | |
| 8109 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8110 | + | "updated_by": "syntaqx", | |
| 8111 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8112 | + | } | |
| 8113 | + | ], | |
| 8114 | + | "notes": "With `include_parents`, the workspace's rulesets that hold in the repository come too, with `level` `workspace`. The ruleset made from the repository's branch protection settings has `source` `branch_protection`." | |
| 8115 | + | }, | |
| 8116 | + | "get_repo_ruleset": { | |
| 8117 | + | "params": { | |
| 8118 | + | "owner": "flagon-io", | |
| 8119 | + | "name": "hello", | |
| 8120 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m" | |
| 8121 | + | }, | |
| 8122 | + | "response": { | |
| 8123 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8124 | + | "level": "repository", | |
| 8125 | + | "workspace": "flagon-io", | |
| 8126 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8127 | + | "repository": "flagon-io/hello", | |
| 8128 | + | "name": "Protect main", | |
| 8129 | + | "enforcement": "active", | |
| 8130 | + | "target": "branch", | |
| 8131 | + | "conditions": { | |
| 8132 | + | "ref_name": { | |
| 8133 | + | "include": [ | |
| 8134 | + | "~DEFAULT_BRANCH" | |
| 8135 | + | ], | |
| 8136 | + | "exclude": [] | |
| 8137 | + | } | |
| 8138 | + | }, | |
| 8139 | + | "bypass_actors": [ | |
| 8140 | + | { | |
| 8141 | + | "kind": "role", | |
| 8142 | + | "value": "admin", | |
| 8143 | + | "mode": "pull_requests" | |
| 8144 | + | } | |
| 8145 | + | ], | |
| 8146 | + | "rules": [ | |
| 8147 | + | { | |
| 8148 | + | "type": "deletion", | |
| 8149 | + | "parameters": {}, | |
| 8150 | + | "applies_to": "everyone" | |
| 8151 | + | }, | |
| 8152 | + | { | |
| 8153 | + | "type": "non_fast_forward", | |
| 8154 | + | "parameters": {}, | |
| 8155 | + | "applies_to": "everyone" | |
| 8156 | + | }, | |
| 8157 | + | { | |
| 8158 | + | "type": "pull_request", | |
| 8159 | + | "parameters": { | |
| 8160 | + | "required_approvals": 1, | |
| 8161 | + | "count_agent_approvals": true, | |
| 8162 | + | "dismiss_stale_reviews_on_push": true, | |
| 8163 | + | "require_code_owner_review": true, | |
| 8164 | + | "require_last_push_approval": false, | |
| 8165 | + | "allowed_merge_methods": [] | |
| 8166 | + | }, | |
| 8167 | + | "applies_to": "everyone" | |
| 8168 | + | }, | |
| 8169 | + | { | |
| 8170 | + | "type": "required_status_checks", | |
| 8171 | + | "parameters": { | |
| 8172 | + | "checks": [ | |
| 8173 | + | { | |
| 8174 | + | "context": "CI", | |
| 8175 | + | "integration": "actions" | |
| 8176 | + | } | |
| 8177 | + | ], | |
| 8178 | + | "strict": true, | |
| 8179 | + | "paths": [], | |
| 8180 | + | "allow_bypass_on_merge": false | |
| 8181 | + | }, | |
| 8182 | + | "applies_to": "everyone" | |
| 8183 | + | }, | |
| 8184 | + | { | |
| 8185 | + | "type": "pull_request", | |
| 8186 | + | "parameters": { | |
| 8187 | + | "required_approvals": 1, | |
| 8188 | + | "count_agent_approvals": false, | |
| 8189 | + | "dismiss_stale_reviews_on_push": false, | |
| 8190 | + | "require_code_owner_review": false, | |
| 8191 | + | "require_last_push_approval": false, | |
| 8192 | + | "allowed_merge_methods": [] | |
| 8193 | + | }, | |
| 8194 | + | "applies_to": "agents" | |
| 8195 | + | }, | |
| 8196 | + | { | |
| 8197 | + | "type": "file_path_restriction", | |
| 8198 | + | "parameters": { | |
| 8199 | + | "restricted_file_paths": [ | |
| 8200 | + | ".g1t/workflows/**", | |
| 8201 | + | "CODEOWNERS" | |
| 8202 | + | ] | |
| 8203 | + | }, | |
| 8204 | + | "applies_to": "agents" | |
| 8205 | + | } | |
| 8206 | + | ], | |
| 8207 | + | "created_by": "syntaqx", | |
| 8208 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8209 | + | "updated_by": "syntaqx", | |
| 8210 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8211 | + | } | |
| 8212 | + | }, | |
| 8213 | + | "create_repo_ruleset": { | |
| 8214 | + | "params": { | |
| 8215 | + | "owner": "flagon-io", | |
| 8216 | + | "name": "hello" | |
| 8217 | + | }, | |
| 8218 | + | "request": { | |
| 8219 | + | "ruleset_name": "Protect main", | |
| 8220 | + | "enforcement": "active", | |
| 8221 | + | "target": "branch", | |
| 8222 | + | "conditions": { | |
| 8223 | + | "ref_name": { | |
| 8224 | + | "include": [ | |
| 8225 | + | "~DEFAULT_BRANCH" | |
| 8226 | + | ], | |
| 8227 | + | "exclude": [] | |
| 8228 | + | } | |
| 8229 | + | }, | |
| 8230 | + | "bypass_actors": [ | |
| 8231 | + | { | |
| 8232 | + | "kind": "role", | |
| 8233 | + | "value": "admin", | |
| 8234 | + | "mode": "pull_requests" | |
| 8235 | + | } | |
| 8236 | + | ], | |
| 8237 | + | "rules": [ | |
| 8238 | + | { | |
| 8239 | + | "type": "deletion", | |
| 8240 | + | "parameters": {}, | |
| 8241 | + | "applies_to": "everyone" | |
| 8242 | + | }, | |
| 8243 | + | { | |
| 8244 | + | "type": "non_fast_forward", | |
| 8245 | + | "parameters": {}, | |
| 8246 | + | "applies_to": "everyone" | |
| 8247 | + | }, | |
| 8248 | + | { | |
| 8249 | + | "type": "pull_request", | |
| 8250 | + | "parameters": { | |
| 8251 | + | "required_approvals": 1, | |
| 8252 | + | "count_agent_approvals": true, | |
| 8253 | + | "dismiss_stale_reviews_on_push": true, | |
| 8254 | + | "require_code_owner_review": true, | |
| 8255 | + | "require_last_push_approval": false, | |
| 8256 | + | "allowed_merge_methods": [] | |
| 8257 | + | }, | |
| 8258 | + | "applies_to": "everyone" | |
| 8259 | + | }, | |
| 8260 | + | { | |
| 8261 | + | "type": "required_status_checks", | |
| 8262 | + | "parameters": { | |
| 8263 | + | "checks": [ | |
| 8264 | + | { | |
| 8265 | + | "context": "CI", | |
| 8266 | + | "integration": "actions" | |
| 8267 | + | } | |
| 8268 | + | ], | |
| 8269 | + | "strict": true, | |
| 8270 | + | "paths": [], | |
| 8271 | + | "allow_bypass_on_merge": false | |
| 8272 | + | }, | |
| 8273 | + | "applies_to": "everyone" | |
| 8274 | + | }, | |
| 8275 | + | { | |
| 8276 | + | "type": "pull_request", | |
| 8277 | + | "parameters": { | |
| 8278 | + | "required_approvals": 1, | |
| 8279 | + | "count_agent_approvals": false, | |
| 8280 | + | "dismiss_stale_reviews_on_push": false, | |
| 8281 | + | "require_code_owner_review": false, | |
| 8282 | + | "require_last_push_approval": false, | |
| 8283 | + | "allowed_merge_methods": [] | |
| 8284 | + | }, | |
| 8285 | + | "applies_to": "agents" | |
| 8286 | + | }, | |
| 8287 | + | { | |
| 8288 | + | "type": "file_path_restriction", | |
| 8289 | + | "parameters": { | |
| 8290 | + | "restricted_file_paths": [ | |
| 8291 | + | ".g1t/workflows/**", | |
| 8292 | + | "CODEOWNERS" | |
| 8293 | + | ] | |
| 8294 | + | }, | |
| 8295 | + | "applies_to": "agents" | |
| 8296 | + | } | |
| 8297 | + | ] | |
| 8298 | + | }, | |
| 8299 | + | "response": { | |
| 8300 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8301 | + | "level": "repository", | |
| 8302 | + | "workspace": "flagon-io", | |
| 8303 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8304 | + | "repository": "flagon-io/hello", | |
| 8305 | + | "name": "Protect main", | |
| 8306 | + | "enforcement": "active", | |
| 8307 | + | "target": "branch", | |
| 8308 | + | "conditions": { | |
| 8309 | + | "ref_name": { | |
| 8310 | + | "include": [ | |
| 8311 | + | "~DEFAULT_BRANCH" | |
| 8312 | + | ], | |
| 8313 | + | "exclude": [] | |
| 8314 | + | } | |
| 8315 | + | }, | |
| 8316 | + | "bypass_actors": [ | |
| 8317 | + | { | |
| 8318 | + | "kind": "role", | |
| 8319 | + | "value": "admin", | |
| 8320 | + | "mode": "pull_requests" | |
| 8321 | + | } | |
| 8322 | + | ], | |
| 8323 | + | "rules": [ | |
| 8324 | + | { | |
| 8325 | + | "type": "deletion", | |
| 8326 | + | "parameters": {}, | |
| 8327 | + | "applies_to": "everyone" | |
| 8328 | + | }, | |
| 8329 | + | { | |
| 8330 | + | "type": "non_fast_forward", | |
| 8331 | + | "parameters": {}, | |
| 8332 | + | "applies_to": "everyone" | |
| 8333 | + | }, | |
| 8334 | + | { | |
| 8335 | + | "type": "pull_request", | |
| 8336 | + | "parameters": { | |
| 8337 | + | "required_approvals": 1, | |
| 8338 | + | "count_agent_approvals": true, | |
| 8339 | + | "dismiss_stale_reviews_on_push": true, | |
| 8340 | + | "require_code_owner_review": true, | |
| 8341 | + | "require_last_push_approval": false, | |
| 8342 | + | "allowed_merge_methods": [] | |
| 8343 | + | }, | |
| 8344 | + | "applies_to": "everyone" | |
| 8345 | + | }, | |
| 8346 | + | { | |
| 8347 | + | "type": "required_status_checks", | |
| 8348 | + | "parameters": { | |
| 8349 | + | "checks": [ | |
| 8350 | + | { | |
| 8351 | + | "context": "CI", | |
| 8352 | + | "integration": "actions" | |
| 8353 | + | } | |
| 8354 | + | ], | |
| 8355 | + | "strict": true, | |
| 8356 | + | "paths": [], | |
| 8357 | + | "allow_bypass_on_merge": false | |
| 8358 | + | }, | |
| 8359 | + | "applies_to": "everyone" | |
| 8360 | + | }, | |
| 8361 | + | { | |
| 8362 | + | "type": "pull_request", | |
| 8363 | + | "parameters": { | |
| 8364 | + | "required_approvals": 1, | |
| 8365 | + | "count_agent_approvals": false, | |
| 8366 | + | "dismiss_stale_reviews_on_push": false, | |
| 8367 | + | "require_code_owner_review": false, | |
| 8368 | + | "require_last_push_approval": false, | |
| 8369 | + | "allowed_merge_methods": [] | |
| 8370 | + | }, | |
| 8371 | + | "applies_to": "agents" | |
| 8372 | + | }, | |
| 8373 | + | { | |
| 8374 | + | "type": "file_path_restriction", | |
| 8375 | + | "parameters": { | |
| 8376 | + | "restricted_file_paths": [ | |
| 8377 | + | ".g1t/workflows/**", | |
| 8378 | + | "CODEOWNERS" | |
| 8379 | + | ] | |
| 8380 | + | }, | |
| 8381 | + | "applies_to": "agents" | |
| 8382 | + | } | |
| 8383 | + | ], | |
| 8384 | + | "created_by": "syntaqx", | |
| 8385 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8386 | + | "updated_by": "syntaqx", | |
| 8387 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8388 | + | }, | |
| 8389 | + | "notes": "Parameters left out take their defaults, and the ruleset comes back as saved: patterns trimmed, roles and teams lowercased. A pattern that does not compile, or a rule the target cannot hold (a pull request rule on tags), is refused with `invalid` and says why." | |
| 8390 | + | }, | |
| 8391 | + | "update_repo_ruleset": { | |
| 8392 | + | "params": { | |
| 8393 | + | "owner": "flagon-io", | |
| 8394 | + | "name": "hello", | |
| 8395 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m" | |
| 8396 | + | }, | |
| 8397 | + | "request": { | |
| 8398 | + | "enforcement": "evaluate" | |
| 8399 | + | }, | |
| 8400 | + | "response": { | |
| 8401 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8402 | + | "level": "repository", | |
| 8403 | + | "workspace": "flagon-io", | |
| 8404 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8405 | + | "repository": "flagon-io/hello", | |
| 8406 | + | "name": "Protect main", | |
| 8407 | + | "enforcement": "evaluate", | |
| 8408 | + | "target": "branch", | |
| 8409 | + | "conditions": { | |
| 8410 | + | "ref_name": { | |
| 8411 | + | "include": [ | |
| 8412 | + | "~DEFAULT_BRANCH" | |
| 8413 | + | ], | |
| 8414 | + | "exclude": [] | |
| 8415 | + | } | |
| 8416 | + | }, | |
| 8417 | + | "bypass_actors": [ | |
| 8418 | + | { | |
| 8419 | + | "kind": "role", | |
| 8420 | + | "value": "admin", | |
| 8421 | + | "mode": "pull_requests" | |
| 8422 | + | } | |
| 8423 | + | ], | |
| 8424 | + | "rules": [ | |
| 8425 | + | { | |
| 8426 | + | "type": "deletion", | |
| 8427 | + | "parameters": {}, | |
| 8428 | + | "applies_to": "everyone" | |
| 8429 | + | }, | |
| 8430 | + | { | |
| 8431 | + | "type": "non_fast_forward", | |
| 8432 | + | "parameters": {}, | |
| 8433 | + | "applies_to": "everyone" | |
| 8434 | + | }, | |
| 8435 | + | { | |
| 8436 | + | "type": "pull_request", | |
| 8437 | + | "parameters": { | |
| 8438 | + | "required_approvals": 1, | |
| 8439 | + | "count_agent_approvals": true, | |
| 8440 | + | "dismiss_stale_reviews_on_push": true, | |
| 8441 | + | "require_code_owner_review": true, | |
| 8442 | + | "require_last_push_approval": false, | |
| 8443 | + | "allowed_merge_methods": [] | |
| 8444 | + | }, | |
| 8445 | + | "applies_to": "everyone" | |
| 8446 | + | }, | |
| 8447 | + | { | |
| 8448 | + | "type": "required_status_checks", | |
| 8449 | + | "parameters": { | |
| 8450 | + | "checks": [ | |
| 8451 | + | { | |
| 8452 | + | "context": "CI", | |
| 8453 | + | "integration": "actions" | |
| 8454 | + | } | |
| 8455 | + | ], | |
| 8456 | + | "strict": true, | |
| 8457 | + | "paths": [], | |
| 8458 | + | "allow_bypass_on_merge": false | |
| 8459 | + | }, | |
| 8460 | + | "applies_to": "everyone" | |
| 8461 | + | }, | |
| 8462 | + | { | |
| 8463 | + | "type": "pull_request", | |
| 8464 | + | "parameters": { | |
| 8465 | + | "required_approvals": 1, | |
| 8466 | + | "count_agent_approvals": false, | |
| 8467 | + | "dismiss_stale_reviews_on_push": false, | |
| 8468 | + | "require_code_owner_review": false, | |
| 8469 | + | "require_last_push_approval": false, | |
| 8470 | + | "allowed_merge_methods": [] | |
| 8471 | + | }, | |
| 8472 | + | "applies_to": "agents" | |
| 8473 | + | }, | |
| 8474 | + | { | |
| 8475 | + | "type": "file_path_restriction", | |
| 8476 | + | "parameters": { | |
| 8477 | + | "restricted_file_paths": [ | |
| 8478 | + | ".g1t/workflows/**", | |
| 8479 | + | "CODEOWNERS" | |
| 8480 | + | ] | |
| 8481 | + | }, | |
| 8482 | + | "applies_to": "agents" | |
| 8483 | + | } | |
| 8484 | + | ], | |
| 8485 | + | "created_by": "syntaqx", | |
| 8486 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8487 | + | "updated_by": "syntaqx", | |
| 8488 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8489 | + | }, | |
| 8490 | + | "notes": "Fields left out stay as they are. `rules` and `bypass_actors`, when given, replace the whole list." | |
| 8491 | + | }, | |
| 8492 | + | "delete_repo_ruleset": { | |
| 8493 | + | "params": { | |
| 8494 | + | "owner": "flagon-io", | |
| 8495 | + | "name": "hello", | |
| 8496 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m" | |
| 8497 | + | }, | |
| 8498 | + | "response": { | |
| 8499 | + | "deleted": true | |
| 8500 | + | } | |
| 8501 | + | }, | |
| 8502 | + | "get_branch_rules": { | |
| 8503 | + | "params": { | |
| 8504 | + | "owner": "flagon-io", | |
| 8505 | + | "name": "hello", | |
| 8506 | + | "branch": "main" | |
| 8507 | + | }, | |
| 8508 | + | "response": { | |
| 8509 | + | "name": "main", | |
| 8510 | + | "target": "branch", | |
| 8511 | + | "default_branch": true, | |
| 8512 | + | "rules": [ | |
| 8513 | + | { | |
| 8514 | + | "type": "deletion", | |
| 8515 | + | "parameters": {}, | |
| 8516 | + | "applies_to": "everyone", | |
| 8517 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8518 | + | "ruleset_name": "Protect main", | |
| 8519 | + | "level": "repository", | |
| 8520 | + | "enforcement": "active" | |
| 8521 | + | }, | |
| 8522 | + | { | |
| 8523 | + | "type": "non_fast_forward", | |
| 8524 | + | "parameters": {}, | |
| 8525 | + | "applies_to": "everyone", | |
| 8526 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8527 | + | "ruleset_name": "Protect main", | |
| 8528 | + | "level": "repository", | |
| 8529 | + | "enforcement": "active" | |
| 8530 | + | }, | |
| 8531 | + | { | |
| 8532 | + | "type": "pull_request", | |
| 8533 | + | "parameters": { | |
| 8534 | + | "required_approvals": 1, | |
| 8535 | + | "count_agent_approvals": true, | |
| 8536 | + | "dismiss_stale_reviews_on_push": true, | |
| 8537 | + | "require_code_owner_review": true, | |
| 8538 | + | "require_last_push_approval": false, | |
| 8539 | + | "allowed_merge_methods": [] | |
| 8540 | + | }, | |
| 8541 | + | "applies_to": "everyone", | |
| 8542 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8543 | + | "ruleset_name": "Protect main", | |
| 8544 | + | "level": "repository", | |
| 8545 | + | "enforcement": "active" | |
| 8546 | + | }, | |
| 8547 | + | { | |
| 8548 | + | "type": "required_status_checks", | |
| 8549 | + | "parameters": { | |
| 8550 | + | "checks": [ | |
| 8551 | + | { | |
| 8552 | + | "context": "CI", | |
| 8553 | + | "integration": "actions" | |
| 8554 | + | } | |
| 8555 | + | ], | |
| 8556 | + | "strict": true, | |
| 8557 | + | "paths": [], | |
| 8558 | + | "allow_bypass_on_merge": false | |
| 8559 | + | }, | |
| 8560 | + | "applies_to": "everyone", | |
| 8561 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8562 | + | "ruleset_name": "Protect main", | |
| 8563 | + | "level": "repository", | |
| 8564 | + | "enforcement": "active" | |
| 8565 | + | }, | |
| 8566 | + | { | |
| 8567 | + | "type": "merge_window", | |
| 8568 | + | "parameters": { | |
| 8569 | + | "time_zone": "-05:00", | |
| 8570 | + | "windows": [ | |
| 8571 | + | { | |
| 8572 | + | "days": [ | |
| 8573 | + | "mon", | |
| 8574 | + | "tue", | |
| 8575 | + | "wed", | |
| 8576 | + | "thu" | |
| 8577 | + | ], | |
| 8578 | + | "start": "09:00", | |
| 8579 | + | "end": "17:00" | |
| 8580 | + | } | |
| 8581 | + | ], | |
| 8582 | + | "freezes": [ | |
| 8583 | + | { | |
| 8584 | + | "start": "2026-12-20T00:00:00Z", | |
| 8585 | + | "end": "2027-01-04T00:00:00Z", | |
| 8586 | + | "reason": "Holidays" | |
| 8587 | + | } | |
| 8588 | + | ], | |
| 8589 | + | "exceptions": [] | |
| 8590 | + | }, | |
| 8591 | + | "applies_to": "everyone", | |
| 8592 | + | "ruleset_id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8593 | + | "ruleset_name": "Release freeze", | |
| 8594 | + | "level": "workspace", | |
| 8595 | + | "enforcement": "evaluate" | |
| 8596 | + | } | |
| 8597 | + | ], | |
| 8598 | + | "rulesets": [ | |
| 8599 | + | { | |
| 8600 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8601 | + | "name": "Protect main", | |
| 8602 | + | "level": "repository", | |
| 8603 | + | "enforcement": "active", | |
| 8604 | + | "bypass_actors": [ | |
| 8605 | + | { | |
| 8606 | + | "kind": "role", | |
| 8607 | + | "value": "admin", | |
| 8608 | + | "mode": "pull_requests" | |
| 8609 | + | } | |
| 8610 | + | ] | |
| 8611 | + | }, | |
| 8612 | + | { | |
| 8613 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8614 | + | "name": "Release freeze", | |
| 8615 | + | "level": "workspace", | |
| 8616 | + | "enforcement": "evaluate", | |
| 8617 | + | "bypass_actors": [ | |
| 8618 | + | { | |
| 8619 | + | "kind": "team", | |
| 8620 | + | "value": "flagon-io/release", | |
| 8621 | + | "mode": "always" | |
| 8622 | + | } | |
| 8623 | + | ] | |
| 8624 | + | } | |
| 8625 | + | ] | |
| 8626 | + | }, | |
| 8627 | + | "notes": "A branch with slashes in its name is URL-encoded as one segment: `/rules/branches/release%2F1.x`. Add `?target=tag` for a tag." | |
| 8628 | + | }, | |
| 8629 | + | "list_rule_evaluations": { | |
| 8630 | + | "params": { | |
| 8631 | + | "owner": "flagon-io", | |
| 8632 | + | "name": "hello" | |
| 8633 | + | }, | |
| 8634 | + | "query": { | |
| 8635 | + | "problems_only": "true" | |
| 8636 | + | }, | |
| 8637 | + | "response": { | |
| 8638 | + | "evaluations": [ | |
| 8639 | + | { | |
| 8640 | + | "id": "rev_01kq3c4d5e6f7g8h9j0k1m2n3p", | |
| 8641 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8642 | + | "workspace": "flagon-io", | |
| 8643 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8644 | + | "ruleset_name": "Protect main", | |
| 8645 | + | "enforcement": "active", | |
| 8646 | + | "action": "push", | |
| 8647 | + | "git_ref": "refs/heads/main", | |
| 8648 | + | "actor": "g1t", | |
| 8649 | + | "actor_kind": "agent", | |
| 8650 | + | "verdict": "fail", | |
| 8651 | + | "violations": [ | |
| 8652 | + | { | |
| 8653 | + | "rule": "pull_request", | |
| 8654 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8655 | + | "ruleset_name": "Protect main", | |
| 8656 | + | "enforcement": "active", | |
| 8657 | + | "message": "Changes to main must be made through a pull request.", | |
| 8658 | + | "remedy": "Push a branch, open a pull request into main, and merge it." | |
| 8659 | + | } | |
| 8660 | + | ], | |
| 8661 | + | "number": null, | |
| 8662 | + | "sha": "9f3c2a1b7e6d5c4b3a2918f7e6d5c4b3a2918f7e", | |
| 8663 | + | "repository": "flagon-io/hello", | |
| 8664 | + | "created_at": "2026-10-07T14:02:11.318Z" | |
| 8665 | + | } | |
| 8666 | + | ], | |
| 8667 | + | "next": null, | |
| 8668 | + | "insights": { | |
| 8669 | + | "days": 30, | |
| 8670 | + | "total": 214, | |
| 8671 | + | "passed": 198, | |
| 8672 | + | "blocked": 9, | |
| 8673 | + | "would_block": 5, | |
| 8674 | + | "bypassed": 2, | |
| 8675 | + | "by_ruleset": [ | |
| 8676 | + | { | |
| 8677 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8678 | + | "ruleset_name": "Protect main", | |
| 8679 | + | "enforcement": "active", | |
| 8680 | + | "total": 120, | |
| 8681 | + | "blocked": 9, | |
| 8682 | + | "would_block": 0, | |
| 8683 | + | "bypassed": 2 | |
| 8684 | + | } | |
| 8685 | + | ], | |
| 8686 | + | "by_rule": [ | |
| 8687 | + | { | |
| 8688 | + | "rule": "pull_request", | |
| 8689 | + | "count": 7 | |
| 8690 | + | }, | |
| 8691 | + | { | |
| 8692 | + | "rule": "non_fast_forward", | |
| 8693 | + | "count": 2 | |
| 8694 | + | } | |
| 8695 | + | ] | |
| 8696 | + | } | |
| 8697 | + | }, | |
| 8698 | + | "notes": "A `fail` of a ruleset in `evaluate` is what it would have refused. Pass `next` as `before` for the next page. `insights` counts the last 30 days." | |
| 8699 | + | }, | |
| 8700 | + | "list_workspace_rulesets": { | |
| 8701 | + | "params": { | |
| 8702 | + | "workspace": "flagon-io" | |
| 8703 | + | }, | |
| 8704 | + | "response": [ | |
| 8705 | + | { | |
| 8706 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8707 | + | "level": "workspace", | |
| 8708 | + | "workspace": "flagon-io", | |
| 8709 | + | "name": "Release freeze", | |
| 8710 | + | "enforcement": "evaluate", | |
| 8711 | + | "target": "branch", | |
| 8712 | + | "conditions": { | |
| 8713 | + | "ref_name": { | |
| 8714 | + | "include": [ | |
| 8715 | + | "~DEFAULT_BRANCH", | |
| 8716 | + | "release/**" | |
| 8717 | + | ], | |
| 8718 | + | "exclude": [] | |
| 8719 | + | }, | |
| 8720 | + | "repository": { | |
| 8721 | + | "include": [ | |
| 8722 | + | "~ALL" | |
| 8723 | + | ], | |
| 8724 | + | "exclude": [ | |
| 8725 | + | "sandbox-*" | |
| 8726 | + | ], | |
| 8727 | + | "visibility": "any", | |
| 8728 | + | "topics": [] | |
| 8729 | + | } | |
| 8730 | + | }, | |
| 8731 | + | "bypass_actors": [ | |
| 8732 | + | { | |
| 8733 | + | "kind": "team", | |
| 8734 | + | "value": "flagon-io/release", | |
| 8735 | + | "mode": "always" | |
| 8736 | + | } | |
| 8737 | + | ], | |
| 8738 | + | "rules": [ | |
| 8739 | + | { | |
| 8740 | + | "type": "merge_window", | |
| 8741 | + | "parameters": { | |
| 8742 | + | "time_zone": "-05:00", | |
| 8743 | + | "windows": [ | |
| 8744 | + | { | |
| 8745 | + | "days": [ | |
| 8746 | + | "mon", | |
| 8747 | + | "tue", | |
| 8748 | + | "wed", | |
| 8749 | + | "thu" | |
| 8750 | + | ], | |
| 8751 | + | "start": "09:00", | |
| 8752 | + | "end": "17:00" | |
| 8753 | + | } | |
| 8754 | + | ], | |
| 8755 | + | "freezes": [ | |
| 8756 | + | { | |
| 8757 | + | "start": "2026-12-20T00:00:00Z", | |
| 8758 | + | "end": "2027-01-04T00:00:00Z", | |
| 8759 | + | "reason": "Holidays" | |
| 8760 | + | } | |
| 8761 | + | ], | |
| 8762 | + | "exceptions": [] | |
| 8763 | + | }, | |
| 8764 | + | "applies_to": "everyone" | |
| 8765 | + | }, | |
| 8766 | + | { | |
| 8767 | + | "type": "cost_cap", | |
| 8768 | + | "parameters": { | |
| 8769 | + | "max_usd": 25.0 | |
| 8770 | + | }, | |
| 8771 | + | "applies_to": "agents" | |
| 8772 | + | } | |
| 8773 | + | ], | |
| 8774 | + | "created_by": "syntaqx", | |
| 8775 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8776 | + | "updated_by": "syntaqx", | |
| 8777 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8778 | + | } | |
| 8779 | + | ] | |
| 8780 | + | }, | |
| 8781 | + | "get_workspace_ruleset": { | |
| 8782 | + | "params": { | |
| 8783 | + | "workspace": "flagon-io", | |
| 8784 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n" | |
| 8785 | + | }, | |
| 8786 | + | "response": { | |
| 8787 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8788 | + | "level": "workspace", | |
| 8789 | + | "workspace": "flagon-io", | |
| 8790 | + | "name": "Release freeze", | |
| 8791 | + | "enforcement": "evaluate", | |
| 8792 | + | "target": "branch", | |
| 8793 | + | "conditions": { | |
| 8794 | + | "ref_name": { | |
| 8795 | + | "include": [ | |
| 8796 | + | "~DEFAULT_BRANCH", | |
| 8797 | + | "release/**" | |
| 8798 | + | ], | |
| 8799 | + | "exclude": [] | |
| 8800 | + | }, | |
| 8801 | + | "repository": { | |
| 8802 | + | "include": [ | |
| 8803 | + | "~ALL" | |
| 8804 | + | ], | |
| 8805 | + | "exclude": [ | |
| 8806 | + | "sandbox-*" | |
| 8807 | + | ], | |
| 8808 | + | "visibility": "any", | |
| 8809 | + | "topics": [] | |
| 8810 | + | } | |
| 8811 | + | }, | |
| 8812 | + | "bypass_actors": [ | |
| 8813 | + | { | |
| 8814 | + | "kind": "team", | |
| 8815 | + | "value": "flagon-io/release", | |
| 8816 | + | "mode": "always" | |
| 8817 | + | } | |
| 8818 | + | ], | |
| 8819 | + | "rules": [ | |
| 8820 | + | { | |
| 8821 | + | "type": "merge_window", | |
| 8822 | + | "parameters": { | |
| 8823 | + | "time_zone": "-05:00", | |
| 8824 | + | "windows": [ | |
| 8825 | + | { | |
| 8826 | + | "days": [ | |
| 8827 | + | "mon", | |
| 8828 | + | "tue", | |
| 8829 | + | "wed", | |
| 8830 | + | "thu" | |
| 8831 | + | ], | |
| 8832 | + | "start": "09:00", | |
| 8833 | + | "end": "17:00" | |
| 8834 | + | } | |
| 8835 | + | ], | |
| 8836 | + | "freezes": [ | |
| 8837 | + | { | |
| 8838 | + | "start": "2026-12-20T00:00:00Z", | |
| 8839 | + | "end": "2027-01-04T00:00:00Z", | |
| 8840 | + | "reason": "Holidays" | |
| 8841 | + | } | |
| 8842 | + | ], | |
| 8843 | + | "exceptions": [] | |
| 8844 | + | }, | |
| 8845 | + | "applies_to": "everyone" | |
| 8846 | + | }, | |
| 8847 | + | { | |
| 8848 | + | "type": "cost_cap", | |
| 8849 | + | "parameters": { | |
| 8850 | + | "max_usd": 25.0 | |
| 8851 | + | }, | |
| 8852 | + | "applies_to": "agents" | |
| 8853 | + | } | |
| 8854 | + | ], | |
| 8855 | + | "created_by": "syntaqx", | |
| 8856 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8857 | + | "updated_by": "syntaqx", | |
| 8858 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8859 | + | } | |
| 8860 | + | }, | |
| 8861 | + | "create_workspace_ruleset": { | |
| 8862 | + | "params": { | |
| 8863 | + | "workspace": "flagon-io" | |
| 8864 | + | }, | |
| 8865 | + | "request": { | |
| 8866 | + | "ruleset_name": "Release freeze", | |
| 8867 | + | "enforcement": "evaluate", | |
| 8868 | + | "target": "branch", | |
| 8869 | + | "conditions": { | |
| 8870 | + | "ref_name": { | |
| 8871 | + | "include": [ | |
| 8872 | + | "~DEFAULT_BRANCH", | |
| 8873 | + | "release/**" | |
| 8874 | + | ], | |
| 8875 | + | "exclude": [] | |
| 8876 | + | }, | |
| 8877 | + | "repository": { | |
| 8878 | + | "include": [ | |
| 8879 | + | "~ALL" | |
| 8880 | + | ], | |
| 8881 | + | "exclude": [ | |
| 8882 | + | "sandbox-*" | |
| 8883 | + | ], | |
| 8884 | + | "visibility": "any", | |
| 8885 | + | "topics": [] | |
| 8886 | + | } | |
| 8887 | + | }, | |
| 8888 | + | "bypass_actors": [ | |
| 8889 | + | { | |
| 8890 | + | "kind": "team", | |
| 8891 | + | "value": "flagon-io/release", | |
| 8892 | + | "mode": "always" | |
| 8893 | + | } | |
| 8894 | + | ], | |
| 8895 | + | "rules": [ | |
| 8896 | + | { | |
| 8897 | + | "type": "merge_window", | |
| 8898 | + | "parameters": { | |
| 8899 | + | "time_zone": "-05:00", | |
| 8900 | + | "windows": [ | |
| 8901 | + | { | |
| 8902 | + | "days": [ | |
| 8903 | + | "mon", | |
| 8904 | + | "tue", | |
| 8905 | + | "wed", | |
| 8906 | + | "thu" | |
| 8907 | + | ], | |
| 8908 | + | "start": "09:00", | |
| 8909 | + | "end": "17:00" | |
| 8910 | + | } | |
| 8911 | + | ], | |
| 8912 | + | "freezes": [ | |
| 8913 | + | { | |
| 8914 | + | "start": "2026-12-20T00:00:00Z", | |
| 8915 | + | "end": "2027-01-04T00:00:00Z", | |
| 8916 | + | "reason": "Holidays" | |
| 8917 | + | } | |
| 8918 | + | ], | |
| 8919 | + | "exceptions": [] | |
| 8920 | + | }, | |
| 8921 | + | "applies_to": "everyone" | |
| 8922 | + | }, | |
| 8923 | + | { | |
| 8924 | + | "type": "cost_cap", | |
| 8925 | + | "parameters": { | |
| 8926 | + | "max_usd": 25.0 | |
| 8927 | + | }, | |
| 8928 | + | "applies_to": "agents" | |
| 8929 | + | } | |
| 8930 | + | ] | |
| 8931 | + | }, | |
| 8932 | + | "response": { | |
| 8933 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8934 | + | "level": "workspace", | |
| 8935 | + | "workspace": "flagon-io", | |
| 8936 | + | "name": "Release freeze", | |
| 8937 | + | "enforcement": "evaluate", | |
| 8938 | + | "target": "branch", | |
| 8939 | + | "conditions": { | |
| 8940 | + | "ref_name": { | |
| 8941 | + | "include": [ | |
| 8942 | + | "~DEFAULT_BRANCH", | |
| 8943 | + | "release/**" | |
| 8944 | + | ], | |
| 8945 | + | "exclude": [] | |
| 8946 | + | }, | |
| 8947 | + | "repository": { | |
| 8948 | + | "include": [ | |
| 8949 | + | "~ALL" | |
| 8950 | + | ], | |
| 8951 | + | "exclude": [ | |
| 8952 | + | "sandbox-*" | |
| 8953 | + | ], | |
| 8954 | + | "visibility": "any", | |
| 8955 | + | "topics": [] | |
| 8956 | + | } | |
| 8957 | + | }, | |
| 8958 | + | "bypass_actors": [ | |
| 8959 | + | { | |
| 8960 | + | "kind": "team", | |
| 8961 | + | "value": "flagon-io/release", | |
| 8962 | + | "mode": "always" | |
| 8963 | + | } | |
| 8964 | + | ], | |
| 8965 | + | "rules": [ | |
| 8966 | + | { | |
| 8967 | + | "type": "merge_window", | |
| 8968 | + | "parameters": { | |
| 8969 | + | "time_zone": "-05:00", | |
| 8970 | + | "windows": [ | |
| 8971 | + | { | |
| 8972 | + | "days": [ | |
| 8973 | + | "mon", | |
| 8974 | + | "tue", | |
| 8975 | + | "wed", | |
| 8976 | + | "thu" | |
| 8977 | + | ], | |
| 8978 | + | "start": "09:00", | |
| 8979 | + | "end": "17:00" | |
| 8980 | + | } | |
| 8981 | + | ], | |
| 8982 | + | "freezes": [ | |
| 8983 | + | { | |
| 8984 | + | "start": "2026-12-20T00:00:00Z", | |
| 8985 | + | "end": "2027-01-04T00:00:00Z", | |
| 8986 | + | "reason": "Holidays" | |
| 8987 | + | } | |
| 8988 | + | ], | |
| 8989 | + | "exceptions": [] | |
| 8990 | + | }, | |
| 8991 | + | "applies_to": "everyone" | |
| 8992 | + | }, | |
| 8993 | + | { | |
| 8994 | + | "type": "cost_cap", | |
| 8995 | + | "parameters": { | |
| 8996 | + | "max_usd": 25.0 | |
| 8997 | + | }, | |
| 8998 | + | "applies_to": "agents" | |
| 8999 | + | } | |
| 9000 | + | ], | |
| 9001 | + | "created_by": "syntaqx", | |
| 9002 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 9003 | + | "updated_by": "syntaqx", | |
| 9004 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 9005 | + | }, | |
| 9006 | + | "notes": "`conditions.repository` chooses the repositories it holds in; left out, every repository of the workspace." | |
| 9007 | + | }, | |
| 9008 | + | "update_workspace_ruleset": { | |
| 9009 | + | "params": { | |
| 9010 | + | "workspace": "flagon-io", | |
| 9011 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n" | |
| 9012 | + | }, | |
| 9013 | + | "request": { | |
| 9014 | + | "enforcement": "active" | |
| 9015 | + | }, | |
| 9016 | + | "response": { | |
| 9017 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 9018 | + | "level": "workspace", | |
| 9019 | + | "workspace": "flagon-io", | |
| 9020 | + | "name": "Release freeze", | |
| 9021 | + | "enforcement": "active", | |
| 9022 | + | "target": "branch", | |
| 9023 | + | "conditions": { | |
| 9024 | + | "ref_name": { | |
| 9025 | + | "include": [ | |
| 9026 | + | "~DEFAULT_BRANCH", | |
| 9027 | + | "release/**" | |
| 9028 | + | ], | |
| 9029 | + | "exclude": [] | |
| 9030 | + | }, | |
| 9031 | + | "repository": { | |
| 9032 | + | "include": [ | |
| 9033 | + | "~ALL" | |
| 9034 | + | ], | |
| 9035 | + | "exclude": [ | |
| 9036 | + | "sandbox-*" | |
| 9037 | + | ], | |
| 9038 | + | "visibility": "any", | |
| 9039 | + | "topics": [] | |
| 9040 | + | } | |
| 9041 | + | }, | |
| 9042 | + | "bypass_actors": [ | |
| 9043 | + | { | |
| 9044 | + | "kind": "team", | |
| 9045 | + | "value": "flagon-io/release", | |
| 9046 | + | "mode": "always" | |
| 9047 | + | } | |
| 9048 | + | ], | |
| 9049 | + | "rules": [ | |
| 9050 | + | { | |
| 9051 | + | "type": "merge_window", | |
| 9052 | + | "parameters": { | |
| 9053 | + | "time_zone": "-05:00", | |
| 9054 | + | "windows": [ | |
| 9055 | + | { | |
| 9056 | + | "days": [ | |
| 9057 | + | "mon", | |
| 9058 | + | "tue", | |
| 9059 | + | "wed", | |
| 9060 | + | "thu" | |
| 9061 | + | ], | |
| 9062 | + | "start": "09:00", | |
| 9063 | + | "end": "17:00" | |
| 9064 | + | } | |
| 9065 | + | ], | |
| 9066 | + | "freezes": [ | |
| 9067 | + | { | |
| 9068 | + | "start": "2026-12-20T00:00:00Z", | |
| 9069 | + | "end": "2027-01-04T00:00:00Z", | |
| 9070 | + | "reason": "Holidays" | |
| 9071 | + | } | |
| 9072 | + | ], | |
| 9073 | + | "exceptions": [] | |
| 9074 | + | }, | |
| 9075 | + | "applies_to": "everyone" | |
| 9076 | + | }, | |
| 9077 | + | { | |
| 9078 | + | "type": "cost_cap", | |
| 9079 | + | "parameters": { | |
| 9080 | + | "max_usd": 25.0 | |
| 9081 | + | }, | |
| 9082 | + | "applies_to": "agents" | |
| 9083 | + | } | |
| 9084 | + | ], | |
| 9085 | + | "created_by": "syntaqx", | |
| 9086 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 9087 | + | "updated_by": "syntaqx", | |
| 9088 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 9089 | + | } | |
| 9090 | + | }, | |
| 9091 | + | "delete_workspace_ruleset": { | |
| 9092 | + | "params": { | |
| 9093 | + | "workspace": "flagon-io", | |
| 9094 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n" | |
| 9095 | + | }, | |
| 9096 | + | "response": { | |
| 9097 | + | "deleted": true | |
| 9098 | + | } | |
| 9099 | + | }, | |
| 9100 | + | "list_workspace_rule_evaluations": { | |
| 9101 | + | "params": { | |
| 9102 | + | "workspace": "flagon-io" | |
| 9103 | + | }, | |
| 9104 | + | "response": { | |
| 9105 | + | "evaluations": [ | |
| 9106 | + | { | |
| 9107 | + | "id": "rev_01kq3c4d5e6f7g8h9j0k1m2n3p", | |
| 9108 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 9109 | + | "workspace": "flagon-io", | |
| 9110 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 9111 | + | "ruleset_name": "Protect main", | |
| 9112 | + | "enforcement": "active", | |
| 9113 | + | "action": "push", | |
| 9114 | + | "git_ref": "refs/heads/main", | |
| 9115 | + | "actor": "g1t", | |
| 9116 | + | "actor_kind": "agent", | |
| 9117 | + | "verdict": "fail", | |
| 9118 | + | "violations": [ | |
| 9119 | + | { | |
| 9120 | + | "rule": "pull_request", | |
| 9121 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 9122 | + | "ruleset_name": "Protect main", | |
| 9123 | + | "enforcement": "active", | |
| 9124 | + | "message": "Changes to main must be made through a pull request.", | |
| 9125 | + | "remedy": "Push a branch, open a pull request into main, and merge it." | |
| 9126 | + | } | |
| 9127 | + | ], | |
| 9128 | + | "number": null, | |
| 9129 | + | "sha": "9f3c2a1b7e6d5c4b3a2918f7e6d5c4b3a2918f7e", | |
| 9130 | + | "repository": "flagon-io/hello", | |
| 9131 | + | "created_at": "2026-10-07T14:02:11.318Z" | |
| 9132 | + | } | |
| 9133 | + | ], | |
| 9134 | + | "next": null, | |
| 9135 | + | "insights": { | |
| 9136 | + | "days": 30, | |
| 9137 | + | "total": 214, | |
| 9138 | + | "passed": 198, | |
| 9139 | + | "blocked": 9, | |
| 9140 | + | "would_block": 5, | |
| 9141 | + | "bypassed": 2, | |
| 9142 | + | "by_ruleset": [ | |
| 9143 | + | { | |
| 9144 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 9145 | + | "ruleset_name": "Protect main", | |
| 9146 | + | "enforcement": "active", | |
| 9147 | + | "total": 120, | |
| 9148 | + | "blocked": 9, | |
| 9149 | + | "would_block": 0, | |
| 9150 | + | "bypassed": 2 | |
| 9151 | + | } | |
| 9152 | + | ], | |
| 9153 | + | "by_rule": [ | |
| 9154 | + | { | |
| 9155 | + | "rule": "pull_request", | |
| 9156 | + | "count": 7 | |
| 9157 | + | }, | |
| 9158 | + | { | |
| 9159 | + | "rule": "non_fast_forward", | |
| 9160 | + | "count": 2 | |
| 9161 | + | } | |
| 9162 | + | ] | |
| 9163 | + | } | |
| 9164 | + | } | |
| 7939 | 9165 | } | |
| 7940 | 9166 | } |
| 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, codeowners, integrations, repos, search, teams, webhooks, work}; | |
| 10 | + | use g1t_contracts::{access, actions, codeowners, integrations, repos, rules, search, teams, webhooks, work}; | |
| 11 | 11 | use g1t_kit::wire::{self, USER_KEYED}; | |
| 12 | 12 | use serde::Serialize; | |
| 13 | 13 | use serde::de::DeserializeOwned; | |
| ⋯ | |||
| 15 | 15 | ||
| 16 | 16 | use crate::openapi::document; | |
| 17 | 17 | use crate::operations::Op; | |
| 18 | + | use crate::rules::RulesOp; | |
| 18 | 19 | ||
| 19 | 20 | /// A key as `#[serde(rename_all = "camelCase")]` writes it. | |
| 20 | 21 | fn camel_key(key: &str) -> String { | |
| ⋯ | |||
| 90 | 91 | Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is), | |
| 91 | 92 | Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is), | |
| 92 | 93 | Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is), | |
| 94 | + | // Rulesets travel in `snake_case` between services too. | |
| 95 | + | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => { | |
| 96 | + | return through::<Vec<rules::Ruleset>>(op, as_is); | |
| 97 | + | } | |
| 98 | + | Op::Rules( | |
| 99 | + | RulesOp::GetRepoRuleset | |
| 100 | + | | RulesOp::CreateRepoRuleset | |
| 101 | + | | RulesOp::UpdateRepoRuleset | |
| 102 | + | | RulesOp::GetWorkspaceRuleset | |
| 103 | + | | RulesOp::CreateWorkspaceRuleset | |
| 104 | + | | RulesOp::UpdateWorkspaceRuleset, | |
| 105 | + | ) => return through::<rules::Ruleset>(op, as_is), | |
| 106 | + | Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is), | |
| 107 | + | Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => { | |
| 108 | + | return through::<rules::EvaluationPage>(op, as_is); | |
| 109 | + | } | |
| 110 | + | // Built by the API itself. | |
| 111 | + | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is, | |
| 93 | 112 | // Built by the API itself, in `snake_case`. | |
| 94 | 113 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 95 | 114 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { | |
| 3 | 3 | use serde_json::{Map, Value}; | |
| 4 | 4 | ||
| 5 | 5 | use crate::operations::Op; | |
| 6 | + | use crate::rules::RulesOp; | |
| 6 | 7 | use crate::security::SecurityOp; | |
| 7 | 8 | ||
| 8 | 9 | pub struct Route { | |
| ⋯ | |||
| 233 | 234 | &[], | |
| 234 | 235 | ), | |
| 235 | 236 | route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]), | |
| 237 | + | // Rulesets: a repository's, a workspace's, the rules of one branch, | |
| 238 | + | // and how they judged pushes and merges. | |
| 239 | + | route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]), | |
| 240 | + | route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]), | |
| 241 | + | route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]), | |
| 242 | + | route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]), | |
| 243 | + | route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]), | |
| 244 | + | route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]), | |
| 245 | + | route( | |
| 246 | + | "GET", | |
| 247 | + | "/repos/:owner/:name/rules/evaluations", | |
| 248 | + | Op::Rules(RulesOp::ListRuleEvaluations), | |
| 249 | + | &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")], | |
| 250 | + | ), | |
| 251 | + | route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]), | |
| 252 | + | route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]), | |
| 253 | + | route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]), | |
| 254 | + | route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]), | |
| 255 | + | route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]), | |
| 256 | + | route( | |
| 257 | + | "GET", | |
| 258 | + | "/workspaces/:workspace/rules/evaluations", | |
| 259 | + | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 260 | + | &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")], | |
| 261 | + | ), | |
| 236 | 262 | route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]), | |
| 237 | 263 | route( | |
| 238 | 264 | "POST", | |
| 1 | + | //! Rulesets over REST and MCP: a repository's and a workspace's rulesets, | |
| 2 | + | //! the rules that hold for one branch or tag, and how the rules judged | |
| 3 | + | //! pushes and merges (the evaluations, with insights). | |
| 4 | + | //! | |
| 5 | + | //! Rulesets travel as the API shows them, `snake_case` between services | |
| 6 | + | //! too, so a ruleset read here, exported from the site or written by hand | |
| 7 | + | //! is created and updated unchanged. The work service decides who may see | |
| 8 | + | //! and change them and validates every one (`g1t_rules::validate`). | |
| 9 | + | ||
| 10 | + | use g1t_contracts::repos::RepoPath; | |
| 11 | + | use g1t_contracts::rules::*; | |
| 12 | + | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 13 | + | use serde::Serialize; | |
| 14 | + | use serde::de::DeserializeOwned; | |
| 15 | + | use serde_json::{Map, Value, json}; | |
| 16 | + | use worker::Result; | |
| 17 | + | ||
| 18 | + | use crate::operations::Services; | |
| 19 | + | ||
| 20 | + | /// One operation on rulesets. | |
| 21 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 22 | + | pub enum RulesOp { | |
| 23 | + | ListRepoRulesets, | |
| 24 | + | GetRepoRuleset, | |
| 25 | + | CreateRepoRuleset, | |
| 26 | + | UpdateRepoRuleset, | |
| 27 | + | DeleteRepoRuleset, | |
| 28 | + | GetBranchRules, | |
| 29 | + | ListRuleEvaluations, | |
| 30 | + | ListWorkspaceRulesets, | |
| 31 | + | GetWorkspaceRuleset, | |
| 32 | + | CreateWorkspaceRuleset, | |
| 33 | + | UpdateWorkspaceRuleset, | |
| 34 | + | DeleteWorkspaceRuleset, | |
| 35 | + | ListWorkspaceRuleEvaluations, | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | /// The keys of a ruleset in a request body. | |
| 39 | + | const SPEC_KEYS: [&str; 6] = ["name", "enforcement", "target", "conditions", "bypass_actors", "rules"]; | |
| 40 | + | ||
| 41 | + | impl RulesOp { | |
| 42 | + | /// Every one: `Op::ALL` lists each as `Op::Rules(…)`, which a test | |
| 43 | + | /// checks against this. | |
| 44 | + | #[cfg(test)] | |
| 45 | + | pub const ALL: [RulesOp; 13] = [ | |
| 46 | + | RulesOp::ListRepoRulesets, | |
| 47 | + | RulesOp::GetRepoRuleset, | |
| 48 | + | RulesOp::CreateRepoRuleset, | |
| 49 | + | RulesOp::UpdateRepoRuleset, | |
| 50 | + | RulesOp::DeleteRepoRuleset, | |
| 51 | + | RulesOp::GetBranchRules, | |
| 52 | + | RulesOp::ListRuleEvaluations, | |
| 53 | + | RulesOp::ListWorkspaceRulesets, | |
| 54 | + | RulesOp::GetWorkspaceRuleset, | |
| 55 | + | RulesOp::CreateWorkspaceRuleset, | |
| 56 | + | RulesOp::UpdateWorkspaceRuleset, | |
| 57 | + | RulesOp::DeleteWorkspaceRuleset, | |
| 58 | + | RulesOp::ListWorkspaceRuleEvaluations, | |
| 59 | + | ]; | |
| 60 | + | ||
| 61 | + | pub fn name(self) -> &'static str { | |
| 62 | + | match self { | |
| 63 | + | RulesOp::ListRepoRulesets => "list_repo_rulesets", | |
| 64 | + | RulesOp::GetRepoRuleset => "get_repo_ruleset", | |
| 65 | + | RulesOp::CreateRepoRuleset => "create_repo_ruleset", | |
| 66 | + | RulesOp::UpdateRepoRuleset => "update_repo_ruleset", | |
| 67 | + | RulesOp::DeleteRepoRuleset => "delete_repo_ruleset", | |
| 68 | + | RulesOp::GetBranchRules => "get_branch_rules", | |
| 69 | + | RulesOp::ListRuleEvaluations => "list_rule_evaluations", | |
| 70 | + | RulesOp::ListWorkspaceRulesets => "list_workspace_rulesets", | |
| 71 | + | RulesOp::GetWorkspaceRuleset => "get_workspace_ruleset", | |
| 72 | + | RulesOp::CreateWorkspaceRuleset => "create_workspace_ruleset", | |
| 73 | + | RulesOp::UpdateWorkspaceRuleset => "update_workspace_ruleset", | |
| 74 | + | RulesOp::DeleteWorkspaceRuleset => "delete_workspace_ruleset", | |
| 75 | + | RulesOp::ListWorkspaceRuleEvaluations => "list_workspace_rule_evaluations", | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | /// For the API reference: "List a repository's rulesets". | |
| 80 | + | pub fn title(self) -> &'static str { | |
| 81 | + | match self { | |
| 82 | + | RulesOp::ListRepoRulesets => "List a repository's rulesets", | |
| 83 | + | RulesOp::GetRepoRuleset => "Get a repository ruleset", | |
| 84 | + | RulesOp::CreateRepoRuleset => "Create a repository ruleset", | |
| 85 | + | RulesOp::UpdateRepoRuleset => "Update a repository ruleset", | |
| 86 | + | RulesOp::DeleteRepoRuleset => "Delete a repository ruleset", | |
| 87 | + | RulesOp::GetBranchRules => "Get the rules for a branch", | |
| 88 | + | RulesOp::ListRuleEvaluations => "List a repository's rule evaluations", | |
| 89 | + | RulesOp::ListWorkspaceRulesets => "List a workspace's rulesets", | |
| 90 | + | RulesOp::GetWorkspaceRuleset => "Get a workspace ruleset", | |
| 91 | + | RulesOp::CreateWorkspaceRuleset => "Create a workspace ruleset", | |
| 92 | + | RulesOp::UpdateWorkspaceRuleset => "Update a workspace ruleset", | |
| 93 | + | RulesOp::DeleteWorkspaceRuleset => "Delete a workspace ruleset", | |
| 94 | + | RulesOp::ListWorkspaceRuleEvaluations => "List a workspace's rule evaluations", | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | pub fn description(self) -> &'static str { | |
| 99 | + | match self { | |
| 100 | + | RulesOp::ListRepoRulesets => "List a repository's rulesets: what may happen to its branches and tags, and what a pull request needs before it merges. With include_parents, also its workspace's rulesets that hold in it (level workspace). Each has its enforcement (active, evaluate: a dry run that records what it would have refused, or disabled), target (branch or tag), conditions (ref_name include and exclude patterns: fnmatch, ~DEFAULT_BRANCH, ~ALL), bypass_actors and rules. The one made from branch protection settings has source branch_protection.", | |
| 101 | + | RulesOp::GetRepoRuleset => "Get one of a repository's rulesets by id (rs_…), or one of its workspace's that holds in it.", | |
| 102 | + | RulesOp::CreateRepoRuleset => "Create a repository ruleset: name, enforcement (active, evaluate or disabled; active by default), target (branch or tag), conditions.ref_name (include and exclude patterns), bypass_actors (each a kind: role, team, user, token or g1t, a value, and a mode: always or pull_requests; nobody bypasses unless listed, g1t included) and rules (each a type, its parameters, and applies_to: everyone, agents or people). Rule types: creation, update, deletion, non_fast_forward, required_linear_history, required_signatures, pull_request, required_status_checks, merge_queue, required_deployments, commit_message_pattern, commit_author_email_pattern, committer_email_pattern, branch_name_pattern, tag_name_pattern, file_path_restriction, file_extension_restriction, max_file_size, max_file_path_length, max_files_changed, secret_scanning, confidence_threshold, cost_cap, path_review, merge_window and agent_auto_merge. Several rulesets stack: every rule of each holds. Takes the Maintain role. Returns the ruleset as saved, tidied.", | |
| 103 | + | RulesOp::UpdateRepoRuleset => "Change a repository ruleset. Fields left out stay as they are; rules and bypass_actors, when given, replace the whole list. Takes the Maintain role.", | |
| 104 | + | RulesOp::DeleteRepoRuleset => "Delete a repository ruleset. Its evaluations stay in the log. Takes the Maintain role.", | |
| 105 | + | RulesOp::GetBranchRules => "Every rule that holds for a branch (or a tag, with target tag) of a repository, from every ruleset that targets it, the repository's and its workspace's: each with its type, parameters and applies_to, and the ruleset_id, ruleset_name, level and enforcement it comes from. Active rules come first, then those of rulesets in evaluate. rulesets lists the rulesets with who may bypass each. A branch name with slashes is URL-encoded in the path.", | |
| 106 | + | RulesOp::ListRuleEvaluations => "List how a repository's rulesets judged pushes, merges and other changes to its branches and tags, newest first: the ruleset, the action (push, merge, create_ref, delete_ref, rename_ref or commit), the ref, the actor and whether they are a person, an agent or g1t, the verdict (pass, fail or bypass) and each rule broken with why. A fail of a ruleset in evaluate is what it would have refused. Filter by ruleset_id or verdict, or problems_only; page with before. insights counts the last 30 days by ruleset and by rule. Takes the Write role.", | |
| 107 | + | RulesOp::ListWorkspaceRulesets => "List a workspace's own rulesets. Each holds in the repositories its conditions.repository selects: names matching include (fnmatch, or ~ALL) and not exclude, of a visibility (any, public or private), and carrying one of topics when given. Members only.", | |
| 108 | + | RulesOp::GetWorkspaceRuleset => "Get one of a workspace's own rulesets by id (rs_…), with its conditions, bypass actors and rules. Members only.", | |
| 109 | + | RulesOp::CreateWorkspaceRuleset => "Create a workspace ruleset, as for a repository, plus conditions.repository: which of the workspace's repositories it holds in (include and exclude name patterns, visibility, topics). Owners only.", | |
| 110 | + | RulesOp::UpdateWorkspaceRuleset => "Change a workspace ruleset. Fields left out stay as they are; rules and bypass_actors, when given, replace the whole list. Owners only.", | |
| 111 | + | RulesOp::DeleteWorkspaceRuleset => "Delete a workspace ruleset: it stops holding in every repository it selected. Its evaluations stay in the log. Owners only.", | |
| 112 | + | RulesOp::ListWorkspaceRuleEvaluations => "List how a workspace's rulesets, and its repositories' own, judged changes across its repositories, newest first, with 30 days of insights. Members only.", | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | /// Whether the operation is about one repository named by `repo`. | |
| 117 | + | pub fn needs_repo(self) -> bool { | |
| 118 | + | matches!( | |
| 119 | + | self, | |
| 120 | + | RulesOp::ListRepoRulesets | |
| 121 | + | | RulesOp::GetRepoRuleset | |
| 122 | + | | RulesOp::CreateRepoRuleset | |
| 123 | + | | RulesOp::UpdateRepoRuleset | |
| 124 | + | | RulesOp::DeleteRepoRuleset | |
| 125 | + | | RulesOp::GetBranchRules | |
| 126 | + | | RulesOp::ListRuleEvaluations | |
| 127 | + | ) | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | pub fn input(self) -> Value { | |
| 131 | + | let repo = || json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 132 | + | let workspace = || json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." }); | |
| 133 | + | let id = || json!({ "type": "string", "description": "The ruleset's id: rs_…" }); | |
| 134 | + | let spec = |mut properties: Value, workspace_level: bool| { | |
| 135 | + | properties["ruleset_name"] = json!({ "type": "string", "description": "What people call it, at most 100 characters. A ruleset as exported names it `name`, which is read too." }); | |
| 136 | + | properties["enforcement"] = json!({ "type": "string", "enum": ["active", "evaluate", "disabled"], "description": "active: its rules hold. evaluate: nothing is refused, and what would have been is recorded. disabled: kept, not evaluated. Default active." }); | |
| 137 | + | properties["target"] = json!({ "type": "string", "enum": ["branch", "tag"], "description": "What its name conditions match. Default branch." }); | |
| 138 | + | let mut conditions = json!({ | |
| 139 | + | "ref_name": { | |
| 140 | + | "type": "object", | |
| 141 | + | "description": "Which branches or tags: include and exclude, each a list of fnmatch patterns (* within a path segment, ** across them), ~DEFAULT_BRANCH or ~ALL.", | |
| 142 | + | "properties": { | |
| 143 | + | "include": { "type": "array", "items": { "type": "string" } }, | |
| 144 | + | "exclude": { "type": "array", "items": { "type": "string" } }, | |
| 145 | + | }, | |
| 146 | + | }, | |
| 147 | + | }); | |
| 148 | + | if workspace_level { | |
| 149 | + | conditions["repository"] = json!({ | |
| 150 | + | "type": "object", | |
| 151 | + | "description": "Which of the workspace's repositories: include and exclude name patterns (or ~ALL), visibility (any, public, private) and topics (any of).", | |
| 152 | + | "properties": { | |
| 153 | + | "include": { "type": "array", "items": { "type": "string" } }, | |
| 154 | + | "exclude": { "type": "array", "items": { "type": "string" } }, | |
| 155 | + | "visibility": { "type": "string", "enum": ["any", "public", "private"] }, | |
| 156 | + | "topics": { "type": "array", "items": { "type": "string" } }, | |
| 157 | + | }, | |
| 158 | + | }); | |
| 159 | + | } | |
| 160 | + | properties["conditions"] = json!({ "type": "object", "properties": conditions }); | |
| 161 | + | properties["bypass_actors"] = json!({ | |
| 162 | + | "type": "array", | |
| 163 | + | "description": "Who it does not hold for. Nobody bypasses unless listed, g1t included. kind role takes read, triage, write, maintain, admin (that role or higher) or owner; team its slug or workspace/slug; user a username; token a token id, or workspace for any of the workspace's tokens; g1t no value. mode always (pushes and merges) or pull_requests (merges only; a person merging asks to, with bypass_rules).", | |
| 164 | + | "items": { | |
| 165 | + | "type": "object", | |
| 166 | + | "properties": { | |
| 167 | + | "kind": { "type": "string", "enum": ["role", "team", "user", "token", "g1t"] }, | |
| 168 | + | "value": { "type": "string" }, | |
| 169 | + | "mode": { "type": "string", "enum": ["always", "pull_requests"] }, | |
| 170 | + | }, | |
| 171 | + | "required": ["kind"], | |
| 172 | + | }, | |
| 173 | + | }); | |
| 174 | + | properties["rules"] = json!({ | |
| 175 | + | "type": "array", | |
| 176 | + | "description": "Its rules. Each: type, parameters (left-out parameters take their defaults) and applies_to (everyone, agents or people). See the Rules guide for every type's parameters.", | |
| 177 | + | "items": { | |
| 178 | + | "type": "object", | |
| 179 | + | "properties": { | |
| 180 | + | "type": { "type": "string" }, | |
| 181 | + | "parameters": { "type": "object" }, | |
| 182 | + | "applies_to": { "type": "string", "enum": ["everyone", "agents", "people"] }, | |
| 183 | + | }, | |
| 184 | + | "required": ["type"], | |
| 185 | + | }, | |
| 186 | + | }); | |
| 187 | + | properties | |
| 188 | + | }; | |
| 189 | + | let evaluations = |mut properties: Value| { | |
| 190 | + | properties["ruleset_id"] = json!({ "type": "string", "description": "Only this ruleset's evaluations." }); | |
| 191 | + | properties["verdict"] = json!({ "type": "string", "enum": ["pass", "fail", "bypass"], "description": "Only evaluations that came out this way." }); | |
| 192 | + | properties["problems_only"] = json!({ "type": "boolean", "description": "Only evaluations that broke a rule: failed, would have failed, or bypassed." }); | |
| 193 | + | properties["before"] = json!({ "type": "string", "description": "An evaluation's id (rev_…): only older ones. The page's next." }); | |
| 194 | + | properties["limit"] = json!({ "type": "integer", "description": "How many, 1 to 100; 30 by default." }); | |
| 195 | + | properties | |
| 196 | + | }; | |
| 197 | + | let (properties, required): (Value, &[&str]) = match self { | |
| 198 | + | RulesOp::ListRepoRulesets => ( | |
| 199 | + | json!({ "repo": repo(), "include_parents": { "type": "boolean", "description": "Also list the workspace's rulesets that hold in it." } }), | |
| 200 | + | &["repo"], | |
| 201 | + | ), | |
| 202 | + | RulesOp::GetRepoRuleset | RulesOp::DeleteRepoRuleset => (json!({ "repo": repo(), "id": id() }), &["repo", "id"]), | |
| 203 | + | RulesOp::CreateRepoRuleset => (spec(json!({ "repo": repo() }), false), &["repo"]), | |
| 204 | + | RulesOp::UpdateRepoRuleset => (spec(json!({ "repo": repo(), "id": id() }), false), &["repo", "id"]), | |
| 205 | + | RulesOp::GetBranchRules => ( | |
| 206 | + | json!({ | |
| 207 | + | "repo": repo(), | |
| 208 | + | "branch": { "type": "string", "description": "The branch (or tag) name, such as main or release/1.x." }, | |
| 209 | + | "target": { "type": "string", "enum": ["branch", "tag"], "description": "branch (the default) or tag." }, | |
| 210 | + | }), | |
| 211 | + | &["repo", "branch"], | |
| 212 | + | ), | |
| 213 | + | RulesOp::ListRuleEvaluations => (evaluations(json!({ "repo": repo() })), &["repo"]), | |
| 214 | + | RulesOp::ListWorkspaceRulesets => (json!({ "workspace": workspace() }), &["workspace"]), | |
| 215 | + | RulesOp::GetWorkspaceRuleset | RulesOp::DeleteWorkspaceRuleset => { | |
| 216 | + | (json!({ "workspace": workspace(), "id": id() }), &["workspace", "id"]) | |
| 217 | + | } | |
| 218 | + | RulesOp::CreateWorkspaceRuleset => (spec(json!({ "workspace": workspace() }), true), &["workspace"]), | |
| 219 | + | RulesOp::UpdateWorkspaceRuleset => (spec(json!({ "workspace": workspace(), "id": id() }), true), &["workspace", "id"]), | |
| 220 | + | RulesOp::ListWorkspaceRuleEvaluations => (evaluations(json!({ "workspace": workspace() })), &["workspace"]), | |
| 221 | + | }; | |
| 222 | + | let mut schema = json!({ "type": "object", "properties": properties }); | |
| 223 | + | if !required.is_empty() { | |
| 224 | + | schema["required"] = json!(required); | |
| 225 | + | } | |
| 226 | + | schema | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | fn ok<T: Serialize>(value: &T) -> Result<Outcome<Value>> { | |
| 231 | + | Ok(Outcome::Ok(serde_json::to_value(value)?)) | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | fn text(input: &Value, key: &str) -> Option<String> { | |
| 235 | + | input[key].as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_owned) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | fn flag(input: &Value, key: &str) -> bool { | |
| 239 | + | match &input[key] { | |
| 240 | + | Value::Bool(value) => *value, | |
| 241 | + | Value::String(text) => matches!(text.trim(), "true" | "1"), | |
| 242 | + | _ => false, | |
| 243 | + | } | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | async fn call<A: Serialize, T: DeserializeOwned>(services: &Services, method: &str, args: &A) -> Result<Outcome<T>> { | |
| 247 | + | g1t_kit::call(&services.work, method, args).await | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | /// The ruleset in a request body, laid over `current` for an update: the | |
| 251 | + | /// fields given replace those it had. | |
| 252 | + | pub(crate) fn spec_of(input: &Value, current: Option<&RulesetSpec>) -> std::result::Result<RulesetSpec, String> { | |
| 253 | + | let mut merged: Map<String, Value> = match current { | |
| 254 | + | Some(current) => match serde_json::to_value(current) { | |
| 255 | + | Ok(Value::Object(fields)) => fields, | |
| 256 | + | _ => Map::new(), | |
| 257 | + | }, | |
| 258 | + | None => Map::new(), | |
| 259 | + | }; | |
| 260 | + | for key in SPEC_KEYS { | |
| 261 | + | if let Some(value) = input.get(key).filter(|value| !value.is_null()) { | |
| 262 | + | merged.insert(key.to_owned(), value.clone()); | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | // Under a repository's address `name` is the repository's, so the API | |
| 266 | + | // names the ruleset `ruleset_name`; an exported ruleset's `name` is read | |
| 267 | + | // as well. | |
| 268 | + | if let Some(name) = input.get("ruleset_name").filter(|value| !value.is_null()) { | |
| 269 | + | merged.insert("name".to_owned(), name.clone()); | |
| 270 | + | } | |
| 271 | + | serde_json::from_value(Value::Object(merged)).map_err(|error| format!("The ruleset could not be read: {error}")) | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | fn owner(op: RulesOp, input: &Value, repo: Option<RepoPath>) -> std::result::Result<Owner, String> { | |
| 275 | + | if op.needs_repo() { | |
| 276 | + | return repo.map(Owner::repo).ok_or_else(|| "Give the repository as \"owner/name\".".to_owned()); | |
| 277 | + | } | |
| 278 | + | text(input, "workspace").map(|slug| Owner::workspace(&slug)).ok_or_else(|| "Give the workspace's slug.".to_owned()) | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | pub async fn run(op: RulesOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 282 | + | let owner = match owner(op, input, crate::operations::repo_path(input)) { | |
| 283 | + | Ok(owner) => owner, | |
| 284 | + | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 285 | + | }; | |
| 286 | + | let actor = || viewer.clone().unwrap_or_default(); | |
| 287 | + | let id = || text(input, "id").unwrap_or_default(); | |
| 288 | + | match op { | |
| 289 | + | RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets => { | |
| 290 | + | call( | |
| 291 | + | services, | |
| 292 | + | "list_rulesets", | |
| 293 | + | &ListRulesetsArgs { viewer: viewer.clone(), owner, include_parents: flag(input, "include_parents") }, | |
| 294 | + | ) | |
| 295 | + | .await | |
| 296 | + | } | |
| 297 | + | RulesOp::GetRepoRuleset | RulesOp::GetWorkspaceRuleset => { | |
| 298 | + | call(services, "get_ruleset", &GetRulesetArgs { viewer: viewer.clone(), owner, id: id() }).await | |
| 299 | + | } | |
| 300 | + | RulesOp::CreateRepoRuleset | RulesOp::CreateWorkspaceRuleset => { | |
| 301 | + | let ruleset = match spec_of(input, None) { | |
| 302 | + | Ok(ruleset) => ruleset, | |
| 303 | + | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 304 | + | }; | |
| 305 | + | call(services, "save_ruleset", &SaveRulesetArgs { actor: actor(), owner, id: None, ruleset, from_api: true }).await | |
| 306 | + | } | |
| 307 | + | RulesOp::UpdateRepoRuleset | RulesOp::UpdateWorkspaceRuleset => { | |
| 308 | + | let current: Outcome<Ruleset> = | |
| 309 | + | call(services, "get_ruleset", &GetRulesetArgs { viewer: viewer.clone(), owner: owner.clone(), id: id() }).await?; | |
| 310 | + | let current = match current { | |
| 311 | + | Outcome::Ok(current) => current, | |
| 312 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 313 | + | }; | |
| 314 | + | if current.level == Level::Workspace && op == RulesOp::UpdateRepoRuleset { | |
| 315 | + | return Ok(Outcome::fail( | |
| 316 | + | FailureCode::Invalid, | |
| 317 | + | "That is the workspace's ruleset: change it with update_workspace_ruleset.", | |
| 318 | + | )); | |
| 319 | + | } | |
| 320 | + | let ruleset = match spec_of(input, Some(¤t.spec)) { | |
| 321 | + | Ok(ruleset) => ruleset, | |
| 322 | + | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 323 | + | }; | |
| 324 | + | call(services, "save_ruleset", &SaveRulesetArgs { actor: actor(), owner, id: Some(current.id), ruleset, from_api: true }) | |
| 325 | + | .await | |
| 326 | + | } | |
| 327 | + | RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset => { | |
| 328 | + | let deleted: Outcome<bool> = | |
| 329 | + | call(services, "delete_ruleset", &DeleteRulesetArgs { actor: actor(), owner, id: id(), from_api: true }).await?; | |
| 330 | + | match deleted { | |
| 331 | + | Outcome::Ok(deleted) => ok(&json!({ "deleted": deleted })), | |
| 332 | + | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | RulesOp::GetBranchRules => { | |
| 336 | + | let Some(repo) = owner.repo else { | |
| 337 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\".")); | |
| 338 | + | }; | |
| 339 | + | let target = match text(input, "target").as_deref() { | |
| 340 | + | None | Some("branch") => Target::Branch, | |
| 341 | + | Some("tag") => Target::Tag, | |
| 342 | + | Some(other) => return Ok(Outcome::fail(FailureCode::Invalid, format!("{other} is not a target: use branch or tag."))), | |
| 343 | + | }; | |
| 344 | + | let Some(name) = text(input, "branch") else { | |
| 345 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Name the branch.")); | |
| 346 | + | }; | |
| 347 | + | call(services, "effective_rules", &EffectiveRulesArgs { viewer: viewer.clone(), repo, name, target }).await | |
| 348 | + | } | |
| 349 | + | RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations => { | |
| 350 | + | let verdict = match text(input, "verdict").as_deref() { | |
| 351 | + | None => None, | |
| 352 | + | Some("pass") => Some(Verdict::Pass), | |
| 353 | + | Some("fail") => Some(Verdict::Fail), | |
| 354 | + | Some("bypass") => Some(Verdict::Bypass), | |
| 355 | + | Some(other) => return Ok(Outcome::fail(FailureCode::Invalid, format!("{other} is not a verdict: use pass, fail or bypass."))), | |
| 356 | + | }; | |
| 357 | + | let limit = match &input["limit"] { | |
| 358 | + | Value::Number(number) => number.as_u64().and_then(|n| u32::try_from(n).ok()), | |
| 359 | + | Value::String(digits) => digits.trim().parse().ok(), | |
| 360 | + | _ => None, | |
| 361 | + | }; | |
| 362 | + | call( | |
| 363 | + | services, | |
| 364 | + | "rule_evaluations", | |
| 365 | + | &EvaluationsArgs { | |
| 366 | + | viewer: viewer.clone(), | |
| 367 | + | owner, | |
| 368 | + | ruleset_id: text(input, "ruleset_id"), | |
| 369 | + | verdict, | |
| 370 | + | problems_only: flag(input, "problems_only"), | |
| 371 | + | before: text(input, "before"), | |
| 372 | + | limit, | |
| 373 | + | }, | |
| 374 | + | ) | |
| 375 | + | .await | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | #[cfg(test)] | |
| 381 | + | mod tests { | |
| 382 | + | use super::*; | |
| 383 | + | ||
| 384 | + | #[test] | |
| 385 | + | fn a_body_is_a_ruleset_and_an_update_keeps_what_it_leaves_out() { | |
| 386 | + | let body = json!({ | |
| 387 | + | "repo": "acme/web", | |
| 388 | + | "name": "Protect main", | |
| 389 | + | "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"] } }, | |
| 390 | + | "rules": [{ "type": "deletion" }, { "type": "pull_request", "parameters": { "required_approvals": 2 } }] | |
| 391 | + | }); | |
| 392 | + | let created = spec_of(&body, None).unwrap(); | |
| 393 | + | assert_eq!(created.name, "Protect main"); | |
| 394 | + | assert_eq!(created.enforcement, Enforcement::Active); | |
| 395 | + | assert_eq!(created.rules.len(), 2); | |
| 396 | + | let updated = spec_of(&json!({ "enforcement": "evaluate" }), Some(&created)).unwrap(); | |
| 397 | + | assert_eq!(updated.enforcement, Enforcement::Evaluate); | |
| 398 | + | assert_eq!(updated.rules, created.rules, "rules left out stay"); | |
| 399 | + | let replaced = spec_of(&json!({ "rules": [] }), Some(&created)).unwrap(); | |
| 400 | + | assert!(replaced.rules.is_empty(), "a list given replaces the list"); | |
| 401 | + | let renamed = spec_of(&json!({ "ruleset_name": "Protect releases" }), Some(&created)).unwrap(); | |
| 402 | + | assert_eq!(renamed.name, "Protect releases"); | |
| 403 | + | assert!(spec_of(&json!({ "rules": [{ "type": "no_such_rule" }] }), None).is_err()); | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | #[test] | |
| 407 | + | fn whose_rulesets_comes_from_repo_or_workspace() { | |
| 408 | + | let input = json!({ "workspace": "Acme" }); | |
| 409 | + | assert_eq!(owner(RulesOp::ListWorkspaceRulesets, &input, None).unwrap(), Owner::workspace("acme")); | |
| 410 | + | assert!(owner(RulesOp::ListRepoRulesets, &input, None).is_err()); | |
| 411 | + | let path = RepoPath { namespace: "acme".into(), name: "web".into() }; | |
| 412 | + | assert_eq!(owner(RulesOp::GetBranchRules, &json!({}), Some(path.clone())).unwrap(), Owner::repo(path)); | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | #[test] | |
| 416 | + | fn each_operation_is_described_with_a_schema() { | |
| 417 | + | for op in RulesOp::ALL { | |
| 418 | + | assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name()); | |
| 419 | + | assert_eq!(op.input()["type"], "object"); | |
| 420 | + | if op.needs_repo() { | |
| 421 | + | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); | |
| 422 | + | } else { | |
| 423 | + | assert!(op.input()["required"].as_array().unwrap().contains(&json!("workspace")), "{}", op.name()); | |
| 424 | + | } | |
| 425 | + | } | |
| 426 | + | } | |
| 427 | + | } |
| 19 | 19 | use serde_json::{Map, Value, json}; | |
| 20 | 20 | ||
| 21 | 21 | use crate::operations::Op; | |
| 22 | + | use crate::rules::RulesOp; | |
| 22 | 23 | use crate::security::SecurityOp; | |
| 23 | 24 | ||
| 24 | 25 | pub struct Action { | |
| ⋯ | |||
| 58 | 59 | Tool { | |
| 59 | 60 | name: "repository", | |
| 60 | 61 | title: "Repositories", | |
| 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`.", | |
| 62 | + | description: "Repositories: find, read and create them, change their settings and rulesets (what may happen to branches and tags, and what a pull request needs to merge), 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`.", | |
| 62 | 63 | default_action: None, | |
| 63 | 64 | actions: &[ | |
| 64 | 65 | a("list", Op::ListRepos, "Repositories you can see"), | |
| 65 | 66 | a("get", Op::GetRepo, "One repository"), | |
| 66 | 67 | a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL"), | |
| 67 | 68 | a("update", Op::UpdateRepo, "Change description, website, topics, default branch, protection"), | |
| 68 | − | a("get_settings", Op::GetRepoSettings, "Branch protection: required checks, approvals, how pull requests merge"), | |
| 69 | − | a("update_settings", Op::UpdateRepoSettings, "Change branch protection and how pull requests merge"), | |
| 70 | − | a("check_names", Op::ListCheckNames, "Check names reported lately, to require on the default branch"), | |
| 69 | + | a("get_settings", Op::GetRepoSettings, "How pull requests merge, and the default branch's protection as its rules stack"), | |
| 70 | + | a("update_settings", Op::UpdateRepoSettings, "Change how pull requests merge and the default branch protection ruleset"), | |
| 71 | + | a("check_names", Op::ListCheckNames, "Check names reported lately, to require in a ruleset"), | |
| 72 | + | a("list_rulesets", Op::Rules(RulesOp::ListRepoRulesets), "Its rulesets, and its workspace's that hold in it"), | |
| 73 | + | a("get_ruleset", Op::Rules(RulesOp::GetRepoRuleset), "One ruleset"), | |
| 74 | + | a("create_ruleset", Op::Rules(RulesOp::CreateRepoRuleset), "Create a ruleset for its branches or tags"), | |
| 75 | + | a("update_ruleset", Op::Rules(RulesOp::UpdateRepoRuleset), "Change a ruleset"), | |
| 76 | + | a("delete_ruleset", Op::Rules(RulesOp::DeleteRepoRuleset), "Delete a ruleset"), | |
| 77 | + | a("branch_rules", Op::Rules(RulesOp::GetBranchRules), "Every rule that holds for a branch or tag, and where it comes from"), | |
| 78 | + | a("rule_evaluations", Op::Rules(RulesOp::ListRuleEvaluations), "How its rules judged pushes and merges, with insights"), | |
| 71 | 79 | a("codeowners", Op::GetCodeownersErrors, "Problems in its CODEOWNERS file, by line"), | |
| 72 | 80 | a("list_labels", Op::ListLabels, "Labels, with colors and how many issues and pull requests carry each"), | |
| 73 | 81 | a("create_label", Op::CreateLabel, "Create a label"), | |
| ⋯ | |||
| 267 | 275 | Tool { | |
| 268 | 276 | name: "workspace", | |
| 269 | 277 | title: "Workspaces", | |
| 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.", | |
| 278 | + | description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, set rulesets that hold across its repositories, and keep your own pinned projects at the top of its sidebar.", | |
| 271 | 279 | default_action: None, | |
| 272 | 280 | actions: &[ | |
| 273 | 281 | a("create", Op::CreateWorkspace, "Create a workspace"), | |
| ⋯ | |||
| 286 | 294 | a("pin_project", Op::PinProject, "Pin a project, at a position or the end"), | |
| 287 | 295 | a("unpin_project", Op::UnpinProject, "Unpin a project"), | |
| 288 | 296 | a("reorder_pinned_projects", Op::ReorderPinnedProjects, "Put your pins in a new order"), | |
| 297 | + | a("list_rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), "Its rulesets, which hold across its repositories"), | |
| 298 | + | a("get_ruleset", Op::Rules(RulesOp::GetWorkspaceRuleset), "One of its rulesets"), | |
| 299 | + | a("create_ruleset", Op::Rules(RulesOp::CreateWorkspaceRuleset), "Create a ruleset for some or all of its repositories"), | |
| 300 | + | a("update_ruleset", Op::Rules(RulesOp::UpdateWorkspaceRuleset), "Change one of its rulesets"), | |
| 301 | + | a("delete_ruleset", Op::Rules(RulesOp::DeleteWorkspaceRuleset), "Delete one of its rulesets"), | |
| 302 | + | a("rule_evaluations", Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), "How rules judged changes across its repositories"), | |
| 289 | 303 | ], | |
| 290 | 304 | }, | |
| 291 | 305 | Tool { | |
| ⋯ | |||
| 391 | 405 | matches!( | |
| 392 | 406 | op, | |
| 393 | 407 | Op::Security(SecurityOp::DeleteCustomPattern | SecurityOp::BypassPushProtection) | |
| 408 | + | | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) | |
| 394 | 409 | | Op::DeleteWorkspace | |
| 395 | 410 | | Op::UpdateWorkspace | |
| 396 | 411 | | Op::DeleteRepo | |
Binary or large file; its contents are not shown.
| 293 | 293 | match self { | |
| 294 | 294 | Scope::RepoRead => "See repositories, their settings, labels, timelines and security alerts, and search", | |
| 295 | 295 | Scope::RepoWrite => "Create repositories, rename branches and change how pull requests merge", | |
| 296 | − | Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, and dismiss security alerts", | |
| 296 | + | Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts", | |
| 297 | 297 | Scope::CodeRead => "Clone and fetch private repositories with git", | |
| 298 | 298 | Scope::CodeWrite => "Push commits with git", | |
| 299 | 299 | Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings", | |
| ⋯ | |||
| 314 | 314 | Scope::AccountWrite => "Change your email addresses, make invites, answer invitations and pin projects", | |
| 315 | 315 | Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch", | |
| 316 | 316 | Scope::NotificationsWrite => "Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories", | |
| 317 | − | Scope::WorkspaceRead => "Read workspace invites, integrations, model routes and teams", | |
| 318 | − | Scope::WorkspaceAdmin => "Create and delete workspaces, invite members, connect integrations, and create, change and delete teams", | |
| 317 | + | Scope::WorkspaceRead => "Read workspace invites, integrations, model routes, teams and rulesets", | |
| 318 | + | Scope::WorkspaceAdmin => "Create and delete workspaces, invite members, connect integrations, create, change and delete teams, and change the workspace's rulesets", | |
| 319 | 319 | Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices", | |
| 320 | 320 | Scope::BillingWrite => "Change a workspace's budget and buy AI credit", | |
| 321 | 321 | Scope::AccessRead => "See who has access to repositories", | |
| ⋯ | |||
| 541 | 541 | ("list_team_repos", Scope::WorkspaceRead), | |
| 542 | 542 | ("list_user_teams", Scope::WorkspaceRead), | |
| 543 | 543 | ("create_team", Scope::WorkspaceAdmin), | |
| 544 | + | ("list_workspace_rulesets", Scope::WorkspaceRead), | |
| 545 | + | ("get_workspace_ruleset", Scope::WorkspaceRead), | |
| 546 | + | ("list_workspace_rule_evaluations", Scope::WorkspaceRead), | |
| 547 | + | ("create_workspace_ruleset", Scope::WorkspaceAdmin), | |
| 548 | + | ("update_workspace_ruleset", Scope::WorkspaceAdmin), | |
| 549 | + | ("delete_workspace_ruleset", Scope::WorkspaceAdmin), | |
| 544 | 550 | ("update_team", Scope::WorkspaceAdmin), | |
| 545 | 551 | ("delete_team", Scope::WorkspaceAdmin), | |
| 546 | 552 | ("set_team_member", Scope::WorkspaceAdmin), | |
| ⋯ | |||
| 577 | 583 | ("create_repo", Scope::RepoWrite), | |
| 578 | 584 | ("update_repo", Scope::RepoWrite), | |
| 579 | 585 | ("update_repo_settings", Scope::RepoWrite), | |
| 586 | + | // Rulesets: reading them is reading the repository; changing them | |
| 587 | + | // changes what everyone, agents included, may do, so it is admin. | |
| 588 | + | ("list_repo_rulesets", Scope::RepoRead), | |
| 589 | + | ("get_repo_ruleset", Scope::RepoRead), | |
| 590 | + | ("get_branch_rules", Scope::RepoRead), | |
| 591 | + | ("list_rule_evaluations", Scope::RepoRead), | |
| 592 | + | ("create_repo_ruleset", Scope::RepoAdmin), | |
| 593 | + | ("update_repo_ruleset", Scope::RepoAdmin), | |
| 594 | + | ("delete_repo_ruleset", Scope::RepoAdmin), | |
| 580 | 595 | ("rename_branch", Scope::RepoWrite), | |
| 581 | 596 | ("rename_repo", Scope::RepoAdmin), | |
| 582 | 597 | ("transfer_repo", Scope::RepoAdmin), | |
| 33 | 33 | export const SCOPES = [ | |
| 34 | 34 | { scope: "repo:read", description: "See repositories, their settings, labels, timelines and security alerts, and search" }, | |
| 35 | 35 | { scope: "repo:write", description: "Create repositories, rename branches and change how pull requests merge" }, | |
| 36 | − | { scope: "repo:admin", description: "Rename, archive, transfer, delete or change who can see a repository, and dismiss security alerts" }, | |
| 36 | + | { scope: "repo:admin", description: "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts" }, | |
| 37 | 37 | { scope: "code:read", description: "Clone and fetch private repositories with git" }, | |
| 38 | 38 | { scope: "code:write", description: "Push commits with git" }, | |
| 39 | 39 | { scope: "security:read", description: "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings" }, | |
| ⋯ | |||
| 54 | 54 | { scope: "account:write", description: "Change your email addresses, make invites, answer invitations and pin projects" }, | |
| 55 | 55 | { scope: "notifications:read", description: "See your inbox, its threads, and what you subscribe to and watch" }, | |
| 56 | 56 | { scope: "notifications:write", description: "Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories" }, | |
| 57 | − | { scope: "workspace:read", description: "Read workspace invites, integrations, model routes and teams" }, | |
| 58 | − | { scope: "workspace:admin", description: "Create and delete workspaces, invite members, connect integrations, and create, change and delete teams" }, | |
| 57 | + | { scope: "workspace:read", description: "Read workspace invites, integrations, model routes, teams and rulesets" }, | |
| 58 | + | { scope: "workspace:admin", description: "Create and delete workspaces, invite members, connect integrations, create, change and delete teams, and change the workspace's rulesets" }, | |
| 59 | 59 | { scope: "billing:read", description: "See a workspace's usage, budget, AI credit and invoices" }, | |
| 60 | 60 | { scope: "billing:write", description: "Change a workspace's budget and buy AI credit" }, | |
| 61 | 61 | { scope: "access:read", description: "See who has access to repositories" }, | |
| ⋯ | |||
| 222 | 222 | ["list_team_repos", "workspace:read"], | |
| 223 | 223 | ["list_user_teams", "workspace:read"], | |
| 224 | 224 | ["create_team", "workspace:admin"], | |
| 225 | + | ["list_workspace_rulesets", "workspace:read"], | |
| 226 | + | ["get_workspace_ruleset", "workspace:read"], | |
| 227 | + | ["list_workspace_rule_evaluations", "workspace:read"], | |
| 228 | + | ["create_workspace_ruleset", "workspace:admin"], | |
| 229 | + | ["update_workspace_ruleset", "workspace:admin"], | |
| 230 | + | ["delete_workspace_ruleset", "workspace:admin"], | |
| 225 | 231 | ["update_team", "workspace:admin"], | |
| 226 | 232 | ["delete_team", "workspace:admin"], | |
| 227 | 233 | ["set_team_member", "workspace:admin"], | |
| ⋯ | |||
| 257 | 263 | ["create_repo", "repo:write"], | |
| 258 | 264 | ["update_repo", "repo:write"], | |
| 259 | 265 | ["update_repo_settings", "repo:write"], | |
| 266 | + | ["list_repo_rulesets", "repo:read"], | |
| 267 | + | ["get_repo_ruleset", "repo:read"], | |
| 268 | + | ["get_branch_rules", "repo:read"], | |
| 269 | + | ["list_rule_evaluations", "repo:read"], | |
| 270 | + | ["create_repo_ruleset", "repo:admin"], | |
| 271 | + | ["update_repo_ruleset", "repo:admin"], | |
| 272 | + | ["delete_repo_ruleset", "repo:admin"], | |
| 260 | 273 | ["rename_branch", "repo:write"], | |
| 261 | 274 | ["rename_repo", "repo:admin"], | |
| 262 | 275 | ["transfer_repo", "repo:admin"], | |