Skip to content

Commit 7259194

Browse files
authored
feat: Support git push via SSH (#2397)
Fixes: #2339 ## Changes - Automatically determine if HTTPS or SSH should be used based on the remote URI - SSH is configured via go-git transport using ssh-agent. Requires users to upload their private keys to ssh-agent so that it can be picked up. SSH-Agent is used so that the private key path + passphrase doesn't need to passed in via env vars or flags. ## Result from running cloudtop Command used: `librarian release init -push`. Resulted in PR created: #2449 Logs from invocation: ``` level=INFO msg="Authenticating with SSH" level=INFO msg="Successfully pushed changes" level=INFO [msg="Creating PR" branch=librarian-20250926T172344Z base=main title="chore: librarian release pull request: 20250926T172344Z"](#2449) level=INFO msg="PR created" url=#2449 level=INFO msg="Labels added to issue" number=2449 labels=[release:pending] ``` --------- Signed-off-by: Lawrence Qiu <[email protected]>
1 parent ef69a31 commit 7259194

File tree

6 files changed

+177
-16
lines changed

6 files changed

+177
-16
lines changed

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,66 @@ command to run the latest released version:
5252
go run github.com/googleapis/librarian/cmd/librarian@latest -help
5353
```
5454

55+
### Setup and Configuration
56+
To run librarian, you will need to configure a Github Token and set it to the `LIBRARIAN_GITHUB_TOKEN` environment variable. This will use the Github token as authentication to push to Github and to run Github API commands (e.g. Creating a PR and Adding Labels to a PR).
57+
58+
Unless specifically configured, Librarian will use HTTPS for pushing to remote. See the [SSH](#using-ssh) section for push to remote via SSH.
59+
60+
### Github Token
61+
There are two main options to get a Github token:
62+
1. Use the [gh cli](https://cli.github.com/) tool to easily authenticate with github. Run the following commands once the tool is installed:
63+
```shell
64+
# Follow the instructions from the tool. If using SSH, see the section
65+
# below regarding additional setup.You still need a Github Token to run
66+
# Github API commands (e.g. creating a pull request).
67+
gh auth login
68+
# This will output the token to use as the Github Token
69+
gh auth token
70+
```
71+
This will create a token with the `repo` scope.
72+
73+
2. Alternatively, follow the steps listed in the Github [guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) to create a Personal Access Token (PAT). You will need the `repo` scope for your token.
74+
75+
Once you have created a token, set the token as the environment variable:
76+
```shell
77+
export LIBRARIAN_GITHUB_TOKEN={YOUR_TOKEN_HERE}
78+
```
79+
80+
### Using SSH
81+
There are two main ways to configure SSH:
82+
1. Using the [gh cli](https://cli.github.com/) to upload your SSH public key to Github. When following the steps in the gh cli tool, you will select your public key to be added to Github. Additionally, you will need to add your private key to the [ssh-agent](https://linux.die.net/man/1/ssh-agent).
83+
84+
Typically, your private keys will be `~/.ssh/id_ed25519` or `~/.ssh/id_rsa` and your public keys will be the same with the `.pub` suffix. You should be able to see this by running `ls ~/.ssh`. If you do not see a public/ private key combination, you can follow this [guide](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) to generate a new key.
85+
86+
Running the following commands to add your private key to ssh-agent:
87+
```sh
88+
# This will start the ssh-agent if it hasn't already been started
89+
eval "$(ssh-agent -s)"
90+
91+
# This adds your private key to the ssh-agent.
92+
# Note: The private key will not have the `.pub` suffix.
93+
ssh-add ~/.ssh/{PRIVATE_KEY_FILE}
94+
95+
# Run this command to verify that your private key is added
96+
# You should see an output of a SHA with an absolute path to the private key
97+
# e.g. `256 SHA:{MY_SHA} .../.ssh/id_ed25519`
98+
ssh-add -l
99+
```
100+
101+
2. Follow the steps [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh). You will need to either create a new SSH key or using an existing one, add it to the ssh-agent, and then upload it to Github.
102+
103+
Once everything has been configured, set the `origin` remote to the SSH URI:
104+
```shell
105+
git remote set-url origin [email protected]:googleapis/librarian.git
106+
```
107+
55108
## Documentation
56109

57110
- [CLI Documentation](https://pkg.go.dev/github.com/googleapis/librarian/cmd/librarian)
58111
- [Language Onboarding Guide](doc/language-onboarding.md)
59112
- [How We Write Go](doc/howwewritego.md)
60113
- [State Schema](doc/state-schema.md)
61-
- [Config Schema](doc/config-schema.md))
114+
- [Config Schema](doc/config-schema.md)
62115
- [Running Tests](doc/testing.md)
63116
- [sidekick](doc/sidekick.md)
64117

cmd/librarian/doc.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ in '.librarian/state.yaml'.
7777
the container to compile and validate the generated code.
7878
- If the '--push' flag is provided, the changes are committed to a new branch,
7979
and a pull request is created on GitHub. Otherwise, the changes are left in
80-
your local working directory for inspection.
80+
your local working directory for inspection. When pushing to a remote branch,
81+
you have the option of using HTTPS or SSH. Librarian will automatically determine
82+
whether to use HTTPS or SSH based on the remote URI.
8183
8284
Example with build and push:
8385
@@ -165,7 +167,9 @@ By default, 'release init' leaves the changes in your local working directory
165167
for inspection. Use the '--push' flag to automatically commit the changes to
166168
a new branch and create a pull request on GitHub. The '--commit' flag may be
167169
used to create a local commit without creating a pull request; this flag is
168-
ignored if '--push' is also specified.
170+
ignored if '--push' is also specified. When pushing to a remote branch,
171+
you have the option of using HTTPS or SSH. Librarian will automatically determine
172+
whether to use HTTPS or SSH based on the remote URI.
169173
170174
Examples:
171175

internal/config/config_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ func TestNew(t *testing.T) {
3535
{
3636
name: "All environment variables set",
3737
envVars: map[string]string{
38-
LibrarianGithubToken: "gh_token",
39-
"LIBRARIAN_SYNC_AUTH_TOKEN": "sync_token",
38+
LibrarianGithubToken: "gh_token",
4039
},
4140
want: Config{
4241
GitHubToken: "gh_token",
@@ -47,7 +46,6 @@ func TestNew(t *testing.T) {
4746
name: "No environment variables set",
4847
envVars: map[string]string{},
4948
want: Config{
50-
GitHubToken: "",
5149
CommandName: "test",
5250
},
5351
},

internal/gitrepo/gitrepo.go

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"log/slog"
22+
"net/url"
2223
"os"
2324
"os/exec"
2425
"path/filepath"
@@ -29,7 +30,9 @@ import (
2930
"github.com/go-git/go-git/v5/config"
3031
"github.com/go-git/go-git/v5/plumbing"
3132
"github.com/go-git/go-git/v5/plumbing/object"
33+
"github.com/go-git/go-git/v5/plumbing/transport"
3234
httpAuth "github.com/go-git/go-git/v5/plumbing/transport/http"
35+
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
3336
)
3437

3538
// Repository defines the interface for git repository operations.
@@ -472,16 +475,29 @@ func (r *LocalRepository) DeleteBranch(branchName string) error {
472475

473476
func (r *LocalRepository) pushRefSpec(refSpec string) error {
474477
slog.Info("Pushing changes", "refSpec", refSpec)
475-
var auth *httpAuth.BasicAuth
476-
if r.gitPassword != "" {
477-
slog.Info("Authenticating with basic auth")
478-
auth = &httpAuth.BasicAuth{
479-
// GitHub's authentication needs the username set to a non-empty value, but
480-
// it does not need to match the token
481-
Username: "cloud-sdk-librarian",
482-
Password: r.gitPassword,
478+
479+
// Check for the configured URI for the `origin` remote.
480+
// If there are multiple URLs, the first one is selected.
481+
var remoteURI string
482+
remotes, err := r.Remotes()
483+
if err != nil {
484+
return err
485+
}
486+
for _, remote := range remotes {
487+
if remote.Name == "origin" {
488+
if len(remote.URLs) > 0 {
489+
remoteURI = remote.URLs[0]
490+
}
483491
}
484492
}
493+
494+
useSSH := canUseSSH(remoteURI)
495+
// While cloning a public repo does not require any authCreds, pushing
496+
// to the repo requires authentication and verification of identity
497+
auth, err := r.authCreds(useSSH)
498+
if err != nil {
499+
return err
500+
}
485501
if err := r.repo.Push(&git.PushOptions{
486502
RemoteName: "origin",
487503
RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
@@ -493,6 +509,55 @@ func (r *LocalRepository) pushRefSpec(refSpec string) error {
493509
return nil
494510
}
495511

512+
// canUseSSH returns if the remote URI can connect via https ssh. It attempts to
513+
// automatically determine the type and returns false as a default if it's unable
514+
// to make a determination.
515+
func canUseSSH(remoteURI string) bool {
516+
// First, try to parse it as a standard URL
517+
// e.g. "https://github.com/golang/go.git", "ssh://[email protected]/golang/go.git"
518+
parsedURL, err := url.Parse(remoteURI)
519+
if err == nil && parsedURL.Scheme != "" {
520+
// Check the scheme directly
521+
switch parsedURL.Scheme {
522+
case "https":
523+
return false
524+
case "ssh":
525+
return true
526+
}
527+
}
528+
529+
// If parsing fails or the scheme is not standard, check for the `user@hostname`
530+
// SSH syntax (e.g., "[email protected]:user/repo.git"). This format doesn't
531+
// have a "://" and contains a ":"
532+
if !strings.Contains(remoteURI, "://") && strings.Contains(remoteURI, ":") {
533+
return true
534+
}
535+
536+
return false
537+
}
538+
539+
// authCreds returns the configured AuthMethod to used to pushing to the
540+
// remote repository. The useSSH determines if Basic Auth or SSH is used.
541+
func (r *LocalRepository) authCreds(useSSH bool) (transport.AuthMethod, error) {
542+
if useSSH {
543+
slog.Info("Authenticating with SSH")
544+
// This is the generic `git` username when cloning via SSH. It is the value
545+
// that exists before the URL. e.g. [email protected]:googleapis/librarian.git
546+
auth, err := ssh.DefaultAuthBuilder("git")
547+
if err != nil {
548+
return nil, err
549+
}
550+
return auth, nil
551+
}
552+
slog.Info("Authenticating with basic auth")
553+
return &httpAuth.BasicAuth{
554+
// GitHub's authentication needs the username set to a non-empty value, but
555+
// it does not need to match the token
556+
Username: "cloud-sdk-librarian",
557+
Password: r.gitPassword,
558+
}, nil
559+
}
560+
496561
// Restore restores changes in the working tree, leaving staged area untouched.
497562
// Note that untracked files, if any, are not touched.
498563
//

internal/gitrepo/gitrepo_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,3 +1368,40 @@ func setupRepoForGetCommitsTest(t *testing.T) (*LocalRepository, map[string]stri
13681368

13691369
return &LocalRepository{Dir: dir, repo: repo}, commits
13701370
}
1371+
1372+
func TestCanUseSSH(t *testing.T) {
1373+
t.Parallel()
1374+
for _, test := range []struct {
1375+
name string
1376+
remoteURI string
1377+
want bool
1378+
}{
1379+
{
1380+
name: "remote_https_uri",
1381+
remoteURI: "https://github.com/googleapis/librarian.git",
1382+
want: false,
1383+
},
1384+
{
1385+
name: "remote_ssh_uri",
1386+
remoteURI: "[email protected]:googleapis/librarian.git",
1387+
want: true,
1388+
},
1389+
{
1390+
name: "remote_ssh_uri_with_scheme",
1391+
remoteURI: "ssh://[email protected]/googleapis/librarian.git",
1392+
want: true,
1393+
},
1394+
{
1395+
name: "invalid_remote_uri",
1396+
remoteURI: "nonsense-uri",
1397+
want: false,
1398+
},
1399+
} {
1400+
t.Run(test.name, func(t *testing.T) {
1401+
got := canUseSSH(test.remoteURI)
1402+
if diff := cmp.Diff(test.want, got); diff != "" {
1403+
t.Errorf("canUseSSH() mismatch in %s (-want +got):\n%s", test.name, diff)
1404+
}
1405+
})
1406+
}
1407+
}

internal/librarian/help.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ in '.librarian/state.yaml'.
6868
the container to compile and validate the generated code.
6969
- If the '--push' flag is provided, the changes are committed to a new branch,
7070
and a pull request is created on GitHub. Otherwise, the changes are left in
71-
your local working directory for inspection.
71+
your local working directory for inspection. When pushing to a remote branch,
72+
you have the option of using HTTPS or SSH. Librarian will automatically determine
73+
whether to use HTTPS or SSH based on the remote URI.
7274
7375
Example with build and push:
7476
LIBRARIAN_GITHUB_TOKEN=xxx librarian generate --push --build`
@@ -96,7 +98,9 @@ By default, 'release init' leaves the changes in your local working directory
9698
for inspection. Use the '--push' flag to automatically commit the changes to
9799
a new branch and create a pull request on GitHub. The '--commit' flag may be
98100
used to create a local commit without creating a pull request; this flag is
99-
ignored if '--push' is also specified.
101+
ignored if '--push' is also specified. When pushing to a remote branch,
102+
you have the option of using HTTPS or SSH. Librarian will automatically determine
103+
whether to use HTTPS or SSH based on the remote URI.
100104
101105
Examples:
102106
# Create a release PR for all libraries with pending changes.

0 commit comments

Comments
 (0)