Add GoReleaser configuration and version command
Some checks failed
goreleaser / goreleaser (push) Failing after 5m46s

This commit is contained in:
Menno van Leeuwen 2025-05-23 15:41:54 +02:00
parent e1f7604439
commit a806b97b9b
Signed by: vleeuwenmenno
SSH Key Fingerprint: SHA256:OJFmjANpakwD3F2Rsws4GLtbdz1TJ5tkQF0RZmF0TRE
5 changed files with 437 additions and 3 deletions

37
.github/workflows/go-releaser.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: goreleaser
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update latest tag
run: |
git tag -f latest
git push -f origin latest

106
.goreleaser.yml Normal file
View File

@ -0,0 +1,106 @@
# This is a GoReleaser configuration file
# For more information about configuration options, visit:
# https://goreleaser.com/customization/
version: 2
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
flags:
- -trimpath
ldflags:
- -s -w
- -X git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.Version={{.Version}}
- -X git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.BuildDate={{.Date}}
mod_timestamp: "{{ .CommitTimestamp }}"
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
# Homebrew formula
brews:
- repository:
owner: vleeuwenmenno
name: homebrew-tap
token: "{{ .Env.GITHUB_TOKEN }}"
directory: Formula
homepage: "https://git.mvl.sh/vleeuwenmenno/sshtunnel"
description: "SSH tunnel manager CLI tool"
license: "MIT"
commit_author:
name: goreleaserbot
email: goreleaser@carlosbecker.com
install: |
bin.install "sshtunnel"
# Generate shell completions
output = Utils.safe_popen_read("#{bin}/sshtunnel", "completion", "bash")
(bash_completion/"sshtunnel").write output
output = Utils.safe_popen_read("#{bin}/sshtunnel", "completion", "zsh")
(zsh_completion/"_sshtunnel").write output
output = Utils.safe_popen_read("#{bin}/sshtunnel", "completion", "fish")
(fish_completion/"sshtunnel.fish").write output
test: |
system "#{bin}/sshtunnel", "version"
# .deb and .rpm packages
nfpms:
- file_name_template: "{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
id: packages
homepage: https://git.mvl.sh/vleeuwenmenno/sshtunnel
description: |-
SSH tunnel manager CLI tool
maintainer: Menno van Leeuwen
license: MIT
vendor: vleeuwenmenno
formats:
- deb
- rpm
dependencies:
- openssh-client
recommends:
- bash-completion
scripts:
postinstall: |
sshtunnel completion bash > /usr/share/bash-completion/completions/sshtunnel
sshtunnel completion zsh > /usr/share/zsh/site-functions/_sshtunnel
sshtunnel completion fish > /usr/share/fish/vendor_completions.d/sshtunnel.fish
chmod 644 /usr/share/bash-completion/completions/sshtunnel || true
chmod 644 /usr/share/zsh/site-functions/_sshtunnel || true
chmod 644 /usr/share/fish/vendor_completions.d/sshtunnel.fish || true

View File

@ -13,6 +13,12 @@ A Go-based command-line tool to manage SSH tunnels. This tool allows you to:
go install git.mvl.sh/vleeuwenmenno/sshtunnel@latest go install git.mvl.sh/vleeuwenmenno/sshtunnel@latest
``` ```
You can also install a specific version:
```
go install git.mvl.sh/vleeuwenmenno/sshtunnel@v1.0.0
```
Or clone this repository and build it yourself: Or clone this repository and build it yourself:
``` ```
@ -22,6 +28,8 @@ make
sudo make install sudo make install
``` ```
The build process will automatically include version information from git tags.
## Uninstallation ## Uninstallation
For installation using `go install`, run: For installation using `go install`, run:
@ -38,6 +46,17 @@ sudo make uninstall
## Usage ## Usage
### Showing version information
```
sshtunnel version
```
This will display the current version of sshtunnel along with build information.
Options:
- `-c, --check-updates`: Check for updates against the latest release
### Listing active tunnels ### Listing active tunnels
``` ```
@ -111,6 +130,58 @@ This command will:
3. Validate all recorded tunnels and their current state 3. Validate all recorded tunnels and their current state
4. Show active SSH tunnel processes and their status 4. Show active SSH tunnel processes and their status
### Version information
```
sshtunnel version --check-updates
```
This displays version information and checks for updates to the tool.
When a new version is available, you'll get instructions on how to update.
### Shell completion
Generate shell completion scripts:
```
# Bash
sshtunnel completion bash > ~/.bash_completion.d/sshtunnel
source ~/.bash_completion.d/sshtunnel
# Zsh
sshtunnel completion zsh > "${fpath[1]}/_sshtunnel"
# Fish
sshtunnel completion fish > ~/.config/fish/completions/sshtunnel.fish
```
For system-wide installation:
```
# Bash (Linux)
sudo sshtunnel completion bash > /etc/bash_completion.d/sshtunnel
# Bash (macOS with Homebrew)
sshtunnel completion bash > $(brew --prefix)/etc/bash_completion.d/sshtunnel
```
## Development
### Release Process
The project includes a release script to help manage version tags:
```
./bin/scripts/release.sh
```
This script will:
1. Find the latest version tag
2. Suggest the next patch version (e.g., v1.0.0 → v1.0.1)
3. Allow you to accept this suggestion or enter a custom version
4. Create and push the new tag
5. Update the "latest" tag to point to the new version
## How it works ## How it works
The tool creates SSH tunnels using the system's SSH client and manages them by tracking their process IDs in a hidden directory (`~/.sshtunnels/`). Each tunnel is assigned a unique ID for easy management. Traffic statistics are collected and stored to help you monitor data transfer through your tunnels. The tool creates SSH tunnels using the system's SSH client and manages them by tracking their process IDs in a hidden directory (`~/.sshtunnels/`). Each tunnel is assigned a unique ID for easy management. Traffic statistics are collected and stored to help you monitor data transfer through your tunnels.

View File

@ -28,8 +28,12 @@ if [ -n "$(git status --porcelain)" ]; then
POSTFIX=" (dirty)" POSTFIX=" (dirty)"
fi fi
# Format a clean version string for the build
VERSION=$(echo "$BRANCH" | sed 's/^v//')
BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S')
printfe "%s\n" "cyan" "Building $BINARY_NAME binary for $BRANCH ($LATEST_COMMIT_HASH)$POSTFIX..." printfe "%s\n" "cyan" "Building $BINARY_NAME binary for $BRANCH ($LATEST_COMMIT_HASH)$POSTFIX..."
go build -o $BINARY_PATH go build -ldflags="-X 'git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.Version=$BRANCH' -X 'git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.BuildDate=$BUILD_DATE'" -o $BINARY_PATH
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
printf "\033[0;31m" printf "\033[0;31m"
@ -39,9 +43,9 @@ if [ $? -ne 0 ]; then
fi fi
# Put tag and hash in .sshtunnel_version file # Put tag and hash in .sshtunnel_version file
echo "$BRANCH ($LATEST_COMMIT_HASH)$POSTFIX" > $BINARY_PATH_VERSION echo "$BRANCH" > $BINARY_PATH_VERSION
printfe "%s\n" "cyan" "Generating Bash completion script..." printfe "%s\n" "cyan" "Generating completion scripts..."
$BINARY_PATH completion bash > $COMPLETION_SCRIPT $BINARY_PATH completion bash > $COMPLETION_SCRIPT
printfe "%s\n" "green" "Bash completion script installed to $COMPLETION_SCRIPT." printfe "%s\n" "green" "Bash completion script installed to $COMPLETION_SCRIPT."

216
cmd/version.go Normal file
View File

@ -0,0 +1,216 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/spf13/cobra"
)
var (
// Version is set during build
Version = "dev"
// BuildDate is set during build
BuildDate = ""
// For go install builds, this will be the go.mod version
versionFromMod = "$GOFLAGS: -ldflags=-X git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.Version=${VERSION}"
checkForUpdates bool
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show sshtunnel version information",
Long: `Display the current version of sshtunnel and check for updates.`,
Run: func(cmd *cobra.Command, args []string) {
// Display current version
showVersion()
// Check for updates if requested
if checkForUpdates {
checkUpdate()
}
},
}
func init() {
rootCmd.AddCommand(versionCmd)
versionCmd.Flags().BoolVarP(&checkForUpdates, "check-updates", "c", false, "Check for updates")
// If version is still "dev", try to read from other sources
if Version == "dev" {
// Check if version was injected via go install's version info
if strings.Contains(versionFromMod, "${VERSION}") {
// Not replaced, try to read from the installed version file
Version = readInstalledVersion()
} else {
// Extract version from the injected string
re := regexp.MustCompile(`Version=(.+)$`)
matches := re.FindStringSubmatch(versionFromMod)
if len(matches) > 1 {
Version = matches[1]
} else {
// Fall back to reading from installed file
Version = readInstalledVersion()
}
}
}
}
func showVersion() {
fmt.Printf("sshtunnel %s\n", Version)
if BuildDate != "" {
fmt.Printf("Build date: %s\n", BuildDate)
}
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
func readInstalledVersion() string {
// Check system-installed version file first
versionFile := "/usr/local/share/sshtunnel/sshtunnel.version"
if _, err := os.Stat(versionFile); err == nil {
content, err := os.ReadFile(versionFile)
if err == nil {
return strings.TrimSpace(string(content))
}
}
// Try user's home directory for go install version
homeDir, err := os.UserHomeDir()
if err == nil {
goPath := filepath.Join(homeDir, "go", "pkg", "mod", "git.mvl.sh", "vleeuwenmenno", "sshtunnel@*")
matches, err := filepath.Glob(goPath)
if err == nil && len(matches) > 0 {
// Sort matches to get the latest version
// Example: .../sshtunnel@v1.0.0 -> extract v1.0.0
latestMatch := matches[len(matches)-1]
parts := strings.Split(filepath.Base(latestMatch), "@")
if len(parts) > 1 {
return parts[1]
}
}
}
// Check if executable name has version info
execPath, err := os.Executable()
if err == nil {
execName := filepath.Base(execPath)
if strings.Contains(execName, "sshtunnel-v") {
parts := strings.Split(execName, "-v")
if len(parts) > 1 {
return "v" + parts[1]
}
}
// Check if it's in GOBIN with a version suffix
re := regexp.MustCompile(`sshtunnel@(v[0-9]+\.[0-9]+\.[0-9]+)$`)
matches := re.FindStringSubmatch(execName)
if len(matches) > 1 {
return matches[1]
}
}
// Default version if we couldn't find any version information
return "dev"
}
func checkUpdate() {
fmt.Println("\nChecking for updates...")
client := http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get("https://git.mvl.sh/api/v1/repos/vleeuwenmenno/sshtunnel/tags")
if err != nil {
fmt.Printf("Error checking for updates: %s\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response: %s\n", err)
return
}
var tags []struct {
Name string `json:"name"`
}
if err := json.Unmarshal(body, &tags); err != nil {
fmt.Printf("Error parsing response: %s\n", err)
return
}
// Find the latest version tag
var latestTag string
for _, tag := range tags {
if strings.HasPrefix(tag.Name, "v") && (latestTag == "" || compareVersions(tag.Name, latestTag) > 0) {
latestTag = tag.Name
}
}
if latestTag == "" {
fmt.Println("No version tags found in the repository")
return
}
// Compare with current version
if compareVersions(latestTag, Version) > 0 {
fmt.Printf("A newer version is available: %s (you have %s)\n", latestTag, Version)
fmt.Println("To update, run: go install git.mvl.sh/vleeuwenmenno/sshtunnel@latest")
} else {
fmt.Println("You are running the latest version")
}
}
// compareVersions compares two semantic version strings (v1.2.3)
// returns: 1 if v1 > v2
// -1 if v1 < v2
// 0 if v1 == v2
func compareVersions(v1, v2 string) int {
// Remove 'v' prefix if any
v1 = strings.TrimPrefix(v1, "v")
v2 = strings.TrimPrefix(v2, "v")
// Split into parts
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
// Compare each part
for i := 0; i < len(parts1) && i < len(parts2); i++ {
// Skip non-numeric parts (like pre-release suffixes)
num1 := 0
fmt.Sscanf(parts1[i], "%d", &num1)
num2 := 0
fmt.Sscanf(parts2[i], "%d", &num2)
if num1 > num2 {
return 1
}
if num1 < num2 {
return -1
}
}
// If we get here, the common parts are equal
if len(parts1) > len(parts2) {
return 1
}
if len(parts1) < len(parts2) {
return -1
}
return 0
}