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)
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
- 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}).
- Apply the configuration above — the create commit is signed (
verified: true, committer GitHub <[email protected]>).
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
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/updateare returned withcommit.verification.verified = true, signed by theweb-flowkey), deleting agithub_repository_fileresource should produce a commit that is also signed.In other words:
terraform applyandterraform destroyon agithub_repository_fileresource should both produce verified commits when the authenticating identity is the same GitHub App.Actual Behavior
terraform applycreating the file → commit is signed (committer:GitHub <[email protected]>,verified: true, reason:valid).terraform destroy(or changingfile/branchand 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:PUT /repos/{owner}/{repo}/contents/{path}→ GitHub substitutescommitterwith theweb-flowidentity and signs the commit.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 commitsbranch ruleset cannot usegithub_repository_fileend-to-end: creates and updates pass the ruleset, but deletes silently violate it. Depending on the ruleset configuration, this either rejects the delete (breakingterraform 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 GraphQLcreateCommitOnBranchmutation, which acceptsfileChanges.deletionsand returns aweb-flow-signed commit regardless of whether the change is an addition or a deletion. MovingresourceGithubRepositoryFileDelete(and ideallyCreate/Updatetoo, 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_filefor protected branches).Terraform Version
Also reproducible on
v6.11.1(latest at time of report).Affected Resource(s)
github_repository_fileTerraform Configuration Files
Steps to Reproduce
Contents: Read & writepermission and install it on a test repository whose default branch has arequire signed commitsruleset (or simply inspect commit verification afterwards viaGET /repos/{owner}/{repo}/commits/{sha}).verified: true, committerGitHub <[email protected]>).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