< Summary - go-semantic-release Coverage

Line coverage
77%
Covered lines: 72
Uncovered lines: 21
Coverable lines: 93
Total lines: 199
Line coverage: 77.4%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Name0%00100%
VerifyConditions0%0065.38%
Publish0%0076%
AddChannel0%00100%
Success0%00100%
Fail0%00100%
setHeaders0%00100%

File(s)

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/gitlab/plugin.go

#LineLine coverage
 1package gitlab
 2
 3import (
 4  "bytes"
 5  "context"
 6  "encoding/json"
 7  "fmt"
 8  "io"
 9  "net/http"
 10  "net/url"
 11  "os"
 12
 13  "github.com/jedi-knights/go-semantic-release/internal/domain"
 14  "github.com/jedi-knights/go-semantic-release/internal/ports"
 15)
 16
 17// Compile-time interface compliance checks.
 18var (
 19  _ ports.Plugin                 = (*Plugin)(nil)
 20  _ ports.VerifyConditionsPlugin = (*Plugin)(nil)
 21  _ ports.PublishPlugin          = (*Plugin)(nil)
 22  _ ports.AddChannelPlugin       = (*Plugin)(nil)
 23  _ ports.SuccessPlugin          = (*Plugin)(nil)
 24  _ ports.FailPlugin             = (*Plugin)(nil)
 25)
 26
 27// PluginConfig holds configuration for the GitLab plugin.
 28type PluginConfig struct {
 29  ProjectID  string   `mapstructure:"project_id"`
 30  Token      string   `mapstructure:"token"`
 31  APIURL     string   `mapstructure:"api_url"`
 32  Assets     []string `mapstructure:"assets"`
 33  Milestones []string `mapstructure:"milestones"`
 34}
 35
 36// Plugin implements lifecycle interfaces for GitLab integration.
 37type Plugin struct {
 38  config PluginConfig
 39  client *http.Client
 40  logger ports.Logger
 41}
 42
 43// NewPlugin creates a GitLab lifecycle plugin.
 44func NewPlugin(cfg PluginConfig, logger ports.Logger) *Plugin {
 45  if cfg.APIURL == "" {
 46    cfg.APIURL = "https://gitlab.com/api/v4"
 47  }
 48  if cfg.Token == "" {
 49    cfg.Token = resolveToken()
 50  }
 51  return &Plugin{
 52    config: cfg,
 53    client: &http.Client{},
 54    logger: logger,
 55  }
 56}
 57
 58func resolveToken() string {
 59  for _, key := range []string{"GL_TOKEN", "GITLAB_TOKEN", "SEMANTIC_RELEASE_GITLAB_TOKEN"} {
 60    if v := os.Getenv(key); v != "" {
 61      return v
 62    }
 63  }
 64  return ""
 65}
 66
 267func (p *Plugin) Name() string { return "gitlab" }
 68
 69// VerifyConditions checks that GitLab credentials and config are valid.
 470func (p *Plugin) VerifyConditions(ctx context.Context, rc *domain.ReleaseContext) error {
 171  if p.config.Token == "" {
 172    return fmt.Errorf("GitLab token not found (set GL_TOKEN, GITLAB_TOKEN, or SEMANTIC_RELEASE_GITLAB_TOKEN)")
 173  }
 174  if p.config.ProjectID == "" {
 175    return fmt.Errorf("GitLab project_id must be configured")
 176  }
 77
 278  apiURL := fmt.Sprintf("%s/projects/%s", p.config.APIURL, url.PathEscape(p.config.ProjectID))
 279  req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody)
 080  if err != nil {
 081    return fmt.Errorf("creating request: %w", err)
 082  }
 283  p.setHeaders(req)
 284
 285  resp, err := p.client.Do(req)
 086  if err != nil {
 087    return fmt.Errorf("verifying GitLab access: %w", err)
 088  }
 289  defer func() { _ = resp.Body.Close() }()
 90
 191  if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
 192    return fmt.Errorf("GitLab token is invalid or lacks permissions (HTTP %d)", resp.StatusCode)
 193  }
 094  if resp.StatusCode != http.StatusOK {
 095    return fmt.Errorf("GitLab API returned HTTP %d for project verification", resp.StatusCode)
 096  }
 97
 198  return nil
 99}
 100
 101// Publish creates a GitLab release.
 3102func (p *Plugin) Publish(ctx context.Context, rc *domain.ReleaseContext) (*domain.ProjectReleaseResult, error) {
 1103  if rc.CurrentProject == nil {
 1104    return nil, nil
 1105  }
 106
 2107  tagName := rc.TagName
 2108  releaseName := tagName
 2109  if rc.CurrentProject.Project.Name != "" {
 2110    releaseName = fmt.Sprintf("%s %s", rc.CurrentProject.Project.Name, rc.CurrentProject.NextVersion.String())
 2111  }
 112
 2113  reqBody := glCreateReleaseRequest{
 2114    TagName:     tagName,
 2115    Name:        releaseName,
 2116    Description: rc.Notes,
 2117    Milestones:  p.config.Milestones,
 2118  }
 2119
 2120  jsonData, err := json.Marshal(reqBody)
 0121  if err != nil {
 0122    return nil, fmt.Errorf("marshaling release request: %w", err)
 0123  }
 124
 2125  apiURL := fmt.Sprintf("%s/projects/%s/releases", p.config.APIURL, url.PathEscape(p.config.ProjectID))
 2126  req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(jsonData))
 0127  if err != nil {
 0128    return nil, fmt.Errorf("creating request: %w", err)
 0129  }
 2130  p.setHeaders(req)
 2131
 2132  resp, err := p.client.Do(req)
 0133  if err != nil {
 0134    return nil, fmt.Errorf("publishing release: %w", err)
 0135  }
 2136  defer func() { _ = resp.Body.Close() }()
 137
 1138  if resp.StatusCode != http.StatusCreated {
 1139    body, _ := io.ReadAll(resp.Body)
 1140    return nil, fmt.Errorf("GitLab create release failed (%d): %s", resp.StatusCode, string(body))
 1141  }
 142
 1143  var release glRelease
 0144  if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
 0145    return nil, fmt.Errorf("decoding release response: %w", err)
 0146  }
 147
 1148  p.logger.Info("created GitLab release", "tag", tagName, "url", release.Links.Self)
 1149
 1150  return &domain.ProjectReleaseResult{
 1151    Project:    rc.CurrentProject.Project,
 1152    Version:    rc.CurrentProject.NextVersion,
 1153    TagName:    tagName,
 1154    Published:  true,
 1155    PublishURL: release.Links.Self,
 1156    Changelog:  rc.Notes,
 1157  }, nil
 158}
 159
 160// AddChannel is a no-op for GitLab (releases don't have a channel/prerelease distinction).
 1161func (p *Plugin) AddChannel(_ context.Context, _ *domain.ReleaseContext) error {
 1162  return nil
 1163}
 164
 165// Success logs the successful release.
 1166func (p *Plugin) Success(_ context.Context, rc *domain.ReleaseContext) error {
 1167  p.logger.Info("GitLab release successful", "tag", rc.TagName)
 1168  return nil
 1169}
 170
 171// Fail logs the failed release.
 2172func (p *Plugin) Fail(_ context.Context, rc *domain.ReleaseContext) error {
 1173  if rc.Error != nil {
 1174    p.logger.Error("GitLab release failed", "error", rc.Error)
 1175  }
 2176  return nil
 177}
 178
 179// --- Helper types ---
 180
 181type glCreateReleaseRequest struct {
 182  TagName     string   `json:"tag_name"`
 183  Name        string   `json:"name"`
 184  Description string   `json:"description"`
 185  Milestones  []string `json:"milestones,omitempty"`
 186}
 187
 188type glRelease struct {
 189  TagName     string `json:"tag_name"`
 190  Description string `json:"description"`
 191  Links       struct {
 192    Self string `json:"self"`
 193  } `json:"_links"`
 194}
 195
 4196func (p *Plugin) setHeaders(req *http.Request) {
 4197  req.Header.Set("PRIVATE-TOKEN", p.config.Token)
 4198  req.Header.Set("Content-Type", "application/json")
 4199}