Skip to content

[BUG]: security_and_analysis.code_security causes a permanent diff (never read back / never written) #3501

Description

@osakiy

Expected Behavior

When security_and_analysis.code_security.status is set to "enabled" (and the repository already has Code Security enabled on GitHub), terraform plan should show no changes. The provider should read the code_security status back from the GitHub API into state and send it on create/update, exactly as it already does for secret_scanning and secret_scanning_push_protection.

Actual Behavior

terraform plan reports a permanent diff on every run, trying to add the code_security block even though the GitHub API already returns code_security.status = "enabled":

   ~ resource "github_repository" "this" {
       id = "<REPO>"
     ~ security_and_analysis {
         + code_security {
             + status = "enabled"
           }
           # (2 unchanged blocks hidden)   # secret_scanning / secret_scanning_push_protection
       }
   }
   Plan: 0 to add, 1 to change, 0 to destroy.

apply does not converge: the next plan shows the same diff again.

Root cause

The code_security block is declared in the schema, but it is wired into neither the read nor the write path:

  • flattenSecurityAndAnalysis() (READ) only sets advanced_security, secret_scanning, secret_scanning_push_protection — it never sets code_security, so the API value (enabled) is dropped and never stored in state.
  • calculateSecurityAndAnalysis() (WRITE) only handles those same three fields — it never sends code_security to the API.

Because config always renders code_security {} but state never contains it, Terraform plans to add it forever, and apply never actually writes it.

Proposed fix

   // calculateSecurityAndAnalysis() — WRITE
   if ok, status := tryGetSecurityAndAnalysisSettingStatus(lookup, "code_security"); ok {
       securityAndAnalysis.CodeSecurity = &github.CodeSecurity{
           Status: github.Ptr(status), // on main: new(status)
       }
   }
   
   // flattenSecurityAndAnalysis() — READ (conditional, like advanced_security)
   codeSecurity := securityAndAnalysis.GetCodeSecurity()
   if codeSecurity != nil {
       securityAndAnalysisMap["code_security"] = []any{map[string]any{
           "status": codeSecurity.GetStatus(),
       }}
   }

Terraform Version

Terraform v1.15.2
on linux_amd64
+ provider registry.terraform.io/integrations/github v6.12.1

GitHub Installation Type

  • GitHub.com (Free, Pro, or Team)
  • GitHub Enterprise Server (on-premises)
  • GitHub Enterprise Cloud with Personal Accounts (github.com)
  • GitHub Enterprise Cloud with Managed Users/EMU (github.com)
  • GitHub Enterprise Cloud with Data Residency (*.ghe.com)
  • I don't know

Affected Resource(s)

  • github_repository

Terraform Configuration Files

resource "github_repository" "test" {
     name        = "code-security-repro"
     description = "repro for code_security perpetual diff"
     visibility  = "private"
   
     security_and_analysis {
       code_security {
         status = "enabled"
       }
       secret_scanning {
         status = "enabled"
       }
       secret_scanning_push_protection {
         status = "enabled"
       }
     }
   }


Precondition: the repository already has Code Security enabled on GitHub (e.g. enabled by an org-level security configuration). Confirm with:


gh api /repos/<OWNER>/<REPO> --jq '.security_and_analysis.code_security'
   {"status":"enabled"}

Steps to Reproduce

  1. Ensure the repo has code_security enabled on GitHub (verify with the gh api command above → "enabled").
  2. Apply the configuration above (apply reports success).
  3. Run terraform plan again.
    => Plan is NOT empty; it shows + code_security { status = "enabled" }.
  4. Run terraform apply then terraform plan repeatedly.
    => The same diff reappears every time; it never converges.

Debug Output

# terraform plan (TF_LOG=DEBUG recommended; relevant excerpt)
   ~ resource "github_repository" "this" {
       id = "<REPO>"
     ~ security_and_analysis {
         + code_security {
             + status = "enabled"
           }
           # (2 unchanged blocks hidden)
       }
   }
   Plan: 0 to add, 1 to change, 0 to destroy.



   # Evidence the API already has it enabled (so this is a provider read/write gap, not API state):
   $ gh api /repos/<OWNER>/<REPO> --jq '.security_and_analysis'
   {
     "code_security": { "status": "enabled" },
     "secret_scanning": { "status": "enabled" },
     "secret_scanning_push_protection": { "status": "enabled" },
     ...
   }

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

    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