Skip to content

Commit 7101da5

Browse files
JDetmarclaude
andcommitted
fix(registeredscript): all changes now trigger replacement instead of update
Webflow API does not support PATCH for registered scripts, which caused 404 errors when Pulumi tried to update them. This change: - Changes all property diffs to use UpdateReplace instead of Update - Makes version field required (removes optional workaround) - Updates Update method to return error as safety net - Adds deprecation comment to PatchRegisteredScript function - Adds tests for replacement behavior and Update error Fixes issues #1 and #4 from ISSUES-TO-FIX.md. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 069a32d commit 7101da5

File tree

10 files changed

+207
-132
lines changed

10 files changed

+207
-132
lines changed

provider/cmd/pulumi-resource-webflow/schema.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,8 @@
942942
"siteId",
943943
"displayName",
944944
"hostedLocation",
945-
"integrityHash"
945+
"integrityHash",
946+
"version"
946947
],
947948
"inputProperties": {
948949
"canCopy": {
@@ -974,7 +975,8 @@
974975
"siteId",
975976
"displayName",
976977
"hostedLocation",
977-
"integrityHash"
978+
"integrityHash",
979+
"version"
978980
]
979981
},
980982
"webflow:index:RobotsTxt": {

provider/registeredscript.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ var patchRegisteredScriptBaseURL = ""
364364
// PatchRegisteredScript updates an existing registered script for a Webflow site.
365365
// It calls PATCH /v2/sites/{site_id}/registered_scripts/{script_id} endpoint.
366366
// Returns the updated script or an error if the request fails.
367+
//
368+
// Deprecated: Webflow API does not actually support PATCH for registered scripts.
369+
// This endpoint returns 404 Not Found. All RegisteredScript changes now trigger
370+
// replacement (delete + recreate) via the Diff method. This function is kept for
371+
// backwards compatibility but should not be used.
367372
func PatchRegisteredScript(
368373
ctx context.Context, client *http.Client,
369374
siteID, scriptID, displayName, hostedLocation, integrityHash, version string, canCopy bool,

provider/registeredscript_resource.go

Lines changed: 20 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ type RegisteredScriptResourceArgs struct {
3939
// Version is the Semantic Version (SemVer) string for the script.
4040
// Format: "major.minor.patch" (e.g., "1.0.0", "2.3.1")
4141
// See https://semver.org/ for more information.
42-
// Note: Marked as optional because Webflow's list API doesn't always return this field.
43-
Version string `pulumi:"version,optional"`
42+
Version string `pulumi:"version"`
4443
// CanCopy indicates whether the script can be copied on site duplication.
4544
// Default: false
4645
CanCopy bool `pulumi:"canCopy,optional"`
@@ -122,57 +121,43 @@ func (state *RegisteredScriptResourceState) Annotate(a infer.Annotator) {
122121
}
123122

124123
// Diff determines what changes need to be made to the registered script resource.
125-
// SiteID and DisplayName changes trigger replacement.
126-
// Other changes trigger in-place update.
124+
// NOTE: Webflow API does not support updating registered scripts (no PATCH endpoint).
125+
// All changes require replacement (delete + recreate), similar to Webhook resources.
127126
func (r *RegisteredScriptResource) Diff(
128127
ctx context.Context, req infer.DiffRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState],
129128
) (infer.DiffResponse, error) {
130129
diff := infer.DiffResponse{}
131130
detailedDiff := map[string]p.PropertyDiff{}
132131

133-
// Check for siteId change (requires replacement - primary key)
132+
// All field changes trigger replacement since Webflow API doesn't support PATCH
134133
if req.State.SiteID != req.Inputs.SiteID {
135134
detailedDiff["siteId"] = p.PropertyDiff{Kind: p.UpdateReplace}
136135
}
137136

138-
// Check for displayName change (requires replacement - secondary key)
139137
if req.State.DisplayName != req.Inputs.DisplayName {
140138
detailedDiff["displayName"] = p.PropertyDiff{Kind: p.UpdateReplace}
141139
}
142140

143-
// Check for hostedLocation change (supports update)
144141
if req.State.HostedLocation != req.Inputs.HostedLocation {
145-
detailedDiff["hostedLocation"] = p.PropertyDiff{Kind: p.Update}
142+
detailedDiff["hostedLocation"] = p.PropertyDiff{Kind: p.UpdateReplace}
146143
}
147144

148-
// Check for integrityHash change (supports update)
149145
if req.State.IntegrityHash != req.Inputs.IntegrityHash {
150-
detailedDiff["integrityHash"] = p.PropertyDiff{Kind: p.Update}
146+
detailedDiff["integrityHash"] = p.PropertyDiff{Kind: p.UpdateReplace}
151147
}
152148

153-
// Check for version change (supports update)
154-
// Only report change if user explicitly specified a version that differs from state.
155-
// This handles the case where req.Inputs.Version may be empty due to deserialization issues
156-
// in pulumi-go-provider when the field is marked optional.
157-
if req.Inputs.Version != "" && req.State.Version != req.Inputs.Version {
158-
detailedDiff["version"] = p.PropertyDiff{Kind: p.Update}
149+
if req.State.Version != req.Inputs.Version {
150+
detailedDiff["version"] = p.PropertyDiff{Kind: p.UpdateReplace}
159151
}
160152

161-
// Check for canCopy change (supports update)
162153
if req.State.CanCopy != req.Inputs.CanCopy {
163-
detailedDiff["canCopy"] = p.PropertyDiff{Kind: p.Update}
154+
detailedDiff["canCopy"] = p.PropertyDiff{Kind: p.UpdateReplace}
164155
}
165156

166-
// If any changes were detected, populate the diff response
157+
// If any changes were detected, all require replacement
167158
if len(detailedDiff) > 0 {
168159
diff.HasChanges = true
169-
// Only set DeleteBeforeReplace if any replacement changes are needed
170-
for _, change := range detailedDiff {
171-
if change.Kind == p.UpdateReplace {
172-
diff.DeleteBeforeReplace = true
173-
break
174-
}
175-
}
160+
diff.DeleteBeforeReplace = true
176161
diff.DetailedDiff = detailedDiff
177162
}
178163

@@ -357,76 +342,17 @@ func (r *RegisteredScriptResource) Read(
357342
}, nil
358343
}
359344

360-
// Update modifies an existing registered script.
345+
// Update is not supported by Webflow API for registered scripts.
346+
// All changes trigger replacement via Diff, so this method should never be called.
347+
// This is a safety net that returns an error if somehow invoked.
361348
func (r *RegisteredScriptResource) Update(
362-
ctx context.Context, req infer.UpdateRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState],
349+
_ context.Context, _ infer.UpdateRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState],
363350
) (infer.UpdateResponse[RegisteredScriptResourceState], error) {
364-
// Validate inputs BEFORE making API calls
365-
if err := ValidateSiteID(req.Inputs.SiteID); err != nil {
366-
return infer.UpdateResponse[RegisteredScriptResourceState]{},
367-
fmt.Errorf("validation failed for RegisteredScript resource: %w", err)
368-
}
369-
if err := ValidateScriptDisplayName(req.Inputs.DisplayName); err != nil {
370-
return infer.UpdateResponse[RegisteredScriptResourceState]{},
371-
fmt.Errorf("validation failed for RegisteredScript resource: %w", err)
372-
}
373-
if err := ValidateHostedLocation(req.Inputs.HostedLocation); err != nil {
374-
return infer.UpdateResponse[RegisteredScriptResourceState]{},
375-
fmt.Errorf("validation failed for RegisteredScript resource: %w", err)
376-
}
377-
if err := ValidateIntegrityHash(req.Inputs.IntegrityHash); err != nil {
378-
return infer.UpdateResponse[RegisteredScriptResourceState]{},
379-
fmt.Errorf("validation failed for RegisteredScript resource: %w", err)
380-
}
381-
if err := ValidateVersion(req.Inputs.Version); err != nil {
382-
return infer.UpdateResponse[RegisteredScriptResourceState]{},
383-
fmt.Errorf("validation failed for RegisteredScript resource: %w", err)
384-
}
385-
386-
state := RegisteredScriptResourceState{
387-
RegisteredScriptResourceArgs: req.Inputs,
388-
ScriptID: req.State.ScriptID, // Preserve the script ID from current state
389-
CreatedOn: req.State.CreatedOn, // Preserve the creation timestamp from current state
390-
LastUpdated: "", // Will be updated from API response
391-
}
392-
393-
// During preview, return expected state without making API calls
394-
if req.DryRun {
395-
state.LastUpdated = time.Now().Format(time.RFC3339)
396-
return infer.UpdateResponse[RegisteredScriptResourceState]{
397-
Output: state,
398-
}, nil
399-
}
400-
401-
// Extract the Webflow script ID from the Pulumi resource ID
402-
_, scriptID, err := ExtractIDsFromRegisteredScriptResourceID(req.ID)
403-
if err != nil {
404-
return infer.UpdateResponse[RegisteredScriptResourceState]{}, fmt.Errorf("invalid resource ID: %w", err)
405-
}
406-
407-
// Get HTTP client
408-
client, err := GetHTTPClient(ctx, providerVersion)
409-
if err != nil {
410-
return infer.UpdateResponse[RegisteredScriptResourceState]{}, fmt.Errorf("failed to create HTTP client: %w", err)
411-
}
412-
413-
// Call Webflow API
414-
response, err := PatchRegisteredScript(
415-
ctx, client, req.Inputs.SiteID, scriptID,
416-
req.Inputs.DisplayName, req.Inputs.HostedLocation, req.Inputs.IntegrityHash,
417-
req.Inputs.Version, req.Inputs.CanCopy,
418-
)
419-
if err != nil {
420-
return infer.UpdateResponse[RegisteredScriptResourceState]{},
421-
fmt.Errorf("failed to update registered script: %w", err)
422-
}
423-
424-
// Update state with values from API response
425-
state.LastUpdated = response.LastUpdated
426-
427-
return infer.UpdateResponse[RegisteredScriptResourceState]{
428-
Output: state,
429-
}, nil
351+
return infer.UpdateResponse[RegisteredScriptResourceState]{},
352+
errors.New("registered scripts cannot be updated in-place: " +
353+
"Webflow API does not support PATCH for registered scripts. " +
354+
"All changes require replacement (delete + recreate). " +
355+
"If you see this error, please report it as a provider bug")
430356
}
431357

432358
// Delete removes a registered script from the Webflow site.

provider/registeredscript_resource_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"testing"
1717
"time"
1818

19+
p "github.com/pulumi/pulumi-go-provider"
1920
"github.com/pulumi/pulumi-go-provider/infer"
2021
)
2122

@@ -660,3 +661,134 @@ func TestRegisteredScriptDiff_VersionFromFallback_NoChange(t *testing.T) {
660661
t.Errorf("DetailedDiff: %+v", diffResp.DetailedDiff)
661662
}
662663
}
664+
665+
// TestRegisteredScriptDiff_ChangesRequireReplacement tests that all property changes
666+
// trigger UpdateReplace since Webflow API doesn't support PATCH for registered scripts.
667+
func TestRegisteredScriptDiff_ChangesRequireReplacement(t *testing.T) {
668+
resource := &RegisteredScriptResource{}
669+
670+
baseInputs := RegisteredScriptResourceArgs{
671+
SiteID: "site123",
672+
DisplayName: "TestScript",
673+
HostedLocation: "https://cdn.example.com/script.js",
674+
IntegrityHash: "sha384-abc123",
675+
Version: "1.0.0",
676+
CanCopy: false,
677+
}
678+
679+
baseState := RegisteredScriptResourceState{
680+
RegisteredScriptResourceArgs: baseInputs,
681+
}
682+
683+
tests := []struct {
684+
name string
685+
modifyFn func(args *RegisteredScriptResourceArgs)
686+
fieldName string
687+
}{
688+
{
689+
name: "hostedLocation change",
690+
modifyFn: func(args *RegisteredScriptResourceArgs) {
691+
args.HostedLocation = "https://cdn.example.com/script-v2.js"
692+
},
693+
fieldName: "hostedLocation",
694+
},
695+
{
696+
name: "integrityHash change",
697+
modifyFn: func(args *RegisteredScriptResourceArgs) {
698+
args.IntegrityHash = "sha384-def456"
699+
},
700+
fieldName: "integrityHash",
701+
},
702+
{
703+
name: "version change",
704+
modifyFn: func(args *RegisteredScriptResourceArgs) {
705+
args.Version = "2.0.0"
706+
},
707+
fieldName: "version",
708+
},
709+
{
710+
name: "canCopy change",
711+
modifyFn: func(args *RegisteredScriptResourceArgs) {
712+
args.CanCopy = true
713+
},
714+
fieldName: "canCopy",
715+
},
716+
}
717+
718+
for _, tt := range tests {
719+
t.Run(tt.name, func(t *testing.T) {
720+
// Create modified inputs
721+
modifiedInputs := baseInputs
722+
tt.modifyFn(&modifiedInputs)
723+
724+
diffReq := infer.DiffRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState]{
725+
Inputs: modifiedInputs,
726+
State: baseState,
727+
}
728+
729+
diffResp, err := resource.Diff(context.Background(), diffReq)
730+
if err != nil {
731+
t.Fatalf("Diff() error = %v", err)
732+
}
733+
734+
// Changes should be detected
735+
if !diffResp.HasChanges {
736+
t.Errorf("Diff() should detect changes for %s", tt.fieldName)
737+
}
738+
739+
// DeleteBeforeReplace should be true (all changes require replacement)
740+
if !diffResp.DeleteBeforeReplace {
741+
t.Errorf("Diff() DeleteBeforeReplace should be true for %s", tt.fieldName)
742+
}
743+
744+
// Field should be marked as UpdateReplace, not Update
745+
if diff, ok := diffResp.DetailedDiff[tt.fieldName]; ok {
746+
if diff.Kind != p.UpdateReplace {
747+
t.Errorf("Diff() %s should be UpdateReplace, got %v", tt.fieldName, diff.Kind)
748+
}
749+
} else {
750+
t.Errorf("Diff() DetailedDiff should contain %s", tt.fieldName)
751+
}
752+
})
753+
}
754+
}
755+
756+
// TestRegisteredScriptUpdate_ReturnsError tests that Update method returns an error
757+
// since Webflow API doesn't support PATCH for registered scripts.
758+
func TestRegisteredScriptUpdate_ReturnsError(t *testing.T) {
759+
resource := &RegisteredScriptResource{}
760+
761+
updateReq := infer.UpdateRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState]{
762+
ID: "site123/registered_scripts/script456",
763+
Inputs: RegisteredScriptResourceArgs{
764+
SiteID: "site123",
765+
DisplayName: "TestScript",
766+
HostedLocation: "https://cdn.example.com/script.js",
767+
IntegrityHash: "sha384-abc123",
768+
Version: "1.0.0",
769+
},
770+
State: RegisteredScriptResourceState{
771+
RegisteredScriptResourceArgs: RegisteredScriptResourceArgs{
772+
SiteID: "site123",
773+
DisplayName: "TestScript",
774+
HostedLocation: "https://cdn.example.com/old-script.js",
775+
IntegrityHash: "sha384-old",
776+
Version: "0.9.0",
777+
},
778+
},
779+
}
780+
781+
_, err := resource.Update(context.Background(), updateReq)
782+
783+
if err == nil {
784+
t.Fatal("Update() should return an error")
785+
}
786+
787+
if !containsStr(err.Error(), "cannot be updated in-place") {
788+
t.Errorf("Update() error should mention updates not supported, got: %v", err)
789+
}
790+
791+
if !containsStr(err.Error(), "PATCH") {
792+
t.Errorf("Update() error should mention PATCH not supported, got: %v", err)
793+
}
794+
}

sdk/dotnet/Webflow/RegisteredScript.cs

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)