1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-03-29 21:47:01 +02:00

feat: Implemented upload mode for Artifactory pipeline

The Artifactory pipeline now supports two different
types of upload modes:

- binary: Only the raw binaries will be uploaded
- archive: All artifacts that are generated will be uploaded
This commit is contained in:
Andy Grunwald 2017-12-09 20:54:04 +01:00
parent 27a9abc73b
commit d875dd0dd5
5 changed files with 201 additions and 90 deletions

View File

@ -200,6 +200,7 @@ type Artifactory struct {
Target string `yaml:",omitempty"`
Name string `yaml:",omitempty"`
Username string `yaml:",omitempty"`
Mode string `yaml:",omitempty"`
// Capture all undefined fields and should be empty after loading
XXX map[string]interface{} `yaml:",inline"`

View File

@ -18,7 +18,7 @@ upload target and a usernameto your `.goreleaser.yml` file:
```yaml
artifactories:
- name: production
target: http://<Your-Instance>:8081/artifactory/example-repo-local/{{ .ProjectName }}/{{ .Version }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
target: http://<Your-Instance>:8081/artifactory/example-repo-local/{{ .ProjectName }}/{{ .Version }}/
username: goreleaser
```
@ -29,15 +29,15 @@ Prerequisites:
### Target
The `target` is the final URL of _without_ the binary name.
The `target` is the URL to upload the artifacts to (_without_ the name of the artifact).
A configuration for `goreleaser` with the target
An example configuration for `goreleaser` in upload mode `binary` with the target can look like
```
http://artifacts.company.com:8081/artifactory/example-repo-local/{{ .ProjectName }}/{{ .Version }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
```
will result in a final deployment like
and will result in a final deployment like
```
http://artifacts.company.com:8081/artifactory/example-repo-local/goreleaser/1.0.0/Darwin/x86_64/goreleaser
@ -45,12 +45,14 @@ http://artifacts.company.com:8081/artifactory/example-repo-local/goreleaser/1.0.
Support variables:
- Os
- Arch
- Arm
- Version
- Tag
- ProjectName
- Os
- Arch
- Arm
*Attention*: Variables _Os_, _Arch_ and _Arm_ are only supported in upload mode `binary`.
### Password / API Key
@ -78,8 +80,13 @@ artifactories:
-
# Unique name of your artifactory instance. Used to identify the instance
name: production
# Upload mode. Valid options are `binary` and `archive`.
# If mode is `archive`, variables _Os_, _Arch_ and _Arm_ for target name are not supported.
# In that case these variables are empty.
# Default is `archive`.
mode: archive
# URL of your Artifactory instance + path to deploy to
target: http://artifacts.company.com:8081/artifactory/example-repo-local/{{ .ProjectName }}/{{ .Version }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
target: http://artifacts.company.com:8081/artifactory/example-repo-local/{{ .ProjectName }}/{{ .Version }}/
# User that will be used for the deployment
username: deployuser
```

View File

@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/goreleaser/goreleaser/config"
@ -46,14 +47,35 @@ type artifactoryChecksums struct {
SHA256 string `json:"sha256,omitempty"`
}
const (
modeBinary = "binary"
modeArchive = "archive"
)
// Pipe for Artifactory
type Pipe struct{}
// Description of the pipe
// String returns the description of the pipe
func (Pipe) String() string {
return "releasing to Artifactory"
}
// Default sets the pipe defaults
func (Pipe) Default(ctx *context.Context) error {
if l := len(ctx.Config.Artifactories); l == 0 {
return nil
}
// Check if a mode was set
for i := range ctx.Config.Artifactories {
if ctx.Config.Artifactories[i].Mode == "" {
ctx.Config.Artifactories[i].Mode = "archive"
}
}
return nil
}
// Run the pipe
//
// Docs: https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-Example-DeployinganArtifact
@ -91,10 +113,34 @@ func doRun(ctx *context.Context) error {
return pipeline.Skip("--skip-publish is set")
}
// Loop over all builds, because we want to publish
// every build to Artifactory
for _, build := range ctx.Config.Builds {
if err := runPipeOnBuild(ctx, build); err != nil {
// Handle every configured artifactory instance
for _, instance := range ctx.Config.Artifactories {
// We support two different modes
// - "archive": Upload all artifacts
// - "binary": Upload only the raw binaries
var err error
switch v := strings.ToLower(instance.Mode); v {
case modeArchive:
err = runPipeForModeArchive(ctx, instance)
case modeBinary:
// Loop over all builds, because we want to publish every build to Artifactory
for _, build := range ctx.Config.Builds {
if err := runPipeForModeBinary(ctx, instance, build); err != nil {
return err
}
}
default:
err := fmt.Errorf("artifactory: mode \"%s\" not supported", v)
log.WithFields(log.Fields{
"instance": instance.Name,
"mode": v,
}).Error(err.Error())
return err
}
if err != nil {
return err
}
}
@ -102,13 +148,116 @@ func doRun(ctx *context.Context) error {
return nil
}
// runPipeOnBuild runs the pipe for every configured build
func runPipeOnBuild(ctx *context.Context, build config.Build) error {
// runPipeForModeArchive uploads all ctx.Artifacts to instance
func runPipeForModeArchive(ctx *context.Context, instance config.Artifactory) error {
sem := make(chan bool, ctx.Parallelism)
var g errgroup.Group
// Lets generate the build matrix, because we want to publish
// every target to Artifactory
// Get all artifacts and upload them
for _, artifact := range ctx.Artifacts {
sem <- true
artifact := artifact
g.Go(func() error {
defer func() {
<-sem
}()
return uploadArchive(ctx, instance, artifact)
})
}
return g.Wait()
}
// uploadArchive will upload artifact in mode archive
func uploadArchive(ctx *context.Context, instance config.Artifactory, artifact string) error {
// Generate the target url
targetURL, err := resolveTargetTemplate(ctx, instance, nil)
if err != nil {
msg := "artifactory: error while building the target url"
log.WithField("instance", instance.Name).WithError(err).Error(msg)
return errors.Wrap(err, msg)
}
// Handle the artifact
var path = filepath.Join(ctx.Config.Dist, artifact)
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close() // nolint: errcheck
_, name := filepath.Split(path)
// The target url needs to contain the artifact name
if !strings.HasSuffix(targetURL, "/") {
targetURL += "/"
}
targetURL += name
return uploadAssetAndLog(ctx, instance, targetURL, file)
}
// uploadBinary will upload the current build and the current target in mode binary
func uploadBinary(ctx *context.Context, instance config.Artifactory, build config.Build, target buildtarget.Target) error {
binary, err := getBinaryForUploadPerBuild(ctx, target)
if err != nil {
return err
}
// Generate the target url
targetURL, err := resolveTargetTemplate(ctx, instance, &target)
if err != nil {
msg := "artifactory: error while building the target url"
log.WithField("instance", instance.Name).WithError(err).Error(msg)
return errors.Wrap(err, msg)
}
// Handle the artifact
file, err := os.Open(binary.Path)
if err != nil {
return err
}
defer file.Close() // nolint: errcheck
// The target url needs to contain the artifact name
if !strings.HasSuffix(targetURL, "/") {
targetURL += "/"
}
targetURL += binary.Name
return uploadAssetAndLog(ctx, instance, targetURL, file)
}
// uploadAssetAndLog uploads file to target and logs all actions
func uploadAssetAndLog(ctx *context.Context, instance config.Artifactory, target string, file *os.File) error {
secret := os.Getenv(fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)))
artifact, _, err := uploadAssetToArtifactory(ctx, target, instance.Username, secret, file)
if err != nil {
msg := "artifactory: upload failed"
log.WithError(err).WithFields(log.Fields{
"instance": instance.Name,
"username": instance.Username,
}).Error(msg)
return errors.Wrap(err, msg)
}
log.WithFields(log.Fields{
"instance": instance.Name,
"mode": instance.Mode,
"uri": artifact.DownloadURI,
}).Info("uploaded successful")
return nil
}
// runPipeForModeBinary uploads all configured builds to instance
func runPipeForModeBinary(ctx *context.Context, instance config.Artifactory, build config.Build) error {
sem := make(chan bool, ctx.Parallelism)
var g errgroup.Group
// Lets generate the build matrix, because we want
// to publish every target to Artifactory
for _, target := range buildtarget.All(build) {
sem <- true
target := target
@ -118,75 +267,14 @@ func runPipeOnBuild(ctx *context.Context, build config.Build) error {
<-sem
}()
return upload(ctx, build, target)
return uploadBinary(ctx, instance, build, target)
})
}
return g.Wait()
}
// upload runs the pipe action of the current build and the current target.
// This is where the real action take place.
// The real action is
// - Identify the binary
// - Loop over all configured artifactories
// - Resolve all variables in the target name
// - Upload the binary to the artifactory
func upload(ctx *context.Context, build config.Build, target buildtarget.Target) error {
binary, err := getBinaryForUploadPerBuild(ctx, target)
if err != nil {
return err
}
// Loop over all configured Artifactory instances
for _, instance := range ctx.Config.Artifactories {
secret := os.Getenv(fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)))
// Generate name of target
uploadTarget, err := buildTargetName(ctx, instance, target)
if err != nil {
msg := "artifactory: error while building the target name"
log.WithError(err).Error(msg)
return errors.Wrap(err, msg)
}
// The upload url to Artifactory needs the binary name
// Here we add the binary to the target url
if !strings.HasSuffix(uploadTarget, "/") {
uploadTarget += "/"
}
uploadTarget += binary.Name
// Upload the binary to Artifactory
file, err := os.Open(binary.Path)
if err != nil {
return err
}
defer file.Close() // nolint: errcheck
artifact, _, err := uploadBinaryToArtifactory(ctx, uploadTarget, instance.Username, secret, file)
if err != nil {
msg := "artifactory: upload failed"
log.WithError(err).WithFields(log.Fields{
"instance": instance.Name,
"username": instance.Username,
}).Error(msg)
return errors.Wrap(err, msg)
}
log.WithFields(log.Fields{
"instance": instance.Name,
"target": target.PrettyString(),
"username": instance.Username,
"uri": artifact.DownloadURI,
}).Info("uploaded successful")
}
return nil
}
// getBinaryForUploadPerBuild determines the correct binary
// for the upload
// getBinaryForUploadPerBuild determines the correct binary for the upload
func getBinaryForUploadPerBuild(ctx *context.Context, target buildtarget.Target) (*context.Binary, error) {
var group = ctx.Binaries[target.String()]
if group == nil {
@ -208,25 +296,32 @@ func getBinaryForUploadPerBuild(ctx *context.Context, target buildtarget.Target)
// targetData is used as a template struct for
// Artifactory.Target
type targetData struct {
Os string
Arch string
Arm string
Version string
Tag string
ProjectName string
// Only supported in mode binary
Os string
Arch string
Arm string
}
// buildTargetName returns the name resolved target name with replaced variables
// resolveTargetTemplate returns the resolved target template with replaced variables
// Those variables can be replaced by the given context, goos, goarch, goarm and more
func buildTargetName(ctx *context.Context, artifactory config.Artifactory, target buildtarget.Target) (string, error) {
func resolveTargetTemplate(ctx *context.Context, artifactory config.Artifactory, target *buildtarget.Target) (string, error) {
data := targetData{
Os: replace(ctx.Config.Archive.Replacements, target.OS),
Arch: replace(ctx.Config.Archive.Replacements, target.Arch),
Arm: replace(ctx.Config.Archive.Replacements, target.Arm),
Version: ctx.Version,
Tag: ctx.Git.CurrentTag,
ProjectName: ctx.Config.ProjectName,
}
// Only supported in mode binary
if target != nil {
data.Os = replace(ctx.Config.Archive.Replacements, target.OS)
data.Arch = replace(ctx.Config.Archive.Replacements, target.Arch)
data.Arm = replace(ctx.Config.Archive.Replacements, target.Arm)
}
var out bytes.Buffer
t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target)
if err != nil {
@ -244,8 +339,8 @@ func replace(replacements map[string]string, original string) string {
return result
}
// uploadBinaryToArtifactory uploads the binary file to target
func uploadBinaryToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) {
// uploadAssetToArtifactory uploads the asset file to target
func uploadAssetToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) {
stat, err := file.Stat()
if err != nil {
return nil, nil, err

View File

@ -189,11 +189,13 @@ func TestRunPipe(t *testing.T) {
Artifactories: []config.Artifactory{
{
Name: "production-us",
Mode: "binary",
Target: fmt.Sprintf("%s/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}", server.URL),
Username: "deployuser",
},
{
Name: "production-eu",
Mode: "binary",
Target: fmt.Sprintf("%s/production-repo-remote/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}", server.URL),
Username: "productionuser",
},
@ -258,6 +260,7 @@ func TestRunPipe_BadCredentials(t *testing.T) {
Artifactories: []config.Artifactory{
{
Name: "production",
Mode: "binary",
Target: fmt.Sprintf("%s/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}", server.URL),
Username: "deployuser",
},
@ -293,6 +296,7 @@ func TestRunPipe_NoFile(t *testing.T) {
Artifactories: []config.Artifactory{
{
Name: "production",
Mode: "binary",
Target: "http://artifacts.company.com/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
Username: "deployuser",
},
@ -338,6 +342,7 @@ func TestRunPipe_UnparsableTarget(t *testing.T) {
Artifactories: []config.Artifactory{
{
Name: "production",
Mode: "binary",
Target: "://artifacts.company.com/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
Username: "deployuser",
},
@ -380,6 +385,7 @@ func TestRunPipe_DirUpload(t *testing.T) {
Artifactories: []config.Artifactory{
{
Name: "production",
Mode: "binary",
Target: "http://artifacts.company.com/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
Username: "deployuser",
},

View File

@ -7,6 +7,7 @@ import (
"github.com/goreleaser/goreleaser/context"
"github.com/goreleaser/goreleaser/pipeline"
"github.com/goreleaser/goreleaser/pipeline/archive"
"github.com/goreleaser/goreleaser/pipeline/artifactory"
"github.com/goreleaser/goreleaser/pipeline/brew"
"github.com/goreleaser/goreleaser/pipeline/build"
"github.com/goreleaser/goreleaser/pipeline/checksums"
@ -31,6 +32,7 @@ var defaulters = []pipeline.Defaulter{
fpm.Pipe{},
checksums.Pipe{},
docker.Pipe{},
artifactory.Pipe{},
brew.Pipe{},
}