Compare commits

...

14 Commits

14 changed files with 1338 additions and 500 deletions
+11 -65
View File
@@ -5,70 +5,16 @@ on:
schedule:
- cron: "0 3 * * *"
env:
GO_VERSION: ">=1.21"
permissions:
contents: write
actions: write
pull-requests: write
jobs:
# This job is responsible for preparation of the build
# environment variables.
prepare:
name: Preparing build context
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
id: cache
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Go get dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
go get ./...
# This job is responsible for running tests and linting the codebase
test:
name: "Unit testing"
runs-on: ubuntu-latest
container: golang:1
needs: [prepare]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Ensure full history is checked out
token: ${{ secrets.GHCR_TOKEN }}
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Install dependencies
run: |
apt-get update
apt-get install ca-certificates make -y
update-ca-certificates
go mod tidy
go get -u -v ./...
go mod tidy -v
- name: Run unit tests
run: |
export GITHUB_TOKEN=${{ secrets.GHCR_TOKEN }}
CI_RUN=${CI} make test
git config --global --add safe.directory /__w/semver-generator/semver-generator
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Update go.mod and go.sum"
commit_options: "--no-verify --signoff"
file_pattern: "go.mod go.sum"
autoupdate:
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
with:
go-version: "1.24"
release-workflow: "release.yaml"
secrets:
pat-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
+16 -213
View File
@@ -1,222 +1,25 @@
name: Test, scan, build, release
name: Test, build, release
on:
workflow_dispatch:
push:
paths-ignore:
- '**.md'
- '**/release.yaml'
- 'action.yml'
- '**.md'
- '**/release.yaml'
- 'action.yml'
branches:
- "master"
- "main"
- main
env:
ENABLE_CODE_LINT: false
ENABLE_CODE_SCANS: false
DEPLOY: false
GO_VERSION: 1.21
permissions:
contents: write
packages: write
jobs:
prepare:
name: Preparing build context
runs-on: ubuntu-latest
outputs:
SANITISED_REPOSITORY_NAME: ${{ steps.get_env.outputs.SANITISED_REPOSITORY_NAME }}
DOCKER_IMAGE: ${{ steps.get_env.outputs.DOCKER_IMAGE }}
GITHUB_COMMIT_NUMBER: ${{ steps.get_env.outputs.GITHUB_COMMIT_NUMBER }}
GITHUB_SHA: ${{ steps.get_env.outputs.GITHUB_SHA }}
GITHUB_RUN_ID: ${{ steps.get_env.outputs.GITHUB_RUN_ID }}
RELEASE_VERSION: ${{ steps.get_env.outputs.RELEASE_VERSION }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: '0'
- name: Setting environment variables
id: get_env
run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/lukaszraczylo/semver-generator/releases/latest | grep -E ".*browser_download_url.*linux-" | grep -vE "(arm64|md5)" \
| cut -d '"' -f 4)
curl -s -L -o semver-gen "$DOWNLOAD_URL" && chmod +x semver-gen
TMP_SANITISED_REPOSITORY_NAME=$(echo ${{ github.event.repository.name }} | sed -e 's|\.|-|g')
TMP_GITHUB_COMMITS_COUNT=$(git rev-list --count HEAD)
TMP_GITHUB_COUNT_NUMBER=$(echo ${GITHUB_RUN_NUMBER})
TMP_RELEASE_VERSION=$(./semver-gen generate -l -c config-release.yaml | sed -e 's|SEMVER ||g')
echo ">> Release version: $TMP_RELEASE_VERSION <<"
# Setting outputs
echo "SANITISED_REPOSITORY_NAME=$TMP_SANITISED_REPOSITORY_NAME" > $GITHUB_OUTPUT
echo "DOCKER_IMAGE=ghcr.io/${{ github.repository_owner }}/$TMP_SANITISED_REPOSITORY_NAME" >> $GITHUB_OUTPUT
echo "GITHUB_COMMIT_NUMBER=$TMP_GITHUB_COMMITS_COUNT" >> $GITHUB_OUTPUT
echo "GITHUB_SHA=$(echo ${GITHUB_SHA::8})" >> $GITHUB_OUTPUT
echo "GITHUB_RUN_ID=$TMP_GITHUB_COUNT_NUMBER" >> $GITHUB_OUTPUT
echo "RELEASE_VERSION=$TMP_RELEASE_VERSION" >> $GITHUB_OUTPUT
test:
needs: [ prepare ]
name: Code checks pipeline
runs-on: ubuntu-latest
container: github/super-linter:v3.15.5
env:
CI: true
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Lint Code Base
if: env.ENABLE_CODE_LINT == true
env:
VALIDATE_ALL_CODEBASE: true
VALIDATE_DOCKERFILE: false # this leaves us with hadolint only
VALIDATE_GO: false # disable bulk validation of go files, run the linter manually
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GHCR_TOKEN }}
LOG_LEVEL: WARN
run: |
golangci-lint run --exclude-use-default ./...
/action/lib/linter.sh
- name: Run unit tests
env:
GITHUB_TOKEN: ${{ secrets.GHCR_TOKEN }}
run: |
make test CI_RUN=${CI}
- name: Upload codecov result
uses: codecov/codecov-action@v3
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
files: coverage.out
fail_ci_if_error: false
code_scans:
needs: [ prepare ]
name: Code scans pipeline
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Configure git for private modules
run: |
make update
- name: WriteGoList
run: go list -json -m all > go.list
- name: Running nancy
if: env.ENABLE_CODE_SCANS == true
uses: sonatype-nexus-community/nancy-github-action@main
- name: Running gosec
if: env.ENABLE_CODE_SCANS == true
uses: securego/gosec@master
with:
args: ./...
build-binary:
needs: [ prepare, test, code_scans ]
name: Binary compilation and release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Build binaries
run: |
LOCAL_VERSION=${{ needs.prepare.outputs.RELEASE_VERSION }} make dist-release
- name: Get list of the commits since last release
run: |
echo "$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"%h %s")" > .release_notes
- name: Create release [semver]
uses: ncipollo/release-action@v1
with:
bodyFile: ./.release_notes
name: version ${{ needs.prepare.outputs.RELEASE_VERSION }}
token: ${{ secrets.GHCR_TOKEN }}
tag: ${{ needs.prepare.outputs.RELEASE_VERSION }}
prerelease: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }}
artifacts: "dist/*"
allowUpdates: true
- name: Delete existing v1 tag and release
run: |
gh release delete v1 --cleanup-tag -y
env:
GH_TOKEN: ${{ secrets.GHCR_TOKEN }}
- name: Create release [v1]
uses: ncipollo/release-action@v1
with:
bodyFile: ./.release_notes
name: v1 - ${{ needs.prepare.outputs.RELEASE_VERSION }}
token: ${{ secrets.GHCR_TOKEN }}
tag: v1
prerelease: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }}
artifacts: "dist/*"
allowUpdates: true
makeLatest: false
build-docker:
needs: [ prepare, test, code_scans, build-binary ]
name: Docker image build
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.ACTOR }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Prepare for push
id: prep
run: |
if [ -z "${{ needs.prepare.outputs.RELEASE_VERSION }}" ]; then
TAGS="${{ needs.prepare.outputs.DOCKER_IMAGE }}:${{ needs.prepare.outputs.GITHUB_SHA }},${{ needs.prepare.outputs.DOCKER_IMAGE }}:latest,${{ needs.prepare.outputs.DOCKER_IMAGE }}:v1"
else
TAGS="${{ needs.prepare.outputs.DOCKER_IMAGE }}:${{ needs.prepare.outputs.GITHUB_SHA }},${{ needs.prepare.outputs.DOCKER_IMAGE }}:${{ needs.prepare.outputs.RELEASE_VERSION }},${{ needs.prepare.outputs.DOCKER_IMAGE }}:latest,${{ needs.prepare.outputs.DOCKER_IMAGE }}:v1"
fi
echo "TAGS=$TAGS" >> $GITHUB_OUTPUT
BRANCH=$(echo ${GITHUB_REF##*/} | tr '[A-Z]' '[a-z]')
LABELS="org.opencontainers.image.revision=${{ needs.prepare.outputs.GITHUB_SHA }}"
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
LABELS="$LABELS,org.opencontainers.image.version=$VERSION"
LABELS="$LABELS,com.github.repo.branch=$BRANCH"
LABELS="$LABELS,com.github.repo.dockerfile=Dockerfile"
echo "LABELS=$LABELS" >> $GITHUB_OUTPUT
BUILD_ARGS="BRANCH=$BRANCH"
echo "args=$BUILD_ARGS" >> $GITHUB_OUTPUT
- name: Build image
id: docker_build
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
platforms: linux/arm64,linux/amd64
push: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
tags: ${{ steps.prep.outputs.tags }}
build-args: |
GITHUB_AUTH_TOKEN=${{ secrets.GHCR_TOKEN }}
MICROSERVICE_NAME=${{ github.event.repository.name }}
GITHUB_COMMIT_NUMBER=${{ needs.prepare.outputs.GITHUB_COMMIT_NUMBER }}
GITHUB_SHA=${{ needs.prepare.outputs.GITHUB_SHA }}
${{ steps.prep.outputs.args }}
labels: ${{ steps.prep.outputs.labels }}
no-cache: false
release:
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
go-version: "1.24"
docker-enabled: true
rolling-release-tag: "v1"
semver-config: "config-release.yaml"
secrets: inherit
+45
View File
@@ -0,0 +1,45 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
paths:
- 'docs/**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: 'docs/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+91
View File
@@ -0,0 +1,91 @@
version: 2
before:
hooks:
- go mod tidy
builds:
- id: semver-gen
main: .
binary: semver-generator
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X main.PKG_VERSION={{.Version}}
archives:
- id: semver-gen
formats: [tar.gz]
name_template: "semver-generator-{{ .Os }}-{{ .Arch }}"
format_overrides:
- goos: windows
formats: [zip]
files:
- LICENSE
- README.md
- config.yaml
checksum:
name_template: "semver-generator-checksums.txt"
algorithm: sha256
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^Merge'
- '^WIP'
- '^Update go.mod'
release:
github:
owner: lukaszraczylo
name: semver-generator
name_template: "version {{.Version}}"
draft: false
prerelease: auto
dockers_v2:
- images:
- "ghcr.io/lukaszraczylo/semver-generator"
tags:
- "{{ .Version }}"
- "latest"
- "v1"
platforms:
- linux/amd64
- linux/arm64
dockerfile: Dockerfile.goreleaser
extra_files:
- config-release.yaml
- entrypoint.sh
homebrew_casks:
- name: semver-generator
repository:
owner: lukaszraczylo
name: homebrew-taps
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
directory: Casks
homepage: https://github.com/lukaszraczylo/semver-generator
description: "Automatic semantic version generator based on git commit messages"
license: MIT
url:
verified: github.com/lukaszraczylo/semver-generator
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{staged_path}/semver-generator"]
end
+7
View File
@@ -0,0 +1,7 @@
FROM ubuntu:jammy
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/semver-generator /go/src/app/semver-generator
COPY config-release.yaml /go/src/app/config.yaml
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+25 -7
View File
@@ -49,15 +49,23 @@ export GITHUB_TOKEN=yourPersonalApiToken
#### As a binary
##### Homebrew (macOS)
```bash
brew install --cask lukaszraczylo/taps/semver-generator
```
##### Manual Download
You can download latest versions of the binaries from the [release page](https://github.com/lukaszraczylo/semver-generator/releases/latest).
**Supported OS and architectures:**
Darwin ARM64/AMD64, Linux ARM64/AMD64, Windows AMD64
```bash
bash$ ./semver-gen generate -r https://github.com/nextapps-de/winbox
bash$ semver-generator generate -r https://github.com/nextapps-de/winbox
SEMVER 9.0.10
bash$ ./semver-gen generate -l
bash$ semver-generator generate -l
SEMVER 5.1.1
```
@@ -65,8 +73,8 @@ SEMVER 5.1.1
```yaml
Usage:
semver-gen generate [flags]
semver-gen [command]
semver-generator generate [flags]
semver-generator [command]
Available Commands:
generate Generates semantic version
@@ -76,15 +84,25 @@ Flags:
-c, --config string Path to config file (default "semver.yaml")
-d, --debug Enable debug mode
-e, --existing Respect existing tags
-h, --help help for semver-gen
-h, --help help for semver-generator
-l, --local Use local repository
-r, --repository string Remote repository URL. (default "https://github.com/lukaszraczylo/simple-gql-client")
-b, --branch string Remote repository URL Branch. (default "main")
-s, --strict Strict matching
-u, --update Update binary with latest
-u, --update Update binary with latest (no authentication required)
-v, --version Display version
```
##### Self-Update
The binary can update itself to the latest version:
```bash
semver-generator -u
```
This downloads the latest release for your platform directly from GitHub releases. No authentication is required.
#### As a github action
```yaml
@@ -199,7 +217,7 @@ wording:
* `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.
* `wording`: words the program should look for in the git commits to increment (patch|minor|major)
### Good to know
### Good to knows
* Word matching uses fuzzy search AND is case INSENSITIVE
* I do not recommend using common words ( like "the" from the example configuration )
+12 -13
View File
@@ -26,27 +26,26 @@ import (
)
var (
err error
repo *Setup
PKG_VERSION string
)
// Setup represents the application setup
type Setup struct {
RepositoryName string
RepositoryBranch string
LocalConfigFile string
Generate bool
UseLocal bool
GitRepo utils.GitRepository
Config *utils.Config
Semver utils.SemVer
RepositoryName string
RepositoryBranch string
LocalConfigFile string
Generate bool
UseLocal bool
GitRepo utils.GitRepository
Config *utils.Config
Semver utils.SemVer
}
// Initialize the fuzzy search function in the utils package
func init() {
utils.InitLogger(false) // Will be updated in main based on debug flag
// Set the fuzzy search function
utils.FuzzyFind = fuzzy.FindNormalizedFold
}
@@ -72,12 +71,12 @@ func main() {
if PKG_VERSION != latestRelease && latestReleaseOk {
outdatedMsg = fmt.Sprintf("(Latest available: %s)", latestRelease)
}
utils.Info("semver-gen", map[string]interface{}{
"version": PKG_VERSION,
"version": PKG_VERSION,
"outdated": outdatedMsg,
})
if outdatedMsg != "" {
utils.Info("semver-gen", map[string]interface{}{
"message": "You can update automatically with: semver-gen -u",
+395 -120
View File
@@ -1,148 +1,423 @@
package utils
import (
"flag"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"github.com/lukaszraczylo/ask"
graphql "github.com/lukaszraczylo/go-simple-graphql"
"github.com/melbahja/got"
"strings"
"time"
)
// UpdatePackage updates the binary with the latest version
func UpdatePackage() bool {
ghToken, ghTokenSet := os.LookupEnv("GITHUB_TOKEN")
if !ghTokenSet {
Error("GITHUB_TOKEN not set", nil)
return false
}
const (
// GitHub API endpoint for latest release
githubReleasesURL = "https://api.github.com/repos/lukaszraczylo/semver-generator/releases/latest"
// Request timeout for HTTP requests
requestTimeout = 10 * time.Second
)
binaryName := fmt.Sprintf("semver-gen-%s-%s", runtime.GOOS, runtime.GOARCH)
Info("Checking for updates", map[string]interface{}{"binaryName": binaryName})
gql := graphql.NewConnection()
gql.SetEndpoint("https://api.github.com/graphql")
gql.SetOutput("mapstring")
// ReleaseInfo contains information about a GitHub release
type ReleaseInfo struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Name string `json:"name"`
Assets []ReleaseAsset `json:"assets"`
}
headers := map[string]interface{}{
"Authorization": fmt.Sprintf("Bearer %s", ghToken),
}
variables := map[string]interface{}{
"binaryName": binaryName,
}
var query = `query ($binaryName: String) {
repository(name: "semver-generator", owner: "lukaszraczylo") {
latestRelease {
releaseAssets(first: 10, name: $binaryName) {
edges {
node {
name
downloadUrl
}
}
}
}
}
}`
result, err := gql.Query(query, variables, headers)
// ReleaseAsset contains information about a release asset
type ReleaseAsset struct {
Name string `json:"name"`
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,
}
// CheckLatestRelease checks for the latest release version using REST API
// Returns the latest version tag and true if successful, empty string and false otherwise
func CheckLatestRelease() (string, bool) {
release, err := fetchLatestRelease(context.Background())
if err != nil {
Error("Unable to query GitHub API", map[string]interface{}{"error": err.Error()})
Debug("Unable to check latest release", map[string]interface{}{"error": err.Error()})
return "", false
}
version := normalizeVersion(release.TagName)
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)
release, err := fetchLatestRelease(context.Background())
if err != nil {
Error("Unable to fetch latest release", map[string]interface{}{"error": err.Error()})
return false
}
output, ok := ask.For(result, "repository.latestRelease.releaseAssets.edges[0].node.downloadUrl").String("")
if !ok {
Error("Unable to obtain download url for the binary", map[string]interface{}{
"binary": binaryName,
"output": output,
downloadURL := findBinaryAsset(release.Assets)
if downloadURL == "" {
Error("Unable to find binary for current platform", map[string]interface{}{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
})
return false
}
// Skip actual download in test mode
if flag.Lookup("test.v") == nil && os.Getenv("CI") == "" {
downloadedBinaryPath := fmt.Sprintf("/tmp/%s", binaryName)
g := got.New()
err = g.Download(output, downloadedBinaryPath)
if err != nil {
Error("Unable to download binary", map[string]interface{}{
"error": err.Error(),
"binaryPath": downloadedBinaryPath,
})
return false
}
currentBinary, err := os.Executable()
if err != nil {
Error("Unable to obtain current binary path", map[string]interface{}{
"error": err.Error(),
})
return false
}
err = os.Rename(downloadedBinaryPath, currentBinary)
if err != nil {
Error("Unable to overwrite current binary", map[string]interface{}{
"error": err.Error(),
})
return false
}
err = os.Chmod(currentBinary, 0777)
if err != nil {
Error("Unable to make binary executable", map[string]interface{}{
"error": err.Error(),
})
return false
}
Info("Downloading update", map[string]interface{}{
"version": release.TagName,
"url": downloadURL,
})
// Download to temp file
tempFile, err := downloadBinary(downloadURL)
if err != nil {
Error("Unable to download binary", map[string]interface{}{"error": err.Error()})
return false
}
defer os.Remove(tempFile) // Clean up temp file on failure
// Get current binary path
currentBinary, err := os.Executable()
if err != nil {
Error("Unable to get current binary path", map[string]interface{}{"error": err.Error()})
return false
}
// Replace current binary
if err := replaceBinary(tempFile, currentBinary); err != nil {
Error("Unable to replace binary", map[string]interface{}{"error": err.Error()})
return false
}
Info("Update successful", map[string]interface{}{
"version": release.TagName,
})
return true
}
// CheckLatestRelease checks for the latest release version
func CheckLatestRelease() (string, bool) {
ghToken, ghTokenSet := os.LookupEnv("GITHUB_TOKEN")
if !ghTokenSet {
return "[no GITHUB_TOKEN set]", false
// fetchLatestRelease fetches the latest release info from GitHub REST API
func fetchLatestRelease(ctx context.Context) (*ReleaseInfo, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, githubReleasesURL, nil)
if err != nil {
return nil, err
}
gql := graphql.NewConnection()
gql.SetEndpoint("https://api.github.com/graphql")
headers := map[string]interface{}{
"Authorization": fmt.Sprintf("bearer %s", ghToken),
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "semver-generator")
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
variables := map[string]interface{}{}
var query = `query {
repository(name: "semver-generator", owner: "lukaszraczylo", followRenames: true) {
releases(last: 2) {
nodes {
tag {
name
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}
var release ReleaseInfo
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return nil, err
}
return &release, nil
}
// findBinaryAsset finds the download URL for the current platform
func findBinaryAsset(assets []ReleaseAsset) string {
// Build expected binary name pattern
// Format: semver-gen-{version}-{os}-{arch}.tar.gz or just semver-gen-{os}-{arch}
osName := runtime.GOOS
archName := runtime.GOARCH
for _, asset := range assets {
name := strings.ToLower(asset.Name)
// Match patterns like "semver-gen-1.0.0-darwin-arm64.tar.gz" or "semver-gen-darwin-arm64"
if strings.Contains(name, osName) && strings.Contains(name, archName) {
// Prefer tar.gz archives
if strings.HasSuffix(name, ".tar.gz") {
return asset.BrowserDownloadURL
}
}
}`
result, err := gql.Query(query, variables, headers)
}
// Fallback: try to find any matching binary without tar.gz
for _, asset := range assets {
name := strings.ToLower(asset.Name)
if strings.Contains(name, osName) && strings.Contains(name, archName) {
// Skip checksums
if strings.Contains(name, "checksum") || strings.HasSuffix(name, ".sha256") || strings.HasSuffix(name, ".md5") {
continue
}
return asset.BrowserDownloadURL
}
}
return ""
}
// downloadBinary downloads the binary to a temp file and returns the path
func downloadBinary(url string) (string, error) {
resp, err := httpClient.Get(url)
if err != nil {
Error("Unable to query GitHub API", map[string]interface{}{"error": err.Error()})
return "", false
return "", err
}
output, _ := ask.For(result, "repository.releases.nodes[0].tag.name").String("")
if output == "v1" {
output, _ = ask.For(result, "repository.releases.nodes[1].tag.name").String("")
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("download failed with status %d", resp.StatusCode)
}
return output, true
}
// Create temp file
tempFile, err := os.CreateTemp("", "semver-generator-update-*")
if err != nil {
return "", err
}
tempPath := tempFile.Name()
// Check if it's a tar.gz archive
if strings.HasSuffix(url, ".tar.gz") {
// For tar.gz, we need to extract the binary
if err := extractTarGz(resp.Body, tempFile); err != nil {
tempFile.Close()
os.Remove(tempPath)
return "", err
}
} else {
// Direct binary download
if _, err := io.Copy(tempFile, resp.Body); err != nil {
tempFile.Close()
os.Remove(tempPath)
return "", err
}
}
tempFile.Close()
return tempPath, nil
}
// extractTarGz extracts the semver-generator binary from a tar.gz archive
func extractTarGz(r io.Reader, destFile *os.File) error {
// For simplicity, we'll download the whole archive to a temp file first,
// then use tar command to extract. This avoids adding archive/tar dependency.
// Create temp archive file
archiveFile, err := os.CreateTemp("", "semver-generator-archive-*.tar.gz")
if err != nil {
return err
}
archivePath := archiveFile.Name()
defer os.Remove(archivePath)
if _, err := io.Copy(archiveFile, r); err != nil {
archiveFile.Close()
return err
}
archiveFile.Close()
// Extract using tar command
extractDir, err := os.MkdirTemp("", "semver-generator-extract-*")
if err != nil {
return err
}
defer os.RemoveAll(extractDir)
// Use tar to extract
cmd := fmt.Sprintf("tar -xzf %s -C %s", archivePath, extractDir)
if err := runCommand(cmd); err != nil {
return fmt.Errorf("failed to extract archive: %w", err)
}
// Find the binary in the extracted files
// Support both new name (semver-generator) and old name (semver-gen) for backwards compatibility
binaryPath := ""
entries, err := os.ReadDir(extractDir)
if err != nil {
return err
}
// First try to find semver-generator (new name)
for _, entry := range entries {
if entry.Name() == "semver-generator" {
binaryPath = fmt.Sprintf("%s/%s", extractDir, entry.Name())
break
}
}
// Fallback to semver-gen (old name) for older releases
if binaryPath == "" {
for _, entry := range entries {
if entry.Name() == "semver-gen" {
binaryPath = fmt.Sprintf("%s/%s", extractDir, entry.Name())
break
}
}
}
if binaryPath == "" {
return fmt.Errorf("binary not found in archive (looked for semver-generator and semver-gen)")
}
// Copy the binary to the destination
srcFile, err := os.Open(binaryPath)
if err != nil {
return err
}
defer srcFile.Close()
// Seek to beginning of dest file and truncate
if _, err := destFile.Seek(0, 0); err != nil {
return err
}
if err := destFile.Truncate(0); err != nil {
return err
}
if _, err := io.Copy(destFile, srcFile); err != nil {
return err
}
return nil
}
// runCommand runs a shell command
func runCommand(cmdStr string) error {
return runCommandFunc(cmdStr)
}
// runCommandFunc is the function used to run commands (allows mocking in tests)
var runCommandFunc = func(cmdStr string) error {
cmd := exec.Command("sh", "-c", cmdStr)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// replaceBinary replaces the current binary with the new one
func replaceBinary(newBinary, currentBinary string) error {
// Make the new binary executable
if err := os.Chmod(newBinary, 0755); err != nil {
return err
}
// Rename (atomic on most systems)
if err := os.Rename(newBinary, currentBinary); err != nil {
// If rename fails (e.g., cross-device), try copy
return copyFile(newBinary, currentBinary)
}
return nil
}
// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
// Make executable
return os.Chmod(dst, 0755)
}
// normalizeVersion removes 'v' or 'V' prefix and trims whitespace
func normalizeVersion(v string) string {
v = strings.TrimSpace(v)
v = strings.TrimPrefix(v, "v")
v = strings.TrimPrefix(v, "V")
return v
}
// isNewerVersion compares two semver-like versions
// Returns true if latest is newer than current
func isNewerVersion(latest, current string) bool {
latestParts := parseVersionParts(latest)
currentParts := parseVersionParts(current)
for i := 0; i < len(latestParts) && i < len(currentParts); i++ {
if latestParts[i] > currentParts[i] {
return true
}
if latestParts[i] < currentParts[i] {
return false
}
}
return len(latestParts) > len(currentParts)
}
// parseVersionParts splits a version string into numeric parts
func parseVersionParts(v string) []int {
// Remove any suffix like -beta, -rc1, etc.
if idx := strings.IndexAny(v, "-+"); idx != -1 {
v = v[:idx]
}
parts := strings.Split(v, ".")
result := make([]int, 0, len(parts))
for _, p := range parts {
var num int
fmt.Sscanf(p, "%d", &num)
result = append(result, num)
}
return result
}
// FormatUpdateMessage formats a user-friendly update notification
func (u *UpdateInfo) FormatUpdateMessage() string {
return fmt.Sprintf("New version available: %s (current: %s) - %s",
u.LatestVersion, u.CurrentVersion, u.ReleaseURL)
}
+227 -53
View File
@@ -1,66 +1,240 @@
package utils
import (
"flag"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCheckLatestRelease(t *testing.T) {
// Initialize logger
InitLogger(true)
// Save original environment variables
originalToken := os.Getenv("GITHUB_TOKEN")
defer os.Setenv("GITHUB_TOKEN", originalToken)
// Test with no token
os.Unsetenv("GITHUB_TOKEN")
release, ok := CheckLatestRelease()
assert.Equal(t, "[no GITHUB_TOKEN set]", release, "Should return no token message")
assert.False(t, ok, "Should return false when no token is set")
// Test with token but simulating API error
// Set a dummy token that won't work with the GitHub API
os.Setenv("GITHUB_TOKEN", "dummy-token")
release, ok = CheckLatestRelease()
assert.Equal(t, "", release, "Should return empty string on API error")
assert.False(t, ok, "Should return false on API error")
// We can't reliably test the successful API call in unit tests
// as it would require a valid GitHub token and network access
}
func TestUpdatePackage(t *testing.T) {
// Initialize logger
InitLogger(true)
// Save original environment variables
originalToken := os.Getenv("GITHUB_TOKEN")
defer os.Setenv("GITHUB_TOKEN", originalToken)
// Test with no token
os.Unsetenv("GITHUB_TOKEN")
result := UpdatePackage()
assert.False(t, result, "Should return false when no token is set")
// Test with token but simulating API error
os.Setenv("GITHUB_TOKEN", "dummy-token")
result = UpdatePackage()
assert.False(t, result, "Should return false on API error")
// Create a test flag to simulate test mode
if flag.Lookup("test.v") == nil {
// This is a hack to simulate the test flag being set
// which is used in the UpdatePackage function to skip actual download
flag.Bool("test.v", true, "")
func TestNormalizeVersion(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"v1.0.0", "1.0.0"},
{"1.0.0", "1.0.0"},
{" v2.1.3 ", "2.1.3"},
{"V1.0.0", "1.0.0"},
{"v", ""},
{"", ""},
}
// We can't fully test the update functionality as it would modify the binary
// but we've tested the token check logic and API error handling
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := normalizeVersion(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Note: We're not using mock transports for these tests to avoid
// adding complexity. The tests focus on the token presence logic and error handling.
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"},
{Name: "semver-gen-1.0.0-darwin-arm64.tar.gz", BrowserDownloadURL: "https://example.com/darwin-arm64.tar.gz"},
{Name: "semver-gen-1.0.0-darwin-amd64.tar.gz", BrowserDownloadURL: "https://example.com/darwin-amd64.tar.gz"},
{Name: "semver-gen-1.0.0-windows-amd64.zip", BrowserDownloadURL: "https://example.com/windows-amd64.zip"},
{Name: "semver-gen-1.0.0-checksums.txt", BrowserDownloadURL: "https://example.com/checksums.txt"},
}
// Test finding the correct asset for the current platform
url := findBinaryAsset(assets)
assert.NotEmpty(t, url, "Should find a binary for the current platform")
assert.NotContains(t, url, "checksum", "Should not return checksum file")
}
func TestFindBinaryAssetEmpty(t *testing.T) {
assets := []ReleaseAsset{}
url := findBinaryAsset(assets)
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)
// Create a mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"tag_name": "v1.2.3",
"html_url": "https://github.com/lukaszraczylo/semver-generator/releases/tag/v1.2.3",
"name": "Release 1.2.3",
"assets": []
}`))
}))
defer server.Close()
// Note: In a real test, we'd need to mock the HTTP client or the URL
// For now, we just test the network error case
release, ok := CheckLatestRelease()
// This will either succeed (if network is available) or fail gracefully
if ok {
assert.NotEmpty(t, release)
}
}
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)
// Save original client
originalClient := httpClient
defer func() { httpClient = originalClient }()
// Create a mock server that returns an error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
// We can't easily test this without modifying the URL constant
// but we can test the error handling by checking that it fails gracefully
release, ok := CheckLatestRelease()
// The result depends on whether the real GitHub API is accessible
_ = release
_ = ok
}
func TestCopyFile(t *testing.T) {
// Create a temp source file
srcContent := []byte("test content")
srcFile, err := os.CreateTemp("", "test-*")
assert.NoError(t, err)
defer os.Remove(srcFile.Name())
_, err = srcFile.Write(srcContent)
assert.NoError(t, err)
srcFile.Close()
// Create destination path
dstPath := srcFile.Name() + ".copy"
defer os.Remove(dstPath)
// Copy the file
err = copyFile(srcFile.Name(), dstPath)
assert.NoError(t, err)
// Verify the content
content, err := os.ReadFile(dstPath)
assert.NoError(t, err)
assert.Equal(t, srcContent, content)
}
func TestReplaceBinary(t *testing.T) {
// Create a temp "new" binary
newContent := []byte("new binary content")
newFile, err := os.CreateTemp("", "new-binary-*")
assert.NoError(t, err)
defer os.Remove(newFile.Name())
_, err = newFile.Write(newContent)
assert.NoError(t, err)
newFile.Close()
// Create a temp "current" binary
currentFile, err := os.CreateTemp("", "current-binary-*")
assert.NoError(t, err)
currentPath := currentFile.Name()
defer os.Remove(currentPath)
currentFile.Close()
// Replace the binary
err = replaceBinary(newFile.Name(), currentPath)
assert.NoError(t, err)
// Verify the content was replaced
content, err := os.ReadFile(currentPath)
assert.NoError(t, err)
assert.Equal(t, newContent, content)
}
func TestUpdatePackageNoBinary(t *testing.T) {
InitLogger(false)
// This test verifies UpdatePackage handles the case where no binary is found
// by testing with a mock that returns empty assets
// Note: This would need proper mocking of httpClient to test fully
}
+1
View File
@@ -0,0 +1 @@
semver-generator.raczylo.com
+500
View File
@@ -0,0 +1,500 @@
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>semver-generator - Automatic Semantic Version Generator</title>
<meta
name="description"
content="Automatic semantic version generator based on git commit messages. Use as CLI, GitHub Action, or Docker container."
/>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class'
}
</script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<style>
body { font-family: "Inter", sans-serif; }
code, pre { font-family: "JetBrains Mono", monospace; }
.theme-transition {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.animate-fade-in-up { animation: fadeInUp 0.6s ease-out; }
.animate-float { animation: float 3s ease-in-out infinite; }
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .glass {
background: rgba(17, 24, 39, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.gradient-text {
background: linear-gradient(135deg, #10b981 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dark .gradient-text {
background: linear-gradient(135deg, #34d399 0%, #60a5fa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); }
.dark .shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.4); }
html { scroll-behavior: smooth; }
</style>
<script>
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
</script>
</head>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 theme-transition">
<!-- Navigation -->
<nav class="fixed w-full glass shadow-modern z-50 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="flex justify-between h-16 items-center">
<a href="#" class="flex items-center hover:opacity-80 transition-opacity duration-300 gap-2">
<i class="fas fa-code-branch text-2xl gradient-text"></i>
<span class="text-xl font-bold gradient-text">semver-generator</span>
</a>
<div class="hidden md:flex space-x-6">
<a href="#features" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Features</a>
<a href="#installation" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Installation</a>
<a href="#usage" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Usage</a>
<a href="#configuration" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Configuration</a>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle theme">
<i class="fas fa-moon dark:hidden text-xl"></i>
<i class="fas fa-sun hidden dark:inline text-xl"></i>
</button>
<a href="https://github.com/lukaszraczylo/semver-generator" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="View on GitHub">
<i class="fab fa-github text-xl"></i>
</a>
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle menu">
<i class="fas fa-bars text-xl" id="menu-open-icon"></i>
<i class="fas fa-times text-xl hidden" id="menu-close-icon"></i>
</button>
</div>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
<a href="#features" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Features</a>
<a href="#installation" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Installation</a>
<a href="#usage" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Usage</a>
<a href="#configuration" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Configuration</a>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="relative pt-24 sm:pt-32 pb-12 sm:pb-20 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-emerald-50 via-cyan-50 to-blue-50 dark:from-gray-900 dark:via-emerald-900/20 dark:to-blue-900/20 theme-transition"></div>
<div class="absolute top-0 -left-4 w-72 h-72 bg-emerald-300 dark:bg-emerald-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float"></div>
<div class="absolute top-0 -right-4 w-72 h-72 bg-cyan-300 dark:bg-cyan-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 1s;"></div>
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-blue-300 dark:bg-blue-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 2s;"></div>
<div class="relative max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center">
<div class="mb-8 sm:mb-10 flex justify-center animate-fade-in-up">
<div class="text-8xl sm:text-9xl animate-float">
<i class="fas fa-code-branch gradient-text"></i>
</div>
</div>
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4 sm:mb-6 leading-tight animate-fade-in-up" style="animation-delay: 0.1s;">
Semantic Version<br /><span class="gradient-text">Generator</span>
</h1>
<p class="text-base sm:text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-10 max-w-2xl mx-auto leading-relaxed px-4 animate-fade-in-up" style="animation-delay: 0.2s;">
Automatic semantic versioning based on git commit messages. Use as a CLI tool, GitHub Action, or Docker container.
</p>
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center mb-8 sm:mb-12 px-4 animate-fade-in-up" style="animation-delay: 0.3s;">
<a href="#installation" class="group relative bg-gradient-to-r from-emerald-500 to-blue-600 hover:from-emerald-600 hover:to-blue-700 text-white px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center shadow-lg hover:shadow-xl hover:scale-105">
<span class="relative z-10">Get Started</span>
</a>
<a href="https://github.com/lukaszraczylo/semver-generator" class="group glass hover:shadow-lg text-gray-900 dark:text-gray-100 px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center hover:scale-105">
<i class="fab fa-github mr-2"></i>View on GitHub
</a>
</div>
<div class="flex flex-wrap justify-center gap-2 sm:gap-4 text-sm px-4">
<img src="https://img.shields.io/github/v/release/lukaszraczylo/semver-generator" alt="Version" class="h-5" />
<img src="https://img.shields.io/github/license/lukaszraczylo/semver-generator" alt="License" class="h-5" />
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/semver-generator" alt="Go Report" class="h-5" />
</div>
<div class="mt-12 sm:mt-16 max-w-3xl mx-auto px-4 animate-fade-in-up" style="animation-delay: 0.4s;">
<div class="relative group">
<div class="absolute -inset-1 bg-gradient-to-r from-emerald-500 to-blue-600 rounded-xl blur opacity-25 group-hover:opacity-50 transition duration-500"></div>
<div class="relative bg-gray-900 rounded-xl p-6 text-left">
<div class="flex items-center gap-2 mb-4">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span class="ml-2 text-gray-400 text-sm">terminal</span>
</div>
<pre class="text-gray-100 text-sm sm:text-base overflow-x-auto"><code><span class="text-gray-400">$</span> semver-generator generate -l
<span class="text-emerald-400">SEMVER</span> 1.5.2
<span class="text-gray-400">$</span> semver-generator generate -r https://github.com/user/repo
<span class="text-emerald-400">SEMVER</span> 2.3.0</code></pre>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Features</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Automatic versioning that just works</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-magic text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Automatic Versioning</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Calculates version based on commit message keywords</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fab fa-github text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">GitHub Action</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Use directly in your CI/CD workflows</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-cyan-500 to-cyan-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fab fa-docker text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Docker Ready</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Multi-arch Docker images for any environment</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-cog text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Configurable Keywords</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Define your own trigger words for version bumps</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-tag text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Respects Tags</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Optional mode to respect existing git tags</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-pink-500 to-pink-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-flask text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Release Candidates</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Support for RC versions like 1.3.37-rc.1</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Installation Section -->
<section id="installation" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Installation</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Get started in seconds</p>
</div>
<div class="max-w-3xl mx-auto space-y-6">
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center">
<i class="fas fa-beer mr-2 text-amber-500"></i>
Homebrew (macOS)
</h3>
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>brew install --cask lukaszraczylo/taps/semver-generator</code></pre>
</div>
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center">
<i class="fas fa-download mr-2 text-emerald-500"></i>
Manual Download
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-3">Download from the <a href="https://github.com/lukaszraczylo/semver-generator/releases/latest" class="text-emerald-600 dark:text-emerald-400 hover:underline">releases page</a>.</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Supported: Darwin ARM64/AMD64, Linux ARM64/AMD64, Windows AMD64</p>
</div>
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center">
<i class="fab fa-docker mr-2 text-blue-500"></i>
Docker
</h3>
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>docker pull ghcr.io/lukaszraczylo/semver-generator:latest</code></pre>
</div>
</div>
</div>
</section>
<!-- Usage Section -->
<section id="usage" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Usage</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Multiple ways to use semver-generator</p>
</div>
<div class="max-w-4xl mx-auto space-y-8">
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-terminal mr-2 text-emerald-500"></i>
CLI Usage
</h3>
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4"><code><span class="text-gray-400"># Local repository</span>
semver-generator generate -l
<span class="text-gray-400"># Remote repository</span>
semver-generator generate -r https://github.com/user/repo
<span class="text-gray-400"># With custom config</span>
semver-generator generate -l -c semver.yaml
<span class="text-gray-400"># Strict mode (only exact matches)</span>
semver-generator generate -l -s
<span class="text-gray-400"># Respect existing tags</span>
semver-generator generate -l -e
<span class="text-gray-400"># Self-update to latest version (no auth required)</span>
semver-generator -u</code></pre>
<div class="grid sm:grid-cols-2 gap-4 text-sm">
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Flags</h4>
<ul class="space-y-1 text-gray-600 dark:text-gray-400">
<li><code class="text-emerald-600 dark:text-emerald-400">-l, --local</code> Use local repository</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-r, --repository</code> Remote repository URL</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-c, --config</code> Path to config file</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-s, --strict</code> Strict matching</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-e, --existing</code> Respect existing tags</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-d, --debug</code> Enable debug mode</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-u, --update</code> Self-update to latest version</li>
<li><code class="text-emerald-600 dark:text-emerald-400">-v, --version</code> Display current version</li>
</ul>
</div>
</div>
</div>
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fab fa-github mr-2 text-blue-500"></i>
GitHub Action
</h3>
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm"><code>jobs:
version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.semver.outputs.semantic_version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Generate version
id: semver
uses: lukaszraczylo/semver-generator@v1
with:
config_file: semver.yaml
repository_local: true
- name: Use version
run: echo "Version: ${{ steps.semver.outputs.semantic_version }}"</code></pre>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">How It Works</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Version calculation based on commit messages</p>
</div>
<div class="max-w-3xl mx-auto">
<div class="glass p-6 rounded-xl">
<pre class="text-gray-700 dark:text-gray-300 text-sm overflow-x-auto"><code>0.0.1 - PATCH - starting commit
0.0.2 - PATCH - another commit
0.0.4 - PATCH - commit with 'Update' => DOUBLE increment PATCH
0.1.0 - MINOR - commit with 'Change' => increment MINOR, reset PATCH
0.1.1 - PATCH - additional commit
1.0.1 - MAJOR - commit with 'BREAKING' => INCREMENT MAJOR, reset MINOR
1.0.2 - PATCH - another commit</code></pre>
</div>
<div class="mt-6 grid sm:grid-cols-3 gap-4 text-center">
<div class="glass p-4 rounded-xl">
<div class="text-3xl font-bold text-red-500 mb-2">MAJOR</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Breaking changes</p>
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">Keywords: "breaking"</p>
</div>
<div class="glass p-4 rounded-xl">
<div class="text-3xl font-bold text-yellow-500 mb-2">MINOR</div>
<p class="text-sm text-gray-600 dark:text-gray-400">New features</p>
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">Keywords: "change", "improve"</p>
</div>
<div class="glass p-4 rounded-xl">
<div class="text-3xl font-bold text-emerald-500 mb-2">PATCH</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Bug fixes</p>
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">Keywords: "update", "initial"</p>
</div>
</div>
</div>
</div>
</section>
<!-- Configuration Section -->
<section id="configuration" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Configuration</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Customize keywords and behavior</p>
</div>
<div class="max-w-3xl mx-auto">
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">semver.yaml</h3>
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm"><code><span class="text-gray-400">version:</span> 1
<span class="text-gray-400"># Starting version (optional)</span>
<span class="text-emerald-400">force:</span>
<span class="text-blue-400">major:</span> 1
<span class="text-blue-400">minor:</span> 0
<span class="text-blue-400">patch:</span> 0
<span class="text-blue-400">commit:</span> 69fbe2df696f40281b9104ff073d26186cde1024
<span class="text-gray-400"># Commits to ignore</span>
<span class="text-emerald-400">blacklist:</span>
- "Merge branch"
- "Merge pull request"
- "feature/"
<span class="text-gray-400"># Keywords for version bumps</span>
<span class="text-emerald-400">wording:</span>
<span class="text-blue-400">patch:</span>
- update
- initial
- fix
<span class="text-blue-400">minor:</span>
- change
- improve
- add
<span class="text-blue-400">major:</span>
- breaking
<span class="text-blue-400">release:</span>
- release-candidate
- add-rc</code></pre>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="py-8 bg-gray-100 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-2">
<i class="fas fa-code-branch text-xl gradient-text"></i>
<span class="font-semibold gradient-text">semver-generator</span>
</div>
<div class="flex items-center gap-6">
<a href="https://github.com/lukaszraczylo/semver-generator" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">
<i class="fab fa-github text-xl"></i>
</a>
<a href="https://github.com/lukaszraczylo/semver-generator/issues" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
Issues
</a>
<a href="https://github.com/lukaszraczylo/semver-generator/releases" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
Releases
</a>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm">MIT License</p>
</div>
</div>
</footer>
<script>
// Theme toggle
document.getElementById('theme-toggle').addEventListener('click', function() {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
} else {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
}
});
// Mobile menu toggle
document.getElementById('mobile-menu-toggle').addEventListener('click', function() {
const menu = document.getElementById('mobile-menu');
const openIcon = document.getElementById('menu-open-icon');
const closeIcon = document.getElementById('menu-close-icon');
menu.classList.toggle('hidden');
openIcon.classList.toggle('hidden');
closeIcon.classList.toggle('hidden');
});
// Close mobile menu when clicking a link
document.querySelectorAll('#mobile-menu a').forEach(link => {
link.addEventListener('click', () => {
document.getElementById('mobile-menu').classList.add('hidden');
document.getElementById('menu-open-icon').classList.remove('hidden');
document.getElementById('menu-close-icon').classList.add('hidden');
});
});
</script>
</body>
</html>
+2 -2
View File
@@ -60,11 +60,11 @@ if [[ ! -z "$INPUT_DEBUGMODE" ]]; then
echo "----"
echo "FLAGS: $FLAGS"
echo "----"
/go/src/app/semver-gen generate $FLAGS $*
/go/src/app/semver-generator generate $FLAGS $*
echo "----"
fi
OUT_SEMVER_GEN=$(/go/src/app/semver-gen generate $FLAGS $*)
OUT_SEMVER_GEN=$(/go/src/app/semver-generator generate $FLAGS $*)
[ $? -eq 0 ] || exit 1
CLEAN_SEMVER=$(echo $OUT_SEMVER_GEN | sed -e 's|SEMVER ||g')
echo "semantic_version=$CLEAN_SEMVER" >> $GITHUB_OUTPUT
+2 -9
View File
@@ -7,11 +7,8 @@ toolchain go1.24.6
require (
github.com/go-git/go-git/v5 v5.16.4
github.com/lithammer/fuzzysearch v1.1.8
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
github.com/lukaszraczylo/go-simple-graphql v1.2.89
github.com/lukaszraczylo/graphql-monitoring-proxy v0.41.20
github.com/lukaszraczylo/graphql-monitoring-proxy v0.42.4
github.com/lukaszraczylo/pandati v0.0.29
github.com/melbahja/got v0.7.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
@@ -21,18 +18,16 @@ 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/avast/retry-go/v4 v4.7.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/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // 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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/gookit/goutil v0.7.2 // 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
@@ -60,9 +55,7 @@ require (
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/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+4 -18
View File
@@ -9,8 +9,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
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/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
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=
@@ -35,8 +33,8 @@ 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.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
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-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=
@@ -45,15 +43,11 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
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/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gookit/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo=
github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -71,12 +65,8 @@ 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/ask v0.0.0-20240916204100-6e9ef53a62d9 h1:pL8B9mjv6RPUfKYYGm/uJ7QL6Ndf+z+OEl0qJE6KmEc=
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
github.com/lukaszraczylo/go-simple-graphql v1.2.89 h1:Xbu1Ny+a0lT2Sr2SaSC8mcHmGQDwGD4TJKk4DDd+PwA=
github.com/lukaszraczylo/go-simple-graphql v1.2.89/go.mod h1:PxQYblQDZISmYYj8sNfazAWxAOh1rhAtU208y+uPV8s=
github.com/lukaszraczylo/graphql-monitoring-proxy v0.41.20 h1:554N+HD5cTY074Y0LrL82cYQNCG1qDV3QKULgdLovs0=
github.com/lukaszraczylo/graphql-monitoring-proxy v0.41.20/go.mod h1:1FLcH7q+7cjUgQxyeVeF7ouBamGpcJZgqDF+j+cuFxI=
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/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=
@@ -86,8 +76,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
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/melbahja/got v0.7.0 h1:YHbiuNZVS8fIkyV0iXyThQQliwlKZb5h4k80zBVovxg=
github.com/melbahja/got v0.7.0/go.mod h1:27cUstWCEfj6HBESMTGzCFY24Qj+QNMWot3+KuxguQU=
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=
@@ -166,8 +154,6 @@ golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=