diff --git a/.changesets/1790903048-d99c727b.yaml b/.changesets/1790903048-d99c727b.yaml new file mode 100644 index 00000000..7548345e --- /dev/null +++ b/.changesets/1790903048-d99c727b.yaml @@ -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" diff --git a/pkg/generate/snapshots/cli_release_go_test.go b/pkg/generate/snapshots/cli_release_go_test.go index b2efee14..d96dce60 100644 --- a/pkg/generate/snapshots/cli_release_go_test.go +++ b/pkg/generate/snapshots/cli_release_go_test.go @@ -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: @@ -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: @@ -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: diff --git a/templates/templates/cli/README.md b/templates/templates/cli/README.md index d7d2a8c9..897ade1d 100644 --- a/templates/templates/cli/README.md +++ b/templates/templates/cli/README.md @@ -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. diff --git a/templates/templates/cli/auxiliary/internal/config/config.go.stmpl b/templates/templates/cli/auxiliary/internal/config/config.go.stmpl index 07129d6c..c9cf5e7d 100644 --- a/templates/templates/cli/auxiliary/internal/config/config.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/config/config.go.stmpl @@ -152,13 +152,14 @@ 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" @@ -166,6 +167,9 @@ func ResolveCredential(cmd *cobra.Command, flagName string) (value, source strin if val := GetConfigValue(flagName); val != "" { return val, "config" } + if !changed && flagValue != "" { + return flagValue, "default" + } return "", "unset" } diff --git a/templates/templates/cli/tests/primary/configure_test.go.stmpl b/templates/templates/cli/tests/primary/configure_test.go.stmpl index 3480c5af..2a2cb41e 100644 --- a/templates/templates/cli/tests/primary/configure_test.go.stmpl +++ b/templates/templates/cli/tests/primary/configure_test.go.stmpl @@ -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")) + 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 diff --git a/templates/templates/cli/whoami.go.stmpl b/templates/templates/cli/whoami.go.stmpl index c4648434..9a3fbb45 100644 --- a/templates/templates/cli/whoami.go.stmpl +++ b/templates/templates/cli/whoami.go.stmpl @@ -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}} diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index 55877891..a707abaf 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -218,7 +218,7 @@ trackedFiles: internal/cli/version.go: last_write_checksum: sha1:ef4d924ad83b7b2e9e4698a9f0ac229bac4060d4 internal/cli/whoami.go: - last_write_checksum: sha1:9b243266c806ac4e720aa333304186b8bb7d2f18 + last_write_checksum: sha1:3bd64fadb167501a3a217b9e102260727730a094 internal/client/client.go: last_write_checksum: sha1:972bb2e7dd3b2e5d2f9ff10e31e7f54e40550c9d internal/client/diagnostics.go: @@ -226,7 +226,7 @@ trackedFiles: internal/clierrors/clierrors.go: last_write_checksum: sha1:e17fb3e33e439c6997834182011540427d760857 internal/config/config.go: - last_write_checksum: sha1:28c3d5fbeafb9b44cfb4501198135088d30d7145 + last_write_checksum: sha1:d82e1115bbd3bbc9043c485afd90e28dafe76b69 internal/config/keyring.go: last_write_checksum: sha1:f72a8a7e8804523766bac15ca99e97af94c8ff30 internal/explorer/explorer.go: diff --git a/zSDKs/sdk-cli/docs/cli_whoami.md b/zSDKs/sdk-cli/docs/cli_whoami.md index 17dbaaa9..ce4fa376 100644 --- a/zSDKs/sdk-cli/docs/cli_whoami.md +++ b/zSDKs/sdk-cli/docs/cli_whoami.md @@ -11,6 +11,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. diff --git a/zSDKs/sdk-cli/internal/cli/whoami.go b/zSDKs/sdk-cli/internal/cli/whoami.go index 288efad4..e6d6f98e 100644 --- a/zSDKs/sdk-cli/internal/cli/whoami.go +++ b/zSDKs/sdk-cli/internal/cli/whoami.go @@ -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.`, diff --git a/zSDKs/sdk-cli/internal/config/config.go b/zSDKs/sdk-cli/internal/config/config.go index 58f8f8d7..eafea32a 100644 --- a/zSDKs/sdk-cli/internal/config/config.go +++ b/zSDKs/sdk-cli/internal/config/config.go @@ -207,13 +207,14 @@ 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" @@ -221,6 +222,9 @@ func ResolveCredential(cmd *cobra.Command, flagName string) (value, source strin if val := GetConfigValue(flagName); val != "" { return val, "config" } + if !changed && flagValue != "" { + return flagValue, "default" + } return "", "unset" }