Skip to content

Commit

Permalink
feat: Add support for custom publishers (#1481)
Browse files Browse the repository at this point in the history
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
radeksimko and kodiakhq[bot] committed May 10, 2020
1 parent 31fedc4 commit 8749030
Show file tree
Hide file tree
Showing 14 changed files with 710 additions and 3 deletions.
4 changes: 4 additions & 0 deletions internal/artifact/artifact.go
Expand Up @@ -261,6 +261,10 @@ func And(filters ...Filter) Filter {
// is accepted.
// You can compose filters by using the And and Or filters.
func (artifacts *Artifacts) Filter(filter Filter) Artifacts {
if filter == nil {
return *artifacts
}

var result = New()
for _, a := range artifacts.items {
if filter(a) {
Expand Down
2 changes: 2 additions & 0 deletions internal/artifact/artifact_test.go
Expand Up @@ -86,6 +86,8 @@ func TestFilter(t *testing.T) {
assert.Len(t, artifacts.Filter(ByType(Checksum)).items, 2)
assert.Len(t, artifacts.Filter(ByType(Binary)).items, 0)

assert.Len(t, artifacts.Filter(nil).items, 5)

assert.Len(t, artifacts.Filter(
And(
ByType(Checksum),
Expand Down
167 changes: 167 additions & 0 deletions internal/exec/exec.go
@@ -0,0 +1,167 @@
package exec

import (
"fmt"
"os/exec"

"github.com/apex/log"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/logext"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/mattn/go-shellwords"
)

func Execute(ctx *context.Context, publishers []config.Publisher) error {
if ctx.SkipPublish {
return pipe.ErrSkipPublishEnabled
}

for _, p := range publishers {
log.WithField("name", p.Name).Debug("executing custom publisher")
err := executePublisher(ctx, p)
if err != nil {
return err
}
}

return nil
}

func executePublisher(ctx *context.Context, publisher config.Publisher) error {
log.Debugf("filtering %d artifacts", len(ctx.Artifacts.List()))
artifacts := filterArtifacts(ctx.Artifacts, publisher)
log.Debugf("will execute custom publisher with %d artifacts", len(artifacts))

var g = semerrgroup.New(ctx.Parallelism)
for _, artifact := range artifacts {
artifact := artifact
g.Go(func() error {
c, err := resolveCommand(ctx, publisher, artifact)
if err != nil {
return err
}

return executeCommand(c)
})
}

return g.Wait()
}

func executeCommand(c *command) error {
log.WithField("args", c.Args).
WithField("env", c.Env).
Debug("executing command")

// nolint: gosec
var cmd = exec.CommandContext(c.Ctx, c.Args[0], c.Args[1:]...)
cmd.Env = c.Env
if c.Dir != "" {
cmd.Dir = c.Dir
}

entry := log.WithField("cmd", c.Args[0])
cmd.Stderr = logext.NewErrWriter(entry)
cmd.Stdout = logext.NewWriter(entry)

log.WithField("cmd", cmd.Args).Info("publishing")
if err := cmd.Run(); err != nil {
return fmt.Errorf("publishing: %s failed: %w",
c.Args[0], err)
}

log.Debugf("command %s finished successfully", c.Args[0])
return nil
}

func filterArtifacts(artifacts artifact.Artifacts, publisher config.Publisher) []*artifact.Artifact {
filters := []artifact.Filter{
artifact.ByType(artifact.UploadableArchive),
artifact.ByType(artifact.UploadableFile),
artifact.ByType(artifact.LinuxPackage),
artifact.ByType(artifact.UploadableBinary),
}

if publisher.Checksum {
filters = append(filters, artifact.ByType(artifact.Checksum))
}

if publisher.Signature {
filters = append(filters, artifact.ByType(artifact.Signature))
}

var filter = artifact.Or(filters...)

if len(publisher.IDs) > 0 {
filter = artifact.And(filter, artifact.ByIDs(publisher.IDs...))
}

return artifacts.Filter(filter).List()
}

type command struct {
Ctx *context.Context
Dir string
Env []string
Args []string
}

// resolveCommand returns the a command based on publisher template with replaced variables
// Those variables can be replaced by the given context, goos, goarch, goarm and more
func resolveCommand(ctx *context.Context, publisher config.Publisher, artifact *artifact.Artifact) (*command, error) {
var err error

replacements := make(map[string]string)
// TODO: Replacements should be associated only with relevant artifacts/archives
archives := ctx.Config.Archives
if len(archives) > 0 {
replacements = archives[0].Replacements
}

dir := publisher.Dir
if dir != "" {
dir, err = tmpl.New(ctx).
WithArtifact(artifact, replacements).
Apply(dir)
if err != nil {
return nil, err
}
}

cmd := publisher.Cmd
if cmd != "" {
cmd, err = tmpl.New(ctx).
WithArtifact(artifact, replacements).
Apply(cmd)
if err != nil {
return nil, err
}
}

args, err := shellwords.Parse(cmd)
if err != nil {
return nil, err
}

env := make([]string, len(publisher.Env))
for i, e := range publisher.Env {
e, err = tmpl.New(ctx).
WithArtifact(artifact, replacements).
Apply(e)
if err != nil {
return nil, err
}
env[i] = e
}

return &command{
Ctx: ctx,
Dir: dir,
Env: env,
Args: args,
}, nil
}
104 changes: 104 additions & 0 deletions internal/exec/exec_mock.go
@@ -0,0 +1,104 @@
package exec

import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
)

// nolint: gochecknoglobals
var (
MockEnvVar = "GORELEASER_MOCK_DATA"
MockCmd = os.Args[0]
)

type MockData struct {
AnyOf []MockCall `json:"any_of,omitempty"`
}

type MockCall struct {
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
ExpectedArgs []string `json:"args"`
ExpectedEnv []string `json:"env"`
ExitCode int `json:"exit_code"`
}

func (m *MockData) MarshalJSON() ([]byte, error) {
type t MockData
return json.Marshal((*t)(m))
}

func (m *MockData) UnmarshalJSON(b []byte) error {
type t MockData
return json.Unmarshal(b, (*t)(m))
}

// nolint: interfacer
func MarshalMockEnv(data *MockData) string {
b, err := data.MarshalJSON()
if err != nil {
errData := &MockData{
AnyOf: []MockCall{
{
Stderr: fmt.Sprintf("unable to marshal mock data: %s", err),
ExitCode: 1,
},
},
}
b, _ = errData.MarshalJSON()
}

return MockEnvVar + "=" + string(b)
}

func ExecuteMockData(jsonData string) int {
md := &MockData{}
err := md.UnmarshalJSON([]byte(jsonData))
if err != nil {
fmt.Fprintf(os.Stderr, "unable to unmarshal mock data: %s", err)
return 1
}

givenArgs := os.Args[1:]
givenEnv := filterEnv(os.Environ())

if len(md.AnyOf) == 0 {
fmt.Fprintf(os.Stderr, "no mock calls expected. args: %q, env: %q",
givenArgs, givenEnv)
return 1
}

for _, item := range md.AnyOf {
if item.ExpectedArgs == nil {
item.ExpectedArgs = []string{}
}
if item.ExpectedEnv == nil {
item.ExpectedEnv = []string{}
}

if reflect.DeepEqual(item.ExpectedArgs, givenArgs) &&
reflect.DeepEqual(item.ExpectedEnv, givenEnv) {
fmt.Fprint(os.Stdout, item.Stdout)
fmt.Fprint(os.Stderr, item.Stderr)

return item.ExitCode
}
}

fmt.Fprintf(os.Stderr, "no mock calls matched. args: %q, env: %q",
givenArgs, givenEnv)
return 1
}

func filterEnv(vars []string) []string {
for i, env := range vars {
if strings.HasPrefix(env, MockEnvVar+"=") {
return append(vars[:i], vars[i+1:]...)
}
}

return vars
}
15 changes: 15 additions & 0 deletions internal/exec/exec_mock_test.go
@@ -0,0 +1,15 @@
package exec

import (
"os"
"testing"
)

func TestMain(m *testing.M) {
if v := os.Getenv(MockEnvVar); v != "" {
os.Exit(ExecuteMockData(v))
return
}

os.Exit(m.Run())
}

0 comments on commit 8749030

Please sign in to comment.