fix(gitutil): fall back to git CLI for push when no token is set
ci / vet, staticcheck, test, build (push) Successful in 4m9s
release / Build and publish release (push) Successful in 5m28s

go-git's HTTPS transport does not use the system credential store,
so pushes to remotes that require credentials fail silently or with
"authentication required" when no token is provided. When token is
empty, delegate to the system git binary so that credential helpers,
SSH agents, and netrc all work as expected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 01:02:37 +02:00
parent 0dc6d0747d
commit 6ffe282105
+32 -7
View File
@@ -3,6 +3,8 @@ package gitutil
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"os/exec"
"sort" "sort"
"time" "time"
@@ -231,9 +233,17 @@ func CreateTag(repo *gogit.Repository, tagName string) error {
} }
// Push pushes the given branch and tag to the "origin" remote. // Push pushes the given branch and tag to the "origin" remote.
// If token is non-empty, HTTPS basic auth (oauth2/token) is used. // When token is non-empty, go-git is used with HTTPS basic auth (oauth2/token) — suitable for CI.
// Passing an empty token lets go-git use the system credential helper or SSH agent. // When token is empty, the system git binary is invoked so that credential helpers,
// SSH agents, and netrc are all available as they would be for a regular git push.
func Push(repo *gogit.Repository, branchName, tagName, token string) error { func Push(repo *gogit.Repository, branchName, tagName, token string) error {
if token != "" {
return pushWithGoGit(repo, branchName, tagName, token)
}
return pushWithCLI(repo, branchName, tagName)
}
func pushWithGoGit(repo *gogit.Repository, branchName, tagName, token string) error {
remote, err := repo.Remote("origin") remote, err := repo.Remote("origin")
if err != nil { if err != nil {
return fmt.Errorf("remote origin not found: %w", err) return fmt.Errorf("remote origin not found: %w", err)
@@ -244,13 +254,10 @@ func Push(repo *gogit.Repository, branchName, tagName, token string) error {
gitconfig.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branchName, branchName)), gitconfig.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branchName, branchName)),
gitconfig.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", tagName, tagName)), gitconfig.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", tagName, tagName)),
}, },
} Auth: &githttp.BasicAuth{
if token != "" {
opts.Auth = &githttp.BasicAuth{
Username: "oauth2", Username: "oauth2",
Password: token, Password: token,
} },
} }
if err := remote.Push(opts); err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) { if err := remote.Push(opts); err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) {
@@ -259,6 +266,24 @@ func Push(repo *gogit.Repository, branchName, tagName, token string) error {
return nil return nil
} }
func pushWithCLI(repo *gogit.Repository, branchName, tagName string) error {
wt, err := repo.Worktree()
if err != nil {
return fmt.Errorf("get worktree: %w", err)
}
cmd := exec.Command("git", "-C", wt.Filesystem.Root(), "push", "origin",
fmt.Sprintf("HEAD:refs/heads/%s", branchName),
fmt.Sprintf("refs/tags/%s:refs/tags/%s", tagName, tagName),
)
cmd.Stdout = os.Stderr // git push status goes to stderr conventionally
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("git push: %w", err)
}
return nil
}
// resolveTagToCommit follows tag objects until it reaches a commit. // resolveTagToCommit follows tag objects until it reaches a commit.
// Handles both lightweight tags (ref → commit) and annotated tags (ref → tag object → … → commit). // Handles both lightweight tags (ref → commit) and annotated tags (ref → tag object → … → commit).
func resolveTagToCommit(repo *gogit.Repository, ref *plumbing.Reference) (plumbing.Hash, error) { func resolveTagToCommit(repo *gogit.Repository, ref *plumbing.Reference) (plumbing.Hash, error) {