diff --git a/scripts/github-repos/Gemfile b/scripts/github-repos/Gemfile index 680c7cf..92aa6ae 100644 --- a/scripts/github-repos/Gemfile +++ b/scripts/github-repos/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' -ruby '3.2.2' +ruby '< 3.0' gem 'octokit' gem 'csv' -gem 'yaml' +gem 'tqdm' diff --git a/scripts/github-repos/Gemfile.lock b/scripts/github-repos/Gemfile.lock index a0cb690..8a5cbcd 100644 --- a/scripts/github-repos/Gemfile.lock +++ b/scripts/github-repos/Gemfile.lock @@ -1,33 +1,35 @@ -GEM - remote: https://rubygems.org/ - specs: - addressable (2.8.4) - public_suffix (>= 2.0.2, < 6.0) - csv (3.2.6) - faraday (2.7.5) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - octokit (6.1.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - public_suffix (5.0.1) - ruby2_keywords (0.0.5) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - yaml (0.2.1) - -PLATFORMS - x86_64-linux - -DEPENDENCIES - csv - octokit - yaml - -RUBY VERSION - ruby 3.2.2p53 - -BUNDLED WITH - 2.4.13 +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) + base64 (0.1.1) + csv (3.2.7) + faraday (2.7.11) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + public_suffix (5.0.3) + ruby2_keywords (0.0.5) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + tqdm (0.4.1) + +PLATFORMS + arm64-darwin-22 + +DEPENDENCIES + csv + octokit + tqdm + +RUBY VERSION + ruby 2.6.10p210 + +BUNDLED WITH + 2.4.19 diff --git a/scripts/github-repos/README.md b/scripts/github-repos/README.md index 3210ded..98af182 100644 --- a/scripts/github-repos/README.md +++ b/scripts/github-repos/README.md @@ -1,56 +1,144 @@ -# Bulk creation/deletion of many repos and cs169a-team +# Bulk creation/deletion of many repos and student teams ``` -Usage: ./github-repos.rb [required options] [invite|repos|remove|remove_access] +Usage: ./github-repos.rb [required options] [invite|create_groups|indiv_repos|group_repos|remove_indiv_repos|remove_group_repos|remove_teams|remove_group_repos_access] [optional options] 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 - It's safe to run multiple times. -Required arguments: - -c, --csv=CSVFILE CSV file containing at least "Team" and "Email" named columns - -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) - -s, --studentteam=STUDENTTEAM The team name of all the students team - -g, --gsiteam=GSITEAM The team name of all the staff team +'invite': +Create a team called STUDENTTEAM under the org and send invitations to students to the team. +If STUDENTTEAM already exists, the script will resend invitation to students. + +If PREFIX is provided, it will assume the csv file contains "Group" column, and create groups(child teams) under +the STUDENTTEAM, and invites them to groups. + +Required options: + -c, --csv=CSVFILE CSV file containing at least "Username" named columns + -s, --studentteam=STUDENTTEAM The team name for students' team + -o, --orgname=ORGNAME The name of the org + +Optional options: + -p, --prefix=PREFIX Semester prefix, eg fa23. If prefix is not provided, it will not create groups. + +'create_groups' +Assuming students are in STUDENTTEAM, create groups for students for CHIP 10.5 and add them to corresponding groups. + +Required options: + -c, --csv=CSVFILE CSV file containing at least "Group" and "Username" named columns + -s, --studentteam=STUDENTTEAM The team name for students' team + -p, --prefix=PREFIX Semester prefix, eg fa23. + -o, --orgname=ORGNAME The name of the org + +'indiv_repos' +Create CHIPS repos for each student in STUDENTTEAM. Repos' names are form like "[PREFIX]-[username]-[ASSIGNMENT]" +[username] stands for the GitHub username. + +Required options: + -o, --orgname=ORGNAME The name of the org + -s, --studentteam=STUDENTTEAM The team name for students' team + -p, --prefix=PREFIX Semester prefix, eg fa23. + -t, --template=TEMPLATE The repo name within the org to use as template + -g, --gsiteam=GSITEAM The team name of staff team + -a, --assignment=ASSIGNMENT The assignment name + +Optional options: + -perm, --permission=PERMISSION The permission of the created repos, can be 'pull', 'push', or 'admin'. Default: 'push' + -u, --public A flag infering to create public repository, otherwise the new repo is private. + +'group_repos' +Create repos for each group. Repos' names are form like "[PREFIX]-[ASSIGNMENT]-[GROUPNUM]" +Make sure groups are formed before running this command. + +Required options: + -o, --orgname=ORGNAME The name of the org + -p, --prefix=PREFIX Semester prefix, eg fa23. + -t, --template=TEMPLATE The repo name within the org to use as template + -a, --assignment=ASSIGNMENT The assignment name. eg chip-10.5 + -s, --studentteam=STUDENTTEAM The team name for students' team + -g, --gsiteam=GSITEAM The team name of staff team + +Optional options: + -perm, --permission=PERMISSION The permission of the created repos, can be 'pull', 'push', or 'admin'. Default: 'push' + -u, --public A flag infering to create public repository, otherwise the new repo is private. + +'remove_indiv_repos' +Delete all repos whose names are formed like "[PREFIX]-[username]-[ASSIGNMENT]". + +Required options: + -o, --orgname=ORGNAME The name of the org + -p, --prefix=PREFIX Semester prefix, eg fa23. + -a, --assignment=ASSIGNMENT The assignment name. eg chip-10.5 + +'remove_group_repos' +Delete all repos whose names are formed like "[PREFIX]-[ASSIGNMENT]-[GROUPNUM]". + +Required options: + -o, --orgname=ORGNAME The name of the org + -p, --prefix=PREFIX Semester prefix, eg fa23. + -a, --assignment=ASSIGNMENT The assignment name. eg chip-10.5 + +'remove_teams' +Remove all students and groups in STUDENTTEAM from the org. +Remove STUDENTTEAM as well. + +Required options: + -o, --orgname=ORGNAME The name of the org + -p, --prefix=PREFIX Semester prefix, eg fa23. + -s, --studentteam=STUDENTTEAM The team name for students' team + +'remove_group_repos_access' +Remove group access to group repos that are formed like "[PREFIX]-[ASSIGNMENT]-[GROUPNUM]". + +Required options: + -o, --orgname=ORGNAME The name of the org + -p, --prefix=PREFIX Semester prefix, eg fa23. + -a, --assignment=ASSIGNMENT The assignment name. eg chip-10.5 ``` -This script creates a team that include all stundents in the same semester, eg fa23. -Then creates different child teams for them for the chip10.5. At a minimum, -you need a CSV file listing all enrolled students with columns 'Email' and 'Team'. -The values in the Team columns should be nonnegative integers identifying teams. +This script offer commands to manage student teams and groups, repositories. Org setting: Assume Base permissions is no permission, each students can only access their team repos. **Assumption:** You have a GITHUB ORG API key. -### Create a team under the org that includes all students in the same semester +### Create a student team under the org and send invitations to students + +**Use case:** Create a team called STUDENTTEAM if it don't exist, and send invitations +to the students in csv file. For students NOT in STUDENTTEAM, invite them to STUDENTTEAM. + +### Create groups for students for CHIP 10.5 + +**Use case:** Create groups for students for CHIP 10.5. + +### Create CHIPS repo for each student + +**Use case:** Create CHIP repos for each student in STUDENTTEAM. +The repos' names are formed like "[PREFIX]-[username]-[ASSIGNMENT]". There will +be only one collabrator. Give GSITEAM admin access to the created repos. + +### Create 10.5 repos for each group + +**Use case:** Each group (as identified by "Group" column in CSV file) +gets a repo for chip 10.5. Repos' names are formed like "[PREFIX]-[ASSIGNMENT]-[TEAMNUM]" +Every one in the same group has write access to the repo. +Give GSITEAM admin access to the created repos. + +### Remove the CHIPS repos -**Use case:** Create a team called STUDENTTEAM, and send invitations -to the students in csv file. If STUDENTTEAM exists, delete the team and -create a new one. (Team repos still exist) -For all students NOT in that team, add/invite into that team. +**Use case:** Delete all repos whose names are formed like "[PREFIX]-[username]-[ASSIGNMENT]". -### Create 10.5 repos for each child team +### Remove CHIP 10.5 repos -**Use case:** Each team (as identified by Team column in CSV file) -gets a repo for chip 10.5. Repos' names are formed -by concatenating the prefix, base file name, and the team ID. -(eg "fa23-actionmap-04" for team #4) For all students on that team who do -NOT already have access to the repo, give them write access on the repo. -Add all repos to the gsiteam with admin permission. +**Use case:** Delete all repos whose names are formed like "[PREFIX]-[ASSIGNMENT]-[GROUPNUM]". -### Remove all team members from students team, and delete all the repos +### Remove all team members from students teams, then delete all teams -**Use case:** Delete all repos whose name matches the "PREFIX-FILENAME-team number". -Remove all students and subteams in STUDENTTEAM from the org. +**Use case:** Remove all students and groups in STUDENTTEAM from the org. ### Remove student access to all the chip 10.5 repos -**Use case:** Only remove the access of students teams, repos still can be accessed by +**Use case:** Only remove the access of students groups, repos can still be accessed by gsi team. diff --git a/scripts/github-repos/github-repos.rb b/scripts/github-repos/github-repos.rb index 7f258df..d01a46b 100755 --- a/scripts/github-repos/github-repos.rb +++ b/scripts/github-repos/github-repos.rb @@ -3,224 +3,426 @@ require 'optparse' require 'octokit' require 'csv' - -ENV['GITHUB_ORG_API_KEY'] = "" +require 'tqdm' 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.on('-cCSVFILE', '--csv=CSVFILE', 'CSV file containing at least "Team" and "Email" named columns') do |csv| - org.read_teams_and_emails_from csv - end - opt.on('-oORGNAME', '--orgname=ORGNAME', 'The name of the org') do |orgname| - org.orgname = orgname - end - opt.on('-fFILENAME', '--filename=FILENAME', 'The base filename for repos') do |filename| - org.base_filename = filename - end - opt.on('-pPREFIX', '--prefix=PREFIX', 'Semester prefix, eg "fa23" create a repos prefix, "fa23-actionmap-04", etc') do |pfx| - org.semester = pfx - end - opt.on('-tTEMPLATE', '--template=TEMPLATE', 'The repo name within the org to use as template eg repo_name') do |template| - org.template = template - end - opt.on('-sSTUDENTTEAM', '--studentteam=STUDENTTEAM', 'The team name of all the students team') do |studentteam| - org.parentteam = studentteam - end - opt.on('-gGSITEAM', '--gsiteam=GSITEAM', 'The team name of all the staff team') do |gsiteam| - org.gsiteam = gsiteam - end + puts "Script start." + org = OrgManager.new + $opts = OptionParser.new do |opt| + opt.banner = "Usage: #{__FILE__} [required options] [invite|create_groups|indiv_repos|group_repos| + remove_indiv_repos|remove_group_repos|remove_groups|remove_group_repos_access] [optional options] + GITHUB_ORG_API_KEY for the org must be set as an environment variable." + + opt.separator "" + opt.separator "Commands:" + opt.separator " invite: Create a team called STUDENTTEAM under the org and send invitations to students to the team. + If STUDENTTEAM already exists, the script will resend invitation to students. + If PREFIX is provided, it will assume the csv file contains \"Group\" column, + and create groups (child teams) under the STUDENTTEAM, and invites them to groups.\n" + opt.separator " create_groups: Assuming students are in STUDENTTEAM, create groups for students for CHIP 10.5 and add them to corresponding groups.\n" + opt.separator " indiv_repos: Create CHIPS repo for each stedent in STUDENTTEAM. Repos' names are form like \"[PREFIX]-[username]-[ASSIGNMENT]\" + [username] stands for the GitHub username.\n" + opt.separator " group_repos: Create 10.5 repos for each child team. Repos' names are form like \"[PREFIX]-[ASSIGNMENT]-[GROUPNUM]\" + Make sure groups are formed before running this command.\n" + opt.separator " remove_indiv_repos: Delete all repos whose names are formed like \"[PREFIX]-[username]-[ASSIGNMENT]\".\n" + opt.separator " remove_group_repos: Delete all repos whose names are formed like \"[PREFIX]-[ASSIGNMENT]-[GROUPNUM]\".\n" + opt.separator " remove_groups: Remove all students and groups in STUDENTTEAM from the org. Remove STUDENTTEAM as well.\n" + opt.separator " remove_group_repos_access: Remove students access to CHIP 10.5 repos that are formed like \"[PREFIX]-[ASSIGNMENT]-[GROUPNUM]\".\n" + opt.separator "Options:" + + opt.on('-cCSVFILE', '--csv=CSVFILE', 'CSV file containing at least "Username" named columns') do |csv| + org.csv = csv + end + opt.on('-oORGNAME', '--orgname=ORGNAME', 'The name of the org') do |orgname| + org.orgname = orgname + end + opt.on('-sSTUDENTTEAM', '--studentteam=STUDENTTEAM', 'The team name for students\' team') do |studentteam| + org.parentteam = studentteam end - $opts.parse! - command = ARGV.pop - case command - when 'invite' then org.invite - when 'repos' then org.create_repos - when 'remove' then org.remove - when 'remove_access' then org.remove_access - else org.print_error - end - puts "Run successfully." - puts "Script ends." + opt.on('-pPREFIX', '--prefix=PREFIX', 'Semester prefix, eg fa23.') do |pfx| + org.semester = pfx + end + opt.on('-aASSIGNMENT', '--assignment=ASSIGNMENT', 'The assignment name. eg chip-10.5') do |assignment| + org.assignment = assignment + end + opt.on('-tTEMPLATE', '--template=TEMPLATE', 'The repo name within the org to use as template') do |template| + org.template = template + end + opt.on('-gGSITEAM', '--gsiteam=GSITEAM', 'The team name of staff team') do |gsiteam| + org.gsiteam = gsiteam + end + opt.on('-ePERMISSION', '--permission=PERMISSION', 'The permission of the created repos, can be \'pull\', \'push\', or \'admin\'. Default: \'push\'') do |permission| + org.permission = permission + end + opt.on('-u', '--public', 'Create public repository, otherwise the new repo is private.') do + org.isPublic = true + end + end + $opts.parse! + command = ARGV.pop + org.invoke_command command + puts "Run successfully." + puts "Script ends." end class OrgManager - attr_accessor :orgname, :base_filename, :semester, :template, :parentteam, :gsiteam + attr_accessor :orgname, :assignment, :semester, :template, :parentteam, :gsiteam, :csv, :users, :permission, :isPublic - def initialize - @orgname = nil - @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']) - @client = Octokit::Client.new(access_token: @key) - end + def initialize + @orgname = nil + @assignment = nil + @semester = nil + @template = nil + @csv = nil + @isPublic = false + @permission = 'push' + @users = [] + @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']) + @client = Octokit::Client.new(access_token: @key, scopes: ['user']) + @client.auto_paginate = true # auto pagination on + end - private + private - def gsiteam_valid? - gsiteam_obj = nil - if !@gsiteam.nil? && @gsiteam.length > 0 - begin - gsiteam_obj = @client.team_by_name(@orgname, @gsiteam) - rescue Octokit::NotFound - end - end - !gsiteam_obj.nil? + def read_users_from csv + data = CSV.parse(IO.read(csv), headers: true) + hash = data.first.to_h + print_error "Need at least 'Username' (str) columns in #{csv}" unless + hash.has_key?('Username') + data.each do |row| + @users << row['Username'] end + end - public + def read_groups_and_users_from csv + data = CSV.parse(IO.read(csv), headers: true) + hash = data.first.to_h + print_error "Need at least 'Group' (int) and 'Username' (str) columns in #{csv}" unless + hash.has_key?('Group') && hash.has_key?('Username') + data.each do |row| + username = row['Username'] + @childteams[row['Group']] << username + end + end + + def to_slug(input) + # Convert to lowercase + slug = input.downcase + + # Replace characters other than 0-9, a-z, '.', '_', and '-' + slug.gsub!(/[^0-9a-z.\-_]+/, '-') + + # Remove leading and trailing hyphens + slug.gsub!(/^-+/, '') + slug.gsub!(/-+$/, '') + + # Limit the string to 63 characters + slug = slug[0, 63] + + return slug + end - def valid? - @orgname && @base_filename && @semester && @childteams.length > 0 && @template && @parentteam && gsiteam_valid? + def invite_valid? + if @orgname.nil? || @orgname.empty? || @csv.nil? || @parentteam.nil? || @parentteam.empty? + return false end + # process the csv file + if @semester.nil? + read_users_from @csv + else + read_groups_and_users_from @csv + end + true + end - def print_error(msg=nil) - STDERR.puts "Error: #{msg}" if msg - STDERR.puts $opts - exit 1 + def create_groups_valid? + if @orgname.nil? || @orgname.empty? || @csv.nil? || @parentteam.nil? || @parentteam.empty? || @semester.nil? + return false end + read_groups_and_users_from @csv + true + 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 - hash.has_key?('Team') && hash.has_key?('Email') - data.each do |row| - username = row['Email'] - @childteams[row['Team']] << username - end + def repos_valid? + !(@orgname.nil? || @parentteam.nil? || @semester.nil? || @template.nil? || !gsiteam_valid? || @assignment.nil? || !(['pull', 'push', 'admin'].include? (@permission))) + end + + def remove_valid? + !(@orgname.nil? || @semester.nil? || @assignment.nil?) + end + + def remove_groups_valid? + !(@orgname.nil? || @semester.nil? || @parentteam.nil?) + end + + def gsiteam_valid? + gsiteam_obj = nil + if !@gsiteam.nil? && @gsiteam.length > 0 + begin + gsiteam_obj = @client.team_by_name(@orgname, to_slug(@gsiteam)) + rescue Octokit::NotFound + print_error "Can't find the gsi team in the org." + end end + !gsiteam_obj.nil? + end + + public - def invite - print_error "csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed." unless valid? - first_child_team_name = %Q{#{@semester}-#{@childteams.keys[0]}} + def invoke_command command + command = command.to_sym + if respond_to? command + send(command) + else + print_error + end + end + def print_error(msg=nil) + STDERR.puts "Error: #{msg}" if msg + STDERR.puts $opts + exit 1 + end + + def invite + print_error "csv file, organization name, student team name are needed." unless invite_valid? + # Looking for the STUDENTTEAM in the org, see if it is exist. + begin + parentteam_id = @client.team_by_name(@orgname, to_slug(@parentteam))['id'] + rescue Octokit::NotFound + parentteam_id = @client.create_team(@orgname, {name: @parentteam, privacy: 'closed'})['id'] + end + if @semester.nil? + # Semester prefix is not provided + + @users.tqdm.each do |username| + begin + @client.put(%Q{/orgs/#{@orgname}/teams/#{to_slug(@parentteam)}/memberships/#{username}}) + rescue Octokit::Forbidden + print_error "Can't invite or add students if team synchronization is set up" + rescue Octokit::UnprocessableEntity + print_error "Incorrect GitHub username (An org name provided)" + end + end + else + # Semester prefix is provied + # create groups and invite students to the groups + @childteams.tqdm.each_key do |team| + childteam_name = %Q{#{@semester}-#{team}} begin - parentteam_obj = @client.team_by_name(@orgname, @parentteam) + childteam = @client.team_by_name(@orgname, to_slug(childteam_name)) rescue Octokit::NotFound - parentteam_obj = nil + childteam = @client.create_team(@orgname, {name: %Q{#{@semester}-#{team}}, parent_team_id: parentteam_id}) + end + childteam_id = childteam['id'] + @childteams[team].tqdm.each do |member| + # Try no check invitations before create one. + # send invitation + begin + @client.put(%Q{/orgs/#{@orgname}/teams/#{childteam.slug}/memberships/#{member}}) + rescue Octokit::Forbidden + print_error "Can't invite or add students if team synchronization is set up" + rescue Octokit::UnprocessableEntity + print_error "Incorrect GitHub username (An org name provided)" + end end + end + + end + end + + def create_groups + print_error "csv file, students team name, semester prefix, org name are needed." unless create_groups_valid? + + # Looking for the STUDENTTEAM in the org, see if it is exist. + begin + parentteam_id = @client.team_by_name(@orgname, to_slug(@parentteam))['id'] + rescue Octokit::NotFound + print_error "Please make sure the invite command runs first, or students team is created." + end + + failed_username = [] + # create groups and add students to their groups + @childteams.tqdm.each_key do |team| + childteam_name = %Q{#{@semester}-#{team}} + begin + childteam = @client.team_by_name(@orgname, to_slug(childteam_name)) + rescue Octokit::NotFound + childteam = @client.create_team(@orgname, {name: %Q{#{@semester}-#{team}}, parent_team_id: parentteam_id}) + end + childteam_id = childteam['id'] + @childteams[team].tqdm.each do |username| begin - if parentteam_obj - @client.team_by_name(@orgname, first_child_team_name) - parentteam_id = parentteam_obj['id'] - else - parentteam_id = @client.create_team(@orgname, {name: @parentteam, privacy: 'closed'})['id'] - end - rescue Octokit::NotFound - # students team exists before invite - # remove all members in this team, and delete the team. - parentteam_id = parentteam_obj['id'] - old_team_members = @client.team_members(parentteam_id) - old_team_members.each do |member| - if @client.organization_membership(@orgname, :user => member['login'])['role'] == 'member' - @client.remove_organization_member(@orgname, member['login']) - end - end - @client.delete_team parentteam_id - parentteam_id = @client.create_team(@orgname, {name: @parentteam, privacy: 'closed'})['id'] + @client.add_team_member(childteam_id, username) + rescue Octokit::UnprocessableEntity + # save the incorrect username + failed_username << username end + end + end - @childteams.each_key do |team| - childteam_name = %Q{#{@semester}-#{team}} - begin - childteam = @client.team_by_name(@orgname, childteam_name) - rescue Octokit::NotFound - childteam = @client.create_team(@orgname, {name: %Q{#{@semester}-#{team}}, parent_team_id: parentteam_id}) - end - childteam_id = childteam['id'] - @childteams[team].each do |member| - if !@client.team_invitations(childteam_id).any? {|invitations| invitations.email == member} - # send invitation - begin - @client.post(%Q{/orgs/#{@orgname}/invitations}, {org: @orgname, email: member, role: 'direct_member', team_ids: [childteam_id]}) - rescue Octokit::UnprocessableEntity - # member is already a part of org - next - end - end - end + # print the usernames that is failed to add to groups + if !failed_username.empty? + puts 'Failed to add these usernames to groups:' + puts failed_username + end + end + + def indiv_repos + print_error "Missing required options or invalid options." unless repos_valid? + + # Looking for the STUDENTTEAM in the org, see if it is exist. + begin + parentteam_id = @client.team_by_name(@orgname, to_slug(@parentteam))['id'] + rescue Octokit::NotFound + print_error "Please make sure students team is created." + end + + gsiteam_id = @client.team_by_name(@orgname, to_slug(@gsiteam)).id + self_user_name = @client.user.login + + # create individual repos and give each student in the student team write access to the repos + @client.team_members(parentteam_id).tqdm.each do |mem| + if self_user_name != mem.login + new_repo_name = %Q{#{@semester}-#{mem.login}-#{@assignment}} + 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: !@isPublic}) + rescue Octokit::NotFound + print_error "Template not found." + end + else + new_repo = @client.repo(%Q{#{@orgname}/#{new_repo_name}}) + @client.update_repository(new_repo.full_name, { private: !@isPublic }) end + @client.add_collab(new_repo['full_name'], mem.login, permission: @permission) + @client.add_team_repository(gsiteam_id, new_repo['full_name'], {permission: 'admin'}) + 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? - @childteams.each_key do |team| + # create individual repos and give each student in each group write access to the repos + @client.child_teams(parentteam_id).tqdm.each do |childteam| + @client.team_members(childteam.id).each do |mem| + if !mem.login.nil? && self_user_name != mem.login + new_repo_name = %Q{#{@semester}-#{mem.login}-#{@assignment}} + if !@client.repository? %Q{#{@orgname}/#{new_repo_name}} begin - team_id = @client.team_by_name(@orgname, %Q{#{@semester}-#{team}})['id'] + new_repo = @client.create_repository_from_template(%Q{#{@orgname}/#{@template}}, new_repo_name, + {owner: @orgname, private: !@isPublic}) rescue Octokit::NotFound - print_error "Students teams information mismatched." - 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}) - rescue Octokit::NotFound - print_error "Template not found." - end - @client.add_team_repository(team_id, new_repo['full_name'], {permission: 'push'}) - @client.add_team_repository(gsiteam_id, new_repo['full_name'], {permission: 'admin'}) + print_error "Template not found." end + else + new_repo = @client.repo(%Q{#{@orgname}/#{new_repo_name}}) + @client.update_repository(new_repo.full_name, { private: !@isPublic }) + end + @client.add_collab(new_repo['full_name'], mem.login, permission: @permission) + @client.add_team_repository(gsiteam_id, new_repo['full_name'], {permission: 'admin'}) end + end end + end - def remove - print_error "csv file, base filename, template repo name, semester prefix, students team name, and gsi team name needed." 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| - repo_name = %Q{#{@orgname}/#{@semester}-#{@base_filename}-#{team}} - @client.delete_repository(repo_name) - begin - childteam_id = @client.team_by_name(@orgname, %Q{#{@semester}-#{team}})['id'] # eg slug fa23-01 - rescue Octokit::NotFound - next - end + def group_repos + print_error "Missing required options or invalid options." unless repos_valid? - # remove students from the student teams - team_members = @client.team_members(childteam_id) - team_members.each do |member| - if @client.organization_membership(@orgname, :user => member['login'])['role'] == 'member' - @client.remove_organization_member(@orgname, member['login']) - end - end + child_teams = @client.child_teams(@client.team_by_name(@orgname, to_slug(@parentteam)).id) + gsiteam_id = @client.team_by_name(@orgname, to_slug(@gsiteam))['id'] - @client.team_invitations(childteam_id).each do |invitation| - # cancel all pending invitations - @client.delete(%Q{/orgs/#{@orgname}/invitations/#{invitation[:id]}}) - end - @client.delete_team childteam_id - end - # delete students team + # create tean repos and give each team a write access + child_teams.tqdm.each do |team| + group_num = team.slug.match(/-(\d+)$/)[1] + new_repo_name = %Q{#{@semester}-#{@assignment}-#{group_num}} + if !@client.repository? %Q{#{@orgname}/#{new_repo_name}} begin - team_id = @client.team_by_name(@orgname, @parentteam)['id'] - @client.delete_team team_id + new_repo = @client.create_repository_from_template(%Q{#{@orgname}/#{@template}}, new_repo_name, + {owner: @orgname, private: !@isPublic}) rescue Octokit::NotFound - # do nothing if no such team + print_error "Template not found." end + else + new_repo = @client.repo(%Q{#{@orgname}/#{new_repo_name}}) + @client.update_repository(new_repo.full_name, { private: !@isPublic }) + end + @client.add_team_repository(team.id, new_repo['full_name'], {permission: @permission}) + @client.add_team_repository(gsiteam_id, new_repo['full_name'], {permission: 'admin'}) end - - def remove_access - @childteams.each_key do |team| - repo_name = %Q{#{@orgname}/#{@semester}-#{@base_filename}-#{team}} - begin - childteam_id = @client.team_by_name(@orgname, %Q{#{@semester}-#{team}})['id'] # eg slug fa23-1 - rescue Octokit::NotFound - next - end - @client.remove_team_repository(childteam_id, repo_name) + + end + + def remove_indiv_repos + print_error "org name, assignment name, semester prefix are needed." unless remove_valid? + + repos = @client.org_repos(@orgname) + repos.tqdm.each do |repo| + if repo.name =~ /^#{Regexp.escape(@semester)}-(.*)-#{Regexp.escape(@assignment)}$/ + @client.delete_repository(repo.full_name) + end + end + end + + def remove_group_repos + print_error "org name, assignment name, semester prefix are needed." unless remove_valid? + + repos = @client.org_repos(@orgname) + repos.tqdm.each do |repo| + if repo.name =~ /^#{Regexp.escape(@semester)}-#{Regexp.escape(@assignment)}-\d+$/ + @client.delete_repository(repo.full_name) + end + end + end + + def remove_groups + print_error "org name, semester prefix, students team name are needed." unless remove_groups_valid? + + # Looking for the STUDENTTEAM in the org, see if it is exist. + begin + parentteam_id = @client.team_by_name(@orgname, to_slug(@parentteam))['id'] + rescue Octokit::NotFound + print_error "Please make sure students team is created." + end + + # Remove students in the students team from org + self_user_name = @client.user.login + @client.team_members(parentteam_id).tqdm.each do |mem| + if !mem.login.nil? && mem.login != self_user_name + @client.remove_organization_member(@orgname, mem.login) + end + end + + # Remove students in each group + @client.child_teams(parentteam_id).tqdm.each do |childteam| + @client.team_members(childteam.id).each do |mem| + if !mem.login.nil? && mem.login != self_user_name + @client.remove_organization_member(@orgname, mem.login) + end + end + end + + # Remove students team, groups will be removed as well + @client.delete_team parentteam_id + end + + def remove_group_repos_access + print_error "org name, assignment name, semester prefix are needed." unless remove_valid? + + repos = @client.org_repos(@orgname) + repos.tqdm.each do |repo| + match = repo.name.match(/^#{Regexp.escape(@semester)}-#{Regexp.escape(@assignment)}-(\d+)$/) + if match + group_num = match[1] + begin + childteam_id = @client.team_by_name(@orgname, to_slug(%Q{#{@semester}-#{group_num}}))['id'] # eg slug fa23-1 + rescue Octokit::NotFound + next end + @client.remove_team_repository(childteam_id, repo.full_name) + end end + end end main \ No newline at end of file diff --git a/scripts/github-repos/team-test.csv b/scripts/github-repos/team-test.csv new file mode 100644 index 0000000..3472ea9 --- /dev/null +++ b/scripts/github-repos/team-test.csv @@ -0,0 +1,3 @@ +Group,Username,Name +1,KCsama,Amy +2,Gangster-Who,Gang \ No newline at end of file