Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eea3c74
Add webhook authorization token option
heyglassy Sep 1, 2026
1055c18
Add webhook authorization token option
heyglassy Sep 1, 2026
13d9d57
Add webhook authorization token option
heyglassy Sep 1, 2026
eaaa6b9
Add webhook authorization token option
heyglassy Sep 1, 2026
b9a6def
Add webhook authorization token option
heyglassy Sep 1, 2026
118feda
Add webhook authorization token option
heyglassy Sep 1, 2026
6c81ea9
Expose webhook authorization token configuration
heyglassy Sep 1, 2026
8a09887
Expose webhook authorization token configuration
heyglassy Sep 1, 2026
2c09c97
Expose webhook authorization token configuration
heyglassy Sep 1, 2026
df9c839
Rename webhook authorization token fields
heyglassy Sep 1, 2026
b6c758b
Rename webhook authorization token fields
heyglassy Sep 1, 2026
0474dde
Rename webhook authorization token fields
heyglassy Sep 1, 2026
0c333ed
Rename webhook authorization token fields
heyglassy Sep 1, 2026
f77d28a
Rename webhook authorization token fields
heyglassy Sep 1, 2026
f9952ba
Rename webhook authorization token fields
heyglassy Sep 1, 2026
7d1c936
Rename webhook authorization token fields
heyglassy Sep 1, 2026
40c5460
Format renamed webhook token fields
heyglassy Sep 1, 2026
73a4cf9
Format renamed webhook token fields
heyglassy Sep 1, 2026
688b5c6
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
cd7e6c8
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
681a448
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
8ba8a9f
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
38bc6a8
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
bf83fe0
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
496950e
Clean up webhook token rename formatting
heyglassy Sep 1, 2026
dc99d44
Add explicit webhook token removal
heyglassy Sep 1, 2026
f22154b
Clarify webhook token status heading
heyglassy Sep 1, 2026
953b736
Clarify webhook token flag help
heyglassy Sep 1, 2026
52486b5
Tighten webhook token copy
heyglassy Sep 1, 2026
7cf74ce
Fix webhook token help punctuation
heyglassy Sep 1, 2026
1a426e5
Merge remote-tracking branch 'origin/main' into pr-1393
heyglassy Sep 1, 2026
81b2bbf
Align webhook flags with the authorization header API.
mscoutermarsh Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions internal/cmd/webhook/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

func CreateCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
url string
events []string
enabled bool
url string
authorizationHeader string
events []string
enabled bool
}

cmd := &cobra.Command{
Expand All @@ -30,10 +31,11 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command {
}

req := &planetscale.CreateWebhookRequest{
Organization: ch.Config.Organization,
Database: database,
URL: flags.url,
Events: flags.events,
Organization: ch.Config.Organization,
Database: database,
URL: flags.url,
AuthorizationHeader: flags.authorizationHeader,
Events: flags.events,
}

if cmd.Flags().Changed("enabled") {
Expand Down Expand Up @@ -61,6 +63,7 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command {
}

cmd.Flags().StringVar(&flags.url, "url", "", "The URL to send webhook events to (required)")
cmd.Flags().StringVar(&flags.authorizationHeader, "authorization-header", "", "The complete Authorization header value, for example Bearer token")
Comment thread
mscoutermarsh marked this conversation as resolved.
cmd.Flags().StringSliceVar(&flags.events, "events", nil, "Comma-separated list of events to subscribe to")
cmd.Flags().BoolVar(&flags.enabled, "enabled", true, "Whether the webhook is enabled")

Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/webhook/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestWebhook_CreateCmd(t *testing.T) {
db := "mydb"
url := "https://example.com/webhook"
events := []string{"branch.created", "branch.deleted"}
authorizationHeader := "Bearer automation-token"
createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)

webhook := &ps.Webhook{
Expand All @@ -42,6 +43,7 @@ func TestWebhook_CreateCmd(t *testing.T) {
c.Assert(req.Organization, qt.Equals, org)
c.Assert(req.Database, qt.Equals, db)
c.Assert(req.URL, qt.Equals, url)
c.Assert(req.AuthorizationHeader, qt.Equals, authorizationHeader)
c.Assert(req.Events, qt.DeepEquals, events)
return webhook, nil
},
Expand All @@ -60,7 +62,7 @@ func TestWebhook_CreateCmd(t *testing.T) {
}

cmd := CreateCmd(ch)
cmd.SetArgs([]string{db, "--url", url, "--events", "branch.created,branch.deleted"})
cmd.SetArgs([]string{db, "--url", url, "--authorization-header", authorizationHeader, "--events", "branch.created,branch.deleted"})
err := cmd.Execute()

c.Assert(err, qt.IsNil)
Expand Down
27 changes: 23 additions & 4 deletions internal/cmd/webhook/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (

func UpdateCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
url string
events []string
enabled bool
url string
authorizationHeader string
clearAuthorizationHeader bool
events []string
enabled bool
}

cmd := &cobra.Command{
Expand Down Expand Up @@ -43,6 +45,20 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command {
changed = true
}

if cmd.Flags().Changed("authorization-header") {
if flags.authorizationHeader == "" {
return fmt.Errorf("--authorization-header cannot be empty; use --clear-authorization-header to remove the configured header")
}
req.AuthorizationHeader = &flags.authorizationHeader
changed = true
}

if flags.clearAuthorizationHeader {
empty := ""
req.AuthorizationHeader = &empty
changed = true
}

if cmd.Flags().Changed("events") {
req.Events = flags.events
changed = true
Expand All @@ -54,7 +70,7 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command {
}

if !changed {
return fmt.Errorf("at least one of --url, --events, or --enabled must be provided")
return fmt.Errorf("at least one of --url, --authorization-header, --clear-authorization-header, --events, or --enabled must be provided")
}

end := ch.Printer.PrintProgress(fmt.Sprintf("Updating webhook %s for %s", printer.BoldBlue(webhookID), printer.BoldBlue(database)))
Expand All @@ -78,8 +94,11 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command {
}

cmd.Flags().StringVar(&flags.url, "url", "", "The URL to send webhook events to")
cmd.Flags().StringVar(&flags.authorizationHeader, "authorization-header", "", "The complete Authorization header value, for example Bearer token")
cmd.Flags().BoolVar(&flags.clearAuthorizationHeader, "clear-authorization-header", false, "Remove the configured Authorization header")
cmd.Flags().StringSliceVar(&flags.events, "events", nil, "Comma-separated list of events to subscribe to")
cmd.Flags().BoolVar(&flags.enabled, "enabled", true, "Whether the webhook is enabled")
cmd.MarkFlagsMutuallyExclusive("authorization-header", "clear-authorization-header")

return cmd
}
122 changes: 122 additions & 0 deletions internal/cmd/webhook/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,128 @@ func TestWebhook_UpdateCmd_EnabledFlag(t *testing.T) {
c.Assert(svc.UpdateFnInvoked, qt.IsTrue)
}

func TestWebhook_UpdateCmd_AuthorizationHeaderFlag(t *testing.T) {
c := qt.New(t)

var buf bytes.Buffer
format := printer.JSON
p := printer.NewPrinter(&format)
p.SetResourceOutput(&buf)

org := "planetscale"
db := "mydb"
webhookID := "webhook-123"
authorizationHeader := "Bearer automation-token"

webhook := &ps.Webhook{
ID: webhookID,
URL: "https://example.com/webhook",
Enabled: true,
}

svc := &mock.WebhooksService{
UpdateFn: func(ctx context.Context, req *ps.UpdateWebhookRequest) (*ps.Webhook, error) {
c.Assert(req.Organization, qt.Equals, org)
c.Assert(req.Database, qt.Equals, db)
c.Assert(req.ID, qt.Equals, webhookID)
c.Assert(*req.AuthorizationHeader, qt.Equals, authorizationHeader)
return webhook, nil
},
}

ch := &cmdutil.Helper{
Printer: p,
Config: &config.Config{
Organization: org,
},
Client: func() (*ps.Client, error) {
return &ps.Client{
Webhooks: svc,
}, nil
},
}

cmd := UpdateCmd(ch)
cmd.SetArgs([]string{db, webhookID, "--authorization-header", authorizationHeader})
err := cmd.Execute()

c.Assert(err, qt.IsNil)
c.Assert(svc.UpdateFnInvoked, qt.IsTrue)
}

func TestWebhook_UpdateCmd_ClearAuthorizationHeaderFlag(t *testing.T) {
c := qt.New(t)

var buf bytes.Buffer
format := printer.JSON
p := printer.NewPrinter(&format)
p.SetResourceOutput(&buf)

org := "planetscale"
db := "mydb"
webhookID := "webhook-123"
webhook := &ps.Webhook{ID: webhookID, URL: "https://example.com/webhook", Enabled: true}

svc := &mock.WebhooksService{
UpdateFn: func(ctx context.Context, req *ps.UpdateWebhookRequest) (*ps.Webhook, error) {
c.Assert(req.AuthorizationHeader, qt.IsNotNil)
c.Assert(*req.AuthorizationHeader, qt.Equals, "")
return webhook, nil
},
}

ch := &cmdutil.Helper{
Printer: p,
Config: &config.Config{Organization: org},
Client: func() (*ps.Client, error) {
return &ps.Client{Webhooks: svc}, nil
},
}

cmd := UpdateCmd(ch)
cmd.SetArgs([]string{db, webhookID, "--clear-authorization-header"})
err := cmd.Execute()

c.Assert(err, qt.IsNil)
c.Assert(svc.UpdateFnInvoked, qt.IsTrue)
}

func TestWebhook_UpdateCmd_RejectsEmptyAuthorizationHeader(t *testing.T) {
c := qt.New(t)

ch := &cmdutil.Helper{
Printer: printer.NewPrinter(nil),
Config: &config.Config{Organization: "planetscale"},
Client: func() (*ps.Client, error) {
return &ps.Client{}, nil
},
}

cmd := UpdateCmd(ch)
cmd.SetArgs([]string{"mydb", "webhook-123", "--authorization-header", ""})
err := cmd.Execute()

c.Assert(err, qt.ErrorMatches, `--authorization-header cannot be empty; use --clear-authorization-header to remove the configured header`)
}

func TestWebhook_UpdateCmd_RejectsSetAndClearAuthorizationHeader(t *testing.T) {
c := qt.New(t)

ch := &cmdutil.Helper{
Printer: printer.NewPrinter(nil),
Config: &config.Config{Organization: "planetscale"},
Client: func() (*ps.Client, error) {
return &ps.Client{}, nil
},
}

cmd := UpdateCmd(ch)
cmd.SetArgs([]string{"mydb", "webhook-123", "--authorization-header", "Bearer automation-token", "--clear-authorization-header"})
err := cmd.Execute()

c.Assert(err, qt.ErrorMatches, `if any flags in the group \[authorization-header clear-authorization-header\] are set none of the others can be; \[authorization-header clear-authorization-header\] were all set`)
}

func TestWebhook_UpdateCmd_RequiresAtLeastOneFlag(t *testing.T) {
c := qt.New(t)

Expand Down
60 changes: 32 additions & 28 deletions internal/cmd/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ func WebhookCmd(ch *cmdutil.Helper) *cobra.Command {

// Webhook returns a table and json serializable webhook for printing.
type Webhook struct {
ID string `header:"id" json:"id"`
URL string `header:"url" json:"url"`
Events string `header:"events" json:"events"`
Enabled bool `header:"enabled" json:"enabled"`
CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"`
UpdatedAt int64 `header:"updated_at,timestamp(ms|utc|human)" json:"updated_at"`
ID string `header:"id" json:"id"`
URL string `header:"url" json:"url"`
AuthorizationHeaderConfigured bool `header:"authorization header configured" json:"authorization_header_configured"`
Events string `header:"events" json:"events"`
Enabled bool `header:"enabled" json:"enabled"`
CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"`
UpdatedAt int64 `header:"updated_at,timestamp(ms|utc|human)" json:"updated_at"`

orig *ps.Webhook
}
Expand All @@ -50,13 +51,14 @@ func (w *Webhook) MarshalJSON() ([]byte, error) {
// toWebhook returns a struct that prints out the various fields of a webhook model.
func toWebhook(webhook *ps.Webhook) *Webhook {
return &Webhook{
ID: webhook.ID,
URL: webhook.URL,
Events: strings.Join(webhook.Events, ", "),
Enabled: webhook.Enabled,
CreatedAt: printer.GetMilliseconds(webhook.CreatedAt),
UpdatedAt: printer.GetMilliseconds(webhook.UpdatedAt),
orig: webhook,
ID: webhook.ID,
URL: webhook.URL,
AuthorizationHeaderConfigured: webhook.AuthorizationHeaderConfigured,
Events: strings.Join(webhook.Events, ", "),
Enabled: webhook.Enabled,
CreatedAt: printer.GetMilliseconds(webhook.CreatedAt),
UpdatedAt: printer.GetMilliseconds(webhook.UpdatedAt),
orig: webhook,
}
}

Expand All @@ -70,13 +72,14 @@ func toWebhooks(webhooks []*ps.Webhook) []*Webhook {

// WebhookWithSecret includes the webhook secret for display.
type WebhookWithSecret struct {
ID string `header:"id" json:"id"`
URL string `header:"url" json:"url"`
Secret string `header:"secret" json:"secret"`
Events string `header:"events" json:"events"`
Enabled bool `header:"enabled" json:"enabled"`
CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"`
UpdatedAt int64 `header:"updated_at,timestamp(ms|utc|human)" json:"updated_at"`
ID string `header:"id" json:"id"`
URL string `header:"url" json:"url"`
Secret string `header:"secret" json:"secret"`
AuthorizationHeaderConfigured bool `header:"authorization header configured" json:"authorization_header_configured"`
Events string `header:"events" json:"events"`
Enabled bool `header:"enabled" json:"enabled"`
CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"`
UpdatedAt int64 `header:"updated_at,timestamp(ms|utc|human)" json:"updated_at"`

orig *ps.Webhook
}
Expand All @@ -88,13 +91,14 @@ func (w *WebhookWithSecret) MarshalJSON() ([]byte, error) {
// toWebhookWithSecret returns a struct that includes the webhook secret.
func toWebhookWithSecret(webhook *ps.Webhook) *WebhookWithSecret {
return &WebhookWithSecret{
ID: webhook.ID,
URL: webhook.URL,
Secret: webhook.Secret,
Events: strings.Join(webhook.Events, ", "),
Enabled: webhook.Enabled,
CreatedAt: printer.GetMilliseconds(webhook.CreatedAt),
UpdatedAt: printer.GetMilliseconds(webhook.UpdatedAt),
orig: webhook,
ID: webhook.ID,
URL: webhook.URL,
Secret: webhook.Secret,
AuthorizationHeaderConfigured: webhook.AuthorizationHeaderConfigured,
Events: strings.Join(webhook.Events, ", "),
Enabled: webhook.Enabled,
CreatedAt: printer.GetMilliseconds(webhook.CreatedAt),
UpdatedAt: printer.GetMilliseconds(webhook.UpdatedAt),
orig: webhook,
}
}
Loading