Skip to content

Commit 2b33998

Browse files
committed
feat: implement rulesets
1 parent c316c8e commit 2b33998

File tree

11 files changed

+564
-13
lines changed

11 files changed

+564
-13
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: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,157 @@ 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+
pub(crate) enum RulesetActorType {
529+
Integration,
530+
OrganizationAdmin,
531+
RepositoryRole,
532+
Team,
533+
}
534+
535+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
536+
#[serde(rename_all = "lowercase")]
537+
pub(crate) enum RulesetBypassMode {
538+
Always,
539+
PullRequest,
540+
}
541+
542+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
543+
pub(crate) struct RulesetConditions {
544+
#[serde(skip_serializing_if = "Option::is_none")]
545+
pub(crate) ref_name: Option<RulesetRefNameCondition>,
546+
}
547+
548+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
549+
pub(crate) struct RulesetRefNameCondition {
550+
pub(crate) include: Vec<String>,
551+
pub(crate) exclude: Vec<String>,
552+
}
553+
554+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
555+
#[serde(tag = "type", rename_all = "snake_case")]
556+
pub(crate) enum RulesetRule {
557+
Creation,
558+
Update,
559+
Deletion,
560+
RequiredLinearHistory,
561+
MergeQueue {
562+
parameters: MergeQueueParameters,
563+
},
564+
RequiredDeployments {
565+
parameters: RequiredDeploymentsParameters,
566+
},
567+
RequiredSignatures,
568+
PullRequest {
569+
parameters: PullRequestParameters,
570+
},
571+
RequiredStatusChecks {
572+
parameters: RequiredStatusChecksParameters,
573+
},
574+
NonFastForward,
575+
}
576+
577+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
578+
pub(crate) struct MergeQueueParameters {
579+
pub(crate) check_response_timeout_minutes: i32,
580+
pub(crate) grouping_strategy: MergeQueueGroupingStrategy,
581+
pub(crate) max_entries_to_build: i32,
582+
pub(crate) max_entries_to_merge: i32,
583+
pub(crate) merge_method: MergeQueueMergeMethod,
584+
pub(crate) min_entries_to_merge: i32,
585+
pub(crate) min_entries_to_merge_wait_minutes: i32,
586+
}
587+
588+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
589+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
590+
pub(crate) enum MergeQueueGroupingStrategy {
591+
Allgreen,
592+
Headgreen,
593+
}
594+
595+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
596+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
597+
pub(crate) enum MergeQueueMergeMethod {
598+
Merge,
599+
Squash,
600+
Rebase,
601+
}
602+
603+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
604+
pub(crate) struct RequiredDeploymentsParameters {
605+
pub(crate) required_deployment_environments: Vec<String>,
606+
}
607+
608+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
609+
pub(crate) struct PullRequestParameters {
610+
pub(crate) dismiss_stale_reviews_on_push: bool,
611+
pub(crate) require_code_owner_review: bool,
612+
pub(crate) require_last_push_approval: bool,
613+
pub(crate) required_approving_review_count: i32,
614+
pub(crate) required_review_thread_resolution: bool,
615+
}
616+
617+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
618+
pub(crate) struct RequiredStatusChecksParameters {
619+
#[serde(skip_serializing_if = "Option::is_none")]
620+
pub(crate) do_not_enforce_on_create: Option<bool>,
621+
pub(crate) required_status_checks: Vec<RequiredStatusCheck>,
622+
pub(crate) strict_required_status_checks_policy: bool,
623+
}
624+
625+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
626+
pub(crate) struct RequiredStatusCheck {
627+
pub(crate) context: String,
628+
#[serde(skip_serializing_if = "Option::is_none")]
629+
pub(crate) integration_id: Option<i64>,
630+
}
631+
632+
pub(crate) enum RulesetOp {
633+
CreateForRepo,
634+
UpdateRuleset(i64),
635+
}

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)