Skip to content

Commit 83270f4

Browse files
feat: add script to create organization team linked to IdP group (#157)
* feat: add script to create organization team linked to IdP group * refactor(gh-cli): address PR review comments for create-team-and-link-idp-group - Add proper flag parsing with --secret and --hostname support - Error on unknown flags and excess positional arguments - Take first match when multiple IdP groups match (case-insensitive) - Distinguish 404 from real errors in team membership removal - Update README with --secret flag and prerequisites
1 parent faa51d4 commit 83270f4

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

gh-cli/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,18 @@ Usage:
469469

470470
The `repos.txt` file should contain one repository name per line. The script will automatically look up repository IDs.
471471

472+
### create-team-and-link-idp-group.sh
473+
474+
Creates an organization team and links it to an Identity Provider (IdP) external group. Requires SAML SSO / team synchronization to be enabled and IdP groups provisioned to the organization.
475+
476+
```shell
477+
./create-team-and-link-idp-group.sh [--secret] [--hostname <host>] <org> <team-name> <idp-group-name>
478+
```
479+
480+
Prerequisites:
481+
- Token must have the `admin:org` scope (`gh auth refresh -h github.com -s admin:org`)
482+
- Enterprise has to be EMU or Data Residency (untested with non-EMU/DR; should work with SAML SSO similarly)
483+
472484
### create-teams-from-list.sh
473485

474486
Loops through a list of teams and creates them.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/bin/bash
2+
3+
# Creates an organization team in GitHub and links it to an Identity Provider
4+
# (IdP) external group. The script lists external groups available in the
5+
# organization, finds the target group by display name, creates a team, and
6+
# then links the team to the external group.
7+
#
8+
# Prerequisites:
9+
# 1. gh cli must be installed and authenticated (gh auth login)
10+
# 2. Token must have the `admin:org` scope
11+
# - Run: gh auth refresh -h github.com -s admin:org
12+
# 3. Enterprise has to be EMU or Data Residency
13+
# - Untested with non-EMU/DR enterprises; should work with SAML SSO / team synchronization similarly though
14+
#
15+
# Usage:
16+
# ./create-team-and-link-idp-group.sh [--secret] [--hostname <host>] <org> <team-name> <idp-group-name>
17+
#
18+
# Notes:
19+
# - The script paginates through external groups to find the target group
20+
# - If the IdP group is not found, the script exits with an error
21+
# - The team is created with 'closed' (visible to org members) privacy by default
22+
# - Pass --secret to create a 'secret' (only visible to team members) team
23+
# - For GHES / GHE Data Residency, set GH_HOST or pass --hostname before running
24+
25+
usage() {
26+
echo "Usage: $0 [--secret] [--hostname <host>] <org> <team-name> <idp-group-name>"
27+
echo ""
28+
echo "Example: $0 my-org my-team \"Engineering Team\""
29+
echo " $0 --secret --hostname github.example.com my-org my-team \"Engineering Team\""
30+
}
31+
32+
org=""
33+
team_name=""
34+
idp_group_name=""
35+
privacy="closed"
36+
37+
while [ "$#" -gt 0 ]; do
38+
case "$1" in
39+
--secret)
40+
privacy="secret"
41+
shift
42+
;;
43+
--hostname)
44+
if [ -z "${2:-}" ]; then
45+
echo "Error: --hostname requires a hostname value" >&2
46+
usage
47+
exit 1
48+
fi
49+
GH_HOST="$2"
50+
export GH_HOST
51+
shift 2
52+
;;
53+
--*)
54+
echo "Error: unknown option: $1" >&2
55+
usage
56+
exit 1
57+
;;
58+
*)
59+
if [ -z "$org" ]; then
60+
org="$1"
61+
elif [ -z "$team_name" ]; then
62+
team_name="$1"
63+
elif [ -z "$idp_group_name" ]; then
64+
idp_group_name="$1"
65+
else
66+
echo "Error: too many positional arguments: $1" >&2
67+
usage
68+
exit 1
69+
fi
70+
shift
71+
;;
72+
esac
73+
done
74+
75+
if [ -z "$org" ] || [ -z "$team_name" ] || [ -z "$idp_group_name" ]; then
76+
echo "Error: missing required arguments" >&2
77+
usage
78+
exit 1
79+
fi
80+
81+
# --- Find the external IdP group by display name ---
82+
echo "Searching for external group '$idp_group_name' in organization '$org'..."
83+
84+
group_id=$(gh api \
85+
--method GET \
86+
--paginate \
87+
"/orgs/$org/external-groups" \
88+
| jq -r --arg name "$idp_group_name" '[.groups[] | select(.group_name | ascii_downcase == ($name | ascii_downcase)) | .group_id] | first // empty')
89+
90+
if [ -n "$group_id" ]; then
91+
echo "Found external group '$idp_group_name' with group_id: $group_id"
92+
else
93+
echo "Error: external group '$idp_group_name' not found in organization '$org'."
94+
echo "Available groups:"
95+
gh api \
96+
--method GET \
97+
--paginate \
98+
"/orgs/$org/external-groups" \
99+
--jq '.groups[] | " - \(.group_name) (id: \(.group_id))"'
100+
exit 1
101+
fi
102+
103+
# --- Create the team ---
104+
echo ""
105+
echo "Creating team '$team_name' in organization '$org'..."
106+
107+
create_response=$(gh api \
108+
--method POST \
109+
"/orgs/$org/teams" \
110+
-f name="$team_name" \
111+
-f privacy="$privacy")
112+
113+
team_slug=$(echo "$create_response" | jq -r '.slug')
114+
115+
if [ -z "$team_slug" ] || [ "$team_slug" = "null" ]; then
116+
echo "Error: failed to create team '$team_name'."
117+
echo "$create_response" | jq .
118+
exit 1
119+
fi
120+
121+
echo "Team '$team_name' created successfully (slug: $team_slug)."
122+
123+
# --- Remove the creating user from the team ---
124+
# When a user creates a team, they are automatically added as a member.
125+
# The team must have no explicit members before it can be linked to an
126+
# external IdP group.
127+
echo ""
128+
echo "Removing creating user from team to allow external group linking..."
129+
130+
current_user=$(gh auth status --json hosts --jq '[.hosts[][]] | map(select(.active)) | .[0].login' 2>/dev/null)
131+
if [ -z "$current_user" ]; then
132+
current_user=$(gh api /user --jq '.login')
133+
fi
134+
delete_output=$(gh api \
135+
--method DELETE \
136+
"/orgs/$org/teams/$team_slug/memberships/$current_user" 2>&1)
137+
delete_status=$?
138+
if [ "$delete_status" -eq 0 ]; then
139+
echo "Removed '$current_user' from team '$team_slug'."
140+
elif echo "$delete_output" | grep -q "404"; then
141+
echo "User '$current_user' was not a member of team '$team_slug' (this is OK)."
142+
else
143+
echo "Error: failed to remove '$current_user' from team '$team_slug'." >&2
144+
echo "$delete_output" >&2
145+
exit 1
146+
fi
147+
148+
# --- Link the team to the external IdP group ---
149+
echo ""
150+
echo "Linking team '$team_slug' to external group '$idp_group_name' (group_id: $group_id)..."
151+
152+
link_response=$(gh api \
153+
--method PATCH \
154+
"/orgs/$org/teams/$team_slug/external-groups" \
155+
-F group_id="$group_id")
156+
157+
linked_group=$(echo "$link_response" | jq -r '.group_name // empty')
158+
159+
if [ -n "$linked_group" ]; then
160+
echo "Team '$team_slug' successfully linked to external group '$linked_group'!"
161+
echo "$link_response" | jq .
162+
else
163+
echo "Error: failed to link team to external group."
164+
echo "$link_response" | jq .
165+
exit 1
166+
fi

0 commit comments

Comments
 (0)