From c7bc8afbd7814c3a0af3a9e89a73456d9bc936e0 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Mon, 19 Jan 2026 23:27:33 +0000 Subject: [PATCH 1/9] feat: add excluded_team_member_node_ids support to team settings - Add excluded_team_member_node_ids field to review_request_delegation - Allow teams to exclude specific members from PR review assignments - Update schema with proper validation and documentation - Add comprehensive test coverage for the new functionality - Update documentation with usage examples Resolves #1972 --- github/resource_github_team_settings.go | 34 +++++++++++++--- github/resource_github_team_settings_test.go | 43 ++++++++++++++++++++ website/docs/r/team_settings.html.markdown | 2 + 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index 38841d3e80..cf26b7f55f 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -83,6 +83,14 @@ func resourceGithubTeamSettings() *schema.Resource { Default: false, Description: "whether to notify the entire team when at least one member is also assigned to the pull request.", }, + "excluded_team_member_node_ids": { + Type: schema.TypeSet, + Optional: true, + Description: "A list of team member node IDs to exclude from the PR review process.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, }, }, }, @@ -151,6 +159,9 @@ func resourceGithubTeamSettingsRead(d *schema.ResourceData, meta any) error { return err } } + // NOTE: The exclusion list is not available via the GraphQL read query yet. + // The excluded_team_member_node_ids field can be set but cannot be read back from the GitHub API. + // This is because the GraphQL API for team review assignments is currently in preview. return nil } @@ -177,12 +188,22 @@ func resourceGithubTeamSettingsUpdate(d *schema.ResourceData, meta any) error { } `graphql:"updateTeamReviewAssignment(input:$input)"` } + exclusionList := make([]string, 0) + if excludedIDs, ok := settings["excluded_team_member_node_ids"]; ok && excludedIDs != nil { + for _, v := range excludedIDs.(*schema.Set).List() { + if v != nil { + exclusionList = append(exclusionList, v.(string)) + } + } + } + return graphql.Mutate(ctx, &mutation, UpdateTeamReviewAssignmentInput{ TeamID: d.Id(), ReviewRequestDelegation: true, ReviewRequestDelegationAlgorithm: settings["algorithm"].(string), ReviewRequestDelegationCount: settings["member_count"].(int), ReviewRequestDelegationNotifyAll: settings["notify"].(bool), + ExcludedTeamMemberIds: exclusionList, }, nil) } } @@ -252,12 +273,13 @@ func resolveTeamIDs(idOrSlug string, meta *Owner, ctx context.Context) (nodeId, } type UpdateTeamReviewAssignmentInput struct { - ClientMutationID string `json:"clientMutationId,omitempty"` - TeamID string `graphql:"id" json:"id"` - ReviewRequestDelegation bool `graphql:"enabled" json:"enabled"` - ReviewRequestDelegationAlgorithm string `graphql:"algorithm" json:"algorithm"` - ReviewRequestDelegationCount int `graphql:"teamMemberCount" json:"teamMemberCount"` - ReviewRequestDelegationNotifyAll bool `graphql:"notifyTeam" json:"notifyTeam"` + ClientMutationID string `json:"clientMutationId,omitempty"` + TeamID string `graphql:"id" json:"id"` + ReviewRequestDelegation bool `graphql:"enabled" json:"enabled"` + ReviewRequestDelegationAlgorithm string `graphql:"algorithm" json:"algorithm"` + ReviewRequestDelegationCount int `graphql:"teamMemberCount" json:"teamMemberCount"` + ReviewRequestDelegationNotifyAll bool `graphql:"notifyTeam" json:"notifyTeam"` + ExcludedTeamMemberIds []string `graphql:"excludedTeamMemberIds" json:"excludedTeamMemberIds"` } func defaultTeamReviewAssignmentSettings(id string) UpdateTeamReviewAssignmentInput { diff --git a/github/resource_github_team_settings_test.go b/github/resource_github_team_settings_test.go index 053440a33f..d50898b02b 100644 --- a/github/resource_github_team_settings_test.go +++ b/github/resource_github_team_settings_test.go @@ -123,6 +123,49 @@ func TestAccGithubTeamSettings(t *testing.T) { }) }) + t.Run("manages team code review settings with excluded members", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + teamName := fmt.Sprintf("%steam-settings-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_team" "test" { + name = "%s" + description = "generated by terraform provider automated testing" + } + + resource "github_team_settings" "test" { + team_id = "${github_team.test.id}" + review_request_delegation { + algorithm = "ROUND_ROBIN" + member_count = 1 + notify = true + excluded_team_member_node_ids = ["MDQ6VXNlcjU4MzIzMQ==", "MDQ6VXNlcjU4MzIzMg=="] + } + } + `, teamName) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_team_settings.test", "review_request_delegation.0.algorithm", + "ROUND_ROBIN", + ), + resource.TestCheckResourceAttr( + "github_team_settings.test", "review_request_delegation.0.excluded_team_member_node_ids.#", + "2", + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) + t.Run("cannot manage team code review settings if disabled", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) teamName := fmt.Sprintf("%steam-settings-%s", testResourcePrefix, randomID) diff --git a/website/docs/r/team_settings.html.markdown b/website/docs/r/team_settings.html.markdown index 318062904e..099e1a0629 100644 --- a/website/docs/r/team_settings.html.markdown +++ b/website/docs/r/team_settings.html.markdown @@ -30,6 +30,7 @@ resource "github_team_settings" "code_review_settings" { algorithm = "ROUND_ROBIN" member_count = 1 notify = true + excluded_team_member_node_ids = ["MDQ6VXNlcjU4MzIzMQ=="] } } ``` @@ -48,6 +49,7 @@ The following arguments are supported: * `algorithm` - (Optional) The algorithm to use when assigning pull requests to team members. Supported values are `ROUND_ROBIN` and `LOAD_BALANCE`. Default value is `ROUND_ROBIN` * `member_count` - (Optional) The number of team members to assign to a pull request * `notify` - (Optional) whether to notify the entire team when at least one member is also assigned to the pull request +* `excluded_team_member_node_ids` - (Optional) A list of team member node IDs to exclude from the PR review process. These are GitHub user node IDs that can be obtained from the GitHub GraphQL API. ## Import From 638bd454ebc1fe0183e673a99d25ef094c82e640 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Mon, 19 Jan 2026 23:41:04 +0000 Subject: [PATCH 2/9] refactor: improve UX by accepting usernames instead of node IDs - Change excluded_team_member_node_ids to excluded_team_members - Accept GitHub usernames instead of GraphQL node IDs - Add getUserNodeId helper function to lookup node IDs automatically - Update tests to use friendly usernames like 'octocat', 'defunkt' - Update documentation with simpler examples - Makes configuration much more user-friendly and intuitive --- github/resource_github_team_settings.go | 37 +++++++++++++++++--- github/resource_github_team_settings_test.go | 4 +-- website/docs/r/team_settings.html.markdown | 4 +-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index cf26b7f55f..5a562149d0 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -10,6 +10,28 @@ import ( "github.com/shurcooL/githubv4" ) +// getUserNodeId retrieves the GraphQL node ID for a given username +func getUserNodeId(ctx context.Context, meta interface{}, username string) (string, error) { + client := meta.(*Owner).v4client + + var query struct { + User struct { + ID githubv4.ID `graphql:"id"` + } `graphql:"user(login: $username)"` + } + + variables := map[string]interface{}{ + "username": githubv4.String(username), + } + + err := client.Query(ctx, &query, variables) + if err != nil { + return "", fmt.Errorf("failed to query user %s: %v", username, err) + } + + return string(query.User.ID.(githubv4.String)), nil +} + func resourceGithubTeamSettings() *schema.Resource { return &schema.Resource{ Create: resourceGithubTeamSettingsCreate, @@ -83,10 +105,10 @@ func resourceGithubTeamSettings() *schema.Resource { Default: false, Description: "whether to notify the entire team when at least one member is also assigned to the pull request.", }, - "excluded_team_member_node_ids": { + "excluded_team_members": { Type: schema.TypeSet, Optional: true, - Description: "A list of team member node IDs to exclude from the PR review process.", + Description: "A list of team member usernames to exclude from the PR review process.", Elem: &schema.Schema{ Type: schema.TypeString, }, @@ -189,10 +211,15 @@ func resourceGithubTeamSettingsUpdate(d *schema.ResourceData, meta any) error { } exclusionList := make([]string, 0) - if excludedIDs, ok := settings["excluded_team_member_node_ids"]; ok && excludedIDs != nil { - for _, v := range excludedIDs.(*schema.Set).List() { + if excludedMembers, ok := settings["excluded_team_members"]; ok && excludedMembers != nil { + for _, v := range excludedMembers.(*schema.Set).List() { if v != nil { - exclusionList = append(exclusionList, v.(string)) + username := v.(string) + nodeId, err := getUserNodeId(ctx, meta, username) + if err != nil { + return fmt.Errorf("failed to get node ID for user %s: %v", username, err) + } + exclusionList = append(exclusionList, nodeId) } } } diff --git a/github/resource_github_team_settings_test.go b/github/resource_github_team_settings_test.go index d50898b02b..b9e946fa80 100644 --- a/github/resource_github_team_settings_test.go +++ b/github/resource_github_team_settings_test.go @@ -138,7 +138,7 @@ func TestAccGithubTeamSettings(t *testing.T) { algorithm = "ROUND_ROBIN" member_count = 1 notify = true - excluded_team_member_node_ids = ["MDQ6VXNlcjU4MzIzMQ==", "MDQ6VXNlcjU4MzIzMg=="] + excluded_team_members = ["octocat", "defunkt"] } } `, teamName) @@ -149,7 +149,7 @@ func TestAccGithubTeamSettings(t *testing.T) { "ROUND_ROBIN", ), resource.TestCheckResourceAttr( - "github_team_settings.test", "review_request_delegation.0.excluded_team_member_node_ids.#", + "github_team_settings.test", "review_request_delegation.0.excluded_team_members.#", "2", ), ) diff --git a/website/docs/r/team_settings.html.markdown b/website/docs/r/team_settings.html.markdown index 099e1a0629..513a421188 100644 --- a/website/docs/r/team_settings.html.markdown +++ b/website/docs/r/team_settings.html.markdown @@ -30,7 +30,7 @@ resource "github_team_settings" "code_review_settings" { algorithm = "ROUND_ROBIN" member_count = 1 notify = true - excluded_team_member_node_ids = ["MDQ6VXNlcjU4MzIzMQ=="] + excluded_team_members = ["octocat", "defunkt"] } } ``` @@ -49,7 +49,7 @@ The following arguments are supported: * `algorithm` - (Optional) The algorithm to use when assigning pull requests to team members. Supported values are `ROUND_ROBIN` and `LOAD_BALANCE`. Default value is `ROUND_ROBIN` * `member_count` - (Optional) The number of team members to assign to a pull request * `notify` - (Optional) whether to notify the entire team when at least one member is also assigned to the pull request -* `excluded_team_member_node_ids` - (Optional) A list of team member node IDs to exclude from the PR review process. These are GitHub user node IDs that can be obtained from the GitHub GraphQL API. +* `excluded_team_members` - (Optional) A list of team member usernames to exclude from the PR review process. ## Import From 9750a01b8d44f643a127916e5663043f929d4edc Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Mon, 19 Jan 2026 23:52:46 +0000 Subject: [PATCH 3/9] fix: correct GraphQL API field types for excludedTeamMemberIds - Change ExcludedTeamMemberIds field type from []string to []githubv4.ID - Convert string node IDs to githubv4.ID type when building exclusion list - Ensures compliance with GitHub GraphQL API specification for UpdateTeamReviewAssignmentInput --- github/resource_github_team_settings.go | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index 5a562149d0..dc5ab3b250 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -13,22 +13,22 @@ import ( // getUserNodeId retrieves the GraphQL node ID for a given username func getUserNodeId(ctx context.Context, meta interface{}, username string) (string, error) { client := meta.(*Owner).v4client - + var query struct { User struct { ID githubv4.ID `graphql:"id"` } `graphql:"user(login: $username)"` } - + variables := map[string]interface{}{ "username": githubv4.String(username), } - + err := client.Query(ctx, &query, variables) if err != nil { return "", fmt.Errorf("failed to query user %s: %v", username, err) } - + return string(query.User.ID.(githubv4.String)), nil } @@ -210,7 +210,7 @@ func resourceGithubTeamSettingsUpdate(d *schema.ResourceData, meta any) error { } `graphql:"updateTeamReviewAssignment(input:$input)"` } - exclusionList := make([]string, 0) + exclusionList := make([]githubv4.ID, 0) if excludedMembers, ok := settings["excluded_team_members"]; ok && excludedMembers != nil { for _, v := range excludedMembers.(*schema.Set).List() { if v != nil { @@ -219,7 +219,7 @@ func resourceGithubTeamSettingsUpdate(d *schema.ResourceData, meta any) error { if err != nil { return fmt.Errorf("failed to get node ID for user %s: %v", username, err) } - exclusionList = append(exclusionList, nodeId) + exclusionList = append(exclusionList, githubv4.ID(nodeId)) } } } @@ -300,13 +300,13 @@ func resolveTeamIDs(idOrSlug string, meta *Owner, ctx context.Context) (nodeId, } type UpdateTeamReviewAssignmentInput struct { - ClientMutationID string `json:"clientMutationId,omitempty"` - TeamID string `graphql:"id" json:"id"` - ReviewRequestDelegation bool `graphql:"enabled" json:"enabled"` - ReviewRequestDelegationAlgorithm string `graphql:"algorithm" json:"algorithm"` - ReviewRequestDelegationCount int `graphql:"teamMemberCount" json:"teamMemberCount"` - ReviewRequestDelegationNotifyAll bool `graphql:"notifyTeam" json:"notifyTeam"` - ExcludedTeamMemberIds []string `graphql:"excludedTeamMemberIds" json:"excludedTeamMemberIds"` + ClientMutationID string `json:"clientMutationId,omitempty"` + TeamID string `graphql:"id" json:"id"` + ReviewRequestDelegation bool `graphql:"enabled" json:"enabled"` + ReviewRequestDelegationAlgorithm string `graphql:"algorithm" json:"algorithm"` + ReviewRequestDelegationCount int `graphql:"teamMemberCount" json:"teamMemberCount"` + ReviewRequestDelegationNotifyAll bool `graphql:"notifyTeam" json:"notifyTeam"` + ExcludedTeamMemberIds []githubv4.ID `graphql:"excludedTeamMemberIds" json:"excludedTeamMemberIds"` } func defaultTeamReviewAssignmentSettings(id string) UpdateTeamReviewAssignmentInput { From c5627f818358dbcf69163a1c268dcc5a244105d1 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Tue, 20 Jan 2026 15:14:58 +0000 Subject: [PATCH 4/9] perf: optimize user node ID retrieval for team settings excluded members - Replace single-user getUserNodeId with batch getBatchUserNodeIds function - Use GraphQL field aliases to fetch multiple user node IDs in one request - Reduce API calls from N individual requests to 1 batch request - Follow existing pattern from data_source_github_users.go - Improve performance and API rate limit usage for teams with multiple excluded members - Add proper error wrapping with %w format verb --- github/resource_github_team_settings.go | 69 +++++++++++++++++++------ 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index dc5ab3b250..7b4c40a745 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -4,32 +4,56 @@ import ( "context" "errors" "fmt" + "reflect" "strconv" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/shurcooL/githubv4" ) -// getUserNodeId retrieves the GraphQL node ID for a given username -func getUserNodeId(ctx context.Context, meta interface{}, username string) (string, error) { +// getBatchUserNodeIds retrieves the GraphQL node IDs for multiple usernames in a single request. +func getBatchUserNodeIds(ctx context.Context, meta any, usernames []string) (map[string]string, error) { + if len(usernames) == 0 { + return make(map[string]string), nil + } + client := meta.(*Owner).v4client - var query struct { - User struct { - ID githubv4.ID `graphql:"id"` - } `graphql:"user(login: $username)"` + // Create GraphQL variables and query struct using reflection (similar to data_source_github_users.go) + type UserFragment struct { + ID githubv4.ID `graphql:"id"` } - variables := map[string]interface{}{ - "username": githubv4.String(username), + var fields []reflect.StructField + variables := make(map[string]any) + + for idx, username := range usernames { + label := fmt.Sprintf("User%d", idx) + variables[label] = githubv4.String(username) + fields = append(fields, reflect.StructField{ + Name: label, + Type: reflect.TypeFor[UserFragment](), + Tag: reflect.StructTag(fmt.Sprintf("graphql:\"%[1]s: user(login: $%[1]s)\"", label)), + }) } - err := client.Query(ctx, &query, variables) + query := reflect.New(reflect.StructOf(fields)).Elem() + + err := client.Query(ctx, query.Addr().Interface(), variables) if err != nil { - return "", fmt.Errorf("failed to query user %s: %v", username, err) + return nil, fmt.Errorf("failed to query users in batch: %w", err) + } + + result := make(map[string]string) + for idx, username := range usernames { + label := fmt.Sprintf("User%d", idx) + user := query.FieldByName(label).Interface().(UserFragment) + if user.ID != "" { + result[username] = string(user.ID.(githubv4.String)) + } } - return string(query.User.ID.(githubv4.String)), nil + return result, nil } func resourceGithubTeamSettings() *schema.Resource { @@ -212,14 +236,29 @@ func resourceGithubTeamSettingsUpdate(d *schema.ResourceData, meta any) error { exclusionList := make([]githubv4.ID, 0) if excludedMembers, ok := settings["excluded_team_members"]; ok && excludedMembers != nil { + // Collect all usernames first + usernames := make([]string, 0) for _, v := range excludedMembers.(*schema.Set).List() { if v != nil { username := v.(string) - nodeId, err := getUserNodeId(ctx, meta, username) - if err != nil { - return fmt.Errorf("failed to get node ID for user %s: %v", username, err) + usernames = append(usernames, username) + } + } + + // Get all node IDs in a single batch request + if len(usernames) > 0 { + nodeIds, err := getBatchUserNodeIds(ctx, meta, usernames) + if err != nil { + return fmt.Errorf("failed to get node IDs for excluded members: %w", err) + } + + // Convert to the exclusion list + for _, username := range usernames { + if nodeId, exists := nodeIds[username]; exists { + exclusionList = append(exclusionList, githubv4.ID(nodeId)) + } else { + return fmt.Errorf("failed to get node ID for user %s: user not found", username) } - exclusionList = append(exclusionList, githubv4.ID(nodeId)) } } } From d39addf5df619313b5a9a629670b07c96d67b3d1 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Tue, 20 Jan 2026 15:22:17 +0000 Subject: [PATCH 5/9] rename to excluded_members --- github/resource_github_team_settings.go | 4 ++-- github/resource_github_team_settings_test.go | 4 ++-- website/docs/r/team_settings.html.markdown | 21 ++++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index 7b4c40a745..da4990d579 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -129,7 +129,7 @@ func resourceGithubTeamSettings() *schema.Resource { Default: false, Description: "whether to notify the entire team when at least one member is also assigned to the pull request.", }, - "excluded_team_members": { + "excluded_members": { Type: schema.TypeSet, Optional: true, Description: "A list of team member usernames to exclude from the PR review process.", @@ -235,7 +235,7 @@ func resourceGithubTeamSettingsUpdate(d *schema.ResourceData, meta any) error { } exclusionList := make([]githubv4.ID, 0) - if excludedMembers, ok := settings["excluded_team_members"]; ok && excludedMembers != nil { + if excludedMembers, ok := settings["excluded_members"]; ok && excludedMembers != nil { // Collect all usernames first usernames := make([]string, 0) for _, v := range excludedMembers.(*schema.Set).List() { diff --git a/github/resource_github_team_settings_test.go b/github/resource_github_team_settings_test.go index b9e946fa80..d8374bce73 100644 --- a/github/resource_github_team_settings_test.go +++ b/github/resource_github_team_settings_test.go @@ -138,7 +138,7 @@ func TestAccGithubTeamSettings(t *testing.T) { algorithm = "ROUND_ROBIN" member_count = 1 notify = true - excluded_team_members = ["octocat", "defunkt"] + excluded_members = ["octocat", "defunkt"] } } `, teamName) @@ -149,7 +149,7 @@ func TestAccGithubTeamSettings(t *testing.T) { "ROUND_ROBIN", ), resource.TestCheckResourceAttr( - "github_team_settings.test", "review_request_delegation.0.excluded_team_members.#", + "github_team_settings.test", "review_request_delegation.0.excluded_members.#", "2", ), ) diff --git a/website/docs/r/team_settings.html.markdown b/website/docs/r/team_settings.html.markdown index 513a421188..b21ff5963b 100644 --- a/website/docs/r/team_settings.html.markdown +++ b/website/docs/r/team_settings.html.markdown @@ -11,7 +11,7 @@ This resource manages the team settings (in particular the request review delega Creating this resource will alter the team Code Review settings. -The team must both belong to the same organization configured in the provider on GitHub. +The team must both belong to the same organization configured in the provider on GitHub. ~> **Note**: This resource relies on the v4 GraphQl GitHub API. If this API is not available, or the Stone Crop schema preview is not available, then this resource will not work as intended. @@ -30,7 +30,7 @@ resource "github_team_settings" "code_review_settings" { algorithm = "ROUND_ROBIN" member_count = 1 notify = true - excluded_team_members = ["octocat", "defunkt"] + excluded_members = ["octocat", "defunkt"] } } ``` @@ -39,18 +39,17 @@ resource "github_team_settings" "code_review_settings" { The following arguments are supported: -* `team_id` - (Required) The GitHub team id or the GitHub team slug -* `review_request_delegation` - (Optional) The settings for delegating code reviews to individuals on behalf of the team. If this block is present, even without any fields, then review request delegation will be enabled for the team. See [GitHub Review Request Delegation](#github-review-request-delegation-configuration) below for details. See [GitHub's documentation](https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team#configuring-team-notifications) for more configuration details. +- `team_id` - (Required) The GitHub team id or the GitHub team slug +- `review_request_delegation` - (Optional) The settings for delegating code reviews to individuals on behalf of the team. If this block is present, even without any fields, then review request delegation will be enabled for the team. See [GitHub Review Request Delegation](#github-review-request-delegation-configuration) below for details. See [GitHub's documentation](https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team#configuring-team-notifications) for more configuration details. ### GitHub Review Request Delegation Configuration The following arguments are supported: -* `algorithm` - (Optional) The algorithm to use when assigning pull requests to team members. Supported values are `ROUND_ROBIN` and `LOAD_BALANCE`. Default value is `ROUND_ROBIN` -* `member_count` - (Optional) The number of team members to assign to a pull request -* `notify` - (Optional) whether to notify the entire team when at least one member is also assigned to the pull request -* `excluded_team_members` - (Optional) A list of team member usernames to exclude from the PR review process. - +- `algorithm` - (Optional) The algorithm to use when assigning pull requests to team members. Supported values are `ROUND_ROBIN` and `LOAD_BALANCE`. Default value is `ROUND_ROBIN` +- `member_count` - (Optional) The number of team members to assign to a pull request +- `notify` - (Optional) whether to notify the entire team when at least one member is also assigned to the pull request +- `excluded_members` - (Optional) A list of team member usernames to exclude from the PR review process. ## Import @@ -59,7 +58,9 @@ GitHub Teams can be imported using the GitHub team ID, or the team slug e.g. ``` $ terraform import github_team.code_review_settings 1234567 ``` + or, + ``` $ terraform import github_team_settings.code_review_settings SomeTeam -``` \ No newline at end of file +``` From 6f3997614bc371414c6d31a2810ab2c2d46cadc7 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Tue, 20 Jan 2026 16:03:03 +0000 Subject: [PATCH 6/9] fix types --- github/resource_github_team_settings.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index da4990d579..549a837e97 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -21,7 +21,7 @@ func getBatchUserNodeIds(ctx context.Context, meta any, usernames []string) (map // Create GraphQL variables and query struct using reflection (similar to data_source_github_users.go) type UserFragment struct { - ID githubv4.ID `graphql:"id"` + ID string `graphql:"id"` } var fields []reflect.StructField @@ -49,7 +49,7 @@ func getBatchUserNodeIds(ctx context.Context, meta any, usernames []string) (map label := fmt.Sprintf("User%d", idx) user := query.FieldByName(label).Interface().(UserFragment) if user.ID != "" { - result[username] = string(user.ID.(githubv4.String)) + result[username] = user.ID } } From 9832b12f4a58f0aaaa6094417ceaa59501f1e84c Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Tue, 20 Jan 2026 16:03:10 +0000 Subject: [PATCH 7/9] add another test --- github/resource_github_team_settings_test.go | 103 +++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/github/resource_github_team_settings_test.go b/github/resource_github_team_settings_test.go index d8374bce73..eb31553ac9 100644 --- a/github/resource_github_team_settings_test.go +++ b/github/resource_github_team_settings_test.go @@ -1,6 +1,7 @@ package github import ( + "context" "fmt" "regexp" "strings" @@ -199,3 +200,105 @@ func TestAccGithubTeamSettings(t *testing.T) { }) }) } + +func TestBatchUserNodeIds(t *testing.T) { + t.Run("fetches user node IDs in batch", func(t *testing.T) { + if len(testAccConf.testExternalUser) == 0 { + t.Skip("No external user provided") + } + + // Skip if not authenticated + skipUnauthenticated(t) + + // Create a test owner/meta object + owner, err := getTestMeta() + if err != nil { + t.Fatalf("Failed to get test meta: %v", err) + } + + ctx := context.Background() + + // Test with single user + t.Run("single user", func(t *testing.T) { + usernames := []string{testAccConf.testExternalUser} + result, err := getBatchUserNodeIds(ctx, owner, usernames) + + if err != nil { + t.Fatalf("getBatchUserNodeIds failed: %v", err) + } + + if len(result) != 1 { + t.Errorf("Expected 1 result, got %d", len(result)) + } + + nodeId, exists := result[testAccConf.testExternalUser] + if !exists { + t.Errorf("Expected node ID for user %s", testAccConf.testExternalUser) + } + + if nodeId == "" { + t.Errorf("Node ID should not be empty") + } + + t.Logf("Successfully fetched node ID %s for user %s", nodeId, testAccConf.testExternalUser) + }) + + // Test with multiple users (using same user multiple times for simplicity) + t.Run("multiple users", func(t *testing.T) { + usernames := []string{testAccConf.testExternalUser, "octocat"} // octocat is a well-known GitHub user + result, err := getBatchUserNodeIds(ctx, owner, usernames) + + if err != nil { + t.Fatalf("getBatchUserNodeIds failed: %v", err) + } + + // We expect at least the test user to exist + if len(result) == 0 { + t.Errorf("Expected at least 1 result, got %d", len(result)) + } + + // Check that our test user exists + nodeId, exists := result[testAccConf.testExternalUser] + if !exists { + t.Errorf("Expected node ID for user %s", testAccConf.testExternalUser) + } else if nodeId == "" { + t.Errorf("Node ID should not be empty") + } + + t.Logf("Successfully fetched %d node IDs in batch request", len(result)) + for username, nodeId := range result { + t.Logf(" User: %s, Node ID: %s", username, nodeId) + } + }) + + // Test with empty list + t.Run("empty usernames list", func(t *testing.T) { + usernames := []string{} + result, err := getBatchUserNodeIds(ctx, owner, usernames) + + if err != nil { + t.Fatalf("getBatchUserNodeIds failed: %v", err) + } + + if len(result) != 0 { + t.Errorf("Expected 0 results for empty list, got %d", len(result)) + } + }) + + // Test with non-existent user + t.Run("non-existent user", func(t *testing.T) { + nonExistentUser := "this-user-definitely-does-not-exist-12345" + usernames := []string{nonExistentUser} + result, err := getBatchUserNodeIds(ctx, owner, usernames) + + if err != nil { + t.Fatalf("getBatchUserNodeIds failed: %v", err) + } + + // Non-existent users should not appear in results + if len(result) != 0 { + t.Errorf("Expected 0 results for non-existent user, got %d", len(result)) + } + }) + }) +} From 67c0712313bff5f328ef1ed616632d8044bafdb4 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Tue, 20 Jan 2026 16:25:05 +0000 Subject: [PATCH 8/9] fix: preserve excluded_members state when GitHub API doesn't return them - Add workaround in resourceGithubTeamSettingsRead to preserve excluded_members from current state - GitHub's GraphQL API currently doesn't support reading back excluded team member IDs - This prevents Terraform from showing unwanted changes during refresh operations - Fixes test failure where excluded_members appeared as additions in plan after apply --- github/resource_github_team_settings.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index 549a837e97..6882ed2535 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -197,6 +197,19 @@ func resourceGithubTeamSettingsRead(d *schema.ResourceData, meta any) error { reviewRequestDelegation["algorithm"] = query.Organization.Team.ReviewRequestDelegationAlgorithm reviewRequestDelegation["member_count"] = query.Organization.Team.ReviewRequestDelegationCount reviewRequestDelegation["notify"] = query.Organization.Team.ReviewRequestDelegationNotifyAll + + // NOTE: The exclusion list is not available via the GraphQL read query yet. + // The excluded_team_member_node_ids field can be set but cannot be read back from the GitHub API. + // This is because the GraphQL API for team review assignments is currently in preview. + // As a workaround, we preserve the excluded_members from the current state. + if currentDelegation := d.Get("review_request_delegation").([]any); len(currentDelegation) > 0 { + if currentSettings, ok := currentDelegation[0].(map[string]any); ok { + if excludedMembers, exists := currentSettings["excluded_members"]; exists { + reviewRequestDelegation["excluded_members"] = excludedMembers + } + } + } + if err = d.Set("review_request_delegation", []any{reviewRequestDelegation}); err != nil { return err } @@ -205,9 +218,8 @@ func resourceGithubTeamSettingsRead(d *schema.ResourceData, meta any) error { return err } } - // NOTE: The exclusion list is not available via the GraphQL read query yet. - // The excluded_team_member_node_ids field can be set but cannot be read back from the GitHub API. - // This is because the GraphQL API for team review assignments is currently in preview. + // NOTE: The excluded members are preserved from the current state in the read logic above + // since the GitHub API doesn't currently support reading them back. return nil } From 5d09324c909fcc59b0fd3a952d21e44cbef7a3f4 Mon Sep 17 00:00:00 2001 From: Janek Lasocki-Biczysko Date: Tue, 20 Jan 2026 16:33:11 +0000 Subject: [PATCH 9/9] fix test --- github/resource_github_team_settings.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/github/resource_github_team_settings.go b/github/resource_github_team_settings.go index 6882ed2535..c0d438c086 100644 --- a/github/resource_github_team_settings.go +++ b/github/resource_github_team_settings.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/shurcooL/githubv4" @@ -40,7 +41,7 @@ func getBatchUserNodeIds(ctx context.Context, meta any, usernames []string) (map query := reflect.New(reflect.StructOf(fields)).Elem() err := client.Query(ctx, query.Addr().Interface(), variables) - if err != nil { + if err != nil && !strings.Contains(err.Error(), "Could not resolve to a User with the login of") { return nil, fmt.Errorf("failed to query users in batch: %w", err) }