4 Commits

Author SHA1 Message Date
265107b66b Replace GoReleaser workflow with custom Release pipeline
Some checks failed
Release / goreleaser (push) Failing after 5m10s
2025-05-23 15:55:01 +02:00
a806b97b9b Add GoReleaser configuration and version command
Some checks failed
goreleaser / goreleaser (push) Failing after 5m46s
2025-05-23 15:41:54 +02:00
e1f7604439 Update README.md 2025-05-23 15:26:19 +02:00
1e195a313c Improve version tag fetching and handling in release script 2025-05-23 15:24:09 +02:00
6 changed files with 524 additions and 8 deletions

98
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: Release
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: Set up Git for Gitea
run: |
git config --global url."https://\token:${{ secrets.API_TOKEN }}@git.mvl.sh/".insteadOf "https://git.mvl.sh/"
- name: Build binaries
id: build
run: |
# Get version from tag
VERSION=${GITHUB_REF#refs/tags/}
echo "Building version ${VERSION}..."
# Set up directories
mkdir -p dist/linux_amd64
mkdir -p dist/linux_arm64
mkdir -p dist/darwin_amd64
mkdir -p dist/darwin_arm64
# Build for different platforms
LDFLAGS="-s -w -X git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.Version=${VERSION} -X git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.BuildDate=$(date -u '+%Y-%m-%d %H:%M:%S')"
# Linux amd64
GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/linux_amd64/sshtunnel
tar -czf dist/sshtunnel_Linux_x86_64.tar.gz -C dist/linux_amd64 sshtunnel
# Linux arm64
GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/linux_arm64/sshtunnel
tar -czf dist/sshtunnel_Linux_arm64.tar.gz -C dist/linux_arm64 sshtunnel
# macOS amd64
GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/darwin_amd64/sshtunnel
tar -czf dist/sshtunnel_Darwin_x86_64.tar.gz -C dist/darwin_amd64 sshtunnel
# macOS arm64
GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/darwin_arm64/sshtunnel
tar -czf dist/sshtunnel_Darwin_arm64.tar.gz -C dist/darwin_arm64 sshtunnel
# Generate checksums
cd dist && sha256sum *.tar.gz > checksums.txt
- name: Generate completion scripts
run: |
mkdir -p dist/completions
# Generate completion scripts for different shells
dist/linux_amd64/sshtunnel completion bash > dist/completions/sshtunnel.bash
dist/linux_amd64/sshtunnel completion zsh > dist/completions/sshtunnel.zsh
dist/linux_amd64/sshtunnel completion fish > dist/completions/sshtunnel.fish
- name: Create Gitea Release
env:
GITEA_TOKEN: ${{ secrets.API_TOKEN }}
run: |
# Get version from tag
VERSION=${GITHUB_REF#refs/tags/}
# Create release on Gitea
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: token $GITEA_TOKEN" \
-d "{\"tag_name\": \"$VERSION\", \"name\": \"$VERSION\", \"body\": \"Release $VERSION\"}" \
"https://git.mvl.sh/api/v1/repos/vleeuwenmenno/sshtunnel/releases"
# Upload release assets
for file in dist/*.tar.gz dist/checksums.txt; do
echo "Uploading $file..."
curl -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file" \
"https://git.mvl.sh/api/v1/repos/vleeuwenmenno/sshtunnel/releases/$VERSION/assets"
done
- name: Update latest tag
run: |
git tag -f latest
git push -f origin latest

100
.goreleaser.yml Normal file
View File

@@ -0,0 +1,100 @@
# This is a GoReleaser configuration file for sshtunnel
# Compatible with GoReleaser v1.26.2
# https://goreleaser.com/customization/
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}}
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:
- name: sshtunnel
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 }}"
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: |
mkdir -p /usr/share/bash-completion/completions/
mkdir -p /usr/share/zsh/site-functions/
mkdir -p /usr/share/fish/vendor_completions.d/
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,16 +13,50 @@ A Go-based command-line tool to manage SSH tunnels. This tool allows you to:
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:
```
git clone https://git.mvl.sh/vleeuwenmenno/sshtunnel.git
cd sshtunnel
go build -o sshtunnel ./cmd
make
sudo make install
```
The build process will automatically include version information from git tags.
## Uninstallation
For installation using `go install`, run:
```
go clean -i git.mvl.sh/vleeuwenmenno/sshtunnel
```
For manual installation, run:
```
sudo make uninstall
```
## 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
```
@@ -96,6 +130,58 @@ This command will:
3. Validate all recorded tunnels and their current state
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
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)"
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..."
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
printf "\033[0;31m"
@@ -39,9 +43,9 @@ if [ $? -ne 0 ]; then
fi
# 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
printfe "%s\n" "green" "Bash completion script installed to $COMPLETION_SCRIPT."

View File

@@ -17,8 +17,21 @@ if ! git rev-parse --is-inside-work-tree &> /dev/null; then
exit 1
fi
# Get the latest tag or default to v0.0.0 if no tags exist
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
# Fetch all tags first to ensure we have the latest
printfe "%s\n" "cyan" "Fetching all tags..."
log_and_run git fetch --tags
# Get the latest version tag, excluding 'latest' tag, or default to v0.0.0 if no tags exist
VERSION_TAGS=$(git tag -l 'v*.*.*' | grep -v 'latest' | sort -V)
if [ -z "$VERSION_TAGS" ]; then
printfe "%s\n" "yellow" "No version tags found. Starting with v0.0.0."
LATEST_TAG="v0.0.0"
else
# Get the latest version tag
LATEST_TAG=$(echo "$VERSION_TAGS" | tail -n 1)
printfe "%s" "cyan" "Found latest version tag: "
printfe "%s\n" "yellow" "$LATEST_TAG" false
fi
# Parse the version number
if [[ $LATEST_TAG =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
@@ -56,8 +69,7 @@ printfe "%s" "cyan" "Creating and pushing tag: "
printfe "%s\n" "yellow" "$NEXT_VERSION" false
# Make sure we have the latest changes
printfe "%s\n" "cyan" "Fetching latest tags..."
log_and_run git fetch --tags
# We've already fetched tags at the beginning
# Create the new tag
printfe "%s\n" "cyan" "Creating new tag..."

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
}