Skip to content

[BUG]: github_repository_file delete commits are never signed, breaking rulesets that require signed commits #3364

@LudovicTOURMAN

Description

@LudovicTOURMAN

Expected Behavior

When authenticating the provider with a GitHub App that has permission to sign commits (i.e. commits created via the Contents API for create/update are returned with commit.verification.verified = true, signed by the web-flow key), deleting a github_repository_file resource should produce a commit that is also signed.

In other words: terraform apply and terraform destroy on a github_repository_file resource should both produce verified commits when the authenticating identity is the same GitHub App.

Actual Behavior

  • terraform apply creating the file → commit is signed (committer: GitHub <[email protected]>, verified: true, reason: valid).
  • terraform destroy (or changing file / branch and triggering a replace) → delete commit is unsigned (committer: <app-slug>[bot] <…@users.noreply.github.com>, verified: false, reason: unsigned).

The two commits are created by the same GitHub App, via the same provider, with identical RepositoryContentFileOptions (since PR #2736 which routed the delete through the shared options builder). The only difference is the underlying REST call:

  • Create / Update: PUT /repos/{owner}/{repo}/contents/{path} → GitHub substitutes committer with the web-flow identity and signs the commit.
  • Delete: DELETE /repos/{owner}/{repo}/contents/{path} → GitHub does not substitute the committer and does not sign the commit.

This means PR #2736 aligned the payload shape but did not actually make delete commits signed — the signing gap is a property of the Contents API itself (PUT vs DELETE), and cannot be closed by tweaking the JSON payload sent from the provider.

Evidence (same test file, same branch, same App)

PUT (create) — signed:

{
  "committer_name": "GitHub",
  "committer_email": "[email protected]",
  "reason": "valid",
  "verified": true,
  "signature_present": true
}

DELETE — unsigned:

{
  "committer_name": "<app-slug>[bot]",
  "committer_email": "<id>+<app-slug>[bot]@users.noreply.github.com",
  "reason": "unsigned",
  "verified": false,
  "signature_present": false
}

Impact

Organisations that enforce a require signed commits branch ruleset cannot use github_repository_file end-to-end: creates and updates pass the ruleset, but deletes silently violate it. Depending on the ruleset configuration, this either rejects the delete (breaking terraform destroy / resource replacement) or lets the unsigned commit land and pollutes the protected branch with an unsigned commit.

Suggested direction

Because the signing gap is on GitHub's side of the Contents API, the only way to produce signed delete commits from the provider is to stop using DELETE /contents/{path} and instead create the delete commit through a code path that GitHub does sign. The closest fit is the GraphQL createCommitOnBranch mutation, which accepts fileChanges.deletions and returns a web-flow-signed commit regardless of whether the change is an addition or a deletion. Moving resourceGithubRepositoryFileDelete (and ideally Create / Update too, for consistency) to this mutation would close the gap without requiring any new user-facing configuration.

If that is considered too large a change, a smaller mitigation would be to surface a clear warning in the resource documentation stating that deletes are never signed via the Contents API, so users enforcing signed-commit rulesets can plan around it (e.g. ruleset bypass for the provider's identity, or avoiding github_repository_file for protected branches).


Terraform Version

Terraform v1.14.4
on linux_amd64
+ provider registry.terraform.io/integrations/github v6.10.2

Also reproducible on v6.11.1 (latest at time of report).

Affected Resource(s)

  • github_repository_file

Terraform Configuration Files

terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      version = "6.10.2"
    }
  }
}

provider "github" {
  owner = "<owner>"
  app_auth {
    id              = "<app-id>"
    installation_id = "<installation-id>"
    pem_file        = file("<path-to-app-private-key>.pem")
  }
}

resource "github_repository_file" "test" {
  repository          = "<repo>"
  file                = "signed-delete-test.md"
  content             = "Verifying delete-commit signing\n"
  overwrite_on_create = true
  # commit_author / commit_email intentionally unset so GitHub can
  # attribute and sign the commit as the App.
}

Steps to Reproduce

  1. Create a GitHub App with Contents: Read & write permission and install it on a test repository whose default branch has a require signed commits ruleset (or simply inspect commit verification afterwards via GET /repos/{owner}/{repo}/commits/{sha}).
  2. Apply the configuration above — the create commit is signed (verified: true, committer GitHub <[email protected]>).
  3. terraform destroy — the delete commit is unsigned (verified: false, committer <app>[bot]), and is rejected / flagged by the ruleset.

Debug Output

Not attached — the issue is reproducible purely from the commit metadata returned by GET /repos/{owner}/{repo}/commits/{sha} after each step, and no provider-side error is raised (GitHub accepts the unsigned delete unless a ruleset blocks it).

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions