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>
This commit is contained in:
@@ -5,6 +5,62 @@ import (
|
||||
"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
|
||||
|
||||
|
||||
@@ -35,6 +35,56 @@ func FuzzParse(f *testing.F) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractSubject(t *testing.T) {
|
||||
cases := []struct {
|
||||
header string
|
||||
want string
|
||||
}{
|
||||
{"feat: add login", "add login"},
|
||||
{"feat(auth): add OAuth2", "add OAuth2"},
|
||||
{"feat!: remove API", "remove API"},
|
||||
{"FIX:typo", "typo"},
|
||||
{"plain message", "plain message"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := ExtractSubject(c.header)
|
||||
if got != c.want {
|
||||
t.Errorf("ExtractSubject(%q) = %q, want %q", c.header, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
messages := []string{
|
||||
"feat: add login",
|
||||
"fix: patch null pointer",
|
||||
"feat!: remove legacy API",
|
||||
"chore: update deps",
|
||||
"fix: handle empty response",
|
||||
}
|
||||
breaking, feats, fixes := Group(messages)
|
||||
if len(breaking) != 1 || breaking[0] != "remove legacy API" {
|
||||
t.Errorf("breaking = %v, want [remove legacy API]", breaking)
|
||||
}
|
||||
if len(feats) != 1 || feats[0] != "add login" {
|
||||
t.Errorf("feats = %v, want [add login]", feats)
|
||||
}
|
||||
if len(fixes) != 2 {
|
||||
t.Errorf("fixes = %v, want 2 items", fixes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleasableSet(t *testing.T) {
|
||||
all := ReleasableSet(nil)
|
||||
if !all[TypeFix] || !all[TypeFeat] || !all[TypeBreaking] {
|
||||
t.Error("nil input should return all three types")
|
||||
}
|
||||
only := ReleasableSet([]string{"fix"})
|
||||
if !only[TypeFix] || only[TypeFeat] || only[TypeBreaking] {
|
||||
t.Errorf("fix-only set: %v", only)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeString(t *testing.T) {
|
||||
cases := []struct {
|
||||
t Type
|
||||
|
||||
Reference in New Issue
Block a user