diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10795a1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Download dependencies + run: go mod download + + - name: Format check + run: | + gofmt -l . + test -z "$(gofmt -l .)" + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -v -race -coverprofile=coverage.out ./test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.out + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + + build: + runs-on: ubuntu-latest + needs: [test, lint] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Build + run: go build -v ./... + + - name: Test GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: build --snapshot --clean \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a424ea4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +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: '1.24' + + - name: Run tests + run: go test ./test + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..f9c4ae2 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,40 @@ +name: Validate + +on: + pull_request: + branches: [ main ] + paths: + - '.goreleaser.yml' + - 'main.go' + - 'cmd/**' + - 'internal/**' + - 'go.mod' + - 'go.sum' + +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: '1.24' + + - name: Check GoReleaser config + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: check + + - name: Test GoReleaser build + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: build --snapshot --clean \ No newline at end of file diff --git a/.gitignore b/.gitignore index c135caa..7660d23 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ desktop.ini # Temporary files *.tmp -*.log \ No newline at end of file +*.log + +# GoReleaser artifacts +goreleaser/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..f521bc2 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,129 @@ +# GoReleaser configuration for lnk +version: 2 + +project_name: lnk + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + # Optional: exclude specific combinations + ignore: + - goos: windows + goarch: arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X main.buildTime={{.Date}} + main: ./main.go + binary: lnk + +archives: + - id: default + # this name template makes the OS and Arch compatible with the results of uname. + 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 }} + files: + - README.md + - LICENSE + builds_info: + group: root + owner: root + +checksum: + name_template: 'checksums.txt' + +snapshot: + version_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + use: github + filters: + exclude: + - '^docs:' + - '^test:' + - '^ci:' + - '^chore:' + - '^style:' + - '^refactor:' + groups: + - title: Features + regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' + order: 0 + - title: 'Bug fixes' + regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' + order: 1 + - title: Others + order: 999 + +# GitHub release configuration +release: + github: + owner: yarlson + name: lnk + draft: false + prerelease: auto + mode: replace + header: | + ## Lnk {{.Tag}} + + Git-native dotfiles management that doesn't suck. + + ### Installation + + ```bash + # Quick install + curl -sSL https://raw.githubusercontent.com/yarlson/lnk/main/install.sh | bash + + # Manual download + wget https://github.com/yarlson/lnk/releases/download/{{.Tag}}/lnk_{{.Os}}_{{.Arch}}.tar.gz + tar -xzf lnk_{{.Os}}_{{.Arch}}.tar.gz + sudo mv lnk /usr/local/bin/ + ``` + + footer: | + --- + **Full Changelog**: https://github.com/yarlson/lnk/compare/{{.PreviousTag}}...{{.Tag}} + +# Homebrew tap (optional - you can enable this later) +# brews: +# - repository: +# owner: yarlson +# name: homebrew-tap +# homepage: "https://github.com/yarlson/lnk" +# description: "Git-native dotfiles management" +# license: "MIT" +# test: | +# system "#{bin}/lnk --version" + +# Docker images (optional) +# dockers: +# - image_templates: +# - "yarlson/lnk:latest" +# - "yarlson/lnk:{{ .Tag }}" +# - "yarlson/lnk:v{{ .Major }}" +# dockerfile: Dockerfile +# build_flag_templates: +# - "--label=org.opencontainers.image.created={{.Date}}" +# - "--label=org.opencontainers.image.title={{.ProjectName}}" +# - "--label=org.opencontainers.image.revision={{.FullCommit}}" +# - "--label=org.opencontainers.image.version={{.Version}}" \ No newline at end of file diff --git a/Makefile b/Makefile index caa9647..c26dd27 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ YELLOW=\033[0;33m BLUE=\033[0;34m NC=\033[0m # No Color -.PHONY: help build test clean install uninstall fmt lint vet tidy run dev cross-compile release +.PHONY: help build test clean install uninstall fmt lint vet tidy run dev cross-compile release goreleaser-check goreleaser-snapshot ## help: Show this help message help: @@ -42,8 +42,10 @@ help: @echo " uninstall Remove binary from /usr/local/bin" @echo "" @echo "$(GREEN)Release:$(NC)" - @echo " cross-compile Build for multiple platforms" - @echo " release Create release builds" + @echo " cross-compile Build for multiple platforms (legacy)" + @echo " release Create release builds (legacy)" + @echo " goreleaser-check Validate .goreleaser.yml config" + @echo " goreleaser-snapshot Build snapshot release with GoReleaser" @echo "" @echo "$(GREEN)Utilities:$(NC)" @echo " clean Clean build artifacts" @@ -158,7 +160,27 @@ clean: deps: @echo "$(BLUE)Installing development dependencies...$(NC)" @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @if ! command -v goreleaser >/dev/null 2>&1; then \ + echo "$(BLUE)Installing GoReleaser...$(NC)"; \ + go install github.com/goreleaser/goreleaser@latest; \ + fi @echo "$(GREEN)✓ Dependencies installed$(NC)" +## goreleaser-check: Validate GoReleaser configuration +goreleaser-check: + @echo "$(BLUE)Validating GoReleaser configuration...$(NC)" + @if command -v goreleaser >/dev/null 2>&1; then \ + goreleaser check; \ + echo "$(GREEN)✓ GoReleaser configuration is valid$(NC)"; \ + else \ + echo "$(YELLOW)⚠ GoReleaser not found. Install with: make deps$(NC)"; \ + fi + +## goreleaser-snapshot: Build snapshot release with GoReleaser +goreleaser-snapshot: goreleaser-check + @echo "$(BLUE)Building snapshot release with GoReleaser...$(NC)" + @goreleaser build --snapshot --clean + @echo "$(GREEN)✓ Snapshot release built in dist/$(NC)" + # Default target all: check build \ No newline at end of file diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..313247a --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,131 @@ +# Release Process + +This document describes how to create releases for the lnk project using GoReleaser. + +## Prerequisites + +- Push access to the main repository +- Git tags pushed to GitHub trigger releases automatically +- GoReleaser is configured in `.goreleaser.yml` +- GitHub Actions will handle the release process + +## Creating a Release + +### 1. Ensure everything is ready + +```bash +# Run all quality checks +make check + +# Test GoReleaser configuration +make goreleaser-check + +# Test build process +make goreleaser-snapshot +``` + +### 2. Create and push a version tag + +```bash +# Create a new tag (replace x.y.z with actual version) +git tag -a v1.0.0 -m "Release v1.0.0" + +# Push the tag to trigger the release +git push origin v1.0.0 +``` + +### 3. Monitor the release + +- GitHub Actions will automatically build and release when the tag is pushed +- Check the [Actions tab](https://github.com/yarlson/lnk/actions) for build status +- The release will appear in [GitHub Releases](https://github.com/yarlson/lnk/releases) + +## What GoReleaser Does + +1. **Builds binaries** for multiple platforms: + - Linux (amd64, arm64) + - macOS (amd64, arm64) + - Windows (amd64) + +2. **Creates archives** with consistent naming: + - `lnk_Linux_x86_64.tar.gz` + - `lnk_Darwin_arm64.tar.gz` + - etc. + +3. **Generates checksums** for verification + +4. **Creates GitHub release** with: + - Automatic changelog from conventional commits + - Installation instructions + - Download links for all platforms + +## Manual Release (if needed) + +If you need to create a release manually: + +```bash +# Export GitHub token +export GITHUB_TOKEN="your_token_here" + +# Create release (requires a git tag) +goreleaser release --clean +``` + +## Testing Releases Locally + +```bash +# Test the build process without releasing +make goreleaser-snapshot + +# Built artifacts will be in dist/ +ls -la dist/ + +# Test a binary +./dist/lnk_/lnk --version +``` + +## Version Numbering + +We use [Semantic Versioning](https://semver.org/): + +- `v1.0.0` - Major release (breaking changes) +- `v1.1.0` - Minor release (new features, backward compatible) +- `v1.1.1` - Patch release (bug fixes) + +## Changelog + +GoReleaser automatically generates changelogs from git commits using conventional commit format: + +- `feat:` - New features +- `fix:` - Bug fixes +- `docs:` - Documentation changes (excluded from changelog) +- `test:` - Test changes (excluded from changelog) +- `ci:` - CI changes (excluded from changelog) + +## Installation Script + +The `install.sh` script automatically downloads the latest release: + +```bash +curl -sSL https://raw.githubusercontent.com/yarlson/lnk/main/install.sh | bash +``` + +## Troubleshooting + +### Release failed to create + +1. Check that the tag follows the format `vX.Y.Z` +2. Ensure GitHub Actions has proper permissions +3. Check the Actions log for detailed error messages + +### Missing binaries in release + +1. Verify GoReleaser configuration: `make goreleaser-check` +2. Test build locally: `make goreleaser-snapshot` +3. Check the build matrix in `.goreleaser.yml` + +### Changelog is empty + +1. Ensure commits follow conventional commit format +2. Check that there are commits since the last tag +3. Verify changelog configuration in `.goreleaser.yml` \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 5560762..2374e83 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,12 +7,24 @@ import ( "github.com/spf13/cobra" ) +var ( + version = "dev" + buildTime = "unknown" +) + var rootCmd = &cobra.Command{ Use: "lnk", Short: "Dotfiles, linked. No fluff.", Long: "Lnk is a minimalist CLI tool for managing dotfiles using symlinks and Git.", } +// SetVersion sets the version information for the CLI +func SetVersion(v, bt string) { + version = v + buildTime = bt + rootCmd.Version = fmt.Sprintf("%s (built %s)", version, buildTime) +} + func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..9b65ac1 --- /dev/null +++ b/install.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Lnk installer script +# Downloads and installs the latest release of lnk + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# GitHub repository +REPO="yarlson/lnk" +INSTALL_DIR="/usr/local/bin" +BINARY_NAME="lnk" + +# Detect OS and architecture +detect_platform() { + local os arch + + # Detect OS + case "$(uname -s)" in + Linux) os="Linux" ;; + Darwin) os="Darwin" ;; + MINGW*|MSYS*|CYGWIN*) os="Windows" ;; + *) + echo -e "${RED}Error: Unsupported operating system $(uname -s)${NC}" + exit 1 + ;; + esac + + # Detect architecture + case "$(uname -m)" in + x86_64|amd64) arch="x86_64" ;; + arm64|aarch64) arch="arm64" ;; + *) + echo -e "${RED}Error: Unsupported architecture $(uname -m)${NC}" + exit 1 + ;; + esac + + echo "${os}_${arch}" +} + +# Get the latest release version +get_latest_version() { + curl -s "https://api.github.com/repos/${REPO}/releases/latest" | \ + grep '"tag_name":' | \ + sed -E 's/.*"([^"]+)".*/\1/' +} + +# Download and install +install_lnk() { + local platform version + + echo -e "${BLUE}🔗 Installing lnk...${NC}" + + platform=$(detect_platform) + version=$(get_latest_version) + + if [ -z "$version" ]; then + echo -e "${RED}Error: Failed to get latest version${NC}" + exit 1 + fi + + echo -e "${BLUE}Latest version: ${version}${NC}" + echo -e "${BLUE}Platform: ${platform}${NC}" + + # Download URL + local filename="lnk_${platform}.tar.gz" + local url="https://github.com/${REPO}/releases/download/${version}/${filename}" + + echo -e "${BLUE}Downloading ${url}...${NC}" + + # Create temporary directory + local tmp_dir=$(mktemp -d) + cd "$tmp_dir" + + # Download the binary + if ! curl -sL "$url" -o "$filename"; then + echo -e "${RED}Error: Failed to download ${url}${NC}" + exit 1 + fi + + # Extract the binary + if ! tar -xzf "$filename"; then + echo -e "${RED}Error: Failed to extract ${filename}${NC}" + exit 1 + fi + + # Make binary executable + chmod +x "$BINARY_NAME" + + # Install to system directory + echo -e "${YELLOW}Installing to ${INSTALL_DIR} (requires sudo)...${NC}" + if ! sudo mv "$BINARY_NAME" "$INSTALL_DIR/"; then + echo -e "${RED}Error: Failed to install binary${NC}" + exit 1 + fi + + # Cleanup + cd - > /dev/null + rm -rf "$tmp_dir" + + echo -e "${GREEN}✅ lnk installed successfully!${NC}" + echo -e "${GREEN}Run 'lnk --help' to get started.${NC}" +} + +# Check if running with --help +if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then + echo "Lnk installer script" + echo "" + echo "Usage: curl -sSL https://raw.githubusercontent.com/yarlson/lnk/main/install.sh | bash" + echo "" + echo "This script will:" + echo " 1. Detect your OS and architecture" + echo " 2. Download the latest lnk release" + echo " 3. Install it to /usr/local/bin (requires sudo)" + exit 0 +fi + +# Run the installer +install_lnk \ No newline at end of file diff --git a/main.go b/main.go index 652c17b..e091dde 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,13 @@ package main import "github.com/yarlson/lnk/cmd" +// These variables are set by GoReleaser via ldflags +var ( + version = "dev" + buildTime = "unknown" +) + func main() { + cmd.SetVersion(version, buildTime) cmd.Execute() }