Skip to content

v0.x: setup.Initialiser.Configure gains a context parameter

What changed

The 2026-07-23 architectural review found (CRITICAL) that the forge setup wizard ran its entire interactive credential stage, storage-mode forms, the OAuth device flow, and the keychain write, under one 5-second credentials.KeychainOpTimeout context, so interactive OAuth login always timed out and silently fell back to manual token entry (spec 2026-07-23-setup-credential-stage-context-scoping).

The fix threads the command context through the initialiser seam and scopes keychain deadlines to individual operations. Three public signatures changed:

Before After
Initialiser.Configure(p *props.Props, cfg Editor) error Configure(ctx context.Context, p *props.Props, cfg Editor) error
forge.RunGitHubInit(p, cfg) forge.RunGitHubInit(ctx, p, cfg)
forge.RunBitbucketInit(p, cfg, opts...) forge.RunBitbucketInit(ctx, p, cfg, opts...)

setup.Initialise, forge.RunGitHubInitCmd, and forge.RunBitbucketInitCmd already accepted a context and now forward it instead of dropping it.

Migrating a custom Initialiser

Add the context parameter to your Configure implementation. The contract:

  • The caller's context carries no deadline: interactive stages run at human pace, and cancellation (Ctrl-C) must abort in-flight work.
  • Derive a fresh context.WithTimeout(ctx, credentials.KeychainOpTimeout) at each keychain/backend call site, never spanning a form or OAuth stage.
func (i *MyInitialiser) Configure(ctx context.Context, p *props.Props, cfg setup.Editor) error {
    // forms, prompts ... (ctx unbounded)

    storeCtx, cancel := context.WithTimeout(ctx, credentials.KeychainOpTimeout)
    defer cancel()

    return credentials.Store(storeCtx, toolName, account, secret)
}

Callers that invoked Configure directly should pass the cobra command's context (cmd.Context()); in tests, t.Context().

Scaffolded initialisers

Projects scaffolded with --with-initializer before this fix do not compile

The generator's initialiser template was not updated alongside the interface, so gtb generate command --with-initializer emitted a Configure without the context parameter. The resulting init.go fails to build:

*FooInitialiser does not implement setup.Initialiser
  have Configure(*props.Props, setup.Editor) error
  want Configure(context.Context, *props.Props, setup.Editor) error

This is fixed. Re-run gtb generate command --with-initializer (or gtb regenerate) to refresh init.go.

The generated Init<Name> stub in main.go now takes the context as its first parameter and receives it from Configure, matching the PreRun<Name> stub:

func InitFoo(ctx context.Context, p *props.Props, cfg setup.Editor) error {
    // ...
}

If you already implemented an Init<Name> body against the old signature, add the parameter: regeneration rewrites the generated init.go that calls it, but main.go is yours and is not overwritten.