Skip to content

Commit de5a87c

Browse files
committed
feat: implement rulesets
1 parent 00c975e commit de5a87c

File tree

11 files changed

+766
-14
lines changed

11 files changed

+766
-14
lines changed

config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,7 @@ members-without-zulip-id = [
8080
"therealprof",
8181
"zeenix"
8282
]
83+
84+
enable-rulesets-repos = [
85+
"rust-lang/bors"
86+
]

src/data.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ impl Data {
235235
Ok(sync_team::Config {
236236
special_org_members,
237237
independent_github_orgs: self.config.independent_github_orgs().clone(),
238+
enable_rulesets_repos: self.config.enable_rulesets_repos().clone(),
238239
})
239240
}
240241
}

src/schema.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub(crate) struct Config {
1616
// Use a BTreeSet for consistent ordering in tests
1717
special_org_members: BTreeSet<String>,
1818
members_without_zulip_id: BTreeSet<String>,
19+
#[serde(default)]
20+
enable_rulesets_repos: BTreeSet<String>,
1921
}
2022

2123
impl Config {
@@ -46,6 +48,10 @@ impl Config {
4648
pub(crate) fn members_without_zulip_id(&self) -> &BTreeSet<String> {
4749
&self.members_without_zulip_id
4850
}
51+
52+
pub(crate) fn enable_rulesets_repos(&self) -> &BTreeSet<String> {
53+
&self.enable_rulesets_repos
54+
}
4955
}
5056

5157
// This is an enum to allow two kinds of values for the email field:

sync-team/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ base64.workspace = true
1414
hyper-old-types.workspace = true
1515
serde_json.workspace = true
1616
secrecy.workspace = true
17+
indexmap.workspace = true
1718

1819
[dev-dependencies]
1920
indexmap.workspace = true

sync-team/src/github/api/mod.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,158 @@ pub(crate) struct RepoSettings {
479479
pub archived: bool,
480480
pub auto_merge_enabled: bool,
481481
}
482+
483+
/// GitHub Repository Ruleset
484+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
485+
pub(crate) struct Ruleset {
486+
#[serde(skip_serializing_if = "Option::is_none")]
487+
pub(crate) id: Option<i64>,
488+
pub(crate) name: String,
489+
pub(crate) target: RulesetTarget,
490+
pub(crate) source_type: RulesetSourceType,
491+
pub(crate) enforcement: RulesetEnforcement,
492+
#[serde(skip_serializing_if = "Option::is_none")]
493+
pub(crate) bypass_actors: Option<Vec<RulesetBypassActor>>,
494+
pub(crate) conditions: RulesetConditions,
495+
pub(crate) rules: Vec<RulesetRule>,
496+
}
497+
498+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
499+
#[serde(rename_all = "lowercase")]
500+
pub(crate) enum RulesetTarget {
501+
Branch,
502+
Tag,
503+
}
504+
505+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
506+
#[serde(rename_all = "lowercase")]
507+
pub(crate) enum RulesetSourceType {
508+
Repository,
509+
Organization,
510+
}
511+
512+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
513+
#[serde(rename_all = "lowercase")]
514+
pub(crate) enum RulesetEnforcement {
515+
Active,
516+
Disabled,
517+
Evaluate,
518+
}
519+
520+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
521+
pub(crate) struct RulesetBypassActor {
522+
pub(crate) actor_id: i64,
523+
pub(crate) actor_type: RulesetActorType,
524+
pub(crate) bypass_mode: RulesetBypassMode,
525+
}
526+
527+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
528+
#[serde(rename_all = "snake_case")]
529+
pub(crate) enum RulesetActorType {
530+
Integration,
531+
OrganizationAdmin,
532+
RepositoryRole,
533+
Team,
534+
}
535+
536+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
537+
#[serde(rename_all = "snake_case")]
538+
pub(crate) enum RulesetBypassMode {
539+
Always,
540+
PullRequest,
541+
}
542+
543+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
544+
pub(crate) struct RulesetConditions {
545+
#[serde(skip_serializing_if = "Option::is_none")]
546+
pub(crate) ref_name: Option<RulesetRefNameCondition>,
547+
}
548+
549+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
550+
pub(crate) struct RulesetRefNameCondition {
551+
pub(crate) include: Vec<String>,
552+
pub(crate) exclude: Vec<String>,
553+
}
554+
555+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
556+
#[serde(tag = "type", rename_all = "snake_case")]
557+
pub(crate) enum RulesetRule {
558+
Creation,
559+
Update,
560+
Deletion,
561+
RequiredLinearHistory,
562+
MergeQueue {
563+
parameters: MergeQueueParameters,
564+
},
565+
RequiredDeployments {
566+
parameters: RequiredDeploymentsParameters,
567+
},
568+
RequiredSignatures,
569+
PullRequest {
570+
parameters: PullRequestParameters,
571+
},
572+
RequiredStatusChecks {
573+
parameters: RequiredStatusChecksParameters,
574+
},
575+
NonFastForward,
576+
}
577+
578+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
579+
pub(crate) struct MergeQueueParameters {
580+
pub(crate) check_response_timeout_minutes: i32,
581+
pub(crate) grouping_strategy: MergeQueueGroupingStrategy,
582+
pub(crate) max_entries_to_build: i32,
583+
pub(crate) max_entries_to_merge: i32,
584+
pub(crate) merge_method: MergeQueueMergeMethod,
585+
pub(crate) min_entries_to_merge: i32,
586+
pub(crate) min_entries_to_merge_wait_minutes: i32,
587+
}
588+
589+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
590+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
591+
pub(crate) enum MergeQueueGroupingStrategy {
592+
Allgreen,
593+
Headgreen,
594+
}
595+
596+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
597+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
598+
pub(crate) enum MergeQueueMergeMethod {
599+
Merge,
600+
Squash,
601+
Rebase,
602+
}
603+
604+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
605+
pub(crate) struct RequiredDeploymentsParameters {
606+
pub(crate) required_deployment_environments: Vec<String>,
607+
}
608+
609+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
610+
pub(crate) struct PullRequestParameters {
611+
pub(crate) dismiss_stale_reviews_on_push: bool,
612+
pub(crate) require_code_owner_review: bool,
613+
pub(crate) require_last_push_approval: bool,
614+
pub(crate) required_approving_review_count: i32,
615+
pub(crate) required_review_thread_resolution: bool,
616+
}
617+
618+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
619+
pub(crate) struct RequiredStatusChecksParameters {
620+
#[serde(skip_serializing_if = "Option::is_none")]
621+
pub(crate) do_not_enforce_on_create: Option<bool>,
622+
pub(crate) required_status_checks: Vec<RequiredStatusCheck>,
623+
pub(crate) strict_required_status_checks_policy: bool,
624+
}
625+
626+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
627+
pub(crate) struct RequiredStatusCheck {
628+
pub(crate) context: String,
629+
#[serde(skip_serializing_if = "Option::is_none")]
630+
pub(crate) integration_id: Option<i64>,
631+
}
632+
633+
pub(crate) enum RulesetOp {
634+
CreateForRepo,
635+
UpdateRuleset(i64),
636+
}

sync-team/src/github/api/read.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::github::api::Ruleset;
12
use crate::github::api::{
23
BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, Repo, RepoTeam,
34
RepoUser, Team, TeamMember, TeamRole, team_node_id, url::GitHubUrl, user_node_id,
@@ -59,6 +60,14 @@ pub(crate) trait GithubRead {
5960
org: &str,
6061
repo: &str,
6162
) -> anyhow::Result<HashMap<String, Environment>>;
63+
64+
/// Get rulesets for a repository
65+
/// Returns a vector of rulesets
66+
fn repo_rulesets(
67+
&self,
68+
org: &str,
69+
repo: &str,
70+
) -> anyhow::Result<Vec<crate::github::api::Ruleset>>;
6271
}
6372

6473
pub(crate) struct GitHubApiRead {
@@ -536,4 +545,33 @@ impl GithubRead for GitHubApiRead {
536545
})
537546
.collect()
538547
}
548+
549+
fn repo_rulesets(
550+
&self,
551+
org: &str,
552+
repo: &str,
553+
) -> anyhow::Result<Vec<crate::github::api::Ruleset>> {
554+
#[derive(serde::Deserialize)]
555+
struct RulesetsResponse {
556+
#[serde(default)]
557+
rulesets: Vec<Ruleset>,
558+
}
559+
560+
let mut rulesets: Vec<Ruleset> = Vec::new();
561+
562+
// REST API endpoint for rulesets
563+
// https://docs.github.com/en/rest/repos/rules#get-all-repository-rulesets
564+
self.client.rest_paginated(
565+
&Method::GET,
566+
&GitHubUrl::repos(org, repo, "rulesets")?,
567+
|resp: RulesetsResponse| {
568+
for ruleset in resp.rulesets {
569+
rulesets.push(ruleset);
570+
}
571+
Ok(())
572+
},
573+
)?;
574+
575+
Ok(rulesets)
576+
}
539577
}

sync-team/src/github/api/write.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,4 +766,53 @@ impl GitHubWrite {
766766
}
767767
Ok(())
768768
}
769+
770+
/// Create or update a ruleset for a repository
771+
pub(crate) fn upsert_ruleset(
772+
&self,
773+
op: crate::github::api::RulesetOp,
774+
org: &str,
775+
repo: &str,
776+
ruleset: &crate::github::api::Ruleset,
777+
) -> anyhow::Result<()> {
778+
use crate::github::api::RulesetOp;
779+
780+
match op {
781+
RulesetOp::CreateForRepo => {
782+
debug!("Creating ruleset '{}' in '{}/{}'", ruleset.name, org, repo);
783+
if !self.dry_run {
784+
// REST API: POST /repos/{owner}/{repo}/rulesets
785+
// https://docs.github.com/en/rest/repos/rules#create-a-repository-ruleset
786+
let url = GitHubUrl::repos(org, repo, "rulesets")?;
787+
self.client.send(Method::POST, &url, ruleset)?;
788+
}
789+
}
790+
RulesetOp::UpdateRuleset(id) => {
791+
debug!(
792+
"Updating ruleset '{}' (id: {}) in '{}/{}'",
793+
ruleset.name, id, org, repo
794+
);
795+
if !self.dry_run {
796+
// REST API: PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}
797+
// https://docs.github.com/en/rest/repos/rules#update-a-repository-ruleset
798+
let url = GitHubUrl::repos(org, repo, &format!("rulesets/{}", id))?;
799+
self.client.send(Method::PUT, &url, ruleset)?;
800+
}
801+
}
802+
}
803+
Ok(())
804+
}
805+
806+
/// Delete a ruleset from a repository
807+
pub(crate) fn delete_ruleset(&self, org: &str, repo: &str, id: i64) -> anyhow::Result<()> {
808+
debug!("Deleting ruleset id {} from '{}/{}'", id, org, repo);
809+
if !self.dry_run {
810+
// REST API: DELETE /repos/{owner}/{repo}/rulesets/{ruleset_id}
811+
// https://docs.github.com/en/rest/repos/rules#delete-a-repository-ruleset
812+
let url = GitHubUrl::repos(org, repo, &format!("rulesets/{}", id))?;
813+
self.client
814+
.send(Method::DELETE, &url, &serde_json::json!({}))?;
815+
}
816+
Ok(())
817+
}
769818
}

0 commit comments

Comments
 (0)