From 74642238c531451f4df419a2a2b01833d8187e91 Mon Sep 17 00:00:00 2001 From: Akshit Dhar Date: Thu, 8 Oct 2026 10:59:36 +0200 Subject: [PATCH] fix(team_members): key the members set on the lowercased username GitHub logins are case-insensitive and the API returns the canonical casing. When the configured username differs only in case from what the provider reads back, the two members blocks land on different set keys, so the username DiffSuppressFunc never gets a chance to run and every plan removes and re-adds the member. An apply that is interrupted between the two calls leaves the user removed from the team. Hash the members set on the lowercased username (and role) so both blocks share a key and the existing case-insensitive diff suppression applies. Co-Authored-By: Claude Fable 5.1 --- github/resource_github_team_members.go | 13 +++++ github/resource_github_team_members_test.go | 55 +++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/github/resource_github_team_members.go b/github/resource_github_team_members.go index 9be2b337b5..f6f36c2395 100644 --- a/github/resource_github_team_members.go +++ b/github/resource_github_team_members.go @@ -62,6 +62,7 @@ func resourceGithubTeamMembers() *schema.Resource { "members": { Type: schema.TypeSet, Required: true, + Set: hashTeamMember, Description: "List of users that should be members of the team.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -85,6 +86,18 @@ func resourceGithubTeamMembers() *schema.Resource { } } +// hashTeamMember keys a members block on the lowercased username. GitHub logins +// are case-insensitive and the API returns the canonical casing, so without this +// a configured login that differs only in case lands on a different set key than +// the one read back, and every plan removes and re-adds the member. +func hashTeamMember(v any) int { + m, _ := v.(map[string]any) + username, _ := m["username"].(string) + role, _ := m["role"].(string) + + return schema.HashString(strings.ToLower(username) + ":" + role) +} + func resourceGithubTeamMembersCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { meta, _ := m.(*Owner) client := meta.v3client diff --git a/github/resource_github_team_members_test.go b/github/resource_github_team_members_test.go index 3911bce9fa..5f0dc1de0c 100644 --- a/github/resource_github_team_members_test.go +++ b/github/resource_github_team_members_test.go @@ -57,6 +57,41 @@ resource "github_team_members" "test" { }) }) + t.Run("members_are_case_insensitive", func(t *testing.T) { + t.Parallel() + + team := mustCreateTestTeam(t) + + config := func(username string) string { + return fmt.Sprintf(` +resource "github_team_members" "test" { + team_slug = "%s" + + members { + username = "%s" + role = "member" + } +} +`, team.GetSlug(), username) + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config(testAccConf.testOrgUser1), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_team_members.test", tfjsonpath.New("members"), knownvalue.SetSizeExact(1)), + }, + }, + { + Config: config(flipUsernameCase(testAccConf.testOrgUser1)), + PlanOnly: true, + }, + }, + }) + }) + t.Run("team_by_id_as_slug", func(t *testing.T) { t.Parallel() @@ -431,3 +466,23 @@ resource "github_team_members" "test" { }) }) } + +func Test_hashTeamMember(t *testing.T) { + t.Parallel() + + member := func(username, role string) map[string]any { + return map[string]any{"username": username, "role": role} + } + + if hashTeamMember(member("OctoCat", "member")) != hashTeamMember(member("octocat", "member")) { + t.Error("expected the same hash for a username that differs only in case") + } + + if hashTeamMember(member("OctoCat", "member")) == hashTeamMember(member("OctoCat", "maintainer")) { + t.Error("expected different hashes for different roles") + } + + if hashTeamMember(member("OctoCat", "member")) == hashTeamMember(member("octodog", "member")) { + t.Error("expected different hashes for different usernames") + } +}