Skip to content

Commit ae8addf

Browse files
JDetmarclaude
andauthored
fix(registeredscript): all changes now trigger replacement instead of update (#51)
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 ae8addf

File tree

10 files changed

+221
-132
lines changed

10 files changed

+221
-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: 146 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,148 @@ 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: "siteId change",
690+
modifyFn: func(args *RegisteredScriptResourceArgs) {
691+
args.SiteID = "site456"
692+
},
693+
fieldName: "siteId",
694+
},
695+
{
696+
name: "displayName change",
697+
modifyFn: func(args *RegisteredScriptResourceArgs) {
698+
args.DisplayName = "NewScriptName"
699+
},
700+
fieldName: "displayName",
701+
},
702+
{
703+
name: "hostedLocation change",
704+
modifyFn: func(args *RegisteredScriptResourceArgs) {
705+
args.HostedLocation = "https://cdn.example.com/script-v2.js"
706+
},
707+
fieldName: "hostedLocation",
708+
},
709+
{
710+
name: "integrityHash change",
711+
modifyFn: func(args *RegisteredScriptResourceArgs) {
712+
args.IntegrityHash = "sha384-def456"
713+
},
714+
fieldName: "integrityHash",
715+
},
716+
{
717+
name: "version change",
718+
modifyFn: func(args *RegisteredScriptResourceArgs) {
719+
args.Version = "2.0.0"
720+
},
721+
fieldName: "version",
722+
},
723+
{
724+
name: "canCopy change",
725+
modifyFn: func(args *RegisteredScriptResourceArgs) {
726+
args.CanCopy = true
727+
},
728+
fieldName: "canCopy",
729+
},
730+
}
731+
732+
for _, tt := range tests {
733+
t.Run(tt.name, func(t *testing.T) {
734+
// Create modified inputs
735+
modifiedInputs := baseInputs
736+
tt.modifyFn(&modifiedInputs)
737+
738+
diffReq := infer.DiffRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState]{
739+
Inputs: modifiedInputs,
740+
State: baseState,
741+
}
742+
743+
diffResp, err := resource.Diff(context.Background(), diffReq)
744+
if err != nil {
745+
t.Fatalf("Diff() error = %v", err)
746+
}
747+
748+
// Changes should be detected
749+
if !diffResp.HasChanges {
750+
t.Errorf("Diff() should detect changes for %s", tt.fieldName)
751+
}
752+
753+
// DeleteBeforeReplace should be true (all changes require replacement)
754+
if !diffResp.DeleteBeforeReplace {
755+
t.Errorf("Diff() DeleteBeforeReplace should be true for %s", tt.fieldName)
756+
}
757+
758+
// Field should be marked as UpdateReplace, not Update
759+
if diff, ok := diffResp.DetailedDiff[tt.fieldName]; ok {
760+
if diff.Kind != p.UpdateReplace {
761+
t.Errorf("Diff() %s should be UpdateReplace, got %v", tt.fieldName, diff.Kind)
762+
}
763+
} else {
764+
t.Errorf("Diff() DetailedDiff should contain %s", tt.fieldName)
765+
}
766+
})
767+
}
768+
}
769+
770+
// TestRegisteredScriptUpdate_ReturnsError tests that Update method returns an error
771+
// since Webflow API doesn't support PATCH for registered scripts.
772+
func TestRegisteredScriptUpdate_ReturnsError(t *testing.T) {
773+
resource := &RegisteredScriptResource{}
774+
775+
updateReq := infer.UpdateRequest[RegisteredScriptResourceArgs, RegisteredScriptResourceState]{
776+
ID: "site123/registered_scripts/script456",
777+
Inputs: RegisteredScriptResourceArgs{
778+
SiteID: "site123",
779+
DisplayName: "TestScript",
780+
HostedLocation: "https://cdn.example.com/script.js",
781+
IntegrityHash: "sha384-abc123",
782+
Version: "1.0.0",
783+
},
784+
State: RegisteredScriptResourceState{
785+
RegisteredScriptResourceArgs: RegisteredScriptResourceArgs{
786+
SiteID: "site123",
787+
DisplayName: "TestScript",
788+
HostedLocation: "https://cdn.example.com/old-script.js",
789+
IntegrityHash: "sha384-old",
790+
Version: "0.9.0",
791+
},
792+
},
793+
}
794+
795+
_, err := resource.Update(context.Background(), updateReq)
796+
797+
if err == nil {
798+
t.Fatal("Update() should return an error")
799+
}
800+
801+
if !containsStr(err.Error(), "cannot be updated in-place") {
802+
t.Errorf("Update() error should mention updates not supported, got: %v", err)
803+
}
804+
805+
if !containsStr(err.Error(), "PATCH") {
806+
t.Errorf("Update() error should mention PATCH not supported, got: %v", err)
807+
}
808+
}

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)