You've already forked go-semantic-release
feat(releaser): add gitlab as relase option
This commit is contained in:
225
internal/releaser/gitlab/gitlab.go
Normal file
225
internal/releaser/gitlab/gitlab.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package gitlab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Nightapes/go-semantic-release/internal/releaser/util"
|
||||||
|
"github.com/Nightapes/go-semantic-release/internal/shared"
|
||||||
|
"github.com/Nightapes/go-semantic-release/pkg/config"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GITLAB identifer for gitlab interface
|
||||||
|
const GITLAB = "gitlab"
|
||||||
|
|
||||||
|
// Client type struct
|
||||||
|
type Client struct {
|
||||||
|
config *config.GitLabProvider
|
||||||
|
context context.Context
|
||||||
|
client *http.Client
|
||||||
|
baseURL string
|
||||||
|
apiURL string
|
||||||
|
token string
|
||||||
|
release string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New initialize a new gitlabRelease
|
||||||
|
func New(config *config.GitLabProvider) (*Client, error) {
|
||||||
|
accessToken, err := util.GetAccessToken(GITLAB)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CustomURL == "" {
|
||||||
|
config.CustomURL = "https://gitlab.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL, err := util.CheckURL(config.CustomURL)
|
||||||
|
log.Debugf("Use gitlab url %s", baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
tokenHeader := util.NewAddHeaderTransport(nil, "PRIVATE-TOKEN", accessToken)
|
||||||
|
acceptHeader := util.NewAddHeaderTransport(tokenHeader, "Accept", "application/json")
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: acceptHeader,
|
||||||
|
Timeout: time.Second * 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
token: accessToken,
|
||||||
|
config: config,
|
||||||
|
context: ctx,
|
||||||
|
baseURL: baseURL,
|
||||||
|
apiURL: baseURL + "api/v4",
|
||||||
|
client: httpClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetCommitURL for gitlab
|
||||||
|
func (g *Client) GetCommitURL() string {
|
||||||
|
return fmt.Sprintf("%s%s/commit/{{hash}}", g.baseURL, g.config.Repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetCompareURL for gitlab
|
||||||
|
func (g *Client) GetCompareURL(oldVersion, newVersion string) string {
|
||||||
|
return fmt.Sprintf("%s%s/compare/%s...%s", g.baseURL, g.config.Repo, oldVersion, newVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
//ValidateConfig for gitlab
|
||||||
|
func (g *Client) ValidateConfig() error {
|
||||||
|
log.Debugf("validate gitlab provider config")
|
||||||
|
|
||||||
|
if g.config.Repo == "" {
|
||||||
|
return fmt.Errorf("gitlab Repro is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRelease creates release on remote
|
||||||
|
func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedChangelog *shared.GeneratedChangelog) error {
|
||||||
|
|
||||||
|
tag := releaseVersion.Next.Version.String()
|
||||||
|
g.release = tag
|
||||||
|
log.Debugf("create release with version %s", tag)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(Release{
|
||||||
|
TagName: tag,
|
||||||
|
Name: generatedChangelog.Title,
|
||||||
|
Description: generatedChangelog.Content,
|
||||||
|
Ref: releaseVersion.Branch,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bodyReader := bytes.NewReader(bodyBytes)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/projects/%s/releases", g.apiURL, util.PathEscape(g.config.Repo))
|
||||||
|
log.Debugf("Send release to %s", url)
|
||||||
|
|
||||||
|
resp, err := g.client.Post(url, "application/json", bodyReader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "already_exists") && resp.StatusCode >= http.StatusUnprocessableEntity {
|
||||||
|
return fmt.Errorf("could not create release: %v", err)
|
||||||
|
}
|
||||||
|
log.Infof("A release with tag %s already exits, will not perform a release or update", tag)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("Release repsone: %+v", string(bodyBytes))
|
||||||
|
|
||||||
|
if err := util.IsValidResult(resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Crated release")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadAssets uploads specified assets
|
||||||
|
func (g *Client) UploadAssets(repoDir string, assets []config.Asset) error {
|
||||||
|
filesToUpload, err := util.PrepareAssets(repoDir, assets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, f := range filesToUpload {
|
||||||
|
|
||||||
|
file, err := os.Open(*f)
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileInfo, _ := file.Stat()
|
||||||
|
|
||||||
|
result, err := g.uploadFile(fileInfo.Name(), file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("releaser: gitlab: Could not upload asset %s: %s", file.Name(), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadURL := fmt.Sprintf("%s%s%s", g.baseURL, g.config.Repo, result.URL)
|
||||||
|
|
||||||
|
log.Infof("Uploaded file %s to gitlab can be downloaded under %s", file.Name(), downloadURL)
|
||||||
|
|
||||||
|
path := fmt.Sprintf("%s/projects/%s/releases/%s/assets/links?name=%s&url=%s", g.apiURL, util.PathEscape(g.config.Repo), g.release, util.PathEscape(fileInfo.Name()), downloadURL)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Link file %s with release %s", file.Name(), g.release)
|
||||||
|
|
||||||
|
resp, err := util.Do(g.client, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = util.IsValidResult(resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Link file with release %s done", g.release)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) uploadFile(fileName string, file *os.File) (*ProjectFile, error) {
|
||||||
|
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
w := multipart.NewWriter(b)
|
||||||
|
|
||||||
|
fw, err := w.CreateFormFile("file", fileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(fw, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/projects/%s/uploads", g.apiURL, util.PathEscape(g.config.Repo))
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Body = ioutil.NopCloser(b)
|
||||||
|
req.ContentLength = int64(b.Len())
|
||||||
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||||
|
|
||||||
|
uf := &ProjectFile{}
|
||||||
|
resp, err := util.Do(g.client, req, uf)
|
||||||
|
|
||||||
|
if err = util.IsValidResult(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uf, nil
|
||||||
|
}
|
||||||
25
internal/releaser/gitlab/types.go
Normal file
25
internal/releaser/gitlab/types.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package gitlab
|
||||||
|
|
||||||
|
// Release struct
|
||||||
|
type Release struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Assets struct {
|
||||||
|
Links []*ReleaseLink `json:"links"`
|
||||||
|
} `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseLink struct
|
||||||
|
type ReleaseLink struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectFile struct
|
||||||
|
type ProjectFile struct {
|
||||||
|
Alt string `json:"alt"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Markdown string `json:"markdown"`
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/Nightapes/go-semantic-release/internal/releaser/github"
|
"github.com/Nightapes/go-semantic-release/internal/releaser/github"
|
||||||
|
"github.com/Nightapes/go-semantic-release/internal/releaser/gitlab"
|
||||||
"github.com/Nightapes/go-semantic-release/internal/shared"
|
"github.com/Nightapes/go-semantic-release/internal/shared"
|
||||||
|
|
||||||
"github.com/Nightapes/go-semantic-release/pkg/config"
|
"github.com/Nightapes/go-semantic-release/pkg/config"
|
||||||
@@ -37,6 +38,9 @@ func (r *Releasers) GetReleaser() (Releaser, error) {
|
|||||||
case github.GITHUB:
|
case github.GITHUB:
|
||||||
log.Debugf("initialize new %s-provider", github.GITHUB)
|
log.Debugf("initialize new %s-provider", github.GITHUB)
|
||||||
return github.New(&r.config.GitHubProvider)
|
return github.New(&r.config.GitHubProvider)
|
||||||
|
case gitlab.GITLAB:
|
||||||
|
log.Debugf("initialize new %s-provider", gitlab.GITLAB)
|
||||||
|
return gitlab.New(&r.config.GitLabProvider)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("could not initialize a releaser from this type: %s", r.config.Release)
|
return nil, fmt.Errorf("could not initialize a releaser from this type: %s", r.config.Release)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package util
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -25,6 +27,27 @@ func CreateBearerHTTPClient(ctx context.Context, token string) *http.Client {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddHeaderTransport struct
|
||||||
|
type AddHeaderTransport struct {
|
||||||
|
T http.RoundTripper
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip add header
|
||||||
|
func (adt *AddHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
req.Header.Add(adt.key, adt.value)
|
||||||
|
return adt.T.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewAddHeaderTransport to add default header
|
||||||
|
func NewAddHeaderTransport(T http.RoundTripper, key, value string) *AddHeaderTransport {
|
||||||
|
if T == nil {
|
||||||
|
T = http.DefaultTransport
|
||||||
|
}
|
||||||
|
return &AddHeaderTransport{T, key, value}
|
||||||
|
}
|
||||||
|
|
||||||
// GetAccessToken lookup for the providers accesstoken
|
// GetAccessToken lookup for the providers accesstoken
|
||||||
func GetAccessToken(providerName string) (string, error) {
|
func GetAccessToken(providerName string) (string, error) {
|
||||||
var token string
|
var token string
|
||||||
@@ -109,3 +132,61 @@ func zipFile(repository string, file string) (string, error) {
|
|||||||
|
|
||||||
return zipFileName, nil
|
return zipFileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckURL if is valid
|
||||||
|
func CheckURL(urlStr string) (string, error) {
|
||||||
|
|
||||||
|
if !strings.HasSuffix(urlStr, "/") {
|
||||||
|
urlStr += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//PathEscape to be url save
|
||||||
|
func PathEscape(s string) string {
|
||||||
|
return strings.Replace(url.PathEscape(s), ".", "%2E", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do request for client
|
||||||
|
func Do(client *http.Client, req *http.Request, v interface{}) (*http.Response, error) {
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200, 201, 202, 204, 304:
|
||||||
|
if v != nil {
|
||||||
|
if w, ok := v.(io.Writer); ok {
|
||||||
|
_, err = io.Copy(w, resp.Body)
|
||||||
|
} else {
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidResult(resp *http.Response) error {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200, 201, 202, 204, 304:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s %s: %d", resp.Request.Method, resp.Request.URL, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShouldRetry(resp *http.Response) bool {
|
||||||
|
if resp.StatusCode == http.StatusTooManyRequests {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ type GitHubProvider struct {
|
|||||||
AccessToken string
|
AccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitLabProvider struct
|
||||||
|
type GitLabProvider struct {
|
||||||
|
Repo string `yaml:"repo"`
|
||||||
|
CustomURL string `yaml:"customUrl,omitempty"`
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
// ReleaseConfig struct
|
// ReleaseConfig struct
|
||||||
type ReleaseConfig struct {
|
type ReleaseConfig struct {
|
||||||
CommitFormat string `yaml:"commitFormat"`
|
CommitFormat string `yaml:"commitFormat"`
|
||||||
@@ -36,6 +43,7 @@ type ReleaseConfig struct {
|
|||||||
Changelog ChangelogConfig `yaml:"changelog,omitempty"`
|
Changelog ChangelogConfig `yaml:"changelog,omitempty"`
|
||||||
Release string `yaml:"release,omitempty"`
|
Release string `yaml:"release,omitempty"`
|
||||||
GitHubProvider GitHubProvider `yaml:"github,omitempty"`
|
GitHubProvider GitHubProvider `yaml:"github,omitempty"`
|
||||||
|
GitLabProvider GitLabProvider `yaml:"gitlab,omitempty"`
|
||||||
Assets []Asset `yaml:"assets"`
|
Assets []Asset `yaml:"assets"`
|
||||||
ReleaseTitle string `yaml:"title"`
|
ReleaseTitle string `yaml:"title"`
|
||||||
IsPreRelease, IsDraft bool
|
IsPreRelease, IsDraft bool
|
||||||
|
|||||||
@@ -193,8 +193,8 @@ func (s *SemanticRelease) Release(provider *ci.ProviderConfig, force bool) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if releaseVersion.Next.Version.Equal(releaseVersion.Next.Version) {
|
if releaseVersion.Next.Version.Equal(releaseVersion.Last.Version) {
|
||||||
log.Infof("No new version, no release needed")
|
log.Infof("No new version, no release needed %s <> %s", releaseVersion.Next.Version.String(), releaseVersion.Last.Version.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user