Skip to content

Commit 9d62dbb

Browse files
committed
fix(repository-webhook): preserve write-only config.secret
The webhook `secret` is supplied inside the `config` map but Forgejo, like Gitea, treats it as write-only: it is accepted on create/update but never returned by the API. The provider previously rebuilt `config` wholesale from the API response in `from()`, dropping the `secret` key. The planned `config` therefore differed from the applied `config`, failing Terraform's post-apply consistency check with "Provider produced inconsistent result after apply ... inconsistent values for sensitive attribute". Mirror the existing write-only handling (`authorization_header`, `branch_filter`) by carrying known write-only config keys over from the prior plan/state value when mapping the API response into state.
1 parent 5e7fdf4 commit 9d62dbb

5 files changed

Lines changed: 48 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
BUG FIXES:
4+
5+
- `forgejo_repository_webhook`: Preserve write-only `config.secret` from configuration so managing the webhook secret no longer causes "Provider produced inconsistent result after apply"
6+
17
## 1.5.2 (August 2, 2026)
28

39
ENHANCEMENTS:

docs/resources/repository_webhook.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ resource "forgejo_repository_webhook" "example" {
4040
config = {
4141
"content_type" = "json"
4242
"url" = "http://example.com/invoke"
43+
# The "secret" key is write-only: Forgejo accepts it on create/update but
44+
# never returns it. The provider preserves it from configuration so that
45+
# managing it here does not cause "inconsistent result after apply" errors.
46+
"secret" = "supersecret"
4347
}
4448
}
4549

examples/resources/forgejo_repository_webhook/resource.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ resource "forgejo_repository_webhook" "example" {
2525
config = {
2626
"content_type" = "json"
2727
"url" = "http://example.com/invoke"
28+
# The "secret" key is write-only: Forgejo accepts it on create/update but
29+
# never returns it. The provider preserves it from configuration so that
30+
# managing it here does not cause "inconsistent result after apply" errors.
31+
"secret" = "supersecret"
2832
}
2933
}
3034

internal/provider/repository_webhook_resource.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ type repositoryWebhookResourceModel struct {
5353
UpdatedAt types.String `tfsdk:"updated_at"`
5454
}
5555

56+
// writeOnlyConfigKeys lists keys within the webhook "config" map that the
57+
// Forgejo API accepts on create/update but never returns in responses (a GET
58+
// on the hook only echoes back e.g. "url" and "content_type"). They must be
59+
// preserved from the prior plan/state value, otherwise the applied "config"
60+
// drops the key and Terraform fails its post-apply consistency check with
61+
// "provider produced an unexpected new value: .config".
62+
var writeOnlyConfigKeys = []string{"secret"}
63+
5664
// from is a helper function to load an API struct into Terraform data model.
5765
func (m *repositoryWebhookResourceModel) from(h *forgejo.Hook, ctx context.Context) (diags diag.Diagnostics) {
5866
if h == nil {
@@ -61,9 +69,29 @@ func (m *repositoryWebhookResourceModel) from(h *forgejo.Hook, ctx context.Conte
6169

6270
var d diag.Diagnostics
6371

72+
// The API response never includes write-only config keys (e.g. "secret"),
73+
// so carry them over from the prior model value (the plan on create/update,
74+
// the prior state on read) to keep the applied config consistent.
75+
config := make(map[string]string, len(h.Config))
76+
for k, v := range h.Config {
77+
config[k] = v
78+
}
79+
if !m.Config.IsNull() && !m.Config.IsUnknown() {
80+
var priorConfig map[string]string
81+
d = m.Config.ElementsAs(ctx, &priorConfig, false)
82+
diags.Append(d...)
83+
for _, key := range writeOnlyConfigKeys {
84+
if v, ok := priorConfig[key]; ok {
85+
if _, present := config[key]; !present {
86+
config[key] = v
87+
}
88+
}
89+
}
90+
}
91+
6492
m.WebhookID = types.Int64Value(h.ID)
6593
m.Active = types.BoolValue(h.Active)
66-
m.Config, d = types.MapValueFrom(ctx, types.StringType, h.Config)
94+
m.Config, d = types.MapValueFrom(ctx, types.StringType, config)
6795
diags.Append(d...)
6896
m.CreatedAt = types.StringValue(h.Created.Format(time.RFC3339))
6997
m.Events, d = types.SetValueFrom(ctx, types.StringType, h.Events)

internal/provider/repository_webhook_resource_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ resource "forgejo_repository_webhook" "test" {
118118
config = {
119119
"content_type" = "json"
120120
"url" = "http://example.com/abc12345"
121+
"secret" = "supersecret"
121122
}
122123
active = true
123124
authorization_header = "Bearer token123456"
@@ -140,6 +141,10 @@ resource "forgejo_repository_webhook" "test" {
140141
statecheck.ExpectKnownValue("forgejo_repository_webhook.test", tfjsonpath.New("events").AtSliceIndex(0), knownvalue.StringExact("push")),
141142
statecheck.ExpectKnownValue("forgejo_repository_webhook.test", tfjsonpath.New("config").AtMapKey("content_type"), knownvalue.StringExact("json")),
142143
statecheck.ExpectKnownValue("forgejo_repository_webhook.test", tfjsonpath.New("config").AtMapKey("url"), knownvalue.StringExact("http://example.com/abc12345")),
144+
// The webhook secret is write-only: the API never returns it,
145+
// so the provider must preserve it from the plan to keep the
146+
// applied config consistent. See from() in the resource.
147+
statecheck.ExpectKnownValue("forgejo_repository_webhook.test", tfjsonpath.New("config").AtMapKey("secret"), knownvalue.StringExact("supersecret")),
143148
},
144149
},
145150
// Recreate and Read testing

0 commit comments

Comments
 (0)