Skip to content
1,411 linesCodeBlameRaw
1//! Rulesets: what may happen to a repository's branches and tags, and what
2//! a pull request needs before it merges.
3//!
4//! A **ruleset** belongs to a repository, or to a workspace and through it
5//! to every repository it selects. It targets branches or tags by name
6//! (fnmatch patterns, `~DEFAULT_BRANCH`, `~ALL`), lists the rules that hold
7//! there, and who may bypass them. Its enforcement is `active` (rules hold),
8//! `evaluate` (nothing is refused; what would have been is recorded), or
9//! `disabled`.
10//!
11//! Several rulesets can target the same branch. They stack: every rule of
12//! every active ruleset holds, so the most restrictive wins (the largest
13//! approval count, every required check, the narrowest merge window).
14//!
15//! Each rule can hold for everyone, only for agents' changes, or only for
16//! people's ([`AppliesTo`]). Agents, g1t's own included, obey rules exactly
17//! as people do unless a ruleset lists them as a bypass actor: nobody
18//! bypasses by default.
19//!
20//! The rules engine (`crates/rules`) decides; the work service keeps the
21//! rulesets and every evaluation, and enforces them on merge; the repos
22//! service enforces them on push and on every change to a branch or tag.
23//!
24//! Rulesets travel in the shape the API shows them: `snake_case` fields,
25//! between services too, so that an exported ruleset imports unchanged on
26//! the site, through the API and through MCP. Mirrors
27//! `packages/contracts/src/rules.ts`.
28
29use serde::{Deserialize, Serialize};
30
31use crate::repos::RepoPath;
32pub use crate::work::ConfidenceLevel;
33use crate::{User, Viewer};
34
35/// The repository's default branch, whatever it is called at the time.
36pub const DEFAULT_BRANCH: &str = "~DEFAULT_BRANCH";
37/// Every branch or tag, or every repository.
38pub const ALL: &str = "~ALL";
39/// Rulesets a repository, or a workspace, may have.
40pub const MAX_RULESETS: usize = 75;
41/// Rules in one ruleset.
42pub const MAX_RULES: usize = 50;
43/// Patterns in one list (branches, paths, extensions, repositories).
44pub const MAX_PATTERNS: usize = 100;
45/// The longest pattern, regular expression or name kept.
46pub const MAX_PATTERN_CHARS: usize = 512;
47/// Bypass actors in one ruleset.
48pub const MAX_BYPASS_ACTORS: usize = 50;
49/// Required approvals a pull request rule may ask for.
50pub const MAX_APPROVALS: u32 = 10;
51/// The ruleset made from a repository's branch protection, as it was
52/// before rulesets: its `source`.
53pub const BRANCH_PROTECTION: &str = "branch_protection";
54
55/// Whether a ruleset's rules hold.
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum Enforcement {
59 /// Its rules hold, and what breaks them is refused.
60 #[default]
61 Active,
62 /// A dry run: nothing is refused, and every push or merge it would
63 /// have refused is recorded, for its insights.
64 Evaluate,
65 /// Kept, but not evaluated at all.
66 Disabled,
67}
68
69impl Enforcement {
70 pub fn as_str(self) -> &'static str {
71 match self {
72 Enforcement::Active => "active",
73 Enforcement::Evaluate => "evaluate",
74 Enforcement::Disabled => "disabled",
75 }
76 }
77}
78
79/// What a ruleset's name conditions match.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum Target {
83 #[default]
84 Branch,
85 Tag,
86}
87
88impl Target {
89 pub fn as_str(self) -> &'static str {
90 match self {
91 Target::Branch => "branch",
92 Target::Tag => "tag",
93 }
94 }
95
96 /// The full ref of `name`: `refs/heads/<name>` or `refs/tags/<name>`.
97 pub fn full_ref(self, name: &str) -> String {
98 match self {
99 Target::Branch => format!("refs/heads/{name}"),
100 Target::Tag => format!("refs/tags/{name}"),
101 }
102 }
103
104 /// The target and short name of a full ref, if it is a branch or a tag.
105 pub fn of_ref(git_ref: &str) -> Option<(Target, &str)> {
106 if let Some(name) = git_ref.strip_prefix("refs/heads/") {
107 return Some((Target::Branch, name));
108 }
109 git_ref.strip_prefix("refs/tags/").map(|name| (Target::Tag, name))
110 }
111}
112
113/// Whose ruleset it is.
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum Level {
117 #[default]
118 Repository,
119 Workspace,
120}
121
122impl Level {
123 pub fn as_str(self) -> &'static str {
124 match self {
125 Level::Repository => "repository",
126 Level::Workspace => "workspace",
127 }
128 }
129}
130
131/// Which branches or tags a ruleset holds for, by name. A name matches when
132/// it matches an `include` pattern and no `exclude` pattern. Patterns are
133/// fnmatch: `*` matches within one path segment, `**` across them, `?` one
134/// character, `[abc]` one of a set. `~DEFAULT_BRANCH` is the default
135/// branch, `~ALL` everything. An empty `include` matches nothing.
136#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(default)]
138pub struct RefCondition {
139 pub include: Vec<String>,
140 pub exclude: Vec<String>,
141}
142
143/// Which visibility of repository a workspace ruleset selects.
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum VisibilityCondition {
147 #[default]
148 Any,
149 Public,
150 Private,
151}
152
153/// Which of a workspace's repositories its ruleset holds in: those whose
154/// name matches an `include` pattern (fnmatch, or `~ALL`) and no `exclude`
155/// one, of the `visibility` chosen, and, when `topics` is not empty,
156/// carrying at least one of them.
157#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(default)]
159pub struct RepositoryCondition {
160 pub include: Vec<String>,
161 pub exclude: Vec<String>,
162 pub visibility: VisibilityCondition,
163 pub topics: Vec<String>,
164}
165
166impl Default for RepositoryCondition {
167 fn default() -> Self {
168 RepositoryCondition {
169 include: vec![ALL.to_owned()],
170 exclude: Vec::new(),
171 visibility: VisibilityCondition::Any,
172 topics: Vec::new(),
173 }
174 }
175}
176
177/// Where a ruleset holds. `repository` is a workspace ruleset's only.
178#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(default)]
180pub struct Conditions {
181 pub ref_name: RefCondition,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub repository: Option<RepositoryCondition>,
184}
185
186/// Who a bypass actor is.
187#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
188#[serde(rename_all = "snake_case")]
189pub enum ActorKind {
190 /// Everyone with at least this repository role (`value`: `write`,
191 /// `maintain` or `admin`), or the workspace's owners (`owner`).
192 Role,
193 /// The people of a team (`value`: its slug, or `workspace/slug`), its
194 /// child teams' people included.
195 Team,
196 /// One person, by username.
197 User,
198 /// An access token, by its id; `value` `workspace` is any of the
199 /// workspace's own tokens.
200 Token,
201 /// g1t: its agent at work in a sandbox, and the platform acting on its
202 /// own (the merge queue, security updates). Never a bypass actor
203 /// unless listed.
204 G1t,
205}
206
207/// When a bypass actor may bypass.
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
209#[serde(rename_all = "snake_case")]
210pub enum BypassMode {
211 /// Always: pushes and merges alike.
212 #[default]
213 Always,
214 /// Only when merging a pull request; their pushes obey the rules.
215 PullRequests,
216}
217
218impl BypassMode {
219 pub fn as_str(self) -> &'static str {
220 match self {
221 BypassMode::Always => "always",
222 BypassMode::PullRequests => "pull_requests",
223 }
224 }
225}
226
227/// Someone a ruleset does not hold for.
228#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
229pub struct BypassActor {
230 pub kind: ActorKind,
231 /// Who, as [`ActorKind`] says. Empty for `g1t`.
232 #[serde(default)]
233 pub value: String,
234 #[serde(default)]
235 pub mode: BypassMode,
236}
237
238/// Whose changes a rule holds for.
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
240#[serde(rename_all = "snake_case")]
241pub enum AppliesTo {
242 #[default]
243 Everyone,
244 /// Only agents' changes: a push by an agent, a pull request an agent
245 /// made (g1t's or another's through a token).
246 Agents,
247 /// Only people's changes.
248 People,
249}
250
251impl AppliesTo {
252 pub fn as_str(self) -> &'static str {
253 match self {
254 AppliesTo::Everyone => "everyone",
255 AppliesTo::Agents => "agents",
256 AppliesTo::People => "people",
257 }
258 }
259
260 /// Whether it holds for a change by an agent (`agent`) or a person.
261 pub fn covers(self, agent: bool) -> bool {
262 match self {
263 AppliesTo::Everyone => true,
264 AppliesTo::Agents => agent,
265 AppliesTo::People => !agent,
266 }
267 }
268}
269
270/// A rule with no parameters.
271#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
272pub struct NoParameters {}
273
274/// How a pull request is merged.
275#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
276#[serde(rename_all = "snake_case")]
277pub enum MergeMethod {
278 /// The branch lands as it is, its commits included: how g1t merges.
279 Merge,
280 Squash,
281 Rebase,
282}
283
284impl MergeMethod {
285 pub fn as_str(self) -> &'static str {
286 match self {
287 MergeMethod::Merge => "merge",
288 MergeMethod::Squash => "squash",
289 MergeMethod::Rebase => "rebase",
290 }
291 }
292}
293
294/// `pull_request`: changes reach the branch only by merging a pull request,
295/// and the pull request needs what this says first.
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(default)]
298pub struct PullRequestRule {
299 /// Approving reviews needed. A reviewer who has since asked for
300 /// changes blocks it; nobody approves their own.
301 pub required_approvals: u32,
302 /// Whether an agent's approval (g1t's reviewer) counts towards
303 /// `required_approvals`. Off means only people's approvals count.
304 pub count_agent_approvals: bool,
305 /// Approvals given before the latest push no longer count.
306 pub dismiss_stale_reviews_on_push: bool,
307 /// The code owners of every file it changes must approve.
308 pub require_code_owner_review: bool,
309 /// Someone other than whoever pushed last must approve after that push.
310 pub require_last_push_approval: bool,
311 /// The ways it may be merged. Empty allows every one.
312 pub allowed_merge_methods: Vec<MergeMethod>,
313 /// Pull requests need what this rule says, but pushes straight to the
314 /// branch are still allowed. Off (the default) refuses them. Only the
315 /// ruleset made from branch protection that did not require pull
316 /// requests turns it on.
317 #[serde(skip_serializing_if = "std::ops::Not::not")]
318 pub allow_direct_pushes: bool,
319}
320
321impl Default for PullRequestRule {
322 fn default() -> Self {
323 PullRequestRule {
324 required_approvals: 0,
325 count_agent_approvals: true,
326 dismiss_stale_reviews_on_push: false,
327 require_code_owner_review: false,
328 require_last_push_approval: false,
329 allowed_merge_methods: Vec::new(),
330 allow_direct_pushes: false,
331 }
332 }
333}
334
335/// Where a required check's status must come from.
336#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum Integration {
339 /// Workflow runs (`.g1t/workflows`).
340 Actions,
341 /// Deployments: `g1t / deploy`.
342 Deployments,
343 /// The security suite: code scanning and dependency review.
344 Security,
345 /// g1t itself, such as code owners.
346 G1t,
347}
348
349impl Integration {
350 pub fn as_str(self) -> &'static str {
351 match self {
352 Integration::Actions => "actions",
353 Integration::Deployments => "deployments",
354 Integration::Security => "security",
355 Integration::G1t => "g1t",
356 }
357 }
358
359 pub fn parse(text: &str) -> Option<Integration> {
360 match text {
361 "actions" => Some(Integration::Actions),
362 "deployments" => Some(Integration::Deployments),
363 "security" => Some(Integration::Security),
364 "g1t" => Some(Integration::G1t),
365 _ => None,
366 }
367 }
368}
369
370/// One check that must pass: a workflow's name (`CI`) or another status's
371/// context, and, if set, the integration that must have reported it.
372#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
373pub struct RequiredCheck {
374 pub context: String,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub integration: Option<Integration>,
377}
378
379/// `required_status_checks`: these checks must pass on a pull request's
380/// head before it merges.
381#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(default)]
383pub struct StatusChecksRule {
384 pub checks: Vec<RequiredCheck>,
385 /// The pull request must contain the branch's latest commits, so that
386 /// what merges is what was checked.
387 pub strict: bool,
388 /// Required only when the pull request changes a file matching one of
389 /// these patterns. Empty: always.
390 pub paths: Vec<String>,
391 /// Someone who may merge can merge past checks that have not passed,
392 /// saying so as they merge.
393 pub allow_bypass_on_merge: bool,
394}
395
396impl Default for StatusChecksRule {
397 fn default() -> Self {
398 StatusChecksRule {
399 checks: Vec::new(),
400 strict: false,
401 paths: Vec::new(),
402 allow_bypass_on_merge: false,
403 }
404 }
405}
406
407/// `merge_queue`: merging joins the queue, which tests each pull request
408/// together with those ahead of it. The queue lands on the default branch.
409#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(default)]
411pub struct MergeQueueRule {
412 pub merge_method: MergeMethod,
413 /// Entries tested at once.
414 pub max_entries_to_build: u32,
415 /// Entries a batch waits for before it starts, unless the oldest has
416 /// waited `min_entries_wait_minutes`.
417 pub min_entries_to_merge: u32,
418 pub min_entries_wait_minutes: u32,
419 /// How long a batch's checks may take before it is tested again.
420 pub check_response_timeout_minutes: u32,
421}
422
423impl Default for MergeQueueRule {
424 fn default() -> Self {
425 MergeQueueRule {
426 merge_method: MergeMethod::Merge,
427 max_entries_to_build: 4,
428 min_entries_to_merge: 1,
429 min_entries_wait_minutes: 0,
430 check_response_timeout_minutes: 45,
431 }
432 }
433}
434
435/// `required_deployments`: a pull request's head must have deployed
436/// successfully to these environments: `preview` (its preview), or a
437/// project's slug for a repository with several.
438#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
439#[serde(default)]
440pub struct DeploymentsRule {
441 pub environments: Vec<String>,
442}
443
444/// How a pattern rule compares.
445#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
446#[serde(rename_all = "snake_case")]
447pub enum PatternOperator {
448 #[default]
449 StartsWith,
450 EndsWith,
451 Contains,
452 /// A regular expression, run by a linear-time engine.
453 Regex,
454}
455
456/// A rule about text: a commit message, an author's or committer's email
457/// address, a branch's or tag's name. The text must match the pattern, or
458/// with `negate`, must not.
459#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
460#[serde(default)]
461pub struct PatternRule {
462 /// What people are told the rule is, such as "Conventional commits".
463 pub name: String,
464 pub operator: PatternOperator,
465 pub pattern: String,
466 pub negate: bool,
467}
468
469/// `file_path_restriction`: pushes and pull requests may not change files
470/// matching these patterns (fnmatch, `**` across directories).
471#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(default)]
473pub struct FilePathRule {
474 pub restricted_file_paths: Vec<String>,
475}
476
477/// `file_extension_restriction`: files with these extensions (`.exe`,
478/// `.zip`) may not be added or changed.
479#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
480#[serde(default)]
481pub struct FileExtensionRule {
482 pub restricted_file_extensions: Vec<String>,
483}
484
485/// `max_file_size`: no file larger than this, in megabytes (1 to 100).
486#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487#[serde(default)]
488pub struct MaxFileSizeRule {
489 pub max_file_size_mb: u32,
490}
491
492impl Default for MaxFileSizeRule {
493 fn default() -> Self {
494 MaxFileSizeRule { max_file_size_mb: 10 }
495 }
496}
497
498/// `max_file_path_length`: no path longer than this many characters.
499#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(default)]
501pub struct MaxFilePathLengthRule {
502 pub max_file_path_length: u32,
503}
504
505impl Default for MaxFilePathLengthRule {
506 fn default() -> Self {
507 MaxFilePathLengthRule { max_file_path_length: 255 }
508 }
509}
510
511/// `max_files_changed`: a push's commits, each, and a pull request as a
512/// whole, change at most this many files.
513#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
514#[serde(default)]
515pub struct MaxFilesChangedRule {
516 pub max_files: u32,
517}
518
519impl Default for MaxFilesChangedRule {
520 fn default() -> Self {
521 MaxFilesChangedRule { max_files: 100 }
522 }
523}
524
525/// `confidence_threshold`: an agent's change g1t rates below `minimum`
526/// needs approvals from people before it merges.
527#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(default)]
529pub struct ConfidenceRule {
530 pub minimum: ConfidenceLevel,
531 pub required_approvals: u32,
532}
533
534impl Default for ConfidenceRule {
535 fn default() -> Self {
536 ConfidenceRule { minimum: ConfidenceLevel::Medium, required_approvals: 1 }
537 }
538}
539
540/// `cost_cap`: once agents have spent more than this on a pull request, in
541/// US dollars, it neither merges nor is sent back to its agent until a
542/// person approves it after that.
543#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
544#[serde(default)]
545pub struct CostCapRule {
546 pub max_usd: f64,
547}
548
549impl Default for CostCapRule {
550 fn default() -> Self {
551 CostCapRule { max_usd: 10.0 }
552 }
553}
554
555/// `path_review`: a pull request that changes a file matching `paths`
556/// needs `required_approvals` from people, from `team` when one is named
557/// (its slug, or `workspace/slug`).
558#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(default)]
560pub struct PathReviewRule {
561 pub paths: Vec<String>,
562 pub required_approvals: u32,
563 #[serde(skip_serializing_if = "Option::is_none")]
564 pub team: Option<String>,
565}
566
567impl Default for PathReviewRule {
568 fn default() -> Self {
569 PathReviewRule { paths: Vec::new(), required_approvals: 1, team: None }
570 }
571}
572
573/// A day of the week.
574#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
575#[serde(rename_all = "snake_case")]
576pub enum Weekday {
577 Mon,
578 Tue,
579 Wed,
580 Thu,
581 Fri,
582 Sat,
583 Sun,
584}
585
586impl Weekday {
587 pub const ALL: [Weekday; 7] = [
588 Weekday::Mon,
589 Weekday::Tue,
590 Weekday::Wed,
591 Weekday::Thu,
592 Weekday::Fri,
593 Weekday::Sat,
594 Weekday::Sun,
595 ];
596
597 pub fn as_str(self) -> &'static str {
598 match self {
599 Weekday::Mon => "mon",
600 Weekday::Tue => "tue",
601 Weekday::Wed => "wed",
602 Weekday::Thu => "thu",
603 Weekday::Fri => "fri",
604 Weekday::Sat => "sat",
605 Weekday::Sun => "sun",
606 }
607 }
608}
609
610/// Hours on some days of the week when merging is allowed, `HH:MM` to
611/// `HH:MM` in the rule's time zone. An `end` before `start` runs past
612/// midnight.
613#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
614pub struct WeeklyWindow {
615 pub days: Vec<Weekday>,
616 pub start: String,
617 pub end: String,
618}
619
620/// A stretch of time, RFC 3339 UTC. With no `end`, it lasts until removed:
621/// an incident freeze.
622#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
623pub struct Period {
624 pub start: String,
625 #[serde(default)]
626 pub end: Option<String>,
627 #[serde(default)]
628 pub reason: String,
629}
630
631/// `merge_window`: when pull requests may merge into the branch. Outside
632/// every `windows` entry (when there are any), or during a `freezes` one,
633/// merging waits, unless an `exceptions` entry covers the moment.
634#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(default)]
636pub struct MergeWindowRule {
637 /// A fixed offset from UTC, `+02:00` or `-05:00`; `UTC` or empty is UTC.
638 /// Daylight saving time is not applied.
639 pub time_zone: String,
640 pub windows: Vec<WeeklyWindow>,
641 pub freezes: Vec<Period>,
642 pub exceptions: Vec<Period>,
643}
644
645impl Default for MergeWindowRule {
646 fn default() -> Self {
647 MergeWindowRule {
648 time_zone: "UTC".to_owned(),
649 windows: Vec::new(),
650 freezes: Vec::new(),
651 exceptions: Vec::new(),
652 }
653 }
654}
655
656/// `agent_auto_merge`: whether g1t lands an agent's ready pull request into
657/// the branch without a person pressing merge, and how sure of it g1t must
658/// be. The repository's auto-merge setting must be on as well.
659#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
660#[serde(default)]
661pub struct AgentAutoMergeRule {
662 pub allowed: bool,
663 #[serde(skip_serializing_if = "Option::is_none")]
664 pub minimum_confidence: Option<ConfidenceLevel>,
665}
666
667impl Default for AgentAutoMergeRule {
668 fn default() -> Self {
669 AgentAutoMergeRule { allowed: true, minimum_confidence: None }
670 }
671}
672
673/// One rule and its parameters, tagged by `type`.
674#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
675#[serde(tag = "type", content = "parameters", rename_all = "snake_case")]
676pub enum Rule {
677 /// Only bypass actors may create a matching branch or tag.
678 Creation(NoParameters),
679 /// Only bypass actors may push to (move) a matching branch or tag.
680 Update(NoParameters),
681 /// Only bypass actors may delete a matching branch or tag.
682 Deletion(NoParameters),
683 /// Nobody force pushes: a push must only add to its history.
684 NonFastForward(NoParameters),
685 /// No merge commits: history stays a straight line.
686 RequiredLinearHistory(NoParameters),
687 /// Every commit carries a signature g1t verifies.
688 RequiredSignatures(NoParameters),
689 PullRequest(PullRequestRule),
690 RequiredStatusChecks(StatusChecksRule),
691 MergeQueue(MergeQueueRule),
692 RequiredDeployments(DeploymentsRule),
693 CommitMessagePattern(PatternRule),
694 CommitAuthorEmailPattern(PatternRule),
695 CommitterEmailPattern(PatternRule),
696 BranchNamePattern(PatternRule),
697 TagNamePattern(PatternRule),
698 FilePathRestriction(FilePathRule),
699 FileExtensionRestriction(FileExtensionRule),
700 MaxFileSize(MaxFileSizeRule),
701 MaxFilePathLength(MaxFilePathLengthRule),
702 MaxFilesChanged(MaxFilesChangedRule),
703 /// Pushes that add a secret are refused, whatever the repository's own
704 /// push protection setting says.
705 SecretScanning(NoParameters),
706 ConfidenceThreshold(ConfidenceRule),
707 CostCap(CostCapRule),
708 PathReview(PathReviewRule),
709 MergeWindow(MergeWindowRule),
710 AgentAutoMerge(AgentAutoMergeRule),
711}
712
713impl Rule {
714 /// Its `type`, as the API names it.
715 pub fn kind(&self) -> &'static str {
716 match self {
717 Rule::Creation(_) => "creation",
718 Rule::Update(_) => "update",
719 Rule::Deletion(_) => "deletion",
720 Rule::NonFastForward(_) => "non_fast_forward",
721 Rule::RequiredLinearHistory(_) => "required_linear_history",
722 Rule::RequiredSignatures(_) => "required_signatures",
723 Rule::PullRequest(_) => "pull_request",
724 Rule::RequiredStatusChecks(_) => "required_status_checks",
725 Rule::MergeQueue(_) => "merge_queue",
726 Rule::RequiredDeployments(_) => "required_deployments",
727 Rule::CommitMessagePattern(_) => "commit_message_pattern",
728 Rule::CommitAuthorEmailPattern(_) => "commit_author_email_pattern",
729 Rule::CommitterEmailPattern(_) => "committer_email_pattern",
730 Rule::BranchNamePattern(_) => "branch_name_pattern",
731 Rule::TagNamePattern(_) => "tag_name_pattern",
732 Rule::FilePathRestriction(_) => "file_path_restriction",
733 Rule::FileExtensionRestriction(_) => "file_extension_restriction",
734 Rule::MaxFileSize(_) => "max_file_size",
735 Rule::MaxFilePathLength(_) => "max_file_path_length",
736 Rule::MaxFilesChanged(_) => "max_files_changed",
737 Rule::SecretScanning(_) => "secret_scanning",
738 Rule::ConfidenceThreshold(_) => "confidence_threshold",
739 Rule::CostCap(_) => "cost_cap",
740 Rule::PathReview(_) => "path_review",
741 Rule::MergeWindow(_) => "merge_window",
742 Rule::AgentAutoMerge(_) => "agent_auto_merge",
743 }
744 }
745
746 /// How people are shown it.
747 pub fn label(&self) -> &'static str {
748 match self {
749 Rule::Creation(_) => "Restrict creations",
750 Rule::Update(_) => "Restrict updates",
751 Rule::Deletion(_) => "Restrict deletions",
752 Rule::NonFastForward(_) => "Block force pushes",
753 Rule::RequiredLinearHistory(_) => "Require linear history",
754 Rule::RequiredSignatures(_) => "Require signed commits",
755 Rule::PullRequest(_) => "Require a pull request before merging",
756 Rule::RequiredStatusChecks(_) => "Require status checks to pass",
757 Rule::MergeQueue(_) => "Require the merge queue",
758 Rule::RequiredDeployments(_) => "Require deployments to succeed",
759 Rule::CommitMessagePattern(_) => "Commit message pattern",
760 Rule::CommitAuthorEmailPattern(_) => "Commit author email pattern",
761 Rule::CommitterEmailPattern(_) => "Committer email pattern",
762 Rule::BranchNamePattern(_) => "Branch name pattern",
763 Rule::TagNamePattern(_) => "Tag name pattern",
764 Rule::FilePathRestriction(_) => "Restrict file paths",
765 Rule::FileExtensionRestriction(_) => "Restrict file extensions",
766 Rule::MaxFileSize(_) => "Restrict file size",
767 Rule::MaxFilePathLength(_) => "Restrict file path length",
768 Rule::MaxFilesChanged(_) => "Restrict files changed",
769 Rule::SecretScanning(_) => "Block pushes that add secrets",
770 Rule::ConfidenceThreshold(_) => "Confidence threshold",
771 Rule::CostCap(_) => "Cost cap",
772 Rule::PathReview(_) => "Review for sensitive paths",
773 Rule::MergeWindow(_) => "Merge window",
774 Rule::AgentAutoMerge(_) => "Agent auto-merge",
775 }
776 }
777
778 /// Whether the rule is about pushes: what a push may do or bring.
779 /// Pull request rules hold on merge.
780 pub fn on_push(&self) -> bool {
781 matches!(
782 self,
783 Rule::Creation(_)
784 | Rule::Update(_)
785 | Rule::Deletion(_)
786 | Rule::NonFastForward(_)
787 | Rule::RequiredLinearHistory(_)
788 | Rule::RequiredSignatures(_)
789 | Rule::PullRequest(_)
790 | Rule::MergeQueue(_)
791 | Rule::CommitMessagePattern(_)
792 | Rule::CommitAuthorEmailPattern(_)
793 | Rule::CommitterEmailPattern(_)
794 | Rule::BranchNamePattern(_)
795 | Rule::TagNamePattern(_)
796 | Rule::FilePathRestriction(_)
797 | Rule::FileExtensionRestriction(_)
798 | Rule::MaxFileSize(_)
799 | Rule::MaxFilePathLength(_)
800 | Rule::MaxFilesChanged(_)
801 | Rule::SecretScanning(_)
802 )
803 }
804
805 /// Whether it says anything only tags can break (or only branches).
806 pub fn for_branches_only(&self) -> bool {
807 matches!(
808 self,
809 Rule::PullRequest(_)
810 | Rule::RequiredStatusChecks(_)
811 | Rule::MergeQueue(_)
812 | Rule::RequiredDeployments(_)
813 | Rule::BranchNamePattern(_)
814 | Rule::ConfidenceThreshold(_)
815 | Rule::CostCap(_)
816 | Rule::PathReview(_)
817 | Rule::MergeWindow(_)
818 | Rule::AgentAutoMerge(_)
819 )
820 }
821
822 pub fn for_tags_only(&self) -> bool {
823 matches!(self, Rule::TagNamePattern(_))
824 }
825}
826
827/// One rule of a ruleset, and whose changes it holds for. `parameters`
828/// may be left out, or left partly out: what is missing takes its default.
829#[derive(Clone, Debug, PartialEq, Serialize)]
830pub struct RuleEntry {
831 #[serde(flatten)]
832 pub rule: Rule,
833 pub applies_to: AppliesTo,
834}
835
836impl<'de> Deserialize<'de> for RuleEntry {
837 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
838 #[derive(Deserialize)]
839 struct Written {
840 #[serde(rename = "type")]
841 kind: String,
842 #[serde(default)]
843 parameters: serde_json::Value,
844 #[serde(default)]
845 applies_to: AppliesTo,
846 }
847 let written = Written::deserialize(deserializer)?;
848 let parameters = match written.parameters {
849 serde_json::Value::Null => serde_json::json!({}),
850 other => other,
851 };
852 let rule = serde_json::from_value(serde_json::json!({ "type": written.kind, "parameters": parameters }))
853 .map_err(serde::de::Error::custom)?;
854 Ok(RuleEntry { rule, applies_to: written.applies_to })
855 }
856}
857
858impl RuleEntry {
859 pub fn everyone(rule: Rule) -> RuleEntry {
860 RuleEntry { rule, applies_to: AppliesTo::Everyone }
861 }
862}
863
864/// What a ruleset says, as it is created, changed, exported and imported.
865#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
866#[serde(default)]
867pub struct RulesetSpec {
868 pub name: String,
869 pub enforcement: Enforcement,
870 pub target: Target,
871 pub conditions: Conditions,
872 pub bypass_actors: Vec<BypassActor>,
873 pub rules: Vec<RuleEntry>,
874}
875
876/// A ruleset, as it is kept.
877#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
878pub struct Ruleset {
879 pub id: String,
880 pub level: Level,
881 /// The workspace it belongs to, or its repository's.
882 pub workspace: String,
883 /// A repository ruleset's repository: its id and `owner/name`.
884 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub repo_id: Option<String>,
886 #[serde(default, skip_serializing_if = "Option::is_none")]
887 pub repository: Option<String>,
888 #[serde(flatten)]
889 pub spec: RulesetSpec,
890 /// `branch_protection` for the ruleset made from a repository's branch
891 /// protection settings when rulesets arrived.
892 #[serde(default, skip_serializing_if = "Option::is_none")]
893 pub source: Option<String>,
894 pub created_by: String,
895 /// RFC 3339.
896 pub created_at: String,
897 pub updated_by: String,
898 pub updated_at: String,
899}
900
901/// Whose rulesets: a repository's (`repo`) or a workspace's (`workspace`).
902/// Exactly one is set.
903#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
904#[serde(default)]
905pub struct Owner {
906 #[serde(skip_serializing_if = "Option::is_none")]
907 pub repo: Option<RepoPath>,
908 #[serde(skip_serializing_if = "Option::is_none")]
909 pub workspace: Option<String>,
910}
911
912impl Owner {
913 pub fn repo(path: RepoPath) -> Owner {
914 Owner { repo: Some(path), workspace: None }
915 }
916
917 pub fn workspace(slug: &str) -> Owner {
918 Owner { repo: None, workspace: Some(slug.to_lowercase()) }
919 }
920}
921
922/// `list_rulesets`: a repository's or a workspace's rulesets. With
923/// `include_parents`, a repository's list also has its workspace's
924/// rulesets that hold in it. Anyone who may see the repository (members,
925/// for a workspace). Returns `Outcome<Vec<Ruleset>>`.
926#[derive(Clone, Debug, Serialize, Deserialize)]
927pub struct ListRulesetsArgs {
928 pub viewer: Viewer,
929 #[serde(flatten)]
930 pub owner: Owner,
931 #[serde(default)]
932 pub include_parents: bool,
933}
934
935/// `get_ruleset`. Returns `Outcome<Ruleset>`.
936#[derive(Clone, Debug, Serialize, Deserialize)]
937pub struct GetRulesetArgs {
938 pub viewer: Viewer,
939 #[serde(flatten)]
940 pub owner: Owner,
941 pub id: String,
942}
943
944/// `save_ruleset`: creates one (no `id`) or replaces one. The Maintain
945/// role on a repository (`ManageProtection`); a workspace's owners for its
946/// own. Returns `Outcome<Ruleset>`.
947#[derive(Clone, Debug, Serialize, Deserialize)]
948pub struct SaveRulesetArgs {
949 pub actor: User,
950 #[serde(flatten)]
951 pub owner: Owner,
952 #[serde(default)]
953 pub id: Option<String>,
954 pub ruleset: RulesetSpec,
955}
956
957/// `delete_ruleset`. Returns `Outcome<bool>`.
958#[derive(Clone, Debug, Serialize, Deserialize)]
959pub struct DeleteRulesetArgs {
960 pub actor: User,
961 #[serde(flatten)]
962 pub owner: Owner,
963 pub id: String,
964}
965
966/// `effective_rules`: every rule that holds for a branch (or a tag, with
967/// `target` `tag`) of a repository, with the ruleset each comes from.
968/// Returns `Outcome<EffectiveRules>`.
969#[derive(Clone, Debug, Serialize, Deserialize)]
970pub struct EffectiveRulesArgs {
971 pub viewer: Viewer,
972 pub repo: RepoPath,
973 pub name: String,
974 #[serde(default)]
975 pub target: Target,
976}
977
978/// A rule that holds for a branch, and where it comes from.
979#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
980pub struct EffectiveRule {
981 #[serde(flatten)]
982 pub entry: RuleEntry,
983 pub ruleset_id: String,
984 pub ruleset_name: String,
985 pub level: Level,
986 pub enforcement: Enforcement,
987}
988
989/// What holds for one branch or tag.
990#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
991pub struct EffectiveRules {
992 pub name: String,
993 pub target: Target,
994 /// Whether it is the repository's default branch.
995 pub default_branch: bool,
996 /// Active rules first, then those being evaluated.
997 pub rules: Vec<EffectiveRule>,
998 /// The rulesets that hold, by id: their names and who may bypass them.
999 pub rulesets: Vec<RulesetSummary>,
1000}
1001
1002/// A ruleset in brief.
1003#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1004pub struct RulesetSummary {
1005 pub id: String,
1006 pub name: String,
1007 pub level: Level,
1008 pub enforcement: Enforcement,
1009 pub bypass_actors: Vec<BypassActor>,
1010}
1011
1012/// What a change was: a push, a merge, or a change made through g1t.
1013#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1014#[serde(rename_all = "snake_case")]
1015pub enum Action {
1016 Push,
1017 Merge,
1018 CreateRef,
1019 DeleteRef,
1020 RenameRef,
1021 /// A commit made through g1t, such as a web edit.
1022 Commit,
1023}
1024
1025impl Action {
1026 pub fn as_str(self) -> &'static str {
1027 match self {
1028 Action::Push => "push",
1029 Action::Merge => "merge",
1030 Action::CreateRef => "create_ref",
1031 Action::DeleteRef => "delete_ref",
1032 Action::RenameRef => "rename_ref",
1033 Action::Commit => "commit",
1034 }
1035 }
1036}
1037
1038/// How an evaluation came out.
1039#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1040#[serde(rename_all = "snake_case")]
1041pub enum Verdict {
1042 /// Every rule was met.
1043 Pass,
1044 /// A rule was broken and the change refused (an active ruleset), or
1045 /// would have been (`evaluate`).
1046 Fail,
1047 /// A rule was broken by a bypass actor, who was let through.
1048 Bypass,
1049}
1050
1051impl Verdict {
1052 pub fn as_str(self) -> &'static str {
1053 match self {
1054 Verdict::Pass => "pass",
1055 Verdict::Fail => "fail",
1056 Verdict::Bypass => "bypass",
1057 }
1058 }
1059}
1060
1061/// One rule that a change breaks, and how to meet it.
1062#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1063pub struct Violation {
1064 /// The rule's `type`.
1065 pub rule: String,
1066 pub ruleset_id: String,
1067 pub ruleset_name: String,
1068 pub enforcement: Enforcement,
1069 /// What is wrong, in a sentence.
1070 pub message: String,
1071 /// How to satisfy it, in a sentence. May be empty.
1072 #[serde(default)]
1073 pub remedy: String,
1074}
1075
1076/// One ruleset's evaluation of one change, as it is recorded.
1077#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1078pub struct NewEvaluation {
1079 pub repo_id: String,
1080 pub workspace: String,
1081 pub ruleset_id: String,
1082 pub ruleset_name: String,
1083 pub enforcement: Enforcement,
1084 pub action: Action,
1085 /// The full ref: `refs/heads/main`.
1086 pub git_ref: String,
1087 pub actor: String,
1088 /// `person`, `agent` or `g1t`.
1089 pub actor_kind: String,
1090 pub verdict: Verdict,
1091 #[serde(default)]
1092 pub violations: Vec<Violation>,
1093 /// The pull request merged, for a merge.
1094 #[serde(default)]
1095 pub number: Option<u32>,
1096 /// The commit it would have moved the ref to.
1097 #[serde(default)]
1098 pub sha: Option<String>,
1099}
1100
1101/// A recorded evaluation.
1102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1103pub struct Evaluation {
1104 pub id: String,
1105 #[serde(flatten)]
1106 pub evaluation: NewEvaluation,
1107 /// `owner/name`, as it was.
1108 #[serde(default)]
1109 pub repository: String,
1110 /// RFC 3339.
1111 pub created_at: String,
1112}
1113
1114/// `record_evaluations`: services only. Returns how many were kept.
1115#[derive(Clone, Debug, Serialize, Deserialize)]
1116pub struct RecordEvaluationsArgs {
1117 pub evaluations: Vec<NewEvaluation>,
1118}
1119
1120/// `rule_evaluations`: the latest evaluations of a repository's or a
1121/// workspace's rulesets, newest first, filtered. Returns
1122/// `Outcome<EvaluationPage>`.
1123#[derive(Clone, Debug, Serialize, Deserialize)]
1124pub struct EvaluationsArgs {
1125 pub viewer: Viewer,
1126 #[serde(flatten)]
1127 pub owner: Owner,
1128 #[serde(default)]
1129 pub ruleset_id: Option<String>,
1130 #[serde(default)]
1131 pub verdict: Option<Verdict>,
1132 /// Only those that broke a rule (failed, would have failed, bypassed).
1133 #[serde(default)]
1134 pub problems_only: bool,
1135 /// An evaluation's id: only older ones.
1136 #[serde(default)]
1137 pub before: Option<String>,
1138 #[serde(default)]
1139 pub limit: Option<u32>,
1140}
1141
1142/// A page of evaluations, and how they came out over the last 30 days.
1143#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1144pub struct EvaluationPage {
1145 pub evaluations: Vec<Evaluation>,
1146 /// The `before` for the next page, when there is one.
1147 #[serde(default)]
1148 pub next: Option<String>,
1149 pub insights: Insights,
1150}
1151
1152/// How a ruleset's evaluations came out.
1153#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1154pub struct Insights {
1155 pub days: u32,
1156 pub total: u32,
1157 pub passed: u32,
1158 /// Refused by an active ruleset.
1159 pub blocked: u32,
1160 /// Would have been refused by a ruleset in `evaluate`.
1161 pub would_block: u32,
1162 pub bypassed: u32,
1163 /// Per ruleset, most problems first.
1164 pub by_ruleset: Vec<RulesetInsight>,
1165 /// Per rule type, most problems first.
1166 pub by_rule: Vec<RuleInsight>,
1167}
1168
1169#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1170pub struct RulesetInsight {
1171 pub ruleset_id: String,
1172 pub ruleset_name: String,
1173 pub enforcement: Enforcement,
1174 pub total: u32,
1175 pub blocked: u32,
1176 pub would_block: u32,
1177 pub bypassed: u32,
1178}
1179
1180#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1181pub struct RuleInsight {
1182 pub rule: String,
1183 pub count: u32,
1184}
1185
1186/// A ruleset that holds for refs a service is about to change, with
1187/// whether the actor may bypass it and how. What `ref_rules` returns.
1188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1189pub struct Applicable {
1190 pub id: String,
1191 pub name: String,
1192 pub level: Level,
1193 pub enforcement: Enforcement,
1194 pub target: Target,
1195 pub conditions: RefCondition,
1196 pub rules: Vec<RuleEntry>,
1197 /// How the actor may bypass it, if they may.
1198 #[serde(default)]
1199 pub bypass: Option<BypassMode>,
1200}
1201
1202/// `ref_rules`: services only. The rulesets of a repository (its own and
1203/// its workspace's) that are not disabled and hold for any of `refs` (full
1204/// refs), with whether `actor` may bypass each. Returns
1205/// `Outcome<RefRules>`.
1206#[derive(Clone, Debug, Serialize, Deserialize)]
1207pub struct RefRulesArgs {
1208 pub repo_id: String,
1209 pub actor: Option<User>,
1210 pub refs: Vec<String>,
1211}
1212
1213#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1214pub struct RefRules {
1215 pub default_branch: String,
1216 pub workspace: String,
1217 pub rulesets: Vec<Applicable>,
1218}
1219
1220/// What g1t made of a commit's signature.
1221#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1222#[serde(tag = "state", rename_all = "snake_case")]
1223pub enum Signature {
1224 #[default]
1225 Unsigned,
1226 /// Valid, made with a key the account owning the committer's verified
1227 /// address registered: that account's username.
1228 Verified { signer: String },
1229 /// Signed, but not verified: why.
1230 Unverified { reason: String },
1231}
1232
1233/// One file a commit adds, changes or deletes.
1234#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1235pub struct FileChange {
1236 pub path: String,
1237 /// Its size in bytes, when its content is new and was read.
1238 #[serde(default)]
1239 pub size: Option<u64>,
1240 #[serde(default)]
1241 pub deleted: bool,
1242}
1243
1244/// What rules about commits look at, for one commit.
1245#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1246pub struct CommitFacts {
1247 pub sha: String,
1248 /// At most 4 KiB of it.
1249 pub message: String,
1250 #[serde(default)]
1251 pub author_email: Option<String>,
1252 #[serde(default)]
1253 pub committer_email: Option<String>,
1254 pub parents: u32,
1255 #[serde(default)]
1256 pub signature: Signature,
1257 #[serde(default)]
1258 pub files: Vec<FileChange>,
1259 /// Whether `files` is every file it changes.
1260 #[serde(default)]
1261 pub files_complete: bool,
1262}
1263
1264/// `inspect_commits`: services only. The commits a branch of `source_id`
1265/// adds on top of `base_branch` of `target_id`, read as rules look at
1266/// them, at most `limit`. Returns `Outcome<InspectedCommits>`.
1267#[derive(Clone, Debug, Serialize, Deserialize)]
1268pub struct InspectCommitsArgs {
1269 pub source_id: String,
1270 pub head: String,
1271 pub target_id: String,
1272 pub base_branch: String,
1273 #[serde(default)]
1274 pub limit: Option<u32>,
1275}
1276
1277#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1278pub struct InspectedCommits {
1279 pub commits: Vec<CommitFacts>,
1280 /// Whether `commits` holds every commit the branch adds, each read in
1281 /// full. A change too large to read is not.
1282 pub complete: bool,
1283}
1284
1285/// What kind of actor a change is by, for rules that hold only for agents'
1286/// or people's changes and for the evaluation log.
1287pub fn actor_kind(actor: &User) -> &'static str {
1288 use crate::PrincipalKind;
1289 match actor.kind {
1290 PrincipalKind::System => "g1t",
1291 PrincipalKind::Agent => "agent",
1292 _ if actor.acting.is_some() => "agent",
1293 PrincipalKind::Workspace => "token",
1294 PrincipalKind::User => "person",
1295 }
1296}
1297
1298/// Whether the actor is an agent: g1t's, or another acting through an
1299/// agent token. g1t acting on its own counts as an agent.
1300pub fn is_agent(actor: &User) -> bool {
1301 matches!(actor_kind(actor), "agent" | "g1t")
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307 use serde_json::json;
1308
1309 #[test]
1310 fn a_rule_is_its_type_and_parameters() {
1311 let entry = RuleEntry {
1312 rule: Rule::PullRequest(PullRequestRule { required_approvals: 2, ..PullRequestRule::default() }),
1313 applies_to: AppliesTo::Agents,
1314 };
1315 let value = serde_json::to_value(&entry).unwrap();
1316 assert_eq!(value["type"], "pull_request");
1317 assert_eq!(value["parameters"]["required_approvals"], 2);
1318 assert_eq!(value["applies_to"], "agents");
1319 let back: RuleEntry = serde_json::from_value(value).unwrap();
1320 assert_eq!(back, entry);
1321 }
1322
1323 #[test]
1324 fn parameters_left_out_take_their_defaults() {
1325 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion" })).unwrap();
1326 assert_eq!(entry.rule, Rule::Deletion(NoParameters {}));
1327 assert_eq!(entry.applies_to, AppliesTo::Everyone);
1328 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion", "parameters": {} })).unwrap();
1329 assert_eq!(entry.rule.kind(), "deletion");
1330 let entry: RuleEntry =
1331 serde_json::from_value(json!({ "type": "merge_queue", "parameters": { "max_entries_to_build": 8 } })).unwrap();
1332 let Rule::MergeQueue(queue) = entry.rule else { panic!() };
1333 assert_eq!((queue.max_entries_to_build, queue.check_response_timeout_minutes), (8, 45));
1334 }
1335
1336 #[test]
1337 fn every_rule_type_reads_back_as_it_is_named() {
1338 let rules = [
1339 Rule::Creation(NoParameters {}),
1340 Rule::Update(NoParameters {}),
1341 Rule::Deletion(NoParameters {}),
1342 Rule::NonFastForward(NoParameters {}),
1343 Rule::RequiredLinearHistory(NoParameters {}),
1344 Rule::RequiredSignatures(NoParameters {}),
1345 Rule::PullRequest(PullRequestRule::default()),
1346 Rule::RequiredStatusChecks(StatusChecksRule::default()),
1347 Rule::MergeQueue(MergeQueueRule::default()),
1348 Rule::RequiredDeployments(DeploymentsRule::default()),
1349 Rule::CommitMessagePattern(PatternRule::default()),
1350 Rule::CommitAuthorEmailPattern(PatternRule::default()),
1351 Rule::CommitterEmailPattern(PatternRule::default()),
1352 Rule::BranchNamePattern(PatternRule::default()),
1353 Rule::TagNamePattern(PatternRule::default()),
1354 Rule::FilePathRestriction(FilePathRule::default()),
1355 Rule::FileExtensionRestriction(FileExtensionRule::default()),
1356 Rule::MaxFileSize(MaxFileSizeRule::default()),
1357 Rule::MaxFilePathLength(MaxFilePathLengthRule::default()),
1358 Rule::MaxFilesChanged(MaxFilesChangedRule::default()),
1359 Rule::SecretScanning(NoParameters {}),
1360 Rule::ConfidenceThreshold(ConfidenceRule::default()),
1361 Rule::CostCap(CostCapRule::default()),
1362 Rule::PathReview(PathReviewRule::default()),
1363 Rule::MergeWindow(MergeWindowRule::default()),
1364 Rule::AgentAutoMerge(AgentAutoMergeRule::default()),
1365 ];
1366 for rule in rules {
1367 let value = serde_json::to_value(RuleEntry::everyone(rule.clone())).unwrap();
1368 assert_eq!(value["type"], rule.kind());
1369 let back: RuleEntry = serde_json::from_value(value).unwrap();
1370 assert_eq!(back.rule, rule);
1371 assert!(!rule.label().is_empty());
1372 }
1373 }
1374
1375 #[test]
1376 fn a_ruleset_reads_as_the_api_shows_it() {
1377 let ruleset: RulesetSpec = serde_json::from_value(json!({
1378 "name": "Protect main",
1379 "enforcement": "evaluate",
1380 "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH", "release/**"], "exclude": [] } },
1381 "bypass_actors": [{ "kind": "role", "value": "admin", "mode": "pull_requests" }, { "kind": "g1t" }],
1382 "rules": [{ "type": "non_fast_forward" }, { "type": "required_status_checks", "parameters": { "checks": [{ "context": "CI", "integration": "actions" }], "strict": true } }]
1383 }))
1384 .unwrap();
1385 assert_eq!(ruleset.enforcement, Enforcement::Evaluate);
1386 assert_eq!(ruleset.target, Target::Branch);
1387 assert_eq!(ruleset.bypass_actors[1], BypassActor { kind: ActorKind::G1t, value: String::new(), mode: BypassMode::Always });
1388 let Rule::RequiredStatusChecks(checks) = &ruleset.rules[1].rule else { panic!() };
1389 assert_eq!(checks.checks[0].integration, Some(Integration::Actions));
1390 assert!(checks.strict);
1391 }
1392
1393 #[test]
1394 fn refs_split_into_their_target_and_name() {
1395 assert_eq!(Target::of_ref("refs/heads/release/1.x"), Some((Target::Branch, "release/1.x")));
1396 assert_eq!(Target::of_ref("refs/tags/v1"), Some((Target::Tag, "v1")));
1397 assert_eq!(Target::of_ref("refs/notes/x"), None);
1398 assert_eq!(Target::Tag.full_ref("v2"), "refs/tags/v2");
1399 }
1400
1401 #[test]
1402 fn whose_change_it_is() {
1403 assert!(AppliesTo::Everyone.covers(true) && AppliesTo::Everyone.covers(false));
1404 assert!(AppliesTo::Agents.covers(true) && !AppliesTo::Agents.covers(false));
1405 assert!(AppliesTo::People.covers(false) && !AppliesTo::People.covers(true));
1406 let person = User { id: "usr_1".into(), username: "ada".into(), ..User::default() };
1407 assert_eq!(actor_kind(&person), "person");
1408 assert!(!is_agent(&person));
1409 assert!(is_agent(&User::system("acme")));
1410 }
1411}