Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions cmd/cliflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
CorsEnabledFlag = "cors-enabled"
CorsOriginFlag = "cors-origin"
DataFlag = "data"
DryRunFlag = "dry-run"
DevStreamURIFlag = "dev-stream-uri"
EmailsFlag = "emails"
EnvironmentFlag = "environment"
Expand All @@ -51,6 +52,7 @@ const (
CorsEnabledFlagDescription = "Enable CORS headers for browser-based developer tools (default: false)"
CorsOriginFlagDescription = "Allowed CORS origin. Use '*' for all origins (default: '*')"
DevStreamURIDescription = "Streaming service endpoint that the dev server uses to obtain authoritative flag data. This may be a LaunchDarkly or Relay Proxy endpoint"
DryRunFlagDescription = "Validate the change without persisting it. Returns a preview of the result."
EnvironmentFlagDescription = "Default environment key"
FieldsFlagDescription = "Comma-separated list of top-level fields to include in JSON output (e.g., --fields key,name,kind)"
FlagFlagDescription = "Default feature flag key"
Expand Down
8 changes: 7 additions & 1 deletion cmd/flags/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ func makeArchiveRequest(client resources.Client) func(*cobra.Command, []string)
viper.GetString(cliflags.ProjectFlag),
viper.GetString(cliflags.FlagFlag),
)
var query url.Values
if dryRun, _ := cmd.Flags().GetBool(cliflags.DryRunFlag); dryRun {
query = url.Values{"dryRun": []string{"true"}}
}
res, err := client.MakeRequest(
viper.GetString(cliflags.AccessTokenFlag),
"PATCH",
path,
"application/json",
nil,
query,
[]byte(`[{"op": "replace", "path": "/archived", "value": true}]`),
false,
)
Expand Down Expand Up @@ -75,4 +79,6 @@ func initArchiveFlags(cmd *cobra.Command) {
_ = cmd.MarkFlagRequired(cliflags.ProjectFlag)
_ = cmd.Flags().SetAnnotation(cliflags.ProjectFlag, "required", []string{"true"})
_ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag))

cmd.Flags().Bool(cliflags.DryRunFlag, false, cliflags.DryRunFlagDescription)
}
43 changes: 43 additions & 0 deletions cmd/flags/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,49 @@ func TestArchive(t *testing.T) {
assert.Contains(t, string(output), "test-flag")
})

t.Run("passes dryRun query param when --dry-run is set", func(t *testing.T) {
args := []string{
"flags", "archive",
"--access-token", "abcd1234",
"--flag", "test-flag",
"--project", "test-proj",
"--output", "json",
"--dry-run",
}
_, err := cmd.CallCmd(
t,
cmd.APIClients{
ResourcesClient: mockClient,
},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Equal(t, "true", mockClient.Query.Get("dryRun"))
})

t.Run("does not pass dryRun query param by default", func(t *testing.T) {
args := []string{
"flags", "archive",
"--access-token", "abcd1234",
"--flag", "test-flag",
"--project", "test-proj",
"--output", "json",
}
_, err := cmd.CallCmd(
t,
cmd.APIClients{
ResourcesClient: mockClient,
},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Empty(t, mockClient.Query.Get("dryRun"))
})

t.Run("returns error with missing flags", func(t *testing.T) {
args := []string{
"flags", "archive",
Expand Down
8 changes: 7 additions & 1 deletion cmd/flags/toggle.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,16 @@ func runE(client resources.Client) func(*cobra.Command, []string) error {
viper.GetString(cliflags.ProjectFlag),
viper.GetString(cliflags.FlagFlag),
)
var query url.Values
if dryRun, _ := cmd.Flags().GetBool(cliflags.DryRunFlag); dryRun {
query = url.Values{"dryRun": []string{"true"}}
}
res, err := client.MakeRequest(
viper.GetString(cliflags.AccessTokenFlag),
"PATCH",
path,
"application/json",
nil,
query,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dry-run plaintext output misleadingly says "Successfully updated"

Medium Severity

When --dry-run is set and plaintext output is used, CmdOutput is called with the hardcoded action "update", which produces the prefix "Successfully updated". Nothing was actually persisted — the API only validated and returned a preview. A user seeing "Successfully updated" after explicitly requesting --dry-run may believe the mutation was applied, undermining the purpose of the flag.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5eb2a6f. Configure here.

[]byte(buildPatch(viper.GetString("environment"), toggleOn)),
false,
)
Expand Down Expand Up @@ -102,6 +106,8 @@ func initFlags(cmd *cobra.Command) {
_ = cmd.MarkFlagRequired(cliflags.ProjectFlag)
_ = cmd.Flags().SetAnnotation(cliflags.ProjectFlag, "required", []string{"true"})
_ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag))

cmd.Flags().Bool(cliflags.DryRunFlag, false, cliflags.DryRunFlagDescription)
}

func buildPatch(envKey string, toggleValue bool) string {
Expand Down
45 changes: 45 additions & 0 deletions cmd/flags/toggle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,51 @@ func TestToggleOff(t *testing.T) {
assert.Contains(t, string(output), "test-flag")
})

t.Run("passes dryRun query param when --dry-run is set", func(t *testing.T) {
args := []string{
"flags", "toggle-off",
"--access-token", "abcd1234",
"--environment", "test-env",
"--flag", "test-flag",
"--project", "test-proj",
"--output", "json",
"--dry-run",
}
_, err := cmd.CallCmd(
t,
cmd.APIClients{
ResourcesClient: mockClient,
},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Equal(t, "true", mockClient.Query.Get("dryRun"))
})

t.Run("does not pass dryRun query param by default", func(t *testing.T) {
args := []string{
"flags", "toggle-off",
"--access-token", "abcd1234",
"--environment", "test-env",
"--flag", "test-flag",
"--project", "test-proj",
"--output", "json",
}
_, err := cmd.CallCmd(
t,
cmd.APIClients{
ResourcesClient: mockClient,
},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Empty(t, mockClient.Query.Get("dryRun"))
})

t.Run("returns error with missing required flags", func(t *testing.T) {
args := []string{
"flags", "toggle-off",
Expand Down
2 changes: 2 additions & 0 deletions internal/resources/mock_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
type MockClient struct {
Err error
Input []byte
Query url.Values
Response []byte
StatusCode int
}
Expand All @@ -21,6 +22,7 @@ func (c *MockClient) MakeRequest(
isBeta bool,
) ([]byte, error) {
c.Input = data
c.Query = query

if c.StatusCode > http.StatusBadRequest {
return c.Response, c.Err
Expand Down
Loading