forge

package
v0.39.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 24 Imported by: 0

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

View Source
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.

View Source
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

func NewCmdInitBitbucket(p *props.Props) *cobra.Command

NewCmdInitBitbucket creates the `init bitbucket` subcommand.

func NewCmdInitForge added in v0.36.0

func NewCmdInitForge(p *props.Props, profile Profile) *cobra.Command

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

func NewCmdInitGitHub(p *props.Props) *cobra.Command

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

func RunForgeInit(ctx context.Context, p *props.Props, cfg setup.Editor, profile Profile) error

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

func RunForgeInitCmd(ctx context.Context, p *props.Props, dir string, profile Profile) error

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

func RunGitHubInit(ctx context.Context, p *props.Props, cfg setup.Editor) error

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.

func RunGitHubInitCmd

func RunGitHubInitCmd(ctx context.Context, p *props.Props, dir string) error

RunGitHubInitCmd executes the GitHub configuration and writes the results to the config file, which is seeded from the merged init template when absent.

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

func DisplayFor(id props.FeatureID) (Display, bool)

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

func (i *Initialiser) Configure(ctx context.Context, p *props.Props, cfg setup.Editor) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL