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.Configis*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.ViewOrNilpins one safely from a possibly-store-lessProps.config.Containableis gone. Read-only call sites takeconfig.Reader(satisfied by*config.Viewand the publishedMockReader). Writes go throughstore.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/httpandpkg/grpc*FromContainable→*FromReader(grpc alsoDialLocalFromReader);pkg/gateway'sNewFromContainable/RegisterFromContainableare deleted — useNewFromConfig/RegisterFromConfig;pkg/vcs.ConfigFromContainable→ConfigFromReader;pkg/vcs/repo.SettingsFromContainable→SettingsFromReader. setup.Initialisersplits its surfaces:IsConfigured(config.Reader)andConfigure(p, setup.Editor). TheEditor(View+Set) is backed by the store'sApply, so wizard writes land in the user's file as they happen.setup.Initialisetakes acontext.Contextfirst;InitOptions.SkipLogin/SkipKey/SkipAIare deleted (they were never read — the--skip-*flags drive the behaviour).setup.DefaultConfigis deleted. Embedded defaults are theassets/config.yamldocuments merged across every registered asset bundle;assets/init/config.yamlis the init template written to the user's file. Non-command feature packages announce bundles withsetup.RegisterAssets; the root command applies enabled features' bundles at construction, and the framework baseline registers first viaprops.NewAssets.- Watching is explicit in the module (
Store.Watch) — the root pre-run wires it (andOnReloadError) for every GTB tool, so downstream tools keep hot reload without code changes. Observers now receiveconfig.Observed.
Deliberate behaviour changes¶
- 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 validateno longer fails for a missinglog.level— the framework default heals it.) - An explicit
--configsuppresses the project-local.<tool>.yamllayer. 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. - Flags are a construction-time layer. Every flag the user actually
changed participates in precedence by the hyphen-to-dot convention (author
mappings via
WithBoundFlagsstill apply). Net new:--configand--outputare 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.WriteExclusivevia the newEditor.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 unsetvalidates 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 --writableanswers from the store'sPlanrouting, so a declared-but-not-yet-created config file is reported as the write target instead of the default fallback.config setwarns 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 to0600.- The per-user config now overrides the system
/etcconfig (the default path order was inverted before —/etcwon, backwards from convention), and writes land in the user config rather than the un-writable/etcpath. 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 validatestops 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.