< Summary - go-semantic-release Coverage

Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 190
Line coverage: 100%
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%00100%

File(s)

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

#LineLine coverage
 1package bitbucket
 2
 3import (
 4  "bytes"
 5  "context"
 6  "encoding/json"
 7  "fmt"
 8  "io"
 9  "net/http"
 10  "os"
 11
 12  "github.com/jedi-knights/go-semantic-release/internal/domain"
 13  "github.com/jedi-knights/go-semantic-release/internal/ports"
 14)
 15
 16// Compile-time interface compliance checks.
 17var (
 18  _ ports.Plugin                 = (*Plugin)(nil)
 19  _ ports.VerifyConditionsPlugin = (*Plugin)(nil)
 20  _ ports.PublishPlugin          = (*Plugin)(nil)
 21  _ ports.AddChannelPlugin       = (*Plugin)(nil)
 22  _ ports.SuccessPlugin          = (*Plugin)(nil)
 23  _ ports.FailPlugin             = (*Plugin)(nil)
 24)
 25
 26// PluginConfig holds configuration for the Bitbucket plugin.
 27type PluginConfig struct {
 28  Workspace string `mapstructure:"workspace"`
 29  RepoSlug  string `mapstructure:"repo_slug"`
 30  Token     string `mapstructure:"token"`
 31  APIURL    string `mapstructure:"api_url"`
 32}
 33
 34// Plugin implements lifecycle interfaces for Bitbucket Cloud integration.
 35type Plugin struct {
 36  config PluginConfig
 37  client *http.Client
 38  logger ports.Logger
 39}
 40
 41// NewPlugin creates a Bitbucket lifecycle plugin.
 1742func NewPlugin(cfg PluginConfig, logger ports.Logger) *Plugin {
 143  if cfg.APIURL == "" {
 144    cfg.APIURL = "https://api.bitbucket.org/2.0"
 145  }
 246  if cfg.Token == "" {
 247    cfg.Token = resolveToken()
 248  }
 1749  return &Plugin{
 1750    config: cfg,
 1751    client: &http.Client{},
 1752    logger: logger,
 1753  }
 54}
 55
 256func resolveToken() string {
 257  for _, key := range []string{"BB_TOKEN", "BITBUCKET_TOKEN", "SEMANTIC_RELEASE_BITBUCKET_TOKEN"} {
 158    if v := os.Getenv(key); v != "" {
 159      return v
 160    }
 61  }
 162  return ""
 63}
 64
 65func (p *Plugin) Name() string { return "bitbucket" }
 66
 67// VerifyConditions checks that Bitbucket credentials and config are valid.
 68func (p *Plugin) VerifyConditions(ctx context.Context, rc *domain.ReleaseContext) error {
 69  if p.config.Token == "" {
 70    return fmt.Errorf("bitbucket token not found (set BB_TOKEN, BITBUCKET_TOKEN, or SEMANTIC_RELEASE_BITBUCKET_TOKEN)")
 71  }
 72  if p.config.Workspace == "" || p.config.RepoSlug == "" {
 73    return fmt.Errorf("bitbucket workspace and repo_slug must be configured")
 74  }
 75
 76  apiURL := fmt.Sprintf("%s/repositories/%s/%s", p.config.APIURL, p.config.Workspace, p.config.RepoSlug)
 77  req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody)
 78  if err != nil {
 79    return fmt.Errorf("creating request: %w", err)
 80  }
 81  p.setHeaders(req)
 82
 83  resp, err := p.client.Do(req)
 84  if err != nil {
 85    return fmt.Errorf("verifying Bitbucket access: %w", err)
 86  }
 87  defer func() { _ = resp.Body.Close() }()
 88
 89  if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
 90    return fmt.Errorf("bitbucket token is invalid or lacks permissions (HTTP %d)", resp.StatusCode)
 91  }
 92  if resp.StatusCode != http.StatusOK {
 93    return fmt.Errorf("bitbucket API returned HTTP %d for repo verification", resp.StatusCode)
 94  }
 95
 96  return nil
 97}
 98
 99// Publish creates a Bitbucket pipeline tag. Bitbucket Cloud doesn't have a native
 100// "Releases" concept like GitHub/GitLab, so we create an annotated tag via the API.
 101func (p *Plugin) Publish(ctx context.Context, rc *domain.ReleaseContext) (*domain.ProjectReleaseResult, error) {
 102  if rc.CurrentProject == nil {
 103    return nil, nil
 104  }
 105
 106  tagName := rc.TagName
 107
 108  // Create annotated tag via Bitbucket REST API.
 109  reqBody := bbCreateTagRequest{
 110    Name:    tagName,
 111    Target:  bbTarget{Hash: ""},
 112    Message: rc.Notes,
 113  }
 114
 115  // Get head hash if available.
 116  if rc.CurrentProject.Project.Path != "" {
 117    reqBody.Target.Hash = "HEAD"
 118  }
 119
 120  jsonData, err := json.Marshal(reqBody)
 121  if err != nil {
 122    return nil, fmt.Errorf("marshaling tag request: %w", err)
 123  }
 124
 125  apiURL := fmt.Sprintf("%s/repositories/%s/%s/refs/tags", p.config.APIURL, p.config.Workspace, p.config.RepoSlug)
 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 tag: %w", err)
 135  }
 136  defer func() { _ = resp.Body.Close() }()
 137
 138  if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
 139    body, _ := io.ReadAll(resp.Body)
 140    return nil, fmt.Errorf("bitbucket create tag failed (%d): %s", resp.StatusCode, string(body))
 141  }
 142
 143  repoURL := fmt.Sprintf("https://bitbucket.org/%s/%s/src/%s", p.config.Workspace, p.config.RepoSlug, tagName)
 144  p.logger.Info("created Bitbucket tag", "tag", tagName)
 145
 146  return &domain.ProjectReleaseResult{
 147    Project:    rc.CurrentProject.Project,
 148    Version:    rc.CurrentProject.NextVersion,
 149    TagName:    tagName,
 150    Published:  true,
 151    PublishURL: repoURL,
 152    Changelog:  rc.Notes,
 153  }, nil
 154}
 155
 156// AddChannel is a no-op for Bitbucket.
 157func (p *Plugin) AddChannel(_ context.Context, _ *domain.ReleaseContext) error {
 158  return nil
 159}
 160
 161// Success logs the successful release.
 162func (p *Plugin) Success(_ context.Context, rc *domain.ReleaseContext) error {
 163  p.logger.Info("Bitbucket release successful", "tag", rc.TagName)
 164  return nil
 165}
 166
 167// Fail logs the failed release.
 168func (p *Plugin) Fail(_ context.Context, rc *domain.ReleaseContext) error {
 169  if rc.Error != nil {
 170    p.logger.Error("Bitbucket release failed", "error", rc.Error)
 171  }
 172  return nil
 173}
 174
 175// --- Helper types ---
 176
 177type bbCreateTagRequest struct {
 178  Name    string   `json:"name"`
 179  Target  bbTarget `json:"target"`
 180  Message string   `json:"message,omitempty"`
 181}
 182
 183type bbTarget struct {
 184  Hash string `json:"hash"`
 185}
 186
 187func (p *Plugin) setHeaders(req *http.Request) {
 188  req.Header.Set("Authorization", "Bearer "+p.config.Token)
 189  req.Header.Set("Content-Type", "application/json")
 190}

Methods/Properties

NewPlugin
resolveToken