feat: add script to create organization team linked to IdP group#157
feat: add script to create organization team linked to IdP group#157joshjohanning wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new gh-cli utility script to automate creating an organization team and linking it to an IdP external group (team synchronization), and documents the script in the gh-cli README.
Changes:
- Added
create-team-and-link-idp-group.shto create a team, locate an external IdP group by name, and link the team to that group - Implemented an optional
--secretteam privacy mode - Documented the new script in
gh-cli/README.md
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| gh-cli/create-team-and-link-idp-group.sh | New script to create a team, remove the creator membership, and link to an external IdP group |
| gh-cli/README.md | Adds a new README entry for the script with a basic usage snippet |
Comments suppressed due to low confidence (1)
gh-cli/create-team-and-link-idp-group.sh:48
- This script relies on users setting
GH_HOST, but many othergh-cliscripts accept an explicit optionalhostnameargument and pass it viagh api --hostname ...(e.g.gh-cli/change-repository-visibility.sh:6-16,gh-cli/get-enterprise-members.sh:12-16). To stay consistent and to make GHES/GHE Data Residency usage less error-prone, consider adding an optional--hostname <host>(defaulting togithub.com) and passing it to everygh apicall.
# Usage:
# ./create-team-and-link-idp-group.sh <org> <team-name> <idp-group-name> [--secret]
#
# Notes:
# - The script paginates through external groups to find the target group
# - If the IdP group is not found, the script exits with an error
# - The team is created with 'closed' (visible to org members) privacy by default
# - Pass --secret to create a 'secret' (only visible to team members) team
# - For GHES / GHE Data Residency, set GH_HOST before running
if [ "$#" -lt 3 ]; then
echo "Usage: $0 <org> <team-name> <idp-group-name> [--secret]"
echo ""
echo "Example: $0 my-org my-team \"Engineering Team\""
exit 1
fi
org="$1"
team_name="$2"
idp_group_name="$3"
privacy="closed"
if [ "${4}" = "--secret" ]; then
privacy="secret"
fi
# --- Find the external IdP group by display name ---
echo "Searching for external group '$idp_group_name' in organization '$org'..."
group_id=$(gh api \
--method GET \
--paginate \
"/orgs/$org/external-groups" \
| jq -r --arg name "$idp_group_name" '.groups[] | select(.group_name | ascii_downcase == ($name | ascii_downcase)) | .group_id')
You can also share your feedback on Copilot code review. Take the survey.
| # ./create-team-and-link-idp-group.sh <org> <team-name> <idp-group-name> [--secret] | ||
| # | ||
| # Notes: | ||
| # - The script paginates through external groups to find the target group | ||
| # - If the IdP group is not found, the script exits with an error | ||
| # - The team is created with 'closed' (visible to org members) privacy by default | ||
| # - Pass --secret to create a 'secret' (only visible to team members) team | ||
| # - For GHES / GHE Data Residency, set GH_HOST before running | ||
|
|
||
| if [ "$#" -lt 3 ]; then | ||
| echo "Usage: $0 <org> <team-name> <idp-group-name> [--secret]" | ||
| echo "" | ||
| echo "Example: $0 my-org my-team \"Engineering Team\"" | ||
| exit 1 | ||
| fi | ||
|
|
||
| org="$1" | ||
| team_name="$2" | ||
| idp_group_name="$3" | ||
|
|
||
| privacy="closed" | ||
| if [ "${4}" = "--secret" ]; then | ||
| privacy="secret" |
There was a problem hiding this comment.
The script only supports a 4th positional argument when it is exactly --secret; any other 4th argument is silently ignored. This makes mistakes hard to detect and blocks adding other options (e.g., hostname). Consider parsing optional flags (and erroring on unknown flags) so invocations like --secret/--hostname <host> are handled explicitly.
This issue also appears on line 15 of the same file.
| # ./create-team-and-link-idp-group.sh <org> <team-name> <idp-group-name> [--secret] | |
| # | |
| # Notes: | |
| # - The script paginates through external groups to find the target group | |
| # - If the IdP group is not found, the script exits with an error | |
| # - The team is created with 'closed' (visible to org members) privacy by default | |
| # - Pass --secret to create a 'secret' (only visible to team members) team | |
| # - For GHES / GHE Data Residency, set GH_HOST before running | |
| if [ "$#" -lt 3 ]; then | |
| echo "Usage: $0 <org> <team-name> <idp-group-name> [--secret]" | |
| echo "" | |
| echo "Example: $0 my-org my-team \"Engineering Team\"" | |
| exit 1 | |
| fi | |
| org="$1" | |
| team_name="$2" | |
| idp_group_name="$3" | |
| privacy="closed" | |
| if [ "${4}" = "--secret" ]; then | |
| privacy="secret" | |
| # ./create-team-and-link-idp-group.sh [--secret] [--hostname <host>] <org> <team-name> <idp-group-name> | |
| # | |
| # Notes: | |
| # - The script paginates through external groups to find the target group | |
| # - If the IdP group is not found, the script exits with an error | |
| # - The team is created with 'closed' (visible to org members) privacy by default | |
| # - Pass --secret to create a 'secret' (only visible to team members) team | |
| # - For GHES / GHE Data Residency, set GH_HOST or pass --hostname before running | |
| usage() { | |
| echo "Usage: $0 [--secret] [--hostname <host>] <org> <team-name> <idp-group-name>" | |
| echo "" | |
| echo "Example: $0 --secret my-org my-team \"Engineering Team\"" | |
| } | |
| org="" | |
| team_name="" | |
| idp_group_name="" | |
| privacy="closed" | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| --secret) | |
| privacy="secret" | |
| shift | |
| ;; | |
| --hostname) | |
| if [ -z "${2:-}" ]; then | |
| echo "Error: --hostname requires a hostname value" >&2 | |
| usage | |
| exit 1 | |
| fi | |
| GH_HOST="$2" | |
| shift 2 | |
| ;; | |
| --*) | |
| echo "Error: unknown option: $1" >&2 | |
| usage | |
| exit 1 | |
| ;; | |
| *) | |
| if [ -z "$org" ]; then | |
| org="$1" | |
| elif [ -z "$team_name" ]; then | |
| team_name="$1" | |
| elif [ -z "$idp_group_name" ]; then | |
| idp_group_name="$1" | |
| else | |
| echo "Error: too many positional arguments: $1" >&2 | |
| usage | |
| exit 1 | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| if [ -z "$org" ] || [ -z "$team_name" ] || [ -z "$idp_group_name" ]; then | |
| echo "Error: missing required arguments" >&2 | |
| usage | |
| exit 1 |
| group_id=$(gh api \ | ||
| --method GET \ | ||
| --paginate \ | ||
| "/orgs/$org/external-groups" \ | ||
| | jq -r --arg name "$idp_group_name" '.groups[] | select(.group_name | ascii_downcase == ($name | ascii_downcase)) | .group_id') | ||
|
|
||
| if [ -n "$group_id" ]; then | ||
| echo "Found external group '$idp_group_name' with group_id: $group_id" | ||
| else | ||
| echo "Error: external group '$idp_group_name' not found in organization '$org'." | ||
| echo "Available groups:" | ||
| gh api \ | ||
| --method GET \ | ||
| --paginate \ | ||
| "/orgs/$org/external-groups" \ | ||
| --jq '.groups[] | " - \(.group_name) (id: \(.group_id))"' | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
group_id is derived with a jq filter that can emit multiple group_id values if more than one external group matches the display name. In that case, group_id will contain newlines and the subsequent link request will be invalid/ambiguous. Consider enforcing a single match (error if 0 or >1 matches) or selecting deterministically (e.g., first match) and making that behavior explicit in the output.
| gh api \ | ||
| --method DELETE \ | ||
| "/orgs/$org/teams/$team_slug/memberships/$current_user" \ | ||
| --silent 2>/dev/null && echo "Removed '$current_user' from team '$team_slug'." \ | ||
| || echo "User '$current_user' was not a member of team '$team_slug' (this is OK)." |
There was a problem hiding this comment.
The membership removal step treats any failure from the DELETE request as "this is OK" by discarding stderr and not distinguishing 404 (not a member) from real errors like 403 (insufficient permissions) or 5xx. That can hide a failure to remove the creating user and cause the later external-group linking to fail with a less clear error. Consider checking the HTTP status / exit code and only ignoring the specific "not found" case while surfacing other failures.
| gh api \ | |
| --method DELETE \ | |
| "/orgs/$org/teams/$team_slug/memberships/$current_user" \ | |
| --silent 2>/dev/null && echo "Removed '$current_user' from team '$team_slug'." \ | |
| || echo "User '$current_user' was not a member of team '$team_slug' (this is OK)." | |
| delete_output=$(gh api \ | |
| --method DELETE \ | |
| "/orgs/$org/teams/$team_slug/memberships/$current_user" 2>&1) | |
| delete_status=$? | |
| if [ "$delete_status" -eq 0 ]; then | |
| echo "Removed '$current_user' from team '$team_slug'." | |
| elif echo "$delete_output" | grep -q "HTTP 404"; then | |
| echo "User '$current_user' was not a member of team '$team_slug' (this is OK)." | |
| else | |
| echo "Error: failed to remove '$current_user' from team '$team_slug'." >&2 | |
| echo "$delete_output" >&2 | |
| exit 1 | |
| fi |
| Creates an organization team and links it to an Identity Provider (IdP) external group. | ||
|
|
||
| ```shell | ||
| ./create-team-and-link-idp-group.sh <org> <team-name> <idp-group-name> |
There was a problem hiding this comment.
The README usage snippet doesn’t mention the script’s optional --secret flag (and if you add hostname support, that should be documented here too). Update this usage block to match the script’s supported arguments so users don’t miss the privacy option.
| ./create-team-and-link-idp-group.sh <org> <team-name> <idp-group-name> | |
| ./create-team-and-link-idp-group.sh <org> <team-name> <idp-group-name> [--secret] [--hostname <github-hostname>] |
New script for team creation and IdP group linking:
create-team-and-link-idp-group.shto automate creating a GitHub organization team and linking it to an IdP external group.