mirror of
https://github.com/lukaszraczylo/semver-generator.git
synced 2026-10-12 00:45:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81efa89eb0 | ||
|
|
22b95b2a2a | ||
|
|
90796980d0 | ||
|
|
fbe2ffabc1 | ||
|
|
59d62e0f2b | ||
|
|
700f7c8f59 | ||
|
|
d17aeb8e94 | ||
|
|
8ed7845969 | ||
|
|
40c917e3b8 | ||
|
|
addb360521 | ||
|
|
a0c7f5bf85 | ||
|
|
0361e91e56 | ||
|
|
912fc33e70 | ||
|
|
40c6321d31 | ||
|
|
800dca301c | ||
|
|
9ae6f6fa09 | ||
|
|
2866d2a6cb | ||
|
|
6e5afc22d4 | ||
|
|
c0201f2f6b | ||
|
|
353c5b22d9 | ||
|
|
4dc18e6d7c | ||
|
|
f7ae4c042f | ||
|
|
e90191de4c | ||
|
|
9617b281a9 | ||
|
|
6ba4a2674f | ||
|
|
e9507dd3ff | ||
|
|
b5eb8ae15a | ||
|
|
0da4aa71e1 | ||
|
|
ca874d21e5 | ||
|
|
4c21c3f24f | ||
|
|
9d5b4b0b50 | ||
|
|
8459e0ad1d | ||
|
|
c70ee7f300 | ||
|
|
4c1896ae15 | ||
|
|
7b6c46dcb8 | ||
|
|
865dcfc461 | ||
|
|
a59be0efea | ||
|
|
31808f1e6d | ||
|
|
1b80eab874 | ||
|
|
8040b42016 | ||
|
|
1857cd9cf6 | ||
|
|
888415366a | ||
|
|
dfbc4d4012 | ||
|
|
dfeb03b8bb | ||
|
|
ab52de167e | ||
|
|
564f39ed10 | ||
|
|
99b8bf937b | ||
|
|
b555e410cc | ||
|
|
521e5c1ff1 | ||
|
|
a223f15e1c | ||
|
|
d1b8192b78 | ||
|
|
a3aed9ef93 | ||
|
|
5f205e9856 | ||
|
|
a77bc0c7ae | ||
|
|
99338e8527 | ||
|
|
911513c106 | ||
|
|
c2a7a4e156 | ||
|
|
21b87300cc | ||
|
|
3e0a7239c4 |
@@ -1,11 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Autoupdate go.mod and go.sum
|
||||
name: Update dependencies
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -6,15 +6,15 @@ on:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
autoupdate:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||
update:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/renovate-autoupdate.yaml@main
|
||||
with:
|
||||
go-version: "1.24"
|
||||
release-workflow: "release.yaml"
|
||||
secrets:
|
||||
pat-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
go-version: ">=1.27"
|
||||
node: false
|
||||
release-workflow: release.yaml
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.21"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -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,
|
||||
|
||||
+3
-3
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+44
-4
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+199
-1
@@ -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) {
|
||||
@@ -198,4 +201,199 @@ wording:
|
||||
// Test reading a non-existent config
|
||||
_, 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")
|
||||
})
|
||||
}
|
||||
|
||||
+31
-8
@@ -135,18 +135,25 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
|
||||
|
||||
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,
|
||||
})
|
||||
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
|
||||
}
|
||||
@@ -156,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
|
||||
@@ -172,14 +182,27 @@ func ListExistingTags(repo *GitRepository) {
|
||||
}
|
||||
|
||||
if err := refs.ForEach(func(ref *plumbing.Reference) error {
|
||||
tagName := ref.Name().Short()
|
||||
|
||||
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 {
|
||||
commitHash = tagObj.Target.String()
|
||||
}
|
||||
|
||||
repo.Tags = append(repo.Tags, TagDetails{
|
||||
Name: ref.Name().Short(),
|
||||
Hash: ref.Hash().String(),
|
||||
Name: tagName,
|
||||
Hash: commitHash,
|
||||
})
|
||||
|
||||
Debug("Found tag", map[string]interface{}{
|
||||
"tag": ref.Name().Short(),
|
||||
"hash": ref.Hash().String(),
|
||||
"tag": tagName,
|
||||
"hash": commitHash,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
+14
-14
@@ -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)
|
||||
|
||||
ListExistingTags(repo, nil)
|
||||
|
||||
// Verify no tags were added
|
||||
assert.Empty(t, repo.Tags, "Should have no tags after calling with nil Handler")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -389,49 +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
|
||||
if _, err := fmt.Sscanf(p, "%d", &num); err != nil {
|
||||
// If parsing fails, use 0 for this part
|
||||
num = 0
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -56,4 +56,4 @@ func Critical(message string, pairs map[string]interface{}) {
|
||||
Pairs: pairs,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"}))
|
||||
}
|
||||
+38
-17
@@ -9,29 +9,51 @@ func CalculateSemver(
|
||||
commits []CommitDetails,
|
||||
tags []TagDetails,
|
||||
wording Wording,
|
||||
matcher KeywordMatcher,
|
||||
blacklist []string,
|
||||
initialSemver SemVer,
|
||||
respectExisting bool,
|
||||
strictMode bool,
|
||||
tagPrefixes []string,
|
||||
) SemVer {
|
||||
semver := initialSemver
|
||||
// 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{}
|
||||
}
|
||||
|
||||
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, tagPrefixes)
|
||||
continue
|
||||
semver := initialSemver
|
||||
startIndex := 0
|
||||
|
||||
// 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++
|
||||
@@ -42,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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-13
@@ -1,22 +1,10 @@
|
||||
version: 1
|
||||
force:
|
||||
major: 1
|
||||
minor: 4
|
||||
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
|
||||
- initial
|
||||
- fix
|
||||
minor:
|
||||
- change
|
||||
- improve
|
||||
major:
|
||||
- breaking
|
||||
- breaking
|
||||
|
||||
@@ -1,62 +1,56 @@
|
||||
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.3
|
||||
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/Microsoft/go-winio v0.6.3 // 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/rs/zerolog v1.33.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
||||
github.com/rs/zerolog v1.35.1 // 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
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/gjson v1.20.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.2 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wI2L/jsondiff v0.6.1 // indirect
|
||||
github.com/wI2L/jsondiff v0.7.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.60.0 // indirect
|
||||
golang.org/x/sys v0.49.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
|
||||
)
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
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/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
||||
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
|
||||
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
|
||||
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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -52,10 +48,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,41 +59,37 @@ 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-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-colorable v0.1.16 h1:+2YjeAe7xP1OQbZW1jERPWWCDLlNqFNCvjiqGiAt84A=
|
||||
github.com/mattn/go-colorable v0.1.16/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
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=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
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,34 +104,36 @@ 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=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto=
|
||||
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.2 h1:dz1jrRuE7or/74V490B4/GP1pZm5WKlt2bgCP5A83w8=
|
||||
github.com/tidwall/pretty v1.2.2/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wI2L/jsondiff v0.6.1 h1:ISZb9oNWbP64LHnu4AUhsMF5W0FIj5Ok3Krip9Shqpw=
|
||||
github.com/wI2L/jsondiff v0.6.1/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
|
||||
github.com/wI2L/jsondiff v0.7.1 h1:Fg9+yj+1/x3UtPBJhR91TKEzRkrEEWcAcLbg9dzEaNM=
|
||||
github.com/wI2L/jsondiff v0.7.1/go.mod h1:yAt2W7U6Jd4HK0RA8DGSGk0zDtfEtOUUJVnH/xICpjo=
|
||||
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 +141,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.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.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=
|
||||
@@ -163,25 +155,22 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.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=
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user