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)) }