diff --git a/docs/toml-schema.md b/docs/toml-schema.md index cc6d7f4ad..520a4475e 100644 --- a/docs/toml-schema.md +++ b/docs/toml-schema.md @@ -352,4 +352,12 @@ required-approvals = 1 # can push/merge to the branch. # (optional) allowed-merge-teams = ["awesome-team"] +# Determines the merge queue bot(s) that manage pushes to this branch. +# When a bot manages the queue, some other options, like +# `required-approvals` and `pr-required` options are not valid. +# +# Currently, only the "homu" option is supported. +# When "homu" is used, "bors" has to be in the `bots` array. +# (optional) +merge-bots = ["homu"] ``` diff --git a/rust_team_data/src/v1.rs b/rust_team_data/src/v1.rs index ee1cc303b..384d82e50 100644 --- a/rust_team_data/src/v1.rs +++ b/rust_team_data/src/v1.rs @@ -219,12 +219,19 @@ pub enum BranchProtectionMode { PrNotRequired, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MergeBot { + Homu, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct BranchProtection { pub pattern: String, pub dismiss_stale_review: bool, pub mode: BranchProtectionMode, pub allowed_merge_teams: Vec, + pub merge_bots: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/src/schema.rs b/src/schema.rs index 21b820f9a..be2a524da 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -787,6 +787,12 @@ pub(crate) enum RepoPermission { Admin, } +#[derive(serde_derive::Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum MergeBot { + Homu, +} + #[derive(serde_derive::Deserialize, Debug)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub(crate) struct BranchProtection { @@ -801,4 +807,6 @@ pub(crate) struct BranchProtection { pub pr_required: bool, #[serde(default)] pub allowed_merge_teams: Vec, + #[serde(default)] + pub merge_bots: Vec, } diff --git a/src/static_api.rs b/src/static_api.rs index c58086424..e71d93363 100644 --- a/src/static_api.rs +++ b/src/static_api.rs @@ -1,5 +1,7 @@ use crate::data::Data; -use crate::schema::{Bot, Email, Permissions, RepoPermission, TeamKind, ZulipGroupMember}; +use crate::schema::{ + Bot, Email, MergeBot, Permissions, RepoPermission, TeamKind, ZulipGroupMember, +}; use anyhow::{ensure, Context as _, Error}; use indexmap::IndexMap; use log::info; @@ -59,6 +61,13 @@ impl<'a> Generator<'a> { BranchProtectionMode::PrNotRequired }, allowed_merge_teams: b.allowed_merge_teams.clone(), + merge_bots: b + .merge_bots + .iter() + .map(|bot| match bot { + MergeBot::Homu => v1::MergeBot::Homu, + }) + .collect(), }) .collect(); let managed_by_bors = r.bots.contains(&Bot::Bors); diff --git a/src/validate.rs b/src/validate.rs index 86196d43b..bf30253cd 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,6 +1,8 @@ use crate::data::Data; use crate::github::GitHubApi; -use crate::schema::{Bot, Email, Permissions, Team, TeamKind, TeamPeople, ZulipGroupMember}; +use crate::schema::{ + Bot, Email, MergeBot, Permissions, Team, TeamKind, TeamPeople, ZulipGroupMember, +}; use crate::zulip::ZulipApi; use anyhow::{bail, Error}; use log::{error, warn}; @@ -824,7 +826,8 @@ fn validate_branch_protections(data: &Data, errors: &mut Vec) { let github_teams = data.github_teams(); wrapper(data.repos(), errors, |repo, _| { - let bors_used = repo.bots.iter().any(|b| matches!(b, Bot::Bors)); + let homu_configured = repo.bots.iter().any(|b| matches!(b, Bot::Bors)); + for protection in &repo.branch_protections { for team in &protection.allowed_merge_teams { let key = (repo.org.clone(), team.clone()); @@ -858,19 +861,24 @@ but that team does not seem to exist"#, } } - if bors_used { - if protection.required_approvals.is_some() { + let managed_by_homu = protection.merge_bots.contains(&MergeBot::Homu); + if managed_by_homu { + if !homu_configured { bail!( - r#"repo '{}' uses bors and its branch protection for {} uses the `required-approvals` attribute; -please remove the attribute when using bors"#, + r#"repo '{}' uses homu to manage a branch protection for '{}', but homu is not enabled. Add "bors" to the `bots` array"#, repo.name, protection.pattern, ); } - if !protection.allowed_merge_teams.is_empty() { + if protection.required_approvals.is_some() + || protection.dismiss_stale_review + || !protection.pr_required + || !protection.allowed_merge_teams.is_empty() + { bail!( - r#"repo '{}' uses bors and its branch protection for {} uses the `allowed-merge-teams` attribute; -please remove the attribute when using bors"#, + r#"repo '{}' uses the homu merge bot, but its branch protection for {} uses invalid +attributes (`required-approvals`, `dismiss-stale-review`, `pr-required` or `allowed-merge-teams`). +Please remove the attributes when using bors"#, repo.name, protection.pattern, ); diff --git a/tests/static-api/_expected/v1/repos.json b/tests/static-api/_expected/v1/repos.json index b4290b40a..a3c0e7e58 100644 --- a/tests/static-api/_expected/v1/repos.json +++ b/tests/static-api/_expected/v1/repos.json @@ -25,7 +25,8 @@ "required_approvals": 1 } }, - "allowed_merge_teams": [] + "allowed_merge_teams": [], + "merge_bots": [] } ], "archived": true, @@ -59,7 +60,8 @@ }, "allowed_merge_teams": [ "foo" - ] + ], + "merge_bots": [] } ], "archived": false, diff --git a/tests/static-api/_expected/v1/repos/archived_repo.json b/tests/static-api/_expected/v1/repos/archived_repo.json index ed3a195bf..627537120 100644 --- a/tests/static-api/_expected/v1/repos/archived_repo.json +++ b/tests/static-api/_expected/v1/repos/archived_repo.json @@ -23,7 +23,8 @@ "required_approvals": 1 } }, - "allowed_merge_teams": [] + "allowed_merge_teams": [], + "merge_bots": [] } ], "archived": true, diff --git a/tests/static-api/_expected/v1/repos/some_repo.json b/tests/static-api/_expected/v1/repos/some_repo.json index b4aa247bc..56fe243c8 100644 --- a/tests/static-api/_expected/v1/repos/some_repo.json +++ b/tests/static-api/_expected/v1/repos/some_repo.json @@ -25,7 +25,8 @@ }, "allowed_merge_teams": [ "foo" - ] + ], + "merge_bots": [] } ], "archived": false,