Merge pull request #8 from Nightapes/test/github-release

add tests for github releaser - UploadAssets method omitted because of future deprication
This commit is contained in:
Sebastian
2019-08-08 21:12:48 +02:00
committed by GitHub
8 changed files with 594 additions and 12 deletions

View File

@@ -89,7 +89,7 @@ func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedC
prerelease := releaseVersion.Next.Version.Prerelease() != "" prerelease := releaseVersion.Next.Version.Prerelease() != ""
release, resp, err := g.client.Repositories.CreateRelease(g.context, g.config.User, g.config.Repo, &github.RepositoryRelease{ release, _, err := g.client.Repositories.CreateRelease(g.context, g.config.User, g.config.Repo, &github.RepositoryRelease{
TagName: &tag, TagName: &tag,
TargetCommitish: &releaseVersion.Branch, TargetCommitish: &releaseVersion.Branch,
Name: &generatedChangelog.Title, Name: &generatedChangelog.Title,
@@ -97,19 +97,18 @@ func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedC
Draft: &releaseVersion.Draft, Draft: &releaseVersion.Draft,
Prerelease: &prerelease, Prerelease: &prerelease,
}) })
if err != nil { if err != nil {
if !strings.Contains(err.Error(), "already_exists") && resp.StatusCode >= http.StatusUnprocessableEntity { if strings.Contains(err.Error(), "already_exists") {
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)
return nil
} }
log.Infof("A release with tag %s already exits, will not perform a release or update", tag) return fmt.Errorf("could not create release: %v", err)
} else {
g.release = release
log.Debugf("Release repsone: %+v", *release)
log.Infof("Crated release")
} }
g.release = release
log.Debugf("Release repsone: %+v", *release)
log.Infof("Crated release")
return nil return nil
} }
// UploadAssets uploads specified assets // UploadAssets uploads specified assets

View File

@@ -0,0 +1,237 @@
package github_test
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
log "github.com/sirupsen/logrus"
"github.com/Masterminds/semver"
"github.com/Nightapes/go-semantic-release/internal/releaser/github"
"github.com/Nightapes/go-semantic-release/internal/shared"
"github.com/Nightapes/go-semantic-release/pkg/config"
"github.com/stretchr/testify/assert"
)
type testHelperMethodStruct struct {
config config.GitHubProvider
valid bool
}
type testReleaseStruct struct {
config config.GitHubProvider
releaseVersion *shared.ReleaseVersion
generatedChangelog *shared.GeneratedChangelog
requestResponseBody string
requestResponseCode int
valid bool
}
var testNewClient = []testHelperMethodStruct{
testHelperMethodStruct{config: config.GitHubProvider{
Repo: "foo",
User: "bar",
},
valid: true,
},
testHelperMethodStruct{config: config.GitHubProvider{
Repo: "foo",
User: "bar",
CustomURL: "https://test.com",
},
valid: false,
},
}
var testHelperMethod = []testHelperMethodStruct{
testHelperMethodStruct{config: config.GitHubProvider{
Repo: "foo",
User: "bar",
},
valid: true,
},
testHelperMethodStruct{config: config.GitHubProvider{
Repo: "",
User: "bar",
},
valid: false,
},
testHelperMethodStruct{config: config.GitHubProvider{
Repo: "foo",
User: "",
},
valid: false,
},
}
var lastVersion, _ = semver.NewVersion("1.0.0")
var newVersion, _ = semver.NewVersion("2.0.0")
var testReleases = []testReleaseStruct{
testReleaseStruct{
config: config.GitHubProvider{
Repo: "foo",
User: "bar",
},
releaseVersion: &shared.ReleaseVersion{
Last: shared.ReleaseVersionEntry{
Version: lastVersion,
Commit: "foo",
},
Next: shared.ReleaseVersionEntry{
Version: newVersion,
Commit: "bar",
},
Branch: "master",
Draft: false,
},
generatedChangelog: &shared.GeneratedChangelog{
Title: "title",
Content: "content",
},
requestResponseBody: "{ \"url\": \"https://api.github.com/repos/octocat/Hello-World/releases/1\", \"html_url\": \"https://github.com/octocat/Hello-World/releases/v1.0.0\", \"assets_url\": \"https://api.github.com/repos/octocat/Hello-World/releases/1/assets\", \"upload_url\": \"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}\", \"tarball_url\": \"https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0\", \"zipball_url\": \"https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0\", \"id\": 1, \"node_id\": \"MDc6UmVsZWFzZTE=\", \"tag_name\": \"v1.0.0\", \"target_commitish\": \"master\", \"name\": \"v1.0.0\", \"body\": \"Description of the release\", \"draft\": false, \"prerelease\": false, \"created_at\": \"2013-02-27T19:35:32Z\", \"published_at\": \"2013-02-27T19:35:32Z\", \"author\": { \"login\": \"octocat\", \"id\": 1, \"node_id\": \"MDQ6VXNlcjE=\", \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\", \"gravatar_id\": \"\", \"url\": \"https://api.github.com/users/octocat\", \"html_url\": \"https://github.com/octocat\", \"followers_url\": \"https://api.github.com/users/octocat/followers\", \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\", \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\", \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\", \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\", \"organizations_url\": \"https://api.github.com/users/octocat/orgs\", \"repos_url\": \"https://api.github.com/users/octocat/repos\", \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\", \"received_events_url\": \"https://api.github.com/users/octocat/received_events\", \"type\": \"User\", \"site_admin\": false }, \"assets\": [ ]}",
requestResponseCode: 200,
valid: true,
},
testReleaseStruct{
config: config.GitHubProvider{
Repo: "foo",
User: "bar",
},
releaseVersion: &shared.ReleaseVersion{
Last: shared.ReleaseVersionEntry{
Version: lastVersion,
Commit: "foo",
},
Next: shared.ReleaseVersionEntry{
Version: newVersion,
Commit: "bar",
},
Branch: "master",
Draft: false,
},
generatedChangelog: &shared.GeneratedChangelog{
Title: "title",
Content: "content",
},
requestResponseCode: 400,
valid: false,
},
}
func initHTTPServer(respCode int, body string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
log.Infof("Got call from %s %s", req.Method, req.URL.String())
rw.WriteHeader(respCode)
rw.Header().Set("Content-Type", "application/json")
if _, err := rw.Write([]byte(body)); err != nil {
log.Info(err)
}
}))
}
func TestNew(t *testing.T) {
for _, testOject := range testNewClient {
if testOject.valid {
os.Setenv("GITHUB_ACCESS_TOKEN", "XXX")
}
_, err := github.New(&testOject.config)
assert.Equal(t, testOject.valid, err == nil)
os.Unsetenv("GITHUB_ACCESS_TOKEN")
}
}
func TestGetCommitURL(t *testing.T) {
os.Setenv("GITHUB_ACCESS_TOKEN", "XX")
for _, testOject := range testNewClient {
client, _ := github.New(&testOject.config)
actualURL := client.GetCommitURL()
if testOject.config.CustomURL != "" {
expectedURL := fmt.Sprintf("%s/%s/%s/commit/{{hash}}", testOject.config.CustomURL, testOject.config.User, testOject.config.Repo)
assert.EqualValues(t, expectedURL, actualURL)
} else {
expectedURL := fmt.Sprintf("%s/%s/%s/commit/{{hash}}", "https://github.com", testOject.config.User, testOject.config.Repo)
assert.EqualValues(t, expectedURL, actualURL)
}
}
os.Unsetenv("GITHUB_ACCESS_TOKEN")
}
func TestGetCompareURL(t *testing.T) {
os.Setenv("GITHUB_ACCESS_TOKEN", "XX")
for _, testOject := range testNewClient {
client, _ := github.New(&testOject.config)
actualURL := client.GetCompareURL("1", "2")
if testOject.config.CustomURL != "" {
expectedURL := fmt.Sprintf("%s/%s/%s/compare/%s...%s", testOject.config.CustomURL, testOject.config.User, testOject.config.Repo, "1", "2")
assert.EqualValues(t, expectedURL, actualURL)
} else {
expectedURL := fmt.Sprintf("%s/%s/%s/compare/%s...%s", "https://github.com", testOject.config.User, testOject.config.Repo, "1", "2")
assert.EqualValues(t, expectedURL, actualURL)
}
}
os.Unsetenv("GITHUB_ACCESS_TOKEN")
}
func TestValidateConfig(t *testing.T) {
os.Setenv("GITHUB_ACCESS_TOKEN", "XX")
for _, testOject := range testHelperMethod {
client, _ := github.New(&testOject.config)
err := client.ValidateConfig()
assert.Equal(t, testOject.valid, err == nil)
}
os.Unsetenv("GITHUB_ACCESS_TOKEN")
}
func TestCreateRelease(t *testing.T) {
os.Setenv("GITHUB_ACCESS_TOKEN", "XX")
for _, testObejct := range testReleases {
if testObejct.valid {
server := initHTTPServer(testObejct.requestResponseCode, testObejct.requestResponseBody)
testObejct.config.CustomURL = server.URL
client, _ := github.New(&testObejct.config)
err := client.CreateRelease(testObejct.releaseVersion, testObejct.generatedChangelog)
if err != nil {
t.Log(err)
}
assert.Equal(t, testObejct.valid, err == nil)
server.Close()
} else {
testObejct.config.CustomURL = "http://foo"
client, _ := github.New(&testObejct.config)
err := client.CreateRelease(testObejct.releaseVersion, testObejct.generatedChangelog)
if err != nil {
t.Log(err)
}
assert.Error(t, err)
}
}
os.Unsetenv("GITHUB_ACCESS_TOKEN")
}

View File

@@ -0,0 +1,229 @@
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)
if err != nil {
return err
}
defer file.Close()
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 != nil {
return nil, err
}
if err = util.IsValidResult(resp); err != nil {
return nil, err
}
return uf, nil
}

View 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"`
}

View File

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

View File

@@ -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,60 @@ 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
}
// IsValidResult validates response code
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)
}
}
// ShouldRetry request
func ShouldRetry(resp *http.Response) bool {
return resp.StatusCode == http.StatusTooManyRequests
}

View File

@@ -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

View File

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