Repository: Songmu/tagpr (language: go)

Issue:
feat: Alternative labels for minor and major labels can be specified
# Motive

I would like to avoid setting a separate label for tagpr when the release notes are set to be automatically generated as follows
https://docs.github.com/ja/repositories/releasing-projects-on-github/automatically-generated-release-notes#example-configurations

* .github/release.yml
  ```yml
  changelog:
    exclude:
      labels:
        - ignore-for-release
      authors:
        - octocat
    categories:
      - title: Breaking Changes 🛠
        labels:
          - Semver-Major
          - breaking-change
      - title: Exciting New Features 🎉
        labels:
          - Semver-Minor
          - enhancement
      - title: Other Changes
        labels:
          - "*"
  ```

For example, if it has a `breaking-change` label, we want to treat it as a major version up.

# Proposal

How about adding the following options to the `.tagpr` configuration file?

```
[tagpr]
	vPrefix = true
	releaseBranch = main
	versionFile = version.go,action.yml
        majorLabels = major,breaking-change
        minorLabels = minor,enhancement
```

Allows multiple labels to be specified for `majorLabels` and `minorLabels`, respectively.



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index d9d5d2a..07df805 100644
--- a/README.md
+++ b/README.md
@@ -122,6 +122,12 @@ Pull request template in go template format
 GitHub Release creation behavior after tagging `[true, draft, false]`  
 If this value is not set, the release is to be created.
 
+### tagpr.majorLabels (Optional)
+Label of major update targets. Default is [major]
+
+### tagpr.minorLabels (Optional)
+Label of minor update targets. Default is [minor]
+
 ## Outputs for GitHub Actions
 
 The tagpr produces output to be used in conjunction with subsequent GitHub Actions jobs.
diff --git a/config.go b/config.go
index 5e127ba..10e208a 100644
--- a/config.go
+++ b/config.go
@@ -42,8 +42,17 @@ const (
 #   tagpr.release (Optional)
 #       GitHub Release creation behavior after tagging [true, draft, false]
 #       If this value is not set, the release is to be created.
+#
+#   tagpr.majorLabels (Optional)
+#       Label of major update targets. Default is [major]
+#
+#   tagpr.minorLabels (Optional)
+#       Label of minor update targets. Default is [minor]
+#
 [tagpr]
 `
+	defaultMajorLabels  = "major"
+	defaultMinorLabels  = "minor"
 	envReleaseBranch    = "TAGPR_RELEASE_BRANCH"
 	envVersionFile      = "TAGPR_VERSION_FILE"
 	envVPrefix          = "TAGPR_VPREFIX"
@@ -51,6 +60,8 @@ const (
 	envCommand          = "TAGPR_COMMAND"
 	envTemplate         = "TAGPR_TEMPLATE"
 	envRelease          = "TAGPR_RELEASE"
+	envMajorLabels      = "TAGPR_MAJOR_LABELS"
+	envMinorLabels      = "TAGPR_MAINOR_LABELS"
 	configReleaseBranch = "tagpr.releaseBranch"
 	configVersionFile   = "tagpr.versionFile"
 	configVPrefix       = "tagpr.vPrefix"
@@ -58,6 +69,8 @@ const (
 	configCommand       = "tagpr.command"
 	configTemplate      = "tagpr.template"
 	configRelease       = "tagpr.release"
+	configMajorLabels   = "tagpr.majorLabels"
+	configMinorLabels   = "tagpr.minorLabels"
 )
 
 type config struct {
@@ -68,6 +81,8 @@ type config struct {
 	release       *string
 	vPrefix       *bool
 	changelog     *bool
+	majorLabels   *string
+	minorLabels   *string
 
 	conf      string
 	gitconfig *gitconfig.Config
@@ -154,6 +169,28 @@ func (cfg *config) Reload() error {
 		}
 	}
 
+	if major := os.Getenv(envMajorLabels); major != "" {
+		cfg.majorLabels = github.String(major)
+	} else {
+		major, err := cfg.gitconfig.Get(configMajorLabels)
+		if err == nil {
+			cfg.majorLabels = github.String(major)
+		} else {
+			cfg.majorLabels = github.String(defaultMajorLabels)
+		}
+	}
+
+	if minor := os.Getenv(envMinorLabels); minor != "" {
+		cfg.minorLabels = github.String(minor)
+	} else {
+		minor, err := cfg.gitconfig.Get(configMinorLabels)
+		if err == nil {
+			cfg.minorLabels = github.String(minor)
+		} else {
+			cfg.minorLabels = github.String(defaultMinorLabels)
+		}
+	}
+
 	return nil
 }
 
@@ -249,3 +286,23 @@ func (cfg *config) Release() bool {
 func (cfg *config) ReleaseDraft() bool {
 	return strings.ToLower(stringify(cfg.release)) == "draft"
 }
+
+func (cfg *config) MajorLabels() []string {
+	labels := strings.Split(stringify(cfg.majorLabels), ",")
+
+	for i, v := range labels {
+		labels[i] = strings.TrimSpace(v)
+	}
+
+	return labels
+}
+
+func (cfg *config) MinorLabels() []string {
+	labels := strings.Split(stringify(cfg.minorLabels), ",")
+
+	for i, v := range labels {
+		labels[i] = strings.TrimSpace(v)
+	}
+
+	return labels
+}
diff --git a/tagpr.go b/tagpr.go
index 9b7a2ff..96717de 100644
--- a/tagpr.go
+++ b/tagpr.go
@@ -241,24 +241,7 @@ func (tp *tagpr) Run(ctx context.Context) error {
 		prIssues = append(prIssues, tmpIssues...)
 	}
 
-	var nextMinor, nextMajor bool
-	for _, issue := range prIssues {
-		for _, l := range issue.Labels {
-			switch l.GetName() {
-			case "minor":
-				nextMinor = true
-			case "major":
-				nextMajor = true
-			}
-		}
-	}
-	var nextLabels []string
-	if nextMinor {
-		nextLabels = append(nextLabels, "tagpr:minor")
-	}
-	if nextMajor {
-		nextLabels = append(nextLabels, "tagpr:major")
-	}
+	nextLabels := tp.generatenNextLabels(prIssues)
 
 	rcBranch := fmt.Sprintf("%s%s", branchPrefix, currVer.Tag())
 	tp.c.Git("branch", "-D", rcBranch)
@@ -575,6 +558,31 @@ func (tp *tagpr) searchIssues(ctx context.Context, query string) ([]*github.Issu
 	return issues.Issues, nil
 }
 
+func (tp *tagpr) generatenNextLabels(prIssues []*github.Issue) []string {
+	majorLabels := tp.cfg.MajorLabels()
+	minorLabels := tp.cfg.MinorLabels()
+	var nextMinor, nextMajor bool
+	for _, issue := range prIssues {
+		for _, l := range issue.Labels {
+			if contains(minorLabels, l.GetName()) {
+				nextMinor = true
+			}
+			if contains(majorLabels, l.GetName()) {
+				nextMajor = true
+			}
+		}
+	}
+	var nextLabels []string
+	if nextMinor {
+		nextLabels = append(nextLabels, "tagpr:minor")
+	}
+	if nextMajor {
+		nextLabels = append(nextLabels, "tagpr:major")
+	}
+
+	return nextLabels
+}
+
 func buildChunkSearchIssuesQuery(queryBase string, shasStr string) (chunkQueries []string) {
 	query := queryBase
 	// Make bulk requests with multiple SHAs of the maximum possible length.
@@ -601,3 +609,12 @@ func buildChunkSearchIssuesQuery(queryBase string, shasStr string) (chunkQueries
 
 	return chunkQueries
 }
+
+func contains(elems []string, v string) bool {
+	for _, s := range elems {
+		if s == v {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 0486c82..de5ab42 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -21,5 +21,7 @@ jobs:
       uses: actions/checkout@v3
     - name: test
       run: go test -race -coverprofile coverage.out -covermode atomic ./...
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     - name: Send coverage
       uses: codecov/codecov-action@v1
diff --git a/config_test.go b/config_test.go
index 8e09979..0ac6289 100644
--- a/config_test.go
+++ b/config_test.go
@@ -3,6 +3,7 @@ package tagpr
 import (
 	"os"
 	"path/filepath"
+	"reflect"
 	"strings"
 	"testing"
 
@@ -35,6 +36,12 @@ func TestConfig(t *testing.T) {
 	if e, g := "", cfg.VersionFile(); e != g {
 		t.Errorf("got: %s, expext: %s", g, e)
 	}
+	if e, g := []string{"major"}, cfg.MajorLabels(); !reflect.DeepEqual(e, g) {
+		t.Errorf("got: %s, expext: %s", g, e)
+	}
+	if e, g := []string{"minor"}, cfg.MinorLabels(); !reflect.DeepEqual(e, g) {
+		t.Errorf("got: %s, expext: %s", g, e)
+	}
 
 	b, err := os.ReadFile(confPath)
 	if err != nil {
diff --git a/tagpr_test.go b/tagpr_test.go
index 7bb56b6..b1d51eb 100644
--- a/tagpr_test.go
+++ b/tagpr_test.go
@@ -1,8 +1,14 @@
 package tagpr
 
 import (
+	"context"
+	"fmt"
+	"os"
 	"reflect"
 	"testing"
+	"time"
+
+	"github.com/google/go-github/v47/github"
 )
 
 func TestBuildChunkSearchIssuesQuery(t *testing.T) {
@@ -142,3 +148,176 @@ f5134ae
 		}
 	}
 }
+
+func TestGeneratenNextLabels(t *testing.T) {
+	major := "major"
+	minor := "minor"
+	enhancement := "enhancement"
+	breakingChange := "breaking-change"
+
+	tests := map[string]struct {
+		labels      []*github.Label
+		majorLabels string
+		minorLabels string
+		want        []string
+	}{
+		"onlyMajor": {
+			[]*github.Label{newGithubLabel(&major)},
+			"",
+			"",
+			[]string{"tagpr:major"},
+		},
+		"onlyMinor": {
+			[]*github.Label{newGithubLabel(&minor)},
+			"",
+			"",
+			[]string{"tagpr:minor"},
+		},
+		"other": {
+			[]*github.Label{newGithubLabel(new(string))},
+			"",
+			"",
+			[]string{},
+		},
+		"enhancement": {
+			[]*github.Label{newGithubLabel(&enhancement)},
+			"",
+			"",
+			[]string{},
+		},
+		"breakingChange": {
+			[]*github.Label{newGithubLabel(&breakingChange)},
+			"",
+			"",
+			[]string{},
+		},
+		"empty": {
+			[]*github.Label{},
+			"",
+			"",
+			[]string{},
+		},
+		"majorAndMinor": {
+			[]*github.Label{newGithubLabel(&major), newGithubLabel(&minor)},
+			"",
+			"",
+			[]string{"tagpr:minor", "tagpr:major"},
+		},
+		"minorAndMajor": {
+			[]*github.Label{newGithubLabel(&minor), newGithubLabel(&major)},
+			"",
+			"",
+			[]string{"tagpr:minor", "tagpr:major"},
+		},
+		"Set breakingChange to majorLabels": {
+			[]*github.Label{newGithubLabel(&breakingChange)},
+			breakingChange,
+			"",
+			[]string{"tagpr:major"},
+		},
+		"Set enhancement to minorLabels": {
+			[]*github.Label{newGithubLabel(&enhancement)},
+			"",
+			enhancement,
+			[]string{"tagpr:minor"},
+		},
+		"Include in majorLabels to breakingChange": {
+			[]*github.Label{newGithubLabel(&breakingChange)},
+			fmt.Sprintf("other, %s", breakingChange),
+			"",
+			[]string{"tagpr:major"},
+		},
+		"Include in minorLabels to enhancement": {
+			[]*github.Label{newGithubLabel(&enhancement)},
+			"",
+			fmt.Sprintf(" %s,other", enhancement),
+			[]string{"tagpr:minor"},
+		},
+		"Invalid default label": {
+			[]*github.Label{newGithubLabel(&major), newGithubLabel(&minor)},
+			breakingChange,
+			enhancement,
+			[]string{},
+		},
+		"Include default label": {
+			[]*github.Label{newGithubLabel(&major), newGithubLabel(&enhancement)},
+			"major,breaking-change",
+			"minor,enhancement",
+			[]string{"tagpr:minor", "tagpr:major"},
+		},
+	}
+
+	for name, tt := range tests {
+		tt := tt
+		t.Run(name, func(t *testing.T) {
+			if tt.majorLabels != "" {
+				t.Setenv(envMajorLabels, tt.majorLabels)
+			}
+			if tt.minorLabels != "" {
+				t.Setenv(envMinorLabels, tt.minorLabels)
+			}
+			tp, err := newTagPR(context.Background(), &commander{
+				gitPath: "git", outStream: os.Stdout, errStream: os.Stderr, dir: "."},
+			)
+			if err != nil {
+				t.Error(err)
+			}
+
+			prIssues := []*github.Issue{
+				{
+					ID:                new(int64),
+					Number:            new(int),
+					State:             new(string),
+					Locked:            new(bool),
+					Title:             new(string),
+					Body:              new(string),
+					AuthorAssociation: new(string),
+					User:              &github.User{},
+					Labels:            tt.labels,
+					Assignee:          &github.User{},
+					Comments:          new(int),
+					ClosedAt:          &time.Time{},
+					CreatedAt:         &time.Time{},
+					UpdatedAt:         &time.Time{},
+					ClosedBy:          &github.User{},
+					URL:               new(string),
+					HTMLURL:           new(string),
+					CommentsURL:       new(string),
+					EventsURL:         new(string),
+					LabelsURL:         new(string),
+					RepositoryURL:     new(string),
+					Milestone:         &github.Milestone{},
+					PullRequestLinks:  &github.PullRequestLinks{},
+					Repository:        &github.Repository{},
+					Reactions:         &github.Reactions{},
+					Assignees:         []*github.User{},
+					NodeID:            new(string),
+					TextMatches:       []*github.TextMatch{},
+					ActiveLockReason:  new(string),
+				},
+			}
+
+			got := tp.generatenNextLabels(prIssues)
+
+			if len(got) == 0 && len(tt.want) == 0 {
+				return
+			}
+
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("got:\n%s,\nwant:\n%s", got, tt.want)
+			}
+		})
+	}
+}
+
+func newGithubLabel(name *string) *github.Label {
+	return &github.Label{
+		ID:          new(int64),
+		URL:         new(string),
+		Name:        name,
+		Color:       new(string),
+		Description: new(string),
+		Default:     new(bool),
+		NodeID:      new(string),
+	}
+}