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
- 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).
- Apply the configuration above for the first time:
- 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.
- Run
terraform plan again — it shows members_can_create_public_repositories: true -> false.
- 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:
-
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.
-
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
Expected Behavior
On the initial create of a
github_organization_settingsresource, every boolean attribute explicitly set in the configuration should be sent to the GitHub Org API, including attributes set tofalse. After a singleterraform apply, the server-side org settings should match the configuration exactly, and a subsequentterraform planshould show no changes.Actual Behavior
On create, any boolean attribute the user explicitly sets to
falseis 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 usesd.HasChange()instead ofd.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):
members_can_create_repositories = true,members_can_create_public_repositories = falseapply: server ends with public repo creation ENABLED (andmembers_can_create_public_pagesenabled) — the opposite of thefalsein config.apply(update path): the values are correctly set tofalse.This generalizes to any boolean attribute on this resource that the user intends to be
falseon create, e.g.web_commit_signoff_required, the various fork-related flags, and the security/Dependabot toggles.Terraform Version
Confirmed still present on the current
maintip (theshouldIncludehelper still callsd.GetOkon 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
falseon the initial create.)Affected Resource(s)
github_organization_settingsTerraform Configuration Files
Steps to Reproduce
github_organization_settingsresource currently exists in state (this bug is on the create path), and that the org's server-sidemembers_can_create_public_repositoriesis currentlytrue(e.g. GitHub default).members_can_create_public_repositories = true(andmembers_can_create_public_pages = true) — i.e. thefalsein config was ignored.terraform planagain — it showsmembers_can_create_public_repositories: true -> false.terraform applya second time. This time the update path runs and the value is correctly set tofalse.terraform planis now clean.Root Cause
buildOrganizationSettingsingithub/resource_github_organization_settings.gogates each field on a
shouldIncludehelper that branches on create vs. update:The fields are then conditionally added as pointers, e.g.:
On the create path (
!isUpdate) it usesd.GetOk(fieldName). For aTypeBool,d.GetOkreturnsok = falsewhenever the resolved value is the zero value (false) — it cannot distinguish an explicitly configuredfalsefrom an unset attribute. This is the well-known Terraform SDKGetOk+ zero-value footgun.As a result,
shouldInclude("...")returnsfalsefor every boolean the user set tofalse, the corresponding*boolfield on the go-githubOrganizationstruct is leftnil, and because those fields are taggedomitempty, 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 fromtrue -> false. Hence the "second apply fixes it" behavior.This
shouldInclude/GetOk-on-create logic was introduced in PR #2807 ("fix: HTTP 422 error ingithub_organization_settingsresource"). Note: the PR description claims it "Usedd.GetOkExists()for boolean fields to properly detect whenfalsevalues are explicitly configured," but the merged code (and currentmain) actually usesd.GetOk()inshouldInclude, so the intendedfalse-detection was not achieved on the create path.Why
Default: truedoes not rescue thisSeveral of these booleans declare
Default: truein 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 tofalse, the resolved value isfalse, which is still theTypeBoolzero value.d.GetOkkeys off the zero value, not off whether the value differs from the schema default, sookis stillfalseand the field is still omitted. (web_commit_signoff_requiredhasDefault: falseand is affected identically.)Generalization
This is not limited to the repo-creation booleans in the reproduction. It affects every
TypeBoolattribute ongithub_organization_settingsthat a user intends to set tofalseon the initial create, including (non-exhaustive):members_can_create_public_repositories,members_can_create_private_repositories,members_can_create_internal_repositoriesmembers_can_create_pages,members_can_create_public_pages,members_can_create_private_pagesmembers_can_fork_private_repositoriesweb_commit_signoff_requiredadvanced_security_*/dependabot_*/secret_scanning_*security togglesAny of these set to
falsein 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: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 configuredfalsefrom an unset attribute forTypeBool.Always include booleans on create. Since the schema supplies a
Defaultfor these attributes, the resolved value (d.Get) is always well-defined on create. The create branch ofshouldIncludecan simply returntruefor the boolean fields (or the build function can unconditionally set the*boolfields on create viagithub.Ptr(d.Get(name).(bool))). This sends the user's intended value (includingfalse) 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:Relation to existing issues
github_organization_settings silently drifts repo-creation booleans due to partial PATCH × deprecated legacy field) — same resource and a similar observable symptom (repo-creation booleans not matching config), but a different root cause. [BUG]: github_organization_settings silently drifts repo-creation booleans due to partial PATCH × deprecated legacy field #3429 is about the update path: a partialHasChange-driven PATCH that resets the deprecatedmembers_allowed_repository_creation_typeserver-side, entangling the four repo-creation booleans. This issue is about the create path, whered.GetOkomits anyfalseboolean before the API is even called. The two are independent and would need separate fixes.sha_pinning_required = false is silently ignored due to d.GetOk() zero-value bug) — same root cause (d.GetOkzero-value footgun on aTypeBool), but in different resources (github_actions_organization_permissions/github_actions_repository_permissions). This issue reports the identical class of bug ingithub_organization_settings, where it additionally manifests on everyfalse-intended boolean across the whole resource.Code of Conduct