From ba9c1843da8dd96a7ed7564985020e3fbc068f48 Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 15:50:13 -0800 Subject: [PATCH 1/7] remove_team_members() and remove_org_members() * Pulled out common internals into helper functions --- R/add_team_members.R | 127 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 16 deletions(-) diff --git a/R/add_team_members.R b/R/add_team_members.R index 0af44d0..c815b1a 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] } From 4910360aaf669bc08f76f6d9d814ea43f2b80324 Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 15:50:25 -0800 Subject: [PATCH 2/7] document --- NAMESPACE | 2 ++ man/add_team_members.Rd | 10 +++++++--- man/remove_org_members.Rd | 24 ++++++++++++++++++++++++ man/remove_team_members.Rd | 26 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 man/remove_org_members.Rd create mode 100644 man/remove_team_members.Rd 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/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")) +} +} From 8723432d84dcabe1c4a102d75b1371b107d475a9 Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 15:51:48 -0800 Subject: [PATCH 3/7] Manual tests for GitHub team functions * Use toy team "2021-ilm-rotj-team" and @ateucher's toy account @not-ateucher * Can't run automatically because must accept invitation manually --- tests/manual/test-gh-member-teams.R | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/manual/test-gh-member-teams.R 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") From ea254072d6a94918f862b42d41e74d05b772e0ad Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 15:52:00 -0800 Subject: [PATCH 4/7] Update snapshots --- tests/testthat/_snaps/create_readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From f6685dddee064fdf2a566064798af7871f18efda Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 16:07:35 -0800 Subject: [PATCH 5/7] Update NEWS --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 8840686..76f8392 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # kyber (development version) +* Added two new functions: + - `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) From 267db43a621e820afe1b17f62f8290cb9fec23f9 Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 16:21:54 -0800 Subject: [PATCH 6/7] Edit news --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 76f8392..a5d4a94 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # kyber (development version) -* Added two new functions: +* 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. From a22d33e6a386cf12c14e95cb20c29f7a7adad5ac Mon Sep 17 00:00:00 2001 From: Andy Teucher Date: Wed, 21 Jan 2026 16:27:08 -0800 Subject: [PATCH 7/7] Remove trailing comma --- R/add_team_members.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/add_team_members.R b/R/add_team_members.R index c815b1a..dd99302 100644 --- a/R/add_team_members.R +++ b/R/add_team_members.R @@ -51,7 +51,7 @@ add_remove_team_members_impl_ <- function( params <- list(role = "member") if (method == "DELETE") { - team_members <- list_team_members(team = team, org = org, ) + team_members <- list_team_members(team = team, org = org) missing_members <- setdiff(members, team_members) if (length(missing_members) == length(members)) { cli::cli_abort(