Compare commits

...

5 Commits

Author SHA1 Message Date
lukaszraczylo 3e0a7239c4 Fix ignoring strict.force and strict.commit. 2025-12-15 13:37:07 +00:00
lukaszraczylo 49a46a74c1 Add signing of the builds. 2025-12-15 00:43:42 +00:00
lukaszraczylo 3a48a67c75 Improve calculation logic, add ability to strip prefixes. 2025-12-10 14:37:38 +00:00
lukaszraczylo 18b9b474e0 fix: remove unnecessary verified parameter from homebrew cask
The verified parameter is only needed when the URL domain differs
from the homepage domain. Since both point to github.com, Homebrew
audit flags this as unnecessary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 02:06:43 +00:00
lukaszraczylo f5a118fd1a Use shared PR workflow. 2025-12-08 02:06:43 +00:00
24 changed files with 489 additions and 389 deletions
+16
View File
@@ -0,0 +1,16 @@
name: Pull Request
on:
pull_request:
branches:
- main
push:
branches:
- "**"
- "!main"
jobs:
pr-checks:
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
with:
go-version: "1.24"
+1
View File
@@ -11,6 +11,7 @@ on:
- main
permissions:
id-token: write
contents: write
packages: write
+20 -2
View File
@@ -80,8 +80,6 @@ homebrew_casks:
homepage: https://github.com/lukaszraczylo/semver-generator
description: "Automatic semantic version generator based on git commit messages"
license: MIT
url:
verified: github.com/lukaszraczylo/semver-generator
hooks:
post:
install: |
@@ -89,3 +87,23 @@ homebrew_casks:
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{staged_path}/semver-generator"]
end
signs:
- cmd: cosign
signature: "${artifact}.sigstore.json"
args:
- sign-blob
- "--bundle=${signature}"
- "${artifact}"
- "--yes"
artifacts: checksum
output: true
docker_signs:
- cmd: cosign
artifacts: manifests
output: true
args:
- sign
- "${artifact}@${digest}"
- "--yes"
+56
View File
@@ -16,6 +16,7 @@ Project created overnight, to prove that management of semantic versioning is NO
- [Calculations example \[standard\]](#calculations-example-standard)
- [Calculations example \[strict matching\]](#calculations-example-strict-matching)
- [Release candidates](#release-candidates)
- [Tag prefix stripping](#tag-prefix-stripping)
- [Example configuration](#example-configuration)
- [Good to know](#good-to-know)
@@ -29,6 +30,7 @@ Project created overnight, to prove that management of semantic versioning is NO
* With flag `-e` or config `force.existing: true` the existing tags in versioning will be respected, helping you to avoid the version conflicts.
* With config `force.commit: deadbeef` where `deadbeef` is the commit hash - calculations will start from the specified commit.
* Tag prefix stripping: The `v` prefix is automatically stripped from tags (e.g., `v1.2.3``1.2.3`). Additional prefixes can be configured via `tag_prefixes` for monorepo setups (e.g., `app-1.2.3`, `infra-1.2.3`).
### Important changes
@@ -144,6 +146,25 @@ jobs:
docker pull ghcr.io/lukaszraczylo/semver-generator:latest
```
#### Verifying Release Signatures
All release checksums and Docker images are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify:
```bash
# Verify checksum signature
cosign verify-blob \
--certificate-identity-regexp "https://github.com/lukaszraczylo/semver-generator/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "<checksums-file>.sigstore.json" \
<checksums-file>
# Verify Docker image
cosign verify \
--certificate-identity-regexp "https://github.com/lukaszraczylo/semver-generator/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/lukaszraczylo/semver-generator:latest
```
**Docker supported architectures:**
Linux/arm64, Linux/amd64
@@ -182,6 +203,36 @@ to generate the appropriate release in format `1.3.37-rc.1` and counting up unti
- add-rc
```
#### Tag prefix stripping
When using the `-e` (existing tags) flag, the semver-generator needs to parse existing git tags to determine the current version. Tags often include prefixes that need to be stripped before version parsing.
**Automatic `v` prefix stripping:**
The `v` prefix is always stripped automatically from tags. For example:
- `v1.2.3` → parsed as `1.2.3`
- `v0.5.0` → parsed as `0.5.0`
**Custom prefixes for monorepos:**
In monorepo setups where different components have their own versioned tags, you can configure additional prefixes to strip:
```yaml
tag_prefixes:
- "app-"
- "infra-"
- "api-"
- "frontend-"
```
With this configuration:
- `app-1.2.3` → parsed as `1.2.3`
- `infra-0.5.0` → parsed as `0.5.0`
- `api-2.0.0-rc.1` → parsed as `2.0.0-rc.1` (release candidate)
This is particularly useful when:
- You have multiple services/components in a single repository
- Your CI/CD creates tags with component prefixes
- You want to track versions separately for different parts of your codebase
#### Example configuration
```yaml
@@ -196,6 +247,10 @@ blacklist:
- "Merge pull request"
- "feature/"
- "feature:"
tag_prefixes:
- "app-"
- "infra-"
- "service-"
wording:
patch:
- update
@@ -215,6 +270,7 @@ wording:
* `force`: sets the "starting" version, you don't need to specify this section as the default is always `0`
* `force.commit`: allows you to set commit hash from which the calculations should start
* `blacklist`: terms to ignore when processing commits. Any commit containing these terms will be skipped in version calculations. Useful for ignoring merge commits, feature branch names, and other unwanted triggers.
* `tag_prefixes`: prefixes to strip from existing tags before parsing version numbers. Useful for monorepos where tags are prefixed with component names (e.g., `app-1.2.3`, `infra-0.5.0`). The `v` prefix is always stripped automatically.
* `wording`: words the program should look for in the git commits to increment (patch|minor|major)
### Good to knows
-16
View File
@@ -1,16 +0,0 @@
package cmd
import (
"github.com/lukaszraczylo/semver-generator/cmd/utils"
)
// These functions are now in the utils package
// They are kept here as stubs for backward compatibility
func updatePackage() bool {
return utils.UpdatePackage()
}
func checkLatestRelease() (string, bool) {
return utils.CheckLatestRelease()
}
-52
View File
@@ -1,52 +0,0 @@
package cmd
import (
"testing"
"github.com/lukaszraczylo/semver-generator/cmd/utils"
)
func Test_checkLatestRelease(t *testing.T) {
utils.InitLogger(true)
tests := []struct {
name string
want string
want1 bool
}{
{
name: "Check latest release",
want1: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, got1 := checkLatestRelease()
if got1 != tt.want1 {
t.Errorf("checkLatestRelease() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_updatePackage(t *testing.T) {
utils.InitLogger(true)
if testing.Short() {
t.Skip("Skipping test in short / CI mode")
}
tests := []struct {
name string
want bool
}{
{
name: "Run autoupdater",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := updatePackage(); got != tt.want {
t.Errorf("updatePackage() = %v, want %v", got, tt.want)
}
})
}
}
+6 -1
View File
@@ -121,7 +121,11 @@ func main() {
}
// List commits
utils.ListCommits(&repo.GitRepo)
if _, err := utils.ListCommits(&repo.GitRepo); err != nil {
utils.Error("Unable to list commits", map[string]interface{}{
"error": err.Error(),
})
}
// List existing tags if needed
if params.varExisting || repo.Config.Force.Existing {
@@ -140,6 +144,7 @@ func main() {
repo.Semver,
params.varExisting || repo.Config.Force.Existing,
params.varStrict || repo.Config.Force.Strict,
repo.Config.TagPrefixes,
)
// Print semantic version
+39 -18
View File
@@ -267,7 +267,7 @@ func (suite *Tests) Test_checkMatches() {
if tt.name == "No match" {
return nil
}
// For other test cases, match if the needle is in the haystack
for _, h := range haystack {
if strings.Contains(h, needle) || strings.Contains(needle, h) {
@@ -276,7 +276,7 @@ func (suite *Tests) Test_checkMatches() {
}
return nil
}
got := utils.CheckMatches(tt.args.content, tt.args.targets, tt.blacklist)
assertObj.Equal(tt.want, got, "Unexpected result in "+tt.name)
})
@@ -285,7 +285,8 @@ func (suite *Tests) Test_checkMatches() {
func (suite *Tests) Test_parseExistingSemver() {
type args struct {
tagName string
tagName string
prefixes []string
}
tests := []struct {
name string
@@ -296,7 +297,8 @@ func (suite *Tests) Test_parseExistingSemver() {
{
name: "Test parsing existing semver",
args: args{
tagName: "1.2.3",
tagName: "1.2.3",
prefixes: []string{},
},
currentSemver: utils.SemVer{Major: 1, Minor: 1, Patch: 1},
wantSemanticVersion: utils.SemVer{
@@ -308,7 +310,8 @@ func (suite *Tests) Test_parseExistingSemver() {
{
name: "Test parsing existing semver with v",
args: args{
tagName: "v1.2.3",
tagName: "v1.2.3",
prefixes: []string{"v"},
},
currentSemver: utils.SemVer{Major: 1, Minor: 1, Patch: 1},
wantSemanticVersion: utils.SemVer{
@@ -320,7 +323,8 @@ func (suite *Tests) Test_parseExistingSemver() {
{
name: "Test parsing existing semver with rc",
args: args{
tagName: "1.2.5-rc.7",
tagName: "1.2.5-rc.7",
prefixes: []string{},
},
currentSemver: utils.SemVer{Major: 1, Minor: 1, Patch: 1},
wantSemanticVersion: utils.SemVer{
@@ -331,10 +335,25 @@ func (suite *Tests) Test_parseExistingSemver() {
EnableReleaseCandidate: true,
},
},
{
name: "Test parsing prefixed tag without rc",
args: args{
tagName: "app-0.0.16",
prefixes: []string{"app-", "infra-"},
},
currentSemver: utils.SemVer{Major: 1, Minor: 1, Patch: 1},
wantSemanticVersion: utils.SemVer{
Major: 0,
Minor: 0,
Patch: 16,
EnableReleaseCandidate: false,
},
},
{
name: "Test invalid semver format",
args: args{
tagName: "invalid",
tagName: "invalid",
prefixes: []string{},
},
currentSemver: utils.SemVer{Major: 2, Minor: 3, Patch: 4},
wantSemanticVersion: utils.SemVer{
@@ -346,7 +365,8 @@ func (suite *Tests) Test_parseExistingSemver() {
{
name: "Test partial semver",
args: args{
tagName: "1.2",
tagName: "1.2",
prefixes: []string{},
},
currentSemver: utils.SemVer{Major: 2, Minor: 3, Patch: 4},
wantSemanticVersion: utils.SemVer{
@@ -358,7 +378,8 @@ func (suite *Tests) Test_parseExistingSemver() {
{
name: "Test empty tag",
args: args{
tagName: "",
tagName: "",
prefixes: []string{},
},
currentSemver: utils.SemVer{Major: 2, Minor: 3, Patch: 4},
wantSemanticVersion: utils.SemVer{
@@ -370,7 +391,7 @@ func (suite *Tests) Test_parseExistingSemver() {
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
got := utils.ParseExistingSemver(tt.args.tagName, tt.currentSemver)
got := utils.ParseExistingSemver(tt.args.tagName, tt.currentSemver, tt.args.prefixes)
assertObj.Equal(tt.wantSemanticVersion.Major, got.Major, "Unexpected MAJOR semver result in "+tt.name)
assertObj.Equal(tt.wantSemanticVersion.Minor, got.Minor, "Unexpected MINOR semver result in "+tt.name)
assertObj.Equal(tt.wantSemanticVersion.Patch, got.Patch, "Unexpected PATCH semver result in "+tt.name)
@@ -382,10 +403,10 @@ func (suite *Tests) Test_parseExistingSemver() {
func (suite *Tests) TestSetup_ListCommits() {
type fields struct {
RepositoryName string
RepositoryBranch string
LocalConfigFile string
GitRepo utils.GitRepository
RepositoryName string
RepositoryBranch string
LocalConfigFile string
GitRepo utils.GitRepository
}
tests := []struct {
@@ -441,23 +462,23 @@ func (suite *Tests) TestSetup_ListCommits() {
if tt.name == "List commits from existing repository" {
t.Skip("Skipping test that requires repository access")
}
s := &Setup{
RepositoryName: tt.fields.RepositoryName,
RepositoryBranch: tt.fields.RepositoryBranch,
GitRepo: tt.fields.GitRepo,
}
config, _ := utils.ReadConfig(tt.fields.LocalConfigFile)
s.Config = config
err := utils.PrepareRepository(&s.GitRepo)
if err != nil && !tt.wantErr {
if tt.name != "List commits starting with certain hash" {
t.Fatalf("Failed to prepare repository: %v", err)
}
}
if err == nil {
listOfCommits, err := utils.ListCommits(&s.GitRepo)
if !tt.wantErr {
+3 -3
View File
@@ -30,13 +30,13 @@ func TestExecute(t *testing.T) {
Short: "Test command",
Run: func(cmd *cobra.Command, args []string) {},
}
// Add all the required flags to the test command
testCmd.Flags().Bool("version", false, "Print version information")
testCmd.Flags().String("repository", "test-repo", "Repository URL")
testCmd.Flags().String("branch", "test-branch", "Repository branch")
testCmd.Flags().String("config", "test-config", "Config file path")
rootCmd = testCmd
// Execute should not panic
@@ -82,4 +82,4 @@ func TestSetupCobra(t *testing.T) {
assertions.Equal(t, "test-branch", testRepo.RepositoryBranch, "Repository branch should be set")
assertions.Equal(t, "test-config", testRepo.LocalConfigFile, "Config file should be set")
assertions.True(t, testRepo.UseLocal, "UseLocal should be set to true")
}
}
+22 -12
View File
@@ -26,26 +26,36 @@ type Force struct {
// Config represents the application configuration
type Config struct {
Wording Wording
Force Force
Blacklist []string
Wording Wording
Force Force
Blacklist []string
TagPrefixes []string // Prefixes to strip from tags before parsing (e.g., "app-", "infra-", "v")
}
// ReadConfig reads the configuration from a file
func ReadConfig(file string) (*Config, error) {
config := &Config{}
viper.SetConfigFile(file)
err := viper.ReadInConfig()
if err != nil {
err = fmt.Errorf("fatal error config file: %s", err)
return config, err
}
viper.UnmarshalKey("wording", &config.Wording)
viper.UnmarshalKey("force", &config.Force)
viper.UnmarshalKey("blacklist", &config.Blacklist)
if err := viper.UnmarshalKey("wording", &config.Wording); err != nil {
return config, fmt.Errorf("error parsing wording config: %w", err)
}
if err := viper.UnmarshalKey("force", &config.Force); err != nil {
return config, fmt.Errorf("error parsing force config: %w", err)
}
if err := viper.UnmarshalKey("blacklist", &config.Blacklist); err != nil {
return config, fmt.Errorf("error parsing blacklist config: %w", err)
}
if err := viper.UnmarshalKey("tag_prefixes", &config.TagPrefixes); err != nil {
return config, fmt.Errorf("error parsing tag_prefixes config: %w", err)
}
return config, nil
}
@@ -55,14 +65,14 @@ func ApplyForcedVersioning(force Force, semver *SemVer) {
Debug("Forced versioning (MAJOR)", map[string]interface{}{"major": force.Major})
semver.Major = force.Major
}
if force.Minor > 0 {
Debug("Forced versioning (MINOR)", map[string]interface{}{"minor": force.Minor})
semver.Minor = force.Minor
}
if force.Patch > 0 {
Debug("Forced versioning (PATCH)", map[string]interface{}{"patch": force.Patch})
semver.Patch = force.Patch
}
}
}
+1 -1
View File
@@ -198,4 +198,4 @@ wording:
// Test reading a non-existent config
_, err = ReadConfig("non-existent-file.yaml")
assert.Error(t, err)
}
}
+49 -34
View File
@@ -29,14 +29,14 @@ type TagDetails struct {
// GitRepository represents a git repository
type GitRepository struct {
Handler *git.Repository
Name string
Branch string
LocalPath string
UseLocal bool
Commits []CommitDetails
Tags []TagDetails
StartCommit string
Handler *git.Repository
Name string
Branch string
LocalPath string
UseLocal bool
Commits []CommitDetails
Tags []TagDetails
StartCommit string
}
// PrepareRepository prepares the git repository for use
@@ -47,15 +47,15 @@ func PrepareRepository(repo *GitRepository) error {
u, err := url.Parse(repo.Name)
if err != nil {
Error("Unable to parse repository URL", map[string]interface{}{
"error": err.Error(),
"url": repo.Name,
"error": err.Error(),
"url": repo.Name,
})
return err
}
repo.LocalPath = fmt.Sprintf("/tmp/semver/%s/%s", u.Path, repo.Branch)
os.RemoveAll(repo.LocalPath)
_ = os.RemoveAll(repo.LocalPath) // Ignore error - directory may not exist
repo.Handler, err = git.PlainClone(repo.LocalPath, false, &git.CloneOptions{
URL: repo.Name,
ReferenceName: plumbing.NewBranchReferenceName(repo.Branch),
@@ -66,11 +66,11 @@ func PrepareRepository(repo *GitRepository) error {
},
Tags: git.AllTags,
})
if err != nil {
Error("Unable to clone repository", map[string]interface{}{
"error": err.Error(),
"url": repo.Name,
"error": err.Error(),
"url": repo.Name,
})
return err
}
@@ -79,14 +79,20 @@ func PrepareRepository(repo *GitRepository) error {
repo.Handler, err = git.PlainOpen(repo.LocalPath)
if err != nil {
Error("Unable to open local repository", map[string]interface{}{
"error": err.Error(),
"path": repo.LocalPath,
"error": err.Error(),
"path": repo.LocalPath,
})
return err
}
}
os.Chdir(repo.LocalPath)
if err := os.Chdir(repo.LocalPath); err != nil {
Error("Unable to change directory", map[string]interface{}{
"error": err.Error(),
"path": repo.LocalPath,
})
return err
}
return nil
}
@@ -105,14 +111,14 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
if err != nil {
return []CommitDetails{}, err
}
commitsList, err := repo.Handler.Log(&git.LogOptions{From: ref.Hash()})
if err != nil {
return []CommitDetails{}, err
}
var tmpResults []CommitDetails
commitsList.ForEach(func(c *object.Commit) error {
if err := commitsList.ForEach(func(c *object.Commit) error {
tmpResults = append(tmpResults, CommitDetails{
Hash: c.Hash.String(),
Author: c.Author.String(),
@@ -123,22 +129,31 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
return tmpResults[i].Timestamp.Unix() < tmpResults[j].Timestamp.Unix()
})
return nil
})
}); err != nil {
return []CommitDetails{}, err
}
Debug("Listing commits", map[string]interface{}{"commits": tmpResults})
// Filter commits starting from the specified commit if provided
// Filter commits starting after the specified commit if provided
if repo.StartCommit != "" {
found := false
for commitId, cmt := range tmpResults {
if cmt.Hash == repo.StartCommit {
Debug("Found commit match", map[string]interface{}{
"commit": cmt.Hash,
"index": commitId,
"index": commitId,
})
repo.Commits = tmpResults[commitId:]
// Start from the commit AFTER the specified one
repo.Commits = tmpResults[commitId+1:]
found = true
break
}
}
if !found {
// If specified commit not found, use all commits
repo.Commits = tmpResults
}
} else {
repo.Commits = tmpResults
}
@@ -150,32 +165,32 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
// ListExistingTags lists all tags in the repository
func ListExistingTags(repo *GitRepository) {
Debug("Listing existing tags", nil)
// Check if Handler is nil to avoid panic
if repo.Handler == nil {
Debug("Repository handler is nil, skipping tag listing", nil)
return
}
refs, err := repo.Handler.Tags()
if err != nil {
Error("Unable to list tags", map[string]interface{}{"error": err.Error()})
return
}
if err := refs.ForEach(func(ref *plumbing.Reference) error {
repo.Tags = append(repo.Tags, TagDetails{
Name: ref.Name().Short(),
Hash: ref.Hash().String(),
})
Debug("Found tag", map[string]interface{}{
"tag": ref.Name().Short(),
"tag": ref.Name().Short(),
"hash": ref.Hash().String(),
})
return nil
}); err != nil {
Error("Error iterating tags", map[string]interface{}{"error": err.Error()})
}
}
}
+13 -13
View File
@@ -64,7 +64,7 @@ func TestListCommits(t *testing.T) {
t.Run("Test commit filtering logic", func(t *testing.T) {
// Create a test repository with predefined commits
repo := &GitRepository{}
// Manually populate the commits for testing
repo.Commits = []CommitDetails{
{
@@ -83,7 +83,7 @@ func TestListCommits(t *testing.T) {
// Test with StartCommit specified
repo.StartCommit = "def456"
// Instead of calling ListCommits which would try to use the nil Handler,
// we'll just test the filtering logic directly
if repo.StartCommit != "" {
@@ -94,19 +94,19 @@ func TestListCommits(t *testing.T) {
}
}
}
// Verify the filtering worked correctly
assert.Len(t, repo.Commits, 1, "Should filter commits starting from specified hash")
assert.Equal(t, "def456", repo.Commits[0].Hash, "Commit hash should match")
})
t.Run("Test with nil Handler", func(t *testing.T) {
// Create a test repository with nil Handler
repo := &GitRepository{}
// Now we can safely call ListCommits since we've added a nil check
commits, err := ListCommits(repo)
// Verify the function returns without error
assert.NoError(t, err, "Should not error with nil Handler")
assert.Empty(t, commits, "Should return empty commits with nil Handler")
@@ -120,10 +120,10 @@ func TestListExistingTags(t *testing.T) {
t.Run("Test tag processing", func(t *testing.T) {
// Create a test repository
repo := &GitRepository{}
// Since we can't test the actual git operations, we'll test the function's behavior
// by manually setting up the repository state
// Manually add tags to verify they're processed correctly
repo.Tags = []TagDetails{
{
@@ -131,20 +131,20 @@ func TestListExistingTags(t *testing.T) {
Hash: "abc123",
},
}
assert.Len(t, repo.Tags, 1, "Should have 1 tag")
assert.Equal(t, "v1.0.0", repo.Tags[0].Name, "Tag name should match")
assert.Equal(t, "abc123", repo.Tags[0].Hash, "Tag hash should match")
})
t.Run("Test with nil Handler", func(t *testing.T) {
// Create a test repository with nil Handler
repo := &GitRepository{}
// Now we can safely call ListExistingTags since we've added a nil check
ListExistingTags(repo)
// Verify no tags were added
assert.Empty(t, repo.Tags, "Should have no tags after calling with nil Handler")
})
}
}
+18 -82
View File
@@ -34,14 +34,6 @@ type ReleaseAsset struct {
BrowserDownloadURL string `json:"browser_download_url"`
}
// UpdateInfo contains information about an available update
type UpdateInfo struct {
CurrentVersion string
LatestVersion string
ReleaseURL string
DownloadURL string
}
// httpClient is the HTTP client used for requests (allows mocking in tests)
var httpClient = &http.Client{
Timeout: requestTimeout,
@@ -60,30 +52,6 @@ func CheckLatestRelease() (string, bool) {
return version, true
}
// CheckForUpdate checks if a newer version is available
// Returns UpdateInfo if an update is available, nil otherwise
func CheckForUpdate(currentVersion string) *UpdateInfo {
release, err := fetchLatestRelease(context.Background())
if err != nil {
return nil
}
latestVersion := normalizeVersion(release.TagName)
current := normalizeVersion(currentVersion)
if isNewerVersion(latestVersion, current) {
downloadURL := findBinaryAsset(release.Assets)
return &UpdateInfo{
CurrentVersion: current,
LatestVersion: latestVersion,
ReleaseURL: release.HTMLURL,
DownloadURL: downloadURL,
}
}
return nil
}
// UpdatePackage downloads and installs the latest version
func UpdatePackage() bool {
Info("Checking for updates", nil)
@@ -219,20 +187,23 @@ func downloadBinary(url string) (string, error) {
if strings.HasSuffix(url, ".tar.gz") {
// For tar.gz, we need to extract the binary
if err := extractTarGz(resp.Body, tempFile); err != nil {
tempFile.Close()
os.Remove(tempPath)
_ = tempFile.Close()
_ = os.Remove(tempPath)
return "", err
}
} else {
// Direct binary download
if _, err := io.Copy(tempFile, resp.Body); err != nil {
tempFile.Close()
os.Remove(tempPath)
_ = tempFile.Close()
_ = os.Remove(tempPath)
return "", err
}
}
tempFile.Close()
if err := tempFile.Close(); err != nil {
_ = os.Remove(tempPath)
return "", err
}
return tempPath, nil
}
@@ -250,10 +221,12 @@ func extractTarGz(r io.Reader, destFile *os.File) error {
defer os.Remove(archivePath)
if _, err := io.Copy(archiveFile, r); err != nil {
archiveFile.Close()
_ = archiveFile.Close()
return err
}
if err := archiveFile.Close(); err != nil {
return err
}
archiveFile.Close()
// Extract using tar command
extractDir, err := os.MkdirTemp("", "semver-generator-extract-*")
@@ -336,6 +309,7 @@ var runCommandFunc = func(cmdStr string) error {
// replaceBinary replaces the current binary with the new one
func replaceBinary(newBinary, currentBinary string) error {
// Make the new binary executable
// #nosec G302 -- 0755 is required for executable binaries
if err := os.Chmod(newBinary, 0755); err != nil {
return err
}
@@ -350,13 +324,17 @@ func replaceBinary(newBinary, currentBinary string) error {
}
// copyFile copies a file from src to dst
// Note: This function is only called internally with controlled paths from
// os.CreateTemp and os.Executable, not with user-supplied paths.
func copyFile(src, dst string) error {
// #nosec G304 -- src is from os.CreateTemp, not user input
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
// #nosec G304 -- dst is from os.Executable, not user input
dstFile, err := os.Create(dst)
if err != nil {
return err
@@ -368,6 +346,7 @@ func copyFile(src, dst string) error {
}
// Make executable
// #nosec G302 -- 0755 is required for executable binaries
return os.Chmod(dst, 0755)
}
@@ -378,46 +357,3 @@ func normalizeVersion(v string) string {
v = strings.TrimPrefix(v, "V")
return v
}
// isNewerVersion compares two semver-like versions
// Returns true if latest is newer than current
func isNewerVersion(latest, current string) bool {
latestParts := parseVersionParts(latest)
currentParts := parseVersionParts(current)
for i := 0; i < len(latestParts) && i < len(currentParts); i++ {
if latestParts[i] > currentParts[i] {
return true
}
if latestParts[i] < currentParts[i] {
return false
}
}
return len(latestParts) > len(currentParts)
}
// parseVersionParts splits a version string into numeric parts
func parseVersionParts(v string) []int {
// Remove any suffix like -beta, -rc1, etc.
if idx := strings.IndexAny(v, "-+"); idx != -1 {
v = v[:idx]
}
parts := strings.Split(v, ".")
result := make([]int, 0, len(parts))
for _, p := range parts {
var num int
fmt.Sscanf(p, "%d", &num)
result = append(result, num)
}
return result
}
// FormatUpdateMessage formats a user-friendly update notification
func (u *UpdateInfo) FormatUpdateMessage() string {
return fmt.Sprintf("New version available: %s (current: %s) - %s",
u.LatestVersion, u.CurrentVersion, u.ReleaseURL)
}
-80
View File
@@ -30,57 +30,6 @@ func TestNormalizeVersion(t *testing.T) {
}
}
func TestParseVersionParts(t *testing.T) {
tests := []struct {
input string
expected []int
}{
{"1.0.0", []int{1, 0, 0}},
{"2.1.3", []int{2, 1, 3}},
{"1.0", []int{1, 0}},
{"10.20.30", []int{10, 20, 30}},
{"1.0.0-beta", []int{1, 0, 0}},
{"1.0.0-rc1", []int{1, 0, 0}},
{"1.0.0+build123", []int{1, 0, 0}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := parseVersionParts(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsNewerVersion(t *testing.T) {
tests := []struct {
name string
latest string
current string
expected bool
}{
{"major version bump", "2.0.0", "1.0.0", true},
{"minor version bump", "1.1.0", "1.0.0", true},
{"patch version bump", "1.0.1", "1.0.0", true},
{"same version", "1.0.0", "1.0.0", false},
{"current is newer major", "1.0.0", "2.0.0", false},
{"current is newer minor", "1.0.0", "1.1.0", false},
{"current is newer patch", "1.0.0", "1.0.1", false},
{"multi-digit versions", "1.10.0", "1.9.0", true},
{"longer version is newer", "1.0.1", "1.0", true},
{"shorter version is older", "1.0", "1.0.1", false},
{"complex comparison", "2.1.3", "2.1.2", true},
{"real world example", "0.2.0", "0.1.0", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isNewerVersion(tt.latest, tt.current)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFindBinaryAsset(t *testing.T) {
assets := []ReleaseAsset{
{Name: "semver-gen-1.0.0-linux-amd64.tar.gz", BrowserDownloadURL: "https://example.com/linux-amd64.tar.gz"},
@@ -102,19 +51,6 @@ func TestFindBinaryAssetEmpty(t *testing.T) {
assert.Empty(t, url, "Should return empty string when no assets")
}
func TestUpdateInfo_FormatUpdateMessage(t *testing.T) {
info := &UpdateInfo{
CurrentVersion: "1.0.0",
LatestVersion: "2.0.0",
ReleaseURL: "https://github.com/lukaszraczylo/semver-generator/releases/tag/v2.0.0",
}
msg := info.FormatUpdateMessage()
assert.Contains(t, msg, "2.0.0")
assert.Contains(t, msg, "1.0.0")
assert.Contains(t, msg, "https://github.com/lukaszraczylo/semver-generator/releases/tag/v2.0.0")
}
func TestCheckLatestRelease(t *testing.T) {
// Initialize logger
InitLogger(false)
@@ -141,22 +77,6 @@ func TestCheckLatestRelease(t *testing.T) {
}
}
func TestCheckForUpdate(t *testing.T) {
InitLogger(false)
// Test with a very old version - should show update available if network works
info := CheckForUpdate("0.0.1")
// This will either return update info or nil depending on network
if info != nil {
assert.NotEmpty(t, info.LatestVersion)
assert.Equal(t, "0.0.1", info.CurrentVersion)
}
// Test with a very new version - should not show update
info = CheckForUpdate("999.999.999")
assert.Nil(t, info, "Should not show update for future version")
}
func TestFetchLatestReleaseError(t *testing.T) {
InitLogger(false)
+1 -1
View File
@@ -56,4 +56,4 @@ func Critical(message string, pairs map[string]interface{}) {
Pairs: pairs,
})
}
}
}
+9 -8
View File
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestInitLogger(t *testing.T) {
// Test with debug mode enabled
logger := InitLogger(true)
@@ -25,10 +26,10 @@ func TestLoggingFunctions(t *testing.T) {
Debug("Debug message", map[string]interface{}{"key": "value"})
Info("Info message", map[string]interface{}{"key": "value"})
Error("Error message", map[string]interface{}{"key": "value"})
// Skip testing Critical as it might call os.Exit
// Critical("Critical message", map[string]interface{}{"key": "value"})
// Test passes if we get here without panicking
assert.True(t, true)
}
@@ -43,10 +44,10 @@ func TestLoggingWithNilLogger(t *testing.T) {
Debug("Debug message", map[string]interface{}{"key": "value"})
Info("Info message", map[string]interface{}{"key": "value"})
Error("Error message", map[string]interface{}{"key": "value"})
// Skip testing Critical as it might call os.Exit
// Critical("Critical message", map[string]interface{}{"key": "value"})
// Test passes if we get here without panicking
assert.True(t, true)
}
@@ -56,15 +57,15 @@ func TestCriticalNilLogger(t *testing.T) {
// Save original logger and restore after test
originalLogger := Logger
defer func() { Logger = originalLogger }()
// Set logger to nil
Logger = nil
// This should not panic
Critical("Critical message", map[string]interface{}{"key": "value"})
// Test passes if we get here without panicking
assert.True(t, true)
}
// Note: We don't test Critical with an actual logger because it calls os.Exit
// Note: We don't test Critical with an actual logger because it calls os.Exit
+37 -21
View File
@@ -13,29 +13,45 @@ func CalculateSemver(
initialSemver SemVer,
respectExisting bool,
strictMode bool,
tagPrefixes []string,
) SemVer {
semver := initialSemver
startIndex := 0
for _, commit := range commits {
// Check for existing tags if enabled
if respectExisting {
for _, tagHash := range tags {
if commit.Hash == tagHash.Hash {
Debug("Found existing tag", map[string]interface{}{
"tag": tagHash.Name,
"commit": strings.TrimSuffix(commit.Message, "\n"),
})
semver = ParseExistingSemver(tagHash.Name, semver)
continue
// If respecting existing tags, find the latest tagged commit and start from there
if respectExisting && len(tags) > 0 {
latestTagIndex := -1
var latestTagName string
// Find the latest tagged commit (highest index since commits are oldest-first)
for i, commit := range commits {
for _, tag := range tags {
if commit.Hash == tag.Hash {
if i > latestTagIndex {
latestTagIndex = i
latestTagName = tag.Name
}
}
}
}
// If we found a tagged commit, use its version and start processing after it
if latestTagIndex >= 0 {
Debug("Found latest existing tag", map[string]interface{}{
"tag": latestTagName,
"commit": strings.TrimSuffix(commits[latestTagIndex].Message, "\n"),
})
semver = ParseExistingSemver(latestTagName, semver, tagPrefixes)
startIndex = latestTagIndex + 1
}
}
for _, commit := range commits[startIndex:] {
// In non-strict mode, increment patch by default
if !strictMode {
semver.Patch++
Debug("Incrementing patch (DEFAULT)", map[string]interface{}{
"commit": strings.TrimSuffix(commit.Message, "\n"),
"commit": strings.TrimSuffix(commit.Message, "\n"),
"semver": FormatSemver(semver),
})
}
@@ -55,44 +71,44 @@ func CalculateSemver(
semver.EnableReleaseCandidate = false
semver.Release = 0
Debug("Incrementing major (WORDING)", map[string]interface{}{
"commit": strings.TrimSuffix(commit.Message, "\n"),
"commit": strings.TrimSuffix(commit.Message, "\n"),
"semver": FormatSemver(semver),
})
continue
}
if matchMinor {
semver.Minor++
semver.Patch = 1
semver.EnableReleaseCandidate = false
semver.Release = 0
Debug("Incrementing minor (WORDING)", map[string]interface{}{
"commit": strings.TrimSuffix(commit.Message, "\n"),
"commit": strings.TrimSuffix(commit.Message, "\n"),
"semver": FormatSemver(semver),
})
continue
}
if matchReleaseCandidate {
semver.Release++
semver.Patch = 1
semver.EnableReleaseCandidate = true
Debug("Incrementing release candidate (WORDING)", map[string]interface{}{
"commit": strings.TrimSuffix(commit.Message, "\n"),
"commit": strings.TrimSuffix(commit.Message, "\n"),
"semver": FormatSemver(semver),
})
continue
}
if matchPatch {
semver.Patch++
Debug("Incrementing patch (WORDING)", map[string]interface{}{
"commit": strings.TrimSuffix(commit.Message, "\n"),
"commit": strings.TrimSuffix(commit.Message, "\n"),
"semver": FormatSemver(semver),
})
continue
}
}
return semver
}
}
+52 -11
View File
@@ -19,7 +19,7 @@ func TestCalculateSemver(t *testing.T) {
// More sophisticated mock implementation for testing
for _, h := range haystack {
// Check for substring match to better simulate fuzzy search
if h == needle || (len(h) >= 3 && len(needle) >= 3 &&
if h == needle || (len(h) >= 3 && len(needle) >= 3 &&
(h[:3] == needle[:3] || h[len(h)-3:] == needle[len(needle)-3:])) {
return []string{h}
}
@@ -29,7 +29,7 @@ func TestCalculateSemver(t *testing.T) {
// Test data
now := time.Now()
// Common wording and blacklist for all tests
wording := Wording{
Patch: []string{"update", "fix", "initial"},
@@ -49,6 +49,7 @@ func TestCalculateSemver(t *testing.T) {
initialSemver SemVer
respectExisting bool
strictMode bool
tagPrefixes []string
want SemVer
}{
{
@@ -76,11 +77,12 @@ func TestCalculateSemver(t *testing.T) {
initialSemver: SemVer{},
respectExisting: true,
strictMode: false,
tagPrefixes: []string{},
want: SemVer{
Major: 2,
Minor: 0,
Patch: 1, // Initial tag 2.0.0 + one patch increment
Release: 1,
Major: 2,
Minor: 0,
Patch: 1, // Initial tag 2.0.0 + one patch increment
Release: 1,
EnableReleaseCandidate: true,
},
},
@@ -109,11 +111,12 @@ func TestCalculateSemver(t *testing.T) {
initialSemver: SemVer{},
respectExisting: true,
strictMode: true,
tagPrefixes: []string{},
want: SemVer{
Major: 2,
Minor: 0,
Patch: 1, // Initial tag 2.0.0 + patch from "update" keyword
Release: 1,
Major: 2,
Minor: 0,
Patch: 1, // Initial tag 2.0.0 + patch from "update" keyword
Release: 1,
EnableReleaseCandidate: true,
},
},
@@ -142,6 +145,7 @@ func TestCalculateSemver(t *testing.T) {
initialSemver: SemVer{},
respectExisting: false,
strictMode: false,
tagPrefixes: []string{},
want: SemVer{
Major: 0,
Minor: 1,
@@ -173,6 +177,7 @@ func TestCalculateSemver(t *testing.T) {
initialSemver: SemVer{Major: 1},
respectExisting: false,
strictMode: true,
tagPrefixes: []string{},
want: SemVer{
Major: 1,
Minor: 1,
@@ -199,6 +204,7 @@ func TestCalculateSemver(t *testing.T) {
initialSemver: SemVer{},
respectExisting: false,
strictMode: false,
tagPrefixes: []string{},
want: SemVer{
Major: 0,
Minor: 0,
@@ -225,6 +231,7 @@ func TestCalculateSemver(t *testing.T) {
initialSemver: SemVer{},
respectExisting: false,
strictMode: false,
tagPrefixes: []string{},
want: SemVer{
Major: 0,
Minor: 0,
@@ -233,6 +240,39 @@ func TestCalculateSemver(t *testing.T) {
EnableReleaseCandidate: true,
},
},
{
name: "With prefixed tags should not be RC",
commits: []CommitDetails{
{
Hash: "commit1",
Message: "tagged commit",
Timestamp: now.Add(-3 * time.Hour),
},
{
Hash: "commit2",
Message: "another commit",
Timestamp: now.Add(-2 * time.Hour),
},
},
tags: []TagDetails{
{
Name: "app-0.0.16",
Hash: "commit1",
},
},
wording: wording,
blacklist: blacklist,
initialSemver: SemVer{},
respectExisting: true,
strictMode: true, // Use strict mode for predictable results
tagPrefixes: []string{"app-", "infra-"},
want: SemVer{
Major: 0,
Minor: 0,
Patch: 16, // From tag, no additional increments in strict mode
EnableReleaseCandidate: false,
},
},
}
for _, tt := range tests {
@@ -245,6 +285,7 @@ func TestCalculateSemver(t *testing.T) {
tt.initialSemver,
tt.respectExisting,
tt.strictMode,
tt.tagPrefixes,
)
assert.Equal(t, tt.want.Major, got.Major, "Major version mismatch")
@@ -254,4 +295,4 @@ func TestCalculateSemver(t *testing.T) {
assert.Equal(t, tt.want.EnableReleaseCandidate, got.EnableReleaseCandidate, "EnableReleaseCandidate mismatch")
})
}
}
}
+72 -22
View File
@@ -51,52 +51,102 @@ func FormatSemver(semver SemVer) string {
var extractNumber = regexp.MustCompile("[0-9]+")
// StripTagPrefix removes configured prefixes from a tag name
// The "v" prefix is always stripped by default (e.g., v1.2.3 -> 1.2.3)
func StripTagPrefix(tagName string, prefixes []string) string {
result := tagName
// Always strip "v" prefix by default
if strings.HasPrefix(result, "v") && len(result) > 1 {
// Only strip if followed by a digit (to avoid stripping "version-1.0.0")
if result[1] >= '0' && result[1] <= '9' {
result = result[1:]
Debug("Stripped default 'v' prefix from tag", map[string]interface{}{
"original": tagName,
"result": result,
})
}
}
// Then strip any user-configured prefixes
for _, prefix := range prefixes {
if strings.HasPrefix(result, prefix) {
result = strings.TrimPrefix(result, prefix)
Debug("Stripped prefix from tag", map[string]interface{}{
"original": tagName,
"prefix": prefix,
"result": result,
})
break // Only strip one prefix
}
}
return result
}
// ParseExistingSemver parses a semantic version from a tag name
func ParseExistingSemver(tagName string, currentSemver SemVer) SemVer {
func ParseExistingSemver(tagName string, currentSemver SemVer, prefixes []string) SemVer {
Debug("Parsing existing semver", map[string]interface{}{"tag": tagName})
tagNameParts := strings.Split(tagName, ".")
// Strip configured prefixes before parsing
cleanTagName := StripTagPrefix(tagName, prefixes)
// Check for release candidate pattern (-rc.X) before splitting
isReleaseCandidate := false
rcVersion := 0
if idx := strings.Index(cleanTagName, "-rc."); idx != -1 {
isReleaseCandidate = true
rcPart := cleanTagName[idx+4:] // Get everything after "-rc."
rcMatches := extractNumber.FindAllString(rcPart, 1)
if len(rcMatches) > 0 {
rcVersion, _ = strconv.Atoi(rcMatches[0])
}
// Remove the RC suffix for version parsing
cleanTagName = cleanTagName[:idx]
Debug("Detected release candidate", map[string]interface{}{
"rc_version": rcVersion,
"clean_tag_name": cleanTagName,
})
}
tagNameParts := strings.Split(cleanTagName, ".")
if len(tagNameParts) < 3 {
Debug("Unable to parse incompatible semver (non x.y.z)", map[string]interface{}{"tag": tagName})
return currentSemver
}
semanticVersion := SemVer{}
// Extract major version
majorMatches := extractNumber.FindAllString(tagNameParts[0], -1)
if len(majorMatches) > 0 {
semanticVersion.Major, _ = strconv.Atoi(majorMatches[0])
}
// Extract minor version
minorMatches := extractNumber.FindAllString(tagNameParts[1], -1)
if len(minorMatches) > 0 {
semanticVersion.Minor, _ = strconv.Atoi(minorMatches[0])
}
// Extract patch version
patchMatches := extractNumber.FindAllString(tagNameParts[2], -1)
if len(patchMatches) > 0 {
semanticVersion.Patch, _ = strconv.Atoi(patchMatches[0])
}
// Extract release candidate version if present
if len(tagNameParts) > 3 {
releaseMatches := extractNumber.FindAllString(tagNameParts[3], -1)
if len(releaseMatches) > 0 {
semanticVersion.Release, _ = strconv.Atoi(releaseMatches[0])
semanticVersion.EnableReleaseCandidate = true
}
// Set release candidate if detected
if isReleaseCandidate {
semanticVersion.Release = rcVersion
semanticVersion.EnableReleaseCandidate = true
}
return semanticVersion
}
// CheckMatches checks if any of the targets match the content
func CheckMatches(content []string, targets []string, blacklist []string) bool {
contentStr := strings.Join(content, " ")
// First check if any target matches
hasMatch := false
for _, tgt := range targets {
@@ -104,8 +154,8 @@ func CheckMatches(content []string, targets []string, blacklist []string) bool {
if len(matches) > 0 {
hasMatch = true
Debug("Found match", map[string]interface{}{
"target": tgt,
"match": strings.Join(matches, ","),
"target": tgt,
"match": strings.Join(matches, ","),
"content": contentStr,
})
break
@@ -117,14 +167,14 @@ func CheckMatches(content []string, targets []string, blacklist []string) bool {
for _, blacklistTerm := range blacklist {
if strings.Contains(strings.ToLower(contentStr), strings.ToLower(blacklistTerm)) {
Debug("Blacklisted term detected, ignoring commit", map[string]interface{}{
"content": contentStr,
"content": contentStr,
"blacklist_term": blacklistTerm,
})
return false
}
}
}
return hasMatch
}
@@ -132,4 +182,4 @@ func CheckMatches(content []string, targets []string, blacklist []string) bool {
var FuzzyFind = func(needle string, haystack []string) []string {
// This will be replaced with the actual implementation in main.go
return nil
}
}
+64 -11
View File
@@ -58,15 +58,17 @@ func TestParseExistingSemver(t *testing.T) {
InitLogger(false)
tests := []struct {
name string
tagName string
name string
tagName string
currentSemver SemVer
want SemVer
prefixes []string
want SemVer
}{
{
name: "Standard semver",
tagName: "1.2.3",
name: "Standard semver",
tagName: "1.2.3",
currentSemver: SemVer{},
prefixes: []string{},
want: SemVer{
Major: 1,
Minor: 2,
@@ -74,9 +76,10 @@ func TestParseExistingSemver(t *testing.T) {
},
},
{
name: "With v prefix",
tagName: "v2.3.4",
name: "With v prefix configured",
tagName: "v2.3.4",
currentSemver: SemVer{},
prefixes: []string{"v"},
want: SemVer{
Major: 2,
Minor: 3,
@@ -84,9 +87,32 @@ func TestParseExistingSemver(t *testing.T) {
},
},
{
name: "With release candidate",
tagName: "3.4.5-rc.2",
name: "With app- prefix configured",
tagName: "app-1.2.3",
currentSemver: SemVer{},
prefixes: []string{"app-", "infra-"},
want: SemVer{
Major: 1,
Minor: 2,
Patch: 3,
},
},
{
name: "With prefix but not in config - should still parse numbers",
tagName: "v2.3.4",
currentSemver: SemVer{},
prefixes: []string{}, // v not in prefixes
want: SemVer{
Major: 2,
Minor: 3,
Patch: 4,
},
},
{
name: "With release candidate",
tagName: "3.4.5-rc.2",
currentSemver: SemVer{},
prefixes: []string{},
want: SemVer{
Major: 3,
Minor: 4,
@@ -95,6 +121,31 @@ func TestParseExistingSemver(t *testing.T) {
EnableReleaseCandidate: true,
},
},
{
name: "With prefix and release candidate",
tagName: "app-1.0.0-rc.3",
currentSemver: SemVer{},
prefixes: []string{"app-"},
want: SemVer{
Major: 1,
Minor: 0,
Patch: 0,
Release: 3,
EnableReleaseCandidate: true,
},
},
{
name: "Prefixed tag without RC should NOT be RC",
tagName: "app-0.0.16",
currentSemver: SemVer{},
prefixes: []string{"app-"},
want: SemVer{
Major: 0,
Minor: 0,
Patch: 16,
EnableReleaseCandidate: false,
},
},
{
name: "Invalid format",
tagName: "not-a-semver",
@@ -103,6 +154,7 @@ func TestParseExistingSemver(t *testing.T) {
Minor: 1,
Patch: 1,
},
prefixes: []string{},
want: SemVer{
Major: 1,
Minor: 1,
@@ -117,6 +169,7 @@ func TestParseExistingSemver(t *testing.T) {
Minor: 5,
Patch: 5,
},
prefixes: []string{},
want: SemVer{
Major: 5,
Minor: 5,
@@ -127,7 +180,7 @@ func TestParseExistingSemver(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseExistingSemver(tt.tagName, tt.currentSemver)
got := ParseExistingSemver(tt.tagName, tt.currentSemver, tt.prefixes)
assert.Equal(t, tt.want.Major, got.Major, "Major version mismatch")
assert.Equal(t, tt.want.Minor, got.Minor, "Minor version mismatch")
assert.Equal(t, tt.want.Patch, got.Patch, "Patch version mismatch")
@@ -196,4 +249,4 @@ func TestCheckMatches(t *testing.T) {
assert.Equal(t, tt.want, got)
})
}
}
}
+5
View File
@@ -5,6 +5,11 @@ force:
existing: true
strict: false
commit: 960207e4677476ad31a9f389f74eaf9f33d49613
# tag_prefixes: (optional) prefixes to strip from existing tags
# Note: "v" prefix is always stripped automatically
# tag_prefixes:
# - "app-"
# - "service-"
wording:
patch:
- update
+4
View File
@@ -8,6 +8,10 @@ blacklist:
- "Merge pull request"
- "feature/"
- "feature:"
tag_prefixes:
# Note: "v" prefix is stripped automatically, no need to include it here
- "app-"
- "infra-"
wording:
patch:
- update
+1 -1
View File
@@ -30,4 +30,4 @@ func TestMain(t *testing.T) {
// Verify that the version was set correctly
assert.Equal(t, "test-version", cmd.PKG_VERSION, "PKG_VERSION should be set correctly")
}
}