integrations / integrations/terraform-provider-github
[BUG]: `github_organization_settings` silently ignores boolean attributes set to `false` on create (GetOk zero-value bug in shouldInclude)
- Dominant language
- Go
- Stars
- 1.2k
- Forks
- 1k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 8
Description
### 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
```hcl
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/ --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`](https://github.com/integrations/terraform-provider-github/blob/main/github/resource_github_organization_settings.go#L185-L193)
gates each field on a `shouldInclude` helper that branches on create vs. update:
```go
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.:
```go
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:
```go
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
- **#3429** (`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**. #3429 is about the **update** path: a partial `HasChange`-driven PATCH that resets the deprecated `members_allowed_repository_creation_type` server-side, entangling the four repo-creation booleans. This issue is about the **create** path, where `d.GetOk` omits *any* `false` boolean before the API is even called. The two are independent and would need separate fixes.
- **#3223** (`sha_pinning_required = false is silently ignored due to d.GetOk() zero-value bug`) — **same root cause** (`d.GetOk` zero-value footgun on a `TypeBool`), but in **different resources** (`github_actions_organization_permissions` / `github_actions_repository_permissions`). This issue reports the identical class of bug in `github_organization_settings`, where it additionally manifests on every `false`-intended boolean across the whole resource.
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Assessment
This issue has not been assessed yet.