From e9d11ecb24c7b0141cb21e5aa5b1e9eb95fa7da9 Mon Sep 17 00:00:00 2001
From: Caio Pizzol <33255434+caiopizzol@users.noreply.github.com>
Date: Wed, 19 Aug 2026 07:41:16 -0300
Subject: [PATCH 1/2] fix: make destination deployment idempotent
---
cmd/deploy_destination.go | 54 ++++++--------------------
cmd/deploy_destination_test.go | 69 ++++++++++++++++++++++++++++++++++
2 files changed, 81 insertions(+), 42 deletions(-)
create mode 100644 cmd/deploy_destination_test.go
diff --git a/cmd/deploy_destination.go b/cmd/deploy_destination.go
index 93b3782..8abadd1 100644
--- a/cmd/deploy_destination.go
+++ b/cmd/deploy_destination.go
@@ -2,8 +2,6 @@ package cmd
import (
"context"
- "errors"
- "reflect"
"github.com/amp-labs/cli/flags"
"github.com/amp-labs/cli/logger"
@@ -83,59 +81,31 @@ func generatePatch(oldDest *request.Destination, newDest *request.Destination) *
patch.UpdateMask = append(patch.UpdateMask, "type")
}
- if oldDest.Metadata != nil { //nolint:nestif
- if newDest.Metadata == nil {
- patch.Destination["metadata"] = nil
- patch.UpdateMask = append(patch.UpdateMask, "metadata")
- } else if !reflect.DeepEqual(oldDest.Metadata, newDest.Metadata) {
- patch.Destination["metadata"] = newDest.Metadata
- patch.UpdateMask = append(patch.UpdateMask, "metadata")
- }
- } else {
- if newDest.Metadata != nil {
- patch.Destination["metadata"] = newDest.Metadata
- patch.UpdateMask = append(patch.UpdateMask, "metadata")
- }
+ if newDest.Metadata != nil && (oldDest.Metadata == nil || oldDest.Metadata.URL != newDest.Metadata.URL) {
+ patch.Destination["metadata"] = map[string]any{"url": newDest.Metadata.URL}
+ patch.UpdateMask = append(patch.UpdateMask, "metadata.url")
}
return patch
}
func getOldDest(ctx context.Context, client *request.APIClient, dest *request.Destination) *request.Destination {
- id := findDestId(ctx, client, dest)
- if id == "" {
- return nil
- }
-
- dst, err := client.GetDestination(ctx, id)
- if err != nil {
- if errors.Is(err, request.ErrNotFound) {
- return nil
- } else {
- logger.FatalErr("Unable to get destination", err)
- }
- }
-
- return dst
-}
-
-func findDestId(ctx context.Context, client *request.APIClient, dest *request.Destination) string {
- if dest.Id != "" {
- return dest.Id
- }
-
- dests, err := client.ListDestinations(ctx)
+ destinations, err := client.ListDestinations(ctx)
if err != nil {
logger.FatalErr("Unable to list destinations", err)
}
- for _, d := range dests {
- if d.Name == dest.Name {
- return d.Id
+ for _, existing := range destinations {
+ if dest.Id != "" && existing.Id == dest.Id {
+ return existing
+ }
+
+ if dest.Id == "" && existing.Name == dest.Name {
+ return existing
}
}
- return ""
+ return nil
}
func init() {
diff --git a/cmd/deploy_destination_test.go b/cmd/deploy_destination_test.go
new file mode 100644
index 0000000..ee9e884
--- /dev/null
+++ b/cmd/deploy_destination_test.go
@@ -0,0 +1,69 @@
+package cmd
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+
+ "github.com/amp-labs/cli/request"
+)
+
+func TestGeneratePatchUpdatesOnlyDestinationURL(t *testing.T) {
+ t.Parallel()
+
+ oldDestination := &request.Destination{
+ Metadata: &request.WebhookMetadata{
+ URL: "https://old.example.com/webhook",
+ Headers: map[string]string{"Authorization": "secret"},
+ SvixAppId: "svix-app-id",
+ SvixEndpointId: "svix-endpoint-id",
+ },
+ }
+ newDestination := &request.Destination{
+ Metadata: &request.WebhookMetadata{URL: "https://new.example.com/webhook"},
+ }
+
+ patch := generatePatch(oldDestination, newDestination)
+
+ wantDestination := map[string]any{
+ "metadata": map[string]any{"url": "https://new.example.com/webhook"},
+ }
+ if !reflect.DeepEqual(patch.Destination, wantDestination) {
+ t.Fatalf("patch destination = %#v, want %#v", patch.Destination, wantDestination)
+ }
+
+ wantUpdateMask := []string{"metadata.url"}
+ if !reflect.DeepEqual(patch.UpdateMask, wantUpdateMask) {
+ t.Fatalf("patch update mask = %#v, want %#v", patch.UpdateMask, wantUpdateMask)
+ }
+}
+
+func TestGetOldDestFindsExistingDestinationByName(t *testing.T) {
+ t.Parallel()
+
+ server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodGet || req.URL.Path != "/v1/projects/test-project/destinations" {
+ http.NotFound(writer, req)
+
+ return
+ }
+
+ writer.Header().Set("Content-Type", "application/json")
+ _, _ = writer.Write([]byte(`[{"id":"destination-id","name":"local-webhook","type":"webhook"}]`))
+ }))
+ t.Cleanup(server.Close)
+
+ apiKey := "test-key"
+ client := &request.APIClient{
+ Root: server.URL + "/v1",
+ ProjectId: "test-project",
+ APIKey: &apiKey,
+ Client: request.NewRequestClient(),
+ }
+
+ destination := getOldDest(t.Context(), client, &request.Destination{Name: "local-webhook"})
+ if destination == nil || destination.Id != "destination-id" {
+ t.Fatalf("getOldDest() = %#v, want existing destination", destination)
+ }
+}
From 89a81252acf2914995300456c61b2fc922fc7dd4 Mon Sep 17 00:00:00 2001
From: Caio Pizzol <33255434+caiopizzol@users.noreply.github.com>
Date: Wed, 19 Aug 2026 11:35:03 -0300
Subject: [PATCH 2/2] feat(destinations): expose deployment command
---
cmd/deploy_destination.go | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/cmd/deploy_destination.go b/cmd/deploy_destination.go
index 8abadd1..ca1c63d 100644
--- a/cmd/deploy_destination.go
+++ b/cmd/deploy_destination.go
@@ -12,10 +12,9 @@ import (
)
var deployDestinationCmd = &cobra.Command{ //nolint:gochecknoglobals
- Use: "deploy:destination -i [-o