internal/commands/create.go (183 lines of code) (raw):
package commands
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"gitlab.com/gitlab-org/release-cli/internal/flags"
"gitlab.com/gitlab-org/release-cli/internal/gitlab"
)
type httpClientFn func(ctx *cli.Context, log logrus.FieldLogger) (gitlab.HTTPClient, error)
// Create defines the create command to be used by the CLI
func Create(log logrus.FieldLogger, httpClientFn httpClientFn) *cli.Command {
return createReleaseWithFlags(log, httpClientFn, flags.ParameterFlag{}, "create")
}
// CreateFromFile creates a release when passing in a file with data
func CreateFromFile(log logrus.FieldLogger, httpClientFn httpClientFn) *cli.Command {
return createReleaseWithFlags(log, httpClientFn, flags.FileFlag{}, "create-from-file")
}
func createReleaseWithFlags(log logrus.FieldLogger, httpClientFn httpClientFn, flags flags.PassedInFlags, name string) *cli.Command {
flagList := *flags.ListFlags()
return &cli.Command{
Name: name,
Usage: "Create a Release using GitLab's Releases API https://docs.gitlab.com/ee/api/releases/#create-a-release",
Action: func(ctx *cli.Context) error {
client, err := httpClientFn(ctx, log)
if err != nil {
return err
}
return createRelease(ctx, log, client)
},
Before: flags.BeforeHook(flagList),
Subcommands: nil,
Flags: flagList,
}
}
func createRelease(ctx *cli.Context, log logrus.FieldLogger, httpClient gitlab.HTTPClient) error {
projectID := ctx.String(flags.ProjectID)
serverURL := ctx.String(flags.ServerURL)
jobToken := ctx.String(flags.JobToken)
privateToken := ctx.String(flags.PrivateToken)
l := log.WithFields(logrus.Fields{
"command": ctx.Command.Name,
flags.ServerURL: serverURL,
flags.ProjectID: projectID,
flags.Name: ctx.String(flags.Name),
flags.TagName: ctx.String(flags.TagName),
flags.TagMessage: ctx.String(flags.TagMessage),
flags.Ref: ctx.String(flags.Ref),
flags.CatalogPublish: ctx.Bool(flags.CatalogPublish),
})
l.Info("Creating Release...")
gitlabClient, err := gitlab.New(serverURL, jobToken, privateToken, projectID, httpClient, log)
if err != nil {
return fmt.Errorf("failed to create GitLab client: %w", err)
}
crr, err := newCreateReleaseReq(ctx, log)
if err != nil {
return fmt.Errorf("new CreateReleaseRequest: %w", err)
}
release, err := gitlabClient.CreateRelease(ctx.Context, crr)
if err != nil {
return fmt.Errorf("failed to create release: %w", err)
}
printReleaseOutput(ctx.App.Writer, release, log)
l.Info("release created successfully!")
return nil
}
func getDescription(description string, log logrus.FieldLogger) (string, error) {
l := log.WithFields(logrus.Fields{
"description": description,
})
if description == "" || strings.Contains(strings.TrimSpace(description), " ") {
return description, nil
}
baseDir, err := os.Getwd()
if err != nil {
l.WithError(err).Warn("failed to get working directory, using string value for --description")
return description, nil
}
filePath, err := securejoin.SecureJoin(baseDir, description)
if err != nil {
l.WithError(err).Warn("failed to resolve filepath, using string value for --description")
return description, nil
}
content, err := os.ReadFile(filepath.Clean(filePath))
if errors.Is(err, os.ErrNotExist) {
l.WithError(err).Warn("file does not exist, using string value for --description")
return description, nil
}
return string(content), err
}
func newCreateReleaseReq(ctx *cli.Context, log logrus.FieldLogger) (*gitlab.CreateReleaseRequest, error) {
assetsLink := ctx.StringSlice(flags.AssetsLink)
description := ctx.String(flags.Description)
releasedAt := ctx.String(flags.ReleasedAt)
legacyCatalogPublish := new(bool)
assets, err := gitlab.ParseAssets(assetsLink)
if err != nil {
return nil, fmt.Errorf("failed to parse assets: %w", err)
}
descriptionString, err := getDescription(description, log)
if err != nil {
return nil, err
}
// We define `legacyCatalogPublish` as a pointer to assign `nil` to it along with boolean values.
// That's because we don't want to send this parameter via API if it's not `true`.
*legacyCatalogPublish = ctx.Bool(flags.CatalogPublish)
if !*legacyCatalogPublish {
legacyCatalogPublish = nil
}
crr := &gitlab.CreateReleaseRequest{
ID: ctx.String(flags.ProjectID),
Name: ctx.String(flags.Name),
Description: descriptionString,
TagName: ctx.String(flags.TagName),
TagMessage: ctx.String(flags.TagMessage),
Ref: ctx.String(flags.Ref),
Assets: assets,
Milestones: ctx.StringSlice(flags.Milestone),
LegacyCatalogPublish: legacyCatalogPublish,
}
if releasedAt != "" {
timeReleasedAt, err := gitlab.ParseDateTime(releasedAt)
if err != nil {
return nil, fmt.Errorf("failed to parse released-at: %w", err)
}
crr.ReleasedAt = &timeReleasedAt
}
return crr, nil
}
func printReleaseOutput(w io.Writer, release *gitlab.ReleaseResponse, logger logrus.FieldLogger) {
printReleaseDetails(w, release)
if release.Assets != nil {
printAssetNamesAndUrls(w, release)
if logger.(*logrus.Entry).Logger.Level == logrus.DebugLevel {
printAllAssetsAsJSON(w, release, logger)
}
}
printMilestone(w, release)
fmt.Fprintf(w, "See all available releases here: %s/-/releases\n", os.Getenv("CI_PROJECT_URL"))
}
func printReleaseDetails(w io.Writer, release *gitlab.ReleaseResponse) {
fmt.Fprintf(w, `
Tag: %s
Name: %s
Description: %s
Created At: %s
Released At: %s
`,
release.TagName,
release.Name,
release.Description,
release.CreatedAt,
release.ReleasedAt,
)
}
func printAssetNamesAndUrls(w io.Writer, release *gitlab.ReleaseResponse) {
for _, link := range release.Assets.Links {
fmt.Fprintf(w, `
Asset::Link::Name: %s
Asset::Link::URL: %s
`,
link.Name, link.URL)
}
}
func printMilestone(w io.Writer, release *gitlab.ReleaseResponse) {
for _, m := range release.Milestones {
fmt.Fprintf(w, `
Milestone: %s - %s
`, m.Title, m.Description)
}
}
func printAllAssetsAsJSON(w io.Writer, release *gitlab.ReleaseResponse, logger logrus.FieldLogger) {
o, err := json.MarshalIndent(release.Assets.Links, "", " ")
if err != nil {
logger.WithError(err).Error("parse asset JSON object for debugging")
return
}
logger.Debug("JSON object for generated assets in received response:")
fmt.Fprintln(w, string(o))
}