Compare commits

...
37 Commits
Author SHA1 Message Date
lukaszraczyloandGitHub 9ae6f6fa09 Update go.mod and go.sum (#68) 2026-10-03 04:13:46 +01:00
github-actions[bot] 2866d2a6cb chore: pin action.yml Docker image to v1.18.6 2026-10-02 15:23:34 +00:00
lukaszraczylo 6e5afc22d4 ci: build with Go 1.27
The dependency update pulled a module that needs Go 1.26. The workflows pinned Go 1.25, so the update step could not run.
2026-10-02 16:18:42 +01:00
github-actions[bot] c0201f2f6b chore: pin action.yml Docker image to v1.18.4 2026-10-02 02:02:10 +00:00
lukaszraczyloandGitHub 353c5b22d9 Update go.mod and go.sum (#67) 2026-10-02 02:57:15 +01:00
github-actions[bot] 4dc18e6d7c chore: pin action.yml Docker image to v1.18.2 2026-09-25 18:40:43 +00:00
lukaszraczylo f7ae4c042f ci(release): move the v1 tag onto the image pin commit
The release job moves the rolling v1 tag to the release commit. The
update-action-version job then pins action.yml to the new image in a
later commit. So v1 always pointed at a commit that pinned the
previous image, and "@v1" users ran the previous release.

The job now checks out the released commit, pushes the pin commit with
"git push origin HEAD:main", and moves v1 onto it after that push
succeeds. If main moved during the release, the push fails and v1
stays where it is. The job runs only for main, and a concurrency group
queues overlapping release runs.

Claude-Session: https://claude.ai/code/session_01Qq1gbqQek1Zjtofo4VDRdW
2026-09-25 19:35:52 +01:00
lukaszraczylo e90191de4c feat(matching): add an opt-in regex keyword matching mode
Fuzzy matching compares each keyword with every word of the commit
message. A word matches if it contains the keyword's letters in order.
Thus "feat" matches "fix(delegation):" and "major" matches
"deps(majors):", which gives false minor and major versions.

The new top-level config key "matching" accepts "fuzzy" (the default)
or "regex". In regex mode every wording keyword is a Go regular
expression. The tool compiles all of them when it loads the config and
runs them against the full commit message, so "(?m)^" anchors a
pattern to the start of a line. An invalid value, an invalid pattern,
or any other config parse error in regex mode stops the tool with a
non-zero exit code. It never falls back to fuzzy matching.

Fuzzy mode does not change. The blacklist check moves into one helper
that both modes use. A replay of 15 repositories gives the same
versions as before. This change improves version precision for the
repositories that opt in.

Claude-Session: https://claude.ai/code/session_01Qq1gbqQek1Zjtofo4VDRdW
2026-09-25 19:35:48 +01:00
github-actions[bot] 9617b281a9 chore: pin action.yml Docker image to v1.17.20 2026-08-28 05:43:46 +00:00
lukaszraczyloandGitHub 6ba4a2674f Update go.mod and go.sum (#66) 2026-08-28 06:38:46 +01:00
github-actions[bot] e9507dd3ff chore: pin action.yml Docker image to v1.17.18 2026-08-13 03:49:16 +00:00
lukaszraczyloandGitHub b5eb8ae15a Update go.mod and go.sum (#65) 2026-08-13 04:43:59 +01:00
github-actions[bot] 0da4aa71e1 chore: pin action.yml Docker image to v1.17.16 2026-08-12 03:46:58 +00:00
lukaszraczyloandGitHub ca874d21e5 Update go.mod and go.sum (#64) 2026-08-12 04:42:16 +01:00
github-actions[bot] 4c21c3f24f chore: pin action.yml Docker image to v1.17.14 2026-08-06 04:04:54 +00:00
lukaszraczyloandGitHub 9d5b4b0b50 Update go.mod and go.sum (#63) 2026-08-06 05:00:15 +01:00
github-actions[bot] 8459e0ad1d chore: pin action.yml Docker image to v1.17.12 2026-07-30 05:34:31 +00:00
lukaszraczyloandGitHub c70ee7f300 Update go.mod and go.sum (#62) 2026-07-30 06:29:36 +01:00
github-actions[bot] 4c1896ae15 chore: pin action.yml Docker image to v1.17.10 2026-07-27 06:20:16 +00:00
lukaszraczyloandGitHub 7b6c46dcb8 Update go.mod and go.sum (#61) 2026-07-27 07:15:35 +01:00
github-actions[bot] 865dcfc461 chore: pin action.yml Docker image to v1.17.8 2026-07-25 05:38:22 +00:00
lukaszraczyloandGitHub a59be0efea Update go.mod and go.sum (#60) 2026-07-25 06:33:10 +01:00
github-actions[bot] 31808f1e6d chore: pin action.yml Docker image to v1.17.6 2026-07-16 05:34:43 +00:00
lukaszraczyloandGitHub 1b80eab874 Update go.mod and go.sum (#59) 2026-07-16 06:30:01 +01:00
github-actions[bot] 8040b42016 chore: pin action.yml Docker image to v1.17.4 2026-07-15 08:44:05 +00:00
lukaszraczylo 1857cd9cf6 Update golang version 2026-07-15 09:38:29 +01:00
lukaszraczylo 888415366a feat(telemetry): switch to SendForModule for auto-version resolution
Replace telemetry.Send(name, PKG_VERSION) with telemetry.SendForModule
so the binary reports its release version automatically from
runtime/debug.BuildInfo whenever PKG_VERSION isn't injected (e.g.
`go install` without ldflags). The goreleaser path still wins because
goreleaser's -X main.PKG_VERSION={{.Version}} populates the fallback,
but `go install github.com/lukaszraczylo/[email protected]`
users now also get the correct version without ldflag plumbing.

Bumps oss-telemetry to v0.2.1.
2026-05-22 23:34:58 +01:00
github-actions[bot] dfbc4d4012 chore: pin action.yml Docker image to v1.17.1 2026-05-21 23:47:12 +00:00
lukaszraczylo dfeb03b8bb fix(semver): skip non-semver tags when picking latest existing tag
Rolling tags like 'v1' (and any other tag that doesn't parse as x.y.z
after prefix stripping) used to enter the latest-tag candidate set in
CalculateSemver. When such a tag shared a commit with a real semver tag
(e.g. v1 and v1.16.3 both pointing at the same release commit), go-git's
alphabetical tag iteration made 'v1' win, ParseExistingSemver bailed out
because '1' has only one component, and the calculator silently reset
the baseline to 0.0.0 — producing nonsense like 0.0.5 on this branch.

ListExistingTags now filters tags through a new IsParseableSemverTag
helper before recording them, so non-semver tags never participate in
latest-tag selection. The behavior change is invisible for repos that
only use proper vX.Y.Z tags, and it's covered by a new test table.
2026-05-22 00:39:59 +01:00
lukaszraczylo ab52de167e fixup! docs: add Telemetry section linking to oss-telemetry opt-out docs 2026-05-22 00:09:10 +01:00
lukaszraczylo 564f39ed10 docs: add Telemetry section linking to oss-telemetry opt-out docs
Discloses the single anonymous adoption ping sent on startup and points
users to the upstream README section for full opt-out instructions
instead of duplicating the table here.
2026-05-21 04:06:20 +01:00
lukaszraczylo 99b8bf937b fix(config-release): remove hardcoded force.commit and force.minor
The hardcoded force.commit (960207e4, from 2022) combined with
force.minor: 16 made semver-generator emit deterministic versions that
already existed as tags (v1.16.1-v1.16.5), causing goreleaser to fail
with 'git tag vX was not made against commit HEAD'.

Restores the intent of 99338e8 which was undone by the later fixup
commits a77bc0c and 5f205e9.

Also fixes 2-space leading indentation.
2026-05-21 03:43:25 +01:00
lukaszraczylo b555e410cc fixup! feat: anonymous usage telemetry via oss-telemetry 2026-05-21 03:24:42 +01:00
lukaszraczylo 521e5c1ff1 feat: anonymous usage telemetry via oss-telemetry
Send a single fire-and-forget ping at startup to help track adoption
and version spread. No persistent identifiers are collected.

Opt out via any of:
  DO_NOT_TRACK=1
  OSS_TELEMETRY_DISABLED=1
  SEMVER_GENERATOR_DISABLE_TELEMETRY=1
2026-05-21 02:45:29 +01:00
lukaszraczylo a223f15e1c Update dependencies 2026-05-21 01:37:58 +01:00
lukaszraczylo d1b8192b78 fixup! feat: auto-pin Docker image version in action.yml after release (#58) 2026-05-21 01:31:27 +01:00
MateoandGitHub a3aed9ef93 feat: auto-pin Docker image version in action.yml after release (#58)
Add update-action-version job that automatically updates action.yml
to reference the specific Docker image version after each release.

This ensures users pinning the action version get the matching Docker
image instead of :latest.
2026-01-22 19:21:25 +00:00
20 changed files with 1024 additions and 130 deletions
+2 -3
View File
@@ -14,7 +14,6 @@ jobs:
autoupdate:
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
with:
go-version: "1.24"
go-version: ">=1.27"
release-workflow: "release.yaml"
secrets:
pat-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
secrets: inherit
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
pr-checks:
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
with:
go-version: "1.24"
go-version: ">=1.27"
+48 -2
View File
@@ -6,7 +6,6 @@ on:
paths-ignore:
- '**.md'
- '**/release.yaml'
- 'action.yml'
branches:
- main
@@ -15,12 +14,59 @@ permissions:
contents: write
packages: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
go-version: "1.24"
go-version: ">=1.27"
docker-enabled: true
rolling-release-tag: "v1"
semver-config: "config-release.yaml"
secrets: inherit
update-action-version:
needs: release
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && needs.release.outputs.version != ''
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update action.yml with release version
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
echo "Updating action.yml to version: ${VERSION}"
sed -i "s|ghcr.io/lukaszraczylo/semver-generator:[^\"]*|ghcr.io/lukaszraczylo/semver-generator:${VERSION}|" action.yml
echo "Updated action.yml:"
grep "image:" action.yml
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add action.yml
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "chore: pin action.yml Docker image to v${{ needs.release.outputs.version }}"
# Push the pin commit onto main explicitly (not just "git push"):
# this checkout started from the released commit (github.sha), not
# main's tip, so if main moved in the meantime this push is a
# non-fast-forward and fails safely instead of overwriting it.
git push origin HEAD:main
# Only move the rolling tag once the pin commit is confirmed on
# main. The "release" job above already moved it to the release
# commit, but this job then commits action.yml's image pin on top
# of that, so without this the rolling tag would always be one
# commit behind, pinning the PREVIOUS image.
# "v1" must match the release job's rolling-release-tag input.
git tag -f v1 HEAD
git push -f origin refs/tags/v1
fi
+45 -2
View File
@@ -11,14 +11,20 @@ Project created overnight, to prove that management of semantic versioning is NO
- [Usage](#usage)
- [Authentication](#authentication)
- [As a binary](#as-a-binary)
- [Homebrew (macOS)](#homebrew-macos)
- [Manual Download](#manual-download)
- [Self-Update](#self-update)
- [As a github action](#as-a-github-action)
- [As a docker container](#as-a-docker-container)
- [Verifying Release Signatures](#verifying-release-signatures)
- [Calculations example \[standard\]](#calculations-example-standard)
- [Calculations example \[strict matching\]](#calculations-example-strict-matching)
- [Release candidates](#release-candidates)
- [Tag prefix stripping](#tag-prefix-stripping)
- [Matching modes](#matching-modes)
- [Example configuration](#example-configuration)
- [Good to know](#good-to-know)
- [Good to knows](#good-to-knows)
- [Telemetry](#telemetry)
### How does it work
@@ -233,6 +239,30 @@ This is particularly useful when:
- Your CI/CD creates tags with component prefixes
- You want to track versions separately for different parts of your codebase
#### Matching modes
Config key `matching` controls how each keyword in `wording` is checked against a commit message. Allowed values are `fuzzy` (the default, also used when the key is absent) and `regex`. Any other value is a config error and `semver-gen` exits non-zero rather than silently falling back to `fuzzy`.
**`fuzzy` (default):** the commit message is split on whitespace into words, and a keyword matches as soon as one word contains the keyword's letters *in order*, case-insensitive — it does not need to be a whole word or an exact substring. This is convenient but has a real false-positive risk: keyword `feat` also matches the word `fix(delegation):` (f-e-a-t appear in order inside "fix(delegation):"), and keyword `major` matches `deps(majors):`. Short, generic keywords are the most likely to over-match; test your wording list against real commit messages before relying on it.
**`regex`:** every keyword in `wording.patch` / `wording.minor` / `wording.major` / `wording.release` is a Go RE2 regular expression (the standard library `regexp` package). Patterns are compiled once when the config is loaded — an invalid pattern makes `semver-gen` exit non-zero immediately, naming the wording list and the keyword. Each pattern is matched with `MatchString` against the **full raw commit message** (subject and body, with the original newlines, not the whitespace-split words `fuzzy` uses), so `(?m)^feat(\([^)]*\))?!?:` anchors to the start of a line. Matching is case-sensitive unless the pattern itself uses `(?i)`. The blacklist works exactly the same way in both modes.
Regex patterns must be quoted in YAML (for example `'(?m)^fix:'`), since unquoted `^`, `:` and other YAML-significant characters can break the file's syntax; a YAML syntax error makes the tool fall back to its defaults, the same as it already does for any unreadable config file.
```yaml
matching: regex
wording:
patch:
- '^fix(\([^)]*\))?:'
minor:
- '(?m)^feat(\([^)]*\))?!?:'
major:
# a trailer line anywhere in the commit body, e.g. "Semver-Major: dropped the v1 API"
- '(?mi)^semver-major:'
```
With this config, a subject line of `fix: correct off-by-one` bumps patch, `feat: add widget` bumps minor, and a body trailer of `Semver-Major: ...` bumps major regardless of case. `fix(delegation): route around dead worker` bumps patch too — the patch pattern's `(\([^)]*\))?` group allows an optional scope — but it does NOT bump minor, unlike `fuzzy` mode, where keyword `feat` matches it (see above).
#### Example configuration
```yaml
@@ -271,10 +301,23 @@ wording:
* `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.
* `matching`: how `wording` keywords are matched against commit messages — `fuzzy` (default) or `regex`. See [Matching modes](#matching-modes).
* `wording`: words the program should look for in the git commits to increment (patch|minor|major)
### Good to knows
* Word matching uses fuzzy search AND is case INSENSITIVE
* Word matching uses fuzzy search AND is case INSENSITIVE by default; set `matching: regex` for anchored, case-sensitive regular expression matching instead — see [Matching modes](#matching-modes)
* I do not recommend using common words ( like "the" from the example configuration )
* You can specify env variable `LOG_LEVEL=debug` to see what exactly happens during the calculations
### Telemetry
On startup this binary sends a single anonymous adoption ping — project name,
version, timestamp; no identifiers, no commit content, no repository data.
Fire-and-forget with a 2-second timeout; cannot block startup or panic.
See **[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**
for the exact wire format, source, and full opt-out documentation.
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
or `SEMVER_GENERATOR_DISABLE_TELEMETRY=1`.
+1 -1
View File
@@ -36,4 +36,4 @@ outputs:
description: "Calculated semantic version"
runs:
using: "docker"
image: "docker://ghcr.io/lukaszraczylo/semver-generator:latest"
image: "docker://ghcr.io/lukaszraczylo/semver-generator:1.18.6"
+12 -1
View File
@@ -18,6 +18,7 @@ limitations under the License.
package cmd
import (
"errors"
"fmt"
"os"
@@ -96,6 +97,15 @@ func main() {
// Read configuration
config, err := utils.ReadConfig(repo.LocalConfigFile)
if err != nil {
// A bad "matching" value or an invalid regex keyword is a real
// misconfiguration, not a missing-file situation: fail loudly
// instead of silently falling back to fuzzy matching.
if errors.Is(err, utils.ErrInvalidMatchingConfig) {
utils.Critical("Invalid matching configuration", map[string]interface{}{
"error": err.Error(),
})
os.Exit(1)
}
utils.Error("Unable to find config file. Using defaults and flags.", map[string]interface{}{
"file": repo.LocalConfigFile,
})
@@ -129,7 +139,7 @@ func main() {
// List existing tags if needed
if params.varExisting || repo.Config.Force.Existing {
utils.ListExistingTags(&repo.GitRepo)
utils.ListExistingTags(&repo.GitRepo, repo.Config.TagPrefixes)
}
// Apply forced versioning
@@ -140,6 +150,7 @@ func main() {
repo.GitRepo.Commits,
repo.GitRepo.Tags,
repo.Config.Wording,
repo.Config.Matcher,
repo.Config.Blacklist,
repo.Semver,
params.varExisting || repo.Config.Force.Existing,
+44 -4
View File
@@ -30,6 +30,8 @@ type Config struct {
Force Force
Blacklist []string
TagPrefixes []string // Prefixes to strip from tags before parsing (e.g., "app-", "infra-", "v")
Matching string // Effective matching mode: MatchingFuzzy (default) or MatchingRegex
Matcher KeywordMatcher
}
// ReadConfig reads the configuration from a file
@@ -43,19 +45,57 @@ func ReadConfig(file string) (*Config, error) {
return config, err
}
// Read "matching" first, right after ReadInConfig, so every later parse
// error below can tell whether regex mode is in play. viper.Get (not
// GetString) lets us reject a non-scalar value, such as a YAML list or
// map, as a config error instead of silently treating it as "fuzzy".
switch rawMatching := viper.Get("matching"); v := rawMatching.(type) {
case nil:
config.Matching = MatchingFuzzy
case string:
switch v {
case "":
config.Matching = MatchingFuzzy
case MatchingFuzzy, MatchingRegex:
config.Matching = v
default:
return config, fmt.Errorf("%w: invalid matching value %q, allowed values are %q and %q", ErrInvalidMatchingConfig, v, MatchingFuzzy, MatchingRegex)
}
default:
return config, fmt.Errorf("%w: matching config must be a string, got %v (%T)", ErrInvalidMatchingConfig, v, v)
}
// In regex mode there is no fuzzy fallback: any config parse error below
// (wording/force/blacklist/tag_prefixes) is a real misconfiguration, so
// wrap it with ErrInvalidMatchingConfig, which main.go treats as fatal.
// In fuzzy mode (default), behaviour is unchanged: the error is returned
// as-is and the caller logs it and continues with defaults.
wrapIfRegex := func(err error) error {
if err == nil || config.Matching != MatchingRegex {
return err
}
return fmt.Errorf("%w: %v", ErrInvalidMatchingConfig, err)
}
if err := viper.UnmarshalKey("wording", &config.Wording); err != nil {
return config, fmt.Errorf("error parsing wording config: %w", err)
return config, wrapIfRegex(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)
return config, wrapIfRegex(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)
return config, wrapIfRegex(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, wrapIfRegex(fmt.Errorf("error parsing tag_prefixes config: %w", err))
}
matcher, err := NewKeywordMatcher(config.Matching, config.Wording)
if err != nil {
return config, err
}
config.Matcher = matcher
return config, nil
}
+198
View File
@@ -1,10 +1,13 @@
package utils
import (
"errors"
"os"
"testing"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestApplyForcedVersioning(t *testing.T) {
@@ -199,3 +202,198 @@ wording:
_, err = ReadConfig("non-existent-file.yaml")
assert.Error(t, err)
}
func writeTempConfig(t *testing.T, content string) string {
t.Helper()
tempFile, err := os.CreateTemp("", "semver-config-matching-*.yaml")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
if _, err := tempFile.Write([]byte(content)); err != nil {
t.Fatalf("Failed to write to temp file: %v", err)
}
if err := tempFile.Close(); err != nil {
t.Fatalf("Failed to close temp file: %v", err)
}
t.Cleanup(func() {
if err := os.Remove(tempFile.Name()); err != nil && !os.IsNotExist(err) {
t.Logf("failed to remove temp config %s: %v", tempFile.Name(), err)
}
})
return tempFile.Name()
}
func TestReadConfigMatching(t *testing.T) {
InitLogger(false)
t.Run("absent matching key defaults to fuzzy", func(t *testing.T) {
path := writeTempConfig(t, `
wording:
patch:
- feat
`)
config, err := ReadConfig(path)
assert.NoError(t, err)
assert.Equal(t, MatchingFuzzy, config.Matching)
assert.IsType(t, FuzzyMatcher{}, config.Matcher)
// Behavioural check, not just the type: absent/fuzzy mode means
// "fix(delegation): x" fuzzy-matches keyword "feat", using the real
// fuzzy library (the same one main.go wires up), not a stand-in.
originalFuzzyFind := FuzzyFind
t.Cleanup(func() { FuzzyFind = originalFuzzyFind })
FuzzyFind = fuzzy.FindNormalizedFold
assert.True(t, config.Matcher.Matches("fix(delegation): x", config.Wording.Patch, nil))
})
t.Run("explicit fuzzy", func(t *testing.T) {
path := writeTempConfig(t, `
matching: fuzzy
wording:
patch:
- update
`)
config, err := ReadConfig(path)
assert.NoError(t, err)
assert.Equal(t, MatchingFuzzy, config.Matching)
assert.IsType(t, FuzzyMatcher{}, config.Matcher)
// Behavioural check, not just the type: same as the absent-key
// subtest above, using the real fuzzy library.
originalFuzzyFind := FuzzyFind
t.Cleanup(func() { FuzzyFind = originalFuzzyFind })
FuzzyFind = fuzzy.FindNormalizedFold
assert.True(t, config.Matcher.Matches("fix(delegation): x", []string{"feat"}, nil))
})
t.Run("explicit regex compiles keywords", func(t *testing.T) {
path := writeTempConfig(t, `
matching: regex
wording:
patch:
- "^fix:"
minor:
- "(?m)^feat(\\([^)]*\\))?!?:"
major:
- "(?mi)^semver-major:"
`)
config, err := ReadConfig(path)
assert.NoError(t, err)
assert.Equal(t, MatchingRegex, config.Matching)
assert.IsType(t, RegexMatcher{}, config.Matcher)
// Behavioural check: "fix(delegation): x" matches fuzzy keyword "feat"
// (see the absent/fuzzy subtest below) but must NOT match this
// anchored regex pattern.
assert.False(t, config.Matcher.Matches("fix(delegation): x", config.Wording.Minor, nil))
})
t.Run("unknown matching value errors naming value and allowed values", func(t *testing.T) {
path := writeTempConfig(t, `
matching: substring
wording:
patch:
- update
`)
_, err := ReadConfig(path)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "substring")
assert.Contains(t, err.Error(), MatchingFuzzy)
assert.Contains(t, err.Error(), MatchingRegex)
})
// Fix 1: an invalid "matching" value must be rejected immediately, before
// wording/force/blacklist/tag_prefixes are ever parsed. Previously a
// mistyped value like "substring" was accepted as-is by the case string
// branch, so a malformed wording section below it returned an unwrapped
// parse error instead of ErrInvalidMatchingConfig, and main() silently
// fell back to fuzzy mode instead of failing loudly.
t.Run("invalid matching value combined with malformed wording section still errors as ErrInvalidMatchingConfig", func(t *testing.T) {
path := writeTempConfig(t, `
matching: substring
wording: "not-a-map"
`)
_, err := ReadConfig(path)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "substring")
})
t.Run("invalid regex errors naming wording list and keyword", func(t *testing.T) {
path := writeTempConfig(t, `
matching: regex
wording:
major:
- "[unterminated("
`)
_, err := ReadConfig(path)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "major")
assert.Contains(t, err.Error(), "[unterminated(")
})
// Fix 2: no silent fuzzy fallback in regex mode. A malformed "wording"
// section (a scalar instead of a map) fails viper.UnmarshalKey, not
// NewKeywordMatcher. In regex mode that must still be a fatal,
// ErrInvalidMatchingConfig-wrapped error — main() exits non-zero on it
// (see cmd/main.go's errors.Is(err, utils.ErrInvalidMatchingConfig)
// check) instead of silently logging and falling back to fuzzy matching.
t.Run("regex mode + malformed wording section is a fatal error, not a fuzzy fallback", func(t *testing.T) {
path := writeTempConfig(t, `
matching: regex
wording: "not-a-map"
`)
_, err := ReadConfig(path)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig, "main() only treats ErrInvalidMatchingConfig as fatal; anything else falls back to fuzzy and continues")
assert.Contains(t, err.Error(), "wording")
})
// Contrast case: the exact same malformed "wording" section in fuzzy mode
// (the default) must behave exactly as it did before regex mode existed —
// an error is returned, but it is NOT wrapped in ErrInvalidMatchingConfig,
// so main() logs it and continues with defaults rather than exiting.
t.Run("fuzzy mode + malformed wording section stays non-fatal, same as today", func(t *testing.T) {
path := writeTempConfig(t, `
wording: "not-a-map"
`)
_, err := ReadConfig(path)
assert.Error(t, err)
assert.False(t, errors.Is(err, ErrInvalidMatchingConfig), "fuzzy mode must not fail loudly for non-matching config errors")
})
// Fix 4: a non-scalar "matching" value (a YAML list or map) must be an
// ErrInvalidMatchingConfig error naming the value, never a silent fuzzy
// fallback.
t.Run("matching as a YAML list is an error naming the value", func(t *testing.T) {
path := writeTempConfig(t, `
matching:
- fuzzy
- regex
wording:
patch:
- update
`)
_, err := ReadConfig(path)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "fuzzy")
assert.Contains(t, err.Error(), "regex")
})
t.Run("matching as a YAML map is an error naming the value", func(t *testing.T) {
path := writeTempConfig(t, `
matching:
mode: regex
wording:
patch:
- update
`)
_, err := ReadConfig(path)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "mode")
})
}
+11 -6
View File
@@ -163,10 +163,13 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
}
// ListExistingTags lists all tags in the repository
func ListExistingTags(repo *GitRepository) {
// ListExistingTags lists all tags in the repository.
// Tags that don't parse as proper semver (rolling tags like "v1" or "latest")
// are skipped so they can't out-rank real semver tags pointing to the same
// commit during latest-tag selection.
func ListExistingTags(repo *GitRepository, tagPrefixes []string) {
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
@@ -180,15 +183,17 @@ func ListExistingTags(repo *GitRepository) {
if err := refs.ForEach(func(ref *plumbing.Reference) error {
tagName := ref.Name().Short()
commitHash := ref.Hash().String()
// For annotated tags, dereference to get the actual commit hash
if !IsParseableSemverTag(tagName, tagPrefixes) {
Debug("Skipping non-semver tag", map[string]interface{}{"tag": tagName})
return nil
}
commitHash := ref.Hash().String()
tagObj, err := repo.Handler.TagObject(ref.Hash())
if err == nil {
// This is an annotated tag - get the commit it points to
commitHash = tagObj.Target.String()
}
// If err != nil, it's a lightweight tag - ref.Hash() is already the commit hash
repo.Tags = append(repo.Tags, TagDetails{
Name: tagName,
+1 -1
View File
@@ -142,7 +142,7 @@ func TestListExistingTags(t *testing.T) {
repo := &GitRepository{}
// Now we can safely call ListExistingTags since we've added a nil check
ListExistingTags(repo)
ListExistingTags(repo, nil)
// Verify no tags were added
assert.Empty(t, repo.Tags, "Should have no tags after calling with nil Handler")
+119
View File
@@ -0,0 +1,119 @@
// Package utils implements semver-gen's configuration loading, git access,
// commit-keyword matching (fuzzy and regex) and version calculation.
package utils
import (
"errors"
"fmt"
"regexp"
"strings"
)
// Matching mode values accepted by the "matching" config key.
const (
MatchingFuzzy = "fuzzy"
MatchingRegex = "regex"
)
// ErrInvalidMatchingConfig is wrapped by any error NewKeywordMatcher returns.
// Callers use errors.Is to tell a bad "matching" value or a bad regex keyword
// apart from other, non-fatal config problems.
var ErrInvalidMatchingConfig = errors.New("invalid matching configuration")
// KeywordMatcher decides whether a commit message matches one of a wording
// list's keywords, honoring the blacklist. CalculateSemver picks one
// implementation, chosen once from config, instead of branching on the
// matching mode itself.
type KeywordMatcher interface {
Matches(commitMessage string, keywords []string, blacklist []string) bool
}
// FuzzyMatcher reproduces the original matching behaviour: the commit message
// is split into whitespace-separated words and each keyword is fuzzy-matched
// (letters in order, case-insensitive) against those words via FuzzyFind.
type FuzzyMatcher struct{}
// Matches implements KeywordMatcher.
func (FuzzyMatcher) Matches(commitMessage string, keywords []string, blacklist []string) bool {
return CheckMatches(strings.Fields(commitMessage), keywords, blacklist)
}
// RegexMatcher matches keywords, each a Go RE2 regular expression compiled
// once up front, against the full raw commit message (subject and body,
// newlines preserved).
type RegexMatcher struct {
compiled map[string]*regexp.Regexp
}
// Matches implements KeywordMatcher.
func (m RegexMatcher) Matches(commitMessage string, keywords []string, blacklist []string) bool {
hasMatch := false
for _, keyword := range keywords {
re, ok := m.compiled[keyword]
if !ok {
// NewKeywordMatcher compiles every keyword of every wording list up
// front, so this means Matches was called with a keyword that
// wasn't part of the wording the matcher was built from. Log it
// loudly instead of silently skipping, and treat it as no-match
// rather than panicking on a nil regexp.
Error("Regex keyword was never compiled; treating as no-match", map[string]interface{}{
"keyword": keyword,
})
continue
}
if re.MatchString(commitMessage) {
hasMatch = true
break
}
}
if !hasMatch {
return false
}
// Match blacklist semantics exactly the same way CheckMatches (fuzzy mode)
// does: whitespace-normalized content, so a blacklist term split across a
// newline or tab in the raw commit message still suppresses the match.
contentStr := strings.Join(strings.Fields(commitMessage), " ")
return !isBlacklisted(contentStr, blacklist)
}
// NewKeywordMatcher builds the KeywordMatcher selected by matching ("" and
// "fuzzy" both mean fuzzy, "regex" means regex; anything else is an error).
// In regex mode every keyword across all four wording lists is compiled
// exactly once here, so a bad pattern is caught at config-load time instead
// of failing mid-run. Compile and validation errors wrap
// ErrInvalidMatchingConfig so callers can fail loudly on them.
func NewKeywordMatcher(matching string, wording Wording) (KeywordMatcher, error) {
switch matching {
case "", MatchingFuzzy:
return FuzzyMatcher{}, nil
case MatchingRegex:
wordingLists := []struct {
name string
keywords []string
}{
{"patch", wording.Patch},
{"minor", wording.Minor},
{"major", wording.Major},
{"release", wording.Release},
}
compiled := make(map[string]*regexp.Regexp)
for _, list := range wordingLists {
for _, keyword := range list.keywords {
if _, alreadyCompiled := compiled[keyword]; alreadyCompiled {
continue
}
re, err := regexp.Compile(keyword)
if err != nil {
return nil, fmt.Errorf("%w: invalid regex in wording.%s keyword %q: %v", ErrInvalidMatchingConfig, list.name, keyword, err)
}
compiled[keyword] = re
}
}
return RegexMatcher{compiled: compiled}, nil
default:
return nil, fmt.Errorf("%w: invalid matching value %q, allowed values are %q and %q", ErrInvalidMatchingConfig, matching, MatchingFuzzy, MatchingRegex)
}
}
+220
View File
@@ -0,0 +1,220 @@
package utils
import (
"testing"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewKeywordMatcher(t *testing.T) {
InitLogger(false)
wording := Wording{
Patch: []string{"^fix:"},
Minor: []string{`(?m)^feat(\([^)]*\))?!?:`},
Major: []string{"(?mi)^semver-major:"},
}
t.Run("empty matching defaults to fuzzy", func(t *testing.T) {
m, err := NewKeywordMatcher("", wording)
assert.NoError(t, err)
assert.IsType(t, FuzzyMatcher{}, m)
// Behavioural check, not just the type: fuzzy matching means
// "fix(delegation): x" still matches keyword "feat" (f-e-a-t appear
// in order inside "fix(delegation):"), using the real fuzzy library
// (the same one main.go wires up), not a stand-in.
originalFuzzyFind := FuzzyFind
t.Cleanup(func() { FuzzyFind = originalFuzzyFind })
FuzzyFind = fuzzy.FindNormalizedFold
assert.True(t, m.Matches("fix(delegation): x", []string{"feat"}, nil))
})
t.Run("fuzzy", func(t *testing.T) {
m, err := NewKeywordMatcher(MatchingFuzzy, wording)
assert.NoError(t, err)
assert.IsType(t, FuzzyMatcher{}, m)
// Behavioural check, not just the type: same as the empty-matching
// subtest above, using the real fuzzy library.
originalFuzzyFind := FuzzyFind
t.Cleanup(func() { FuzzyFind = originalFuzzyFind })
FuzzyFind = fuzzy.FindNormalizedFold
assert.True(t, m.Matches("fix(delegation): x", []string{"feat"}, nil))
})
t.Run("regex compiles every keyword once", func(t *testing.T) {
m, err := NewKeywordMatcher(MatchingRegex, wording)
assert.NoError(t, err)
assert.IsType(t, RegexMatcher{}, m)
// Behavioural check: the same input that fuzzy-matches "feat" does not
// match the anchored regex pattern in wording.Minor.
assert.False(t, m.Matches("fix(delegation): x", wording.Minor, nil))
})
t.Run("unknown value errors naming value and allowed values", func(t *testing.T) {
_, err := NewKeywordMatcher("weird", wording)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "weird")
assert.Contains(t, err.Error(), MatchingFuzzy)
assert.Contains(t, err.Error(), MatchingRegex)
})
t.Run("invalid regex errors naming wording list and keyword", func(t *testing.T) {
badWording := Wording{Major: []string{"[unterminated("}}
_, err := NewKeywordMatcher(MatchingRegex, badWording)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "major")
assert.Contains(t, err.Error(), "[unterminated(")
})
t.Run("invalid release regex errors naming wording list and keyword", func(t *testing.T) {
badWording := Wording{Release: []string{"[unterminated("}}
_, err := NewKeywordMatcher(MatchingRegex, badWording)
require.Error(t, err)
require.ErrorIs(t, err, ErrInvalidMatchingConfig)
assert.Contains(t, err.Error(), "release")
assert.Contains(t, err.Error(), "[unterminated(")
})
}
func TestRegexMatcherMatches(t *testing.T) {
InitLogger(false)
t.Run("anchored feat subject matches", func(t *testing.T) {
m, err := NewKeywordMatcher(MatchingRegex, Wording{
Minor: []string{`(?m)^feat(\([^)]*\))?!?:`},
})
assert.NoError(t, err)
got := m.Matches("feat: add widget\n\nsome body text", []string{`(?m)^feat(\([^)]*\))?!?:`}, nil)
assert.True(t, got)
})
t.Run("fix(delegation) subject does not match anchored feat pattern", func(t *testing.T) {
m, err := NewKeywordMatcher(MatchingRegex, Wording{
Minor: []string{`(?m)^feat(\([^)]*\))?!?:`},
})
assert.NoError(t, err)
got := m.Matches("fix(delegation): route around dead worker", []string{`(?m)^feat(\([^)]*\))?!?:`}, nil)
assert.False(t, got)
})
t.Run("body trailer Semver-Major matches multiline case-insensitive pattern", func(t *testing.T) {
pattern := "(?mi)^semver-major:"
m, err := NewKeywordMatcher(MatchingRegex, Wording{Major: []string{pattern}})
assert.NoError(t, err)
message := "fix: patch a bug\n\nDetails here.\n\nSemver-Major: breaking storage format\n"
got := m.Matches(message, []string{pattern}, nil)
assert.True(t, got)
})
t.Run("feat! subject does not match a trailer-only major pattern", func(t *testing.T) {
pattern := "(?mi)^semver-major:"
m, err := NewKeywordMatcher(MatchingRegex, Wording{Major: []string{pattern}})
assert.NoError(t, err)
got := m.Matches("feat!: rework the API\n\nno trailer here", []string{pattern}, nil)
assert.False(t, got)
})
t.Run("case sensitivity: pattern without (?i) does not match different case", func(t *testing.T) {
pattern := "^FEAT:"
m, err := NewKeywordMatcher(MatchingRegex, Wording{Minor: []string{pattern}})
assert.NoError(t, err)
assert.False(t, m.Matches("feat: lowercase subject", []string{pattern}, nil))
assert.True(t, m.Matches("FEAT: uppercase subject", []string{pattern}, nil))
})
t.Run("blacklist still suppresses a regex match", func(t *testing.T) {
pattern := `(?m)^feat(\([^)]*\))?!?:`
m, err := NewKeywordMatcher(MatchingRegex, Wording{Minor: []string{pattern}})
assert.NoError(t, err)
message := "feat: add widget [skip-ci]"
assert.True(t, m.Matches(message, []string{pattern}, nil))
assert.False(t, m.Matches(message, []string{pattern}, []string{"skip-ci"}))
})
t.Run("blacklist term split by a newline still suppresses, same as fuzzy mode", func(t *testing.T) {
pattern := `(?m)^feat(\([^)]*\))?!?:`
m, err := NewKeywordMatcher(MatchingRegex, Wording{Minor: []string{pattern}})
assert.NoError(t, err)
// Blacklist term is "skip ci", but the raw commit message has it split
// across a newline: "skip\nci". CheckMatches (fuzzy mode) would still
// catch this because it joins strings.Fields(msg) with a single space
// before the substring check; the regex-mode blacklist check must do
// the same.
message := "feat: add widget\n\nskip\nci"
assert.True(t, m.Matches(message, []string{pattern}, nil), "sanity: matches without blacklist")
assert.False(t, m.Matches(message, []string{pattern}, []string{"skip ci"}))
})
t.Run("blacklist term split by a tab still suppresses, same as fuzzy mode", func(t *testing.T) {
pattern := `(?m)^feat(\([^)]*\))?!?:`
m, err := NewKeywordMatcher(MatchingRegex, Wording{Minor: []string{pattern}})
assert.NoError(t, err)
message := "feat: add widget\n\nskip\tci"
assert.True(t, m.Matches(message, []string{pattern}, nil), "sanity: matches without blacklist")
assert.False(t, m.Matches(message, []string{pattern}, []string{"skip ci"}))
})
t.Run("full-message anchoring: mid-message keyword-looking text does not match anchored pattern", func(t *testing.T) {
// Discriminating case: if RegexMatcher ran MatchString per
// strings.Fields word instead of against the full raw message, the
// word "feat:" here would match "(?m)^feat...:" on its own (it starts
// a "word"), even though the actual commit subject starts with
// "docs:". Matching against the full message must not match.
pattern := `(?m)^feat(\([^)]*\))?!?:`
m, err := NewKeywordMatcher(MatchingRegex, Wording{Minor: []string{pattern}})
assert.NoError(t, err)
got := m.Matches("docs: explain feat: usage", []string{pattern}, nil)
assert.False(t, got)
})
t.Run("full-message anchoring: multiline trailer matches (?m) anchor on its own line", func(t *testing.T) {
// Discriminating case: strings.Fields on this message splits "BREAKING"
// and "CHANGE:" into separate words, neither of which starts with
// "BREAKING CHANGE:", so a per-word implementation would miss this.
// Matching against the full raw message (newlines kept) must match,
// because (?m) anchors "^" to the start of the trailer's own line.
pattern := `(?m)^BREAKING CHANGE:`
m, err := NewKeywordMatcher(MatchingRegex, Wording{Major: []string{pattern}})
assert.NoError(t, err)
got := m.Matches("refactor: x\n\nBREAKING CHANGE: drop v1", []string{pattern}, nil)
assert.True(t, got)
})
t.Run("keyword missing from compiled map is a no-match, not a panic", func(t *testing.T) {
// The matcher was built from wording.Minor only; calling Matches with
// an unrelated keyword exercises the "not compiled" branch. It must
// log loudly (via the package's Error logger) rather than silently
// return, but it must not miss the check or panic on a nil regexp.
m, err := NewKeywordMatcher(MatchingRegex, Wording{Minor: []string{`(?m)^feat:`}})
assert.NoError(t, err)
assert.NotPanics(t, func() {
got := m.Matches("feat: add widget", []string{"^never-compiled:"}, nil)
assert.False(t, got)
})
})
}
func TestFuzzyMatcherMatches(t *testing.T) {
InitLogger(false)
originalFuzzyFind := FuzzyFind
defer func() { FuzzyFind = originalFuzzyFind }()
FuzzyFind = func(needle string, haystack []string) []string {
for _, h := range haystack {
if h == needle {
return []string{h}
}
}
return nil
}
m := FuzzyMatcher{}
assert.True(t, m.Matches("update dependencies", []string{"update", "fix"}, nil))
assert.False(t, m.Matches("chore dependencies", []string{"update", "fix"}, nil))
assert.False(t, m.Matches("update dependencies skip-ci", []string{"update", "fix"}, []string{"skip-ci"}))
}
+11 -5
View File
@@ -9,12 +9,19 @@ func CalculateSemver(
commits []CommitDetails,
tags []TagDetails,
wording Wording,
matcher KeywordMatcher,
blacklist []string,
initialSemver SemVer,
respectExisting bool,
strictMode bool,
tagPrefixes []string,
) SemVer {
// Default-preserving: callers that don't select a matcher get today's
// fuzzy behaviour rather than a nil-pointer panic.
if matcher == nil {
matcher = FuzzyMatcher{}
}
semver := initialSemver
startIndex := 0
@@ -57,11 +64,10 @@ func CalculateSemver(
}
// Check for keyword matches
commitSlice := strings.Fields(commit.Message)
matchPatch := CheckMatches(commitSlice, wording.Patch, blacklist)
matchMinor := CheckMatches(commitSlice, wording.Minor, blacklist)
matchMajor := CheckMatches(commitSlice, wording.Major, blacklist)
matchReleaseCandidate := CheckMatches(commitSlice, wording.Release, blacklist)
matchPatch := matcher.Matches(commit.Message, wording.Patch, blacklist)
matchMinor := matcher.Matches(commit.Message, wording.Minor, blacklist)
matchMajor := matcher.Matches(commit.Message, wording.Major, blacklist)
matchReleaseCandidate := matcher.Matches(commit.Message, wording.Release, blacklist)
// Apply version changes based on matches
if matchMajor {
+147
View File
@@ -4,6 +4,7 @@ import (
"testing"
"time"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/stretchr/testify/assert"
)
@@ -281,6 +282,7 @@ func TestCalculateSemver(t *testing.T) {
tt.commits,
tt.tags,
tt.wording,
FuzzyMatcher{},
tt.blacklist,
tt.initialSemver,
tt.respectExisting,
@@ -296,3 +298,148 @@ func TestCalculateSemver(t *testing.T) {
})
}
}
// TestCalculateSemverRegexModeEndToEnd exercises the whole config->matcher->
// CalculateSemver path in regex mode, with anchored conventional-commit
// patterns and a "Semver-Major:" trailer.
func TestCalculateSemverRegexModeEndToEnd(t *testing.T) {
InitLogger(false)
wording := Wording{
Patch: []string{`^fix:`},
Minor: []string{`(?m)^feat(\([^)]*\))?!?:`},
Major: []string{`(?mi)^semver-major:`},
Release: []string{},
}
matcher, err := NewKeywordMatcher(MatchingRegex, wording)
assert.NoError(t, err)
// c4 ("fix(delegation): ...") is placed AFTER the major commit (c5) on
// purpose: the anchored minor pattern `(?m)^feat(\([^)]*\))?!?:` must not
// match a message that starts with "fix", so c4 contributes no version
// bump at all under correct regex matching. If a future regression made
// RegexMatcher match per-word instead of against the full raw message
// (the false positive fuzzy mode has, see matcher_test.go's
// "full-message anchoring" cases), c4 would wrongly bump Minor. With c4
// last, nothing runs after it to reset that spurious bump back to 0, so
// the final assertions below would catch it. (With c4 before c5, as it
// used to be, the major commit's Minor=0 reset would mask the same bug.)
now := time.Now()
commits := []CommitDetails{
{Hash: "c1", Message: "chore: init repo", Timestamp: now.Add(-5 * time.Hour)},
{Hash: "c2", Message: "fix: correct off-by-one", Timestamp: now.Add(-4 * time.Hour)},
{Hash: "c3", Message: "feat: add widget\n\nsome body text", Timestamp: now.Add(-3 * time.Hour)},
{Hash: "c5", Message: "docs: update README\n\nSemver-Major: breaking storage format", Timestamp: now.Add(-2 * time.Hour)},
{Hash: "c4", Message: "fix(delegation): route around dead worker", Timestamp: now.Add(-1 * time.Hour)},
}
got := CalculateSemver(
commits,
nil,
wording,
matcher,
nil,
SemVer{},
false,
true, // strict mode: only wording matches move the version
nil,
)
assert.Equal(t, 1, got.Major, "Major version mismatch")
assert.Equal(t, 0, got.Minor, "Minor version mismatch")
assert.Equal(t, 1, got.Patch, "Patch version mismatch")
assert.Equal(t, 0, got.Release, "Release version mismatch")
assert.False(t, got.EnableReleaseCandidate)
}
// TestCalculateSemverRegexModeReleaseCandidateEndToEnd exercises
// wording.Release in regex mode end to end: a commit matching the release
// pattern must bump the release-candidate counter, exactly like fuzzy mode
// does for wording.Release matches.
func TestCalculateSemverRegexModeReleaseCandidateEndToEnd(t *testing.T) {
InitLogger(false)
wording := Wording{
Patch: []string{`^fix:`},
Minor: []string{`(?m)^feat(\([^)]*\))?!?:`},
Major: []string{`(?mi)^semver-major:`},
Release: []string{`(?m)^release-candidate:`},
}
matcher, err := NewKeywordMatcher(MatchingRegex, wording)
assert.NoError(t, err)
now := time.Now()
commits := []CommitDetails{
{Hash: "c1", Message: "fix: correct off-by-one", Timestamp: now.Add(-2 * time.Hour)},
{Hash: "c2", Message: "release-candidate: cut rc for QA", Timestamp: now.Add(-1 * time.Hour)},
}
got := CalculateSemver(
commits,
nil,
wording,
matcher,
nil,
SemVer{},
false,
true, // strict mode: only wording matches move the version
nil,
)
assert.Equal(t, 0, got.Major, "Major version mismatch")
assert.Equal(t, 0, got.Minor, "Minor version mismatch")
assert.Equal(t, 1, got.Patch, "Patch version mismatch")
assert.Equal(t, 1, got.Release, "Release version mismatch")
assert.True(t, got.EnableReleaseCandidate)
}
// TestCalculateSemverFuzzyModeUnchanged runs CalculateSemver with the real
// fuzzy.FindNormalizedFold implementation (the same one main.go wires up) to
// confirm fuzzy mode's results are unchanged by the matcher refactor.
func TestCalculateSemverFuzzyModeUnchanged(t *testing.T) {
InitLogger(false)
originalFuzzyFind := FuzzyFind
defer func() { FuzzyFind = originalFuzzyFind }()
FuzzyFind = fuzzy.FindNormalizedFold
wording := Wording{
Patch: []string{"fix"},
Minor: []string{"feat"},
Major: []string{"breaking"},
}
blacklist := []string{"skip-ci"}
now := time.Now()
commits := []CommitDetails{
{Hash: "c1", Message: "chore: init", Timestamp: now.Add(-6 * time.Hour)},
{Hash: "c2", Message: "fix: correct bug", Timestamp: now.Add(-5 * time.Hour)},
{Hash: "c3", Message: "feat: add widget", Timestamp: now.Add(-4 * time.Hour)},
{Hash: "c4", Message: "docs: update readme", Timestamp: now.Add(-3 * time.Hour)},
{Hash: "c5", Message: "breaking: change api contract", Timestamp: now.Add(-2 * time.Hour)},
{Hash: "c6", Message: "fix: cleanup skip-ci", Timestamp: now.Add(-1 * time.Hour)},
}
matcher, err := NewKeywordMatcher(MatchingFuzzy, wording)
assert.NoError(t, err)
got := CalculateSemver(
commits,
nil,
wording,
matcher,
blacklist,
SemVer{},
false,
false, // non-strict: default patch increment applies too
nil,
)
assert.Equal(t, 1, got.Major, "Major version mismatch")
assert.Equal(t, 0, got.Minor, "Minor version mismatch")
assert.Equal(t, 2, got.Patch, "Patch version mismatch")
assert.Equal(t, 0, got.Release, "Release version mismatch")
assert.False(t, got.EnableReleaseCandidate)
}
+46 -10
View File
@@ -83,6 +83,29 @@ func StripTagPrefix(tagName string, prefixes []string) string {
return result
}
// IsParseableSemverTag reports whether tagName looks like a proper semver tag
// (X.Y.Z, optionally vX.Y.Z, optionally with -rc.N suffix) after configured
// prefixes are stripped. Rolling tags like "v1" or "latest" return false so the
// calculator can skip them when picking the latest existing tag — preventing a
// rolling tag tied to the same commit as a real semver tag from "winning" the
// alphabetical iteration in go-git and resetting the baseline to 0.0.0.
func IsParseableSemverTag(tagName string, prefixes []string) bool {
clean := StripTagPrefix(tagName, prefixes)
if idx := strings.Index(clean, "-rc."); idx != -1 {
clean = clean[:idx]
}
parts := strings.Split(clean, ".")
if len(parts) < 3 {
return false
}
for _, p := range parts[:3] {
if len(extractNumber.FindAllString(p, -1)) == 0 {
return false
}
}
return true
}
// ParseExistingSemver parses a semantic version from a tag name
func ParseExistingSemver(tagName string, currentSemver SemVer, prefixes []string) SemVer {
Debug("Parsing existing semver", map[string]interface{}{"tag": tagName})
@@ -143,6 +166,27 @@ func ParseExistingSemver(tagName string, currentSemver SemVer, prefixes []string
return semanticVersion
}
// isBlacklisted reports whether contentStr contains any blacklist term
// (case-insensitive substring match), logging the same way every caller used
// to log it inline. Both matching modes call this so blacklist semantics stay
// identical between fuzzy and regex matching.
func isBlacklisted(contentStr string, blacklist []string) bool {
if len(blacklist) == 0 {
return false
}
lowerContent := strings.ToLower(contentStr)
for _, blacklistTerm := range blacklist {
if strings.Contains(lowerContent, strings.ToLower(blacklistTerm)) {
Debug("Blacklisted term detected, ignoring commit", map[string]interface{}{
"content": contentStr,
"blacklist_term": blacklistTerm,
})
return true
}
}
return false
}
// CheckMatches checks if any of the targets match the content
func CheckMatches(content []string, targets []string, blacklist []string) bool {
contentStr := strings.Join(content, " ")
@@ -163,16 +207,8 @@ func CheckMatches(content []string, targets []string, blacklist []string) bool {
}
// If we have a match, check against blacklist
if hasMatch && len(blacklist) > 0 {
for _, blacklistTerm := range blacklist {
if strings.Contains(strings.ToLower(contentStr), strings.ToLower(blacklistTerm)) {
Debug("Blacklisted term detected, ignoring commit", map[string]interface{}{
"content": contentStr,
"blacklist_term": blacklistTerm,
})
return false
}
}
if hasMatch && isBlacklisted(contentStr, blacklist) {
return false
}
return hasMatch
+28
View File
@@ -189,6 +189,34 @@ func TestParseExistingSemver(t *testing.T) {
})
}
}
func TestIsParseableSemverTag(t *testing.T) {
InitLogger(false)
tests := []struct {
name string
tag string
prefixes []string
want bool
}{
{name: "plain x.y.z", tag: "1.2.3", want: true},
{name: "v prefix", tag: "v1.16.5", want: true},
{name: "v prefix with rc", tag: "v2.0.0-rc.3", want: true},
{name: "configured app- prefix", tag: "app-1.2.3", prefixes: []string{"app-"}, want: true},
{name: "rolling v1 tag", tag: "v1", want: false},
{name: "rolling latest tag", tag: "latest", want: false},
{name: "two-component", tag: "1.2", want: false},
{name: "empty", tag: "", want: false},
{name: "non-numeric major", tag: "vX.Y.Z", want: false},
{name: "just text", tag: "release-day", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, IsParseableSemverTag(tt.tag, tt.prefixes))
})
}
}
func TestCheckMatches(t *testing.T) {
// Initialize logger for tests
+10 -13
View File
@@ -1,13 +1,10 @@
version: 1
force:
major: 1
minor: 16 # Start from v1.16.x
existing: true
strict: false
commit: 960207e4677476ad31a9f389f74eaf9f33d49613
wording:
minor:
- change
- improve
major:
- breaking
version: 1
force:
existing: true
strict: false
wording:
minor:
- change
- improve
major:
- breaking
+23 -29
View File
@@ -1,46 +1,42 @@
module github.com/lukaszraczylo/semver-generator
go 1.24.0
toolchain go1.24.6
go 1.26.0
require (
github.com/go-git/go-git/v5 v5.16.4
github.com/go-git/go-git/v5 v5.19.2
github.com/lithammer/fuzzysearch v1.1.8
github.com/lukaszraczylo/graphql-monitoring-proxy v0.42.4
github.com/lukaszraczylo/graphql-monitoring-proxy v1.0.10
github.com/lukaszraczylo/oss-telemetry v0.2.5
github.com/lukaszraczylo/pandati v0.0.29
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/ProtonMail/go-crypto v1.5.2 // indirect
github.com/cloudflare/circl v1.6.5 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.7.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/go-git/go-billy/v5 v5.9.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.11.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pjbgf/sha1cd v0.7.0 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/skeema/knownhosts v1.3.3 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
@@ -51,12 +47,10 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect
github.com/wI2L/jsondiff v0.6.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+48 -51
View File
@@ -3,46 +3,44 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-crypto v1.5.2 h1:cucYnvqcY7UOXVD//mSyjeaPY0SSN3v5cDkYPxumINk=
github.com/ProtonMail/go-crypto v1.5.2/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
@@ -52,10 +50,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -65,28 +61,28 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lukaszraczylo/graphql-monitoring-proxy v0.42.4 h1:gq/bEq+JzPes8WR24HHys9VvVWREDEXOWFoVSOFXCCw=
github.com/lukaszraczylo/graphql-monitoring-proxy v0.42.4/go.mod h1:1FLcH7q+7cjUgQxyeVeF7ouBamGpcJZgqDF+j+cuFxI=
github.com/lukaszraczylo/graphql-monitoring-proxy v1.0.10 h1:9edy9Evu90VfygGfgoURB6rlse045gdfL522uL2nR4I=
github.com/lukaszraczylo/graphql-monitoring-proxy v1.0.10/go.mod h1:uV8HpAxnPWmSyx/yNt8k5YSIVpBKEgOloWMy/iqkMuk=
github.com/lukaszraczylo/oss-telemetry v0.2.5 h1:W6Qgwp7IY4yBRO361MmnEAmM59motdQpbAFAx8v6fJM=
github.com/lukaszraczylo/oss-telemetry v0.2.5/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
github.com/lukaszraczylo/pandati v0.0.29 h1:WUEWm1+hWjE5KJbIL8OctG00x2dk4XKGJSlrjhxZ55k=
github.com/lukaszraczylo/pandati v0.0.29/go.mod h1:+DyTWKFaXd+jIfe7GW5w2S5PyTko/RXxMyOa+Vl713A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA=
github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -98,8 +94,8 @@ github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9t
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/skeema/knownhosts v1.3.3 h1:tiFvsjIW+xqP5cqVsod7atn/xm57OY6v9KwxB1xWUyA=
github.com/skeema/knownhosts v1.3.3/go.mod h1:2y0ntYxeqSNxOw5pfqSVDcRyBt2FqfxhzNDPoKz/ifQ=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -114,8 +110,8 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -133,15 +129,16 @@ github.com/wI2L/jsondiff v0.6.1/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -149,8 +146,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -167,21 +164,21 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+9 -1
View File
@@ -15,13 +15,21 @@ limitations under the License.
*/
package main
import "github.com/lukaszraczylo/semver-generator/cmd"
import (
"time"
telemetry "github.com/lukaszraczylo/oss-telemetry"
"github.com/lukaszraczylo/semver-generator/cmd"
)
var (
PKG_VERSION string
)
func main() {
telemetry.SendForModule("semver-generator", "github.com/lukaszraczylo/semver-generator", PKG_VERSION)
defer telemetry.Wait(2 * time.Second)
cmd.PKG_VERSION = PKG_VERSION
cmd.Execute()
}