Skip to content

Compare changes

Choose two branches to see what one has that the other does not, then open a pull request for it.

Open a pull request

1 commit

94 files+1834−420/94 viewed
+14−0
10621062 version = "0.1.0"
10631063 dependencies = [
10641064 "base64 0.22.1",
1065+ "ed25519-dalek",
10651066 "futures-util",
10661067 "g1t-blobstore",
10671068 "g1t-contracts",
10681069 "g1t-kit",
1070+ "g1t-rules",
10691071 "g1t-scan",
10701072 "g1t-secrets",
10711073 "miniz_oxide",
10721074 "serde",
10731075 "serde_json",
1076+ "sha2 0.10.9",
10741077 "similar",
10751078 "worker",
10761079 ]
10771080
10781081 [[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]]
10791092 name = "g1t-runner"
10801093 version = "0.1.0"
10811094 dependencies = [
11831196 "futures-util",
11841197 "g1t-contracts",
11851198 "g1t-kit",
1199+ "g1t-rules",
11861200 "getrandom 0.2.17",
11871201 "hex",
11881202 "serde",
+1−0
1212 g1t-blobstore = { path = "crates/blobstore" }
1313 g1t-contracts = { path = "crates/contracts" }
1414 g1t-kit = { path = "crates/kit" }
15+g1t-rules = { path = "crates/rules" }
1516 g1t-scan = { path = "crates/scan" }
1617 g1t-secrets = { path = "crates/secrets" }
1718 serde = { version = "1", features = ["derive"] }
+1−0
1919 #[cfg(test)]
2020 mod responses;
2121 mod rest;
22+mod rules;
2223 mod runners;
2324 mod security;
2425 mod tools;
+21−0
88 use serde_json::{Map, Value, json};
99
1010 use crate::operations::Op;
11+use crate::rules::RulesOp;
1112 use crate::security::SecurityOp;
1213 use crate::rest::{ROUTES, Route};
1314
198199 ],
199200 ),
200201 (
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+ (
201221 "Issues",
202222 "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
203223 &[
534554 Op::RemoveRequestedReviewers => "Remove requested reviewers",
535555 Op::GetCodeownersErrors => "List CODEOWNERS errors",
536556 Op::Security(op) => op.title(),
557+ Op::Rules(op) => op.title(),
537558 }
538559 }
539560
+35−6
2626 };
2727
2828 use crate::alerts::{AlertKind, SecurityAlert};
29+use crate::rules::RulesOp;
2930 use crate::security::SecurityOp;
3031 use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel};
3132 use g1t_contracts::work::*;
263264 GetCodeownersErrors,
264265 /// The security suite's operations: see [`crate::security`].
265266 Security(SecurityOp),
267+ /// Rulesets: rules.rs.
268+ Rules(RulesOp),
266269 }
267270
268271 fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
625628 }
626629
627630 impl Op {
628− pub const ALL: [Op; 205] = [
631+ pub const ALL: [Op; 218] = [
629632 Op::Whoami,
630633 Op::GetWorkspace,
631634 Op::CreateWorkspace,
831834 Op::Security(SecurityOp::GetWorkspaceSettings),
832835 Op::Security(SecurityOp::UpdateWorkspaceSettings),
833836 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),
834850 ];
835851
836852 pub fn by_name(name: &str) -> Option<Op> {
10151031 Op::RemoveRequestedReviewers => "remove_requested_reviewers",
10161032 Op::GetCodeownersErrors => "get_codeowners_errors",
10171033 Op::Security(op) => op.name(),
1034+ Op::Rules(op) => op.name(),
10181035 }
10191036 }
10201037
10991116 "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."
11001117 }
11011118 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."
11031120 }
11041121 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."
11061123 }
11071124 Op::ListCheckNames => {
11081125 "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)."
12181235 "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."
12191236 }
12201237 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)."
12221239 }
12231240 Op::CreatePullRequest => {
12241241 "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."
12381255 "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."
12391256 }
12401257 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."
12421259 }
12431260 Op::ListEvents => {
12441261 "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first."
15091526 "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."
15101527 }
15111528 Op::Security(op) => op.description(),
1529+ Op::Rules(op) => op.description(),
15121530 }
15131531 }
15141532
22092227 },
22102228 "ignore_checks": {
22112229 "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.",
22132235 },
22142236 })),
22152237 &["repo", "number"],
28062828 &["repo"],
28072829 ),
28082830 Op::Security(op) => op.input(),
2831+ Op::Rules(op) => op.input(),
28092832 }
28102833 }
28112834
28312854 | Op::ListCheckNames
28322855 | Op::GetMergeQueue
28332856 | Op::GetCodeownersErrors
2857+ | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::GetRepoRuleset | RulesOp::GetBranchRules)
28342858 )
28352859 }
28362860
28412865
28422866 /// Whether the operation is about one repository, named by `repo`.
28432867 pub(crate) fn needs_repo(self) -> bool {
2868+ if let Op::Rules(op) = self {
2869+ return op.needs_repo();
2870+ }
28442871 if let Op::Security(op) = self {
28452872 return op.needs_repo();
28462873 }
30783105 summary: text(input, "summary"),
30793106 keep_issue_open: input["keep_issue_open"].as_bool() == Some(true),
30803107 ignore_checks: input["ignore_checks"].as_bool() == Some(true),
3108+ bypass_rules: input["bypass_rules"].as_bool() == Some(true),
30813109 };
30823110 let Services {
30833111 identity,
48014829 // The security suite: the security service decides, this gives
48024830 // each answer its public shape.
48034831 Op::Security(op) => crate::security::run(op, services, viewer, input).await,
4832+ Op::Rules(op) => crate::rules::run(op, services, viewer, input).await,
48044833 Op::ReopenSecurityAlert => {
48054834 let changed: Outcome<AlertChange> = call(
48064835 &services.security,
+1226−0
80018001 "confidence": null
80028002 },
80038003 "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+ }
80049230 }
80059231 }
+20−1
77 //! encoded again, so that every field the type has is sent, not only the
88 //! ones an example shows.
99
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};
1111 use g1t_kit::wire::{self, USER_KEYED};
1212 use serde::Serialize;
1313 use serde::de::DeserializeOwned;
1515
1616 use crate::openapi::document;
1717 use crate::operations::Op;
18+use crate::rules::RulesOp;
1819
1920 /// A key as `#[serde(rename_all = "camelCase")]` writes it.
2021 fn camel_key(key: &str) -> String {
9091 Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is),
9192 Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is),
9293 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,
93112 // Built by the API itself, in `snake_case`.
94113 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
95114 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
+26−0
33 use serde_json::{Map, Value};
44
55 use crate::operations::Op;
6+use crate::rules::RulesOp;
67 use crate::security::SecurityOp;
78
89 pub struct Route {
234235 &[],
235236 ),
236237 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+ ),
237263 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
238264 route(
239265 "POST",
+427−0
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(&current.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+}
+20−5
1919 use serde_json::{Map, Value, json};
2020
2121 use crate::operations::Op;
22+use crate::rules::RulesOp;
2223 use crate::security::SecurityOp;
2324
2425 pub struct Action {
5859 Tool {
5960 name: "repository",
6061 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`.",
6263 default_action: None,
6364 actions: &[
6465 a("list", Op::ListRepos, "Repositories you can see"),
6566 a("get", Op::GetRepo, "One repository"),
6667 a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL"),
6768 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"),
7179 a("codeowners", Op::GetCodeownersErrors, "Problems in its CODEOWNERS file, by line"),
7280 a("list_labels", Op::ListLabels, "Labels, with colors and how many issues and pull requests carry each"),
7381 a("create_label", Op::CreateLabel, "Create a label"),
267275 Tool {
268276 name: "workspace",
269277 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.",
271279 default_action: None,
272280 actions: &[
273281 a("get", Op::GetWorkspace, "A workspace's details and settings"),
287295 a("pin_project", Op::PinProject, "Pin a project, at a position or the end"),
288296 a("unpin_project", Op::UnpinProject, "Unpin a project"),
289297 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"),
290304 ],
291305 },
292306 Tool {
392406 matches!(
393407 op,
394408 Op::Security(SecurityOp::DeleteCustomPattern | SecurityOp::BypassPushProtection)
409+ | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset)
395410 | Op::DeleteWorkspace
396411 | Op::UpdateWorkspace
397412 | Op::DeleteRepo
+1−0
128128 { label: 'Labels', slug: 'guides/labels' },
129129 { label: 'Milestones', slug: 'guides/milestones' },
130130 { label: 'The merge queue', slug: 'guides/merge-queue' },
131+ { label: 'Rules', slug: 'guides/rules' },
131132 { label: 'CODEOWNERS', slug: 'guides/codeowners' },
132133 { label: 'Sessions and why-blame', slug: 'guides/why-blame' },
133134 { label: 'Forks and branches', slug: 'concepts/forks' },
+3−3
209209 a workflow with `name: CI` reports `CI`, with the status context
210210 `CI / pull_request` (the workflow's name and the event).
211211
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,
215215 is still running or has not reported holds the merge. Checks that are not
216216 required are shown on the pull request and never hold it.
217217 - **In a repository that merges through the [merge queue](/guides/merge-queue/)**,
+10−8
3333
3434 ## What holds in another branch
3535
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+
3642 | | Into the default branch | Into another branch |
3743 | --- | --- | --- |
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 |
4246 | Catching up | Merges the default branch in | Merges its base in |
4347 | Its issue | Closes when it merges | Stays open |
4448
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.
4951
5052 ## g1t's agent and other branches
5153
+3−2
241241 ## Require review from code owners
242242
243243 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/)).
246247
247248 With it on, a pull request merges only when every rule that owns a changed
248249 file has the approvals its section asks for, from its owners, and no code
+15−10
9191
9292 ## Protected branches
9393
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:
9799
98100 ```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))
100105 ```
101106
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/).
108113
109114 ## Branches
110115
+11−7
1717
1818 ## Turn it on
1919
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
2121 [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
2528 [required status check](/guides/pull-requests/#required-status-checks),
2629 so that it runs on the queue's states too
2730 ([below](#what-each-state-is-held-to)).
2831
2932 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/):
3135
3236 ```sh
3337 curl -X PATCH https://api.g1t.sh/repos/acme/web/settings \
5660
5761 ## How entries are tested
5862
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
6064 at once, speculatively, each in its own sandbox. Each sandbox builds `main`
6165 with that entry and every entry ahead of it merged in, in queue order:
6266
6973
7074 If every entry passes, the four can land one after another without being
7175 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.
7377
7478 ### What each state is held to
7579
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.