Skip to content

fix: read immediate github_team_members only, not child-team members#3498

Draft
robert-crandall wants to merge 2 commits into
mainfrom
robert-crandall/review-pr-3461
Draft

fix: read immediate github_team_members only, not child-team members#3498
robert-crandall wants to merge 2 commits into
mainfrom
robert-crandall/review-pr-3461

Conversation

@robert-crandall

@robert-crandall robert-crandall commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #3497

Before the change?

PR #3461 switched the github_team_members Read from a GraphQL query (members(membership: IMMEDIATE)) to the REST ListTeamMembers endpoint (ListTeamMembersByIDIter / ListTeamMembersBySlugIter).

Those two are not equivalent. The REST list-members endpoint always includes members inherited from child teams - this is documented ("Team members will include the members of child teams") and confirmed in the github/github implementation, which builds the member scope with a hard-coded membership: "all" and exposes only a role filter (no immediate-only option). The old GraphQL IMMEDIATE returned direct members only.

Result: for organizations that use nested (parent/child) teams, Read pulled in members inherited from child teams that the user never configured. Since github_team_members manages the member set authoritatively, that shows up as perpetual drift, and an apply could try to "remove" inherited members.

After the change?

Read goes back to the GraphQL members(membership: IMMEDIATE) query, so only direct members of the team are returned. Flat team structures are unaffected; nested-team orgs no longer see inherited members as drift.

The useful parts of #3461 are kept:

  • Out-of-band deletion still removes the resource from state. Read now resolves the team via getTeam first; a 404 there means the team is gone, so we d.SetId("") and return cleanly. This covers both id-based and slug-based team_id.
  • Create / Update / Delete are unchanged, including the fix: skip github_team_members deletion when parent team is already gone #3461 fix that treats a 404 on member removal as "team already gone" and skips the remaining deletions.

Username and role are still lowercased on read (matching #3461's released behavior), so this PR changes only the member scope - immediate vs. all - and nothing about casing.

How I verified it

  • go build ./github/ - passes.
  • go vet ./github/ - passes.
  • gofmt -l on the changed file - clean.
  • golangci-lint run ./github/ - 0 issues.
  • go test ./github/ -run TestAccGithubTeamMembers - compiles and skips cleanly without org credentials.
  • Cross-checked the REST vs GraphQL semantics against the github/github monolith: the teams/list-members-in-org endpoint constructs its scope with membership: "all" (valid values are immediate / child_team / all), which is what makes the REST path include child-team members.
  • GraphQL query verified against the live GitHub GraphQL schema. Because the query is hand-written, I introspected the live API to confirm every element: TeamMembershipType has IMMEDIATE (alongside CHILD_TEAM/ALL); Team.members accepts membership/first/after; TeamMemberEdge has node (User) + role; TeamMemberConnection has edges + pageInfo; User.login, PageInfo.endCursor/hasNextPage, and TeamMemberRole = {MEMBER, MAINTAINER} all exist.
  • Ran the exact query end-to-end against a real team (integrations/terraform-provider-core-maintainers): 5 edges, every login populated, role MAINTAINER -> maintainer, correct pageInfo, zero query errors.
  • Demonstrated the regression and the fix on live data. On real nested teams, the REST path (equivalent to membership: ALL) and GraphQL IMMEDIATE diverge: integrations/hubbers reports IMMEDIATE=14 vs ALL=40 (26 inherited child-team members), and github/sales reports IMMEDIATE=144 vs ALL=744 (600 inherited). fix: skip github_team_members deletion when parent team is already gone #3461's REST Read would have surfaced those inherited members as drift; the restored GraphQL IMMEDIATE query reports only the directly-configured members.

What I did NOT verify

  • I could not run the acceptance tests (TF_ACC=1) - I don't have org credentials in this environment.
  • The existing out-of-band-deletion acceptance test exercises the new getTeam-404 -> remove-from-state path and should pass, but I have not run it against a live org.
  • I did not add a dedicated nested-team acceptance test. A clean assertion needs a second org member who is a child-team-only member, and the standard test config only provides a single GH_TEST_ORG_USER. I'd rather flag this than add a test that can't actually demonstrate the fix. Open to adding one if there's an agreed fixture/second-user convention.

Marking this as a draft until the acceptance path can be run against a real org.

Does this introduce a breaking change?

  • Yes
  • No

PR #3461 switched the github_team_members Read from a GraphQL query
(members with membership: IMMEDIATE) to the REST ListTeamMembers
endpoint. That endpoint always includes members inherited from child
teams, so organizations using nested teams would see inherited members
as perpetual drift.

Restore the GraphQL IMMEDIATE query for the member list, which returns
only direct members. The out-of-band-deletion handling added in #3461 is
preserved by resolving the team via getTeam first and removing the
resource from state on a 404. Create, Update, and Delete (including the
404 skip-on-delete fix) are unchanged.

Fixes #3497

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

👋 Hi, and thank you for this contribution!

This repo is maintained by GitHub and community members on a best-effort basis. We'll get to this as soon as we can.

You can help us prioritize by joining the discussion on open issues and PRs, sharing details on the changes you need, and reviewing other contributions.


🤖 This is an automated message.

@stevehipwell

Copy link
Copy Markdown
Collaborator

@robert-crandall I think the first thing to do here is to add a test for the regression, I'll take a look at the tests and open a PR to modernize the existing ones and add a test for ignoring child team members.

@deiga deiga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Do we have a regression test for this?

Comment thread github/resource_github_team_members.go Outdated
teamSlug := team.GetSlug()

var teamMembersAndMaintainers []any
log.Printf("[DEBUG] Reading team members: %s", teamIdString)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's refactor all touched log.* lines to tflog if feasible

Suggested change
log.Printf("[DEBUG] Reading team members: %s", teamIdString)
tflog.Debug(ctx, "Reading team members", map[string]any{"teamID": teamIdString})

Convert the log.Printf calls in the Read function (the function this PR
rewrites) to structured tflog.Debug, matching the tflog.Info calls
already used in this file. Read has a context available, so the
conversion is straightforward.

The log.Printf calls in Create/Update/Delete are left as-is since this
PR does not touch those functions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

github_team_members: Read includes child-team members

3 participants