Skip to content

[BUG]: github_organization_settings silently ignores boolean attributes set to false on create (GetOk zero-value bug in shouldInclude) #3493

Description

@joe-camp-15367

Expected Behavior

On the initial create of a github_organization_settings resource, every boolean attribute explicitly set in the configuration should be sent to the GitHub Org API, including attributes set to false. After a single terraform apply, the server-side org settings should match the configuration exactly, and a subsequent terraform plan should show no changes.

Actual Behavior

On create, any boolean attribute the user explicitly sets to false is omitted from the API PATCH payload. The GitHub API therefore never receives those values and applies its own server-side defaults instead, leaving the org in a state that contradicts the Terraform configuration — with no error or warning at apply time.

A second terraform apply (which goes through the update code path) then corrects the values, because the update path uses d.HasChange() instead of d.GetOk(). So the symptom is: the first apply silently produces the wrong state, and the diff "reappears" until a second apply.

Concrete real-world example (Enterprise Cloud org, fresh resource create):

  • Config: members_can_create_repositories = true, members_can_create_public_repositories = false
  • After the first apply: server ends with public repo creation ENABLED (and members_can_create_public_pages enabled) — the opposite of the false in config.
  • After a second apply (update path): the values are correctly set to false.

This generalizes to any boolean attribute on this resource that the user intends to be false on create, e.g. web_commit_signoff_required, the various fork-related flags, and the security/Dependabot toggles.

Terraform Version

Terraform v1.14.x
+ provider registry.terraform.io/integrations/github v6.12.1  (constraint ~> 6.0)

Confirmed still present on the current main tip (the shouldInclude helper still calls d.GetOk on the create path — see Root Cause).

GitHub Installation Type

GitHub Enterprise Cloud with Personal Accounts (github.com)

(Note: this is a provider-side logic bug on the resource create path and is not specific to the installation type — it reproduces on any org where a boolean is configured false on the initial create.)

Affected Resource(s)

  • github_organization_settings

Terraform Configuration Files

resource "github_organization_settings" "this" {
  billing_email                          = "x@example.com"
  members_can_create_repositories        = true
  members_can_create_public_repositories = false
}

Steps to Reproduce

  1. Ensure no github_organization_settings resource currently exists in state (this bug is on the create path), and that the org's server-side members_can_create_public_repositories is currently true (e.g. GitHub default).
  2. Apply the configuration above for the first time:
    terraform apply
    
  3. Inspect the server state:
    gh api orgs/<org> --jq '{members_can_create_repositories, members_can_create_public_repositories, members_can_create_public_pages}'
    
    Observe members_can_create_public_repositories = true (and members_can_create_public_pages = true) — i.e. the false in config was ignored.
  4. Run terraform plan again — it shows members_can_create_public_repositories: true -> false.
  5. Run terraform apply a second time. This time the update path runs and the value is correctly set to false. terraform plan is now clean.

Root Cause

buildOrganizationSettings in
github/resource_github_organization_settings.go
gates each field on a shouldInclude helper that branches on create vs. update:

shouldInclude := func(fieldName string) bool {
    if !isUpdate {
        // For creates, include if explicitly configured
        _, ok := d.GetOk(fieldName)
        return ok
    }
    // For updates, only include if the field has changed
    return d.HasChange(fieldName)
}

The fields are then conditionally added as pointers, e.g.:

if shouldInclude("members_can_create_public_repositories") {
    settings.MembersCanCreatePublicRepos = github.Ptr(d.Get("members_can_create_public_repositories").(bool))
}

On the create path (!isUpdate) it uses d.GetOk(fieldName). For a TypeBool, d.GetOk returns ok = false whenever the resolved value is the zero value (false) — it cannot distinguish an explicitly configured false from an unset attribute. This is the well-known Terraform SDK GetOk + zero-value footgun.

As a result, shouldInclude("...") returns false for every boolean the user set to false, the corresponding *bool field on the go-github Organization struct is left nil, and because those fields are tagged omitempty, they are dropped from the PATCH body entirely. GitHub then applies its server-side default.

The update path is unaffected because it uses d.HasChange(), which correctly reports a change from true -> false. Hence the "second apply fixes it" behavior.

This shouldInclude/GetOk-on-create logic was introduced in PR #2807 ("fix: HTTP 422 error in github_organization_settings resource"). Note: the PR description claims it "Used d.GetOkExists() for boolean fields to properly detect when false values are explicitly configured," but the merged code (and current main) actually uses d.GetOk() in shouldInclude, so the intended false-detection was not achieved on the create path.

Why Default: true does not rescue this

Several of these booleans declare Default: true in the schema (e.g. members_can_create_repositories, members_can_create_public_repositories, members_can_create_public_pages). The schema default does not help: when the user explicitly sets the attribute to false, the resolved value is false, which is still the TypeBool zero value. d.GetOk keys off the zero value, not off whether the value differs from the schema default, so ok is still false and the field is still omitted. (web_commit_signoff_required has Default: false and is affected identically.)

Generalization

This is not limited to the repo-creation booleans in the reproduction. It affects every TypeBool attribute on github_organization_settings that a user intends to set to false on the initial create, including (non-exhaustive):

  • members_can_create_public_repositories, members_can_create_private_repositories, members_can_create_internal_repositories
  • members_can_create_pages, members_can_create_public_pages, members_can_create_private_pages
  • members_can_fork_private_repositories
  • web_commit_signoff_required
  • the advanced_security_* / dependabot_* / secret_scanning_* security toggles

Any of these set to false in the config will be silently dropped from the create PATCH and left at GitHub's server-side default.

Suggested Fix

Make the create-path inclusion test detect explicit configuration rather than relying on the zero-value semantics of GetOk. Two viable directions:

  1. Use raw config to detect explicit set-ness. On create, include the field when it is present (non-null) in the raw configuration, e.g. via d.GetRawConfig() and checking the attribute is not null. This correctly distinguishes a configured false from an unset attribute for TypeBool.

  2. Always include booleans on create. Since the schema supplies a Default for these attributes, the resolved value (d.Get) is always well-defined on create. The create branch of shouldInclude can simply return true for the boolean fields (or the build function can unconditionally set the *bool fields on create via github.Ptr(d.Get(name).(bool))). This sends the user's intended value (including false) every time.

A minimal patch in the spirit of option (2), mirroring the fix pattern already proposed for the analogous GetOk-zero-value bug in #3223:

shouldInclude := func(fieldName string) bool {
    if !isUpdate {
        // On create, include any attribute that is explicitly present in config.
        // GetOk() is wrong for TypeBool: it returns ok=false for an explicitly
        // configured `false`, silently dropping it from the PATCH.
        return !d.GetRawConfig().GetAttr(fieldName).IsNull()
    }
    return d.HasChange(fieldName)
}

Relation to existing issues

Code of Conduct

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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