`gitlab_project_hook`: any attribute update forces unnecessary resource replacement
## Summary
Updating any attribute on `gitlab_project_hook` (e.g. setting `name` for the first time on an
imported hook) forces the resource to be destroyed and recreated rather than updated in place.
If `lifecycle { ignore_changes = [project_id] }` is added as a workaround to suppress the
replacement, the apply then fails with:
```
Error reading GitLab project hook: Unexpected ID format (""). Expected <part1>:<part2>
```
## Environment
- Provider version: `18.11.0` (reproduced; likely affects all recent versions)
- Terraform / OpenTofu: `OpenTofu ~> 1.5`
## Steps to reproduce
1. Import an existing project hook into state.
2. Set (or change) any optional attribute that differs from the value currently in state — for
example, set `name` on a hook that was imported without one.
3. Run `terraform plan`.
**Expected:** the change is applied as an in-place update.
**Actual:** the plan proposes a destroy + recreate:
```
# gitlab_project_hook.hooks["my-project/my-hook"] must be replaced
-/+ resource "gitlab_project_hook" "hooks" {
+ name = "my-hook"
~ project_id = 12345678 # forces replacement -> (known after apply) # forces replacement
~ hook_id = 99887766 -> (known after apply)
~ id = "12345678:99887766" -> (known after apply)
# (other attributes hidden)
}
```
## Root cause
`id`, `project_id`, and `hook_id` are purely computed identity attributes — set once on Create
and never driven by user configuration. None of them carry a
[`UseStateForUnknown`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier#UseStateForUnknown)
plan modifier.
When any other attribute is updated, the framework marks all three as `(known after apply)`.
Because `project_id` also carries `RequiresReplace`, the comparison `12345678 → (known after
apply)` is treated as a potential change and triggers forced recreation — even though the project
has not changed.
Current schema definitions:
```go
"id": schema.StringAttribute{
Computed: true,
// missing: stringplanmodifier.UseStateForUnknown()
},
"hook_id": schema.Int64Attribute{
Computed: true,
// missing: int64planmodifier.UseStateForUnknown()
},
"project_id": schema.Int64Attribute{
Computed: true,
PlanModifiers: []planmodifier.Int64{
// missing: int64planmodifier.UseStateForUnknown()
int64planmodifier.RequiresReplace(),
},
},
```
## Secondary bug: `Update()` reads the resource ID from `req.Plan`
The `Update` function reads the resource model from `req.Plan`:
```go
func (r *gitlabProjectHookResource) Update(...) {
var data *gitlabProjectHookResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
...
project, hookId, err := data.ResourceGitlabProjectHookParseId(data.ID.ValueString())
```
Because of the root cause above, `data.ID` is `(known after apply)` in the plan, so
`data.ID.ValueString()` returns `""` and `ResourceGitlabProjectHookParseId` fails with the
"Unexpected ID format" error.
The `Read` function correctly uses `req.State` for the same operation and serves as the
reference for the fix.
## Bonus: copy-paste error in `Update` error message
While in the area — the error returned on a failed `EditProjectHook` call reads
`"Error creating GitLab project hook"` instead of `"Error updating GitLab project hook"`.
## Proposed fix
MR incoming with fix:
1. Add `UseStateForUnknown()` to `id`, `hook_id`, and `project_id` — this is the root fix and
eliminates the unnecessary recreation.
2. Fix `Update()` to read the resource ID from `req.State` instead of `req.Plan` — defence in
depth; matches how `Read()` already works.
3. Fix the copy-paste error message in `Update()`.
issue