github.com/triarius/goreleaser@v1.12.5/internal/pipe/aur/aur.go (about)

     1  package aur
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/caarlos0/log"
    16  	"github.com/triarius/goreleaser/internal/artifact"
    17  	"github.com/triarius/goreleaser/internal/client"
    18  	"github.com/triarius/goreleaser/internal/commitauthor"
    19  	"github.com/triarius/goreleaser/internal/git"
    20  	"github.com/triarius/goreleaser/internal/pipe"
    21  	"github.com/triarius/goreleaser/internal/tmpl"
    22  	"github.com/triarius/goreleaser/pkg/config"
    23  	"github.com/triarius/goreleaser/pkg/context"
    24  	"golang.org/x/crypto/ssh"
    25  )
    26  
    27  const (
    28  	aurExtra          = "AURConfig"
    29  	defaultSSHCommand = "ssh -i {{ .KeyPath }} -o StrictHostKeyChecking=accept-new -F /dev/null"
    30  	defaultCommitMsg  = "Update to {{ .Tag }}"
    31  )
    32  
    33  var ErrNoArchivesFound = errors.New("no linux archives found")
    34  
    35  // Pipe for arch linux's AUR pkgbuild.
    36  type Pipe struct{}
    37  
    38  func (Pipe) String() string                 { return "arch user repositories" }
    39  func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.AURs) == 0 }
    40  
    41  func (Pipe) Default(ctx *context.Context) error {
    42  	for i := range ctx.Config.AURs {
    43  		pkg := &ctx.Config.AURs[i]
    44  
    45  		pkg.CommitAuthor = commitauthor.Default(pkg.CommitAuthor)
    46  		if pkg.CommitMessageTemplate == "" {
    47  			pkg.CommitMessageTemplate = defaultCommitMsg
    48  		}
    49  		if pkg.Name == "" {
    50  			pkg.Name = ctx.Config.ProjectName
    51  		}
    52  		if !strings.HasSuffix(pkg.Name, "-bin") {
    53  			pkg.Name += "-bin"
    54  		}
    55  		if len(pkg.Conflicts) == 0 {
    56  			pkg.Conflicts = []string{ctx.Config.ProjectName}
    57  		}
    58  		if len(pkg.Provides) == 0 {
    59  			pkg.Provides = []string{ctx.Config.ProjectName}
    60  		}
    61  		if pkg.Rel == "" {
    62  			pkg.Rel = "1"
    63  		}
    64  		if pkg.GitSSHCommand == "" {
    65  			pkg.GitSSHCommand = defaultSSHCommand
    66  		}
    67  		if pkg.Goamd64 == "" {
    68  			pkg.Goamd64 = "v1"
    69  		}
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  func (Pipe) Run(ctx *context.Context) error {
    76  	cli, err := client.New(ctx)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	return runAll(ctx, cli)
    82  }
    83  
    84  func runAll(ctx *context.Context, cli client.Client) error {
    85  	for _, aur := range ctx.Config.AURs {
    86  		err := doRun(ctx, aur, cli)
    87  		if err != nil {
    88  			return err
    89  		}
    90  	}
    91  	return nil
    92  }
    93  
    94  func doRun(ctx *context.Context, aur config.AUR, cl client.Client) error {
    95  	name, err := tmpl.New(ctx).Apply(aur.Name)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	aur.Name = name
   100  
   101  	filters := []artifact.Filter{
   102  		artifact.ByGoos("linux"),
   103  		artifact.Or(
   104  			artifact.And(
   105  				artifact.ByGoarch("amd64"),
   106  				artifact.ByGoamd64(aur.Goamd64),
   107  			),
   108  			artifact.ByGoarch("arm64"),
   109  			artifact.ByGoarch("386"),
   110  			artifact.And(
   111  				artifact.ByGoarch("arm"),
   112  				artifact.Or(
   113  					artifact.ByGoarm("7"),
   114  					artifact.ByGoarm("6"),
   115  				),
   116  			),
   117  		),
   118  		artifact.Or(
   119  			artifact.ByType(artifact.UploadableArchive),
   120  			artifact.ByType(artifact.UploadableBinary),
   121  		),
   122  	}
   123  	if len(aur.IDs) > 0 {
   124  		filters = append(filters, artifact.ByIDs(aur.IDs...))
   125  	}
   126  
   127  	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
   128  	if len(archives) == 0 {
   129  		return ErrNoArchivesFound
   130  	}
   131  
   132  	pkg, err := tmpl.New(ctx).Apply(aur.Package)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	if strings.TrimSpace(pkg) == "" {
   137  		art := archives[0]
   138  		switch art.Type {
   139  		case artifact.UploadableBinary:
   140  			name := art.Name
   141  			bin := artifact.ExtraOr(*art, artifact.ExtraBinary, art.Name)
   142  			pkg = fmt.Sprintf(`install -Dm755 "./%s "${pkgdir}/usr/bin/%s"`, name, bin)
   143  		case artifact.UploadableArchive:
   144  			for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) {
   145  				pkg = fmt.Sprintf(`install -Dm755 "./%s" "${pkgdir}/usr/bin/%[1]s"`, bin)
   146  				break
   147  			}
   148  		}
   149  		log.Warnf("guessing package to be %q", pkg)
   150  	}
   151  	aur.Package = pkg
   152  
   153  	for _, info := range []struct {
   154  		name, tpl, ext string
   155  		kind           artifact.Type
   156  	}{
   157  		{
   158  			name: "PKGBUILD",
   159  			tpl:  aurTemplateData,
   160  			ext:  ".pkgbuild",
   161  			kind: artifact.PkgBuild,
   162  		},
   163  		{
   164  			name: ".SRCINFO",
   165  			tpl:  srcInfoTemplate,
   166  			ext:  ".srcinfo",
   167  			kind: artifact.SrcInfo,
   168  		},
   169  	} {
   170  		pkgContent, err := buildPkgFile(ctx, aur, cl, archives, info.tpl)
   171  		if err != nil {
   172  			return err
   173  		}
   174  
   175  		path := filepath.Join(ctx.Config.Dist, "aur", aur.Name+info.ext)
   176  		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
   177  			return fmt.Errorf("failed to write %s: %w", info.kind, err)
   178  		}
   179  		log.WithField("file", path).Info("writing")
   180  		if err := os.WriteFile(path, []byte(pkgContent), 0o644); err != nil { //nolint: gosec
   181  			return fmt.Errorf("failed to write %s: %w", info.kind, err)
   182  		}
   183  
   184  		ctx.Artifacts.Add(&artifact.Artifact{
   185  			Name: info.name,
   186  			Path: path,
   187  			Type: info.kind,
   188  			Extra: map[string]interface{}{
   189  				aurExtra:         aur,
   190  				artifact.ExtraID: aur.Name,
   191  			},
   192  		})
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  func buildPkgFile(ctx *context.Context, pkg config.AUR, client client.Client, artifacts []*artifact.Artifact, tpl string) (string, error) {
   199  	data, err := dataFor(ctx, pkg, client, artifacts)
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  	return applyTemplate(ctx, tpl, data)
   204  }
   205  
   206  func fixLines(s string) string {
   207  	lines := strings.Split(s, "\n")
   208  	var result []string
   209  	for _, line := range lines {
   210  		l := strings.TrimSpace(line)
   211  		if l == "" {
   212  			result = append(result, "")
   213  			continue
   214  		}
   215  		result = append(result, "  "+l)
   216  	}
   217  	return strings.Join(result, "\n")
   218  }
   219  
   220  func applyTemplate(ctx *context.Context, tpl string, data templateData) (string, error) {
   221  	t := template.Must(
   222  		template.New(data.Name).
   223  			Funcs(template.FuncMap{
   224  				"fixLines": fixLines,
   225  				"pkgArray": toPkgBuildArray,
   226  			}).
   227  			Parse(tpl),
   228  	)
   229  
   230  	var out bytes.Buffer
   231  	if err := t.Execute(&out, data); err != nil {
   232  		return "", err
   233  	}
   234  
   235  	content, err := tmpl.New(ctx).Apply(out.String())
   236  	if err != nil {
   237  		return "", err
   238  	}
   239  	out.Reset()
   240  
   241  	// Sanitize the template output and get rid of trailing whitespace.
   242  	var (
   243  		r = strings.NewReader(content)
   244  		s = bufio.NewScanner(r)
   245  	)
   246  	for s.Scan() {
   247  		l := strings.TrimRight(s.Text(), " ")
   248  		_, _ = out.WriteString(l)
   249  		_ = out.WriteByte('\n')
   250  	}
   251  	if err := s.Err(); err != nil {
   252  		return "", err
   253  	}
   254  
   255  	return out.String(), nil
   256  }
   257  
   258  func toPkgBuildArray(ss []string) string {
   259  	result := make([]string, 0, len(ss))
   260  	for _, s := range ss {
   261  		result = append(result, fmt.Sprintf("'%s'", s))
   262  	}
   263  	return strings.Join(result, " ")
   264  }
   265  
   266  func toPkgBuildArch(arch string) string {
   267  	switch arch {
   268  	case "amd64":
   269  		return "x86_64"
   270  	case "386":
   271  		return "i686"
   272  	case "arm64":
   273  		return "aarch64"
   274  	case "arm6":
   275  		return "armv6h"
   276  	case "arm7":
   277  		return "armv7h"
   278  	default:
   279  		return "invalid" // should never get here
   280  	}
   281  }
   282  
   283  func dataFor(ctx *context.Context, cfg config.AUR, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
   284  	result := templateData{
   285  		Name:         cfg.Name,
   286  		Desc:         cfg.Description,
   287  		Homepage:     cfg.Homepage,
   288  		Version:      fmt.Sprintf("%d.%d.%d", ctx.Semver.Major, ctx.Semver.Minor, ctx.Semver.Patch),
   289  		License:      cfg.License,
   290  		Rel:          cfg.Rel,
   291  		Maintainers:  cfg.Maintainers,
   292  		Contributors: cfg.Contributors,
   293  		Provides:     cfg.Provides,
   294  		Conflicts:    cfg.Conflicts,
   295  		Backup:       cfg.Backup,
   296  		Depends:      cfg.Depends,
   297  		OptDepends:   cfg.OptDepends,
   298  		Package:      cfg.Package,
   299  	}
   300  
   301  	for _, art := range artifacts {
   302  		sum, err := art.Checksum("sha256")
   303  		if err != nil {
   304  			return result, err
   305  		}
   306  
   307  		if cfg.URLTemplate == "" {
   308  			url, err := cl.ReleaseURLTemplate(ctx)
   309  			if err != nil {
   310  				return result, err
   311  			}
   312  			cfg.URLTemplate = url
   313  		}
   314  		url, err := tmpl.New(ctx).WithArtifact(art, map[string]string{}).Apply(cfg.URLTemplate)
   315  		if err != nil {
   316  			return result, err
   317  		}
   318  
   319  		releasePackage := releasePackage{
   320  			DownloadURL: url,
   321  			SHA256:      sum,
   322  			Arch:        toPkgBuildArch(art.Goarch + art.Goarm),
   323  			Format:      artifact.ExtraOr(*art, artifact.ExtraFormat, ""),
   324  		}
   325  		result.ReleasePackages = append(result.ReleasePackages, releasePackage)
   326  		result.Arches = append(result.Arches, releasePackage.Arch)
   327  	}
   328  
   329  	sort.Strings(result.Arches)
   330  	sort.Slice(result.ReleasePackages, func(i, j int) bool {
   331  		return result.ReleasePackages[i].Arch < result.ReleasePackages[j].Arch
   332  	})
   333  	return result, nil
   334  }
   335  
   336  // Publish the PKGBUILD and .SRCINFO files to the AUR repository.
   337  func (Pipe) Publish(ctx *context.Context) error {
   338  	skips := pipe.SkipMemento{}
   339  	for _, pkgs := range ctx.Artifacts.Filter(
   340  		artifact.Or(
   341  			artifact.ByType(artifact.PkgBuild),
   342  			artifact.ByType(artifact.SrcInfo),
   343  		),
   344  	).GroupByID() {
   345  		err := doPublish(ctx, pkgs)
   346  		if err != nil && pipe.IsSkip(err) {
   347  			skips.Remember(err)
   348  			continue
   349  		}
   350  		if err != nil {
   351  			return err
   352  		}
   353  	}
   354  	return skips.Evaluate()
   355  }
   356  
   357  func doPublish(ctx *context.Context, pkgs []*artifact.Artifact) error {
   358  	cfg, err := artifact.Extra[config.AUR](*pkgs[0], aurExtra)
   359  	if err != nil {
   360  		return err
   361  	}
   362  
   363  	if strings.TrimSpace(cfg.SkipUpload) == "true" {
   364  		return pipe.Skip("aur.skip_upload is set")
   365  	}
   366  
   367  	if strings.TrimSpace(cfg.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
   368  		return pipe.Skip("prerelease detected with 'auto' upload, skipping aur publish")
   369  	}
   370  
   371  	key, err := tmpl.New(ctx).Apply(cfg.PrivateKey)
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	key, err = keyPath(key)
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	url, err := tmpl.New(ctx).Apply(cfg.GitURL)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	if url == "" {
   387  		return pipe.Skip("aur.git_url is empty")
   388  	}
   389  
   390  	sshcmd, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
   391  		"KeyPath": key,
   392  	}).Apply(cfg.GitSSHCommand)
   393  	if err != nil {
   394  		return err
   395  	}
   396  
   397  	msg, err := tmpl.New(ctx).Apply(cfg.CommitMessageTemplate)
   398  	if err != nil {
   399  		return err
   400  	}
   401  
   402  	author, err := commitauthor.Get(ctx, cfg.CommitAuthor)
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	parent := filepath.Join(ctx.Config.Dist, "aur", "repos")
   408  	cwd := filepath.Join(parent, cfg.Name)
   409  
   410  	if err := os.MkdirAll(parent, 0o755); err != nil {
   411  		return err
   412  	}
   413  
   414  	env := []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", sshcmd)}
   415  
   416  	if err := runGitCmds(ctx, parent, env, [][]string{
   417  		{"clone", url, cfg.Name},
   418  	}); err != nil {
   419  		return fmt.Errorf("failed to setup local AUR repo: %w", err)
   420  	}
   421  
   422  	if err := runGitCmds(ctx, cwd, env, [][]string{
   423  		// setup auth et al
   424  		{"config", "--local", "user.name", author.Name},
   425  		{"config", "--local", "user.email", author.Email},
   426  		{"config", "--local", "commit.gpgSign", "false"},
   427  		{"config", "--local", "init.defaultBranch", "master"},
   428  	}); err != nil {
   429  		return fmt.Errorf("failed to setup local AUR repo: %w", err)
   430  	}
   431  
   432  	for _, pkg := range pkgs {
   433  		bts, err := os.ReadFile(pkg.Path)
   434  		if err != nil {
   435  			return fmt.Errorf("failed to read %s: %w", pkg.Name, err)
   436  		}
   437  
   438  		if err := os.WriteFile(filepath.Join(cwd, pkg.Name), bts, 0o644); err != nil {
   439  			return fmt.Errorf("failed to write %s: %w", pkg.Name, err)
   440  		}
   441  	}
   442  
   443  	log.WithField("repo", url).WithField("name", cfg.Name).Info("pushing")
   444  	if err := runGitCmds(ctx, cwd, env, [][]string{
   445  		{"add", "-A", "."},
   446  		{"commit", "-m", msg},
   447  		{"push", "origin", "HEAD"},
   448  	}); err != nil {
   449  		return fmt.Errorf("failed to push %q (%q): %w", cfg.Name, url, err)
   450  	}
   451  
   452  	return nil
   453  }
   454  
   455  func keyPath(key string) (string, error) {
   456  	if key == "" {
   457  		return "", pipe.Skip("aur.private_key is empty")
   458  	}
   459  
   460  	path := key
   461  	if _, err := ssh.ParsePrivateKey([]byte(key)); err == nil {
   462  		// if it can be parsed as a valid private key, we write it to a
   463  		// temp file and use that path on GIT_SSH_COMMAND.
   464  		f, err := os.CreateTemp("", "id_*")
   465  		if err != nil {
   466  			return "", fmt.Errorf("failed to store private key: %w", err)
   467  		}
   468  		defer f.Close()
   469  
   470  		// the key needs to EOF at an empty line, seems like github actions
   471  		// is somehow removing them.
   472  		if !strings.HasSuffix(key, "\n") {
   473  			key += "\n"
   474  		}
   475  
   476  		if _, err := io.WriteString(f, key); err != nil {
   477  			return "", fmt.Errorf("failed to store private key: %w", err)
   478  		}
   479  		if err := f.Close(); err != nil {
   480  			return "", fmt.Errorf("failed to store private key: %w", err)
   481  		}
   482  		path = f.Name()
   483  	}
   484  
   485  	if _, err := os.Stat(path); err != nil {
   486  		return "", fmt.Errorf("could not stat aur.private_key: %w", err)
   487  	}
   488  
   489  	// in any case, ensure the key has the correct permissions.
   490  	if err := os.Chmod(path, 0o600); err != nil {
   491  		return "", fmt.Errorf("failed to ensure aur.private_key permissions: %w", err)
   492  	}
   493  
   494  	return path, nil
   495  }
   496  
   497  func runGitCmds(ctx *context.Context, cwd string, env []string, cmds [][]string) error {
   498  	for _, cmd := range cmds {
   499  		args := append([]string{"-C", cwd}, cmd...)
   500  		if _, err := git.Clean(git.RunWithEnv(ctx, env, args...)); err != nil {
   501  			return fmt.Errorf("%q failed: %w", strings.Join(cmd, " "), err)
   502  		}
   503  	}
   504  	return nil
   505  }