summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMark Rousskov <mark.simulacrum@gmail.com>2019-07-13 17:18:41 -0400
committerGitHub <noreply@github.com>2019-07-13 17:18:41 -0400
commit732feec5e8db47846ae2fbfe69fbf028090a809d (patch)
tree336c1390b505d6f08b8fbf247d06da20a57bf677 /src
parent14a42ebabfd56eb50447d8d8c8d4e2b1235cf1c7 (diff)
parent5fde6a68757402d50fa312ea75103c083f9483af (diff)
Merge pull request #87 from rust-lang/github-ids
Store GitHub IDs in the repo
Diffstat (limited to 'src')
-rw-r--r--src/github.rs152
-rw-r--r--src/main.rs71
-rw-r--r--src/permissions.rs21
-rw-r--r--src/schema.rs7
-rw-r--r--src/static_api.rs14
-rw-r--r--src/validate.rs40
6 files changed, 281 insertions, 24 deletions
diff --git a/src/github.rs b/src/github.rs
new file mode 100644
index 0000000..3810947
--- /dev/null
+++ b/src/github.rs
@@ -0,0 +1,152 @@
+use failure::{bail, Error};
+use reqwest::header::{self, HeaderValue};
+use reqwest::{Client, Method, RequestBuilder};
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+static API_BASE: &str = "https://api.github.com/";
+static TOKEN_VAR: &str = "GITHUB_TOKEN";
+
+#[derive(serde::Deserialize)]
+pub(crate) struct User {
+ pub(crate) login: String,
+ pub(crate) name: Option<String>,
+ pub(crate) email: Option<String>,
+}
+
+#[derive(serde::Deserialize)]
+struct GraphResult<T> {
+ data: Option<T>,
+ #[serde(default)]
+ errors: Vec<GraphError>,
+}
+
+#[derive(serde::Deserialize)]
+struct GraphError {
+ message: String,
+}
+
+#[derive(serde::Deserialize)]
+struct GraphNodes<T> {
+ nodes: Vec<Option<T>>,
+}
+
+pub(crate) struct GitHubApi {
+ http: Client,
+ token: Option<String>,
+}
+
+impl GitHubApi {
+ pub(crate) fn new() -> Self {
+ GitHubApi {
+ http: Client::new(),
+ token: std::env::var(TOKEN_VAR).ok(),
+ }
+ }
+
+ fn prepare(
+ &self,
+ require_auth: bool,
+ method: Method,
+ url: &str,
+ ) -> Result<RequestBuilder, Error> {
+ let url = if url.starts_with("https://") {
+ Cow::Borrowed(url)
+ } else {
+ Cow::Owned(format!("{}{}", API_BASE, url))
+ };
+ if require_auth {
+ self.require_auth()?;
+ }
+
+ let mut req = self.http.request(method, url.as_ref());
+ if let Some(token) = &self.token {
+ req = req.header(
+ header::AUTHORIZATION,
+ HeaderValue::from_str(&format!("token {}", token))?,
+ );
+ }
+ Ok(req)
+ }
+
+ fn graphql<R, V>(&self, query: &str, variables: V) -> Result<R, Error>
+ where
+ R: serde::de::DeserializeOwned,
+ V: serde::Serialize,
+ {
+ #[derive(serde::Serialize)]
+ struct Request<'a, V> {
+ query: &'a str,
+ variables: V,
+ }
+ let res: GraphResult<R> = self
+ .prepare(true, Method::POST, "graphql")?
+ .json(&Request { query, variables })
+ .send()?
+ .error_for_status()?
+ .json()?;
+ if let Some(error) = res.errors.iter().next() {
+ bail!("graphql error: {}", error.message);
+ } else if let Some(data) = res.data {
+ Ok(data)
+ } else {
+ bail!("missing graphql data");
+ }
+ }
+
+ pub(crate) fn require_auth(&self) -> Result<(), Error> {
+ if self.token.is_none() {
+ bail!("missing environment variable {}", TOKEN_VAR);
+ }
+ Ok(())
+ }
+
+ pub(crate) fn user(&self, login: &str) -> Result<User, Error> {
+ Ok(self
+ .prepare(false, Method::GET, &format!("users/{}", login))?
+ .send()?
+ .error_for_status()?
+ .json()?)
+ }
+
+ pub(crate) fn usernames(&self, ids: &[usize]) -> Result<HashMap<usize, String>, Error> {
+ #[derive(serde::Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ struct Usernames {
+ database_id: usize,
+ login: String,
+ }
+ #[derive(serde::Serialize)]
+ struct Params {
+ ids: Vec<String>,
+ }
+ static QUERY: &str = "
+ query($ids: [ID!]!) {
+ nodes(ids: $ids) {
+ ... on User {
+ databaseId
+ login
+ }
+ }
+ }
+ ";
+
+ let mut result = HashMap::new();
+ for chunk in ids.chunks(100) {
+ let res: GraphNodes<Usernames> = self.graphql(
+ QUERY,
+ Params {
+ ids: chunk.iter().map(|id| user_node_id(*id)).collect(),
+ },
+ )?;
+ for node in res.nodes.into_iter().filter_map(|n| n) {
+ result.insert(node.database_id, node.login);
+ }
+ }
+ Ok(result)
+ }
+}
+
+fn user_node_id(id: usize) -> String {
+ base64::encode(&format!("04:User{}", id))
+}
diff --git a/src/main.rs b/src/main.rs
index 1b45257..d8669bf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,12 +3,14 @@
mod data;
#[macro_use]
mod permissions;
+mod github;
mod schema;
mod static_api;
mod validate;
use crate::data::Data;
use failure::{err_msg, Error};
+use log::{error, info, warn};
use std::path::PathBuf;
use structopt::StructOpt;
@@ -16,7 +18,15 @@ use structopt::StructOpt;
#[structopt(name = "team", about = "manage the rust team members")]
enum Cli {
#[structopt(name = "check", help = "check if the configuration is correct")]
- Check,
+ Check {
+ #[structopt(long = "strict", help = "fail if optional checks are not executed")]
+ strict: bool,
+ },
+ #[structopt(
+ name = "add-person",
+ help = "add a new person from their GitHub profile"
+ )]
+ AddPerson { github_name: String },
#[structopt(name = "static-api", help = "generate the static API")]
StaticApi { dest: String },
#[structopt(name = "dump-team", help = "print the members of a team")]
@@ -36,11 +46,19 @@ enum Cli {
}
fn main() {
- env_logger::init();
+ let mut env = env_logger::Builder::new();
+ env.default_format_timestamp(false);
+ env.default_format_module_path(false);
+ env.filter_module("rust_team", log::LevelFilter::Info);
+ if let Ok(content) = std::env::var("RUST_LOG") {
+ env.parse(&content);
+ }
+ env.init();
+
if let Err(e) = run() {
- eprintln!("error: {}", e);
+ error!("{}", e);
for e in e.iter_causes() {
- eprintln!(" cause: {}", e);
+ error!("cause: {}", e);
}
std::process::exit(1);
}
@@ -50,8 +68,46 @@ fn run() -> Result<(), Error> {
let cli = Cli::from_args();
let data = Data::load()?;
match cli {
- Cli::Check => {
- crate::validate::validate(&data)?;
+ Cli::Check { strict } => {
+ crate::validate::validate(&data, strict)?;
+ }
+ Cli::AddPerson { ref github_name } => {
+ #[derive(serde::Serialize)]
+ struct PersonToAdd<'a> {
+ name: &'a str,
+ github: &'a str,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ email: Option<&'a str>,
+ }
+
+ let github = github::GitHubApi::new();
+ let user = github.user(github_name)?;
+ let github_name = user.login;
+
+ if data.person(&github_name).is_some() {
+ failure::bail!("person already in the repo: {}", github_name);
+ }
+
+ let file = format!("people/{}.toml", github_name);
+ std::fs::write(
+ &file,
+ toml::to_string_pretty(&PersonToAdd {
+ name: user.name.as_ref().map(|n| n.as_str()).unwrap_or_else(|| {
+ warn!(
+ "the person is missing the name on GitHub, defaulting to the username"
+ );
+ github_name.as_str()
+ }),
+ github: &github_name,
+ email: user.email.as_ref().map(|e| e.as_str()).or_else(|| {
+ warn!("the person is missing the email on GitHub, leaving the field empty");
+ None
+ }),
+ })?
+ .as_bytes(),
+ )?;
+
+ info!("written data to {}", file);
}
Cli::StaticApi { ref dest } => {
let dest = PathBuf::from(dest);
@@ -100,8 +156,9 @@ fn run() -> Result<(), Error> {
if !crate::schema::Permissions::AVAILABLE.contains(&name.as_str()) {
failure::bail!("unknown permission: {}", name);
}
- let mut allowed = crate::permissions::allowed_github_users(&data, name)?
+ let mut allowed = crate::permissions::allowed_people(&data, name)?
.into_iter()
+ .map(|person| person.github())
.collect::<Vec<_>>();
allowed.sort();
for github_username in &allowed {
diff --git a/src/permissions.rs b/src/permissions.rs
index 5901cb8..c4d2454 100644
--- a/src/permissions.rs
+++ b/src/permissions.rs
@@ -1,4 +1,5 @@
use crate::data::Data;
+use crate::schema::Person;
use failure::{bail, Error};
use std::collections::HashSet;
@@ -149,22 +150,20 @@ permissions! {
}
}
-pub(crate) fn allowed_github_users(
- data: &Data,
+pub(crate) fn allowed_people<'a>(
+ data: &'a Data,
permission: &str,
-) -> Result<HashSet<String>, Error> {
- let mut github_users = HashSet::new();
+) -> Result<Vec<&'a Person>, Error> {
+ let mut members_with_perms = HashSet::new();
for team in data.teams() {
if team.permissions().has(permission) {
for member in team.members(&data)? {
- github_users.insert(member.to_string());
+ members_with_perms.insert(member);
}
}
}
- for person in data.people() {
- if person.permissions().has(permission) {
- github_users.insert(person.github().to_string());
- }
- }
- Ok(github_users)
+ Ok(data
+ .people()
+ .filter(|p| members_with_perms.contains(p.github()) || p.permissions().has(permission))
+ .collect())
}
diff --git a/src/schema.rs b/src/schema.rs
index c7a1b48..570b212 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -38,10 +38,11 @@ pub(crate) enum Email<'a> {
}
#[derive(serde_derive::Deserialize, Debug)]
-#[serde(deny_unknown_fields)]
+#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct Person {
name: String,
github: String,
+ github_id: usize,
irc: Option<String>,
#[serde(default)]
email: EmailField,
@@ -59,6 +60,10 @@ impl Person {
&self.github
}
+ pub(crate) fn github_id(&self) -> usize {
+ self.github_id
+ }
+
#[allow(unused)]
pub(crate) fn irc(&self) -> &str {
if let Some(irc) = &self.irc {
diff --git a/src/static_api.rs b/src/static_api.rs
index c752bc3..15e1dcd 100644
--- a/src/static_api.rs
+++ b/src/static_api.rs
@@ -40,6 +40,7 @@ impl<'a> Generator<'a> {
members.push(v1::TeamMember {
name: person.name().into(),
github: (*github_name).into(),
+ github_id: person.github_id(),
is_lead: leads.contains(github_name),
});
}
@@ -101,13 +102,20 @@ impl<'a> Generator<'a> {
fn generate_permissions(&self) -> Result<(), Error> {
for perm in Permissions::AVAILABLE {
- let mut github_users = crate::permissions::allowed_github_users(&self.data, perm)?
- .into_iter()
+ let allowed = crate::permissions::allowed_people(&self.data, perm)?;
+ let mut github_users = allowed
+ .iter()
+ .map(|p| p.github().to_string())
.collect::<Vec<_>>();
+ let mut github_ids = allowed.iter().map(|p| p.github_id()).collect::<Vec<_>>();
github_users.sort();
+ github_ids.sort();
self.add(
&format!("v1/permissions/{}.json", perm),
- &v1::Permission { github_users },
+ &v1::Permission {
+ github_users,
+ github_ids,
+ },
)?;
}
Ok(())
diff --git a/src/validate.rs b/src/validate.rs
index 87f2354..4c4ebfb 100644
--- a/src/validate.rs
+++ b/src/validate.rs
@@ -1,6 +1,8 @@
use crate::data::Data;
+use crate::github::GitHubApi;
use crate::schema::{Email, Permissions};
use failure::{bail, Error};
+use log::{error, warn};
use regex::Regex;
use std::collections::{HashMap, HashSet};
@@ -23,19 +25,35 @@ static CHECKS: &[fn(&Data, &mut Vec<String>)] = &[
validate_team_names,
];
-pub(crate) fn validate(data: &Data) -> Result<(), Error> {
+static GITHUB_CHECKS: &[fn(&Data, &GitHubApi, &mut Vec<String>)] = &[validate_github_usernames];
+
+pub(crate) fn validate(data: &Data, strict: bool) -> Result<(), Error> {
let mut errors = Vec::new();
for check in CHECKS {
check(data, &mut errors);
}
+ let github = GitHubApi::new();
+ if let Err(err) = github.require_auth() {
+ if strict {
+ return Err(err);
+ } else {
+ warn!("couldn't perform checks relying on the GitHub API, some errors will not be detected");
+ warn!("cause: {}", err);
+ }
+ } else {
+ for check in GITHUB_CHECKS {
+ check(data, &github, &mut errors);
+ }
+ }
+
if !errors.is_empty() {
errors.sort();
errors.dedup_by(|a, b| a == b);
for err in &errors {
- eprintln!("validation error: {}", err);
+ error!("validation error: {}", err);
}
bail!("{} validation errors found", errors.len());
@@ -363,6 +381,24 @@ fn validate_team_names(data: &Data, errors: &mut Vec<String>) {
});
}
+/// Ensure there are no misspelled GitHub account names
+fn validate_github_usernames(data: &Data, github: &GitHubApi, errors: &mut Vec<String>) {
+ let people = data
+ .people()
+ .map(|p| (p.github_id(), p))
+ .collect::<HashMap<_, _>>();
+ match github.usernames(&people.keys().cloned().collect::<Vec<_>>()) {
+ Ok(res) => wrapper(res.iter(), errors, |(id, name), _| {
+ let original = people[id].github();
+ if original != name {
+ bail!("user `{}` changed username to `{}`", original, name);
+ }
+ Ok(())
+ }),
+ Err(err) => errors.push(format!("couldn't verify GitHub usernames: {}", err)),
+ }
+}
+
fn wrapper<T, I, F>(iter: I, errors: &mut Vec<String>, mut func: F)
where
I: Iterator<Item = T>,