diff --git a/.gitignore b/.gitignore index c7d349937..6a6c47274 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk /tests/static-api/_output +/build diff --git a/repos/team.toml b/repos/team.toml new file mode 100644 index 000000000..5d92ea3bf --- /dev/null +++ b/repos/team.toml @@ -0,0 +1,8 @@ +org = "rust-lang" +name = "team" +description = "Rust teams structure" +bots = ["bors", "highfive", "rustbot", "rust-timer"] + +[access.teams] +core = "admin" +mods = "maintain" diff --git a/rust_team_data/src/v1.rs b/rust_team_data/src/v1.rs index 227fffffa..e22f1f6e3 100644 --- a/rust_team_data/src/v1.rs +++ b/rust_team_data/src/v1.rs @@ -77,6 +77,12 @@ pub struct Teams { pub teams: IndexMap, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repos { + #[serde(flatten)] + pub repos: IndexMap>, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct List { pub address: String, @@ -130,3 +136,35 @@ pub struct ZulipMapping { /// Zulip ID to GitHub ID pub users: IndexMap, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repo { + pub org: String, + pub name: String, + pub description: String, + pub bots: Vec, + pub teams: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum Bot { + Bors, + Highfive, + Rustbot, + RustTimer, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RepoTeam { + pub name: String, + pub permission: RepoPermission, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RepoPermission { + Write, + Admin, + Maintain, +} diff --git a/src/data.rs b/src/data.rs index 539cbfa9c..ec8330696 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,4 +1,4 @@ -use crate::schema::{Config, List, Person, Team, ZulipGroup}; +use crate::schema::{Config, List, Person, Repo, Team, ZulipGroup}; use failure::{Error, ResultExt}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; @@ -9,6 +9,7 @@ use std::path::Path; pub(crate) struct Data { people: HashMap, teams: HashMap, + repos: HashMap<(String, String), Repo>, config: Config, } @@ -17,9 +18,17 @@ impl Data { let mut data = Data { people: HashMap::new(), teams: HashMap::new(), + repos: HashMap::new(), config: load_file(Path::new("config.toml"))?, }; + data.load_dir("repos", |this, repo: Repo| { + repo.validate()?; + this.repos + .insert((repo.org.clone(), repo.name.clone()), repo); + Ok(()) + })?; + data.load_dir("people", |this, person: Person| { person.validate()?; this.people.insert(person.github().to_string(), person); @@ -39,7 +48,9 @@ impl Data { T: for<'de> Deserialize<'de>, F: Fn(&mut Self, T) -> Result<(), Error>, { - for entry in std::fs::read_dir(dir)? { + for entry in std::fs::read_dir(dir) + .with_context(|e| format!("`load_dir` failed to read directory '{}': {}", dir, e))? + { let path = entry?.path(); if path.is_file() && path.extension() == Some(OsStr::new("toml")) { @@ -107,6 +118,10 @@ impl Data { } Ok(result) } + + pub(crate) fn repos(&self) -> impl Iterator { + self.repos.iter().map(|(_, repo)| repo) + } } fn load_file Deserialize<'de>>(path: &Path) -> Result { diff --git a/src/schema.rs b/src/schema.rs index 2410217de..e5995dcbb 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,7 +1,7 @@ use crate::data::Data; pub(crate) use crate::permissions::Permissions; use failure::{bail, err_msg, Error}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; #[derive(serde_derive::Deserialize, Debug)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -633,3 +633,48 @@ fn default_true() -> bool { fn default_false() -> bool { false } + +#[derive(serde_derive::Deserialize, Debug)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub(crate) struct Repo { + pub org: String, + pub name: String, + pub description: String, + pub bots: Vec, + pub access: RepoAccess, +} + +impl Repo { + const VALID_ORGS: &'static [&'static str] = &["rust-lang"]; + + pub(crate) fn validate(&self) -> Result<(), Error> { + if !Self::VALID_ORGS.contains(&self.org.as_str()) { + bail!("{} is not a valid repo org", self.org); + } + + Ok(()) + } +} + +#[derive(serde_derive::Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum Bot { + Bors, + Highfive, + Rustbot, + RustTimer, +} + +#[derive(serde_derive::Deserialize, Debug)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub(crate) struct RepoAccess { + pub teams: HashMap, +} + +#[derive(serde_derive::Deserialize, Debug)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub(crate) enum RepoPermission { + Write, + Maintain, + Admin, +} diff --git a/src/static_api.rs b/src/static_api.rs index 1cf808f79..1bf0bed95 100644 --- a/src/static_api.rs +++ b/src/static_api.rs @@ -1,5 +1,5 @@ use crate::data::Data; -use crate::schema::{Permissions, TeamKind, ZulipGroupMember}; +use crate::schema::{Bot, Permissions, RepoPermission, TeamKind, ZulipGroupMember}; use failure::Error; use indexmap::IndexMap; use log::info; @@ -23,6 +23,7 @@ impl<'a> Generator<'a> { pub(crate) fn generate(&self) -> Result<(), Error> { self.generate_teams()?; + self.generate_repos()?; self.generate_lists()?; self.generate_zulip_groups()?; self.generate_permissions()?; @@ -31,6 +32,49 @@ impl<'a> Generator<'a> { Ok(()) } + fn generate_repos(&self) -> Result<(), Error> { + let mut repos: IndexMap> = IndexMap::new(); + for r in self.data.repos() { + let repo = v1::Repo { + org: r.org.clone(), + name: r.name.clone(), + description: r.description.clone(), + bots: r + .bots + .iter() + .map(|b| match b { + Bot::Bors => v1::Bot::Bors, + Bot::Highfive => v1::Bot::Highfive, + Bot::RustTimer => v1::Bot::RustTimer, + Bot::Rustbot => v1::Bot::Rustbot, + }) + .collect(), + teams: r + .access + .teams + .iter() + .map(|(name, permission)| { + let permission = match permission { + RepoPermission::Admin => v1::RepoPermission::Admin, + RepoPermission::Write => v1::RepoPermission::Write, + RepoPermission::Maintain => v1::RepoPermission::Maintain, + }; + v1::RepoTeam { + name: name.clone(), + permission, + } + }) + .collect(), + }; + + self.add(&format!("v1/repos/{}.json", r.name), &repo)?; + repos.entry(r.org.clone()).or_default().push(repo); + } + + self.add("v1/repos.json", &v1::Repos { repos })?; + Ok(()) + } + fn generate_teams(&self) -> Result<(), Error> { let mut teams = IndexMap::new(); diff --git a/src/validate.rs b/src/validate.rs index f63c351e9..e6f4b7ab3 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -42,6 +42,7 @@ static CHECKS: &[Check)>] = checks![ validate_discord_team_members_have_discord_ids, validate_zulip_group_ids, validate_zulip_group_extra_people, + validate_repos, ]; #[allow(clippy::type_complexity)] @@ -653,6 +654,23 @@ fn validate_zulip_group_extra_people(data: &Data, errors: &mut Vec) { }); } +/// Ensure repos reference valid teams +fn validate_repos(data: &Data, errors: &mut Vec) { + wrapper(data.repos(), errors, |repo, _| { + for (team_name, _) in &repo.access.teams { + if data.team(team_name).is_none() { + bail!( + "access for {}/{} is invalid: '{}' is not the name of a team", + repo.org, + repo.name, + team_name + ); + } + } + Ok(()) + }); +} + fn wrapper(iter: I, errors: &mut Vec, mut func: F) where I: Iterator, diff --git a/tests/static-api/_expected/v1/repos.json b/tests/static-api/_expected/v1/repos.json new file mode 100644 index 000000000..4e506e064 --- /dev/null +++ b/tests/static-api/_expected/v1/repos.json @@ -0,0 +1,16 @@ +{ + "rust-lang": [ + { + "org": "rust-lang", + "name": "some_repo", + "description": "A repo!", + "bots": [], + "teams": [ + { + "name": "foo", + "permission": "admin" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/static-api/_expected/v1/repos/some_repo.json b/tests/static-api/_expected/v1/repos/some_repo.json new file mode 100644 index 000000000..508ed23f1 --- /dev/null +++ b/tests/static-api/_expected/v1/repos/some_repo.json @@ -0,0 +1,12 @@ +{ + "org": "rust-lang", + "name": "some_repo", + "description": "A repo!", + "bots": [], + "teams": [ + { + "name": "foo", + "permission": "admin" + } + ] +} \ No newline at end of file diff --git a/tests/static-api/repos/some_repo.toml b/tests/static-api/repos/some_repo.toml new file mode 100644 index 000000000..f0f7f6c2d --- /dev/null +++ b/tests/static-api/repos/some_repo.toml @@ -0,0 +1,7 @@ +org = "rust-lang" +name = "some_repo" +description = "A repo!" +bots = [] + +[access.teams] +foo = "admin" \ No newline at end of file