Compare changes
Choose two branches to see what one has that the other does not, then open a pull request for it.
1 commit
| 1062 | 1062 | version = "0.1.0" | |
| 1063 | 1063 | dependencies = [ | |
| 1064 | 1064 | "base64 0.22.1", | |
| 1065 | + | "ed25519-dalek", | |
| 1065 | 1066 | "futures-util", | |
| 1066 | 1067 | "g1t-blobstore", | |
| 1067 | 1068 | "g1t-contracts", | |
| 1068 | 1069 | "g1t-kit", | |
| 1070 | + | "g1t-rules", | |
| 1069 | 1071 | "g1t-scan", | |
| 1070 | 1072 | "g1t-secrets", | |
| 1071 | 1073 | "miniz_oxide", | |
| 1072 | 1074 | "serde", | |
| 1073 | 1075 | "serde_json", | |
| 1076 | + | "sha2 0.10.9", | |
| 1074 | 1077 | "similar", | |
| 1075 | 1078 | "worker", | |
| 1076 | 1079 | ] | |
| 1077 | 1080 | ||
| 1078 | 1081 | [[package]] | |
| 1082 | + | name = "g1t-rules" | |
| 1083 | + | version = "0.1.0" | |
| 1084 | + | dependencies = [ | |
| 1085 | + | "g1t-contracts", | |
| 1086 | + | "regex", | |
| 1087 | + | "serde", | |
| 1088 | + | "serde_json", | |
| 1089 | + | ] | |
| 1090 | + | ||
| 1091 | + | [[package]] | |
| 1079 | 1092 | name = "g1t-runner" | |
| 1080 | 1093 | version = "0.1.0" | |
| 1081 | 1094 | dependencies = [ | |
| 1183 | 1196 | "futures-util", | |
| 1184 | 1197 | "g1t-contracts", | |
| 1185 | 1198 | "g1t-kit", | |
| 1199 | + | "g1t-rules", | |
| 1186 | 1200 | "getrandom 0.2.17", | |
| 1187 | 1201 | "hex", | |
| 1188 | 1202 | "serde", |
| 12 | 12 | g1t-blobstore = { path = "crates/blobstore" } | |
| 13 | 13 | g1t-contracts = { path = "crates/contracts" } | |
| 14 | 14 | g1t-kit = { path = "crates/kit" } | |
| 15 | + | g1t-rules = { path = "crates/rules" } | |
| 15 | 16 | g1t-scan = { path = "crates/scan" } | |
| 16 | 17 | g1t-secrets = { path = "crates/secrets" } | |
| 17 | 18 | serde = { version = "1", features = ["derive"] } |
| 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 | &[ | |
| 534 | 554 | Op::RemoveRequestedReviewers => "Remove requested reviewers", | |
| 535 | 555 | Op::GetCodeownersErrors => "List CODEOWNERS errors", | |
| 536 | 556 | Op::Security(op) => op.title(), | |
| 557 | + | Op::Rules(op) => op.title(), | |
| 537 | 558 | } | |
| 538 | 559 | } | |
| 539 | 560 |
| 26 | 26 | }; | |
| 27 | 27 | ||
| 28 | 28 | use crate::alerts::{AlertKind, SecurityAlert}; | |
| 29 | + | use crate::rules::RulesOp; | |
| 29 | 30 | use crate::security::SecurityOp; | |
| 30 | 31 | use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel}; | |
| 31 | 32 | use g1t_contracts::work::*; | |
| 263 | 264 | GetCodeownersErrors, | |
| 264 | 265 | /// The security suite's operations: see [`crate::security`]. | |
| 265 | 266 | Security(SecurityOp), | |
| 267 | + | /// Rulesets: rules.rs. | |
| 268 | + | Rules(RulesOp), | |
| 266 | 269 | } | |
| 267 | 270 | ||
| 268 | 271 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| 625 | 628 | } | |
| 626 | 629 | ||
| 627 | 630 | impl Op { | |
| 628 | − | pub const ALL: [Op; 205] = [ | |
| 631 | + | pub const ALL: [Op; 218] = [ | |
| 629 | 632 | Op::Whoami, | |
| 630 | 633 | Op::GetWorkspace, | |
| 631 | 634 | Op::CreateWorkspace, | |
| 831 | 834 | Op::Security(SecurityOp::GetWorkspaceSettings), | |
| 832 | 835 | Op::Security(SecurityOp::UpdateWorkspaceSettings), | |
| 833 | 836 | Op::Security(SecurityOp::GetOverview), | |
| 837 | + | Op::Rules(RulesOp::ListRepoRulesets), | |
| 838 | + | Op::Rules(RulesOp::GetRepoRuleset), | |
| 839 | + | Op::Rules(RulesOp::CreateRepoRuleset), | |
| 840 | + | Op::Rules(RulesOp::UpdateRepoRuleset), | |
| 841 | + | Op::Rules(RulesOp::DeleteRepoRuleset), | |
| 842 | + | Op::Rules(RulesOp::GetBranchRules), | |
| 843 | + | Op::Rules(RulesOp::ListRuleEvaluations), | |
| 844 | + | Op::Rules(RulesOp::ListWorkspaceRulesets), | |
| 845 | + | Op::Rules(RulesOp::GetWorkspaceRuleset), | |
| 846 | + | Op::Rules(RulesOp::CreateWorkspaceRuleset), | |
| 847 | + | Op::Rules(RulesOp::UpdateWorkspaceRuleset), | |
| 848 | + | Op::Rules(RulesOp::DeleteWorkspaceRuleset), | |
| 849 | + | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 834 | 850 | ]; | |
| 835 | 851 | ||
| 836 | 852 | pub fn by_name(name: &str) -> Option<Op> { | |
| 1015 | 1031 | Op::RemoveRequestedReviewers => "remove_requested_reviewers", | |
| 1016 | 1032 | Op::GetCodeownersErrors => "get_codeowners_errors", | |
| 1017 | 1033 | Op::Security(op) => op.name(), | |
| 1034 | + | Op::Rules(op) => op.name(), | |
| 1018 | 1035 | } | |
| 1019 | 1036 | } | |
| 1020 | 1037 | ||
| 1099 | 1116 | "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." | |
| 1100 | 1117 | } | |
| 1101 | 1118 | Op::GetRepoSettings => { | |
| 1102 | − | "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." | |
| 1119 | + | "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." | |
| 1103 | 1120 | } | |
| 1104 | 1121 | Op::UpdateRepoSettings => { | |
| 1105 | − | "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." | |
| 1122 | + | "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." | |
| 1106 | 1123 | } | |
| 1107 | 1124 | Op::ListCheckNames => { | |
| 1108 | 1125 | "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)." | |
| 1218 | 1235 | "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." | |
| 1219 | 1236 | } | |
| 1220 | 1237 | Op::GetPullRequest => { | |
| 1221 | − | "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)." | |
| 1238 | + | "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)." | |
| 1222 | 1239 | } | |
| 1223 | 1240 | Op::CreatePullRequest => { | |
| 1224 | 1241 | "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." | |
| 1238 | 1255 | "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." | |
| 1239 | 1256 | } | |
| 1240 | 1257 | Op::MergePullRequest => { | |
| 1241 | − | "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." | |
| 1258 | + | "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." | |
| 1242 | 1259 | } | |
| 1243 | 1260 | Op::ListEvents => { | |
| 1244 | 1261 | "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first." | |
| 1509 | 1526 | "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." | |
| 1510 | 1527 | } | |
| 1511 | 1528 | Op::Security(op) => op.description(), | |
| 1529 | + | Op::Rules(op) => op.description(), | |
| 1512 | 1530 | } | |
| 1513 | 1531 | } | |
| 1514 | 1532 | ||
| 2209 | 2227 | }, | |
| 2210 | 2228 | "ignore_checks": { | |
| 2211 | 2229 | "type": "boolean", | |
| 2212 | − | "description": "Merge although required checks have not passed, where the repository allows bypassing them (allow_ignoring_checks).", | |
| 2230 | + | "description": "Merge although required checks have not passed, where the rule requiring them allows it (allow_bypass_on_merge).", | |
| 2231 | + | }, | |
| 2232 | + | "bypass_rules": { | |
| 2233 | + | "type": "boolean", | |
| 2234 | + | "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.", | |
| 2213 | 2235 | }, | |
| 2214 | 2236 | })), | |
| 2215 | 2237 | &["repo", "number"], | |
| 2806 | 2828 | &["repo"], | |
| 2807 | 2829 | ), | |
| 2808 | 2830 | Op::Security(op) => op.input(), | |
| 2831 | + | Op::Rules(op) => op.input(), | |
| 2809 | 2832 | } | |
| 2810 | 2833 | } | |
| 2811 | 2834 | ||
| 2831 | 2854 | | Op::ListCheckNames | |
| 2832 | 2855 | | Op::GetMergeQueue | |
| 2833 | 2856 | | Op::GetCodeownersErrors | |
| 2857 | + | | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::GetRepoRuleset | RulesOp::GetBranchRules) | |
| 2834 | 2858 | ) | |
| 2835 | 2859 | } | |
| 2836 | 2860 | ||
| 2841 | 2865 | ||
| 2842 | 2866 | /// Whether the operation is about one repository, named by `repo`. | |
| 2843 | 2867 | pub(crate) fn needs_repo(self) -> bool { | |
| 2868 | + | if let Op::Rules(op) = self { | |
| 2869 | + | return op.needs_repo(); | |
| 2870 | + | } | |
| 2844 | 2871 | if let Op::Security(op) = self { | |
| 2845 | 2872 | return op.needs_repo(); | |
| 2846 | 2873 | } | |
| 3078 | 3105 | summary: text(input, "summary"), | |
| 3079 | 3106 | keep_issue_open: input["keep_issue_open"].as_bool() == Some(true), | |
| 3080 | 3107 | ignore_checks: input["ignore_checks"].as_bool() == Some(true), | |
| 3108 | + | bypass_rules: input["bypass_rules"].as_bool() == Some(true), | |
| 3081 | 3109 | }; | |
| 3082 | 3110 | let Services { | |
| 3083 | 3111 | identity, | |
| 4801 | 4829 | // The security suite: the security service decides, this gives | |
| 4802 | 4830 | // each answer its public shape. | |
| 4803 | 4831 | Op::Security(op) => crate::security::run(op, services, viewer, input).await, | |
| 4832 | + | Op::Rules(op) => crate::rules::run(op, services, viewer, input).await, | |
| 4804 | 4833 | Op::ReopenSecurityAlert => { | |
| 4805 | 4834 | let changed: Outcome<AlertChange> = call( | |
| 4806 | 4835 | &services.security, |
| 8001 | 8001 | "confidence": null | |
| 8002 | 8002 | }, | |
| 8003 | 8003 | "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." | |
| 8004 | + | }, | |
| 8005 | + | "list_repo_rulesets": { | |
| 8006 | + | "params": { | |
| 8007 | + | "owner": "flagon-io", | |
| 8008 | + | "name": "hello" | |
| 8009 | + | }, | |
| 8010 | + | "query": { | |
| 8011 | + | "include_parents": "true" | |
| 8012 | + | }, | |
| 8013 | + | "response": [ | |
| 8014 | + | { | |
| 8015 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8016 | + | "level": "repository", | |
| 8017 | + | "workspace": "flagon-io", | |
| 8018 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8019 | + | "repository": "flagon-io/hello", | |
| 8020 | + | "name": "Protect main", | |
| 8021 | + | "enforcement": "active", | |
| 8022 | + | "target": "branch", | |
| 8023 | + | "conditions": { | |
| 8024 | + | "ref_name": { | |
| 8025 | + | "include": [ | |
| 8026 | + | "~DEFAULT_BRANCH" | |
| 8027 | + | ], | |
| 8028 | + | "exclude": [] | |
| 8029 | + | } | |
| 8030 | + | }, | |
| 8031 | + | "bypass_actors": [ | |
| 8032 | + | { | |
| 8033 | + | "kind": "role", | |
| 8034 | + | "value": "admin", | |
| 8035 | + | "mode": "pull_requests" | |
| 8036 | + | } | |
| 8037 | + | ], | |
| 8038 | + | "rules": [ | |
| 8039 | + | { | |
| 8040 | + | "type": "deletion", | |
| 8041 | + | "parameters": {}, | |
| 8042 | + | "applies_to": "everyone" | |
| 8043 | + | }, | |
| 8044 | + | { | |
| 8045 | + | "type": "non_fast_forward", | |
| 8046 | + | "parameters": {}, | |
| 8047 | + | "applies_to": "everyone" | |
| 8048 | + | }, | |
| 8049 | + | { | |
| 8050 | + | "type": "pull_request", | |
| 8051 | + | "parameters": { | |
| 8052 | + | "required_approvals": 1, | |
| 8053 | + | "count_agent_approvals": true, | |
| 8054 | + | "dismiss_stale_reviews_on_push": true, | |
| 8055 | + | "require_code_owner_review": true, | |
| 8056 | + | "require_last_push_approval": false, | |
| 8057 | + | "allowed_merge_methods": [] | |
| 8058 | + | }, | |
| 8059 | + | "applies_to": "everyone" | |
| 8060 | + | }, | |
| 8061 | + | { | |
| 8062 | + | "type": "required_status_checks", | |
| 8063 | + | "parameters": { | |
| 8064 | + | "checks": [ | |
| 8065 | + | { | |
| 8066 | + | "context": "CI", | |
| 8067 | + | "integration": "actions" | |
| 8068 | + | } | |
| 8069 | + | ], | |
| 8070 | + | "strict": true, | |
| 8071 | + | "paths": [], | |
| 8072 | + | "allow_bypass_on_merge": false | |
| 8073 | + | }, | |
| 8074 | + | "applies_to": "everyone" | |
| 8075 | + | }, | |
| 8076 | + | { | |
| 8077 | + | "type": "pull_request", | |
| 8078 | + | "parameters": { | |
| 8079 | + | "required_approvals": 1, | |
| 8080 | + | "count_agent_approvals": false, | |
| 8081 | + | "dismiss_stale_reviews_on_push": false, | |
| 8082 | + | "require_code_owner_review": false, | |
| 8083 | + | "require_last_push_approval": false, | |
| 8084 | + | "allowed_merge_methods": [] | |
| 8085 | + | }, | |
| 8086 | + | "applies_to": "agents" | |
| 8087 | + | }, | |
| 8088 | + | { | |
| 8089 | + | "type": "file_path_restriction", | |
| 8090 | + | "parameters": { | |
| 8091 | + | "restricted_file_paths": [ | |
| 8092 | + | ".g1t/workflows/**", | |
| 8093 | + | "CODEOWNERS" | |
| 8094 | + | ] | |
| 8095 | + | }, | |
| 8096 | + | "applies_to": "agents" | |
| 8097 | + | } | |
| 8098 | + | ], | |
| 8099 | + | "created_by": "syntaqx", | |
| 8100 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8101 | + | "updated_by": "syntaqx", | |
| 8102 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8103 | + | }, | |
| 8104 | + | { | |
| 8105 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8106 | + | "level": "workspace", | |
| 8107 | + | "workspace": "flagon-io", | |
| 8108 | + | "name": "Release freeze", | |
| 8109 | + | "enforcement": "evaluate", | |
| 8110 | + | "target": "branch", | |
| 8111 | + | "conditions": { | |
| 8112 | + | "ref_name": { | |
| 8113 | + | "include": [ | |
| 8114 | + | "~DEFAULT_BRANCH", | |
| 8115 | + | "release/**" | |
| 8116 | + | ], | |
| 8117 | + | "exclude": [] | |
| 8118 | + | }, | |
| 8119 | + | "repository": { | |
| 8120 | + | "include": [ | |
| 8121 | + | "~ALL" | |
| 8122 | + | ], | |
| 8123 | + | "exclude": [ | |
| 8124 | + | "sandbox-*" | |
| 8125 | + | ], | |
| 8126 | + | "visibility": "any", | |
| 8127 | + | "topics": [] | |
| 8128 | + | } | |
| 8129 | + | }, | |
| 8130 | + | "bypass_actors": [ | |
| 8131 | + | { | |
| 8132 | + | "kind": "team", | |
| 8133 | + | "value": "flagon-io/release", | |
| 8134 | + | "mode": "always" | |
| 8135 | + | } | |
| 8136 | + | ], | |
| 8137 | + | "rules": [ | |
| 8138 | + | { | |
| 8139 | + | "type": "merge_window", | |
| 8140 | + | "parameters": { | |
| 8141 | + | "time_zone": "-05:00", | |
| 8142 | + | "windows": [ | |
| 8143 | + | { | |
| 8144 | + | "days": [ | |
| 8145 | + | "mon", | |
| 8146 | + | "tue", | |
| 8147 | + | "wed", | |
| 8148 | + | "thu" | |
| 8149 | + | ], | |
| 8150 | + | "start": "09:00", | |
| 8151 | + | "end": "17:00" | |
| 8152 | + | } | |
| 8153 | + | ], | |
| 8154 | + | "freezes": [ | |
| 8155 | + | { | |
| 8156 | + | "start": "2026-12-20T00:00:00Z", | |
| 8157 | + | "end": "2027-01-04T00:00:00Z", | |
| 8158 | + | "reason": "Holidays" | |
| 8159 | + | } | |
| 8160 | + | ], | |
| 8161 | + | "exceptions": [] | |
| 8162 | + | }, | |
| 8163 | + | "applies_to": "everyone" | |
| 8164 | + | }, | |
| 8165 | + | { | |
| 8166 | + | "type": "cost_cap", | |
| 8167 | + | "parameters": { | |
| 8168 | + | "max_usd": 25 | |
| 8169 | + | }, | |
| 8170 | + | "applies_to": "agents" | |
| 8171 | + | } | |
| 8172 | + | ], | |
| 8173 | + | "created_by": "syntaqx", | |
| 8174 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8175 | + | "updated_by": "syntaqx", | |
| 8176 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8177 | + | } | |
| 8178 | + | ], | |
| 8179 | + | "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`." | |
| 8180 | + | }, | |
| 8181 | + | "get_repo_ruleset": { | |
| 8182 | + | "params": { | |
| 8183 | + | "owner": "flagon-io", | |
| 8184 | + | "name": "hello", | |
| 8185 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m" | |
| 8186 | + | }, | |
| 8187 | + | "response": { | |
| 8188 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8189 | + | "level": "repository", | |
| 8190 | + | "workspace": "flagon-io", | |
| 8191 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8192 | + | "repository": "flagon-io/hello", | |
| 8193 | + | "name": "Protect main", | |
| 8194 | + | "enforcement": "active", | |
| 8195 | + | "target": "branch", | |
| 8196 | + | "conditions": { | |
| 8197 | + | "ref_name": { | |
| 8198 | + | "include": [ | |
| 8199 | + | "~DEFAULT_BRANCH" | |
| 8200 | + | ], | |
| 8201 | + | "exclude": [] | |
| 8202 | + | } | |
| 8203 | + | }, | |
| 8204 | + | "bypass_actors": [ | |
| 8205 | + | { | |
| 8206 | + | "kind": "role", | |
| 8207 | + | "value": "admin", | |
| 8208 | + | "mode": "pull_requests" | |
| 8209 | + | } | |
| 8210 | + | ], | |
| 8211 | + | "rules": [ | |
| 8212 | + | { | |
| 8213 | + | "type": "deletion", | |
| 8214 | + | "parameters": {}, | |
| 8215 | + | "applies_to": "everyone" | |
| 8216 | + | }, | |
| 8217 | + | { | |
| 8218 | + | "type": "non_fast_forward", | |
| 8219 | + | "parameters": {}, | |
| 8220 | + | "applies_to": "everyone" | |
| 8221 | + | }, | |
| 8222 | + | { | |
| 8223 | + | "type": "pull_request", | |
| 8224 | + | "parameters": { | |
| 8225 | + | "required_approvals": 1, | |
| 8226 | + | "count_agent_approvals": true, | |
| 8227 | + | "dismiss_stale_reviews_on_push": true, | |
| 8228 | + | "require_code_owner_review": true, | |
| 8229 | + | "require_last_push_approval": false, | |
| 8230 | + | "allowed_merge_methods": [] | |
| 8231 | + | }, | |
| 8232 | + | "applies_to": "everyone" | |
| 8233 | + | }, | |
| 8234 | + | { | |
| 8235 | + | "type": "required_status_checks", | |
| 8236 | + | "parameters": { | |
| 8237 | + | "checks": [ | |
| 8238 | + | { | |
| 8239 | + | "context": "CI", | |
| 8240 | + | "integration": "actions" | |
| 8241 | + | } | |
| 8242 | + | ], | |
| 8243 | + | "strict": true, | |
| 8244 | + | "paths": [], | |
| 8245 | + | "allow_bypass_on_merge": false | |
| 8246 | + | }, | |
| 8247 | + | "applies_to": "everyone" | |
| 8248 | + | }, | |
| 8249 | + | { | |
| 8250 | + | "type": "pull_request", | |
| 8251 | + | "parameters": { | |
| 8252 | + | "required_approvals": 1, | |
| 8253 | + | "count_agent_approvals": false, | |
| 8254 | + | "dismiss_stale_reviews_on_push": false, | |
| 8255 | + | "require_code_owner_review": false, | |
| 8256 | + | "require_last_push_approval": false, | |
| 8257 | + | "allowed_merge_methods": [] | |
| 8258 | + | }, | |
| 8259 | + | "applies_to": "agents" | |
| 8260 | + | }, | |
| 8261 | + | { | |
| 8262 | + | "type": "file_path_restriction", | |
| 8263 | + | "parameters": { | |
| 8264 | + | "restricted_file_paths": [ | |
| 8265 | + | ".g1t/workflows/**", | |
| 8266 | + | "CODEOWNERS" | |
| 8267 | + | ] | |
| 8268 | + | }, | |
| 8269 | + | "applies_to": "agents" | |
| 8270 | + | } | |
| 8271 | + | ], | |
| 8272 | + | "created_by": "syntaqx", | |
| 8273 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8274 | + | "updated_by": "syntaqx", | |
| 8275 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8276 | + | } | |
| 8277 | + | }, | |
| 8278 | + | "create_repo_ruleset": { | |
| 8279 | + | "params": { | |
| 8280 | + | "owner": "flagon-io", | |
| 8281 | + | "name": "hello" | |
| 8282 | + | }, | |
| 8283 | + | "request": { | |
| 8284 | + | "ruleset_name": "Protect main", | |
| 8285 | + | "enforcement": "active", | |
| 8286 | + | "target": "branch", | |
| 8287 | + | "conditions": { | |
| 8288 | + | "ref_name": { | |
| 8289 | + | "include": [ | |
| 8290 | + | "~DEFAULT_BRANCH" | |
| 8291 | + | ], | |
| 8292 | + | "exclude": [] | |
| 8293 | + | } | |
| 8294 | + | }, | |
| 8295 | + | "bypass_actors": [ | |
| 8296 | + | { | |
| 8297 | + | "kind": "role", | |
| 8298 | + | "value": "admin", | |
| 8299 | + | "mode": "pull_requests" | |
| 8300 | + | } | |
| 8301 | + | ], | |
| 8302 | + | "rules": [ | |
| 8303 | + | { | |
| 8304 | + | "type": "deletion", | |
| 8305 | + | "parameters": {}, | |
| 8306 | + | "applies_to": "everyone" | |
| 8307 | + | }, | |
| 8308 | + | { | |
| 8309 | + | "type": "non_fast_forward", | |
| 8310 | + | "parameters": {}, | |
| 8311 | + | "applies_to": "everyone" | |
| 8312 | + | }, | |
| 8313 | + | { | |
| 8314 | + | "type": "pull_request", | |
| 8315 | + | "parameters": { | |
| 8316 | + | "required_approvals": 1, | |
| 8317 | + | "count_agent_approvals": true, | |
| 8318 | + | "dismiss_stale_reviews_on_push": true, | |
| 8319 | + | "require_code_owner_review": true, | |
| 8320 | + | "require_last_push_approval": false, | |
| 8321 | + | "allowed_merge_methods": [] | |
| 8322 | + | }, | |
| 8323 | + | "applies_to": "everyone" | |
| 8324 | + | }, | |
| 8325 | + | { | |
| 8326 | + | "type": "required_status_checks", | |
| 8327 | + | "parameters": { | |
| 8328 | + | "checks": [ | |
| 8329 | + | { | |
| 8330 | + | "context": "CI", | |
| 8331 | + | "integration": "actions" | |
| 8332 | + | } | |
| 8333 | + | ], | |
| 8334 | + | "strict": true, | |
| 8335 | + | "paths": [], | |
| 8336 | + | "allow_bypass_on_merge": false | |
| 8337 | + | }, | |
| 8338 | + | "applies_to": "everyone" | |
| 8339 | + | }, | |
| 8340 | + | { | |
| 8341 | + | "type": "pull_request", | |
| 8342 | + | "parameters": { | |
| 8343 | + | "required_approvals": 1, | |
| 8344 | + | "count_agent_approvals": false, | |
| 8345 | + | "dismiss_stale_reviews_on_push": false, | |
| 8346 | + | "require_code_owner_review": false, | |
| 8347 | + | "require_last_push_approval": false, | |
| 8348 | + | "allowed_merge_methods": [] | |
| 8349 | + | }, | |
| 8350 | + | "applies_to": "agents" | |
| 8351 | + | }, | |
| 8352 | + | { | |
| 8353 | + | "type": "file_path_restriction", | |
| 8354 | + | "parameters": { | |
| 8355 | + | "restricted_file_paths": [ | |
| 8356 | + | ".g1t/workflows/**", | |
| 8357 | + | "CODEOWNERS" | |
| 8358 | + | ] | |
| 8359 | + | }, | |
| 8360 | + | "applies_to": "agents" | |
| 8361 | + | } | |
| 8362 | + | ] | |
| 8363 | + | }, | |
| 8364 | + | "response": { | |
| 8365 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8366 | + | "level": "repository", | |
| 8367 | + | "workspace": "flagon-io", | |
| 8368 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8369 | + | "repository": "flagon-io/hello", | |
| 8370 | + | "name": "Protect main", | |
| 8371 | + | "enforcement": "active", | |
| 8372 | + | "target": "branch", | |
| 8373 | + | "conditions": { | |
| 8374 | + | "ref_name": { | |
| 8375 | + | "include": [ | |
| 8376 | + | "~DEFAULT_BRANCH" | |
| 8377 | + | ], | |
| 8378 | + | "exclude": [] | |
| 8379 | + | } | |
| 8380 | + | }, | |
| 8381 | + | "bypass_actors": [ | |
| 8382 | + | { | |
| 8383 | + | "kind": "role", | |
| 8384 | + | "value": "admin", | |
| 8385 | + | "mode": "pull_requests" | |
| 8386 | + | } | |
| 8387 | + | ], | |
| 8388 | + | "rules": [ | |
| 8389 | + | { | |
| 8390 | + | "type": "deletion", | |
| 8391 | + | "parameters": {}, | |
| 8392 | + | "applies_to": "everyone" | |
| 8393 | + | }, | |
| 8394 | + | { | |
| 8395 | + | "type": "non_fast_forward", | |
| 8396 | + | "parameters": {}, | |
| 8397 | + | "applies_to": "everyone" | |
| 8398 | + | }, | |
| 8399 | + | { | |
| 8400 | + | "type": "pull_request", | |
| 8401 | + | "parameters": { | |
| 8402 | + | "required_approvals": 1, | |
| 8403 | + | "count_agent_approvals": true, | |
| 8404 | + | "dismiss_stale_reviews_on_push": true, | |
| 8405 | + | "require_code_owner_review": true, | |
| 8406 | + | "require_last_push_approval": false, | |
| 8407 | + | "allowed_merge_methods": [] | |
| 8408 | + | }, | |
| 8409 | + | "applies_to": "everyone" | |
| 8410 | + | }, | |
| 8411 | + | { | |
| 8412 | + | "type": "required_status_checks", | |
| 8413 | + | "parameters": { | |
| 8414 | + | "checks": [ | |
| 8415 | + | { | |
| 8416 | + | "context": "CI", | |
| 8417 | + | "integration": "actions" | |
| 8418 | + | } | |
| 8419 | + | ], | |
| 8420 | + | "strict": true, | |
| 8421 | + | "paths": [], | |
| 8422 | + | "allow_bypass_on_merge": false | |
| 8423 | + | }, | |
| 8424 | + | "applies_to": "everyone" | |
| 8425 | + | }, | |
| 8426 | + | { | |
| 8427 | + | "type": "pull_request", | |
| 8428 | + | "parameters": { | |
| 8429 | + | "required_approvals": 1, | |
| 8430 | + | "count_agent_approvals": false, | |
| 8431 | + | "dismiss_stale_reviews_on_push": false, | |
| 8432 | + | "require_code_owner_review": false, | |
| 8433 | + | "require_last_push_approval": false, | |
| 8434 | + | "allowed_merge_methods": [] | |
| 8435 | + | }, | |
| 8436 | + | "applies_to": "agents" | |
| 8437 | + | }, | |
| 8438 | + | { | |
| 8439 | + | "type": "file_path_restriction", | |
| 8440 | + | "parameters": { | |
| 8441 | + | "restricted_file_paths": [ | |
| 8442 | + | ".g1t/workflows/**", | |
| 8443 | + | "CODEOWNERS" | |
| 8444 | + | ] | |
| 8445 | + | }, | |
| 8446 | + | "applies_to": "agents" | |
| 8447 | + | } | |
| 8448 | + | ], | |
| 8449 | + | "created_by": "syntaqx", | |
| 8450 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8451 | + | "updated_by": "syntaqx", | |
| 8452 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8453 | + | }, | |
| 8454 | + | "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." | |
| 8455 | + | }, | |
| 8456 | + | "update_repo_ruleset": { | |
| 8457 | + | "params": { | |
| 8458 | + | "owner": "flagon-io", | |
| 8459 | + | "name": "hello", | |
| 8460 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m" | |
| 8461 | + | }, | |
| 8462 | + | "request": { | |
| 8463 | + | "enforcement": "evaluate" | |
| 8464 | + | }, | |
| 8465 | + | "response": { | |
| 8466 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8467 | + | "level": "repository", | |
| 8468 | + | "workspace": "flagon-io", | |
| 8469 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8470 | + | "repository": "flagon-io/hello", | |
| 8471 | + | "name": "Protect main", | |
| 8472 | + | "enforcement": "evaluate", | |
| 8473 | + | "target": "branch", | |
| 8474 | + | "conditions": { | |
| 8475 | + | "ref_name": { | |
| 8476 | + | "include": [ | |
| 8477 | + | "~DEFAULT_BRANCH" | |
| 8478 | + | ], | |
| 8479 | + | "exclude": [] | |
| 8480 | + | } | |
| 8481 | + | }, | |
| 8482 | + | "bypass_actors": [ | |
| 8483 | + | { | |
| 8484 | + | "kind": "role", | |
| 8485 | + | "value": "admin", | |
| 8486 | + | "mode": "pull_requests" | |
| 8487 | + | } | |
| 8488 | + | ], | |
| 8489 | + | "rules": [ | |
| 8490 | + | { | |
| 8491 | + | "type": "deletion", | |
| 8492 | + | "parameters": {}, | |
| 8493 | + | "applies_to": "everyone" | |
| 8494 | + | }, | |
| 8495 | + | { | |
| 8496 | + | "type": "non_fast_forward", | |
| 8497 | + | "parameters": {}, | |
| 8498 | + | "applies_to": "everyone" | |
| 8499 | + | }, | |
| 8500 | + | { | |
| 8501 | + | "type": "pull_request", | |
| 8502 | + | "parameters": { | |
| 8503 | + | "required_approvals": 1, | |
| 8504 | + | "count_agent_approvals": true, | |
| 8505 | + | "dismiss_stale_reviews_on_push": true, | |
| 8506 | + | "require_code_owner_review": true, | |
| 8507 | + | "require_last_push_approval": false, | |
| 8508 | + | "allowed_merge_methods": [] | |
| 8509 | + | }, | |
| 8510 | + | "applies_to": "everyone" | |
| 8511 | + | }, | |
| 8512 | + | { | |
| 8513 | + | "type": "required_status_checks", | |
| 8514 | + | "parameters": { | |
| 8515 | + | "checks": [ | |
| 8516 | + | { | |
| 8517 | + | "context": "CI", | |
| 8518 | + | "integration": "actions" | |
| 8519 | + | } | |
| 8520 | + | ], | |
| 8521 | + | "strict": true, | |
| 8522 | + | "paths": [], | |
| 8523 | + | "allow_bypass_on_merge": false | |
| 8524 | + | }, | |
| 8525 | + | "applies_to": "everyone" | |
| 8526 | + | }, | |
| 8527 | + | { | |
| 8528 | + | "type": "pull_request", | |
| 8529 | + | "parameters": { | |
| 8530 | + | "required_approvals": 1, | |
| 8531 | + | "count_agent_approvals": false, | |
| 8532 | + | "dismiss_stale_reviews_on_push": false, | |
| 8533 | + | "require_code_owner_review": false, | |
| 8534 | + | "require_last_push_approval": false, | |
| 8535 | + | "allowed_merge_methods": [] | |
| 8536 | + | }, | |
| 8537 | + | "applies_to": "agents" | |
| 8538 | + | }, | |
| 8539 | + | { | |
| 8540 | + | "type": "file_path_restriction", | |
| 8541 | + | "parameters": { | |
| 8542 | + | "restricted_file_paths": [ | |
| 8543 | + | ".g1t/workflows/**", | |
| 8544 | + | "CODEOWNERS" | |
| 8545 | + | ] | |
| 8546 | + | }, | |
| 8547 | + | "applies_to": "agents" | |
| 8548 | + | } | |
| 8549 | + | ], | |
| 8550 | + | "created_by": "syntaqx", | |
| 8551 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8552 | + | "updated_by": "syntaqx", | |
| 8553 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8554 | + | }, | |
| 8555 | + | "notes": "Fields left out stay as they are. `rules` and `bypass_actors`, when given, replace the whole list." | |
| 8556 | + | }, | |
| 8557 | + | "delete_repo_ruleset": { | |
| 8558 | + | "params": { | |
| 8559 | + | "owner": "flagon-io", | |
| 8560 | + | "name": "hello", | |
| 8561 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m" | |
| 8562 | + | }, | |
| 8563 | + | "response": { | |
| 8564 | + | "deleted": true | |
| 8565 | + | } | |
| 8566 | + | }, | |
| 8567 | + | "get_branch_rules": { | |
| 8568 | + | "params": { | |
| 8569 | + | "owner": "flagon-io", | |
| 8570 | + | "name": "hello", | |
| 8571 | + | "branch": "main" | |
| 8572 | + | }, | |
| 8573 | + | "response": { | |
| 8574 | + | "name": "main", | |
| 8575 | + | "target": "branch", | |
| 8576 | + | "default_branch": true, | |
| 8577 | + | "rules": [ | |
| 8578 | + | { | |
| 8579 | + | "type": "deletion", | |
| 8580 | + | "parameters": {}, | |
| 8581 | + | "applies_to": "everyone", | |
| 8582 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8583 | + | "ruleset_name": "Protect main", | |
| 8584 | + | "level": "repository", | |
| 8585 | + | "enforcement": "active" | |
| 8586 | + | }, | |
| 8587 | + | { | |
| 8588 | + | "type": "non_fast_forward", | |
| 8589 | + | "parameters": {}, | |
| 8590 | + | "applies_to": "everyone", | |
| 8591 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8592 | + | "ruleset_name": "Protect main", | |
| 8593 | + | "level": "repository", | |
| 8594 | + | "enforcement": "active" | |
| 8595 | + | }, | |
| 8596 | + | { | |
| 8597 | + | "type": "pull_request", | |
| 8598 | + | "parameters": { | |
| 8599 | + | "required_approvals": 1, | |
| 8600 | + | "count_agent_approvals": true, | |
| 8601 | + | "dismiss_stale_reviews_on_push": true, | |
| 8602 | + | "require_code_owner_review": true, | |
| 8603 | + | "require_last_push_approval": false, | |
| 8604 | + | "allowed_merge_methods": [] | |
| 8605 | + | }, | |
| 8606 | + | "applies_to": "everyone", | |
| 8607 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8608 | + | "ruleset_name": "Protect main", | |
| 8609 | + | "level": "repository", | |
| 8610 | + | "enforcement": "active" | |
| 8611 | + | }, | |
| 8612 | + | { | |
| 8613 | + | "type": "required_status_checks", | |
| 8614 | + | "parameters": { | |
| 8615 | + | "checks": [ | |
| 8616 | + | { | |
| 8617 | + | "context": "CI", | |
| 8618 | + | "integration": "actions" | |
| 8619 | + | } | |
| 8620 | + | ], | |
| 8621 | + | "strict": true, | |
| 8622 | + | "paths": [], | |
| 8623 | + | "allow_bypass_on_merge": false | |
| 8624 | + | }, | |
| 8625 | + | "applies_to": "everyone", | |
| 8626 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8627 | + | "ruleset_name": "Protect main", | |
| 8628 | + | "level": "repository", | |
| 8629 | + | "enforcement": "active" | |
| 8630 | + | }, | |
| 8631 | + | { | |
| 8632 | + | "type": "merge_window", | |
| 8633 | + | "parameters": { | |
| 8634 | + | "time_zone": "-05:00", | |
| 8635 | + | "windows": [ | |
| 8636 | + | { | |
| 8637 | + | "days": [ | |
| 8638 | + | "mon", | |
| 8639 | + | "tue", | |
| 8640 | + | "wed", | |
| 8641 | + | "thu" | |
| 8642 | + | ], | |
| 8643 | + | "start": "09:00", | |
| 8644 | + | "end": "17:00" | |
| 8645 | + | } | |
| 8646 | + | ], | |
| 8647 | + | "freezes": [ | |
| 8648 | + | { | |
| 8649 | + | "start": "2026-12-20T00:00:00Z", | |
| 8650 | + | "end": "2027-01-04T00:00:00Z", | |
| 8651 | + | "reason": "Holidays" | |
| 8652 | + | } | |
| 8653 | + | ], | |
| 8654 | + | "exceptions": [] | |
| 8655 | + | }, | |
| 8656 | + | "applies_to": "everyone", | |
| 8657 | + | "ruleset_id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8658 | + | "ruleset_name": "Release freeze", | |
| 8659 | + | "level": "workspace", | |
| 8660 | + | "enforcement": "evaluate" | |
| 8661 | + | } | |
| 8662 | + | ], | |
| 8663 | + | "rulesets": [ | |
| 8664 | + | { | |
| 8665 | + | "id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8666 | + | "name": "Protect main", | |
| 8667 | + | "level": "repository", | |
| 8668 | + | "enforcement": "active", | |
| 8669 | + | "bypass_actors": [ | |
| 8670 | + | { | |
| 8671 | + | "kind": "role", | |
| 8672 | + | "value": "admin", | |
| 8673 | + | "mode": "pull_requests" | |
| 8674 | + | } | |
| 8675 | + | ] | |
| 8676 | + | }, | |
| 8677 | + | { | |
| 8678 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8679 | + | "name": "Release freeze", | |
| 8680 | + | "level": "workspace", | |
| 8681 | + | "enforcement": "evaluate", | |
| 8682 | + | "bypass_actors": [ | |
| 8683 | + | { | |
| 8684 | + | "kind": "team", | |
| 8685 | + | "value": "flagon-io/release", | |
| 8686 | + | "mode": "always" | |
| 8687 | + | } | |
| 8688 | + | ] | |
| 8689 | + | } | |
| 8690 | + | ] | |
| 8691 | + | }, | |
| 8692 | + | "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." | |
| 8693 | + | }, | |
| 8694 | + | "list_rule_evaluations": { | |
| 8695 | + | "params": { | |
| 8696 | + | "owner": "flagon-io", | |
| 8697 | + | "name": "hello" | |
| 8698 | + | }, | |
| 8699 | + | "query": { | |
| 8700 | + | "problems_only": "true" | |
| 8701 | + | }, | |
| 8702 | + | "response": { | |
| 8703 | + | "evaluations": [ | |
| 8704 | + | { | |
| 8705 | + | "id": "rev_01kq3c4d5e6f7g8h9j0k1m2n3p", | |
| 8706 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 8707 | + | "workspace": "flagon-io", | |
| 8708 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8709 | + | "ruleset_name": "Protect main", | |
| 8710 | + | "enforcement": "active", | |
| 8711 | + | "action": "push", | |
| 8712 | + | "git_ref": "refs/heads/main", | |
| 8713 | + | "actor": "g1t", | |
| 8714 | + | "actor_kind": "agent", | |
| 8715 | + | "verdict": "fail", | |
| 8716 | + | "violations": [ | |
| 8717 | + | { | |
| 8718 | + | "rule": "pull_request", | |
| 8719 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8720 | + | "ruleset_name": "Protect main", | |
| 8721 | + | "enforcement": "active", | |
| 8722 | + | "message": "Changes to main must be made through a pull request.", | |
| 8723 | + | "remedy": "Push a branch, open a pull request into main, and merge it." | |
| 8724 | + | } | |
| 8725 | + | ], | |
| 8726 | + | "number": null, | |
| 8727 | + | "sha": "9f3c2a1b7e6d5c4b3a2918f7e6d5c4b3a2918f7e", | |
| 8728 | + | "repository": "flagon-io/hello", | |
| 8729 | + | "created_at": "2026-10-07T14:02:11.318Z" | |
| 8730 | + | } | |
| 8731 | + | ], | |
| 8732 | + | "next": null, | |
| 8733 | + | "insights": { | |
| 8734 | + | "days": 30, | |
| 8735 | + | "total": 214, | |
| 8736 | + | "passed": 198, | |
| 8737 | + | "blocked": 9, | |
| 8738 | + | "would_block": 5, | |
| 8739 | + | "bypassed": 2, | |
| 8740 | + | "by_ruleset": [ | |
| 8741 | + | { | |
| 8742 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 8743 | + | "ruleset_name": "Protect main", | |
| 8744 | + | "enforcement": "active", | |
| 8745 | + | "total": 120, | |
| 8746 | + | "blocked": 9, | |
| 8747 | + | "would_block": 0, | |
| 8748 | + | "bypassed": 2 | |
| 8749 | + | } | |
| 8750 | + | ], | |
| 8751 | + | "by_rule": [ | |
| 8752 | + | { | |
| 8753 | + | "rule": "pull_request", | |
| 8754 | + | "count": 7 | |
| 8755 | + | }, | |
| 8756 | + | { | |
| 8757 | + | "rule": "non_fast_forward", | |
| 8758 | + | "count": 2 | |
| 8759 | + | } | |
| 8760 | + | ] | |
| 8761 | + | } | |
| 8762 | + | }, | |
| 8763 | + | "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." | |
| 8764 | + | }, | |
| 8765 | + | "list_workspace_rulesets": { | |
| 8766 | + | "params": { | |
| 8767 | + | "workspace": "flagon-io" | |
| 8768 | + | }, | |
| 8769 | + | "response": [ | |
| 8770 | + | { | |
| 8771 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8772 | + | "level": "workspace", | |
| 8773 | + | "workspace": "flagon-io", | |
| 8774 | + | "name": "Release freeze", | |
| 8775 | + | "enforcement": "evaluate", | |
| 8776 | + | "target": "branch", | |
| 8777 | + | "conditions": { | |
| 8778 | + | "ref_name": { | |
| 8779 | + | "include": [ | |
| 8780 | + | "~DEFAULT_BRANCH", | |
| 8781 | + | "release/**" | |
| 8782 | + | ], | |
| 8783 | + | "exclude": [] | |
| 8784 | + | }, | |
| 8785 | + | "repository": { | |
| 8786 | + | "include": [ | |
| 8787 | + | "~ALL" | |
| 8788 | + | ], | |
| 8789 | + | "exclude": [ | |
| 8790 | + | "sandbox-*" | |
| 8791 | + | ], | |
| 8792 | + | "visibility": "any", | |
| 8793 | + | "topics": [] | |
| 8794 | + | } | |
| 8795 | + | }, | |
| 8796 | + | "bypass_actors": [ | |
| 8797 | + | { | |
| 8798 | + | "kind": "team", | |
| 8799 | + | "value": "flagon-io/release", | |
| 8800 | + | "mode": "always" | |
| 8801 | + | } | |
| 8802 | + | ], | |
| 8803 | + | "rules": [ | |
| 8804 | + | { | |
| 8805 | + | "type": "merge_window", | |
| 8806 | + | "parameters": { | |
| 8807 | + | "time_zone": "-05:00", | |
| 8808 | + | "windows": [ | |
| 8809 | + | { | |
| 8810 | + | "days": [ | |
| 8811 | + | "mon", | |
| 8812 | + | "tue", | |
| 8813 | + | "wed", | |
| 8814 | + | "thu" | |
| 8815 | + | ], | |
| 8816 | + | "start": "09:00", | |
| 8817 | + | "end": "17:00" | |
| 8818 | + | } | |
| 8819 | + | ], | |
| 8820 | + | "freezes": [ | |
| 8821 | + | { | |
| 8822 | + | "start": "2026-12-20T00:00:00Z", | |
| 8823 | + | "end": "2027-01-04T00:00:00Z", | |
| 8824 | + | "reason": "Holidays" | |
| 8825 | + | } | |
| 8826 | + | ], | |
| 8827 | + | "exceptions": [] | |
| 8828 | + | }, | |
| 8829 | + | "applies_to": "everyone" | |
| 8830 | + | }, | |
| 8831 | + | { | |
| 8832 | + | "type": "cost_cap", | |
| 8833 | + | "parameters": { | |
| 8834 | + | "max_usd": 25 | |
| 8835 | + | }, | |
| 8836 | + | "applies_to": "agents" | |
| 8837 | + | } | |
| 8838 | + | ], | |
| 8839 | + | "created_by": "syntaqx", | |
| 8840 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8841 | + | "updated_by": "syntaqx", | |
| 8842 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8843 | + | } | |
| 8844 | + | ] | |
| 8845 | + | }, | |
| 8846 | + | "get_workspace_ruleset": { | |
| 8847 | + | "params": { | |
| 8848 | + | "workspace": "flagon-io", | |
| 8849 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n" | |
| 8850 | + | }, | |
| 8851 | + | "response": { | |
| 8852 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8853 | + | "level": "workspace", | |
| 8854 | + | "workspace": "flagon-io", | |
| 8855 | + | "name": "Release freeze", | |
| 8856 | + | "enforcement": "evaluate", | |
| 8857 | + | "target": "branch", | |
| 8858 | + | "conditions": { | |
| 8859 | + | "ref_name": { | |
| 8860 | + | "include": [ | |
| 8861 | + | "~DEFAULT_BRANCH", | |
| 8862 | + | "release/**" | |
| 8863 | + | ], | |
| 8864 | + | "exclude": [] | |
| 8865 | + | }, | |
| 8866 | + | "repository": { | |
| 8867 | + | "include": [ | |
| 8868 | + | "~ALL" | |
| 8869 | + | ], | |
| 8870 | + | "exclude": [ | |
| 8871 | + | "sandbox-*" | |
| 8872 | + | ], | |
| 8873 | + | "visibility": "any", | |
| 8874 | + | "topics": [] | |
| 8875 | + | } | |
| 8876 | + | }, | |
| 8877 | + | "bypass_actors": [ | |
| 8878 | + | { | |
| 8879 | + | "kind": "team", | |
| 8880 | + | "value": "flagon-io/release", | |
| 8881 | + | "mode": "always" | |
| 8882 | + | } | |
| 8883 | + | ], | |
| 8884 | + | "rules": [ | |
| 8885 | + | { | |
| 8886 | + | "type": "merge_window", | |
| 8887 | + | "parameters": { | |
| 8888 | + | "time_zone": "-05:00", | |
| 8889 | + | "windows": [ | |
| 8890 | + | { | |
| 8891 | + | "days": [ | |
| 8892 | + | "mon", | |
| 8893 | + | "tue", | |
| 8894 | + | "wed", | |
| 8895 | + | "thu" | |
| 8896 | + | ], | |
| 8897 | + | "start": "09:00", | |
| 8898 | + | "end": "17:00" | |
| 8899 | + | } | |
| 8900 | + | ], | |
| 8901 | + | "freezes": [ | |
| 8902 | + | { | |
| 8903 | + | "start": "2026-12-20T00:00:00Z", | |
| 8904 | + | "end": "2027-01-04T00:00:00Z", | |
| 8905 | + | "reason": "Holidays" | |
| 8906 | + | } | |
| 8907 | + | ], | |
| 8908 | + | "exceptions": [] | |
| 8909 | + | }, | |
| 8910 | + | "applies_to": "everyone" | |
| 8911 | + | }, | |
| 8912 | + | { | |
| 8913 | + | "type": "cost_cap", | |
| 8914 | + | "parameters": { | |
| 8915 | + | "max_usd": 25 | |
| 8916 | + | }, | |
| 8917 | + | "applies_to": "agents" | |
| 8918 | + | } | |
| 8919 | + | ], | |
| 8920 | + | "created_by": "syntaqx", | |
| 8921 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 8922 | + | "updated_by": "syntaqx", | |
| 8923 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 8924 | + | } | |
| 8925 | + | }, | |
| 8926 | + | "create_workspace_ruleset": { | |
| 8927 | + | "params": { | |
| 8928 | + | "workspace": "flagon-io" | |
| 8929 | + | }, | |
| 8930 | + | "request": { | |
| 8931 | + | "ruleset_name": "Release freeze", | |
| 8932 | + | "enforcement": "evaluate", | |
| 8933 | + | "target": "branch", | |
| 8934 | + | "conditions": { | |
| 8935 | + | "ref_name": { | |
| 8936 | + | "include": [ | |
| 8937 | + | "~DEFAULT_BRANCH", | |
| 8938 | + | "release/**" | |
| 8939 | + | ], | |
| 8940 | + | "exclude": [] | |
| 8941 | + | }, | |
| 8942 | + | "repository": { | |
| 8943 | + | "include": [ | |
| 8944 | + | "~ALL" | |
| 8945 | + | ], | |
| 8946 | + | "exclude": [ | |
| 8947 | + | "sandbox-*" | |
| 8948 | + | ], | |
| 8949 | + | "visibility": "any", | |
| 8950 | + | "topics": [] | |
| 8951 | + | } | |
| 8952 | + | }, | |
| 8953 | + | "bypass_actors": [ | |
| 8954 | + | { | |
| 8955 | + | "kind": "team", | |
| 8956 | + | "value": "flagon-io/release", | |
| 8957 | + | "mode": "always" | |
| 8958 | + | } | |
| 8959 | + | ], | |
| 8960 | + | "rules": [ | |
| 8961 | + | { | |
| 8962 | + | "type": "merge_window", | |
| 8963 | + | "parameters": { | |
| 8964 | + | "time_zone": "-05:00", | |
| 8965 | + | "windows": [ | |
| 8966 | + | { | |
| 8967 | + | "days": [ | |
| 8968 | + | "mon", | |
| 8969 | + | "tue", | |
| 8970 | + | "wed", | |
| 8971 | + | "thu" | |
| 8972 | + | ], | |
| 8973 | + | "start": "09:00", | |
| 8974 | + | "end": "17:00" | |
| 8975 | + | } | |
| 8976 | + | ], | |
| 8977 | + | "freezes": [ | |
| 8978 | + | { | |
| 8979 | + | "start": "2026-12-20T00:00:00Z", | |
| 8980 | + | "end": "2027-01-04T00:00:00Z", | |
| 8981 | + | "reason": "Holidays" | |
| 8982 | + | } | |
| 8983 | + | ], | |
| 8984 | + | "exceptions": [] | |
| 8985 | + | }, | |
| 8986 | + | "applies_to": "everyone" | |
| 8987 | + | }, | |
| 8988 | + | { | |
| 8989 | + | "type": "cost_cap", | |
| 8990 | + | "parameters": { | |
| 8991 | + | "max_usd": 25 | |
| 8992 | + | }, | |
| 8993 | + | "applies_to": "agents" | |
| 8994 | + | } | |
| 8995 | + | ] | |
| 8996 | + | }, | |
| 8997 | + | "response": { | |
| 8998 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 8999 | + | "level": "workspace", | |
| 9000 | + | "workspace": "flagon-io", | |
| 9001 | + | "name": "Release freeze", | |
| 9002 | + | "enforcement": "evaluate", | |
| 9003 | + | "target": "branch", | |
| 9004 | + | "conditions": { | |
| 9005 | + | "ref_name": { | |
| 9006 | + | "include": [ | |
| 9007 | + | "~DEFAULT_BRANCH", | |
| 9008 | + | "release/**" | |
| 9009 | + | ], | |
| 9010 | + | "exclude": [] | |
| 9011 | + | }, | |
| 9012 | + | "repository": { | |
| 9013 | + | "include": [ | |
| 9014 | + | "~ALL" | |
| 9015 | + | ], | |
| 9016 | + | "exclude": [ | |
| 9017 | + | "sandbox-*" | |
| 9018 | + | ], | |
| 9019 | + | "visibility": "any", | |
| 9020 | + | "topics": [] | |
| 9021 | + | } | |
| 9022 | + | }, | |
| 9023 | + | "bypass_actors": [ | |
| 9024 | + | { | |
| 9025 | + | "kind": "team", | |
| 9026 | + | "value": "flagon-io/release", | |
| 9027 | + | "mode": "always" | |
| 9028 | + | } | |
| 9029 | + | ], | |
| 9030 | + | "rules": [ | |
| 9031 | + | { | |
| 9032 | + | "type": "merge_window", | |
| 9033 | + | "parameters": { | |
| 9034 | + | "time_zone": "-05:00", | |
| 9035 | + | "windows": [ | |
| 9036 | + | { | |
| 9037 | + | "days": [ | |
| 9038 | + | "mon", | |
| 9039 | + | "tue", | |
| 9040 | + | "wed", | |
| 9041 | + | "thu" | |
| 9042 | + | ], | |
| 9043 | + | "start": "09:00", | |
| 9044 | + | "end": "17:00" | |
| 9045 | + | } | |
| 9046 | + | ], | |
| 9047 | + | "freezes": [ | |
| 9048 | + | { | |
| 9049 | + | "start": "2026-12-20T00:00:00Z", | |
| 9050 | + | "end": "2027-01-04T00:00:00Z", | |
| 9051 | + | "reason": "Holidays" | |
| 9052 | + | } | |
| 9053 | + | ], | |
| 9054 | + | "exceptions": [] | |
| 9055 | + | }, | |
| 9056 | + | "applies_to": "everyone" | |
| 9057 | + | }, | |
| 9058 | + | { | |
| 9059 | + | "type": "cost_cap", | |
| 9060 | + | "parameters": { | |
| 9061 | + | "max_usd": 25 | |
| 9062 | + | }, | |
| 9063 | + | "applies_to": "agents" | |
| 9064 | + | } | |
| 9065 | + | ], | |
| 9066 | + | "created_by": "syntaqx", | |
| 9067 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 9068 | + | "updated_by": "syntaqx", | |
| 9069 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 9070 | + | }, | |
| 9071 | + | "notes": "`conditions.repository` chooses the repositories it holds in; left out, every repository of the workspace." | |
| 9072 | + | }, | |
| 9073 | + | "update_workspace_ruleset": { | |
| 9074 | + | "params": { | |
| 9075 | + | "workspace": "flagon-io", | |
| 9076 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n" | |
| 9077 | + | }, | |
| 9078 | + | "request": { | |
| 9079 | + | "enforcement": "active" | |
| 9080 | + | }, | |
| 9081 | + | "response": { | |
| 9082 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n", | |
| 9083 | + | "level": "workspace", | |
| 9084 | + | "workspace": "flagon-io", | |
| 9085 | + | "name": "Release freeze", | |
| 9086 | + | "enforcement": "active", | |
| 9087 | + | "target": "branch", | |
| 9088 | + | "conditions": { | |
| 9089 | + | "ref_name": { | |
| 9090 | + | "include": [ | |
| 9091 | + | "~DEFAULT_BRANCH", | |
| 9092 | + | "release/**" | |
| 9093 | + | ], | |
| 9094 | + | "exclude": [] | |
| 9095 | + | }, | |
| 9096 | + | "repository": { | |
| 9097 | + | "include": [ | |
| 9098 | + | "~ALL" | |
| 9099 | + | ], | |
| 9100 | + | "exclude": [ | |
| 9101 | + | "sandbox-*" | |
| 9102 | + | ], | |
| 9103 | + | "visibility": "any", | |
| 9104 | + | "topics": [] | |
| 9105 | + | } | |
| 9106 | + | }, | |
| 9107 | + | "bypass_actors": [ | |
| 9108 | + | { | |
| 9109 | + | "kind": "team", | |
| 9110 | + | "value": "flagon-io/release", | |
| 9111 | + | "mode": "always" | |
| 9112 | + | } | |
| 9113 | + | ], | |
| 9114 | + | "rules": [ | |
| 9115 | + | { | |
| 9116 | + | "type": "merge_window", | |
| 9117 | + | "parameters": { | |
| 9118 | + | "time_zone": "-05:00", | |
| 9119 | + | "windows": [ | |
| 9120 | + | { | |
| 9121 | + | "days": [ | |
| 9122 | + | "mon", | |
| 9123 | + | "tue", | |
| 9124 | + | "wed", | |
| 9125 | + | "thu" | |
| 9126 | + | ], | |
| 9127 | + | "start": "09:00", | |
| 9128 | + | "end": "17:00" | |
| 9129 | + | } | |
| 9130 | + | ], | |
| 9131 | + | "freezes": [ | |
| 9132 | + | { | |
| 9133 | + | "start": "2026-12-20T00:00:00Z", | |
| 9134 | + | "end": "2027-01-04T00:00:00Z", | |
| 9135 | + | "reason": "Holidays" | |
| 9136 | + | } | |
| 9137 | + | ], | |
| 9138 | + | "exceptions": [] | |
| 9139 | + | }, | |
| 9140 | + | "applies_to": "everyone" | |
| 9141 | + | }, | |
| 9142 | + | { | |
| 9143 | + | "type": "cost_cap", | |
| 9144 | + | "parameters": { | |
| 9145 | + | "max_usd": 25 | |
| 9146 | + | }, | |
| 9147 | + | "applies_to": "agents" | |
| 9148 | + | } | |
| 9149 | + | ], | |
| 9150 | + | "created_by": "syntaqx", | |
| 9151 | + | "created_at": "2026-10-07T14:02:11.318Z", | |
| 9152 | + | "updated_by": "syntaqx", | |
| 9153 | + | "updated_at": "2026-10-07T14:02:11.318Z" | |
| 9154 | + | } | |
| 9155 | + | }, | |
| 9156 | + | "delete_workspace_ruleset": { | |
| 9157 | + | "params": { | |
| 9158 | + | "workspace": "flagon-io", | |
| 9159 | + | "id": "rs_01kq3b2c3d4e5f6g7h8j9k0m1n" | |
| 9160 | + | }, | |
| 9161 | + | "response": { | |
| 9162 | + | "deleted": true | |
| 9163 | + | } | |
| 9164 | + | }, | |
| 9165 | + | "list_workspace_rule_evaluations": { | |
| 9166 | + | "params": { | |
| 9167 | + | "workspace": "flagon-io" | |
| 9168 | + | }, | |
| 9169 | + | "response": { | |
| 9170 | + | "evaluations": [ | |
| 9171 | + | { | |
| 9172 | + | "id": "rev_01kq3c4d5e6f7g8h9j0k1m2n3p", | |
| 9173 | + | "repo_id": "rep_01kq0f3a7b8c9d0e1f2g3h4j5k", | |
| 9174 | + | "workspace": "flagon-io", | |
| 9175 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 9176 | + | "ruleset_name": "Protect main", | |
| 9177 | + | "enforcement": "active", | |
| 9178 | + | "action": "push", | |
| 9179 | + | "git_ref": "refs/heads/main", | |
| 9180 | + | "actor": "g1t", | |
| 9181 | + | "actor_kind": "agent", | |
| 9182 | + | "verdict": "fail", | |
| 9183 | + | "violations": [ | |
| 9184 | + | { | |
| 9185 | + | "rule": "pull_request", | |
| 9186 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 9187 | + | "ruleset_name": "Protect main", | |
| 9188 | + | "enforcement": "active", | |
| 9189 | + | "message": "Changes to main must be made through a pull request.", | |
| 9190 | + | "remedy": "Push a branch, open a pull request into main, and merge it." | |
| 9191 | + | } | |
| 9192 | + | ], | |
| 9193 | + | "number": null, | |
| 9194 | + | "sha": "9f3c2a1b7e6d5c4b3a2918f7e6d5c4b3a2918f7e", | |
| 9195 | + | "repository": "flagon-io/hello", | |
| 9196 | + | "created_at": "2026-10-07T14:02:11.318Z" | |
| 9197 | + | } | |
| 9198 | + | ], | |
| 9199 | + | "next": null, | |
| 9200 | + | "insights": { | |
| 9201 | + | "days": 30, | |
| 9202 | + | "total": 214, | |
| 9203 | + | "passed": 198, | |
| 9204 | + | "blocked": 9, | |
| 9205 | + | "would_block": 5, | |
| 9206 | + | "bypassed": 2, | |
| 9207 | + | "by_ruleset": [ | |
| 9208 | + | { | |
| 9209 | + | "ruleset_id": "rs_01kq3a8b9c0d1e2f3g4h5j6k7m", | |
| 9210 | + | "ruleset_name": "Protect main", | |
| 9211 | + | "enforcement": "active", | |
| 9212 | + | "total": 120, | |
| 9213 | + | "blocked": 9, | |
| 9214 | + | "would_block": 0, | |
| 9215 | + | "bypassed": 2 | |
| 9216 | + | } | |
| 9217 | + | ], | |
| 9218 | + | "by_rule": [ | |
| 9219 | + | { | |
| 9220 | + | "rule": "pull_request", | |
| 9221 | + | "count": 7 | |
| 9222 | + | }, | |
| 9223 | + | { | |
| 9224 | + | "rule": "non_fast_forward", | |
| 9225 | + | "count": 2 | |
| 9226 | + | } | |
| 9227 | + | ] | |
| 9228 | + | } | |
| 9229 | + | } | |
| 8004 | 9230 | } | |
| 8005 | 9231 | } |
| 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 { | |
| 234 | 235 | &[], | |
| 235 | 236 | ), | |
| 236 | 237 | route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]), | |
| 238 | + | // Rulesets: a repository's, a workspace's, the rules of one branch, | |
| 239 | + | // and how they judged pushes and merges. | |
| 240 | + | route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]), | |
| 241 | + | route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]), | |
| 242 | + | route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]), | |
| 243 | + | route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]), | |
| 244 | + | route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]), | |
| 245 | + | route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]), | |
| 246 | + | route( | |
| 247 | + | "GET", | |
| 248 | + | "/repos/:owner/:name/rules/evaluations", | |
| 249 | + | Op::Rules(RulesOp::ListRuleEvaluations), | |
| 250 | + | &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")], | |
| 251 | + | ), | |
| 252 | + | route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]), | |
| 253 | + | route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]), | |
| 254 | + | route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]), | |
| 255 | + | route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]), | |
| 256 | + | route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]), | |
| 257 | + | route( | |
| 258 | + | "GET", | |
| 259 | + | "/workspaces/:workspace/rules/evaluations", | |
| 260 | + | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 261 | + | &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")], | |
| 262 | + | ), | |
| 237 | 263 | route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]), | |
| 238 | 264 | route( | |
| 239 | 265 | "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("get", Op::GetWorkspace, "A workspace's details and settings"), | |
| 287 | 295 | a("pin_project", Op::PinProject, "Pin a project, at a position or the end"), | |
| 288 | 296 | a("unpin_project", Op::UnpinProject, "Unpin a project"), | |
| 289 | 297 | a("reorder_pinned_projects", Op::ReorderPinnedProjects, "Put your pins in a new order"), | |
| 298 | + | a("list_rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), "Its rulesets, which hold across its repositories"), | |
| 299 | + | a("get_ruleset", Op::Rules(RulesOp::GetWorkspaceRuleset), "One of its rulesets"), | |
| 300 | + | a("create_ruleset", Op::Rules(RulesOp::CreateWorkspaceRuleset), "Create a ruleset for some or all of its repositories"), | |
| 301 | + | a("update_ruleset", Op::Rules(RulesOp::UpdateWorkspaceRuleset), "Change one of its rulesets"), | |
| 302 | + | a("delete_ruleset", Op::Rules(RulesOp::DeleteWorkspaceRuleset), "Delete one of its rulesets"), | |
| 303 | + | a("rule_evaluations", Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), "How rules judged changes across its repositories"), | |
| 290 | 304 | ], | |
| 291 | 305 | }, | |
| 292 | 306 | Tool { | |
| 392 | 406 | matches!( | |
| 393 | 407 | op, | |
| 394 | 408 | Op::Security(SecurityOp::DeleteCustomPattern | SecurityOp::BypassPushProtection) | |
| 409 | + | | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) | |
| 395 | 410 | | Op::DeleteWorkspace | |
| 396 | 411 | | Op::UpdateWorkspace | |
| 397 | 412 | | Op::DeleteRepo |
| 128 | 128 | { label: 'Labels', slug: 'guides/labels' }, | |
| 129 | 129 | { label: 'Milestones', slug: 'guides/milestones' }, | |
| 130 | 130 | { label: 'The merge queue', slug: 'guides/merge-queue' }, | |
| 131 | + | { label: 'Rules', slug: 'guides/rules' }, | |
| 131 | 132 | { label: 'CODEOWNERS', slug: 'guides/codeowners' }, | |
| 132 | 133 | { label: 'Sessions and why-blame', slug: 'guides/why-blame' }, | |
| 133 | 134 | { label: 'Forks and branches', slug: 'concepts/forks' }, |
| 209 | 209 | a workflow with `name: CI` reports `CI`, with the status context | |
| 210 | 210 | `CI / pull_request` (the workflow's name and the event). | |
| 211 | 211 | ||
| 212 | − | - **Which checks a merge needs** is up to the default branch's | |
| 213 | − | [required status checks](/guides/pull-requests/#required-status-checks), | |
| 214 | − | under **Settings → Branches and merging**. A required check that failed, | |
| 212 | + | - **Which checks a merge needs** is up to the [rules](/guides/rules/) of the branch it merges into, | |
| 213 | + | their [required status checks](/guides/pull-requests/#required-status-checks), | |
| 214 | + | under **Settings → Rules**. A required check that failed, | |
| 215 | 215 | is still running or has not reported holds the merge. Checks that are not | |
| 216 | 216 | required are shown on the pull request and never hold it. | |
| 217 | 217 | - **In a repository that merges through the [merge queue](/guides/merge-queue/)**, |
| 33 | 33 | ||
| 34 | 34 | ## What holds in another branch | |
| 35 | 35 | ||
| 36 | + | A pull request is held to the [rules](/guides/rules/) of the branch it | |
| 37 | + | merges into: every ruleset, the repository's and its workspace's, whose | |
| 38 | + | patterns cover that branch. A ruleset that targets `release/*` holds for | |
| 39 | + | pull requests into `release/1.x` just as one that targets | |
| 40 | + | `~DEFAULT_BRANCH` holds for those into the default branch. | |
| 41 | + | ||
| 36 | 42 | | | Into the default branch | Into another branch | | |
| 37 | 43 | | --- | --- | --- | | |
| 38 | − | | [Required status checks](/guides/pull-requests/#required-status-checks) | Must pass | Not required | | |
| 39 | − | | Required approvals | As the repository asks | Not required | | |
| 40 | − | | Must be up to date | As the repository asks | No | | |
| 41 | − | | [Merge queue](/guides/merge-queue/) | Joins it, when it is on | Never; it merges directly | | |
| 44 | + | | Required checks, approvals, being up to date | As the rules covering it ask | As the rules covering it ask; nothing when none cover it | | |
| 45 | + | | [Merge queue](/guides/merge-queue/) | Joins it, when a rule requires it | Never; it merges directly | | |
| 42 | 46 | | Catching up | Merges the default branch in | Merges its base in | | |
| 43 | 47 | | Its issue | Closes when it merges | Stays open | | |
| 44 | 48 | ||
| 45 | − | The repository's protection settings guard the default branch, so they do | |
| 46 | − | not hold for a pull request into another branch. Merging still needs the | |
| 47 | − | Write role, and the default branch takes the work only through a pull | |
| 48 | − | request into it, which is held to everything above. | |
| 49 | + | Merging still needs the Write role. Under **Settings → Rules**, **What holds | |
| 50 | + | for a branch** shows every rule that covers a branch. | |
| 49 | 51 | ||
| 50 | 52 | ## g1t's agent and other branches | |
| 51 | 53 |
| 241 | 241 | ## Require review from code owners | |
| 242 | 242 | ||
| 243 | 243 | Someone with the Maintain role or higher turns it on under the | |
| 244 | − | repository's **Settings → Branches and merging**, in **Branch protection**: | |
| 245 | − | **Require review from code owners**. It is off by default. | |
| 244 | + | repository's **Settings → Rules**, in a ruleset's **Require a pull request before merging** rule: | |
| 245 | + | **Require review from code owners**. It is off by default. From the API it | |
| 246 | + | is the `pull_request` rule's `require_code_owner_review` (see [rules](/guides/rules/)). | |
| 246 | 247 | ||
| 247 | 248 | With it on, a pull request merges only when every rule that owns a changed | |
| 248 | 249 | file has the approvals its section asks for, from its owners, and no code |
| 91 | 91 | ||
| 92 | 92 | ## Protected branches | |
| 93 | 93 | ||
| 94 | − | A repository can protect its default branch under **Settings → Branches and | |
| 95 | − | merging**. Pushing to it is then refused for everyone, whatever their role, and for agents, and | |
| 96 | − | git says why: | |
| 94 | + | A repository protects its branches and tags with [rulesets](/guides/rules/), | |
| 95 | + | under **Settings → Rules**. A push that breaks a rule is refused for | |
| 96 | + | everyone, whatever their role, and for agents, unless a ruleset lists them | |
| 97 | + | as able to bypass it. Git prints which ruleset and rule refused it, and how | |
| 98 | + | to fix it: | |
| 97 | 99 | ||
| 98 | 100 | ```text | |
| 99 | − | ! [remote rejected] main -> main (main is protected: push a branch and open a pull request) | |
| 101 | + | remote: error: rules for refs/heads/main declined this push: | |
| 102 | + | remote: - Changes to main must be made through a pull request. [ruleset "Protect main", pull_request] | |
| 103 | + | remote: Push a branch, open a pull request into main, and merge it. | |
| 104 | + | ! [remote rejected] main -> main (declined by ruleset "Protect main" (pull_request)) | |
| 100 | 105 | ``` | |
| 101 | 106 | ||
| 102 | − | Changes reach a protected branch only by merging a pull request. The first | |
| 103 | − | push to an empty repository is still allowed. | |
| 104 | − | ||
| 105 | − | The same page sets what a merge needs: the | |
| 106 | − | [required status checks](/guides/pull-requests/#required-status-checks) | |
| 107 | − | and approvals. | |
| 107 | + | With **Require a pull request before merging**, changes reach a branch only | |
| 108 | + | by merging a pull request. Creating the branch, such as the first push to | |
| 109 | + | an empty repository, is still allowed. Rulesets also block force pushes and | |
| 110 | + | deletions, restrict who creates branches and tags, check commit messages, | |
| 111 | + | signatures and the files a push changes, and set what a merge needs. See | |
| 112 | + | [rules](/guides/rules/). | |
| 108 | 113 | ||
| 109 | 114 | ## Branches | |
| 110 | 115 |
| 17 | 17 | ||
| 18 | 18 | ## Turn it on | |
| 19 | 19 | ||
| 20 | − | 1. Open the project's **Settings → Branches and merging**. You need the Maintain | |
| 20 | + | 1. Open the project's **Settings → Rules**. You need the Maintain | |
| 21 | 21 | [role](/guides/access-and-roles/) or higher on its repository. | |
| 22 | − | 2. Turn on **Merge through a queue**. | |
| 23 | − | 3. Save. | |
| 24 | − | 4. Add `merge_group` to the `on:` of every workflow behind a | |
| 22 | + | 2. Open the ruleset that covers the default branch, or create one. | |
| 23 | + | 3. Choose **Add a rule**, then **Require the merge queue**. Set how many | |
| 24 | + | pull requests it tests at once, the smallest batch it starts with and | |
| 25 | + | how long it waits for one, and how long a batch's checks may take. | |
| 26 | + | 4. Save. | |
| 27 | + | 5. Add `merge_group` to the `on:` of every workflow behind a | |
| 25 | 28 | [required status check](/guides/pull-requests/#required-status-checks), | |
| 26 | 29 | so that it runs on the queue's states too | |
| 27 | 30 | ([below](#what-each-state-is-held-to)). | |
| 28 | 31 | ||
| 29 | 32 | From the API, send `merge_queue` to `PATCH /repos/{owner}/{name}/settings` | |
| 30 | − | (or `update_repo_settings`): | |
| 33 | + | (or `update_repo_settings`), which adds the rule to the "Default branch | |
| 34 | + | protection" [ruleset](/guides/rules/): | |
| 31 | 35 | ||
| 32 | 36 | ```sh | |
| 33 | 37 | curl -X PATCH https://api.g1t.sh/repos/acme/web/settings \ | |
| 56 | 60 | ||
| 57 | 61 | ## How entries are tested | |
| 58 | 62 | ||
| 59 | − | g1t takes up to four entries from the front of the queue and tests them all | |
| 63 | + | g1t takes up to four entries (the rule's `max_entries_to_build`) from the front of the queue and tests them all | |
| 60 | 64 | at once, speculatively, each in its own sandbox. Each sandbox builds `main` | |
| 61 | 65 | with that entry and every entry ahead of it merged in, in queue order: | |
| 62 | 66 | ||
| 69 | 73 | ||
| 70 | 74 | If every entry passes, the four can land one after another without being | |
| 71 | 75 | tested again. The next batch starts when nothing is being tested. A batch | |
| 72 | − | that takes longer than 45 minutes is tested again. | |
| 76 | + | that takes longer than 45 minutes (`check_response_timeout_minutes`) is tested again. | |
| 73 | 77 | ||
| 74 | 78 | ### What each state is held to | |
| 75 | 79 |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.