Skip to content

Commit d9ec7fa

Browse files
committed
use rulesets in the rust repo
1 parent a041d4e commit d9ec7fa

File tree

5 files changed

+176
-36
lines changed

5 files changed

+176
-36
lines changed

config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ enable-rulesets-repos = [
155155
"rust-lang/rustc-perf",
156156
"rust-lang/rustc-stable-hash",
157157
"rust-lang/rustfmt",
158+
"rust-lang/rust",
158159
"rust-lang/rustup",
159160
"rust-lang/simpleinfra",
160161
"rust-lang/socket2",

repos/rust-lang/rust.toml

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,67 +26,131 @@ rust-analyzer = "write"
2626
style = "write"
2727
types = "write"
2828
triage = "write"
29+
# needed for the branch protection
30+
rust-timer = "write"
2931

32+
# Only `bors` can push to `main`
3033
[[branch-protections]]
34+
name = "main"
3135
pattern = "main"
3236
allowed-merge-apps = ["bors"]
37+
prevent-update = true
3338

39+
# No one can force push to main
3440
[[branch-protections]]
41+
name = "main - force-push"
42+
pattern = "main"
43+
# `main` already requires users to open PRs.
44+
# This allows bors to push without opening a PR.
45+
pr-required = false
46+
47+
# Only `bors` and `promote-release` can push to `stable`
48+
[[branch-protections]]
49+
name = "stable"
3550
pattern = "stable"
36-
allowed-merge-apps = ["bors"]
51+
allowed-merge-apps = ["bors", "promote-release"]
52+
prevent-update = true
53+
54+
# Only `promote-release` can force-push to `stable`
55+
[[branch-protections]]
56+
name = "stable - force-push"
57+
pattern = "stable"
58+
allowed-merge-apps = ["promote-release"]
59+
# `stable` already requires users to open PRs.
60+
# This allows bors to push without opening a PR.
61+
pr-required = false
62+
63+
# No one create or delete the stable branch.
64+
[[branch-protections]]
65+
name = "stable - misc"
66+
pattern = "stable"
67+
pr-required = false
68+
prevent-force-push = false
3769

70+
# Only `bors` and `promote-release` can push to `beta`
3871
[[branch-protections]]
72+
name = "beta"
3973
pattern = "beta"
40-
allowed-merge-apps = ["bors"]
74+
allowed-merge-apps = ["bors", "promote-release"]
75+
prevent-update = true
76+
77+
# Only `promote-release` can force-push to `beta`
78+
[[branch-protections]]
79+
name = "beta - force-push"
80+
pattern = "beta"
81+
allowed-merge-apps = ["promote-release"]
82+
# `beta` already requires users to open PRs.
83+
# This allows bors to push without opening a PR.
84+
pr-required = false
85+
86+
# no one can create or delete the beta branch.
87+
[[branch-protections]]
88+
name = "beta - misc"
89+
pattern = "beta"
90+
pr-required = false
91+
prevent-force-push = false
4192

4293
[[branch-protections]]
4394
pattern = "*"
95+
allowed-merge-apps = ["promote-release"]
96+
prevent-deletion = false
97+
prevent-update = true
4498

4599
[[branch-protections]]
46100
pattern = "*/**/*"
47101
pr-required = false
102+
prevent-update = true
103+
prevent-deletion = false
48104

49105
[[branch-protections]]
50106
pattern = "cargo_update"
51107
pr-required = false
108+
prevent-deletion = false
109+
prevent-force-push = false
52110

53111
# Required for running try builds created by bors.
54112
# Must support force-pushes.
55113
[[branch-protections]]
56114
pattern = "automation/bors/try"
57115
allowed-merge-apps = ["bors"]
116+
prevent-update = true
58117

59118
# Required for running try builds created by bors.
60119
# Must support force-pushes.
61120
[[branch-protections]]
62121
pattern = "automation/bors/try-merge"
63122
allowed-merge-apps = ["bors"]
123+
prevent-update = true
64124

65125
# Required for running auto builds created by bors.
66126
# Must support force-pushes.
67127
[[branch-protections]]
68128
pattern = "automation/bors/auto"
69129
allowed-merge-apps = ["bors"]
130+
prevent-update = true
70131

71132
# Required for running auto builds created by bors.
72133
# Must support force-pushes.
73134
[[branch-protections]]
74135
pattern = "automation/bors/auto-merge"
75136
allowed-merge-apps = ["bors"]
137+
prevent-update = true
76138

77139
# Required for unrolled PR builds created by perfbot.
78140
# Must support force-pushes.
79141
[[branch-protections]]
80142
pattern = "try-perf"
81-
allowed-merge-apps = ["rust-timer"]
143+
allowed-merge-teams = ["rust-timer"]
82144
pr-required = false
145+
prevent-update = true
83146

84147
# Required for unrolled PR builds created by perfbot.
85148
# Must support force-pushes.
86149
[[branch-protections]]
87150
pattern = "perf-tmp"
88-
allowed-merge-apps = ["rust-timer"]
151+
allowed-merge-teams = ["rust-timer"]
89152
pr-required = false
153+
prevent-update = true
90154

91155
[environments.bors]
92156
branches = ["automation/bors/auto", "automation/bors/try", "try-perf"]

src/sync/github/mod.rs

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::sync::Config;
99
use crate::sync::github::api::{
1010
GithubRead, Login, PushAllowanceActor, RepoPermission, RepoSettings, Ruleset,
1111
};
12+
use anyhow::Context as _;
1213
use futures_util::StreamExt;
1314
use log::debug;
1415
use rust_team_data::v1::{Bot, BranchProtectionMode, MergeBot, ProtectionTarget};
@@ -447,6 +448,78 @@ impl SyncGitHub {
447448
self.config.enable_rulesets_repos.contains(&repo_full_name)
448449
}
449450

451+
async fn construct_ruleset(
452+
&self,
453+
expected_repo: &rust_team_data::v1::Repo,
454+
branch_protection: &rust_team_data::v1::BranchProtection,
455+
) -> anyhow::Result<api::Ruleset> {
456+
let bypass_actors = self.bypass_actors(expected_repo, branch_protection).await?;
457+
458+
Ok(construct_ruleset(branch_protection, bypass_actors))
459+
}
460+
461+
async fn bypass_actors(
462+
&self,
463+
expected_repo: &rust_team_data::v1::Repo,
464+
branch_protection: &rust_team_data::v1::BranchProtection,
465+
) -> Result<Vec<api::RulesetBypassActor>, anyhow::Error> {
466+
use api::{RulesetActorType, RulesetBypassActor, RulesetBypassMode};
467+
468+
let mut bypass_actors = Vec::new();
469+
let allowed_teams = self
470+
.allowed_merge_teams(expected_repo, branch_protection)
471+
.await?;
472+
bypass_actors.extend(allowed_teams);
473+
let allowed_apps = branch_protection
474+
.allowed_merge_apps
475+
.iter()
476+
.filter_map(|app| {
477+
app.app_id().map(|app_id| RulesetBypassActor {
478+
actor_id: app_id,
479+
actor_type: RulesetActorType::Integration,
480+
bypass_mode: RulesetBypassMode::Always,
481+
})
482+
});
483+
bypass_actors.extend(allowed_apps);
484+
Ok(bypass_actors)
485+
}
486+
487+
async fn allowed_merge_teams(
488+
&self,
489+
expected_repo: &rust_team_data::v1::Repo,
490+
branch_protection: &rust_team_data::v1::BranchProtection,
491+
) -> Result<Vec<api::RulesetBypassActor>, anyhow::Error> {
492+
use api::{RulesetActorType, RulesetBypassActor, RulesetBypassMode};
493+
494+
let mut allowed = vec![];
495+
496+
for team_name in &branch_protection.allowed_merge_teams {
497+
let github_team = self
498+
.github
499+
.team(&expected_repo.org, team_name)
500+
.await?
501+
.with_context(|| {
502+
format!(
503+
"failed to find GitHub team '{team_name}' in org '{}' for repo '{}/{}'",
504+
expected_repo.org, expected_repo.org, expected_repo.name
505+
)
506+
})?;
507+
let team_id = github_team.id.with_context(|| {
508+
format!(
509+
"GitHub team '{team_name}' in org '{}' is missing an ID",
510+
expected_repo.org
511+
)
512+
})?;
513+
514+
allowed.push(RulesetBypassActor {
515+
actor_id: team_id as i64,
516+
actor_type: RulesetActorType::Team,
517+
bypass_mode: RulesetBypassMode::Always,
518+
});
519+
}
520+
Ok(allowed)
521+
}
522+
450523
async fn diff_repo(
451524
&self,
452525
expected_repo: &rust_team_data::v1::Repo,
@@ -481,7 +554,9 @@ impl SyncGitHub {
481554
let use_rulesets = self.should_use_rulesets(expected_repo);
482555
if use_rulesets {
483556
for branch_protection in &expected_repo.branch_protections {
484-
let ruleset = construct_ruleset(branch_protection);
557+
let ruleset = self
558+
.construct_ruleset(expected_repo, branch_protection)
559+
.await?;
485560
rulesets.push(ruleset);
486561
}
487562
}
@@ -799,7 +874,9 @@ impl SyncGitHub {
799874

800875
// Process each branch protection as a potential ruleset
801876
for branch_protection in &expected_repo.branch_protections {
802-
let expected_ruleset = construct_ruleset(branch_protection);
877+
let expected_ruleset = self
878+
.construct_ruleset(expected_repo, branch_protection)
879+
.await?;
803880

804881
if let Some(actual_ruleset) = rulesets_by_name.remove(&expected_ruleset.name) {
805882
let Ruleset {
@@ -1179,11 +1256,12 @@ fn github_int(value: u32) -> i32 {
11791256
i32::try_from(value).unwrap_or_else(|_| panic!("Value {value} exceeds GitHub's Int range"))
11801257
}
11811258

1182-
pub fn construct_ruleset(branch_protection: &rust_team_data::v1::BranchProtection) -> api::Ruleset {
1259+
pub fn construct_ruleset(
1260+
branch_protection: &rust_team_data::v1::BranchProtection,
1261+
bypass_actors: Vec<api::RulesetBypassActor>,
1262+
) -> api::Ruleset {
11831263
use api::*;
11841264

1185-
let branch_protection_mode = get_branch_protection_mode(branch_protection);
1186-
11871265
// Use a BTreeSet to ensure a consistent order. This avoids unnecessary diffs when the order of rules changes,
11881266
// since GitHub does not guarantee any specific order for rules.
11891267
let mut rules: BTreeSet<RulesetRule> = BTreeSet::new();
@@ -1214,22 +1292,22 @@ pub fn construct_ruleset(branch_protection: &rust_team_data::v1::BranchProtectio
12141292
// Add pull request rule if PRs are required
12151293
if let BranchProtectionMode::PrRequired {
12161294
required_approvals, ..
1217-
} = &branch_protection_mode
1295+
} = branch_protection.mode
12181296
{
12191297
rules.insert(RulesetRule::PullRequest {
12201298
parameters: PullRequestParameters {
12211299
dismiss_stale_reviews_on_push: branch_protection.dismiss_stale_review,
12221300
require_code_owner_review: REQUIRE_CODE_OWNER_REVIEW_DEFAULT,
12231301
require_last_push_approval: REQUIRE_LAST_PUSH_APPROVAL_DEFAULT,
1224-
required_approving_review_count: github_int(*required_approvals),
1302+
required_approving_review_count: github_int(required_approvals),
12251303
required_review_thread_resolution: branch_protection
12261304
.require_conversation_resolution,
12271305
},
12281306
});
12291307
}
12301308

12311309
// Add required status checks if any
1232-
if let BranchProtectionMode::PrRequired { ci_checks, .. } = &branch_protection_mode
1310+
if let BranchProtectionMode::PrRequired { ci_checks, .. } = &branch_protection.mode
12331311
&& !ci_checks.is_empty()
12341312
{
12351313
let mut checks = ci_checks.clone();
@@ -1271,19 +1349,6 @@ pub fn construct_ruleset(branch_protection: &rust_team_data::v1::BranchProtectio
12711349
rules.insert(RulesetRule::MergeQueue { parameters });
12721350
}
12731351

1274-
// Build bypass actors from allowed merge apps
1275-
let bypass_actors: Vec<RulesetBypassActor> = branch_protection
1276-
.allowed_merge_apps
1277-
.iter()
1278-
.filter_map(|app| {
1279-
app.app_id().map(|app_id| RulesetBypassActor {
1280-
actor_id: app_id,
1281-
actor_type: RulesetActorType::Integration,
1282-
bypass_mode: RulesetBypassMode::Always,
1283-
})
1284-
})
1285-
.collect();
1286-
12871352
api::Ruleset {
12881353
id: None,
12891354
name: branch_protection

src/sync/github/tests/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ fn ruleset_creation_logs_non_default_disabled_flags() {
980980
protection.prevent_deletion = false;
981981
protection.prevent_force_push = false;
982982

983-
let ruleset = construct_ruleset(&protection);
983+
let ruleset = construct_ruleset(&protection, vec![]);
984984
let mut rendered = String::new();
985985
log_ruleset(&ruleset, None, &mut rendered).unwrap();
986986

@@ -992,15 +992,17 @@ fn ruleset_creation_logs_non_default_disabled_flags() {
992992

993993
#[test]
994994
fn ruleset_updates_log_disabled_toggle_rules_as_false() {
995-
let old =
996-
construct_ruleset(&BranchProtectionBuilder::pr_required("main", &["test"], 1).build());
995+
let old = construct_ruleset(
996+
&BranchProtectionBuilder::pr_required("main", &["test"], 1).build(),
997+
vec![],
998+
);
997999

9981000
let mut new_protection = BranchProtectionBuilder::pr_required("main", &["test"], 1).build();
9991001

10001002
// Change default
10011003
new_protection.prevent_force_push = false;
10021004

1003-
let new = construct_ruleset(&new_protection);
1005+
let new = construct_ruleset(&new_protection, vec![]);
10041006
let mut rendered = String::new();
10051007
log_ruleset(&old, Some(&new), &mut rendered).unwrap();
10061008

src/validate.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,12 +1150,22 @@ fn validate_branch_protections(data: &Data, errors: &mut Vec<String>) {
11501150

11511151
wrapper(data.repos(), errors, |repo, _| {
11521152
let bors_configured = repo.bots.iter().any(|b| matches!(b, Bot::Bors));
1153-
let mut patterns = HashSet::new();
1153+
let mut pattern_counts = HashMap::new();
11541154

11551155
for protection in &repo.branch_protections {
1156-
if !patterns.insert((protection.target, &protection.pattern)) {
1156+
*pattern_counts
1157+
.entry((protection.target, protection.pattern.as_str()))
1158+
.or_insert(0usize) += 1;
1159+
}
1160+
1161+
for protection in &repo.branch_protections {
1162+
let key = (protection.target, protection.pattern.as_str());
1163+
let occurrences = pattern_counts
1164+
.get(&key)
1165+
.with_context(|| format!("pattern_counts should contain the key {key:?}"))?;
1166+
if *occurrences > 1 && protection.name.is_none() {
11571167
bail!(
1158-
r#"repo '{}' uses multiple {:?} protections with the pattern `{}`"#,
1168+
r#"repo '{}' uses multiple {:?} protections with the pattern `{}`; when multiple protections share the same target and pattern, each protection must specify `name`"#,
11591169
repo.name,
11601170
protection.target,
11611171
protection.pattern,
@@ -1166,17 +1176,15 @@ fn validate_branch_protections(data: &Data, errors: &mut Vec<String>) {
11661176
let key = (repo.org.clone(), team.clone());
11671177
if !github_teams.contains(&key) {
11681178
bail!(
1169-
r#"repo '{}' uses a branch protection for {} that mentions the '{}' github team;
1170-
but that team does not seem to exist"#,
1179+
r#"repo '{}' uses a branch protection for {} that mentions the '{}' github team; but that team does not seem to exist"#,
11711180
repo.name,
11721181
protection.pattern,
11731182
team
11741183
);
11751184
}
11761185
if !repo.access.teams.contains_key(team) {
11771186
bail!(
1178-
r#"repo '{}' uses a branch protection for {} that has an allowed merge team '{}',
1179-
but that team is not mentioned in [access.teams]"#,
1187+
r#"repo '{}' uses a branch protection for {} that has an allowed merge team '{}', but that team is not mentioned in [access.teams]"#,
11801188
repo.name,
11811189
protection.pattern,
11821190
team

0 commit comments

Comments
 (0)