Skip to content

feat(config): scope state directory by release channel - #3467

Draft
Dave Shoup (shouples) wants to merge 2 commits into
djs/migrate-mocker-to-mockgenfrom
djs/channel-state-dir
Draft

feat(config): scope state directory by release channel#3467
Dave Shoup (shouples) wants to merge 2 commits into
djs/migrate-mocker-to-mockgenfrom
djs/channel-state-dir

Conversation

@shouples

Copy link
Copy Markdown
Contributor

Release Notes

No user-facing changes for published releases. A GA (stable) build keeps its state in ~/.confluent exactly as before; only locally-built binaries move to their own directory today (with a prerelease directory reserved for when the pipeline can produce one - see What).

Checklist

  • I have successfully built and used a custom CLI binary, without linter issues from this PR.
  • I have clearly specified in the What section below whether this PR applies to Confluent Cloud, Confluent Platform, or both.
  • I have verified this PR in Confluent Cloud pre-prod or production environment, if applicable.
  • I have verified this PR in Confluent Platform on-premises environment, if applicable.
  • I have attached manual CLI verification results or screenshots in the Test & Review section below.
  • I have added appropriate CLI integration or unit tests for any new or updated commands and functionality.
  • I confirm that this PR introduces no breaking changes or backward compatibility issues.
  • I have indicated the potential customer impact if something goes wrong in the Blast Radius section below.
  • I have put checkmarks below confirming that the feature associated with this PR is enabled in:
    • Confluent Cloud prod
    • Confluent Cloud stag
    • Confluent Platform
    • Check this box if the feature is enabled for certain organizations only

Applies to both Cloud and Platform. Feature-flag items are N/A - this is not a gated feature, and a stable build's behavior is unchanged.

What

A locally-built or prerelease CLI shares its state directory (~/.confluent) with an installed GA release, so logins, contexts, and cached tokens overwrite each other. Testing a dev build could log you out of your real CLI, and vice versa. This PR derives the state directory from the build's release channel so the three kinds of binary self-isolate.

The channel is derived automatically from the version string the linker stamps into the binary - not a flag, environment variable, or make target anyone sets. main() classifies it via ChannelOf before the config load or command construction, since both resolve the state-dir path:

  • stable - a clean release version like 4.73.0 -> ~/.confluent (unchanged, so existing installs are unaffected)
  • prerelease - a published pre-release tag like 4.73.0-rc1 -> ~/.confluent-prerelease
  • dev - anything the release pipeline didn't produce: an unstamped 0.0.0 binary, any 0.x version, or a make build snapshot (...-SNAPSHOT-<sha>) -> ~/.confluent-dev

The prerelease channel isn't reachable yet - no -rc/preview tag exists in this repo, and the release pipeline doesn't emit one today - but the classifier is ready for when it does. Why prerelease needs its own bucket instead of folding into dev, and the RC-cycle edge case that forces it, is in the collapsed section below.

Along the way, config.StateDir() now returns an error instead of silently writing state into the working directory when the home directory can't be resolved, and one of three hardcoded ~/.confluent/config.json help strings is genericized (the other two are deferred cleanup).

Applies to: both Confluent Cloud and Confluent Platform.

Why three channels instead of just prod / non-prod?

They map to three audiences whose state must not mix: production users, testers validating a published candidate, and the developer building locally. Folding prerelease into dev breaks during an RC cycle: make build stamps a SNAPSHOT suffix onto the RC's own version (e.g. 4.73.0-rc1-SNAPSHOT-<sha>), so without a separate dev bucket that local build would land in the same directory as the published candidate a tester relies on. channel_test.go covers this case directly.

Blast Radius

The risk concentrates in the channel classifier: if it misclassified a stable release as prerelease or dev, that release would look for state in the wrong directory and behave as if freshly installed - customers would appear logged out and lose their configured contexts until they re-ran confluent login. This is why stable maps to the historical path with an empty suffix and is covered by pkg/version/channel_test.go, and why the classifier errs toward isolating an unfamiliar build rather than assuming stable. No data is deleted; the old directory is left intact.

References

  • Stacked on fix-log-flush; the base of the channel-state / dev-build work.

Test & Review

  • pkg/version/channel_test.go covers the classifier across stable, prerelease, snapshot/dev, and malformed version strings (including the -rc1-SNAPSHOT case a release-candidate make build produces).
  • pkg/config/config_test.go covers StateDir() / StateDirName(), including the stable path staying .confluent and the error path when the home directory is unresolvable.
  • test/channel_state_test.go is a new integration test asserting the running binary resolves its state directory by channel.
  • Manual: make build (a dev binary) writes to ~/.confluent-dev, leaving an installed release's ~/.confluent untouched.
  • go build ./... and go test ./pkg/version/... ./pkg/config/... pass.

@confluent-cla-assistant

Copy link
Copy Markdown

🎉 All Contributor License Agreements have been signed. Ready to merge.
Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.

A dev or prerelease binary that shares ~/.confluent with a production
install can read and overwrite its contexts and credentials. Classify the
build's channel from the version stamped in at link time and derive the
state directory from it: stable keeps ~/.confluent unchanged, a prerelease
uses ~/.confluent-prerelease, and any local build uses ~/.confluent-dev.
Config, the managed plugins directory, and Flink statement history all
follow it.

Stable-channel behavior is unchanged, so installed CLIs are unaffected.

Touches pkg/version, pkg/config, and pkg/plugin (restricted zones) by
necessity; the mechanism has nowhere else to live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 21, 2026 21:38
@shouples
Dave Shoup (shouples) changed the base branch from djs/fix-log-flush to djs/migrate-mocker-to-mockgen August 21, 2026 21:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Scopes CLI state by release channel while preserving ~/.confluent for stable releases.

Changes:

  • Adds stable, prerelease, and dev channel classification.
  • Applies channel-specific paths to config, plugins, and Flink history.
  • Adds tests, integration coverage, and updated help output.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Summary
test/live/live_test.go Uses channel-aware test state paths.
test/fixtures/output/configuration/list-help.golden Updates configuration help output.
test/fixtures/output/configuration/list-help-onprem.golden Updates on-premises help output.
test/fixtures/output/configuration/help.golden Updates configuration command help.
test/fixtures/output/configuration/help-onprem.golden Updates on-premises configuration help.
test/channel_state_test.go Adds stamped-build state isolation tests.
pkg/version/channel.go Implements release-channel classification.
pkg/version/channel_test.go Tests channel classification behavior.
pkg/plugin/plugin.go Discovers plugins in channel-specific directories.
pkg/flink/internal/history/history.go Scopes Flink history by channel.
pkg/flink/config/env_variables.go Updates Flink path configuration. Moderate (2 votes): retain the deprecated exported HomeConfluentPathDefault alias for source compatibility.
pkg/config/config.go Adds channel-aware state directory helpers. Nit (3 votes): add coverage for the StateDir error branch.
pkg/config/config_test.go Tests channel-specific configuration paths.
internal/plugin/command_search.go Uses the channel state directory for temporary files.
internal/plugin/command_install.go Installs plugins under the channel state directory.
internal/configuration/command_list.go Genericizes the hardcoded configuration path in help.
cmd/whitelist/main.go Uses the stable channel for generated whitelist data.
cmd/lint/main.go Uses the stable channel for linting.
cmd/docs/main.go Uses the stable channel for documentation generation.
cmd/confluent/main.go Initializes the channel before configuration loading.
Suppressed comments (8)

pkg/config/config.go:674

  • Config.Load still obtains its default filename through this helper (Config.GetFilename), and GetDefaultFilename ignores os.UserHomeDir errors. With HOME unset, it returns .confluent-dev/config.json relative to the working directory; Load then calls Save, which creates and writes that path. Thus normal CLI startup still silently stores state in the working directory despite the new StateDir error path. Propagate the home-directory error through config initialization/loading instead of retaining this fallback.
func GetDefaultFilename() string {
	home, _ := os.UserHomeDir()
	return filepath.Join(home, StateDirName(), "config.json")

pkg/flink/internal/history/history.go:67

  • The history tests only assert that the paths are non-empty, so they do not verify this new channel-dependent default. Please add a focused test for initPath under at least the dev and stable channels (while preserving the HOME_CONFLUENT_PATH override) to catch Flink history being written to the wrong state directory.
	confluentDir := os.Getenv(config.HomeConfluentPathEnvVar)
	if confluentDir == "" {
		confluentDir = pconfig.StateDirName()
	}

pkg/plugin/plugin.go:42

  • The existing TestSearchPath only verifies plugins found through PATH; it never exercises the new channel-scoped stateDir/plugins entry added here. A regression could make installed plugins undiscoverable while the suite remains green, so please add a test that creates a plugin under config.StateDir()/plugins and verifies it is returned.
		pluginDir := filepath.Join(stateDir, "plugins")
		log.CliLogger.Debugf("Searching $PATH and %s for plugins. Plugins can be disabled in %s.", pluginDir, cfg.GetFilename())
		if !slices.Contains(pathDirList, pluginDir) {
			pathDirList = append(pathDirList, pluginDir)
		}

pkg/version/channel.go:40

  • The default branch maps every unknown Channel value to the stable suffix, so SetProcessChannel(Channel(99)) (or a newly added enum value before this switch is updated) silently shares .confluent with production. That violates the isolation rule and makes an unfamiliar value risk customer state; handle Stable explicitly and make the default fall back to the dev suffix (or reject invalid values).
	default:
		return ""

pkg/version/channel.go:68

  • strings.Contains classifies any prerelease containing the substring snapshot as a local build. A valid published label such as 4.73.0-snapshot-review or 4.73.0-not-snapshot.1 would therefore use .confluent-dev instead of .confluent-prerelease, contrary to the classifier's documented rule. Match SNAPSHOT as a delimiter-bounded token in the goreleaser form instead.
	case strings.Contains(strings.ToUpper(prerelease), snapshotMarker):

test/channel_state_test.go:64

  • On Windows this go build -o target has no .exe suffix. The repository's Windows integration target explicitly builds test/bin/confluent.exe (Makefile:109), so exec.Command(binary, ...) can fail to locate the generated PE binary here. Give the temporary output a platform-appropriate executable suffix.
	binary := filepath.Join(t.TempDir(), "confluent")

test/channel_state_test.go:69

  • The stamped binaries leave main.isTest false. Both the default version invocation and configuration update run notifyIfUpdateAvailable (pkg/cmd/prerunner.go:109), so each test can call the real update service and mutate LastUpdateCheckAt in the temporary config. Build these fixtures with -X main.isTest=true (as the normal integration build does) to keep this test hermetic.
	args := []string{"build", "-o", binary}
	if version != "" {
		args = append(args, "-ldflags=-X main.version="+version)
	}
	args = append(args, "../cmd/confluent")

test/channel_state_test.go:69

  • This helper assumes the test process's current directory is test, but TestCLI.SetupSuite changes the shared process directory to the repository root at test/cli_test.go:85 and does not restore it. If this top-level test runs after TestCLI, ../cmd/confluent resolves outside the checkout and the build fails; derive the repository root independently of the current working directory or restore the directory after the suite.
	args = append(args, "../cmd/confluent")

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/config/config.go
Comment on lines +660 to +665
home, err := os.UserHomeDir()
if err != nil {
return "", errors.NewErrorWithSuggestions(
fmt.Sprintf("unable to determine the home directory holding the CLI's state: %v", err),
"Set the `HOME` environment variable (`USERPROFILE` on Windows) to a writable directory.",
)
Comment on lines +3 to 5
// Overrides the directory under $HOME that Flink statement history is written to. The default is
// no longer a constant here: it follows the build's release channel, via config.StateDirName.
const HomeConfluentPathEnvVar = "HOME_CONFLUENT_PATH"
@shouples
Dave Shoup (shouples) changed the base branch from djs/migrate-mocker-to-mockgen to main August 21, 2026 21:50
@shouples
Dave Shoup (shouples) changed the base branch from main to djs/migrate-mocker-to-mockgen August 21, 2026 21:51
@sonarqube-confluent

Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
72.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube

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