< Summary - go-semantic-release Coverage

Line coverage
83%
Covered lines: 15
Uncovered lines: 3
Coverable lines: 18
Total lines: 199
Line coverage: 83.3%
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
NewPlugin0%00100%
resolveToken0%0050%

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.
 1344func NewPlugin(cfg PluginConfig, logger ports.Logger) *Plugin {
 145  if cfg.APIURL == "" {
 146    cfg.APIURL = "https://gitlab.com/api/v4"
 147  }
 148  if cfg.Token == "" {
 149    cfg.Token = resolveToken()
 150  }
 1351  return &Plugin{
 1352    config: cfg,
 1353    client: &http.Client{},
 1354    logger: logger,
 1355  }
 56}
 57
 158func resolveToken() string {
 159  for _, key := range []string{"GL_TOKEN", "GITLAB_TOKEN", "SEMANTIC_RELEASE_GITLAB_TOKEN"} {
 060    if v := os.Getenv(key); v != "" {
 061      return v
 062    }
 63  }
 164  return ""
 65}
 66
 67func (p *Plugin) Name() string { return "gitlab" }
 68
 69// VerifyConditions checks that GitLab credentials and config are valid.
 70func (p *Plugin) VerifyConditions(ctx context.Context, rc *domain.ReleaseContext) error {
 71  if p.config.Token == "" {
 72    return fmt.Errorf("GitLab token not found (set GL_TOKEN, GITLAB_TOKEN, or SEMANTIC_RELEASE_GITLAB_TOKEN)")
 73  }
 74  if p.config.ProjectID == "" {
 75    return fmt.Errorf("GitLab project_id must be configured")
 76  }
 77
 78  apiURL := fmt.Sprintf("%s/projects/%s", p.config.APIURL, url.PathEscape(p.config.ProjectID))
 79  req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody)
 80  if err != nil {
 81    return fmt.Errorf("creating request: %w", err)
 82  }
 83  p.setHeaders(req)
 84
 85  resp, err := p.client.Do(req)
 86  if err != nil {
 87    return fmt.Errorf("verifying GitLab access: %w", err)
 88  }
 89  defer func() { _ = resp.Body.Close() }()
 90
 91  if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
 92    return fmt.Errorf("GitLab token is invalid or lacks permissions (HTTP %d)", resp.StatusCode)
 93  }
 94  if resp.StatusCode != http.StatusOK {
 95    return fmt.Errorf("GitLab API returned HTTP %d for project verification", resp.StatusCode)
 96  }
 97
 98  return nil
 99}
 100
 101// Publish creates a GitLab release.
 102func (p *Plugin) Publish(ctx context.Context, rc *domain.ReleaseContext) (*domain.ProjectReleaseResult, error) {
 103  if rc.CurrentProject == nil {
 104    return nil, nil
 105  }
 106
 107  tagName := rc.TagName
 108  releaseName := tagName
 109  if rc.CurrentProject.Project.Name != "" {
 110    releaseName = fmt.Sprintf("%s %s", rc.CurrentProject.Project.Name, rc.CurrentProject.NextVersion.String())
 111  }
 112
 113  reqBody := glCreateReleaseRequest{
 114    TagName:     tagName,
 115    Name:        releaseName,
 116    Description: rc.Notes,
 117    Milestones:  p.config.Milestones,
 118  }
 119
 120  jsonData, err := json.Marshal(reqBody)
 121  if err != nil {
 122    return nil, fmt.Errorf("marshaling release request: %w", err)
 123  }
 124
 125  apiURL := fmt.Sprintf("%s/projects/%s/releases", p.config.APIURL, url.PathEscape(p.config.ProjectID))
 126  req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(jsonData))
 127  if err != nil {
 128    return nil, fmt.Errorf("creating request: %w", err)
 129  }
 130  p.setHeaders(req)
 131
 132  resp, err := p.client.Do(req)
 133  if err != nil {
 134    return nil, fmt.Errorf("publishing release: %w", err)
 135  }
 136  defer func() { _ = resp.Body.Close() }()
 137
 138  if resp.StatusCode != http.StatusCreated {
 139    body, _ := io.ReadAll(resp.Body)
 140    return nil, fmt.Errorf("GitLab create release failed (%d): %s", resp.StatusCode, string(body))
 141  }
 142
 143  var release glRelease
 144  if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
 145    return nil, fmt.Errorf("decoding release response: %w", err)
 146  }
 147
 148  p.logger.Info("created GitLab release", "tag", tagName, "url", release.Links.Self)
 149
 150  return &domain.ProjectReleaseResult{
 151    Project:    rc.CurrentProject.Project,
 152    Version:    rc.CurrentProject.NextVersion,
 153    TagName:    tagName,
 154    Published:  true,
 155    PublishURL: release.Links.Self,
 156    Changelog:  rc.Notes,
 157  }, nil
 158}
 159
 160// AddChannel is a no-op for GitLab (releases don't have a channel/prerelease distinction).
 161func (p *Plugin) AddChannel(_ context.Context, _ *domain.ReleaseContext) error {
 162  return nil
 163}
 164
 165// Success logs the successful release.
 166func (p *Plugin) Success(_ context.Context, rc *domain.ReleaseContext) error {
 167  p.logger.Info("GitLab release successful", "tag", rc.TagName)
 168  return nil
 169}
 170
 171// Fail logs the failed release.
 172func (p *Plugin) Fail(_ context.Context, rc *domain.ReleaseContext) error {
 173  if rc.Error != nil {
 174    p.logger.Error("GitLab release failed", "error", rc.Error)
 175  }
 176  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
 196func (p *Plugin) setHeaders(req *http.Request) {
 197  req.Header.Set("PRIVATE-TOKEN", p.config.Token)
 198  req.Header.Set("Content-Type", "application/json")
 199}

Methods/Properties

NewPlugin
resolveToken