Compare commits

...

2 Commits

Author SHA1 Message Date
lukaszraczylo 98f6c31e9d fixup! Update documentation. 2025-02-28 18:10:56 +00:00
lukaszraczylo f87a53ec65 Update documentation. 2025-02-28 18:02:27 +00:00
17 changed files with 359 additions and 278 deletions
+210 -129
View File
@@ -1,69 +1,137 @@
## Semantic version generator # Semantic Version Generator
[![Docker image build.](https://github.com/lukaszraczylo/semver-generator/actions/workflows/release.yaml/badge.svg)](https://github.com/lukaszraczylo/semver-generator/actions/workflows/release.yaml) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/lukaszraczylo/semver-generator) [![codecov](https://codecov.io/gh/lukaszraczylo/semver-generator/branch/main/graph/badge.svg?token=FY9BKETB59)](https://codecov.io/gh/lukaszraczylo/semver-generator) [![Docker image build.](https://github.com/lukaszraczylo/semver-generator/actions/workflows/release.yaml/badge.svg)](https://github.com/lukaszraczylo/semver-generator/actions/workflows/release.yaml) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/lukaszraczylo/semver-generator) [![codecov](https://codecov.io/gh/lukaszraczylo/semver-generator/branch/main/graph/badge.svg?token=FY9BKETB59)](https://codecov.io/gh/lukaszraczylo/semver-generator)
Project created overnight, to prove that management of semantic versioning is NOT painful and do not require arguments and debates within the team. Simple, clean and only thing the project team should need to agree to are the keywords. A lightweight, configurable tool that simplifies semantic versioning by automatically calculating version numbers based on git commit messages. No more manual version management or team debates about versioning - just agree on the keywords and let the tool handle the rest.
- [Semantic version generator](#semantic-version-generator) ## Table of Contents
- [How does it work](#how-does-it-work)
- [Additional features](#additional-features)
- [Important changes](#important-changes)
- [Usage](#usage)
- [Authentication](#authentication)
- [As a binary](#as-a-binary)
- [As a github action](#as-a-github-action)
- [As a docker container](#as-a-docker-container)
- [Calculations example \[standard\]](#calculations-example-standard)
- [Calculations example \[strict matching\]](#calculations-example-strict-matching)
- [Release candidates](#release-candidates)
- [Example configuration](#example-configuration)
- [Good to know](#good-to-know)
### How does it work - [How It Works](#how-it-works)
- [Key Features](#key-features)
- [Important Changes](#important-changes)
- [Installation](#installation)
- [Prerequisites](#prerequisites)
- [Binary](#binary)
- [Docker](#docker)
- [GitHub Action](#github-action)
- [Usage](#usage)
- [Authentication](#authentication)
- [Command Line Options](#command-line-options)
- [Configuration File](#configuration-file)
- [Versioning Behavior](#versioning-behavior)
- [Release Candidates](#release-candidates)
- [Examples](#examples)
- [Standard Mode](#standard-mode)
- [Strict Matching Mode](#strict-matching-mode)
- [Advanced Features](#advanced-features)
- [Force Settings](#force-settings)
- [Blacklist Terms](#blacklist-terms)
- [Tips & Best Practices](#tips--best-practices)
* Binary clones the github repository ## How It Works
* Iterates through the list of commits looking for the keywords specified in config file for additional bumps of versions
* Returns the semantic version which can be included in the release
### Additional features The semantic version generator follows a simple process:
* With flag `-e` or config `force.existing: true` the existing tags in versioning will be respected, helping you to avoid the version conflicts. 1. Clones the specified GitHub repository (or uses a local repository)
* With config `force.commit: deadbeef` where `deadbeef` is the commit hash - calculations will start from the specified commit. 2. Iterates through the commit history, analyzing each commit message
3. Looks for predefined keywords (configurable) that trigger version increments
4. Calculates the appropriate semantic version based on the matches
5. Returns the resulting version that can be used for releases
### Important changes ## Key Features
* From version `1.4.2+` as pointed out in [issue #12](https://github.com/lukaszraczylo/semver-generator/issues/12) commits from merge will not be included in the calculations and commits themselves will bump the version on first match ( starting checks from `patch` upwards ). - **Effortless Version Calculation**: Automatically determine the appropriate version based on commit messages
* Added support for blacklisting terms to ignore specific commits, branch names, and merge messages from version calculations. - **Configurable Keywords**: Define your own keywords for patch, minor, and major version increments
- **Support for Existing Tags**: Option to respect existing version tags to avoid conflicts
- **Release Candidate Support**: Generate release candidate versions with incrementing counter (e.g., `1.2.3-rc.1`)
- **Flexible Repository Source**: Work with either local or remote Git repositories
- **Blacklist Support**: Ignore specific commits or branch merges from version calculations
- **Force Options**: Start calculations from a specific commit or set a minimum version
### Usage ## Important Changes
#### Authentication - **Since v1.4.2+**: Commits from merge requests are no longer included in calculations
- **Commit Matching Behavior**: Commits will bump the version on the first match (checking from `patch` upwards)
- **Blacklist Support**: Added ability to ignore specific terms in commit messages, branch names, and merge requests
If you intend to use this project with remote repositories ( regardless of them being private or public ) you need to authenticate with your repository. ## Installation
To do so you can utilise the following environment variables ( they are NOT github specific, for other providers you can use the password )
### Prerequisites
When using with remote repositories, authentication is required. Set the following environment variables:
```bash ```bash
export GITHUB_USERNAME=lukaszraczylo export GITHUB_USERNAME=yourusername
export GITHUB_TOKEN=yourPersonalApiToken export GITHUB_TOKEN=yourpersonalapitoken
``` ```
#### As a binary ### Binary
You can download latest versions of the binaries from the [release page](https://github.com/lukaszraczylo/semver-generator/releases/latest). Download the latest binary from the [release page](https://github.com/lukaszraczylo/semver-generator/releases/latest).
**Supported OS and architectures:** **Supported platforms**:
Darwin ARM64/AMD64, Linux ARM64/AMD64, Windows AMD64 - Darwin (macOS): ARM64/AMD64
- Linux: ARM64/AMD64
- Windows: AMD64
### Docker
```bash ```bash
bash$ ./semver-gen generate -r https://github.com/nextapps-de/winbox docker pull ghcr.io/lukaszraczylo/semver-generator:latest
SEMVER 9.0.10
bash$ ./semver-gen generate -l
SEMVER 5.1.1
``` ```
**Local repository flag `-l` will always take precedence over remote repository URL** **Supported architectures**:
- Linux/arm64
- Linux/amd64
### GitHub Action
Add to your GitHub workflow:
```yaml ```yaml
jobs:
prepare:
name: Preparing build context
runs-on: ubuntu-latest
outputs:
RELEASE_VERSION: ${{ steps.semver.outputs.semantic_version }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
fetch-depth: '0'
- name: Semver run
id: semver
uses: lukaszraczylo/semver-generator@v1
with:
config_file: semver.yaml
# Either use local repository
repository_local: true
# Or specify remote repository
# repository_url: https://github.com/lukaszraczylo/simple-gql-client
# github_username: ${{ secrets.GH_USERNAME }}
# github_token: ${{ secrets.GH_TOKEN }}
strict: false
existing: true
- name: Use semantic version
run: |
echo "Semantic version detected: ${{ steps.semver.outputs.semantic_version }}"
```
## Usage
### Authentication
For remote repositories (public or private), authenticate using:
```bash
export GITHUB_USERNAME=yourusername
export GITHUB_TOKEN=yourpersonalapitoken
```
### Command Line Options
```
Usage: Usage:
semver-gen generate [flags] semver-gen generate [flags]
semver-gen [command] semver-gen [command]
@@ -85,86 +153,11 @@ Flags:
-v, --version Display version -v, --version Display version
``` ```
#### As a github action **Note**: The `-l/--local` flag takes precedence over the repository URL.
```yaml ### Configuration File
jobs:
prepare:
name: Preparing build context
runs-on: ubuntu-latest
outputs:
RELEASE_VERSION: ${{ steps.semver.outputs.semantic_version }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
fetch-depth: '0'
- name: Semver run
id: semver
uses: lukaszraczylo/semver-generator@PLACE_LATEST_TAG_HERE
# you can also use v1 tag which _should_ automatically upgrade to latest
# uses: lukaszraczylo/semver-generator@v1
with:
config_file: semver.yaml
# either...
repository_local: true
# or...
repository_url: https://github.com/lukaszraczylo/simple-gql-client
# when using remote repository, especially with private one:
github_username: lukaszraczylo
github_token: MySupeRSecr3tPa$$w0rd
strict: true
existing: false
- name: Semver check
run: |
echo "Semantic version detected: ${{ steps.semver.outputs.semantic_version }}"
```
#### As a docker container Create a `semver.yaml` file (or specify a different path with `-c`):
```bash
docker pull ghcr.io/lukaszraczylo/semver-generator:latest
```
**Docker supported architectures:**
Linux/arm64, Linux/amd64
#### Calculations example [standard]
```bash
- 0.0.1 - PATCH - starting commit
- 0.0.2 - PATCH - another commit
- 0.0.4 - PATCH - another commit with word 'Update' => DOUBLE increment PATCH
- 0.1.0 - MINOR - after commit with word 'Change' => increment MINOR, reset PATCH
- 0.1.1 - PATCH - additional commit
- 1.0.1 - MAJOR - commit with word 'BREAKING' = > INCREMENT MAJOR, reset MINOR
- 1.0.2 - PATCH - another commit
```
#### Calculations example [strict matching]
```bash
- 0.0.1 - PATCH - starting commit
- 0.0.1 - PATCH - another commit
- 0.0.1 - PATCH - another commit with word 'Update' => SINGLE increment PATCH
- 0.1.0 - MINOR - after commit with word 'Change' => increment MINOR, reset PATCH
- 0.1.0 - PATCH - additional commit
- 1.0.0 - MAJOR - commit with word 'BREAKING' = > INCREMENT MAJOR, reset MINOR
- 1.0.0 - PATCH - another commit
```
#### Release candidates
The `semver-gen` supports release candidates generation as well. Add following configuration ( and change the trigger keywords to anything what suits you )
to generate the appropriate release in format `1.3.37-rc.1` and counting up until next `minor` trigger will be detected.
```yaml
release:
- release-candidate
- add-rc
```
#### Example configuration
```yaml ```yaml
version: 1 version: 1
@@ -173,6 +166,8 @@ force:
minor: 0 minor: 0
patch: 1 patch: 1
commit: 69fbe2df696f40281b9104ff073d26186cde1024 commit: 69fbe2df696f40281b9104ff073d26186cde1024
existing: true
strict: false
blacklist: blacklist:
- "Merge branch" - "Merge branch"
- "Merge pull request" - "Merge pull request"
@@ -182,25 +177,111 @@ wording:
patch: patch:
- update - update
- initial - initial
- fix
minor: minor:
- change - change
- improve - improve
- add
major: major:
- breaking - breaking
- the # For testing purposes - redesign
release: release:
- release-candidate - release-candidate
- add-rc - add-rc
``` ```
* `version`: is not respected at the moment, introduced for potential backwards compatibility in future Configuration options:
* `force`: sets the "starting" version, you don't need to specify this section as the default is always `0` - `version`: Reserved for future backward compatibility
* `force.commit`: allows you to set commit hash from which the calculations should start - `force`: Set starting version or other constraints
* `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. - `blacklist`: Terms to ignore when processing commits
* `wording`: words the program should look for in the git commits to increment (patch|minor|major) - `wording`: Keywords that trigger version increments
### Good to know ### Versioning Behavior
* Word matching uses fuzzy search AND is case INSENSITIVE The version calculation follows semantic versioning rules:
* I do not recommend using common words ( like "the" from the example configuration ) - **MAJOR**: Incremented for incompatible API changes
* You can specify env variable `LOG_LEVEL=debug` to see what exactly happens during the calculations - **MINOR**: Incremented for backward-compatible new features
- **PATCH**: Incremented for backward-compatible bug fixes
When keywords are matched:
- **MAJOR** match: Increments major version, resets minor and patch versions
- **MINOR** match: Increments minor version, resets patch version
- **PATCH** match: Increments patch version
- By default (non-strict mode), every commit increments patch version
### Release Candidates
Add the following to your configuration to generate release candidates:
```yaml
wording:
release:
- release-candidate
- add-rc
```
When a release candidate keyword is found, the version will be formatted as `1.2.3-rc.1`, with the counter incrementing for each new release candidate.
## Examples
### Standard Mode
```
- 0.0.1 - PATCH - starting commit
- 0.0.2 - PATCH - another commit
- 0.0.4 - PATCH - another commit with word 'Update' => DOUBLE increment PATCH
- 0.1.0 - MINOR - after commit with word 'Change' => increment MINOR, reset PATCH
- 0.1.1 - PATCH - additional commit
- 1.0.1 - MAJOR - commit with word 'BREAKING' => INCREMENT MAJOR, reset MINOR
- 1.0.2 - PATCH - another commit
```
### Strict Matching Mode
In strict mode (using `-s` flag or `force.strict: true`), versions only increment when a keyword is matched:
```
- 0.0.1 - PATCH - starting commit
- 0.0.1 - PATCH - another commit (no change - no keyword match)
- 0.0.2 - PATCH - another commit with word 'Update' => increment PATCH
- 0.1.0 - MINOR - after commit with word 'Change' => increment MINOR, reset PATCH
- 0.1.0 - PATCH - additional commit (no change - no keyword match)
- 1.0.0 - MAJOR - commit with word 'BREAKING' => INCREMENT MAJOR, reset MINOR
- 1.0.0 - PATCH - another commit (no change - no keyword match)
```
## Advanced Features
### Force Settings
Control versioning with force settings in configuration:
```yaml
force:
major: 1 # Set minimum major version
minor: 0 # Set minimum minor version
patch: 1 # Set minimum patch version
commit: 69fbe2df696f40281b9104ff073d26186cde1024 # Start from specific commit
existing: true # Respect existing tags (same as -e flag)
strict: false # Use strict matching (same as -s flag)
```
### Blacklist Terms
Ignore specific commits from version calculations:
```yaml
blacklist:
- "Merge branch" # Ignore merge commits
- "Merge pull request" # Ignore PR merges
- "feature/" # Ignore feature branch names
- "chore:" # Ignore chore commits
```
## Tips & Best Practices
- Word matching uses fuzzy search and is case INSENSITIVE
- Avoid common words as version triggers (e.g., "the", "and")
- Use `LOG_LEVEL=debug` environment variable to see detailed calculation steps
- When using as a GitHub Action, ensure `fetch-depth: '0'` to get the complete commit history
- For complex projects, consider using a more specific configuration to distinguish between types of changes
+9 -10
View File
@@ -26,21 +26,20 @@ import (
) )
var ( var (
err error
repo *Setup repo *Setup
PKG_VERSION string PKG_VERSION string
) )
// Setup represents the application setup // Setup represents the application setup
type Setup struct { type Setup struct {
RepositoryName string RepositoryName string
RepositoryBranch string RepositoryBranch string
LocalConfigFile string LocalConfigFile string
Generate bool Generate bool
UseLocal bool UseLocal bool
GitRepo utils.GitRepository GitRepo utils.GitRepository
Config *utils.Config Config *utils.Config
Semver utils.SemVer Semver utils.SemVer
} }
// Initialize the fuzzy search function in the utils package // Initialize the fuzzy search function in the utils package
@@ -74,7 +73,7 @@ func main() {
} }
utils.Info("semver-gen", map[string]interface{}{ utils.Info("semver-gen", map[string]interface{}{
"version": PKG_VERSION, "version": PKG_VERSION,
"outdated": outdatedMsg, "outdated": outdatedMsg,
}) })
+4 -4
View File
@@ -382,10 +382,10 @@ func (suite *Tests) Test_parseExistingSemver() {
func (suite *Tests) TestSetup_ListCommits() { func (suite *Tests) TestSetup_ListCommits() {
type fields struct { type fields struct {
RepositoryName string RepositoryName string
RepositoryBranch string RepositoryBranch string
LocalConfigFile string LocalConfigFile string
GitRepo utils.GitRepository GitRepo utils.GitRepository
} }
tests := []struct { tests := []struct {
+13 -13
View File
@@ -29,14 +29,14 @@ type TagDetails struct {
// GitRepository represents a git repository // GitRepository represents a git repository
type GitRepository struct { type GitRepository struct {
Handler *git.Repository Handler *git.Repository
Name string Name string
Branch string Branch string
LocalPath string LocalPath string
UseLocal bool UseLocal bool
Commits []CommitDetails Commits []CommitDetails
Tags []TagDetails Tags []TagDetails
StartCommit string StartCommit string
} }
// PrepareRepository prepares the git repository for use // PrepareRepository prepares the git repository for use
@@ -48,7 +48,7 @@ func PrepareRepository(repo *GitRepository) error {
if err != nil { if err != nil {
Error("Unable to parse repository URL", map[string]interface{}{ Error("Unable to parse repository URL", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"url": repo.Name, "url": repo.Name,
}) })
return err return err
} }
@@ -70,7 +70,7 @@ func PrepareRepository(repo *GitRepository) error {
if err != nil { if err != nil {
Error("Unable to clone repository", map[string]interface{}{ Error("Unable to clone repository", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"url": repo.Name, "url": repo.Name,
}) })
return err return err
} }
@@ -80,7 +80,7 @@ func PrepareRepository(repo *GitRepository) error {
if err != nil { if err != nil {
Error("Unable to open local repository", map[string]interface{}{ Error("Unable to open local repository", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"path": repo.LocalPath, "path": repo.LocalPath,
}) })
return err return err
} }
@@ -133,7 +133,7 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
if cmt.Hash == repo.StartCommit { if cmt.Hash == repo.StartCommit {
Debug("Found commit match", map[string]interface{}{ Debug("Found commit match", map[string]interface{}{
"commit": cmt.Hash, "commit": cmt.Hash,
"index": commitId, "index": commitId,
}) })
repo.Commits = tmpResults[commitId:] repo.Commits = tmpResults[commitId:]
break break
@@ -170,7 +170,7 @@ func ListExistingTags(repo *GitRepository) {
}) })
Debug("Found tag", map[string]interface{}{ Debug("Found tag", map[string]interface{}{
"tag": ref.Name().Short(), "tag": ref.Name().Short(),
"hash": ref.Hash().String(), "hash": ref.Hash().String(),
}) })
+1 -1
View File
@@ -71,7 +71,7 @@ func UpdatePackage() bool {
err = g.Download(output, downloadedBinaryPath) err = g.Download(output, downloadedBinaryPath)
if err != nil { if err != nil {
Error("Unable to download binary", map[string]interface{}{ Error("Unable to download binary", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"binaryPath": downloadedBinaryPath, "binaryPath": downloadedBinaryPath,
}) })
return false return false
+1
View File
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestInitLogger(t *testing.T) { func TestInitLogger(t *testing.T) {
// Test with debug mode enabled // Test with debug mode enabled
logger := InitLogger(true) logger := InitLogger(true)
+1 -1
View File
@@ -22,7 +22,7 @@ func CalculateSemver(
for _, tagHash := range tags { for _, tagHash := range tags {
if commit.Hash == tagHash.Hash { if commit.Hash == tagHash.Hash {
Debug("Found existing tag", map[string]interface{}{ Debug("Found existing tag", map[string]interface{}{
"tag": tagHash.Name, "tag": tagHash.Name,
"commit": strings.TrimSuffix(commit.Message, "\n"), "commit": strings.TrimSuffix(commit.Message, "\n"),
}) })
semver = ParseExistingSemver(tagHash.Name, semver) semver = ParseExistingSemver(tagHash.Name, semver)
+8 -8
View File
@@ -77,10 +77,10 @@ func TestCalculateSemver(t *testing.T) {
respectExisting: true, respectExisting: true,
strictMode: false, strictMode: false,
want: SemVer{ want: SemVer{
Major: 2, Major: 2,
Minor: 0, Minor: 0,
Patch: 1, // Initial tag 2.0.0 + one patch increment Patch: 1, // Initial tag 2.0.0 + one patch increment
Release: 1, Release: 1,
EnableReleaseCandidate: true, EnableReleaseCandidate: true,
}, },
}, },
@@ -110,10 +110,10 @@ func TestCalculateSemver(t *testing.T) {
respectExisting: true, respectExisting: true,
strictMode: true, strictMode: true,
want: SemVer{ want: SemVer{
Major: 2, Major: 2,
Minor: 0, Minor: 0,
Patch: 1, // Initial tag 2.0.0 + patch from "update" keyword Patch: 1, // Initial tag 2.0.0 + patch from "update" keyword
Release: 1, Release: 1,
EnableReleaseCandidate: true, EnableReleaseCandidate: true,
}, },
}, },
+3 -3
View File
@@ -104,8 +104,8 @@ func CheckMatches(content []string, targets []string, blacklist []string) bool {
if len(matches) > 0 { if len(matches) > 0 {
hasMatch = true hasMatch = true
Debug("Found match", map[string]interface{}{ Debug("Found match", map[string]interface{}{
"target": tgt, "target": tgt,
"match": strings.Join(matches, ","), "match": strings.Join(matches, ","),
"content": contentStr, "content": contentStr,
}) })
break break
@@ -117,7 +117,7 @@ func CheckMatches(content []string, targets []string, blacklist []string) bool {
for _, blacklistTerm := range blacklist { for _, blacklistTerm := range blacklist {
if strings.Contains(strings.ToLower(contentStr), strings.ToLower(blacklistTerm)) { if strings.Contains(strings.ToLower(contentStr), strings.ToLower(blacklistTerm)) {
Debug("Blacklisted term detected, ignoring commit", map[string]interface{}{ Debug("Blacklisted term detected, ignoring commit", map[string]interface{}{
"content": contentStr, "content": contentStr,
"blacklist_term": blacklistTerm, "blacklist_term": blacklistTerm,
}) })
return false return false
+9 -9
View File
@@ -58,14 +58,14 @@ func TestParseExistingSemver(t *testing.T) {
InitLogger(false) InitLogger(false)
tests := []struct { tests := []struct {
name string name string
tagName string tagName string
currentSemver SemVer currentSemver SemVer
want SemVer want SemVer
}{ }{
{ {
name: "Standard semver", name: "Standard semver",
tagName: "1.2.3", tagName: "1.2.3",
currentSemver: SemVer{}, currentSemver: SemVer{},
want: SemVer{ want: SemVer{
Major: 1, Major: 1,
@@ -74,8 +74,8 @@ func TestParseExistingSemver(t *testing.T) {
}, },
}, },
{ {
name: "With v prefix", name: "With v prefix",
tagName: "v2.3.4", tagName: "v2.3.4",
currentSemver: SemVer{}, currentSemver: SemVer{},
want: SemVer{ want: SemVer{
Major: 2, Major: 2,
@@ -84,8 +84,8 @@ func TestParseExistingSemver(t *testing.T) {
}, },
}, },
{ {
name: "With release candidate", name: "With release candidate",
tagName: "3.4.5-rc.2", tagName: "3.4.5-rc.2",
currentSemver: SemVer{}, currentSemver: SemVer{},
want: SemVer{ want: SemVer{
Major: 3, Major: 3,