From a20f4ba4416fd66a4143da98ddd46cbb8d4b3db8 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 22 Aug 2026 00:03:46 -0700 Subject: [PATCH] fix(login): use ssh-agent instead of prompting for key passphrase --- modules/config/login.go | 15 +++++++++++--- modules/config/login_test.go | 33 +++++++++++++++++++++++++++++++ modules/task/login_create.go | 6 +++++- modules/task/login_create_test.go | 32 ++++++++++++++++++++++++++++++ modules/task/pull_checkout.go | 9 ++++----- modules/task/pull_clean.go | 2 +- modules/task/pull_create.go | 2 +- modules/task/repo_clone.go | 4 ++-- 8 files changed, 90 insertions(+), 13 deletions(-) diff --git a/modules/config/login.go b/modules/config/login.go index 3cd1adb7..107c290f 100644 --- a/modules/config/login.go +++ b/modules/config/login.go @@ -481,7 +481,7 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client { fmt.Fprintf(os.Stderr, "Failed to read SSH passphrase: %s\n", err) os.Exit(1) } - options = append(options, gitea.UseSSHCert(l.SSHCertPrincipal, l.SSHKey, l.SSHPassphrase)) + options = append(options, gitea.UseSSHCert(l.SSHCertPrincipal, l.SSHKeyPath(), l.SSHPassphrase)) } if l.SSHKeyFingerprint != "" { @@ -489,7 +489,7 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client { fmt.Fprintf(os.Stderr, "Failed to read SSH passphrase: %s\n", err) os.Exit(1) } - options = append(options, gitea.UseSSHPubkey(l.SSHKeyFingerprint, l.SSHKey, l.SSHPassphrase)) + options = append(options, gitea.UseSSHPubkey(l.SSHKeyFingerprint, l.SSHKeyPath(), l.SSHPassphrase)) } client, err := gitea.NewClient(l.URL, options...) @@ -505,7 +505,7 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client { } func (l *Login) askForSSHPassphrase() error { - if ok, err := utils.IsKeyEncrypted(l.SSHKey); ok && err == nil && l.SSHPassphrase == "" { + if ok, err := utils.IsKeyEncrypted(l.SSHKeyPath()); ok && err == nil && l.SSHPassphrase == "" { return huh.NewInput(). Title("ssh-key is encrypted please enter the passphrase: "). Validate(huh.ValidateNotEmpty()). @@ -517,6 +517,15 @@ func (l *Login) askForSSHPassphrase() error { return nil } +// SSHKeyPath returns the on-disk SSH key to use, or an empty string when the +// login is configured to authenticate through a running ssh-agent. +func (l *Login) SSHKeyPath() string { + if l.SSHAgent { + return "" + } + return l.SSHKey +} + // GetSSHHost returns SSH host name func (l *Login) GetSSHHost() string { if l.SSHHost != "" { diff --git a/modules/config/login_test.go b/modules/config/login_test.go index 9348c1ae..6743c19a 100644 --- a/modules/config/login_test.go +++ b/modules/config/login_test.go @@ -90,6 +90,39 @@ func TestLoginClientWithSSHPubkeyDoesNotDeadlockOnFirstRequest(t *testing.T) { assert.EqualValues(t, 1, signedIssueRequests.Load()) } +func TestLoginSSHKeyPath(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + login Login + want string + }{ + { + name: "disk key", + login: Login{SSHKey: "/tmp/id_ed25519"}, + want: "/tmp/id_ed25519", + }, + { + name: "ssh agent ignores disk key", + login: Login{SSHKey: "/tmp/id_ed25519", SSHAgent: true}, + want: "", + }, + { + name: "ssh agent without disk key", + login: Login{SSHAgent: true}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, tt.login.SSHKeyPath()) + }) + } +} + func writeTestSSHKey(t *testing.T) (string, string) { t.Helper() diff --git a/modules/task/login_create.go b/modules/task/login_create.go index 2a0e1050..4fa8a416 100644 --- a/modules/task/login_create.go +++ b/modules/task/login_create.go @@ -130,7 +130,7 @@ func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, ssh // so we just use the host login.SSHHost = serverURL.Host - if len(sshKey) == 0 { + if shouldFindSSHKey(sshKey, sshAgent) { login.SSHKey, err = findSSHKey(ctx, client) if err != nil { fmt.Printf("Warning: problem while finding a SSH key: %s\n", err) @@ -153,6 +153,10 @@ func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, ssh return nil } +func shouldFindSSHKey(sshKey string, sshAgent bool) bool { + return sshKey == "" && !sshAgent +} + func shouldCheckTokenUniqueness(token string, sshAgent bool, sshKey, sshCertPrincipal, sshKeyFingerprint string) bool { if sshAgent || sshKey != "" || sshCertPrincipal != "" || sshKeyFingerprint != "" { return false diff --git a/modules/task/login_create_test.go b/modules/task/login_create_test.go index 593d37ca..a37e954f 100644 --- a/modules/task/login_create_test.go +++ b/modules/task/login_create_test.go @@ -55,3 +55,35 @@ func TestShouldCheckTokenUniqueness(t *testing.T) { }) } } + +func TestShouldFindSSHKey(t *testing.T) { + tests := []struct { + name string + sshKey string + sshAgent bool + want bool + }{ + { + name: "explicit key", + sshKey: "~/.ssh/id_ed25519", + want: false, + }, + { + name: "ssh agent", + sshAgent: true, + want: false, + }, + { + name: "no key configured", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldFindSSHKey(tt.sshKey, tt.sshAgent); got != tt.want { + t.Fatalf("expected %v, got %v", tt.want, got) + } + }) + } +} diff --git a/modules/task/pull_checkout.go b/modules/task/pull_checkout.go index 2b4dd89d..ba1f401d 100644 --- a/modules/task/pull_checkout.go +++ b/modules/task/pull_checkout.go @@ -61,10 +61,9 @@ func remoteURLForPR(login *config.Login, pr *gitea.PullRequest) string { if isRemoteDeleted(pr) { repo = pr.Base.Repository } - if len(login.SSHKey) != 0 { - // login.SSHKey is nonempty, if user specified a key manually or we automatically - // found a matching private key on this machine during login creation. - // this means, we are very likely to have a working ssh setup. + if login.SSHKeyPath() != "" || login.SSHAgent { + // Use SSH when a key file is configured, or when the login authenticates + // through a running ssh-agent. In both cases we have a working SSH setup. return repo.SSHURL } return repo.CloneURL @@ -84,7 +83,7 @@ func doPRFetch( if err != nil { return "", err } - auth, err := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKey, callback) + auth, err := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKeyPath(), callback) if err != nil { return "", err } diff --git a/modules/task/pull_clean.go b/modules/task/pull_clean.go index d96dab0b..226f5e62 100644 --- a/modules/task/pull_clean.go +++ b/modules/task/pull_clean.go @@ -95,7 +95,7 @@ call me again with the --ignore-sha flag`, remoteBranch) if urlErr != nil { return urlErr } - auth, authErr := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKey, callback) + auth, authErr := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKeyPath(), callback) if authErr != nil { return authErr } diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go index f35f5783..429f5a1f 100644 --- a/modules/task/pull_create.go +++ b/modules/task/pull_create.go @@ -211,7 +211,7 @@ func CreateAgitFlowPull(requestCtx stdctx.Context, ctx *context.TeaContext, remo return err } - auth, err := local_git.GetAuthForURL(url, ctx.Login.GetAccessToken(), ctx.Login.SSHKey, callback) + auth, err := local_git.GetAuthForURL(url, ctx.Login.GetAccessToken(), ctx.Login.SSHKeyPath(), callback) if err != nil { return err } diff --git a/modules/task/repo_clone.go b/modules/task/repo_clone.go index 185c6bf2..92d3b970 100644 --- a/modules/task/repo_clone.go +++ b/modules/task/repo_clone.go @@ -33,7 +33,7 @@ func RepoClone( return nil, err } - auth, err := local_git.GetAuthForURL(originURL, login.GetAccessToken(), login.SSHKey, callback) + auth, err := local_git.GetAuthForURL(originURL, login.GetAccessToken(), login.SSHKeyPath(), callback) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func RepoClone( func cloneURL(repo *gitea.Repository, login *config.Login) (*url.URL, error) { urlStr := repo.CloneURL - if login.SSHKey != "" { + if login.SSHKeyPath() != "" || login.SSHAgent { urlStr = repo.SSHURL } return local_git.ParseURL(urlStr)