diff --git a/internal/releaser/github/github.go b/internal/releaser/github/github.go index 2ba7eab..0f5af06 100644 --- a/internal/releaser/github/github.go +++ b/internal/releaser/github/github.go @@ -91,6 +91,8 @@ func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedC prerelease := releaseVersion.Next.Version.Prerelease() != "" + log.Debugf("Send %+v", generatedChangelog) + release, _, err := g.client.Repositories.CreateRelease(g.context, g.config.User, g.config.Repo, &github.RepositoryRelease{ TagName: &tag, TargetCommitish: &releaseVersion.Branch, @@ -104,7 +106,7 @@ func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedC log.Infof("A release with tag %s already exits, will not perform a release or update", tag) return nil } - return fmt.Errorf("could not create release: %s", err.Error()) + return fmt.Errorf("could not create release: %s", err.Error()) } g.release = release log.Debugf("Release repsone: %+v", *release) diff --git a/internal/releaser/gitlab/gitlab.go b/internal/releaser/gitlab/gitlab.go index 173f18e..1f2b465 100644 --- a/internal/releaser/gitlab/gitlab.go +++ b/internal/releaser/gitlab/gitlab.go @@ -31,17 +31,12 @@ type Client struct { baseURL string apiURL string token string - release string + Release string log *log.Entry } // New initialize a new gitlabRelease -func New(config *config.GitLabProvider) (*Client, error) { - accessToken, err := util.GetAccessToken(GITLAB) - if err != nil { - return nil, err - } - +func New(config *config.GitLabProvider, accessToken string) (*Client, error) { ctx := context.Background() tokenHeader := util.NewAddHeaderTransport(nil, "PRIVATE-TOKEN", accessToken) acceptHeader := util.NewAddHeaderTransport(tokenHeader, "Accept", "application/json") @@ -50,44 +45,46 @@ func New(config *config.GitLabProvider) (*Client, error) { Timeout: time.Second * 60, } + logger := log.WithField("releaser", GITLAB) + + logger.Debugf("validate gitlab provider config") + + if config.Repo == "" { + return nil, fmt.Errorf("gitlab Repro is not set") + } + + config.Repo = strings.Trim(config.Repo, "/") + + if config.CustomURL == "" { + config.CustomURL = "https://gitlab.com" + } + + config.CustomURL = strings.Trim(config.CustomURL, "/") + logger.Debugf("Use gitlab url %s", config.CustomURL) + return &Client{ token: accessToken, config: config, context: ctx, baseURL: config.CustomURL, - apiURL: config.CustomURL + "api/v4", + apiURL: config.CustomURL + "/api/v4", client: httpClient, - log: log.WithField("releaser", GITLAB), + log: logger, }, nil } //GetCommitURL for gitlab func (g *Client) GetCommitURL() string { - return fmt.Sprintf("%s%s/commit/{{hash}}", g.baseURL, g.config.Repo) + 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) + return fmt.Sprintf("%s/%s/compare/%s...%s", g.baseURL, g.config.Repo, oldVersion, newVersion) } //ValidateConfig for gitlab func (g *Client) ValidateConfig() error { - g.log.Debugf("validate gitlab provider config") - - if g.config.Repo == "" { - return fmt.Errorf("gitlab Repro is not set") - } - - g.config.Repo = strings.Trim(g.config.Repo, "/") - - if g.config.CustomURL == "" { - g.config.CustomURL = "https://gitlab.com" - } - - g.config.CustomURL = strings.Trim(g.config.CustomURL, "/") - g.log.Debugf("Use gitlab url %s", g.config.CustomURL) - return nil } @@ -96,10 +93,10 @@ func (g *Client) ValidateConfig() error { func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedChangelog *shared.GeneratedChangelog) error { tag := releaseVersion.Next.Version.String() - g.release = tag - g.log.Debugf("create release with version %s", tag) + g.Release = tag + g.log.Infof("create release with version %s", tag) url := fmt.Sprintf("%s/projects/%s/releases", g.apiURL, util.PathEscape(g.config.Repo)) - g.log.Debugf("Send release to %s", url) + g.log.Infof("Send release to %s", url) bodyBytes, err := json.Marshal(Release{ TagName: tag, @@ -113,23 +110,15 @@ func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedC req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) if err != nil { - return err + return fmt.Errorf("could not create request: %s", err.Error()) } - req.Header.Set("Content-Type", "application/json") resp, err := util.Do(g.client, req, nil) if err != nil { - return fmt.Errorf("could not create release: %s", err.Error()) } - respBodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - log.Debugf("Release repsone: %+v", string(respBodyBytes)) - if err := util.IsValidResult(resp); err != nil { return err } @@ -160,18 +149,18 @@ func (g *Client) UploadAssets(repoDir string, assets []config.Asset) error { return fmt.Errorf("could not upload asset %s: %s", file.Name(), err.Error()) } - downloadURL := fmt.Sprintf("%s%s%s", g.baseURL, g.config.Repo, result.URL) + 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) + 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) + log.Infof("Link file %s with release %s", file.Name(), g.Release) resp, err := util.Do(g.client, req, nil) if err != nil { @@ -182,7 +171,7 @@ func (g *Client) UploadAssets(repoDir string, assets []config.Asset) error { return err } - log.Infof("Link file with release %s is done", g.release) + log.Infof("Link file with release %s is done", g.Release) } return nil } diff --git a/internal/releaser/gitlab/gitlab_test.go b/internal/releaser/gitlab/gitlab_test.go new file mode 100644 index 0000000..944dd87 --- /dev/null +++ b/internal/releaser/gitlab/gitlab_test.go @@ -0,0 +1,327 @@ +package gitlab_test + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Masterminds/semver" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + //"github.com/Nightapes/go-semantic-release/internal/releaser/util" + "github.com/Nightapes/go-semantic-release/internal/releaser/gitlab" + "github.com/Nightapes/go-semantic-release/internal/shared" + "github.com/Nightapes/go-semantic-release/pkg/config" +) + +func TestGetCommitURL(t *testing.T) { + + client, err := gitlab.New(&config.GitLabProvider{ + CustomURL: "https://localhost/", + Repo: "test/test", + }, "aToken") + assert.NoError(t, err) + assert.Equal(t, "https://localhost/test/test/commit/{{hash}}", client.GetCommitURL()) +} + +func TestGetCompareURL(t *testing.T) { + + client, err := gitlab.New(&config.GitLabProvider{ + CustomURL: "https://localhost/", + Repo: "test/test", + }, "aToken") + assert.NoError(t, err) + assert.Equal(t, "https://localhost/test/test/compare/1.0.0...1.0.1", client.GetCompareURL("1.0.0", "1.0.1")) +} + +func TestValidateConfig_EmptyRepro(t *testing.T) { + _, err := gitlab.New(&config.GitLabProvider{ + CustomURL: "https://localhost/", + }, "aToken") + assert.Error(t, err) +} + +func TestValidateConfig_DefaultURL(t *testing.T) { + config := &config.GitLabProvider{ + Repo: "localhost/test", + } + _, err := gitlab.New(config, "aToken") + assert.NoError(t, err) + assert.Equal(t, "https://gitlab.com", config.CustomURL) +} + +func TestValidateConfig_CustomURL(t *testing.T) { + config := &config.GitLabProvider{ + Repo: "/localhost/test/", + CustomURL: "https://localhost/", + } + _, err := gitlab.New(config, "aToken") + assert.NoError(t, err) + assert.Equal(t, "https://localhost", config.CustomURL) + assert.Equal(t, "localhost/test", config.Repo) +} + +func TestCreateRelease(t *testing.T) { + + lastVersion, _ := semver.NewVersion("1.0.0") + newVersion, _ := semver.NewVersion("2.0.0") + + testReleases := []struct { + config config.GitLabProvider + releaseVersion *shared.ReleaseVersion + generatedChangelog *shared.GeneratedChangelog + responseBody string + responseCode int + requestBody string + valid bool + }{ + { + config: config.GitLabProvider{ + Repo: "foo/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", + }, + responseBody: "{}", + responseCode: 200, + requestBody: `{"tag_name":"2.0.0","name":"title","ref":"master","description":"content","assets":{"links":null}}`, + valid: true, + }, + { + config: config.GitLabProvider{ + Repo: "foo/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", + }, + responseBody: "{}", + responseCode: 500, + requestBody: `{"tag_name":"2.0.0","name":"title","ref":"master","description":"content","assets":{"links":null}}`, + valid: false, + }, + { + config: config.GitLabProvider{ + Repo: "foo/bar", + CustomURL: "broken", + }, + 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", + }, + responseCode: 400, + responseBody: "{}", + requestBody: `{"tag_name":"2.0.0","name":"title","ref":"master","description":"content","assets":{"links":null}}`, + valid: false, + }, + } + + for _, testObject := range testReleases { + testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + log.Infof("Got call from %s %s", req.Method, req.URL.String()) + + assert.Equal(t, req.Header.Get("PRIVATE-TOKEN"), "aToken") + assert.Equal(t, req.Header.Get("Accept"), "application/json") + + bodyBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + log.Fatal(err) + } + assert.Equal(t, testObject.requestBody, string(bodyBytes)) + + rw.WriteHeader(testObject.responseCode) + rw.Header().Set("Content-Type", "application/json") + + if _, err := rw.Write([]byte(testObject.responseBody)); err != nil { + log.Info(err) + } + + })) + + if testObject.config.CustomURL == "" { + testObject.config.CustomURL = testServer.URL + } + + client, err := gitlab.New(&testObject.config, "aToken") + assert.NoError(t, err) + + err = client.CreateRelease(testObject.releaseVersion, testObject.generatedChangelog) + if err != nil { + t.Log(err) + } + assert.Equal(t, testObject.valid, err == nil) + + testServer.Close() + + } +} + +func TestUploadAssets(t *testing.T) { + + file, err := ioutil.TempFile("", "prefix") + if err != nil { + log.Fatal(err) + } + defer os.Remove(file.Name()) + + _, err = file.WriteString("testFile") + assert.NoError(t, err) + + testReleases := []struct { + config config.GitLabProvider + responseBody []string + responseCode []int + assets []config.Asset + requestBody []string + testDir string + url []string + method []string + valid bool + }{ + { + config: config.GitLabProvider{ + Repo: "foo/bar", + }, + responseBody: []string{`{"alt" : "dk", "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", "markdown" :"![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"}`, ""}, + responseCode: []int{200, 200}, + requestBody: []string{ + filepath.Base(file.Name()), ""}, + url: []string{`/api/v4/projects/foo%2Fbar/uploads`, "/api/v4/projects/foo%2Fbar/releases/1.0.0/assets/links?name=" + filepath.Base(file.Name()) + "&url=/foo/bar/uploads/"}, + method: []string{"POST", "POST"}, + valid: true, + testDir: os.TempDir(), + assets: []config.Asset{ + config.Asset{ + Name: filepath.Base(file.Name()), + Compress: false, + }, + }, + }, + { + config: config.GitLabProvider{ + Repo: "foo/bar", + }, + responseBody: []string{`{"alt" : "dk", "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", "markdown" :"![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"}`, ""}, + responseCode: []int{400, 200}, + requestBody: []string{ + filepath.Base(file.Name()), ""}, + url: []string{`/api/v4/projects/foo%2Fbar/uploads`, "/api/v4/projects/foo%2Fbar/releases/1.0.0/assets/links?name=" + filepath.Base(file.Name()) + "&url=/foo/bar/uploads/"}, + method: []string{"POST", "POST"}, + valid: false, + testDir: os.TempDir(), + assets: []config.Asset{ + config.Asset{ + Name: filepath.Base(file.Name()), + Compress: false, + }, + }, + }, + { + config: config.GitLabProvider{ + Repo: "foo/bar", + }, + responseBody: []string{`broken`, ""}, + responseCode: []int{200, 200}, + requestBody: []string{ + filepath.Base(file.Name()), ""}, + url: []string{`/api/v4/projects/foo%2Fbar/uploads`, "/api/v4/projects/foo%2Fbar/releases/1.0.0/assets/links?name=" + filepath.Base(file.Name()) + "&url=/foo/bar/uploads/"}, + method: []string{"POST", "POST"}, + valid: false, + testDir: os.TempDir(), + assets: []config.Asset{ + config.Asset{ + Name: filepath.Base(file.Name()), + Compress: false, + }, + }, + }, + } + + for _, testObject := range testReleases { + calls := 0 + testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + log.Infof("Got call from %s %s", req.Method, req.URL.String()) + + assert.Contains(t, req.URL.String(), strings.ReplaceAll(testObject.url[calls], "", testObject.config.CustomURL)) + assert.Equal(t, req.Method, testObject.method[calls]) + + assert.Equal(t, req.Header.Get("PRIVATE-TOKEN"), "aToken") + assert.Equal(t, req.Header.Get("Accept"), "application/json") + + bodyBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + log.Fatal(err) + } + assert.Contains(t, string(bodyBytes), testObject.requestBody[calls]) + + rw.WriteHeader(testObject.responseCode[calls]) + rw.Header().Set("Content-Type", "application/json") + + if _, err := rw.Write([]byte(testObject.responseBody[calls])); err != nil { + log.Info(err) + } + calls++ + })) + + if testObject.config.CustomURL == "" { + testObject.config.CustomURL = testServer.URL + } + + client, err := gitlab.New(&testObject.config, "aToken") + assert.NoError(t, err) + client.Release = "1.0.0" + + err = client.UploadAssets(testObject.testDir, testObject.assets) + if err != nil { + t.Log(err) + } + assert.Equal(t, testObject.valid, err == nil) + + testServer.Close() + + } +} diff --git a/internal/releaser/releaser.go b/internal/releaser/releaser.go index b2a9863..d96f49f 100644 --- a/internal/releaser/releaser.go +++ b/internal/releaser/releaser.go @@ -5,6 +5,7 @@ import ( "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/releaser/util" "github.com/Nightapes/go-semantic-release/internal/shared" "github.com/Nightapes/go-semantic-release/pkg/config" @@ -40,7 +41,11 @@ func (r *Releasers) GetReleaser() (Releaser, error) { return github.New(&r.config.GitHubProvider) case gitlab.GITLAB: log.Debugf("initialize new %s-provider", gitlab.GITLAB) - return gitlab.New(&r.config.GitLabProvider) + accessToken, err := util.GetAccessToken(gitlab.GITLAB) + if err != nil { + return nil, err + } + return gitlab.New(&r.config.GitLabProvider, accessToken) } return nil, fmt.Errorf("could not initialize a releaser from this type: %s", r.config.Release) } diff --git a/internal/releaser/util/util.go b/internal/releaser/util/util.go index 61523fc..5bde7e3 100644 --- a/internal/releaser/util/util.go +++ b/internal/releaser/util/util.go @@ -133,21 +133,6 @@ func zipFile(repository string, file string) (string, error) { 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) @@ -162,7 +147,7 @@ func Do(client *http.Client, req *http.Request, v interface{}) (*http.Response, defer resp.Body.Close() switch resp.StatusCode { - case 200, 201, 202, 204, 304: + case 200, 201, 202, 204: if v != nil { if w, ok := v.(io.Writer); ok { _, err = io.Copy(w, resp.Body) @@ -178,7 +163,7 @@ func Do(client *http.Client, req *http.Request, v interface{}) (*http.Response, // IsValidResult validates response code func IsValidResult(resp *http.Response) error { switch resp.StatusCode { - case 200, 201, 202, 204, 304: + case 200, 201, 202, 204: return nil default: return fmt.Errorf("%s %s: %d", resp.Request.Method, resp.Request.URL, resp.StatusCode) diff --git a/internal/releaser/util/util_test.go b/internal/releaser/util/util_test.go index 9cf12b4..52822ea 100644 --- a/internal/releaser/util/util_test.go +++ b/internal/releaser/util/util_test.go @@ -3,9 +3,15 @@ package util_test import ( "context" "fmt" + "net/http" + "net/http/httptest" + "net/url" "os" "strings" "testing" + "time" + + log "github.com/sirupsen/logrus" "github.com/Nightapes/go-semantic-release/pkg/config" @@ -117,3 +123,108 @@ func TestPrepareAssets(t *testing.T) { } } + +func TestShouldRetry(t *testing.T) { + assert.True(t, util.ShouldRetry(&http.Response{StatusCode: 429})) + assert.False(t, util.ShouldRetry(&http.Response{StatusCode: 200})) +} + +func TestIsValidResult(t *testing.T) { + assert.NoError(t, util.IsValidResult(&http.Response{StatusCode: 200})) + assert.NoError(t, util.IsValidResult(&http.Response{StatusCode: 201})) + assert.NoError(t, util.IsValidResult(&http.Response{StatusCode: 202})) + assert.NoError(t, util.IsValidResult(&http.Response{StatusCode: 204})) + + u, err := url.Parse("https://localhost") + assert.NoError(t, err) + assert.Error(t, util.IsValidResult(&http.Response{StatusCode: 500, Request: &http.Request{ + Method: "POST", + URL: u, + }})) +} + +func TestPathEscape(t *testing.T) { + assert.Equal(t, "test%2Ftest", util.PathEscape("test/test")) + assert.Equal(t, "test", util.PathEscape("test")) + assert.Equal(t, "test%2Etest", util.PathEscape("test.test")) +} + +type example struct { + Test string `json:"test"` +} + +func TestDoAndRoundTrip(t *testing.T) { + tokenHeader := util.NewAddHeaderTransport(nil, "PRIVATE-TOKEN", "aToken") + acceptHeader := util.NewAddHeaderTransport(tokenHeader, "Accept", "application/json") + httpClient := &http.Client{ + Transport: acceptHeader, + Timeout: time.Second * 60, + } + + testsDoMethod := []struct { + statusCode int + body string + responseBody interface{} + responseBodyType interface{} + hasError bool + path string + }{ + { + statusCode: 200, + body: `{"test" : "hallo"}`, + responseBody: &example{ + Test: "hallo", + }, + responseBodyType: &example{}, + hasError: false, + path: "", + }, + { + statusCode: 400, + body: `{"test" : "hallo"}`, + responseBody: &example{}, + responseBodyType: &example{}, + hasError: false, + path: "", + }, + { + statusCode: 200, + body: `{"test" : "hallo"}`, + hasError: true, + responseBody: &example{}, + responseBodyType: &example{}, + path: "broken", + }, + } + + for _, testOject := range testsDoMethod { + testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + log.Infof("Got call from %s %s", req.Method, req.URL.String()) + + assert.Equal(t, req.Header.Get("PRIVATE-TOKEN"), "aToken") + assert.Equal(t, req.Header.Get("Accept"), "application/json") + + rw.WriteHeader(testOject.statusCode) + rw.Header().Set("Content-Type", "application/json") + + if _, err := rw.Write([]byte(testOject.body)); err != nil { + log.Info(err) + } + + })) + + req, err := http.NewRequest("POST", testServer.URL+testOject.path, nil) + assert.NoError(t, err) + + resp, err := util.Do(httpClient, req, testOject.responseBodyType) + + assert.Equal(t, testOject.hasError, err != nil) + + if !testOject.hasError { + assert.Equal(t, testOject.statusCode, resp.StatusCode) + assert.Equal(t, testOject.responseBody, testOject.responseBodyType) + } + testServer.Close() + } +}