Skip to content

Migration: the configuration Store (Viper removed)

GTB has moved from the Viper-backed config container (go/config v0.2.0) to the Store architecture (go/config v0.4.0, plus config-afero v0.1.0). This inverts the thesis of the config extraction note: the config module was "the toolkit's Viper layer — it carries the Viper stack so nothing else has to". As of this release nothing carries Viper: github.com/spf13/viper is absent from GTB's dependency graph, and an architecture guard keeps it out.

API changes

  • props.Props.Config is *config.Store. Reads pin a snapshot: props.Config.View().GetString(...). A view is coherent — every value read from it belongs to the same resolved configuration — and goes stale after a reload, so take one per logical operation. props.ViewOrNil pins one safely from a possibly-store-less Props.
  • config.Containable is gone. Read-only call sites take config.Reader (satisfied by *config.View and the published MockReader). Writes go through store.Apply(ctx, config.Set(...)/config.Remove(...)), which edits the target document in place — your comments survive, and only the named keys change.
  • Renames (pre-1.0 clean break, no shims): pkg/http and pkg/grpc *FromContainable*FromReader (grpc also DialLocalFromReader); pkg/gateway's NewFromContainable/RegisterFromContainable are deleted — use NewFromConfig/RegisterFromConfig; pkg/vcs.ConfigFromContainableConfigFromReader; pkg/vcs/repo.SettingsFromContainableSettingsFromReader.
  • setup.Initialiser splits its surfaces: IsConfigured(config.Reader) and Configure(p, setup.Editor). The Editor (View + Set) is backed by the store's Apply, so wizard writes land in the user's file as they happen. setup.Initialise takes a context.Context first; InitOptions.SkipLogin/SkipKey/SkipAI are deleted (they were never read — the --skip-* flags drive the behaviour).
  • setup.DefaultConfig is deleted. Embedded defaults are the assets/config.yaml documents merged across every registered asset bundle; assets/init/config.yaml is the init template written to the user's file. Non-command feature packages announce bundles with setup.RegisterAssets; the root command applies enabled features' bundles at construction, and the framework baseline registers first via props.NewAssets.
  • Watching is explicit in the module (Store.Watch) — the root pre-run wires it (and OnReloadError) for every GTB tool, so downstream tools keep hot reload without code changes. Observers now receive config.Observed.

Deliberate behaviour changes

  1. Embedded defaults always apply. Previously the compiled-in defaults substituted for a missing config file and were ignored once any file existed. Now they are the store's lowest layer: a key absent from your file resolves to the shipped default rather than a zero value, and setting one key no longer costs you every default. (Consequently config validate no longer fails for a missing log.level — the framework default heals it.)
  2. An explicit --config suppresses the project-local .<tool>.yaml layer. The flag was always meant to be authoritative — it replaces the default paths — and a repo-local file the user did not name no longer silently overrides files they did.
  3. Flags are a construction-time layer. Every flag the user actually changed participates in precedence by the hyphen-to-dot convention (author mappings via WithBoundFlags still apply). Net new: --config and --output are now visible as ordinary config keys, and inherited persistent flags participate; a defaulted flag still never overrides configuration.

Two provenance refinements ride along: config validate only warns about unknown keys the user authored (defaults-supplied keys are not the user's to remove), and config migrate-credentials no longer treats a defaults-supplied target key (like the github bundle's auth.env) as "already migrated" — the literal in your file is still migrated out.

Three follow-ups sharpen the write path (see the write-batching spec):

  • Credential wizards commit each storage-mode switch as one transactional write (setup.WriteExclusive via the new Editor.Apply), and stale keys from a prior mode are removed from your file rather than blanked to "". Bitbucket's wizard now clears stale keys at all — previously a mode switch left the old credential behind.
  • config unset validates the layered result, so unsetting a defaults-supplied key (e.g. log.level) now succeeds and falls back to the shipped default instead of being refused.
  • config path --writable answers from the store's Plan routing, so a declared-but-not-yet-created config file is reported as the write target instead of the default fallback.
  • config set warns before writing a recognised credential into a project-local .<tool>.yaml (the committable repo-root layer the store routes to ahead of your private config), and asks to confirm when interactive. It never blocks the write, and re-tightens the written file to 0600.
  • The per-user config now overrides the system /etc config (the default path order was inverted before — /etc won, backwards from convention), and writes land in the user config rather than the un-writable /etc path. Only config files that exist are declared as layers; the highest-precedence path is always available as the write target and is created on first write.
  • Fresh-tool polish: the AI init template no longer seeds empty credential placeholders (they warned "set but empty" on every command); config validate stops flagging recognised framework/tool keys as "unknown" while still catching genuine typos and bad values; and config-independent built-ins (version, changelog, man, docs) run before any config file exists.

Migrating downstream code

You had You write now
props.Config.GetString(k) props.Config.View().GetString(k) (one view per operation)
func F(cfg config.Containable) (read-only) func F(cfg config.Reader)
props.Config.Set(k, v) + write-back props.Config.Apply(ctx, config.Set(k, v))
GetViper() escape hatches Snapshot().Values(), View().Explain(k), Apply
Initialiser.Configure(p, cfg.Set(...)) Configure(p, editor) with editor.Set(...) errors checked
setup.DefaultConfig stanza additions your own assets/config.yaml in the owning package

Regenerated scaffolds pick all of this up automatically — gtb generate emits the Reader/Editor signatures and seeds assets/config.yaml.