feat: add `release create` command

This commit is contained in:
Clement Sam 2020-12-29 22:47:57 +00:00
parent dbb7f2dd46
commit c707d7d3f7
7 changed files with 497 additions and 1 deletions

View File

@ -12,6 +12,9 @@ workflow:
- if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable$/'
#
stages:
- test
default:
image: golang:1.15
@ -37,3 +40,31 @@ code_navigation:
artifacts:
reports:
lsif: dump.lsif
.test:
variables:
GOPATH: "/builds/go"
GIT_CLONE_PATH: $GOPATH/src/github.com/profclems/glab
GITLAB_TOKEN: "qRC87Xg9Wd46RhB8J8sp"
CODECOV_TOKEN: "1597edbc-8760-42c1-ab79-c192ad151717"
before_script:
# Add a non-sensitive ssh key for test to work.
- mkdir ~/.ssh && chmod 700 ~/.ssh
- echo -e "Host *\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
- echo -e "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCc54ULcDSmuQBMaM52dqxNMETAlwn01AF79vJb6Nw3SzkNKEMY1pBttM1U8wz7dHpVteAQGCU2AH+T2UhswZWPIzcMfVwB+1RgcIkHW184V5FX1xxqo4Xx6CC6LTSq3md/EmXfQR7gAwaKQHy6txxYpEprlhw+EWICgD5znUMprRRbcsb2FoAdsodMWSl5aRIyC1rA06SKKwqN09o4mgNhv49McyzDnUCGmSTc0oQpygPadDRqW46iCkMdiJa1fuP9pSGkAuTaQayILbDenAM3cV7LqsIm7tOXLPW5fQm9FB91ftRLXxk1Mp6lMYqNc5eGoR4ATzHn7M/VHbBaqVlp profclems@ProfClems" > ~/.ssh/id_rsa.pub
- echo -e "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAnOeFC3A0prkATGjOdnasTTBEwJcJ9NQBe/byW+jcN0s5DShDGNaQ\nbbTNVPMM+3R6VbXgEBglNgB/k9lIbMGVjyM3DH1cAftUYHCJB1tfOFeRV9ccaqOF8eggui\n00qt5nfxJl30Ee4AMGikB8urccWKRKa5YcPhFiAoA+c51DKa0UW3LG9haAHbKHTFkpeWkS\nMgtawNOkiisKjdPaOJoDYb+PTHMsw51Ahpkk3NKEKcoD2nQ0aluOogpDHYiWtX7j/aUhpA\nLk2kGsiC2w3pwDN3Fey6rCJu7Tlyz1uX0JvRQfdX7US18ZNTKepTGKjXOXhqEeAE8x5+zP\n1R2wWqlZaQAAA8j2Yikl9mIpJQAAAAdzc2gtcnNhAAABAQCc54ULcDSmuQBMaM52dqxNME\nTAlwn01AF79vJb6Nw3SzkNKEMY1pBttM1U8wz7dHpVteAQGCU2AH+T2UhswZWPIzcMfVwB\n+1RgcIkHW184V5FX1xxqo4Xx6CC6LTSq3md/EmXfQR7gAwaKQHy6txxYpEprlhw+EWICgD\n5znUMprRRbcsb2FoAdsodMWSl5aRIyC1rA06SKKwqN09o4mgNhv49McyzDnUCGmSTc0oQp\nygPadDRqW46iCkMdiJa1fuP9pSGkAuTaQayILbDenAM3cV7LqsIm7tOXLPW5fQm9FB91ft\nRLXxk1Mp6lMYqNc5eGoR4ATzHn7M/VHbBaqVlpAAAAAwEAAQAAAQAJvxv1nO+4V4+cL3p7\nw11qog/zQq6cpbq935YofWuIh8Swe4rHdTSdi/ihSUPKLu8WeejENyvAkgFaxsmH7/KBZL\nebsAHSIbGZGAR7D4L3tgDSSwt52FSOtVOrHPnDj3MwYo0vdBUd5zI1zlGxK4S4QORakIWK\nmXvUGfFHL0KnyP6uH3Z/j8hQaAE8TIVrGM6PyLes3NWFTlXIakrV8jiJc5bxVnhzDcIKdf\n9JUYGO3DUCQI+pdkCfMNptbBuGwqHjruruGYMfh+mx7FrnpjbJ3y2TG01pcXBeCRIJNm2/\n4htjlAxdz2Zxa8JL437s56Jf4ZtDOYt367dhTMRBleoxAAAAgDo0de+4nNSVb/H3aGo5Y0\nLg/q/npYUNVSvZ9R0GfRY0qDbNCaKeqbyJDkReRn5R/KKT9+Gx0/zqXGNi1ubnGtxqCCmC\nYHDys0PSw6yAEDZ3JOeYWEIO7ntH0DKdErcEUj+FqatdIpoZZO48XZDSXzO1+B1n2y+AML\nfyMggdFXIBAAAAgQDMnIFss9kLpnqC5QOAiDO+jsnHwNHENBgQKE9/jM6gagaK3T4TDkJl\nPszXRDE11LSBCxahL5rxwWDog1vG67tnbfznjhGm8Fd+mEs6omuQ5pA0o6Qx40xtkfn3vi\nj4NLkpo5rYg0DTtciDgGeibtt1oT1EhKZ59ystGdFgSGWxtQAAAIEAxE+xPCLHzqEUUlxI\nSJC6+e2hUT/9s64OwmH9pCrQ8+96G9TtbR9NFD1LSArnLXvl6ooYODw0QDjLdgCUQj63r7\nqOV9QjWKL6xD1atN37WFbrIpem8pFOJWinYrQtddANSBd5tp5QsDkI58ovqhdDuYoVfL1y\nmEfWvucOVlZraWUAAAATcHJvZmNsZW1zQFByb2ZDbGVtcw==\n-----END OPENSSH PRIVATE KEY-----" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa*
- eval `ssh-agent -s`
- ssh-add ~/.ssh/id_rsa
script:
- go version
- export PATH=$GOPATH/bin:$PATH
- go get -t -v ./...
- make test
after_script:
- bash <(curl -s https://codecov.io/bash)
test:
stage: test
extends: .test
image: golang:1.15

View File

@ -2,6 +2,19 @@ package api
import "github.com/xanzy/go-gitlab"
var CreateRelease = func(client *gitlab.Client, projectID interface{}, opts *gitlab.CreateReleaseOptions) (*gitlab.Release, error) {
if client == nil {
client = apiClient.Lab()
}
release, _, err := client.Releases.CreateRelease(projectID, opts)
if err != nil {
return nil, err
}
return release, nil
}
var GetRelease = func(client *gitlab.Client, projectID interface{}, tag string) (*gitlab.Release, error) {
if client == nil {
client = apiClient.Lab()
@ -14,6 +27,7 @@ var GetRelease = func(client *gitlab.Client, projectID interface{}, tag string)
return release, nil
}
var ListReleases = func(client *gitlab.Client, projectID interface{}, opts *gitlab.ListReleasesOptions) ([]*gitlab.Release, error) {
if client == nil {
client = apiClient.Lab()

View File

@ -1,6 +1,10 @@
package cmdutils
import "errors"
import (
"errors"
"github.com/spf13/cobra"
)
// FlagError is the kind of error raised in flag processing
type FlagError struct {
@ -17,3 +21,16 @@ func (fe FlagError) Unwrap() error {
// SilentError is an error that triggers exit code 1 without any error messaging
var SilentError = errors.New("SilentError")
func MinimumArgs(n int, msg string) cobra.PositionalArgs {
if msg == "" {
return cobra.MinimumNArgs(1)
}
return func(cmd *cobra.Command, args []string) error {
if len(args) < n {
return &FlagError{Err: errors.New(msg)}
}
return nil
}
}

View File

@ -0,0 +1,324 @@
package create
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"time"
"github.com/profclems/glab/commands/release/upload"
"github.com/profclems/glab/internal/glinstance"
"github.com/profclems/glab/internal/glrepo"
"github.com/profclems/glab/pkg/iostreams"
"github.com/profclems/glab/commands/cmdutils"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)
type CreateOpts struct {
Name string
Ref string
TagName string
Notes string
NotesFile string
Milestone []string
AssetLinksAsJson string
ReleasedAt string
AssetLinks []*upload.ReleaseAsset
AssetFiles []*upload.ReleaseFile
IO *iostreams.IOStreams
HTTPClient func() (*gitlab.Client, error)
BaseRepo func() (glrepo.Interface, error)
}
func NewCmdCreate(f *cmdutils.Factory, runE func(opts *CreateOpts) error) *cobra.Command {
opts := &CreateOpts{
IO: f.IO,
}
cmd := &cobra.Command{
Use: "create <tag> [<files>...]",
Short: "Create a new GitLab Release for a repository",
Long: `Create a new GitLab Release for a repository.
You need push access to the repository to create a Release.`,
Args: cmdutils.MinimumArgs(1, "no tag name provided"),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
opts.HTTPClient = f.HttpClient
opts.BaseRepo = f.BaseRepo
opts.TagName = args[0]
opts.AssetFiles, err = AssetsFromArgs(args[1:])
if err != nil {
return err
}
if opts.AssetLinksAsJson != "" {
err := json.Unmarshal([]byte(opts.AssetLinksAsJson), &opts.AssetLinks)
if err != nil {
return fmt.Errorf("failed to parse JSON string: %w", err)
}
}
if opts.NotesFile != "" {
if opts.NotesFile == "-" {
b, err := ioutil.ReadAll(opts.IO.In)
_ = opts.IO.In.Close()
if err != nil {
return err
}
opts.Notes = string(b)
}
}
if runE != nil {
return runE(opts)
}
return createRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Name, "name", "n", "", "The release name or title")
cmd.Flags().StringVarP(&opts.Ref, "ref", "r", "", "If a tag specified doesnt exist, the release is created from ref and tagged with the specified tag name. It can be a commit SHA, another tag name, or a branch name.")
cmd.Flags().StringVarP(&opts.Notes, "notes", "N", "", "The release notes/description. You can use Markdown")
cmd.Flags().StringVarP(&opts.NotesFile, "notes-file", "F", "", "Read release notes `file`")
cmd.Flags().StringVarP(&opts.ReleasedAt, "released-at", "D", "", "The `date` when the release is/was ready. Defaults to the current datetime. Expected in ISO 8601 format (2019-03-15T08:00:00Z)")
cmd.Flags().StringSliceVarP(&opts.Milestone, "milestone", "m", []string{}, "The title of each milestone the release is associated with")
cmd.Flags().StringVarP(&opts.AssetLinksAsJson, "assets", "a", "", "`JSON` string representation of assets links (e.g. --assets='[{\"name\": \"Asset1\", \"url\":\"https://<domain>/some/location/1\", \"link_type\": \"other\", \"filepath\": \"path/to/file\" }]'")
return cmd
}
func createRun(opts *CreateOpts) error {
client, err := opts.HTTPClient()
if err != nil {
return err
}
repo, err := opts.BaseRepo()
if err != nil {
return err
}
color := opts.IO.Color()
fmt.Fprintf(opts.IO.StdErr, "%s creating or updating release %s=%s %s=%s\n",
color.ProgressIcon(),
color.Blue("repo"), repo.FullName(),
color.Blue("tag"), opts.TagName)
release, resp, err := client.Releases.GetRelease(repo.FullName(), opts.TagName)
if err != nil && (resp == nil || resp.StatusCode != 403) {
return err
}
var releasedAt time.Time
if opts.ReleasedAt != "" {
// Parse the releasedAt to the expected format of the API
// From the API docs "Expected in ISO 8601 format (2019-03-15T08:00:00Z)".
releasedAt, err = time.Parse(time.RFC3339[:len(opts.ReleasedAt)], opts.ReleasedAt)
if err != nil {
return err
}
}
if resp.StatusCode == 403 || release == nil {
release, _, err = client.Releases.CreateRelease(repo.FullName(), &gitlab.CreateReleaseOptions{
Name: &opts.Name,
Description: &opts.Notes,
Ref: &opts.Ref,
TagName: &opts.TagName,
ReleasedAt: &releasedAt,
Milestones: opts.Milestone,
})
if err != nil {
return err
}
fmt.Fprintf(opts.IO.StdErr, "%s, release created\t%s=%s\n", color.ProgressIcon(),
color.Blue("url"), fmt.Sprintf("%s://%s/%s/releases/tag/%s",
glinstance.OverridableDefaultProtocol(), glinstance.OverridableDefault(),
repo.FullName(), release.TagName))
} else {
apiOpts := &gitlab.UpdateReleaseOptions{}
if opts.Notes != "" {
apiOpts.Description = &opts.Notes
}
if opts.Name != "" {
apiOpts.Name = &opts.Name
}
if opts.ReleasedAt != "" {
apiOpts.ReleasedAt = &releasedAt
}
if len(opts.Milestone) > 0 {
apiOpts.Milestones = opts.Milestone
}
release, _, err = client.Releases.UpdateRelease(repo.FullName(), opts.TagName, apiOpts)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.StdErr, "%s release updated\t%s=%s\n", color.ProgressIcon(),
color.Blue("url"), fmt.Sprintf("%s://%s/%s/-/releases/tag/%s",
glinstance.OverridableDefaultProtocol(), glinstance.OverridableDefault(),
repo.FullName(), release.TagName))
}
// upload files and create asset link
if opts.AssetFiles != nil || opts.AssetLinks != nil {
fmt.Fprintf(opts.IO.StdErr, "\n%s Uploading release assets\n", color.ProgressIcon())
uploadCtx := upload.Context{
IO: opts.IO,
Client: client,
AssetsLinks: opts.AssetLinks,
AssetFiles: opts.AssetFiles,
}
if err = uploadCtx.UploadFiles(repo.FullName(), release.TagName); err != nil {
return err
}
// create asset link for assets provided as json
if err = uploadCtx.CreateReleaseAssetLinks(repo.FullName(), release.TagName); err != nil {
return err
}
}
if len(opts.Milestone) > 0 {
// close all associated milestones
for _, milestone := range opts.Milestone {
// run loading msg
opts.IO.StartSpinner("closing milestone %q", milestone)
// close milestone
err := closeMilestone(opts, milestone)
// stop loading
opts.IO.StopSpinner("")
if err != nil {
fmt.Fprintln(opts.IO.StdErr, color.FailedIcon(), err.Error())
} else {
fmt.Fprintf(opts.IO.StdErr, "%s closed milestone %q\n", color.GreenCheck(), milestone)
}
}
}
fmt.Fprintf(opts.IO.StdErr, "%s release succeeded", color.GreenCheck())
return nil
}
func getMilestoneByTitle(c *CreateOpts, title string) (*gitlab.Milestone, error) {
opts := &gitlab.ListMilestonesOptions{
Title: &title,
}
client, err := c.HTTPClient()
if err != nil {
return nil, err
}
repo, err := c.BaseRepo()
if err != nil {
return nil, err
}
for {
milestones, resp, err := client.Milestones.ListMilestones(repo.FullName(), opts)
if err != nil {
return nil, err
}
for _, milestone := range milestones {
if milestone != nil && milestone.Title == title {
return milestone, nil
}
}
if resp.NextPage == 0 {
break
}
opts.Page = resp.NextPage
}
return nil, nil
}
// CloseMilestone closes a given milestone.
func closeMilestone(c *CreateOpts, title string) error {
client, err := c.HTTPClient()
if err != nil {
return err
}
repo, err := c.BaseRepo()
if err != nil {
return err
}
milestone, err := getMilestoneByTitle(c, title)
if err != nil {
return err
}
if milestone == nil {
return fmt.Errorf("could not find milestone: %q", title)
}
closeStateEvent := "close"
opts := &gitlab.UpdateMilestoneOptions{
Description: &milestone.Description,
DueDate: milestone.DueDate,
StartDate: milestone.StartDate,
StateEvent: &closeStateEvent,
Title: &milestone.Title,
}
_, _, err = client.Milestones.UpdateMilestone(
repo.FullName(),
milestone.ID,
opts,
)
return err
}
func AssetsFromArgs(args []string) (assets []*upload.ReleaseFile, err error) {
for _, arg := range args {
var label string
fn := arg
if idx := strings.IndexRune(arg, '#'); idx > 0 {
fn = arg[0:idx]
label = arg[idx+1:]
}
var fi os.FileInfo
fi, err = os.Stat(fn)
if err != nil {
return
}
if label == "" {
label = fi.Name()
}
assets = append(assets, &upload.ReleaseFile{
Open: func() (io.ReadCloser, error) {
return os.Open(fn)
},
Name: fi.Name(),
Label: label,
Path: fn,
})
}
return
}

View File

@ -2,6 +2,7 @@ package release
import (
"github.com/profclems/glab/commands/cmdutils"
releaseCreateCmd "github.com/profclems/glab/commands/release/create"
releaseListCmd "github.com/profclems/glab/commands/release/list"
"github.com/spf13/cobra"
@ -15,7 +16,9 @@ func NewCmdRelease(f *cmdutils.Factory) *cobra.Command {
}
cmdutils.EnableRepoOverride(releaseCmd, f)
releaseCmd.AddCommand(releaseListCmd.NewCmdReleaseList(f))
releaseCmd.AddCommand(releaseCreateCmd.NewCmdCreate(f, nil))
return releaseCmd
}

View File

@ -0,0 +1,103 @@
package upload
import (
"fmt"
"io"
"github.com/profclems/glab/internal/glinstance"
"github.com/profclems/glab/pkg/iostreams"
"github.com/xanzy/go-gitlab"
)
type ReleaseAsset struct {
Name *string `json:"name,omitempty"`
URL *string `json:"url,omitempty"`
FilePath *string `json:"filepath,omitempty"`
LinkType *gitlab.LinkTypeValue `json:"link_type,omitempty"`
}
type ReleaseFile struct {
Open func() (io.ReadCloser, error)
Name string
Label string
Path string
}
func CreateLink(c *gitlab.Client, projectID, tagName string, asset *ReleaseAsset) (*gitlab.ReleaseLink, error) {
releaseLink, _, err := c.ReleaseLinks.CreateReleaseLink(projectID, tagName, &gitlab.CreateReleaseLinkOptions{
Name: asset.Name,
URL: asset.URL,
FilePath: asset.FilePath,
LinkType: asset.LinkType,
})
if err != nil {
return nil, err
}
return releaseLink, nil
}
type Context struct {
Client *gitlab.Client
IO *iostreams.IOStreams
AssetFiles []*ReleaseFile
AssetsLinks []*ReleaseAsset
}
// UploadFiles uploads a file into a release repository.
func (c *Context) UploadFiles(projectID, tagName string) error {
if c.AssetFiles == nil {
return nil
}
color := c.IO.Color()
for _, file := range c.AssetFiles {
fmt.Fprintf(c.IO.StdErr, "%s Uploading to release\t%s=%s %s=%s\n",
color.ProgressIcon(), color.Blue("file"), file.Path,
color.Blue("name"), file.Name)
projectFile, _, err := c.Client.Projects.UploadFile(
projectID,
file.Path,
nil,
)
if err != nil {
return err
}
gitlabBaseURL := fmt.Sprintf("%s://%s/", glinstance.OverridableDefaultProtocol(), glinstance.OverridableDefault())
// projectFile.URL from upload: /uploads/<hash>/filename.txt
linkURL := gitlabBaseURL + projectID + projectFile.URL
filename := "/" + file.Name
_, err = CreateLink(c.Client, projectID, tagName, &ReleaseAsset{
Name: &file.Label,
URL: &linkURL,
FilePath: &filename,
})
if err != nil {
return err
}
}
c.AssetFiles = nil
return nil
}
func (c *Context) CreateReleaseAssetLinks(projectID string, tagName string) error {
if c.AssetsLinks == nil {
return nil
}
color := c.IO.Color()
for _, asset := range c.AssetsLinks {
releaseLink, err := CreateLink(c.Client, projectID, tagName, asset)
if err != nil {
return err
}
fmt.Fprintf(c.IO.StdErr, "%s Added release asset\t%s=%s %s=%s\n",
color.GreenCheck(), color.Blue("name"), *asset.Name,
color.Blue("url"), releaseLink.DirectAssetURL)
}
c.AssetsLinks = nil
return nil
}

View File

@ -15,3 +15,7 @@ func (c *ColorPalette) WarnIcon() string {
func (c *ColorPalette) RedCheck() string {
return c.Red("✓")
}
func (c *ColorPalette) ProgressIcon() string {
return c.Blue("•")
}