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