Skip to content
1,439 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 /// Reported through the API, by a token or an integration: a status
348 /// or a check run.
349 Api,
350}
351
352impl Integration {
353 pub fn as_str(self) -> &'static str {
354 match self {
355 Integration::Actions => "actions",
356 Integration::Deployments => "deployments",
357 Integration::Security => "security",
358 Integration::G1t => "g1t",
359 Integration::Api => "api",
360 }
361 }
362
363 pub fn parse(text: &str) -> Option<Integration> {
364 match text {
365 "actions" => Some(Integration::Actions),
366 "deployments" => Some(Integration::Deployments),
367 "security" => Some(Integration::Security),
368 "g1t" => Some(Integration::G1t),
369 "api" => Some(Integration::Api),
370 _ => None,
371 }
372 }
373}
374
375/// One check that must pass: a workflow's name (`CI`) or another status's
376/// context, and, if set, the integration that must have reported it.
377#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
378pub struct RequiredCheck {
379 pub context: String,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub integration: Option<Integration>,
382}
383
384/// `required_status_checks`: these checks must pass on a pull request's
385/// head before it merges.
386#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(default)]
388#[derive(Default)]
389pub struct StatusChecksRule {
390 pub checks: Vec<RequiredCheck>,
391 /// The pull request must contain the branch's latest commits, so that
392 /// what merges is what was checked.
393 pub strict: bool,
394 /// Required only when the pull request changes a file matching one of
395 /// these patterns. Empty: always.
396 pub paths: Vec<String>,
397 /// Someone who may merge can merge past checks that have not passed,
398 /// saying so as they merge.
399 pub allow_bypass_on_merge: bool,
400}
401
402
403/// `merge_queue`: merging joins the queue, which tests each pull request
404/// together with those ahead of it. The queue lands on the default branch.
405#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(default)]
407pub struct MergeQueueRule {
408 pub merge_method: MergeMethod,
409 /// Entries tested at once.
410 pub max_entries_to_build: u32,
411 /// Entries a batch waits for before it starts, unless the oldest has
412 /// waited `min_entries_wait_minutes`.
413 pub min_entries_to_merge: u32,
414 pub min_entries_wait_minutes: u32,
415 /// How long a batch's checks may take before it is tested again.
416 pub check_response_timeout_minutes: u32,
417}
418
419impl Default for MergeQueueRule {
420 fn default() -> Self {
421 MergeQueueRule {
422 merge_method: MergeMethod::Merge,
423 max_entries_to_build: 4,
424 min_entries_to_merge: 1,
425 min_entries_wait_minutes: 0,
426 check_response_timeout_minutes: 45,
427 }
428 }
429}
430
431/// `required_deployments`: a pull request's head must have deployed
432/// successfully to these environments: `preview` (its preview), or a
433/// project's slug for a repository with several.
434#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(default)]
436pub struct DeploymentsRule {
437 pub environments: Vec<String>,
438}
439
440/// How a pattern rule compares.
441#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
442#[serde(rename_all = "snake_case")]
443pub enum PatternOperator {
444 #[default]
445 StartsWith,
446 EndsWith,
447 Contains,
448 /// A regular expression, run by a linear-time engine.
449 Regex,
450}
451
452/// A rule about text: a commit message, an author's or committer's email
453/// address, a branch's or tag's name. The text must match the pattern, or
454/// with `negate`, must not.
455#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(default)]
457pub struct PatternRule {
458 /// What people are told the rule is, such as "Conventional commits".
459 pub name: String,
460 pub operator: PatternOperator,
461 pub pattern: String,
462 pub negate: bool,
463}
464
465/// `file_path_restriction`: pushes and pull requests may not change files
466/// matching these patterns (fnmatch, `**` across directories).
467#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(default)]
469pub struct FilePathRule {
470 pub restricted_file_paths: Vec<String>,
471}
472
473/// `file_extension_restriction`: files with these extensions (`.exe`,
474/// `.zip`) may not be added or changed.
475#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(default)]
477pub struct FileExtensionRule {
478 pub restricted_file_extensions: Vec<String>,
479}
480
481/// `max_file_size`: no file larger than this, in megabytes (1 to 100).
482#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(default)]
484pub struct MaxFileSizeRule {
485 pub max_file_size_mb: u32,
486}
487
488impl Default for MaxFileSizeRule {
489 fn default() -> Self {
490 MaxFileSizeRule { max_file_size_mb: 10 }
491 }
492}
493
494/// `max_file_path_length`: no path longer than this many characters.
495#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(default)]
497pub struct MaxFilePathLengthRule {
498 pub max_file_path_length: u32,
499}
500
501impl Default for MaxFilePathLengthRule {
502 fn default() -> Self {
503 MaxFilePathLengthRule { max_file_path_length: 255 }
504 }
505}
506
507/// `max_files_changed`: a push's commits, each, and a pull request as a
508/// whole, change at most this many files.
509#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(default)]
511pub struct MaxFilesChangedRule {
512 pub max_files: u32,
513}
514
515impl Default for MaxFilesChangedRule {
516 fn default() -> Self {
517 MaxFilesChangedRule { max_files: 100 }
518 }
519}
520
521/// `confidence_threshold`: an agent's change g1t rates below `minimum`
522/// needs approvals from people before it merges.
523#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
524#[serde(default)]
525pub struct ConfidenceRule {
526 pub minimum: ConfidenceLevel,
527 pub required_approvals: u32,
528}
529
530impl Default for ConfidenceRule {
531 fn default() -> Self {
532 ConfidenceRule { minimum: ConfidenceLevel::Medium, required_approvals: 1 }
533 }
534}
535
536/// `cost_cap`: once agents have spent more than this on a pull request, in
537/// US dollars, it neither merges nor is sent back to its agent until a
538/// person approves it after that.
539#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
540#[serde(default)]
541pub struct CostCapRule {
542 pub max_usd: f64,
543}
544
545impl Default for CostCapRule {
546 fn default() -> Self {
547 CostCapRule { max_usd: 10.0 }
548 }
549}
550
551/// `path_review`: a pull request that changes a file matching `paths`
552/// needs `required_approvals` from people, from `team` when one is named
553/// (its slug, or `workspace/slug`).
554#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
555#[serde(default)]
556pub struct PathReviewRule {
557 pub paths: Vec<String>,
558 pub required_approvals: u32,
559 #[serde(skip_serializing_if = "Option::is_none")]
560 pub team: Option<String>,
561}
562
563impl Default for PathReviewRule {
564 fn default() -> Self {
565 PathReviewRule { paths: Vec::new(), required_approvals: 1, team: None }
566 }
567}
568
569/// A day of the week.
570#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
571#[serde(rename_all = "snake_case")]
572pub enum Weekday {
573 Mon,
574 Tue,
575 Wed,
576 Thu,
577 Fri,
578 Sat,
579 Sun,
580}
581
582impl Weekday {
583 pub const ALL: [Weekday; 7] = [
584 Weekday::Mon,
585 Weekday::Tue,
586 Weekday::Wed,
587 Weekday::Thu,
588 Weekday::Fri,
589 Weekday::Sat,
590 Weekday::Sun,
591 ];
592
593 pub fn as_str(self) -> &'static str {
594 match self {
595 Weekday::Mon => "mon",
596 Weekday::Tue => "tue",
597 Weekday::Wed => "wed",
598 Weekday::Thu => "thu",
599 Weekday::Fri => "fri",
600 Weekday::Sat => "sat",
601 Weekday::Sun => "sun",
602 }
603 }
604}
605
606/// Hours on some days of the week when merging is allowed, `HH:MM` to
607/// `HH:MM` in the rule's time zone. An `end` before `start` runs past
608/// midnight.
609#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
610pub struct WeeklyWindow {
611 pub days: Vec<Weekday>,
612 pub start: String,
613 pub end: String,
614}
615
616/// A stretch of time, RFC 3339 UTC. With no `end`, it lasts until removed:
617/// an incident freeze.
618#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
619pub struct Period {
620 pub start: String,
621 #[serde(default)]
622 pub end: Option<String>,
623 #[serde(default)]
624 pub reason: String,
625}
626
627/// `merge_window`: when pull requests may merge into the branch. Outside
628/// every `windows` entry (when there are any), or during a `freezes` one,
629/// merging waits, unless an `exceptions` entry covers the moment.
630#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
631#[serde(default)]
632pub struct MergeWindowRule {
633 /// A fixed offset from UTC, `+02:00` or `-05:00`; `UTC` or empty is UTC.
634 /// Daylight saving time is not applied.
635 pub time_zone: String,
636 pub windows: Vec<WeeklyWindow>,
637 pub freezes: Vec<Period>,
638 pub exceptions: Vec<Period>,
639}
640
641impl Default for MergeWindowRule {
642 fn default() -> Self {
643 MergeWindowRule {
644 time_zone: "UTC".to_owned(),
645 windows: Vec::new(),
646 freezes: Vec::new(),
647 exceptions: Vec::new(),
648 }
649 }
650}
651
652/// `agent_auto_merge`: whether g1t lands an agent's ready pull request into
653/// the branch without a person pressing merge, and how sure of it g1t must
654/// be. The repository's auto-merge setting must be on as well.
655#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(default)]
657pub struct AgentAutoMergeRule {
658 pub allowed: bool,
659 #[serde(skip_serializing_if = "Option::is_none")]
660 pub minimum_confidence: Option<ConfidenceLevel>,
661}
662
663impl Default for AgentAutoMergeRule {
664 fn default() -> Self {
665 AgentAutoMergeRule { allowed: true, minimum_confidence: None }
666 }
667}
668
669/// One rule and its parameters, tagged by `type`.
670#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
671#[serde(tag = "type", content = "parameters", rename_all = "snake_case")]
672pub enum Rule {
673 /// Only bypass actors may create a matching branch or tag.
674 Creation(NoParameters),
675 /// Only bypass actors may push to (move) a matching branch or tag.
676 Update(NoParameters),
677 /// Only bypass actors may delete a matching branch or tag.
678 Deletion(NoParameters),
679 /// Nobody force pushes: a push must only add to its history.
680 NonFastForward(NoParameters),
681 /// No merge commits: history stays a straight line.
682 RequiredLinearHistory(NoParameters),
683 /// Every commit carries a signature g1t verifies.
684 RequiredSignatures(NoParameters),
685 PullRequest(PullRequestRule),
686 RequiredStatusChecks(StatusChecksRule),
687 MergeQueue(MergeQueueRule),
688 RequiredDeployments(DeploymentsRule),
689 CommitMessagePattern(PatternRule),
690 CommitAuthorEmailPattern(PatternRule),
691 CommitterEmailPattern(PatternRule),
692 BranchNamePattern(PatternRule),
693 TagNamePattern(PatternRule),
694 FilePathRestriction(FilePathRule),
695 FileExtensionRestriction(FileExtensionRule),
696 MaxFileSize(MaxFileSizeRule),
697 MaxFilePathLength(MaxFilePathLengthRule),
698 MaxFilesChanged(MaxFilesChangedRule),
699 /// Pushes that add a secret are refused, whatever the repository's own
700 /// push protection setting says.
701 SecretScanning(NoParameters),
702 ConfidenceThreshold(ConfidenceRule),
703 CostCap(CostCapRule),
704 PathReview(PathReviewRule),
705 MergeWindow(MergeWindowRule),
706 AgentAutoMerge(AgentAutoMergeRule),
707}
708
709impl Rule {
710 /// Its `type`, as the API names it.
711 pub fn kind(&self) -> &'static str {
712 match self {
713 Rule::Creation(_) => "creation",
714 Rule::Update(_) => "update",
715 Rule::Deletion(_) => "deletion",
716 Rule::NonFastForward(_) => "non_fast_forward",
717 Rule::RequiredLinearHistory(_) => "required_linear_history",
718 Rule::RequiredSignatures(_) => "required_signatures",
719 Rule::PullRequest(_) => "pull_request",
720 Rule::RequiredStatusChecks(_) => "required_status_checks",
721 Rule::MergeQueue(_) => "merge_queue",
722 Rule::RequiredDeployments(_) => "required_deployments",
723 Rule::CommitMessagePattern(_) => "commit_message_pattern",
724 Rule::CommitAuthorEmailPattern(_) => "commit_author_email_pattern",
725 Rule::CommitterEmailPattern(_) => "committer_email_pattern",
726 Rule::BranchNamePattern(_) => "branch_name_pattern",
727 Rule::TagNamePattern(_) => "tag_name_pattern",
728 Rule::FilePathRestriction(_) => "file_path_restriction",
729 Rule::FileExtensionRestriction(_) => "file_extension_restriction",
730 Rule::MaxFileSize(_) => "max_file_size",
731 Rule::MaxFilePathLength(_) => "max_file_path_length",
732 Rule::MaxFilesChanged(_) => "max_files_changed",
733 Rule::SecretScanning(_) => "secret_scanning",
734 Rule::ConfidenceThreshold(_) => "confidence_threshold",
735 Rule::CostCap(_) => "cost_cap",
736 Rule::PathReview(_) => "path_review",
737 Rule::MergeWindow(_) => "merge_window",
738 Rule::AgentAutoMerge(_) => "agent_auto_merge",
739 }
740 }
741
742 /// How people are shown it.
743 pub fn label(&self) -> &'static str {
744 match self {
745 Rule::Creation(_) => "Restrict creations",
746 Rule::Update(_) => "Restrict updates",
747 Rule::Deletion(_) => "Restrict deletions",
748 Rule::NonFastForward(_) => "Block force pushes",
749 Rule::RequiredLinearHistory(_) => "Require linear history",
750 Rule::RequiredSignatures(_) => "Require signed commits",
751 Rule::PullRequest(_) => "Require a pull request before merging",
752 Rule::RequiredStatusChecks(_) => "Require status checks to pass",
753 Rule::MergeQueue(_) => "Require the merge queue",
754 Rule::RequiredDeployments(_) => "Require deployments to succeed",
755 Rule::CommitMessagePattern(_) => "Commit message pattern",
756 Rule::CommitAuthorEmailPattern(_) => "Commit author email pattern",
757 Rule::CommitterEmailPattern(_) => "Committer email pattern",
758 Rule::BranchNamePattern(_) => "Branch name pattern",
759 Rule::TagNamePattern(_) => "Tag name pattern",
760 Rule::FilePathRestriction(_) => "Restrict file paths",
761 Rule::FileExtensionRestriction(_) => "Restrict file extensions",
762 Rule::MaxFileSize(_) => "Restrict file size",
763 Rule::MaxFilePathLength(_) => "Restrict file path length",
764 Rule::MaxFilesChanged(_) => "Restrict files changed",
765 Rule::SecretScanning(_) => "Block pushes that add secrets",
766 Rule::ConfidenceThreshold(_) => "Confidence threshold",
767 Rule::CostCap(_) => "Cost cap",
768 Rule::PathReview(_) => "Review for sensitive paths",
769 Rule::MergeWindow(_) => "Merge window",
770 Rule::AgentAutoMerge(_) => "Agent auto-merge",
771 }
772 }
773
774 /// Whether the rule is about pushes: what a push may do or bring.
775 /// Pull request rules hold on merge.
776 pub fn on_push(&self) -> bool {
777 matches!(
778 self,
779 Rule::Creation(_)
780 | Rule::Update(_)
781 | Rule::Deletion(_)
782 | Rule::NonFastForward(_)
783 | Rule::RequiredLinearHistory(_)
784 | Rule::RequiredSignatures(_)
785 | Rule::PullRequest(_)
786 | Rule::MergeQueue(_)
787 | Rule::CommitMessagePattern(_)
788 | Rule::CommitAuthorEmailPattern(_)
789 | Rule::CommitterEmailPattern(_)
790 | Rule::BranchNamePattern(_)
791 | Rule::TagNamePattern(_)
792 | Rule::FilePathRestriction(_)
793 | Rule::FileExtensionRestriction(_)
794 | Rule::MaxFileSize(_)
795 | Rule::MaxFilePathLength(_)
796 | Rule::MaxFilesChanged(_)
797 | Rule::SecretScanning(_)
798 )
799 }
800
801 /// Whether it says anything only tags can break (or only branches).
802 pub fn for_branches_only(&self) -> bool {
803 matches!(
804 self,
805 Rule::PullRequest(_)
806 | Rule::RequiredStatusChecks(_)
807 | Rule::MergeQueue(_)
808 | Rule::RequiredDeployments(_)
809 | Rule::BranchNamePattern(_)
810 | Rule::ConfidenceThreshold(_)
811 | Rule::CostCap(_)
812 | Rule::PathReview(_)
813 | Rule::MergeWindow(_)
814 | Rule::AgentAutoMerge(_)
815 )
816 }
817
818 pub fn for_tags_only(&self) -> bool {
819 matches!(self, Rule::TagNamePattern(_))
820 }
821}
822
823/// One rule of a ruleset, and whose changes it holds for. `parameters`
824/// may be left out, or left partly out: what is missing takes its default.
825#[derive(Clone, Debug, PartialEq, Serialize)]
826pub struct RuleEntry {
827 #[serde(flatten)]
828 pub rule: Rule,
829 pub applies_to: AppliesTo,
830}
831
832impl<'de> Deserialize<'de> for RuleEntry {
833 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
834 #[derive(Deserialize)]
835 struct Written {
836 #[serde(rename = "type")]
837 kind: String,
838 #[serde(default)]
839 parameters: serde_json::Value,
840 #[serde(default)]
841 applies_to: AppliesTo,
842 }
843 let written = Written::deserialize(deserializer)?;
844 let parameters = match written.parameters {
845 serde_json::Value::Null => serde_json::json!({}),
846 other => other,
847 };
848 let rule = serde_json::from_value(serde_json::json!({ "type": written.kind, "parameters": parameters }))
849 .map_err(serde::de::Error::custom)?;
850 Ok(RuleEntry { rule, applies_to: written.applies_to })
851 }
852}
853
854impl RuleEntry {
855 pub fn everyone(rule: Rule) -> RuleEntry {
856 RuleEntry { rule, applies_to: AppliesTo::Everyone }
857 }
858}
859
860/// What a ruleset says, as it is created, changed, exported and imported.
861#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
862#[serde(default)]
863pub struct RulesetSpec {
864 pub name: String,
865 pub enforcement: Enforcement,
866 pub target: Target,
867 pub conditions: Conditions,
868 pub bypass_actors: Vec<BypassActor>,
869 pub rules: Vec<RuleEntry>,
870}
871
872/// A ruleset, as it is kept.
873#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
874pub struct Ruleset {
875 pub id: String,
876 pub level: Level,
877 /// The workspace it belongs to, or its repository's.
878 pub workspace: String,
879 /// A repository ruleset's repository: its id and `owner/name`.
880 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub repo_id: Option<String>,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
883 pub repository: Option<String>,
884 #[serde(flatten)]
885 pub spec: RulesetSpec,
886 /// `branch_protection` for the ruleset made from a repository's branch
887 /// protection settings when rulesets arrived.
888 #[serde(default, skip_serializing_if = "Option::is_none")]
889 pub source: Option<String>,
890 pub created_by: String,
891 /// RFC 3339.
892 pub created_at: String,
893 pub updated_by: String,
894 pub updated_at: String,
895}
896
897/// Whose rulesets: a repository's (`repo`) or a workspace's (`workspace`).
898/// Exactly one is set.
899#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
900#[serde(default)]
901pub struct Owner {
902 #[serde(skip_serializing_if = "Option::is_none")]
903 pub repo: Option<RepoPath>,
904 #[serde(skip_serializing_if = "Option::is_none")]
905 pub workspace: Option<String>,
906}
907
908impl Owner {
909 pub fn repo(path: RepoPath) -> Owner {
910 Owner { repo: Some(path), workspace: None }
911 }
912
913 pub fn workspace(slug: &str) -> Owner {
914 Owner { repo: None, workspace: Some(slug.to_lowercase()) }
915 }
916}
917
918/// `list_rulesets`: a repository's or a workspace's rulesets. With
919/// `include_parents`, a repository's list also has its workspace's
920/// rulesets that hold in it. Anyone who may see the repository (members,
921/// for a workspace). Returns `Outcome<Vec<Ruleset>>`.
922#[derive(Clone, Debug, Serialize, Deserialize)]
923pub struct ListRulesetsArgs {
924 pub viewer: Viewer,
925 #[serde(flatten)]
926 pub owner: Owner,
927 #[serde(default)]
928 pub include_parents: bool,
929}
930
931/// `get_ruleset`. Returns `Outcome<Ruleset>`.
932#[derive(Clone, Debug, Serialize, Deserialize)]
933pub struct GetRulesetArgs {
934 pub viewer: Viewer,
935 #[serde(flatten)]
936 pub owner: Owner,
937 pub id: String,
938}
939
940/// `save_ruleset`: creates one (no `id`) or replaces one. The Maintain
941/// role on a repository (`ManageProtection`); a workspace's owners for its
942/// own. Returns `Outcome<Ruleset>`.
943#[derive(Clone, Debug, Serialize, Deserialize)]
944pub struct SaveRulesetArgs {
945 pub actor: User,
946 #[serde(flatten)]
947 pub owner: Owner,
948 #[serde(default)]
949 pub id: Option<String>,
950 pub ruleset: RulesetSpec,
951 /// Set by the API, which records the change in the audit log itself.
952 #[serde(default)]
953 pub from_api: bool,
954}
955
956/// `delete_ruleset`. Returns `Outcome<bool>`.
957#[derive(Clone, Debug, Serialize, Deserialize)]
958pub struct DeleteRulesetArgs {
959 pub actor: User,
960 #[serde(flatten)]
961 pub owner: Owner,
962 pub id: String,
963 #[serde(default)]
964 pub from_api: bool,
965}
966
967/// `effective_rules`: every rule that holds for a branch (or a tag, with
968/// `target` `tag`) of a repository, with the ruleset each comes from.
969/// Returns `Outcome<EffectiveRules>`.
970#[derive(Clone, Debug, Serialize, Deserialize)]
971pub struct EffectiveRulesArgs {
972 pub viewer: Viewer,
973 pub repo: RepoPath,
974 pub name: String,
975 #[serde(default)]
976 pub target: Target,
977}
978
979/// A rule that holds for a branch, and where it comes from.
980#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
981pub struct EffectiveRule {
982 #[serde(flatten)]
983 pub entry: RuleEntry,
984 pub ruleset_id: String,
985 pub ruleset_name: String,
986 pub level: Level,
987 pub enforcement: Enforcement,
988}
989
990/// What holds for one branch or tag.
991#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
992pub struct EffectiveRules {
993 pub name: String,
994 pub target: Target,
995 /// Whether it is the repository's default branch.
996 pub default_branch: bool,
997 /// Active rules first, then those being evaluated.
998 pub rules: Vec<EffectiveRule>,
999 /// The rulesets that hold, by id: their names and who may bypass them.
1000 pub rulesets: Vec<RulesetSummary>,
1001}
1002
1003/// A ruleset in brief.
1004#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1005pub struct RulesetSummary {
1006 pub id: String,
1007 pub name: String,
1008 pub level: Level,
1009 pub enforcement: Enforcement,
1010 pub bypass_actors: Vec<BypassActor>,
1011}
1012
1013/// What a change was: a push, a merge, or a change made through g1t.
1014#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1015#[serde(rename_all = "snake_case")]
1016pub enum Action {
1017 Push,
1018 Merge,
1019 CreateRef,
1020 DeleteRef,
1021 RenameRef,
1022 /// A commit made through g1t, such as a web edit.
1023 Commit,
1024}
1025
1026impl Action {
1027 pub fn as_str(self) -> &'static str {
1028 match self {
1029 Action::Push => "push",
1030 Action::Merge => "merge",
1031 Action::CreateRef => "create_ref",
1032 Action::DeleteRef => "delete_ref",
1033 Action::RenameRef => "rename_ref",
1034 Action::Commit => "commit",
1035 }
1036 }
1037}
1038
1039/// How an evaluation came out.
1040#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1041#[serde(rename_all = "snake_case")]
1042pub enum Verdict {
1043 /// Every rule was met.
1044 Pass,
1045 /// A rule was broken and the change refused (an active ruleset), or
1046 /// would have been (`evaluate`).
1047 Fail,
1048 /// A rule was broken by a bypass actor, who was let through.
1049 Bypass,
1050}
1051
1052impl Verdict {
1053 pub fn as_str(self) -> &'static str {
1054 match self {
1055 Verdict::Pass => "pass",
1056 Verdict::Fail => "fail",
1057 Verdict::Bypass => "bypass",
1058 }
1059 }
1060}
1061
1062/// One rule that a change breaks, and how to meet it.
1063#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1064pub struct Violation {
1065 /// The rule's `type`.
1066 pub rule: String,
1067 pub ruleset_id: String,
1068 pub ruleset_name: String,
1069 pub enforcement: Enforcement,
1070 /// What is wrong, in a sentence.
1071 pub message: String,
1072 /// How to satisfy it, in a sentence. May be empty.
1073 #[serde(default)]
1074 pub remedy: String,
1075}
1076
1077/// One ruleset's evaluation of one change, as it is recorded.
1078#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1079pub struct NewEvaluation {
1080 pub repo_id: String,
1081 pub workspace: String,
1082 pub ruleset_id: String,
1083 pub ruleset_name: String,
1084 pub enforcement: Enforcement,
1085 pub action: Action,
1086 /// The full ref: `refs/heads/main`.
1087 pub git_ref: String,
1088 pub actor: String,
1089 /// `person`, `agent` or `g1t`.
1090 pub actor_kind: String,
1091 pub verdict: Verdict,
1092 #[serde(default)]
1093 pub violations: Vec<Violation>,
1094 /// The pull request merged, for a merge.
1095 #[serde(default)]
1096 pub number: Option<u32>,
1097 /// The commit it would have moved the ref to.
1098 #[serde(default)]
1099 pub sha: Option<String>,
1100}
1101
1102/// A recorded evaluation.
1103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1104pub struct Evaluation {
1105 pub id: String,
1106 #[serde(flatten)]
1107 pub evaluation: NewEvaluation,
1108 /// `owner/name`, as it was.
1109 #[serde(default)]
1110 pub repository: String,
1111 /// RFC 3339.
1112 pub created_at: String,
1113}
1114
1115/// `record_evaluations`: services only. Returns how many were kept.
1116#[derive(Clone, Debug, Serialize, Deserialize)]
1117pub struct RecordEvaluationsArgs {
1118 pub evaluations: Vec<NewEvaluation>,
1119}
1120
1121/// `rule_evaluations`: the latest evaluations of a repository's or a
1122/// workspace's rulesets, newest first, filtered. Returns
1123/// `Outcome<EvaluationPage>`.
1124#[derive(Clone, Debug, Serialize, Deserialize)]
1125pub struct EvaluationsArgs {
1126 pub viewer: Viewer,
1127 #[serde(flatten)]
1128 pub owner: Owner,
1129 #[serde(default)]
1130 pub ruleset_id: Option<String>,
1131 #[serde(default)]
1132 pub verdict: Option<Verdict>,
1133 /// Only those that broke a rule (failed, would have failed, bypassed).
1134 #[serde(default)]
1135 pub problems_only: bool,
1136 /// An evaluation's id: only older ones.
1137 #[serde(default)]
1138 pub before: Option<String>,
1139 #[serde(default)]
1140 pub limit: Option<u32>,
1141}
1142
1143/// A page of evaluations, and how they came out over the last 30 days.
1144#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1145pub struct EvaluationPage {
1146 pub evaluations: Vec<Evaluation>,
1147 /// The `before` for the next page, when there is one.
1148 #[serde(default)]
1149 pub next: Option<String>,
1150 pub insights: Insights,
1151}
1152
1153/// How a ruleset's evaluations came out.
1154#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1155pub struct Insights {
1156 pub days: u32,
1157 pub total: u32,
1158 pub passed: u32,
1159 /// Refused by an active ruleset.
1160 pub blocked: u32,
1161 /// Would have been refused by a ruleset in `evaluate`.
1162 pub would_block: u32,
1163 pub bypassed: u32,
1164 /// Per ruleset, most problems first.
1165 pub by_ruleset: Vec<RulesetInsight>,
1166 /// Per rule type, most problems first.
1167 pub by_rule: Vec<RuleInsight>,
1168}
1169
1170#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1171pub struct RulesetInsight {
1172 pub ruleset_id: String,
1173 pub ruleset_name: String,
1174 pub enforcement: Enforcement,
1175 pub total: u32,
1176 pub blocked: u32,
1177 pub would_block: u32,
1178 pub bypassed: u32,
1179}
1180
1181#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1182pub struct RuleInsight {
1183 pub rule: String,
1184 pub count: u32,
1185}
1186
1187/// A ruleset that holds for refs a service is about to change, with
1188/// whether the actor may bypass it and how. What `ref_rules` returns.
1189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1190pub struct Applicable {
1191 pub id: String,
1192 pub name: String,
1193 pub level: Level,
1194 pub enforcement: Enforcement,
1195 pub target: Target,
1196 pub conditions: RefCondition,
1197 pub rules: Vec<RuleEntry>,
1198 /// How the actor may bypass it, if they may.
1199 #[serde(default)]
1200 pub bypass: Option<BypassMode>,
1201}
1202
1203/// `ref_rules`: services only. The rulesets of a repository (its own and
1204/// its workspace's) that are not disabled and hold for any of `refs` (full
1205/// refs), with whether `actor` may bypass each. Returns
1206/// `Outcome<RefRules>`.
1207#[derive(Clone, Debug, Serialize, Deserialize)]
1208pub struct RefRulesArgs {
1209 /// The repository, as the repos service read it.
1210 pub repo: crate::repos::Repo,
1211 pub actor: Option<User>,
1212 pub refs: Vec<String>,
1213}
1214
1215/// What a pull request's merge box shows of the rules for the branch it
1216/// merges into, for whoever is looking.
1217#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1218pub struct MergeRules {
1219 /// Rules not met, which refuse the merge.
1220 pub unmet: Vec<Violation>,
1221 /// Rules not met that the viewer may bypass, by asking to as they
1222 /// merge (`bypass_rules`).
1223 pub bypassable: Vec<Violation>,
1224 /// Rules of rulesets in `evaluate` that would refuse it.
1225 pub evaluate: Vec<Violation>,
1226 /// The rulesets that hold for the branch.
1227 pub rulesets: Vec<RulesetSummary>,
1228 /// Whether merging joins the merge queue.
1229 pub merge_queue: bool,
1230 /// What the active rules ask, as they stack: the approvals a merge
1231 /// needs, whether it must be up to date, and whether a merger may merge
1232 /// past required checks that have not passed.
1233 #[serde(default)]
1234 pub required_approvals: u32,
1235 #[serde(default)]
1236 pub strict: bool,
1237 #[serde(default)]
1238 pub allow_bypass_on_merge: bool,
1239}
1240
1241#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1242pub struct RefRules {
1243 pub default_branch: String,
1244 pub workspace: String,
1245 pub rulesets: Vec<Applicable>,
1246}
1247
1248/// What g1t made of a commit's signature.
1249#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1250#[serde(tag = "state", rename_all = "snake_case")]
1251pub enum Signature {
1252 #[default]
1253 Unsigned,
1254 /// Valid, made with a key the account owning the committer's verified
1255 /// address registered: that account's username.
1256 Verified { signer: String },
1257 /// Signed, but not verified: why.
1258 Unverified { reason: String },
1259}
1260
1261/// One file a commit adds, changes or deletes.
1262#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1263pub struct FileChange {
1264 pub path: String,
1265 /// Its size in bytes, when its content is new and was read.
1266 #[serde(default)]
1267 pub size: Option<u64>,
1268 #[serde(default)]
1269 pub deleted: bool,
1270}
1271
1272/// What rules about commits look at, for one commit.
1273#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1274pub struct CommitFacts {
1275 pub sha: String,
1276 /// At most 4 KiB of it.
1277 pub message: String,
1278 #[serde(default)]
1279 pub author_email: Option<String>,
1280 #[serde(default)]
1281 pub committer_email: Option<String>,
1282 pub parents: u32,
1283 #[serde(default)]
1284 pub signature: Signature,
1285 #[serde(default)]
1286 pub files: Vec<FileChange>,
1287 /// Whether `files` is every file it changes.
1288 #[serde(default)]
1289 pub files_complete: bool,
1290}
1291
1292/// `inspect_commits`: services only. The commits a branch of `source_id`
1293/// adds on top of `base_branch` of `target_id`, read as rules look at
1294/// them, at most `limit`. Returns `Outcome<InspectedCommits>`.
1295#[derive(Clone, Debug, Serialize, Deserialize)]
1296pub struct InspectCommitsArgs {
1297 pub source_id: String,
1298 pub head: String,
1299 pub target_id: String,
1300 pub base_branch: String,
1301 #[serde(default)]
1302 pub limit: Option<u32>,
1303}
1304
1305#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1306pub struct InspectedCommits {
1307 pub commits: Vec<CommitFacts>,
1308 /// Whether `commits` holds every commit the branch adds, each read in
1309 /// full. A change too large to read is not.
1310 pub complete: bool,
1311}
1312
1313/// What kind of actor a change is by, for rules that hold only for agents'
1314/// or people's changes and for the evaluation log.
1315pub fn actor_kind(actor: &User) -> &'static str {
1316 use crate::PrincipalKind;
1317 match actor.kind {
1318 PrincipalKind::System => "g1t",
1319 PrincipalKind::Agent => "agent",
1320 _ if actor.acting.is_some() => "agent",
1321 PrincipalKind::Workspace => "token",
1322 PrincipalKind::User => "person",
1323 }
1324}
1325
1326/// Whether the actor is an agent: g1t's, or another acting through an
1327/// agent token. g1t acting on its own counts as an agent.
1328pub fn is_agent(actor: &User) -> bool {
1329 matches!(actor_kind(actor), "agent" | "g1t")
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334 use super::*;
1335 use serde_json::json;
1336
1337 #[test]
1338 fn a_rule_is_its_type_and_parameters() {
1339 let entry = RuleEntry {
1340 rule: Rule::PullRequest(PullRequestRule { required_approvals: 2, ..PullRequestRule::default() }),
1341 applies_to: AppliesTo::Agents,
1342 };
1343 let value = serde_json::to_value(&entry).unwrap();
1344 assert_eq!(value["type"], "pull_request");
1345 assert_eq!(value["parameters"]["required_approvals"], 2);
1346 assert_eq!(value["applies_to"], "agents");
1347 let back: RuleEntry = serde_json::from_value(value).unwrap();
1348 assert_eq!(back, entry);
1349 }
1350
1351 #[test]
1352 fn parameters_left_out_take_their_defaults() {
1353 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion" })).unwrap();
1354 assert_eq!(entry.rule, Rule::Deletion(NoParameters {}));
1355 assert_eq!(entry.applies_to, AppliesTo::Everyone);
1356 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion", "parameters": {} })).unwrap();
1357 assert_eq!(entry.rule.kind(), "deletion");
1358 let entry: RuleEntry =
1359 serde_json::from_value(json!({ "type": "merge_queue", "parameters": { "max_entries_to_build": 8 } })).unwrap();
1360 let Rule::MergeQueue(queue) = entry.rule else { panic!() };
1361 assert_eq!((queue.max_entries_to_build, queue.check_response_timeout_minutes), (8, 45));
1362 }
1363
1364 #[test]
1365 fn every_rule_type_reads_back_as_it_is_named() {
1366 let rules = [
1367 Rule::Creation(NoParameters {}),
1368 Rule::Update(NoParameters {}),
1369 Rule::Deletion(NoParameters {}),
1370 Rule::NonFastForward(NoParameters {}),
1371 Rule::RequiredLinearHistory(NoParameters {}),
1372 Rule::RequiredSignatures(NoParameters {}),
1373 Rule::PullRequest(PullRequestRule::default()),
1374 Rule::RequiredStatusChecks(StatusChecksRule::default()),
1375 Rule::MergeQueue(MergeQueueRule::default()),
1376 Rule::RequiredDeployments(DeploymentsRule::default()),
1377 Rule::CommitMessagePattern(PatternRule::default()),
1378 Rule::CommitAuthorEmailPattern(PatternRule::default()),
1379 Rule::CommitterEmailPattern(PatternRule::default()),
1380 Rule::BranchNamePattern(PatternRule::default()),
1381 Rule::TagNamePattern(PatternRule::default()),
1382 Rule::FilePathRestriction(FilePathRule::default()),
1383 Rule::FileExtensionRestriction(FileExtensionRule::default()),
1384 Rule::MaxFileSize(MaxFileSizeRule::default()),
1385 Rule::MaxFilePathLength(MaxFilePathLengthRule::default()),
1386 Rule::MaxFilesChanged(MaxFilesChangedRule::default()),
1387 Rule::SecretScanning(NoParameters {}),
1388 Rule::ConfidenceThreshold(ConfidenceRule::default()),
1389 Rule::CostCap(CostCapRule::default()),
1390 Rule::PathReview(PathReviewRule::default()),
1391 Rule::MergeWindow(MergeWindowRule::default()),
1392 Rule::AgentAutoMerge(AgentAutoMergeRule::default()),
1393 ];
1394 for rule in rules {
1395 let value = serde_json::to_value(RuleEntry::everyone(rule.clone())).unwrap();
1396 assert_eq!(value["type"], rule.kind());
1397 let back: RuleEntry = serde_json::from_value(value).unwrap();
1398 assert_eq!(back.rule, rule);
1399 assert!(!rule.label().is_empty());
1400 }
1401 }
1402
1403 #[test]
1404 fn a_ruleset_reads_as_the_api_shows_it() {
1405 let ruleset: RulesetSpec = serde_json::from_value(json!({
1406 "name": "Protect main",
1407 "enforcement": "evaluate",
1408 "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH", "release/**"], "exclude": [] } },
1409 "bypass_actors": [{ "kind": "role", "value": "admin", "mode": "pull_requests" }, { "kind": "g1t" }],
1410 "rules": [{ "type": "non_fast_forward" }, { "type": "required_status_checks", "parameters": { "checks": [{ "context": "CI", "integration": "actions" }], "strict": true } }]
1411 }))
1412 .unwrap();
1413 assert_eq!(ruleset.enforcement, Enforcement::Evaluate);
1414 assert_eq!(ruleset.target, Target::Branch);
1415 assert_eq!(ruleset.bypass_actors[1], BypassActor { kind: ActorKind::G1t, value: String::new(), mode: BypassMode::Always });
1416 let Rule::RequiredStatusChecks(checks) = &ruleset.rules[1].rule else { panic!() };
1417 assert_eq!(checks.checks[0].integration, Some(Integration::Actions));
1418 assert!(checks.strict);
1419 }
1420
1421 #[test]
1422 fn refs_split_into_their_target_and_name() {
1423 assert_eq!(Target::of_ref("refs/heads/release/1.x"), Some((Target::Branch, "release/1.x")));
1424 assert_eq!(Target::of_ref("refs/tags/v1"), Some((Target::Tag, "v1")));
1425 assert_eq!(Target::of_ref("refs/notes/x"), None);
1426 assert_eq!(Target::Tag.full_ref("v2"), "refs/tags/v2");
1427 }
1428
1429 #[test]
1430 fn whose_change_it_is() {
1431 assert!(AppliesTo::Everyone.covers(true) && AppliesTo::Everyone.covers(false));
1432 assert!(AppliesTo::Agents.covers(true) && !AppliesTo::Agents.covers(false));
1433 assert!(AppliesTo::People.covers(false) && !AppliesTo::People.covers(true));
1434 let person = User { id: "usr_1".into(), username: "ada".into(), ..User::default() };
1435 assert_eq!(actor_kind(&person), "person");
1436 assert!(!is_agent(&person));
1437 assert!(is_agent(&User::system("acme")));
1438 }
1439}