Modeling teams in Sourcegraph
Teams in Sourcegraph are groups of users with a common handle. Teams are structured as a tree, so teams can have child teams.
Example team structure that can be modeled:
Engineering ├─ Security ├─ Code Graph │ ├─ Batch Changes │ ├─ Code Insights ├─ source │ ├─ Repo Management │ ├─ IAM Product
Teams in Sourcegraph are usable in code ownership, and other features in the future. Teams can be code owners and will influence the code ownership experience. You can search for code owned by a specific team, and in the future advanced ownership analytics will be informed by given team structures.
Configuring teams
Teams can either be defined directly in Sourcegraph, or be ingested from external systems into Sourcegraph using src-cli. A team name must be globally unique, and the global namespace for names is shared among users, teams, and orgs.
From the UI
Go to Teams from the user navbar item. On this page, click "Create team". A team needs to have a unique name and can optionally take a display name. Additionally, you can add a parent team to build a tree structure as outlined above.
After hitting create, you will be redirected to the team page where you can add Sourcegraph users as team members.
From the CLI
If you prefer a command line based approach, or would like to integrate an external system of record for teams into Sourcegraph, src-cli (v5.0+) provides commands to manage teams:
# List configured teams. Lists root teams, using -parent-team can read child teams. src teams list [-query=<query>] [-parent-team=<name>] # Create a new team. src teams create -name=<name> [-display-name=<displayName>] [-parent-team=<name>] [-read-only] # Update a team. src teams update -name=<name> [-display-name=<displayName>] [-parent-team=<name>] # Delete a team. src teams delete -name=<name> # List team members. src teams members list -name=<name> [-query=<query>] # Add a new team member. See user account matching for details on how this works. src teams members add -team-name=<name> [-email=<email>] [-username=<username>] [-id=<ID>] [-external-account-service-id=<serviceID> -external-account-service-type=<serviceType> [-external-account-account-id=<accountID>] [-external-account-login=<login>]] [-skip-unmatched-members] # Remove a team member. See user account matching for details on how this works. src teams members remove -team-name=<name> [-email=<email>] [-username=<username>] [-id=<ID>] [-external-account-service-id=<serviceID> -external-account-service-type=<serviceType> [-external-account-account-id=<accountID>] [-external-account-login=<login>]] [-skip-unmatched-members]
User account matching
Matching a user account in Sourcegraph from an external system can be achieved in a few different ways: Sourcegraph User ID, Sourcegraph account email, Sourcegraph username or an explicit external-account mapping can be provided.
The matching order is as follows:
- try Sourcegraph user ID
- then try email
- then try username
- then try external-account
Example for external account matching with configured GitHub auth provider:
# Match a user with the account ID 123123123: src teams members add \ -team-name='engineering' \ -external-account-service-id='https://github.com/' \ -external-account-service-type='github' \ -external-account-account-id='123123123' # Match a user with the GitHub login handle alice: src teams members add \ -team-name='engineering' \ -external-account-service-id='https://github.com/' \ -external-account-service-type='github' \ -external-account-login='alice'
Permissions in teams
For now, team permissions are based on membership. Read-only teams are only editable by site-admins. The creator of a team can always modify it, even if they are not a member of it.
Action | Site-admin | Regular user | Direct team member |
---|---|---|---|
Reading teams, metadata and members | 🟢 | 🟢 | 🟢 |
Creating a new team | 🟢 | 🟢 | n/a |
Creating a new child team | 🟢 | 🔴 | 🟢 |
Creating a new read-only team | 🟢 | 🔴 | n/a |
Updating team details/metadata | 🟢 | 🔴 | 🟢 |
Deleting a team | 🟢 | 🔴 | 🟢 |
Deleting a read-only team | 🟢 | 🔴 | 🔴 |
Adding a member to a team | 🟢 | 🔴 | 🟢 |
Removing a member from a team | 🟢 | 🔴 | 🟢 |
Adding a member to a read-only team | 🟢 | 🔴 | 🔴 |
Removing a member from a read-only team | 🟢 | 🔴 | 🔴 |
Known limitations
- Read-only teams can only be created by site-admins
- Identity Provider / SCIM integrations are not available at the moment
Common integrations
GitHub teams
Using the GitHub CLI along with Sourcegraph's CLI, you can ingest teams data from GitHub into Sourcegraph. You may want to run this process regularly.
#!/usr/bin/env bash set -e ORG=<YOUR_ORG_NAME> export ORG if [[ -z "${ORG}" ]]; then echo "ORG environment variable is required." exit 1 fi SRC_ENDPOINT=<YOUR_SOURCEGRAPH_INSTANCE> export SRC_ENDPOINT if [[ -z "${GITHUB_TOKEN}" ]]; then echo "GITHUB_TOKEN environment variable is required." exit 1 fi if [[ -z "${SRC_ACCESS_TOKEN}" ]]; then echo "SRC_ACCESS_TOKEN environment variable is required." exit 1 fi # get_json_property parses the first argument string as JSON and returns the # path passed as the second argument. Empty strings and null are truncated. function get_json_property() { val="$(jq -r ".${2} | select (.!=null)" <<<"${1}")" if [[ -z "$val" || "$val" == "null" ]]; then echo -n return fi echo -n "$val" } # fetch_teams_paginated reads teams from the GitHub API in the configured organization. # It reads all teams until pagination indicates all results have been fetched. function fetch_teams_paginated() { query=$(cat <<EOF query(\$endCursor: String) { organization(login: "${ORG}") { teams(first: 100, after: \$endCursor) { nodes { name slug parentTeam { slug } } pageInfo { hasNextPage endCursor } } } } EOF ) res=$(gh api graphql --paginate -f query="${query}") readarray -t p <<< "$(jq -c '.data.organization.teams.nodes[]' <<<"$res")" teams+=("${p[@]}") printf '%s\n' "${teams[@]}" } # fetch_team_members_paginated reads members of a team with the slug $1 from the # GitHub API in the configured organization. # It reads all members until pagination indicates all results have been fetched. function fetch_team_members_paginated() { team_slug="$1" query=$(cat <<EOF query(\$endCursor: String) { organization(login: "${ORG}") { team(slug: "${team_slug}") { members(membership: IMMEDIATE, first: 100, after: \$endCursor) { nodes { databaseId login } pageInfo { hasNextPage endCursor } } } } } EOF ) res=$(gh api graphql --paginate -f query="${query}") readarray -t members <<< "$(jq -c '.data.organization.team.members.nodes[]' <<<"$res")" printf '%s\n' "${members[@]}" } # create_team_members attempts to leniently create team members for the given team $1. # Team members are matched by account ID. # This is a purely additive function, meaning members that are no longer part of a team # will not be removed. function create_team_members() { team="$1" readarray -t team_members < <(fetch_team_members_paginated "${team}") for member in "${team_members[@]}" do if [ -z "$member" ]; then continue fi echo "$(get_json_property "${member}" "login") is a member of ${team}" src teams members add \ -skip-unmatched-members \ -team-name="${team}" \ -external-account-service-type=github \ -external-account-service-id=https://github.com/ \ -external-account-account-id="$(get_json_property "${member}" "databaseId")" done } # create_team creates a team for the GitHub JSON representation $1 of it. # create_team then calls itself recursively for all of the teams child teams. function create_team() { team="$1" function get_team_property() { get_json_property "${team}" "${1}" } name="$(get_team_property 'slug')" display_name="$(get_team_property 'name')" echo -n "Creating team ${name}" parent="$(get_team_property 'parentTeam.slug')" if [[ -n "$parent" ]]; then echo -n " (child of ${parent})" fi # Newline. echo set +e src team create -name="${name}" -display-name="${display_name}" -parent-team="${parent}" -read-only exit_code="$?" set -e # If team already exists, update instead. if [[ "$exit_code" == "3" ]]; then echo "Updating existing team ${name}" set +e src team update -name="${name}" -display-name="${display_name}" -parent-team="${parent}" exit_code="$?" set -e if [[ "${exit_code}" != "0" ]]; then echo "Failed to update team ${name}, skipping" else create_team_members "${name}" fi elif [[ "$exit_code" != "0" ]]; then exit "$exit_code" else create_team_members "${name}" fi # Create child teams. readarray -t child_teams < <(jq -c ". | select(.parentTeam.slug == \"${name}\")" <<<"${all_teams[@]}") for team in "${child_teams[@]}" do create_team "$team" done } # First, fetch all teams in the organization. readarray -t all_teams < <(fetch_teams_paginated) # Then, extract the root teams (ie. those without a parent). readarray -t root_teams < <(jq -c '. | select(.parentTeam == null)' <<<"${all_teams[@]}") # Recursively call create_team, starting at the root teams. for team in "${root_teams[@]}" do create_team "$team" done
Known limitations
- This script does not remove teams that are no longer present in GitHub
- This script needs to run regularly as new users join Sourcegraph, to add them to the correct teams
- This script does not remove team members that are no longer part of a team, if they were added to the team in Sourcegraph before
GitLab teams
Using the GitLab API, you can ingest teams data from GitLab into Sourcegraph. You may want to run this process regularly.
TODO: Script here that scrapes the GitLab API for teams and converts them into Sourcegraph teams.