integrations / integrations/terraform-provider-github
[FEAT]: github_repository_file delete commits are never signed, breaking rulesets that require signed commits
- Dominant language
- Go
- Stars
- 1.2k
- Forks
- 1k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 8
Description
## 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 `, `verified: true`, reason: `valid`).
- `terraform destroy` (or changing `file` / `branch` and triggering a replace) → delete commit is **unsigned** (committer: `[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](https://github.com/integrations/terraform-provider-github/pull/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:
```json
{
"committer_name": "GitHub",
"committer_email": "noreply@github.com",
"reason": "valid",
"verified": true,
"signature_present": true
}
```
`DELETE` — unsigned:
```json
{
"committer_name": "[bot]",
"committer_email": "+[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
```text
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
```hcl
terraform {
required_providers {
github = {
source = "integrations/github"
version = "6.10.2"
}
}
}
provider "github" {
owner = ""
app_auth {
id = ""
installation_id = ""
pem_file = file(".pem")
}
}
resource "github_repository_file" "test" {
repository = ""
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 `).
3. `terraform destroy` — the delete commit is unsigned (`verified: false`, committer `[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
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Assessment
This issue has not been assessed yet.