Documentation
¶
Overview ¶
Package forge provides a single, provider-parameterised interactive setup initialiser for git forges.
One generic Initialiser is driven by a Profile describing a forge's credential shape. Two profiles are registered:
- GitHub (SingleToken): a token recorded via an env-var reference, the OS keychain, or a literal in config; forge-driven OAuth login with a manual-PAT fallback on headless hosts; and optional SSH key discovery, generation, and upload. Registered with the `init github` command (NewCmdInitGitHub) and an embedded asset bundle.
- Bitbucket (DualUserPass): the dual-credential model (username + app_password) across the same three storage modes — env-var mode records two env-var names, keychain mode stores a single JSON blob, literal mode writes both fields to config. No login, no SSH. Registered with the `init bitbucket` command (NewCmdInitBitbucket).
Runtime credential resolution lives in pkg/vcs and the forge providers; this package handles first-run setup, not API client construction. All three storage modes honour the credential-storage hardening spec: literal mode is refused under CI, and every write commits its mode's keys exclusively so switching modes never leaves a stale secret or reference behind.
Index ¶
- Constants
- func ConfigureSSHKey(profile Profile, props *props.Props, cfg config.Reader, ...) (string, string, error)
- func NewCmdInitBitbucket(p *props.Props) *cobra.Command
- func NewCmdInitForge(p *props.Props, profile Profile) *cobra.Command
- func NewCmdInitGitHub(p *props.Props) *cobra.Command
- func RunBitbucketInit(ctx context.Context, p *props.Props, cfg setup.Editor, opts ...DualFormOption) error
- func RunBitbucketInitCmd(ctx context.Context, p *props.Props, dir string, opts ...DualFormOption) error
- func RunForgeInit(ctx context.Context, p *props.Props, cfg setup.Editor, profile Profile) error
- func RunForgeInitCmd(ctx context.Context, p *props.Props, dir string, profile Profile) error
- func RunGitHubInit(ctx context.Context, p *props.Props, cfg setup.Editor) error
- func RunGitHubInitCmd(ctx context.Context, p *props.Props, dir string) error
- type AuthConfig
- type AuthFormOption
- type ConfigureSSHKeyOption
- type CredentialShape
- type Display
- type DualConfig
- type DualFormOption
- type GenerateKeyOption
- type Initialiser
- type InitialiserOption
- func WithAuthForms(opts ...AuthFormOption) InitialiserOption
- func WithDualForms(opts ...DualFormOption) InitialiserOption
- func WithPrompter(p forgeapi.Prompter) InitialiserOption
- func WithProviderFactory(fn func(context.Context, config.Reader) (forgeapi.Provider, error)) InitialiserOption
- func WithSSHForms(opts ...ConfigureSSHKeyOption) InitialiserOption
- type Profile
Constants ¶
const ( // GithubFeature gates the GitHub single-token setup wizard. GithubFeature = props.FeatureID("github") // GitlabFeature gates the GitLab single-token setup wizard. GitlabFeature = props.FeatureID("gitlab") // GiteaFeature gates the Gitea single-token setup wizard. GiteaFeature = props.FeatureID("gitea") // CodebergFeature gates the Codeberg single-token setup wizard. // // Codeberg is a Forgejo instance and forge-gitea serves both, but it is its // own feature rather than a flag on Gitea's: it resolves from its own // `codeberg` config section, so a token stored for one is not stored for // the other, and gitea.url.api cannot redirect a Codeberg lookup at a // self-hosted instance. See spec 0185 D2. CodebergFeature = props.FeatureID("codeberg") // BitbucketFeature gates the Bitbucket dual-credential setup wizard. BitbucketFeature = props.FeatureID("bitbucket") )
Feature identities for the forge integrations this package registers.
They carry no "Cmd" suffix: unlike the framework's built-ins, a forge feature does not exist to gate a command. It gates an initialiser, a config section and an embedded asset bundle — the `init <forge>` subcommand it also contributes is incidental. See spec 0184 D10.
const PackagePath = "gitlab.com/phpboyscout/go-tool-base/pkg/setup/forge"
PackagePath is this package's import path — the ConstPackage its feature descriptors carry, so generated source qualifies these constants against the package that actually declares them rather than assuming props.
Variables ¶
This section is empty.
Functions ¶
func ConfigureSSHKey ¶
func ConfigureSSHKey(profile Profile, props *props.Props, cfg config.Reader, opts ...ConfigureSSHKeyOption) (string, string, error)
ConfigureSSHKey runs the interactive SSH key configuration flow.
func NewCmdInitBitbucket ¶
NewCmdInitBitbucket creates the `init bitbucket` subcommand.
func NewCmdInitForge ¶ added in v0.36.0
NewCmdInitForge creates the `init <forge>` subcommand for a single-token forge, deriving its wording from the profile so a new forge contributes a command by existing rather than by adding a builder.
func NewCmdInitGitHub ¶
NewCmdInitGitHub creates the `init github` subcommand.
func RunBitbucketInit ¶
func RunBitbucketInit(ctx context.Context, p *props.Props, cfg setup.Editor, opts ...DualFormOption) error
RunBitbucketInit executes the wizard against an existing config container, typically invoked by NewCmdInitBitbucket. Optional [DualFormOption]s are propagated into the wizard so tests can inject deterministic form creators, mirroring pkg/setup/ai.RunAIInit.
func RunBitbucketInitCmd ¶
func RunBitbucketInitCmd(ctx context.Context, p *props.Props, dir string, opts ...DualFormOption) error
RunBitbucketInitCmd materialises the target config (seeded from the merged init template when absent) and runs the wizard over it; writes land in the file as they are applied, with 0600 permissions. Optional [DualFormOption]s are propagated into the wizard so tests can inject deterministic form creators, mirroring pkg/setup/ai.RunAIInit.
func RunForgeInit ¶ added in v0.36.0
RunForgeInit runs a single-token forge's wizard against an existing config container. ctx is the command context: it carries no deadline of its own so a device-flow login can run at human pace, while Ctrl-C aborts an in-flight poll.
func RunForgeInitCmd ¶ added in v0.36.0
RunForgeInitCmd executes a single-token forge's configuration and writes the results to the config file, which is seeded from the merged init template when absent.
func RunGitHubInit ¶
RunGitHubInit forcibly runs both login and SSH configuration regardless of current state. This is used by the explicit `init github` command. ctx is the command context: it carries no deadline of its own so the OAuth device flow can run at human pace, while Ctrl-C aborts an in-flight poll.
Types ¶
type AuthConfig ¶
type AuthConfig struct {
// StorageMode is set by the storage-mode selector. Defaults to
// [credentials.ModeEnvVar] when the form presents the choice.
StorageMode credentials.Mode
// EnvVarName is the env var NAME recorded under <prefix>.auth.env in
// env-var mode. Ignored in keychain/literal modes.
EnvVarName string
// FetchToken is true when the user wants the wizard to run OAuth (or the
// manual fallback) on their behalf. Only relevant in env-var mode —
// keychain/literal always need a token.
FetchToken bool
// Token is the captured token from OAuth / manual entry. Cleared after it
// has been written (or displayed for env-var mode) so it does not linger in
// the AuthConfig longer than necessary.
Token string
}
AuthConfig captures the single-token wizard's output from each stage. All fields are populated incrementally so test-injected form creators can override any subset and the runner still produces a coherent config.
type AuthFormOption ¶
type AuthFormOption func(*authFormConfig)
AuthFormOption configures form creators for the single-token auth wizard. Used by tests to inject deterministic form-answering creators without driving a real TTY.
func WithAuthForm ¶
func WithAuthForm( creator func(*AuthConfig) []*huh.Form, displayOnceCreator func(envVarName, token string) *huh.Form, ) AuthFormOption
WithAuthForm injects custom form creators into the single-token flow for testability. The creator returns forms in order:
[0] storage-mode selector [1] env-var name input [2] "fetch token now?" confirm [3] display-once token view (takes envVarName, token)
Returning fewer forms is allowed — the runner skips stages whose slot is nil or absent. The display-once creator has a different signature because it needs the captured token passed in.
type ConfigureSSHKeyOption ¶
type ConfigureSSHKeyOption func(*configureSSHKeyConfig)
ConfigureSSHKeyOption is a functional option for ConfigureSSHKey.
func WithGenerateKeyOptions ¶
func WithGenerateKeyOptions(opts ...GenerateKeyOption) ConfigureSSHKeyOption
WithGenerateKeyOptions passes options through to the key generation step.
func WithSSHKeyPathForm ¶
func WithSSHKeyPathForm(creator func(*string) *huh.Form) ConfigureSSHKeyOption
WithSSHKeyPathForm overrides the SSH key path input form (for testing).
func WithSSHKeySelectForm ¶
func WithSSHKeySelectForm(creator func(*string, []huh.Option[string]) *huh.Form) ConfigureSSHKeyOption
WithSSHKeySelectForm overrides the SSH key selection form (for testing).
type CredentialShape ¶
type CredentialShape int
CredentialShape discriminates the credential layout a forge setup wizard captures: a single API token, or a username + app-password pair.
const ( // SingleToken is a one-credential wizard (e.g. GitHub): three storage // modes over a single token, optional forge-driven login and SSH upload. SingleToken CredentialShape = iota // DualUserPass is a two-credential wizard (e.g. Bitbucket): three storage // modes over a username + app-password pair, no login, no SSH. DualUserPass )
type Display ¶ added in v0.36.0
type Display struct {
// ID is the forge's feature identity, and the value --git-backend takes.
ID props.FeatureID
// Label is the human-facing forge name (e.g. "GitLab").
Label string
// Host is the default host, empty for a forge that has none. Gitea is the
// case in point: every instance is self-hosted.
Host string
// RepoDescription and RepoPlaceholder describe the repository path this
// forge expects.
RepoDescription string
RepoPlaceholder string
// NestedNamespaces marks a forge whose owner segment may contain slashes,
// as GitLab subgroups do.
NestedNamespaces bool
}
Display is the subset of a forge's definition that user-facing choosers need — most immediately the project generator's git-backend selector.
It exists so callers outside this package can render a forge without Profile becoming public API. A Profile carries config-key layout, credential shape, fallback env vars and token-creation URLs; none of that is any of the generator's business, and exporting it to supply four display strings would freeze the whole surface. See spec 0185 D5.
func DisplayFor ¶ added in v0.36.0
DisplayFor returns the display data for a registered forge feature, and reports whether one exists. It is the only route from a feature ID to a forge's presentation, so a caller cannot half-know a forge.
func Displays ¶ added in v0.36.0
func Displays() []Display
Displays returns every registered forge's display data, in the registry's deterministic order. A chooser built from this cannot offer a forge with no initialiser behind it, nor omit one that has an initialiser — which is how the generator came to offer GitLab, which had no credential path, while hiding Bitbucket, which did.
type DualConfig ¶
type DualConfig struct {
StorageMode credentials.Mode
// Username and AppPassword hold the collected credentials for keychain and
// literal modes. Unused in env-var mode.
Username string
AppPassword string
// UsernameEnvName and AppPasswordEnvName hold env-var names for env-var
// mode. Default to the profile's fallback env-var names.
UsernameEnvName string
AppPasswordEnvName string
}
DualConfig captures the dual-credential wizard's outputs. Fields unused by the selected storage mode are ignored.
type DualFormOption ¶
type DualFormOption func(*dualFormConfig)
DualFormOption configures the dual-credential init form for testability.
func WithDualForm ¶
func WithDualForm(creator func(*DualConfig) []*huh.Form) DualFormOption
WithDualForm injects custom form creators into the dual-credential wizard for testing. The creator returns forms in order:
[0] storage-mode selector [1] env-var names (env-var mode only) [2] username + app_password inputs (keychain / literal modes)
Returning fewer forms is allowed — the runner skips stages whose slot is nil or absent.
type GenerateKeyOption ¶
type GenerateKeyOption func(*generateKeyConfig)
GenerateKeyOption is a functional option for SSH key generation.
func WithKeyManager ¶
func WithKeyManager(factory func(context.Context, config.Reader) (forgeapi.KeyManager, error)) GenerateKeyOption
WithKeyManager overrides the forgeapi.KeyManager constructor used when uploading SSH keys. Tests pass a factory returning a fake; production callers omit it to get the registered provider's key-upload capability.
func WithPassphraseForm ¶
func WithPassphraseForm(creator func(*string) *huh.Form) GenerateKeyOption
WithPassphraseForm overrides the passphrase input form (for testing).
func WithUploadConfirmForm ¶
func WithUploadConfirmForm(creator func(*bool) *huh.Form) GenerateKeyOption
WithUploadConfirmForm overrides the upload confirmation form (for testing).
type Initialiser ¶
type Initialiser struct {
// SkipLogin suppresses the login stage of a single-token profile; it is
// ignored by dual-credential profiles, which have no login step.
//
// SkipKey suppresses the SSH stage for any profile that offers it,
// whatever its credential shape.
SkipLogin bool
SkipKey bool
// contains filtered or unexported fields
}
Initialiser is the single, profile-parameterised setup.Initialiser shared by every forge setup wizard. The Profile it carries selects the single-token or dual-credential flow and supplies every provider-specific value; the injectable seams below let tests drive the flow without a real forge, keychain, or TTY.
func New ¶
func New(_ *props.Props, profile Profile, opts ...InitialiserOption) *Initialiser
New constructs a profile-driven Initialiser with production defaults (the registered forge provider and the CLI prompter) and applies opts.
func NewBitbucketInitialiser ¶
func NewBitbucketInitialiser(p *props.Props, opts ...InitialiserOption) *Initialiser
NewBitbucketInitialiser builds the dual-credential Bitbucket initialiser.
func NewGitHubInitialiser ¶
func NewGitHubInitialiser(p *props.Props, skipLogin, skipKey bool, opts ...InitialiserOption) *Initialiser
NewGitHubInitialiser builds the single-token GitHub initialiser. Its asset bundle is registered from init() via setup.RegisterAssets, applied for enabled features at root construction.
func (*Initialiser) Configure ¶
Configure runs the interactive wizard for the profile's credential shape and then, for a profile that offers SSH, the key stage.
The SSH stage runs here rather than inside a per-shape branch: whether a forge is offered a key is a property of the profile, not a consequence of how many fields its credential has. It runs after credential capture because an upload is authorised by the credential just captured — Bitbucket's UploadKey needs the username and app password, so the order is a requirement rather than an accident of where the call sat.
ctx is the caller's context — it deliberately carries no stage-wide deadline (see setup.Initialiser); keychain operations derive their own KeychainOpTimeout bounds at each call site.
func (*Initialiser) IsConfigured ¶
func (i *Initialiser) IsConfigured(cfg config.Reader) bool
IsConfigured reports whether the profile's credential — and, for any profile that offers it, SSH — is already present in the config.
func (*Initialiser) Name ¶
func (i *Initialiser) Name() string
Name returns the human-readable label for this initialiser.
type InitialiserOption ¶
type InitialiserOption func(*Initialiser)
InitialiserOption configures an Initialiser.
func WithAuthForms ¶
func WithAuthForms(opts ...AuthFormOption) InitialiserOption
WithAuthForms propagates [AuthFormOption]s into the single-token wizard. Tests use this to inject deterministic form creators via WithAuthForm.
func WithDualForms ¶
func WithDualForms(opts ...DualFormOption) InitialiserOption
WithDualForms propagates [DualFormOption]s into the dual-credential wizard. Tests use this to inject deterministic form creators via WithDualForm.
func WithPrompter ¶
func WithPrompter(p forgeapi.Prompter) InitialiserOption
WithPrompter overrides the forgeapi.Prompter that renders the device-code step. Tests pass a no-op prompter; production callers omit it to get the default CLI prompter.
func WithProviderFactory ¶
func WithProviderFactory(fn func(context.Context, config.Reader) (forgeapi.Provider, error)) InitialiserOption
WithProviderFactory overrides the forge provider constructor used for interactive login. Tests pass a factory returning a fake provider (optionally implementing forgeapi.Authenticator); production callers omit it to get the registered provider.
func WithSSHForms ¶ added in v0.36.0
func WithSSHForms(opts ...ConfigureSSHKeyOption) InitialiserOption
WithSSHForms propagates [ConfigureSSHKeyOption]s into the SSH stage. The stage's form creators and key-manager factory were already injectable, but unreachable from Configure — so the stage could only be driven directly, not as part of the wizard it actually runs in.
type Profile ¶
type Profile struct {
// Provider is the forge registry key passed to [forgeapi.Lookup]
// (e.g. "github").
Provider string
// ConfigPrefix is the config-key namespace (e.g. "github" →
// "github.auth.env"). It reproduces each provider's existing key layout
// exactly so pre-existing configs keep resolving.
ConfigPrefix string
// Label is the human-facing provider name used in prompts and errors
// (e.g. "GitHub").
Label string
// DisplayName is the initialiser's Name() label shown by the setup
// runner (e.g. "GitHub integration").
DisplayName string
// Feature is the feature flag that gates this initialiser.
Feature props.FeatureID
// Host is the default web host: the manual-token URL host and the forge
// provider's release-source host.
Host string
// KeychainAccount is the account portion of the "<service>/<account>"
// keychain reference (e.g. "github.auth").
KeychainAccount string
// Credential selects the single-token or dual-credential flow.
Credential CredentialShape
// FallbackEnv is the well-known token env var used by the single-token
// flow's already-configured detection and env-var-name default
// (e.g. "GITHUB_TOKEN"). SingleToken only.
FallbackEnv string
// OffersSSH enables the SSH key discovery/generation/upload stage.
//
// It is a capability claim — "this forge can accept an SSH key" — not a
// statement about the credential shape. The stage takes a Profile and
// nothing else, and runs from Configure for either shape.
OffersSSH bool
// OffersLogin enables the forge [forgeapi.Authenticator] login attempt
// before the manual-token fallback. SingleToken only.
OffersLogin bool
// TokenCreateURLTemplate is the provider's "create a personal access token"
// URL, with the literal "{host}" standing in for Host (so an Enterprise host
// is substituted). Empty degrades the manual-token wizard to a generic
// "create a personal access token on <host>" message — forge-specific URL
// paths and scope names must live here, not hard-coded in the wizard.
// SingleToken only.
TokenCreateURLTemplate string
// TokenScopes is the human-readable scope list shown alongside the URL
// (e.g. "repo, read:org, gist"). Empty omits the line. SingleToken only.
TokenScopes string
// UserFallbackEnv / PassFallbackEnv are the well-known env-var-name
// defaults for the dual flow (e.g. "BITBUCKET_USERNAME" /
// "BITBUCKET_APP_PASSWORD"). DualUserPass only.
UserFallbackEnv string
PassFallbackEnv string
// LoginClientID is an OAuth application client ID this framework ships for
// the forge's device-flow login, applied only when the resolved API host
// matches LoginClientIDHost and the user's config names no client ID of
// its own.
//
// It is deliberately NOT shipped in the forge's embedded config bundle,
// which would be simpler. An embedded default always populates the key, so
// the adapter's Settings.ClientID would never be empty — which would (a)
// dead-letter the provider's own well-known env-var fallback, and (b) send
// a gitlab.com client ID to a self-hosted instance, where login fails as an
// invalid client instead of degrading to the manual-token path. Gating on
// the host keeps both behaviours intact. See spec 0185 D8.
LoginClientID string
// LoginClientIDHost is the API host LoginClientID is registered against.
// Empty means "never apply it".
LoginClientIDHost string
// RepoDescription and RepoPlaceholder are the repository-field help text
// and placeholder the project generator shows for this forge. They live
// here, beside the forge's other definitional data, so adding a forge does
// not also mean editing a table in internal/cmd/generate — see spec 0185
// D5. They reach the generator through [DisplayFor], not through Profile
// itself, which stays unexported.
RepoDescription string
RepoPlaceholder string
// NestedNamespaces marks a forge whose owner segment may itself contain
// slashes (GitLab subgroups). It selects the namespace-validation rules
// and is the one structural difference between forges' repository paths.
NestedNamespaces bool
}
Profile describes a forge's interactive setup shape. One generic Initialiser is parameterised by a Profile so GitHub and Bitbucket (and any future forge) share the storage-mode wizard, the single-credential-key exclusivity invariant, and the config-write plumbing — differing only in the per-provider values collected here.