diff --git a/scripts/github-repos/README.md b/scripts/github-repos/README.md index 413110f..9c7cc66 100644 --- a/scripts/github-repos/README.md +++ b/scripts/github-repos/README.md @@ -1,11 +1,11 @@ # Bulk creation/deletion of many repos and cs169a-team ```text -Usage: ./github-repos.rb [required options] [invite|repos|remove|remove_access] +Usage: ./github-repos.rb [required options] [invite|team_repos|remove|remove_access] GITHUB_ORG_API_KEY for the org must be set as an environment variable. -'invite' invites students provided in .csv file and creates teams, 'repos' creates team repos, 'remove' remove students, repos, teams from the org +'invite' invites students provided in .csv file and creates teams, 'team_repos' creates team repos, 'remove' remove students, repos, teams from the org It's safe to run multiple times. @@ -14,7 +14,7 @@ Required arguments: -o, --orgname=ORGNAME The name of the org eg org_name -f, --filename=FILENAME The base filename for repos, eg "fa23-actionmap-04", actionmap is the base file name of the repo -p, --prefix=PREFIX Semester prefix, eg "fa23" create a repos prefix, "fa23-actionmap-04", etc. - -t, --template=TEMPLATE The repo name within the org to use as template eg repo_name (Assume the repo own by org) + -t, --template=TEMPLATE The repo template to use to generate individual or team repos (should be of format org/repo-name), eg saasbook/chips-3.5 -s, --studentteam=STUDENTTEAM The team name of all the students team -g, --gsiteam=GSITEAM The team name of all the staff team ``` diff --git a/scripts/github-repos/example_sheet.csv b/scripts/github-repos/example_sheet.csv new file mode 100644 index 0000000..dd36912 --- /dev/null +++ b/scripts/github-repos/example_sheet.csv @@ -0,0 +1,2 @@ +Team,Email,Name,What is your student ID?,GitHub Username +16,connorbernard@berkeley.edu,Connor Bernard,3035597811,connor-bernard diff --git a/scripts/github-repos/github-repos.rb b/scripts/github-repos/github-repos.rb index 0ec670d..ebc9d6f 100755 --- a/scripts/github-repos/github-repos.rb +++ b/scripts/github-repos/github-repos.rb @@ -8,10 +8,11 @@ def main() puts "Script start." org = OrgManager.new $opts = OptionParser.new do |opt| - opt.banner = "Usage: #{__FILE__} [required options] [invite|repos|remove] - GITHUB_ORG_API_KEY for the org must be set as an environment variable. - 'invite' invites students provided in .csv file and creates teams, - 'repos' creates team repos, 'remove' remove students, repos, teams from the org." + opt.banner = "Usage: #{__FILE__} [required options] [invite|team_repos|individual_repos|remove|remove_access] + GITHUB_ORG_API_KEY for the org must be set as an environment variable. + 'invite' invites students provided in .csv file and creates teams, + 'team_repos' creates team repos, 'individual_repos' creates individual repos, + 'remove' remove students, repos, teams from the org." opt.on('-cCSVFILE', '--csv=CSVFILE', 'CSV file containing at least "Team" and "Email" named columns') do |csv| org.read_teams_and_emails_from csv end @@ -38,10 +39,13 @@ def main() command = ARGV.pop case command when 'invite' then org.invite - when 'repos' then org.create_repos + when 'team_repos' then org.create_team_repos + when 'individual_repos' then org.create_individual_repos when 'remove' then org.remove when 'remove_access' then org.remove_access - else org.print_error + else + STDERR.puts $opts + exit 1 end puts "Run successfully." puts "Script ends." @@ -55,8 +59,8 @@ def initialize @base_filename = nil @semester = nil @template = nil - @childteams = Hash.new { |hash, key| hash[key] = [] } # teamID => [email1, email2, ...] - print_error("GITHUB_ORG_API_KEY not defined in environment") unless (@key = ENV['GITHUB_ORG_API_KEY']) + @childteams = Hash.new { |hash, key| hash[key] = [] } + log("GITHUB_ORG_API_KEY not defined in environment", :fatal) unless (@key = ENV['GITHUB_ORG_API_KEY']) @client = Octokit::Client.new(access_token: @key) end @@ -80,21 +84,16 @@ def valid? end def log(msg, type=:info, output_file=nil) - output_file ||= STDERR if type === :error + output_file ||= STDERR if type === :error || type === :fatal output_file ||= STDOUT output_file.puts "[#{type.upcase}]: #{msg}" - end - - def print_error(msg=nil) - log(msg, :error) if !msg.nil? - STDERR.puts $opts - exit 1 + exit 1 if type === :fatal end def read_teams_and_emails_from csv data = CSV.parse(IO.read(csv), headers: true) hash = data.first.to_h - print_error "Need at least 'Team' (int) and 'Email' (str) columns in #{csv}" unless + log("Need at least 'Team' (int) and 'Email' (str) columns in #{csv}", :fatal) unless hash.has_key?('Team') && hash.has_key?('Email') log "geting GitHub users. Please wait..." data.each do |row| @@ -106,8 +105,7 @@ def read_teams_and_emails_from csv begin user['uid'] = @client.user(username).id rescue Octokit::NotFound - user['username'] = nil - log("GitHub Account '#{username}' does not exist. Using '#{row['email']}' instead") + log("GitHub Account '#{username}' does not exist. Using '#{row['Email']}' instead", :warn) end else log "no gh username for user #{row['Email']}; using email instead" @@ -118,7 +116,7 @@ def read_teams_and_emails_from csv end def invite - print_error "csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed." unless valid? + log("csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed.", :fatal) unless valid? first_child_team_name = %Q{#{@semester}-#{@childteams.keys[0]}} begin @@ -190,28 +188,31 @@ def invite end end - def create_repos - print_error "csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed." unless valid? + def create_team_repos + log("csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed.", :fatal) unless valid? @childteams.each_key do |team| begin team_id = @client.team_by_name(@orgname, %Q{#{@semester}-#{team}})['id'] rescue Octokit::NotFound - print_error "students teams information mismatched - could not find team '#{@semester}-#{team}' in org '#{@orgname}'" + log("students teams information mismatched - could not find team '#{@semester}-#{team}' in org '#{@orgname}'", :fatal) end gsiteam_id = @client.team_by_name(@orgname, @gsiteam)['id'] new_repo_name = %Q{#{@semester}-#{@base_filename}-#{team}} if !@client.repository? %Q{#{@orgname}/#{new_repo_name}} begin - new_repo = @client.create_repository_from_template(%Q{#{@orgname}/#{@template}}, new_repo_name, - {owner: @orgname, private: true}) + new_repo = @client.create_repository_from_template( + @template, + new_repo_name, + {owner: @orgname, private: true}, + ) log "created repo '#{new_repo_name}' from template '#{@template}' in org '#{@orgname}'" rescue Octokit::NotFound - print_error "failed to create repo: template not found." + log("failed to create repo: template not found.", :fatal) end if @client.add_team_repository(team_id, new_repo['full_name'], {permission: 'push'}) log "added repo '#{new_repo_name}' to team '#{@semester}-#{team}' with permission 'push' in org '#{@orgname}'" else - log("failed to add repo '#{new_repo_name}' to team '#{@semester}-#{team}' with permission 'push' in org '#{@orgname}'", :warn) + log("failed to add repo '#{new_repo_name}' to team '#{@semester}-#{team}' with permission 'push' in org '#{@orgname}'", :error) end if @client.add_team_repository(gsiteam_id, new_repo['full_name'], {permission: 'admin'}) log "added repo '#{new_repo_name}' to team '#{@gsiteam}' with permission 'admin' in org '#{@orgname}'" @@ -222,8 +223,63 @@ def create_repos end end + def create_individual_repos + log("csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed.", :fatal) unless valid? + gsiteam_id = @client.team_by_name(@orgname, @gsiteam)['id'] + users = @childteams.values.flatten + log("all users must have GitHub usernames to create individual repos", :fatal) unless users.all? { |user| user['username'] } + did_fail_to_add_all_users = false + users.each do |user| + # their email username after replacing non-alphanumeric chars with '-' + email_username_sanitized = user['email'][/^.*(?=@)/].gsub(/\W|_/, '-') + curr_repo_name = "#{@semester}-#{email_username_sanitized}-#{@base_filename}" + curr_repo = nil + begin + curr_repo = @client.repository "#{@orgname}/#{curr_repo_name}" + rescue Octokit::NotFound + if !curr_repo + begin + curr_repo ||= @client.create_repository_from_template( + @template, + curr_repo_name, + {owner: @orgname, private: true}, + ) + if curr_repo + log "created repo '#{curr_repo_name}' from template '#{@template}' in org '#{@orgname}'" + else + log("failed to create repo '#{curr_repo_name}' from template '#{@template}' in org '#{@orgname}'", :error) + next + end + rescue Octokit::NotFound + log("failed to create repo: template not found.", :fatal) + # apparently they don't know what 429 errors are, so they just 422 instead? + rescue Octokit::UnprocessableEntity + log("rate limited. The script will resume in one minute", :warn) + sleep 60 + retry + end + end + end + if @client.add_team_repository(gsiteam_id, curr_repo['full_name'], {permission: 'admin'}) + log "added repo '#{curr_repo_name}' to team '#{@gsiteam}' with permission 'admin' in org '#{@orgname}'" + else + log("failed to add repo '#{curr_repo_name}' to team '#{@gsiteam}' with permission 'admin' in org '#{@orgname}'", :warn) + end + begin + @client.invite_user_to_repository(curr_repo['full_name'], user['username']) + log "invited user '#{user['username']}' to repo '#{curr_repo['full_name']}' in org '#{@orgname}'" + rescue Octokit::Forbidden + did_fail_to_add_all_users = true + log("Could find GitHub user '#{user['username']}' in org '#{@orgname}' to add to repo '#{curr_repo_name}'", :error) + end + end + log("Could not add all users. See error logs", :fatal) if did_fail_to_add_all_users + puts @client.say "Let the CHIPs begin" + return + end + def remove - print_error "csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed." unless valid? + log("csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed.", :fatal) unless valid? # remove and delete all repos from the students team, delete all child teams # also cancel all pending invitaions @childteams.each_key do |team| @@ -231,7 +287,7 @@ def remove if @client.delete_repository(repo_name) log "deleted repo '#{@semester}-#{@base_filename}-#{team}' from org #{@orgname}" else - log("failed to delete repo '#{@semester}-#{@base_filename}-#{team}' from org '#{@orgname}'", :warn) + log("failed to delete repo '#{@semester}-#{@base_filename}-#{team}' from org '#{@orgname}'", :error) end begin childteam_id = @client.team_by_name(@orgname, %Q{#{@semester}-#{team}})['id'] # eg slug fa23-01