Compare commits

...

1 Commits

Author SHA1 Message Date
Michael Patterson
0fe1114fc7 fix(cli): skip false deprecation warnings for aliased env vars
When old and new options share the same Value pointer (e.g.,
CODER_NOTIFICATIONS_EMAIL_FROM and CODER_EMAIL_FROM), serpent
propagates ValueSource across all siblings. This caused false
deprecation warnings when only the new env var was set.

Add isDirectlySet() to verify the deprecated option was itself
the source (via os.LookupEnv for env vars, flag.Changed for
flags) before emitting the warning.

Fixes #23847

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:00:07 +00:00
2 changed files with 139 additions and 0 deletions

View File

@@ -1616,6 +1616,17 @@ func PrintDeprecatedOptions() serpent.MiddlewareFunc {
continue
}
// Verify that this specific option was directly set,
// not just inheriting a ValueSource from a sibling
// option that shares the same Value pointer. Without
// this check, setting CODER_EMAIL_FROM (new name)
// would also warn about CODER_NOTIFICATIONS_EMAIL_FROM
// (old name) because serpent propagates ValueSource
// across options sharing the same Value.
if !isDirectlySet(inv, opt) {
continue
}
var warnStr strings.Builder
_, _ = warnStr.WriteString(translateSource(opt.ValueSource, opt))
_, _ = warnStr.WriteString(" is deprecated, please use ")
@@ -1637,6 +1648,29 @@ func PrintDeprecatedOptions() serpent.MiddlewareFunc {
}
}
// isDirectlySet checks whether a specific option was directly set by
// the user, rather than inheriting its ValueSource from another option
// that shares the same Value pointer.
func isDirectlySet(inv *serpent.Invocation, opt serpent.Option) bool {
switch opt.ValueSource {
case serpent.ValueSourceFlag:
if inv.ParsedFlags() == nil {
return false
}
f := inv.ParsedFlags().Lookup(opt.Flag)
return f != nil && f.Changed
case serpent.ValueSourceEnv:
_, exists := os.LookupEnv(opt.Env)
return exists
case serpent.ValueSourceYAML:
// YAML source cannot be cheaply verified here without
// re-parsing the config file. Assume it is correct.
return true
default:
return false
}
}
// translateSource provides the name of the source of the option, depending on the
// supplied target ValueSource.
func translateSource(target serpent.ValueSource, opt serpent.Option) string {

View File

@@ -191,6 +191,111 @@ func Test_wrapTransportWithTelemetryHeader(t *testing.T) {
require.Equal(t, ti.Command, "test")
}
//nolint:paralleltest // t.Setenv is incompatible with t.Parallel.
func TestPrintDeprecatedOptions(t *testing.T) {
//nolint:paralleltest // t.Setenv is incompatible with t.Parallel.
t.Run("NoWarningWhenNewEnvSet", func(t *testing.T) {
// Simulate the scenario from #23847: the new env var
// (CODER_EMAIL_FROM) is set, but the old deprecated one
// (CODER_NOTIFICATIONS_EMAIL_FROM) is not. Both options
// share the same Value pointer, so serpent propagates
// ValueSource to both. The warning should NOT fire for the
// deprecated option because it was not directly set.
var value serpent.String
newOpt := serpent.Option{
Name: "Email From",
Flag: "email-from",
Env: "CODER_TEST_EMAIL_FROM",
Value: &value,
ValueSource: serpent.ValueSourceEnv,
}
deprecatedOpt := serpent.Option{
Name: "Notifications Email From",
Flag: "notifications-email-from",
Env: "CODER_TEST_NOTIFICATIONS_EMAIL_FROM",
Value: &value,
// Serpent propagates ValueSource from the new option.
ValueSource: serpent.ValueSourceEnv,
UseInstead: serpent.OptionSet{newOpt},
}
// Set only the new env var, not the deprecated one.
t.Setenv("CODER_TEST_EMAIL_FROM", "test@example.com")
cmd := &serpent.Command{
Use: "test",
Options: serpent.OptionSet{
newOpt,
deprecatedOpt,
},
Handler: func(_ *serpent.Invocation) error {
return nil
},
}
var stderr bytes.Buffer
inv := cmd.Invoke()
inv.Stderr = &stderr
inv.Stdout = io.Discard
mw := PrintDeprecatedOptions()
err := mw(func(_ *serpent.Invocation) error {
return nil
})(inv)
require.NoError(t, err)
require.Empty(t, stderr.String(), "should not warn when deprecated env var is not set")
})
//nolint:paralleltest // t.Setenv is incompatible with t.Parallel.
t.Run("WarningWhenDeprecatedEnvSet", func(t *testing.T) {
var value serpent.String
newOpt := serpent.Option{
Name: "Email From",
Flag: "email-from",
Env: "CODER_TEST_EMAIL_FROM_2",
Value: &value,
}
deprecatedOpt := serpent.Option{
Name: "Notifications Email From",
Flag: "notifications-email-from-2",
Env: "CODER_TEST_NOTIFICATIONS_EMAIL_FROM_2",
Value: &value,
ValueSource: serpent.ValueSourceEnv,
UseInstead: serpent.OptionSet{newOpt},
}
// Set the deprecated env var.
t.Setenv("CODER_TEST_NOTIFICATIONS_EMAIL_FROM_2", "test@example.com")
cmd := &serpent.Command{
Use: "test",
Options: serpent.OptionSet{
newOpt,
deprecatedOpt,
},
Handler: func(_ *serpent.Invocation) error {
return nil
},
}
var stderr bytes.Buffer
inv := cmd.Invoke()
inv.Stderr = &stderr
inv.Stdout = io.Discard
mw := PrintDeprecatedOptions()
err := mw(func(_ *serpent.Invocation) error {
return nil
})(inv)
require.NoError(t, err)
require.Contains(t, stderr.String(), "CODER_TEST_NOTIFICATIONS_EMAIL_FROM_2",
"should warn when deprecated env var is set")
require.Contains(t, stderr.String(), "deprecated")
})
}
func Test_wrapTransportWithEntitlementsCheck(t *testing.T) {
t.Parallel()