diff --git a/NAMESPACE b/NAMESPACE index c56a6a7..fe75cf1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,6 +20,8 @@ export(kyber_file) export(list_team_members) export(list_teams) export(md_agenda) +export(remove_org_members) +export(remove_team_members) export(short_names) import(knitr) importFrom(cli,cli_abort) diff --git a/NEWS.md b/NEWS.md index 8840686..a5d4a94 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # kyber (development version) +* Added two new functions (#174): + - `remove_team_members()` for removing a list of people from a GitHub team + - `remove_org_members()` for removing people from an organization. + # kyber 0.2.0 * Ensure total duration of call agendas == sum of section durations (#66) diff --git a/R/add_team_members.R b/R/add_team_members.R index 0af44d0..dd99302 100644 --- a/R/add_team_members.R +++ b/R/add_team_members.R @@ -1,18 +1,124 @@ #' Add GitHub Users to a Team #' +#' This also adds them to the organization that contains the team. All users +#' will get an invitation by email and on GitHub.com, that they need to accept. +#' This invitation expires after seven days. +#' #' @param team The name of the team. #' @param members A vector of GitHub usernames. #' @param org The GitHub organization that owns the team and the repository. #' @importFrom purrr map safely map_lgl +#' +#' @returns list of responses from the GitHub API +#' #' @export #' @examples #' \dontrun{ -#' -#' kyber::add_team_members("2021-ilm-rotj-team", members = c("erinmr", "seankross")) +#' add_team_members("2021-ilm-rotj-team", members = c("not-ateucher", "seankross")) #' } add_team_members <- function(team, members, org = "openscapes") { + resp <- add_remove_team_members_impl_(team, members, org, method = "PUT") + invisible(resp) +} + +#' Remove GitHub Users from a Team +#' +#' @inheritParams add_team_members +#' +#' @returns list of responses from the GitHub API +#' +#' @export +#' @examples +#' \dontrun{ +#' remove_team_members("2021-ilm-rotj-team", members = "not-ateucher")) +#' } +remove_team_members <- function(team, members, org = "openscapes") { + resp <- add_remove_team_members_impl_(team, members, org, method = "DELETE") + invisible(resp) +} + +add_remove_team_members_impl_ <- function( + team, + members, + org, + method = c("PUT", "DELETE") +) { check_gh_pat() + members <- check_user_names(members) + + # role for default `PUT` method when adding members + params <- list(role = "member") + + if (method == "DELETE") { + team_members <- list_team_members(team = team, org = org) + missing_members <- setdiff(members, team_members) + if (length(missing_members) == length(members)) { + cli::cli_abort( + "None of the specified members are part of the {.val {team}} team" + ) + } + if (length(missing_members)) { + cli::cli_warn( + "User{cli::qty(missing_members)}{?s} {.val {missing_members}} {?is/are} not part of the {.val {team}} team" + ) + } + # No role when removing members, so pass empty list of params + params <- list() + } + + responses <- list() + + for (i in seq_along(members)) { + responses[[i]] <- gh( + "/orgs/{org}/teams/{team_slug}/memberships/{username}", + org = org, + team_slug = team, + username = members[i], + .method = method, + .params = params + ) + } + + responses +} + +#' Remove GitHub organization members +#' +#' @inheritParams add_team_members +#' +#' @returns list of responses from the GitHub API +#' +#' @export +#' @examples +#' \dontrun{ +#' remove_org_members(members = "not-ateucher") +#' } +remove_org_members <- function(members, org = "openscapes") { + check_gh_pat() + + members <- check_user_names(members) + + responses <- list() + + for (i in seq_along(members)) { + responses[[i]] <- gh( + "DELETE /orgs/{org}/members/{username}", + org = org, + username = members[i] + ) + } + + invisible(responses) +} + +#' Check usernames and return valid usernames +#' +#' @param members character vector or usernames +#' +#' @returns valid usernames +#' @noRd +check_user_names <- function(members) { if (!identical(members, gsub("\\s", "", members))) { stop( "GitHub usernames: ", @@ -25,6 +131,7 @@ add_team_members <- function(team, members, org = "openscapes") { map(safely(~ gh("/users/{username}", username = .x))) invalid_usernames <- responses %>% map_lgl(~ is.null(.x$"result")) + if (any(invalid_usernames)) { warning( "GitHub username(s): ", @@ -35,20 +142,8 @@ add_team_members <- function(team, members, org = "openscapes") { # All usernames are invalid if (sum(invalid_usernames) == length(invalid_usernames)) { - return(invisible(responses)) + cli::cli_abort("All usernames are invalid") } - members <- members[!invalid_usernames] - - responses <- list() - for (i in seq_along(members)) { - responses[[i]] <- gh( - "PUT /orgs/{org}/teams/{team_slug}/memberships/{username}", - org = org, - team_slug = team, - username = members[i], - role = "member" - ) - } - invisible(responses) + members[!invalid_usernames] } diff --git a/man/add_team_members.Rd b/man/add_team_members.Rd index 822ad6d..b24cd47 100644 --- a/man/add_team_members.Rd +++ b/man/add_team_members.Rd @@ -13,12 +13,16 @@ add_team_members(team, members, org = "openscapes") \item{org}{The GitHub organization that owns the team and the repository.} } +\value{ +list of responses from the GitHub API +} \description{ -Add GitHub Users to a Team +This also adds them to the organization that contains the team. All users +will get an invitation by email and on GitHub.com, that they need to accept. +This invitation expires after seven days. } \examples{ \dontrun{ - -kyber::add_team_members("2021-ilm-rotj-team", members = c("erinmr", "seankross")) +add_team_members("2021-ilm-rotj-team", members = c("not-ateucher", "seankross")) } } diff --git a/man/remove_org_members.Rd b/man/remove_org_members.Rd new file mode 100644 index 0000000..48e2fcb --- /dev/null +++ b/man/remove_org_members.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/add_team_members.R +\name{remove_org_members} +\alias{remove_org_members} +\title{Remove GitHub organization members} +\usage{ +remove_org_members(members, org = "openscapes") +} +\arguments{ +\item{members}{A vector of GitHub usernames.} + +\item{org}{The GitHub organization that owns the team and the repository.} +} +\value{ +list of responses from the GitHub API +} +\description{ +Remove GitHub organization members +} +\examples{ +\dontrun{ + remove_org_members(members = "not-ateucher") +} +} diff --git a/man/remove_team_members.Rd b/man/remove_team_members.Rd new file mode 100644 index 0000000..7a19ced --- /dev/null +++ b/man/remove_team_members.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/add_team_members.R +\name{remove_team_members} +\alias{remove_team_members} +\title{Remove GitHub Users from a Team} +\usage{ +remove_team_members(team, members, org = "openscapes") +} +\arguments{ +\item{team}{The name of the team.} + +\item{members}{A vector of GitHub usernames.} + +\item{org}{The GitHub organization that owns the team and the repository.} +} +\value{ +list of responses from the GitHub API +} +\description{ +Remove GitHub Users from a Team +} +\examples{ +\dontrun{ +remove_team_members("2021-ilm-rotj-team", members = "not-ateucher")) +} +} diff --git a/tests/manual/test-gh-member-teams.R b/tests/manual/test-gh-member-teams.R new file mode 100644 index 0000000..eb7fa88 --- /dev/null +++ b/tests/manual/test-gh-member-teams.R @@ -0,0 +1,20 @@ +teams_created <- create_team("2021-ilm-rotj-team", maintainers = "ateucher") + +teams_created + +members_added <- add_team_members("2021-ilm-rotj-team", "not-ateucher") + +members_added + +# Need to go to not-ateucher account and accept invitation + +list_team_members("2021-ilm-rotj-team") + +members_removed <- remove_team_members( + "2021-ilm-rotj-team", + members = "not-ateucher" +) + +list_team_members("2021-ilm-rotj-team") + +org_members_removed <- remove_org_members("not-ateucher") diff --git a/tests/testthat/_snaps/create_readme.md b/tests/testthat/_snaps/create_readme.md index 4838ef6..263f8ed 100644 --- a/tests/testthat/_snaps/create_readme.md +++ b/tests/testthat/_snaps/create_readme.md @@ -1,4 +1,4 @@ -# Templates can be created. +# Templates can be created and cohort name populated Code readLines(test_readme, n = 3)