Implementation notes β provider-aware repository auth¶
Spec: 2026-06-12-provider-aware-repo-auth
Branch: feat/provider-aware-repo-auth
What was implemented¶
D1 β Forge dimension (per-forge subtrees)¶
resolveForge(pkg/vcs/repo/repo.go) derives the forge fromTool.ReleaseSource.Type, lowercased/trimmed, overridable by thevcs.providerconfig key (mirroringpkg/setup/update.go). Empty ordirectfalls back togithub(O4:directhas no git remote, andgithubpreserves the pre-change default).NewReposelects<forge>.sshvs token auth on the resolved forge instead of hard-codedgithub.*.configureTokenAuthresolves via the sharedvcs.ResolveToken(p.Config.Sub(forge), "<FORGE>_TOKEN")primitive β same chain as before (auth.envβauth.keychainβauth.valueβ fallback env). Thegithubvcs.GetGitHubTokendependency was dropped from the repo layer (it hard-errored on missing tokens, conflicting with D2;GetGitHubTokenitself is unchanged for its other callers).- Basic-auth usernames are forge-mapped:
x-access-token(GitHub + unknown forges),oauth2(GitLab),x-token-auth(Bitbucket).
D2 β Missing auth non-fatal for public repos¶
- No credential +
ReleaseSource.Private: falseβ DEBUG log,nilauth, no error. - No credential +
Private: trueβ error with a hint naming<FORGE>_TOKENand<forge>.auth.env(mirrorsrequireReleaseTokenin the release layer).
D3 β SSH auth generalised and library-pure¶
configureSSHAuthreads<forge>.ssh.key(subtree-nil guard from MR !63 retained, log messages generalised with aforgekeyval).GetSSHKeyno longer imports or invokeshuh. Passphrase detection useserrors.Asagainst the typed*gossh.PassphraseMissingError; the error is wrapped with a hint and propagates out ofNewRepo.- New additive API
GetSSHKeyWithPassphrase(filePath, fs, passphrase)for callers that obtained the passphrase.GetSSHKey(path, fs)is now a zero-passphrase shim over it β signature unchanged.
D4 β Guards on all RepoLike methods¶
ErrNoWorktreeguards:Checkout,CheckoutCommit,Commit,CreateBranch(tree leg).ErrNoRepositoryguards:Push,CreateBranch,CreateRemote,Remote, and (via a new sharedheadTreehelper)WalkTree,FileExists,DirectoryExists,GetFile.AddToFSneeds no guard (does not touchr.repo/r.tree).ThreadSafeRepoinherits all guards by delegation.RepoLikeitself is unchanged β no mock regeneration was needed.
Tests¶
pkg/vcs/repo/provider_auth_unit_test.go: table-driven forge/token matrix (GitLab subtree selection, GitHub back-compat,vcs.provideroverride, public-no-token, private-no-token errors, gitea fallback env, bitbucket username, empty/directdefaults), per-forge SSH subtree selection, typed-passphrase tests (encrypted ed25519 fixture generated viagossh.MarshalPrivateKeyWithPassphrase), and a no-panic sentinel sweep over everyRepoLikemethod on bothRepoandThreadSafeRepo.TestForgeAuthClonePush(integration,INT_TEST_VCS=1): clone/commit/push against a local bare remote with GitHub- and GitLab-shaped token configs.- Updated
TestRepo_Unit_NewRepo_TokenAuthFailsto setPrivate: true(the old expectation contradicted D2).
Docs¶
docs/components/vcs/repo.mdβ forge-aware auth table, non-fatal-public behaviour,GetSSHKeyWithPassphrase, CLI-prompt responsibility, unopened-repo contract.docs/concepts/vcs-repositories.mdβ forge-aware auth table + scope-boundary note (repo auth β release auth).docs/migration/v0.16-repo-provider-auth.md(+ index row) β the two behaviour changes.pkg/vcs/repo/doc.goβ package comment rewritten (it previously described URL parsing, which this package never did).
Deviations from the spec¶
- None substantive. One judgement call: the spec's "private repos require/enforce a token" is enforced at
NewRepotime on the token path only. An SSH-configured private repo is not pre-checked (SSH agent presence can't be cheaply validated), matching the previous behaviour. CreateBranchwas lightly refactored (branchExistshelper) to stay under the cyclop threshold after adding the guards.
Breaking-change assessment¶
No public signature changed. Two behaviour changes, called out with a BREAKING CHANGE: footer in the commit and a migration note:
GetSSHKeyno longer interactively prompts for passphrases (returns the typed error instead).NewRepono longer errors when no token is configured for a public repository.
Open questions for review¶
- No CLI call site for the passphrase prompt was found. Nothing in this repository (commands, setup, init, train) calls
GetSSHKeyor hits the encrypted-key path β thehuhprompt was effectively dead interactive code inside the library. I therefore did not relocate the prompt anywhere; the typed error +GetSSHKeyWithPassphraseretry pattern is documented indocs/components/vcs/repo.mdand the migration note as the CLI layer's responsibility. If a downstream tool needs a ready-made prompt helper, should one be added topkg/forms? - Basic-auth usernames per forge: GitLab
oauth2, Bitbucketx-token-auth, everything elsex-access-token. Gitea/Codeberg accept a token as the basic-auth password with an arbitrary username, so they use the default β worth confirming against a real Gitea instance (no integration test covers gitea/codeberg HTTP auth shapes). - Bitbucket app-password mode: the release layer special-cases Bitbucket (username + app password); the repo layer only supports the single-token (
x-token-auth) shape. Is repo-layer app-password support needed, or is token auth sufficient for git-over-HTTPS? - Fallback env for the forge override: when
vcs.provideroverrides the type, the fallback env var follows the override (e.g.GITLAB_TOKEN), consistent withrequireReleaseToken. Confirm that is the intended interaction. - Integration coverage:
TestForgeAuthClonePushexercises GitHub/GitLab token shapes against a local bare remote (file transport, so credentials are constructed but not validated by a server). Real HTTP-auth round-trips would need a local smart-HTTP server β worth a follow-up?