Skip to content

[BUG]: github_organization_settings silently drifts repo-creation booleans due to partial PATCH × deprecated legacy field #3429

@naokit-dev

Description

@naokit-dev

Expected Behavior

When terraform apply updates any of the four repo-creation boolean fields on github_organization_settings:

  • members_can_create_repositories
  • members_can_create_public_repositories
  • members_can_create_private_repositories
  • members_can_create_internal_repositories

…the resulting server-side state should match the Terraform config exactly, regardless of which subset of those four actually changed in the diff.

Actual Behavior

Provider silently leaves the org in a state that contradicts the Terraform config, with no warning at apply time. The next terraform plan may report no drift because state is refreshed from the (now-wrong) server values.

Real-world example (Enterprise Cloud org, 2026-05-14):

  • Config: create=true, public=false, private=true, internal=false
  • State before apply: create=false, private=false (drift from UI), public=false, internal=false (matched)
  • terraform apply sent a PATCH containing only the two HasChange fields (members_can_create_repositories=true, members_can_create_private_repositories=true) per resource_github_organization_settings.go L183
  • Server response: create=true, public=true, private=true, internal=true, members_allowed_repository_creation_type=all

The GitHub Org API, when receiving a PATCH that sets members_can_create_repositories=true without also specifying the granular booleans, resets the deprecated members_allowed_repository_creation_type to all, which then silently overrides the unspecified granular booleans server-side. We had to manually reconcile via gh api -X PATCH after every apply that touches these fields.

Terraform Version

Terraform v1.14.8
+ provider registry.terraform.io/integrations/github v6.11.1

Affected Resource(s)

  • github_organization_settings

Steps to Reproduce

  1. Real org with Enterprise plan and config:
resource "github_organization_settings" "this" {
  billing_email                            = "x@example.com"
  members_can_create_repositories          = true
  members_can_create_public_repositories   = false
  members_can_create_private_repositories  = true
  members_can_create_internal_repositories = false
}
  1. Have server state diverge such that some but not all of the four booleans match config (easiest: toggle "members can create repositories" off in the UI, leaving the others at their existing values).
  2. terraform apply.
  3. gh api orgs/<org> --jq '{members_can_create_repositories, members_can_create_public_repositories, members_can_create_private_repositories, members_can_create_internal_repositories, members_allowed_repository_creation_type}' — observe members_allowed_repository_creation_type=all and the unspecified booleans flipped to true.

Root cause

buildOrganizationSettings determines what to PATCH via d.HasChange() per field, independently. This is correct in isolation but the four repo-creation booleans are mutually entangled at the API layer through the deprecated members_allowed_repository_creation_type field. Sending a partial update for the entangled set causes the API to recompute the legacy field from the partial input, which then resets the omitted fields server-side.

The go-github type explicitly documents this entanglement:

Deprecated: Use MembersCanCreatePublicRepos, MembersCanCreatePrivateRepos, MembersCanCreateInternalRepos instead. The new fields overrides the existing MembersAllowedRepositoryCreationType during 'edit' operation and does not consider 'internal' repositories during 'get' operation

Proposed Fix

Treat the four repo-creation booleans as an atomic group in buildOrganizationSettings: if any has changed, send all four (or three on non-Enterprise plans):

anyRepoCreateChange :=
    d.HasChange("members_can_create_repositories") ||
    d.HasChange("members_can_create_public_repositories") ||
    d.HasChange("members_can_create_private_repositories") ||
    (isEnterprise && d.HasChange("members_can_create_internal_repositories"))

if anyRepoCreateChange {
    settings.MembersCanCreateRepos        = github.Ptr(d.Get("members_can_create_repositories").(bool))
    settings.MembersCanCreatePublicRepos  = github.Ptr(d.Get("members_can_create_public_repositories").(bool))
    settings.MembersCanCreatePrivateRepos = github.Ptr(d.Get("members_can_create_private_repositories").(bool))
    if isEnterprise {
        settings.MembersCanCreateInternalRepos = github.Ptr(d.Get("members_can_create_internal_repositories").(bool))
    }
}

This approach:

  • Fixes the silent drift for all existing users without schema changes (no breaking change)
  • Aligns with go-github's deprecation guidance (use the new fields, not the legacy one)
  • Avoids exposing a deprecated field via schema (which would be the alternative)

Relation to #3388

This issue and #3388 attack the same underlying problem from different angles:

Implementing the fix here makes #3388's schema exposure optional rather than necessary, and avoids the awkwardness of a Deprecated: schema field. The two approaches are not mutually exclusive.

If maintainers prefer the fix proposed here, I'm willing to put up a PR.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: BugSomething isn't working as documentedvNext

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions