mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-10 03:47:03 +02:00
ec2db4a727
<!-- Hi, thanks for contributing! Please make sure you read our CONTRIBUTING guide. Also, add tests and the respective documentation changes as well. --> <!-- If applied, this commit will... --> ... <!-- Why is this change being made? --> ... <!-- # Provide links to any relevant tickets, URLs or other resources --> ... --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
240 lines
6.4 KiB
Go
240 lines
6.4 KiB
Go
package client
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/caarlos0/log"
|
|
"github.com/charmbracelet/x/exp/ordered"
|
|
"github.com/goreleaser/goreleaser/v2/internal/git"
|
|
"github.com/goreleaser/goreleaser/v2/internal/pipe"
|
|
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/context"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
var gil sync.Mutex
|
|
|
|
// DefaulGitSSHCommand used for git over SSH.
|
|
const DefaulGitSSHCommand = `ssh -i "{{ .KeyPath }}" -o StrictHostKeyChecking=accept-new -F /dev/null`
|
|
|
|
type gitClient struct {
|
|
branch string
|
|
}
|
|
|
|
// NewGitUploadClient
|
|
func NewGitUploadClient(branch string) FilesCreator {
|
|
return &gitClient{
|
|
branch: branch,
|
|
}
|
|
}
|
|
|
|
// CreateFiles implements FilesCreator.
|
|
func (g *gitClient) CreateFiles(
|
|
ctx *context.Context,
|
|
commitAuthor config.CommitAuthor,
|
|
repo Repo,
|
|
message string,
|
|
files []RepoFile,
|
|
) (err error) {
|
|
gil.Lock()
|
|
defer gil.Unlock()
|
|
|
|
url, err := tmpl.New(ctx).Apply(repo.GitURL)
|
|
if err != nil {
|
|
return fmt.Errorf("git: failed to template git url: %w", err)
|
|
}
|
|
|
|
if url == "" {
|
|
return pipe.Skip("url is empty")
|
|
}
|
|
|
|
repo.Name = ordered.First(repo.Name, nameFromURL(url))
|
|
|
|
key, err := tmpl.New(ctx).Apply(repo.PrivateKey)
|
|
if err != nil {
|
|
return fmt.Errorf("git: failed to template private key: %w", err)
|
|
}
|
|
|
|
key, err = keyPath(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sshcmd, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
|
|
"KeyPath": key,
|
|
}).Apply(ordered.First(repo.GitSSHCommand, DefaulGitSSHCommand))
|
|
if err != nil {
|
|
return fmt.Errorf("git: failed to template ssh command: %w", err)
|
|
}
|
|
|
|
parent := filepath.Join(ctx.Config.Dist, "git")
|
|
name := repo.Name + "-" + g.branch
|
|
cwd := filepath.Join(parent, name)
|
|
env := []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", sshcmd)}
|
|
|
|
if _, err := os.Stat(cwd); errors.Is(err, os.ErrNotExist) {
|
|
log.Infof("cloning %s %s", name, cwd)
|
|
if err := os.MkdirAll(parent, 0o755); err != nil {
|
|
return fmt.Errorf("git: failed to create parent: %w", err)
|
|
}
|
|
|
|
if err := cloneRepoWithRetries(ctx, parent, url, name, env); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := runGitCmds(ctx, cwd, env, [][]string{
|
|
{"config", "--local", "user.name", commitAuthor.Name},
|
|
{"config", "--local", "user.email", commitAuthor.Email},
|
|
{"config", "--local", "commit.gpgSign", "false"},
|
|
{"config", "--local", "init.defaultBranch", ordered.First(g.branch, "master")},
|
|
}); err != nil {
|
|
return fmt.Errorf("git: failed to setup local repository: %w", err)
|
|
}
|
|
if g.branch != "" {
|
|
if err := runGitCmds(ctx, cwd, env, [][]string{
|
|
{"checkout", g.branch},
|
|
}); err != nil {
|
|
if err := runGitCmds(ctx, cwd, env, [][]string{
|
|
{"checkout", "-b", g.branch},
|
|
}); err != nil {
|
|
return fmt.Errorf("git: could not checkout branch %s: %w", g.branch, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, file := range files {
|
|
location := filepath.Join(cwd, file.Path)
|
|
log.WithField("path", location).Info("writing")
|
|
if err := os.MkdirAll(filepath.Dir(location), 0o755); err != nil {
|
|
return fmt.Errorf("failed to create parent dirs for %s: %w", file.Path, err)
|
|
}
|
|
if err := os.WriteFile(location, file.Content, 0o644); err != nil {
|
|
return fmt.Errorf("failed to write %s: %w", file.Path, err)
|
|
}
|
|
log.
|
|
WithField("repository", url).
|
|
WithField("name", repo.Name).
|
|
WithField("file", file.Path).
|
|
Info("pushing")
|
|
}
|
|
|
|
if err := runGitCmds(ctx, cwd, env, [][]string{
|
|
{"add", "-A", "."},
|
|
{"commit", "-m", message},
|
|
{"push", "origin", "HEAD"},
|
|
}); err != nil {
|
|
return fmt.Errorf("git: failed to push %q (%q): %w", repo.Name, url, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateFile implements FileCreator.
|
|
func (g *gitClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo Repo, content []byte, path string, message string) error {
|
|
return g.CreateFiles(ctx, commitAuthor, repo, message, []RepoFile{{
|
|
Path: path,
|
|
Content: content,
|
|
}})
|
|
}
|
|
|
|
func keyPath(key string) (string, error) {
|
|
if key == "" {
|
|
return "", pipe.Skip("private_key is empty")
|
|
}
|
|
|
|
path := key
|
|
|
|
_, err := ssh.ParsePrivateKey([]byte(key))
|
|
if isPasswordError(err) {
|
|
return "", fmt.Errorf("git: key is password-protected")
|
|
}
|
|
|
|
if err == nil {
|
|
// if it can be parsed as a valid private key, we write it to a
|
|
// temp file and use that path on GIT_SSH_COMMAND.
|
|
f, err := os.CreateTemp("", "id_*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("git: failed to store private key: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// the key needs to EOF at an empty line, seems like github actions
|
|
// is somehow removing them.
|
|
if !strings.HasSuffix(key, "\n") {
|
|
key += "\n"
|
|
}
|
|
|
|
if _, err := io.WriteString(f, key); err != nil {
|
|
return "", fmt.Errorf("git: failed to store private key: %w", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return "", fmt.Errorf("git: failed to store private key: %w", err)
|
|
}
|
|
path = f.Name()
|
|
}
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
return "", fmt.Errorf("git: could not stat private_key: %w", err)
|
|
}
|
|
|
|
// in any case, ensure the key has the correct permissions.
|
|
if err := os.Chmod(path, 0o600); err != nil {
|
|
return "", fmt.Errorf("git: failed to ensure private_key permissions: %w", err)
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
func isPasswordError(err error) bool {
|
|
var kerr *ssh.PassphraseMissingError
|
|
return errors.As(err, &kerr)
|
|
}
|
|
|
|
func cloneRepoWithRetries(ctx *context.Context, parent, url, name string, env []string) error {
|
|
var try int
|
|
for try < 10 {
|
|
try++
|
|
err := runGitCmds(ctx, parent, env, [][]string{{"clone", url, name}})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if isRetriableCloneError(err) {
|
|
log.WithField("try", try).
|
|
WithField("image", name).
|
|
WithError(err).
|
|
Warnf("failed to push image, will retry")
|
|
time.Sleep(time.Duration(try*10) * time.Second)
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to clone local repository: %w", err)
|
|
}
|
|
return fmt.Errorf("failed to push %s after %d tries", name, try)
|
|
}
|
|
|
|
func isRetriableCloneError(err error) bool {
|
|
return strings.Contains(err.Error(), "Connection reset")
|
|
}
|
|
|
|
func runGitCmds(ctx *context.Context, cwd string, env []string, cmds [][]string) error {
|
|
for _, cmd := range cmds {
|
|
args := append([]string{"-C", cwd}, cmd...)
|
|
if _, err := git.Clean(git.RunWithEnv(ctx, env, args...)); err != nil {
|
|
return fmt.Errorf("%q failed: %w", strings.Join(cmd, " "), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nameFromURL(url string) string {
|
|
return strings.TrimSuffix(url[strings.LastIndex(url, "/")+1:], ".git")
|
|
}
|