internal/gitlab/release.go (199 lines of code) (raw):
package gitlab
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
)
const dateLayout = "2006-01-02"
// Assets describes the assets as Links associated to a release.
type Assets struct {
Count int `json:"count,omitempty"`
Sources []struct {
Format string `json:"format"`
URL string `json:"url"`
} `json:"sources,omitempty"`
Links []*Link `json:"links"`
}
// Link describes the Link request/response body.
type Link struct {
ID int64 `json:"id,omitempty"`
Name string `json:"name"`
URL string `json:"url"`
LinkType string `json:"link_type,omitempty"`
DirectAssetPath string `json:"direct_asset_path,omitempty"`
// Deprecated Filepath redirects to `direct_asset_path` and will be removed in %18.0.
Filepath string `json:"filepath,omitempty"`
}
// Milestone response body when creating a release. Only uses a subset of all the fields.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html#create-a-release
type Milestone struct {
ID int `json:"id"`
Iid int `json:"iid"`
ProjectID int `json:"project_id"`
Title string `json:"title"`
Description string `json:"description"`
State string `json:"state"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DueDate *Date `json:"due_date"`
StartDate *Date `json:"start_date"`
WebURL string `json:"web_url"`
IssueStats struct {
Total int `json:"total"`
Closed int `json:"closed"`
} `json:"issue_stats"`
}
// Date is a custom time.Time wrapper that can parse a date without a timestamp
// See https://gitlab.com/gitlab-org/release-cli/-/issues/121.
type Date time.Time
// UnmarshalJSON implements the json.Unmarshaler interface
func (d *Date) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
t, err := time.Parse(dateLayout, s)
*d = Date(t)
return err
}
// MarshalJSON implements the json.Marshaler interface
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%s\"", time.Time(d).Format(dateLayout))), nil
}
// CreateReleaseRequest body.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html#create-a-release
type CreateReleaseRequest struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
TagName string `json:"tag_name"`
TagMessage string `json:"tag_message"`
Ref string `json:"ref,omitempty"`
Assets *Assets `json:"assets,omitempty"`
Milestones []string `json:"milestones,omitempty"`
ReleasedAt *time.Time `json:"released_at,omitempty"`
LegacyCatalogPublish *bool `json:"legacy_catalog_publish,omitempty"`
}
// UpdateReleaseRequest body.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html#update-a-release
type UpdateReleaseRequest struct {
ID string `json:"id"`
TagName string `json:"tag_name"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Milestones []string `json:"milestones,omitempty"`
ReleasedAt *time.Time `json:"released_at,omitempty"`
}
// ReleaseResponse body.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html
type ReleaseResponse struct {
Name string `json:"name"`
Description string `json:"description"`
DescriptionHTML string `json:"description_html"`
TagName string `json:"tag_name"`
CreatedAt time.Time `json:"created_at"`
ReleasedAt time.Time `json:"released_at"`
Assets *Assets `json:"assets"`
Milestones []*Milestone `json:"milestones"`
Author *Author `json:"author"`
Commit *Commit `json:"commit"`
CommitPath string `json:"commit_path"`
TagPath string `json:"tag_path"`
Evidences []*Evidence `json:"evidences"`
}
// Author body
type Author struct {
ID int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
State string `json:"state"`
AvatarURL string `json:"avatar_url"`
WebURL string `json:"web_url"`
}
// Commit body
type Commit struct {
ID string `json:"id"`
ShortID string `json:"short_id"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
ParentIds []string `json:"parent_ids"`
Message string `json:"message"`
AuthorName string `json:"author_name"`
AuthorEmail string `json:"author_email"`
AuthoredDate time.Time `json:"authored_date"`
CommitterName string `json:"committer_name"`
CommitterEmail string `json:"committer_email"`
CommittedDate time.Time `json:"committed_date"`
}
// Evidence body
type Evidence struct {
Sha string `json:"sha"`
Filepath string `json:"filepath"`
CollectedAt time.Time `json:"collected_at"`
}
// CreateRelease will try to create a release via GitLab's Releases API
func (gc *Client) CreateRelease(ctx context.Context, createReleaseReq *CreateReleaseRequest) (*ReleaseResponse, error) {
body, err := json.Marshal(createReleaseReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
req, err := gc.request(ctx, http.MethodPost, fmt.Sprintf("/projects/%s/releases", gc.projectID), bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
var response ReleaseResponse
if err := gc.makeRequest(req, &response); err != nil {
return nil, err
}
return &response, nil
}
// GetRelease by tagName
func (gc *Client) GetRelease(ctx context.Context, tagName string, includeHTML bool) (*ReleaseResponse, error) {
q := url.Values{}
q.Set("include_html_description", strconv.FormatBool(includeHTML))
req, err := gc.request(ctx, http.MethodGet,
fmt.Sprintf("/projects/%s/releases/%s", gc.projectID, url.QueryEscape(tagName)),
nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.URL.RawQuery = q.Encode()
var response ReleaseResponse
if err := gc.makeRequest(req, &response); err != nil {
return nil, err
}
return &response, nil
}
// UpdateRelease will try to update a release via GitLab's Releases API
func (gc *Client) UpdateRelease(ctx context.Context, updateReleaseRequest *UpdateReleaseRequest) (*ReleaseResponse, error) {
body, err := json.Marshal(updateReleaseRequest)
if err != nil {
return nil, fmt.Errorf("marshal request body: %w", err)
}
req, err := gc.request(ctx, http.MethodPut, fmt.Sprintf("/projects/%s/releases/%s", gc.projectID, url.QueryEscape(updateReleaseRequest.TagName)), bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
var response ReleaseResponse
if err := gc.makeRequest(req, &response); err != nil {
return nil, err
}
return &response, nil
}
func (gc *Client) makeRequest(req *http.Request, response interface{}) error {
res, err := gc.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %w", err)
}
defer checkClosed(res.Body)
if gc.logger.(*logrus.Entry).Logger.Level == logrus.DebugLevel {
dres, err := httputil.DumpResponse(res, true)
printResponse(gc.logger, dres, err)
}
if res.StatusCode >= http.StatusBadRequest {
errResponse := ErrorResponse{
statusCode: res.StatusCode,
}
err := json.NewDecoder(res.Body).Decode(&errResponse)
if err != nil {
return fmt.Errorf("failed to decode error response: %w", err)
}
return &errResponse
}
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}
func printResponse(log logrus.FieldLogger, dres []byte, e error) {
if e != nil {
log.WithError(e).Debug("Error printing the response")
} else {
log.Debug("Received response:")
fmt.Println(string(dres))
}
}