Merge pull request #1516 from zed-industries/bootstrap-script

Max Brunsfeld created

Add bootstrap script, avoid hard-coding zed team members

Change summary

crates/collab/migrations/20210527024318_initial_schema.sql |  6 
crates/collab/src/bin/seed.rs                              | 85 ++++---
crates/collab/src/db.rs                                    |  2 
script/bootstrap                                           | 13 +
script/seed-db                                             |  4 
script/sqlx                                                |  2 
6 files changed, 63 insertions(+), 49 deletions(-)

Detailed changes

crates/collab/src/bin/seed.rs 🔗

@@ -1,8 +1,7 @@
-use clap::Parser;
 use collab::{Error, Result};
 use db::{Db, PostgresDb, UserId};
 use rand::prelude::*;
-use serde::Deserialize;
+use serde::{de::DeserializeOwned, Deserialize};
 use std::fmt::Write;
 use time::{Duration, OffsetDateTime};
 
@@ -10,62 +9,52 @@ use time::{Duration, OffsetDateTime};
 #[path = "../db.rs"]
 mod db;
 
-#[derive(Parser)]
-struct Args {
-    /// Seed users from GitHub.
-    #[clap(short, long)]
-    github_users: bool,
-}
-
 #[derive(Debug, Deserialize)]
 struct GitHubUser {
     id: usize,
     login: String,
+    email: Option<String>,
 }
 
 #[tokio::main]
 async fn main() {
-    let args = Args::parse();
     let mut rng = StdRng::from_entropy();
     let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
     let db = PostgresDb::new(&database_url, 5)
         .await
         .expect("failed to connect to postgres database");
+    let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
+    let client = reqwest::Client::new();
+
+    let current_user =
+        fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
+    let staff_users = fetch_github::<Vec<GitHubUser>>(
+        &client,
+        &github_token,
+        "https://api.github.com/orgs/zed-industries/teams/staff/members",
+    )
+    .await;
 
-    let mut zed_users = vec![
-        ("nathansobo".to_string(), Some("nathan@zed.dev")),
-        ("maxbrunsfeld".to_string(), Some("max@zed.dev")),
-        ("as-cii".to_string(), Some("antonio@zed.dev")),
-        ("iamnbutler".to_string(), Some("nate@zed.dev")),
-        ("gibusu".to_string(), Some("greg@zed.dev")),
-        ("Kethku".to_string(), Some("keith@zed.dev")),
-    ];
+    let mut zed_users = Vec::new();
+    zed_users.push((current_user, true));
+    zed_users.extend(staff_users.into_iter().map(|user| (user, true)));
 
-    if args.github_users {
-        let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
-        let client = reqwest::Client::new();
+    let user_count = db
+        .get_all_users(0, 200)
+        .await
+        .expect("failed to load users from db")
+        .len();
+    if user_count < 100 {
         let mut last_user_id = None;
-        for page in 0..20 {
-            println!("Downloading users from GitHub, page {}", page);
+        for _ in 0..10 {
             let mut uri = "https://api.github.com/users?per_page=100".to_string();
             if let Some(last_user_id) = last_user_id {
                 write!(&mut uri, "&since={}", last_user_id).unwrap();
             }
-            let response = client
-                .get(uri)
-                .bearer_auth(&github_token)
-                .header("user-agent", "zed")
-                .send()
-                .await
-                .expect("failed to fetch github users");
-            let users = response
-                .json::<Vec<GitHubUser>>()
-                .await
-                .expect("failed to deserialize github user");
-            zed_users.extend(users.iter().map(|user| (user.login.clone(), None)));
-
+            let users = fetch_github::<Vec<GitHubUser>>(&client, &github_token, &uri).await;
             if let Some(last_user) = users.last() {
                 last_user_id = Some(last_user.id);
+                zed_users.extend(users.into_iter().map(|user| (user, false)));
             } else {
                 break;
             }
@@ -73,16 +62,16 @@ async fn main() {
     }
 
     let mut zed_user_ids = Vec::<UserId>::new();
-    for (zed_user, email) in zed_users {
+    for (github_user, admin) in zed_users {
         if let Some(user) = db
-            .get_user_by_github_login(&zed_user)
+            .get_user_by_github_login(&github_user.login)
             .await
             .expect("failed to fetch user")
         {
             zed_user_ids.push(user.id);
         } else {
             zed_user_ids.push(
-                db.create_user(&zed_user, email, true)
+                db.create_user(&github_user.login, github_user.email.as_deref(), admin)
                     .await
                     .expect("failed to insert user"),
             );
@@ -140,3 +129,21 @@ async fn main() {
             .expect("failed to insert channel membership");
     }
 }
+
+async fn fetch_github<T: DeserializeOwned>(
+    client: &reqwest::Client,
+    access_token: &str,
+    url: &str,
+) -> T {
+    let response = client
+        .get(url)
+        .bearer_auth(&access_token)
+        .header("user-agent", "zed")
+        .send()
+        .await
+        .expect(&format!("failed to fetch '{}'", url));
+    response
+        .json()
+        .await
+        .expect(&format!("failed to deserialize github user from '{}'", url))
+}

crates/collab/src/db.rs 🔗

@@ -709,7 +709,7 @@ impl Db for PostgresDb {
                 user_durations.user_id = project_durations.user_id AND
                 user_durations.user_id = users.id AND
                 project_durations.project_id = project_collaborators.project_id
-            ORDER BY total_duration DESC, user_id ASC
+            ORDER BY total_duration DESC, user_id ASC, project_id ASC
         ";
 
         let mut rows = sqlx::query_as::<_, (UserId, String, ProjectId, i64, i64)>(query)

script/bootstrap 🔗

@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+echo "installing foreman..."
+which foreman > /dev/null || brew install foreman
+
+echo "creating database..."
+script/sqlx database create
+
+echo "migrating database..."
+script/sqlx migrate run
+
+echo "seeding database..."
+script/seed-db

script/seed-db 🔗

@@ -4,6 +4,6 @@ set -e
 cd crates/collab
 
 # Export contents of .env.toml
-eval "$(cargo run --bin dotenv)"
+eval "$(cargo run --quiet --bin dotenv)"
 
-cargo run --package=collab --features seed-support --bin seed -- $@
+cargo run --quiet --package=collab --features seed-support --bin seed -- $@

script/sqlx 🔗

@@ -8,7 +8,7 @@ set -e
 cd crates/collab
 
 # Export contents of .env.toml
-eval "$(cargo run --bin dotenv)"
+eval "$(cargo run --quiet --bin dotenv)"
 
 # Run sqlx command
 sqlx $@