Files
releaser/internal/commits/commits.go
T
k3nny 5af107b06d
ci / vet, staticcheck, test, build (push) Successful in 3m54s
release / Build and publish release (push) Successful in 4m11s
feat(releaser): release v1.4.0 — GitHub, SSH agent, configurable bumps
- GitHub release support: internal/ghclient (no SDK, minimal HTTP client);
  GITHUB_TOKEN env var; github.token/github.repo config; GitHub takes
  precedence over GitLab when both are configured; publisher interface
  (releasePublisher) in cmd/main.go makes providers interchangeable
- SSH agent push: gitutil.Push() now tries gitssh.NewSSHAgentAuth for
  git@/ssh:// remotes before falling back to the system git binary
- --release-env-file flag: override dotenv artifact path; pass "" to disable
- git.releasable_types config: filter which commit types trigger a bump
  (defaults to fix, feat, breaking); useful for maintenance branches
- commits.Group(), ExtractSubject(), ReleasableSet() exported helpers:
  notes.go and changelog.go now delegate to these instead of duplicating
- CHANGELOG deduplication guard: changelog.Update() is idempotent — skips
  write if ## [version] section already exists
- Always load config sources: LoadWithSources() called unconditionally;
  removes the dual config path that previously only tracked sources in
  --verbose mode
- .releaser.gitlab-ci.yml: adds artifacts: reports: dotenv: release.env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-11 00:15:09 +02:00

120 lines
3.1 KiB
Go

package commits
import (
"regexp"
"strings"
)
// headerSubjectRe captures the subject (description) from a conventional commit header.
var headerSubjectRe = regexp.MustCompile(`(?i)^\w+(?:\([^)]*\))?!?\s*:\s*(.+)`)
// ExtractSubject returns the description part of a conventional commit header.
// Falls back to the trimmed raw header if the pattern does not match.
func ExtractSubject(header string) string {
m := headerSubjectRe.FindStringSubmatch(header)
if m != nil {
return strings.TrimSpace(m[1])
}
return strings.TrimSpace(header)
}
// Group splits messages into breaking changes, features, and fixes.
// Only the first line of each message is considered; the subject is extracted.
// Messages with TypeNone are silently dropped.
func Group(messages []string) (breaking, feats, fixes []string) {
for _, msg := range messages {
t := Parse(msg)
if t == TypeNone {
continue
}
first := strings.SplitN(strings.TrimSpace(msg), "\n", 2)[0]
subject := ExtractSubject(first)
switch t {
case TypeBreaking:
breaking = append(breaking, subject)
case TypeFeat:
feats = append(feats, subject)
case TypeFix:
fixes = append(fixes, subject)
}
}
return
}
// ReleasableSet converts a slice of type-name strings to a set for use in version.Next.
// An empty or nil slice defaults to all three releasable types (fix, feat, breaking).
func ReleasableSet(typeNames []string) map[Type]bool {
if len(typeNames) == 0 {
return map[Type]bool{TypeFix: true, TypeFeat: true, TypeBreaking: true}
}
m := make(map[Type]bool, len(typeNames))
for _, name := range typeNames {
switch strings.ToLower(name) {
case "fix":
m[TypeFix] = true
case "feat":
m[TypeFeat] = true
case "breaking":
m[TypeBreaking] = true
}
}
return m
}
// Type represents the semantic weight of a commit for versioning purposes.
type Type int
const (
TypeNone Type = iota // no version bump
TypeFix // fix: → patch bump
TypeFeat // feat: → patch bump (minor is pinned to branch)
TypeBreaking // feat!: or BREAKING CHANGE → patch bump
)
// headerRe matches conventional commit headers in non-strict mode:
// case-insensitive, optional scope, optional breaking marker, flexible whitespace around colon.
var headerRe = regexp.MustCompile(`(?i)^(\w+)(?:\([^)]*\))?(!)?\s*:\s*\S`)
// Parse extracts the commit type from a commit message.
// Unparseable messages return TypeNone — never an error.
func Parse(message string) Type {
msg := strings.TrimSpace(message)
// BREAKING CHANGE anywhere in the body/footer takes priority (spec §10).
if strings.Contains(msg, "BREAKING CHANGE") {
return TypeBreaking
}
first := strings.SplitN(msg, "\n", 2)[0]
m := headerRe.FindStringSubmatch(first)
if m == nil {
return TypeNone
}
if m[2] == "!" {
return TypeBreaking
}
switch strings.ToLower(m[1]) {
case "feat":
return TypeFeat
case "fix":
return TypeFix
default:
return TypeNone
}
}
func (t Type) String() string {
switch t {
case TypeFix:
return "fix"
case TypeFeat:
return "feat"
case TypeBreaking:
return "breaking"
default:
return "none"
}
}