Skip to content

feat: add hand-rolled get-sdk-active command for environments#671

Merged
ari-launchdarkly merged 4 commits intomainfrom
devin/1774384599-sdk-active-command
Mar 25, 2026
Merged

feat: add hand-rolled get-sdk-active command for environments#671
ari-launchdarkly merged 4 commits intomainfrom
devin/1774384599-sdk-active-command

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Mar 24, 2026

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Related to the getSdkActiveForEnvironment endpoint (GET /api/v2/projects/{projectKey}/environments/{environmentKey}/sdk-active) which exists in the hidden API spec but is not available via make openapi-spec-update since it is not in the public spec.

Describe the solution you've provided

Adds a hand-rolled custom CLI command get-sdk-active registered as a subcommand of environments. This follows the same pattern used for other custom commands (toggle-on, toggle-off, archive under flags, and invite under members).

Usage:

ldcli environments get-sdk-active --project <key> --environment <key> [--sdk-name <name>] [--sdk-wrapper-name <name>]

The command:

  • Makes a GET request to /api/v2/projects/{projectKey}/environments/{environmentKey}/sdk-active
  • Requires --project and --environment flags
  • Supports optional --sdk-name and --sdk-wrapper-name flags to narrow the check to a specific SDK (e.g. go-server-sdk). These map to sdk_name and sdk_wrapper_name query parameters matching the gonfalon handler's req.URL.Query().Get(...) usage. The @launchdarkly/ prefix matching is handled server-side.
  • Supports --output json for raw JSON passthrough
  • Plaintext output displays SDK active: true/false

Tests cover plaintext output, JSON output, optional filter flags, and missing required flag validation.

Key review points:

  • The response shape is { "active": boolean }, matching gonfalon's UsageSdkActiveRep struct (json:"active" tag). Plaintext output reads resp.Active directly.
  • The plaintext output bypasses the standard CmdOutput/CmdOutputSingular pipeline because the response shape doesn't have the name/key fields that SingularPlaintextOutputFn expects. Instead, it uses a typed sdkActiveResponse struct and json.Unmarshal directly. Verify this approach is acceptable vs. adding a new PlaintextOutputFn.
  • Query params are passed via url.Values through MakeRequest's query argument (not embedded in the URL path), because MakeRequest overwrites req.URL.RawQuery on line 59 of client.go.
  • Test coverage gap: The MockClient doesn't capture the request URL or query params, so the optional filter tests only verify the flags are accepted without error — they cannot assert the params actually reach the request. Manual verification or an integration test would be needed to confirm end-to-end behavior.
  • Command is named get-sdk-active under environments — confirm this is the right name/location.

Describe alternatives you've considered

  1. Adding the endpoint to ld-openapi.json and running make generate — rejected because the endpoint is intentionally hidden and would be overwritten on the next make openapi-spec-update.
  2. Using CmdOutputSingular with a custom PlaintextOutputFn — the resource type alias used by PlaintextOutputFn is unexported from the output package, so a custom function can't be passed from outside the package without changes. The direct json.Unmarshal approach was simpler.

Additional context

This is the first hidden-only endpoint exposed as a custom CLI command. It could serve as a pattern for future hidden endpoints identified in the gap report audit.

Updates since last revision

  • Fixed response field: changed from sdkActive to active to match the actual gonfalon UsageSdkActiveRep struct (json:"active" tag).
  • Added optional --sdk-name and --sdk-wrapper-name filter flags, matching gonfalon's query parameter support for narrowing the active check to a specific SDK.
  • Fixed query param delivery: params are now passed via url.Values through MakeRequest's query argument instead of being embedded in the URL string (which MakeRequest silently overwrites via req.URL.RawQuery = query.Encode()).

Link to Devin session: https://app.devin.ai/sessions/8951a04e5c5f46b1915e5d00a281914e

Adds a custom CLI command to check SDK active status for an environment.
This endpoint (GET /api/v2/projects/{projectKey}/environments/{environmentKey}/sdk-active)
is hidden in the API spec and cannot be synced via make openapi-spec-update,
so it is implemented as a hand-rolled command registered under the
environments parent command.

The command accepts --project and --environment flags and supports
both plaintext and JSON output formats.

Co-Authored-By: Ari Salem <asalem@launchdarkly.com>
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

_ = cmd.MarkFlagRequired(cliflags.EnvironmentFlag)
_ = cmd.Flags().SetAnnotation(cliflags.EnvironmentFlag, "required", []string{"true"})
_ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag))
}
Copy link

Choose a reason for hiding this comment

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

Global viper binding overwrites break existing commands

High Severity

Calling viper.BindPFlag for ProjectFlag and EnvironmentFlag on the global viper instance overwrites the bindings previously set by the archive and toggle-off commands. Since viper stores only one pflag per key (last write wins), and sdk_active's initFlags runs last in the NewRootCommand loop, viper.GetString("project") and viper.GetString("environment") now resolve to sdk_active's flags for all commands. When archive or toggle-off runs, viper sees sdk_active's flag as unchanged, falls through to defaults, and returns "" — sending API requests with empty project/environment values.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a false positive. The same viper.BindPFlag pattern is used by toggle.go (lines 88-101) and archive.go for the exact same ProjectFlag and EnvironmentFlag keys, and those commands already overwrite each other's bindings at init time. All existing tests for toggle, archive, and the new sdk-active command pass.

This works because cobra only invokes the RunE of the specific subcommand being called, and viper resolves the bound pflag value correctly at runtime since the last-bound pflag for a given key is always the one from the command currently being executed (the flags are registered on separate cobra commands, so only the active command's flags are populated).

The gonfalon UsageSdkActiveRep struct uses json:"active", not
json:"sdkActive". Updated the response struct and tests to match
the actual API response shape.

Co-Authored-By: Ari Salem <asalem@launchdarkly.com>

type sdkActiveResponse struct {
Active bool `json:"active"`
}
Copy link

Choose a reason for hiding this comment

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

Wrong JSON tag causes field to always be false

High Severity

The sdkActiveResponse struct uses the JSON tag json:"active" but the PR description states the API response field is sdkActive. When the real API returns {"sdkActive": true, "lastSeenAt": "..."}, json.Unmarshal won't match the active tag to the sdkActive key, so resp.Active will always be false. The test masks this by mocking the response as {"active": true} instead of the real API shape {"sdkActive": true}.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a false positive. The API response field is active, not sdkActive. I verified this directly in gonfalon's UsageSdkActiveRep struct at internal/reps/usage_sdk_active_rep.go:

type UsageSdkActiveRep struct {
    Active bool `json:"active"`
}

The second commit (fix: correct response field from sdkActive to active) specifically corrected this — the original code had sdkActive which was wrong. The current code and tests use the correct field name.

The gonfalon endpoint supports sdk_name and sdk_wrapper_name query
parameters to narrow the active check to a specific SDK. These are
now exposed as optional --sdk-name and --sdk-wrapper-name flags.

Co-Authored-By: Ari Salem <asalem@launchdarkly.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

MakeRequest overwrites req.URL.RawQuery with the query argument,
so embedding params in the URL string was silently stripping them.
Now sdk_name and sdk_wrapper_name are passed via url.Values through
the query parameter.

Co-Authored-By: Ari Salem <asalem@launchdarkly.com>
@ari-launchdarkly ari-launchdarkly merged commit b3c447b into main Mar 25, 2026
3 checks passed
@ari-launchdarkly ari-launchdarkly deleted the devin/1774384599-sdk-active-command branch March 25, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants