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:
@@ -4,19 +4,17 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.k3nny.fr/releaser/internal/commits"
|
||||
)
|
||||
|
||||
var headerSubjectRe = regexp.MustCompile(`(?i)^\w+(?:\([^)]*\))?!?\s*:\s*(.+)`)
|
||||
|
||||
// Update inserts a new release section into the CHANGELOG file at path.
|
||||
// If the file does not exist it is created with a standard header.
|
||||
// Only commits with a releasable type (fix, feat, breaking) produce bullets;
|
||||
// if none are found the file is left untouched.
|
||||
// If a section for version already exists the file is left untouched (idempotent).
|
||||
func Update(path, tag, version string, messages []string) error {
|
||||
section := buildSection(version, messages)
|
||||
if section == "" {
|
||||
@@ -31,6 +29,10 @@ func Update(path, tag, version string, messages []string) error {
|
||||
return fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
if strings.Contains(existing, "## ["+version+"]") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out string
|
||||
if existing == "" {
|
||||
out = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n" +
|
||||
@@ -48,26 +50,7 @@ func Update(path, tag, version string, messages []string) error {
|
||||
}
|
||||
|
||||
func buildSection(version string, messages []string) string {
|
||||
var breaking, feats, fixes []string
|
||||
|
||||
for _, msg := range messages {
|
||||
t := commits.Parse(msg)
|
||||
if t == commits.TypeNone {
|
||||
continue
|
||||
}
|
||||
first := strings.SplitN(strings.TrimSpace(msg), "\n", 2)[0]
|
||||
subject := extractSubject(first)
|
||||
|
||||
switch t {
|
||||
case commits.TypeBreaking:
|
||||
breaking = append(breaking, subject)
|
||||
case commits.TypeFeat:
|
||||
feats = append(feats, subject)
|
||||
case commits.TypeFix:
|
||||
fixes = append(fixes, subject)
|
||||
}
|
||||
}
|
||||
|
||||
breaking, feats, fixes := commits.Group(messages)
|
||||
if len(breaking)+len(feats)+len(fixes) == 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -91,11 +74,3 @@ func writeSection(sb *strings.Builder, title string, items []string) {
|
||||
fmt.Fprintf(sb, "- %s\n", item)
|
||||
}
|
||||
}
|
||||
|
||||
func extractSubject(header string) string {
|
||||
m := headerSubjectRe.FindStringSubmatch(header)
|
||||
if m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
return strings.TrimSpace(header)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+42
-17
@@ -17,14 +17,16 @@ type Config struct {
|
||||
Git GitConfig `yaml:"git"`
|
||||
Maven MavenConfig `yaml:"maven"`
|
||||
GitLab GitLabConfig `yaml:"gitlab"`
|
||||
GitHub GitHubConfig `yaml:"github"`
|
||||
}
|
||||
|
||||
type GitConfig struct {
|
||||
TagPrefix string `yaml:"tag_prefix"`
|
||||
BranchPattern string `yaml:"branch_pattern"`
|
||||
CommitMessage string `yaml:"commit_message"`
|
||||
AuthorName string `yaml:"author_name"`
|
||||
AuthorEmail string `yaml:"author_email"`
|
||||
TagPrefix string `yaml:"tag_prefix"`
|
||||
BranchPattern string `yaml:"branch_pattern"`
|
||||
CommitMessage string `yaml:"commit_message"`
|
||||
AuthorName string `yaml:"author_name"`
|
||||
AuthorEmail string `yaml:"author_email"`
|
||||
ReleasableTypes []string `yaml:"releasable_types"`
|
||||
}
|
||||
|
||||
type MavenConfig struct {
|
||||
@@ -37,6 +39,11 @@ type GitLabConfig struct {
|
||||
Project string `yaml:"project"`
|
||||
}
|
||||
|
||||
type GitHubConfig struct {
|
||||
Token string `yaml:"token"`
|
||||
Repo string `yaml:"repo"` // "owner/repo"
|
||||
}
|
||||
|
||||
func defaults() Config {
|
||||
return Config{
|
||||
Git: GitConfig{
|
||||
@@ -57,15 +64,18 @@ type Sources map[string]string
|
||||
|
||||
func defaultSources() Sources {
|
||||
return Sources{
|
||||
"git.tag_prefix": "default",
|
||||
"git.branch_pattern": "default",
|
||||
"git.commit_message": "default",
|
||||
"git.author_name": "default",
|
||||
"git.author_email": "default",
|
||||
"maven.pom_path": "default",
|
||||
"gitlab.url": "default",
|
||||
"gitlab.token": "default",
|
||||
"gitlab.project": "default",
|
||||
"git.tag_prefix": "default",
|
||||
"git.branch_pattern": "default",
|
||||
"git.commit_message": "default",
|
||||
"git.author_name": "default",
|
||||
"git.author_email": "default",
|
||||
"git.releasable_types": "default",
|
||||
"maven.pom_path": "default",
|
||||
"gitlab.url": "default",
|
||||
"gitlab.token": "default",
|
||||
"gitlab.project": "default",
|
||||
"github.token": "default",
|
||||
"github.repo": "default",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +123,9 @@ func LoadWithSources(dir string) (Config, Sources, error) {
|
||||
if overlay.Git.AuthorEmail != "" {
|
||||
src["git.author_email"] = "config file"
|
||||
}
|
||||
if len(overlay.Git.ReleasableTypes) > 0 {
|
||||
src["git.releasable_types"] = "config file"
|
||||
}
|
||||
if overlay.Maven.PomPath != "" {
|
||||
src["maven.pom_path"] = "config file"
|
||||
}
|
||||
@@ -125,11 +138,17 @@ func LoadWithSources(dir string) (Config, Sources, error) {
|
||||
if overlay.GitLab.Project != "" {
|
||||
src["gitlab.project"] = "config file"
|
||||
}
|
||||
if overlay.GitHub.Token != "" {
|
||||
src["github.token"] = "config file"
|
||||
}
|
||||
if overlay.GitHub.Repo != "" {
|
||||
src["github.repo"] = "config file"
|
||||
}
|
||||
|
||||
return cfg, src, nil
|
||||
}
|
||||
|
||||
// ApplyEnv fills empty GitLab fields from the standard GitLab CI environment variables.
|
||||
// ApplyEnv fills empty GitLab and GitHub fields from environment variables.
|
||||
// Values already set in the config file are never overwritten.
|
||||
func (c *Config) ApplyEnv() {
|
||||
c.ApplyEnvWithSources(nil)
|
||||
@@ -147,7 +166,6 @@ func (c *Config) ApplyEnvWithSources(src Sources) {
|
||||
}
|
||||
}
|
||||
if c.GitLab.URL == "" {
|
||||
// CI_SERVER_URL is the cleanest source ("https://gitlab.example.com")
|
||||
if v := os.Getenv("CI_SERVER_URL"); v != "" {
|
||||
c.GitLab.URL = v
|
||||
if src != nil {
|
||||
@@ -156,7 +174,6 @@ func (c *Config) ApplyEnvWithSources(src Sources) {
|
||||
}
|
||||
}
|
||||
if c.GitLab.Project == "" {
|
||||
// Prefer numeric ID; fall back to namespace/project path
|
||||
if id := os.Getenv("CI_PROJECT_ID"); id != "" {
|
||||
c.GitLab.Project = id
|
||||
if src != nil {
|
||||
@@ -169,4 +186,12 @@ func (c *Config) ApplyEnvWithSources(src Sources) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.GitHub.Token == "" {
|
||||
if v := os.Getenv("GITHUB_TOKEN"); v != "" {
|
||||
c.GitHub.Token = v
|
||||
if src != nil {
|
||||
src["github.token"] = "env: GITHUB_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package ghclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is a minimal GitHub API client covering only the Releases endpoint.
|
||||
type Client struct {
|
||||
token string
|
||||
repo string // "owner/repo"
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// New creates a Client. repo must be in "owner/repo" format.
|
||||
func New(token, repo string) *Client {
|
||||
return &Client{
|
||||
token: token,
|
||||
repo: repo,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type createReleaseRequest struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// CreateRelease creates a GitHub release on an existing tag.
|
||||
// The tag must already be pushed to the remote before calling this.
|
||||
func (c *Client) CreateRelease(ctx context.Context, tagName, body string) error {
|
||||
payload, _ := json.Marshal(createReleaseRequest{
|
||||
TagName: tagName,
|
||||
Name: tagName,
|
||||
Body: body,
|
||||
})
|
||||
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/releases", c.repo)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GitHub API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
var errBody struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&errBody) //nolint:errcheck
|
||||
if errBody.Message != "" {
|
||||
return fmt.Errorf("GitHub API returned %d: %s", resp.StatusCode, errBody.Message)
|
||||
}
|
||||
return fmt.Errorf("GitHub API returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,14 +6,16 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
gitconfig "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/plumbing/storer"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
|
||||
"git.k3nny.fr/releaser/internal/branch"
|
||||
)
|
||||
@@ -241,15 +243,53 @@ func CreateTag(repo *gogit.Repository, tagName string) error {
|
||||
|
||||
// Push pushes the given branch and tag to the "origin" remote.
|
||||
// When token is non-empty, go-git is used with HTTPS basic auth (oauth2/token) — suitable for CI.
|
||||
// When token is empty, the system git binary is invoked so that credential helpers,
|
||||
// SSH agents, and netrc are all available as they would be for a regular git push.
|
||||
// When token is empty and the remote URL is SSH, go-git SSH agent auth is attempted first.
|
||||
// Falls back to the system git binary so that credential helpers, netrc, and SSH keys work normally.
|
||||
func Push(repo *gogit.Repository, branchName, tagName, token string) error {
|
||||
if token != "" {
|
||||
return pushWithGoGit(repo, branchName, tagName, token)
|
||||
}
|
||||
// Try SSH agent auth when the remote URL uses SSH transport.
|
||||
if remote, err := repo.Remote("origin"); err == nil {
|
||||
urls := remote.Config().URLs
|
||||
if len(urls) > 0 && isSSHURL(urls[0]) {
|
||||
if err := pushWithSSHAgent(repo, branchName, tagName); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return pushWithCLI(repo, branchName, tagName)
|
||||
}
|
||||
|
||||
func isSSHURL(u string) bool {
|
||||
return strings.HasPrefix(u, "git@") || strings.HasPrefix(u, "ssh://")
|
||||
}
|
||||
|
||||
func pushWithSSHAgent(repo *gogit.Repository, branchName, tagName string) error {
|
||||
auth, err := gitssh.NewSSHAgentAuth("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remote, err := repo.Remote("origin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("remote origin not found: %w", err)
|
||||
}
|
||||
|
||||
opts := &gogit.PushOptions{
|
||||
RefSpecs: []gitconfig.RefSpec{
|
||||
gitconfig.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branchName, branchName)),
|
||||
gitconfig.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", tagName, tagName)),
|
||||
},
|
||||
Auth: auth,
|
||||
}
|
||||
|
||||
if err := remote.Push(opts); err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) {
|
||||
return fmt.Errorf("git push via SSH agent: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushWithGoGit(repo *gogit.Repository, branchName, tagName, token string) error {
|
||||
remote, err := repo.Remote("origin")
|
||||
if err != nil {
|
||||
|
||||
+1
-33
@@ -2,38 +2,16 @@ package notes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.k3nny.fr/releaser/internal/commits"
|
||||
)
|
||||
|
||||
// headerSubjectRe captures the subject (description) part of a conventional commit header.
|
||||
var headerSubjectRe = regexp.MustCompile(`(?i)^\w+(?:\([^)]*\))?!?\s*:\s*(.+)`)
|
||||
|
||||
// Generate produces grouped markdown release notes from a list of commit messages.
|
||||
// Commits are grouped into Breaking Changes, Features, and Bug Fixes.
|
||||
// Commits with no releasable type are omitted.
|
||||
func Generate(tagName string, messages []string) string {
|
||||
var breaking, feats, fixes []string
|
||||
|
||||
for _, msg := range messages {
|
||||
t := commits.Parse(msg)
|
||||
if t == commits.TypeNone {
|
||||
continue
|
||||
}
|
||||
first := strings.SplitN(strings.TrimSpace(msg), "\n", 2)[0]
|
||||
subject := extractSubject(first)
|
||||
|
||||
switch t {
|
||||
case commits.TypeBreaking:
|
||||
breaking = append(breaking, subject)
|
||||
case commits.TypeFeat:
|
||||
feats = append(feats, subject)
|
||||
case commits.TypeFix:
|
||||
fixes = append(fixes, subject)
|
||||
}
|
||||
}
|
||||
breaking, feats, fixes := commits.Group(messages)
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "## %s\n", tagName)
|
||||
@@ -53,13 +31,3 @@ func writeSection(sb *strings.Builder, title string, items []string) {
|
||||
fmt.Fprintf(sb, "- %s\n", item)
|
||||
}
|
||||
}
|
||||
|
||||
// extractSubject returns the description part of a conventional commit header.
|
||||
// Falls back to the 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)
|
||||
}
|
||||
|
||||
@@ -69,25 +69,6 @@ func TestGenerateEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzGenerate verifies that Generate never panics on arbitrary inputs and
|
||||
// always includes the tag name in the output.
|
||||
func FuzzGenerate(f *testing.F) {
|
||||
|
||||
@@ -8,10 +8,14 @@ import (
|
||||
|
||||
// Next computes the next version string (without tag prefix, e.g. "1.2.4").
|
||||
// currentPatch is -1 when no tag exists yet (first release will be X.Y.0).
|
||||
// releasable is the set of commit types that trigger a bump; nil defaults to all three.
|
||||
// Returns ("", false) when there are no releasable commits.
|
||||
func Next(major, minor, currentPatch int, types []commits.Type) (string, bool) {
|
||||
func Next(major, minor, currentPatch int, types []commits.Type, releasable map[commits.Type]bool) (string, bool) {
|
||||
if releasable == nil {
|
||||
releasable = commits.ReleasableSet(nil)
|
||||
}
|
||||
for _, t := range types {
|
||||
if t != commits.TypeNone {
|
||||
if releasable[t] {
|
||||
return fmt.Sprintf("%d.%d.%d", major, minor, currentPatch+1), true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestNext(t *testing.T) {
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
got, ok := Next(c.major, c.minor, c.currentPatch, c.types)
|
||||
got, ok := Next(c.major, c.minor, c.currentPatch, c.types, nil)
|
||||
if ok != c.wantOk {
|
||||
t.Errorf("ok=%v, want %v", ok, c.wantOk)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user