Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changesets/1790903048-d99c727b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1790903048-d99c727b
features:
- globals
targets:
- cli
type: fix
bump: patch
description: report defaulted global parameters in whoami
author: TristanSpeakEasy
date: "2026-10-02"
27 changes: 27 additions & 0 deletions pkg/generate/snapshots/cli_release_go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ jobs:
with:
fetch-depth: 0

- name: Check release version
run: |
tag_version="${GITHUB_REF_NAME#v}"
cli_version=$(sed -n 's/^var Version = "\(.*\)"$/\1/p' internal/cli/version.go)
if [ -z "$cli_version" ] || [ "$tag_version" != "$cli_version" ]; then
printf '::error::Release tag %s does not match generated CLI version %s. Set cli.version in .speakeasy/gen.yaml to %s and regenerate before tagging.\n' "$GITHUB_REF_NAME" "$cli_version" "$tag_version"
exit 1
fi

- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand Down Expand Up @@ -1070,6 +1079,15 @@ jobs:
with:
fetch-depth: 0

- name: Check release version
run: |
tag_version="${GITHUB_REF_NAME#v}"
cli_version=$(sed -n 's/^var Version = "\(.*\)"$/\1/p' internal/cli/version.go)
if [ -z "$cli_version" ] || [ "$tag_version" != "$cli_version" ]; then
printf '::error::Release tag %s does not match generated CLI version %s. Set cli.version in .speakeasy/gen.yaml to %s and regenerate before tagging.\n' "$GITHUB_REF_NAME" "$cli_version" "$tag_version"
exit 1
fi

- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand Down Expand Up @@ -2117,6 +2135,15 @@ jobs:
with:
fetch-depth: 0

- name: Check release version
run: |
tag_version="${GITHUB_REF_NAME#v}"
cli_version=$(sed -n 's/^var Version = "\(.*\)"$/\1/p' internal/cli/version.go)
if [ -z "$cli_version" ] || [ "$tag_version" != "$cli_version" ]; then
printf '::error::Release tag %s does not match generated CLI version %s. Set cli.version in .speakeasy/gen.yaml to %s and regenerate before tagging.\n' "$GITHUB_REF_NAME" "$cli_version" "$tag_version"
exit 1
fi

- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand Down
6 changes: 4 additions & 2 deletions templates/templates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1185,12 +1185,14 @@ cli response-headers response-headers --status-code 200 --include-headers --outp
| Field type | Resolution function | Priority chain |
| -------------------- | ----------------------------- | -------------------------------------------------- |
| Security credentials | `ResolveSecurityCredential()` | CLI flag > env var > **OS keychain** > config file |
| Global parameters | `ResolveCredential()` | CLI flag > env var > config file |
| Global parameters | `ResolveCredential()` | CLI flag > env var > config file > flag default |

**Commands**:

- `configure` - Interactive prompt for credentials and global parameters. When the OS keychain is available (via `go-keyring`), security credentials are stored in the keychain instead of the config file. Falls back to config file on headless/CI environments
- `whoami` - Displays current credential values and global parameter settings with their sources (flag/env/keyring/config/unset)
- `whoami` - Displays current credential values and global parameter settings with their sources (flag/env/keyring/config/default/unset)

`whoami` uses `ResolveCredential()` in `auxiliary/internal/config/config.go.stmpl` to report unchanged, non-empty global parameter flag values as `[default]`, or `"source": "default"` in machine output. String defaults, boolean `false`, and numeric `0` are retained. Explicit flags, environment variables, and config values override defaults. Empty flag values and missing flags fall through to environment and config; values are `[unset]` only when those sources are also empty. Security credential resolution and masking are unchanged.

The `configure` and `whoami` commands are generated whenever the API has global security schemes and/or global parameters (controlled by `hasConfigurableSettings()` in `security.ts`). When both are present, the configure command shows separate "Authentication" and "Global Parameters" sections.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,24 @@ func GetConfigStringSlice(key string) []string {
{{- end}}

// ResolveCredential resolves a credential value using the priority chain:
// flag > env var > config file. Returns the value and its source
// ("flag", "env", "config", or "unset").
// flag > env var > config file > flag default. Returns the value and its source
// ("flag", "env", "config", "default", or "unset").
// Used for global parameters. For security credentials, use ResolveSecurityCredential
// which includes the OS keychain tier.
func ResolveCredential(cmd *cobra.Command, flagName string) (value, source string) {
if val, changed := flagutil.GetStringFlag(cmd, flagName); changed && val != "" {
return val, "flag"
flagValue, changed := flagutil.GetStringFlag(cmd, flagName)
if changed && flagValue != "" {
return flagValue, "flag"
}
if val := GetEnvValue(flagName); val != "" {
return val, "env"
}
if val := GetConfigValue(flagName); val != "" {
return val, "config"
}
if !changed && flagValue != "" {
return flagValue, "default"
}
return "", "unset"
}

Expand Down
90 changes: 90 additions & 0 deletions templates/templates/cli/tests/primary/configure_test.go.stmpl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,96 @@ func setupConfigureTest(t *testing.T) (h *CLITestHarness, tmpDir string) {
return h, tmpDir
}

func TestWhoamiGlobalDefaults(t *testing.T) {
for _, format := range []string{"pretty", "json"} {
t.Run(format, func(t *testing.T) {
resetConfig(t)
h := NewCLITestHarness(t)
h.resetAndSetupEnv()
root, err := cli.NewRootCommand()
require.NoError(t, err)
root.SetOut(h.stdout)
root.SetErr(h.stderr)

flag := root.PersistentFlags().Lookup("global-query-param")
require.NotNil(t, flag)
flag.DefValue = "default-query"
require.NoError(t, flag.Value.Set(flag.DefValue))
require.False(t, flag.Changed)

require.NoError(t, cli.ExecuteRoot(context.Background(), root, []string{
"whoami", "--no-interactive", "--output-format", format,
}))
if format == "json" {
var info struct {
Parameters map[string]struct {
Source string `json:"source"`
Value string `json:"value"`
} `json:"global_parameters"`
}
require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &info))
for name, value := range map[string]string{
"global-query-param": "default-query",
"global-header-param": "false",
"global-path-param": "0",
} {
assert.Equal(t, "default", info.Parameters[name].Source)
assert.Equal(t, value, info.Parameters[name].Value)
}
assert.Equal(t, "unset", info.Parameters["global-optional-path-param"].Source)
} else {
assert.Regexp(t, `--global-query-param\s+\[default\]\s+default-query`, h.GetStdout())
assert.Regexp(t, `--global-header-param\s+\[default\]\s+false`, h.GetStdout())
assert.Regexp(t, `--global-path-param\s+\[default\]\s+0`, h.GetStdout())
}
})
}
}

func TestResolveCredentialDefaultPrecedence(t *testing.T) {
for _, tt := range []struct {
name string
flag string
env string
stored string
want string
source string
}{
{name: "default", want: "default-query", source: "default"},
{name: "config", stored: "config-query", want: "config-query", source: "config"},
{name: "environment", env: "env-query", stored: "config-query", want: "env-query", source: "env"},
{name: "flag", flag: "flag-query", env: "env-query", stored: "config-query", want: "flag-query", source: "flag"},
} {
t.Run(tt.name, func(t *testing.T) {
resetConfig(t)
h := NewCLITestHarness(t)
h.resetAndSetupEnv()
writeConfigFile(t, os.Getenv("HOME"), map[string]interface{}{
"globals": map[string]interface{}{"global_query_param": tt.stored},
})
t.Setenv("CLI_GLOBAL_QUERY_PARAM", tt.env)
root, err := cli.NewRootCommand()
require.NoError(t, err)
flag := root.PersistentFlags().Lookup("global-query-param")
require.NotNil(t, flag)
flag.DefValue = "default-query"
require.NoError(t, flag.Value.Set(flag.DefValue))
if tt.flag != "" {
require.NoError(t, root.PersistentFlags().Set(flag.Name, tt.flag))
}
child, _, err := root.Find([]string{"whoami"})
require.NoError(t, err)
require.NoError(t, config.Init("cli", "CLI"))
Comment thread
TristanSpeakEasy marked this conversation as resolved.
value, source := config.ResolveCredential(child, flag.Name)
assert.Equal(t, tt.want, value)
assert.Equal(t, tt.source, source)
value, source = config.ResolveCredential(child, "missing-flag")
assert.Empty(t, value)
assert.Equal(t, "unset", source)
})
}
}

// TestConfigureSkipsOptionalCredentials verifies that when all security
// schemes are OR alternatives (multiple options), the configure command
// allows skipping credentials by pressing Enter (empty input) and does
Expand Down
1 change: 1 addition & 0 deletions templates/templates/cli/whoami.go.stmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Sources are shown as:
[env] - Set via environment variable ({{.Global.Config.EnvVarPrefix}}_*)
[keyring] - Set via OS keychain (stored by configure command)
[config] - Set via config file (~/.config/{{sanitizeCliName}}/config.yaml)
[default] - Built-in global parameter flag default
[unset] - Not configured
{{- if hasGlobalSecurity}}

Expand Down
4 changes: 2 additions & 2 deletions zSDKs/sdk-cli/.speakeasy/gen.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zSDKs/sdk-cli/docs/cli_whoami.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zSDKs/sdk-cli/internal/cli/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Sources are shown as:
[env] - Set via environment variable (CLI_*)
[keyring] - Set via OS keychain (stored by configure command)
[config] - Set via config file (~/.config/cli/config.yaml)
[default] - Built-in global parameter flag default
[unset] - Not configured

Credential values are masked for security.`,
Expand Down
12 changes: 8 additions & 4 deletions zSDKs/sdk-cli/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,24 @@ func GetConfigValue(key string) string {
}

// ResolveCredential resolves a credential value using the priority chain:
// flag > env var > config file. Returns the value and its source
// ("flag", "env", "config", or "unset").
// flag > env var > config file > flag default. Returns the value and its source
// ("flag", "env", "config", "default", or "unset").
// Used for global parameters. For security credentials, use ResolveSecurityCredential
// which includes the OS keychain tier.
func ResolveCredential(cmd *cobra.Command, flagName string) (value, source string) {
if val, changed := flagutil.GetStringFlag(cmd, flagName); changed && val != "" {
return val, "flag"
flagValue, changed := flagutil.GetStringFlag(cmd, flagName)
if changed && flagValue != "" {
return flagValue, "flag"
}
if val := GetEnvValue(flagName); val != "" {
return val, "env"
}
if val := GetConfigValue(flagName); val != "" {
return val, "config"
}
if !changed && flagValue != "" {
return flagValue, "default"
}
return "", "unset"
}

Expand Down
Loading