Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for custom publishers (#1481)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
- Loading branch information
1 parent
31fedc4
commit 8749030
Showing
14 changed files
with
710 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) | ||
} |
Oops, something went wrong.