feat: implement CI/CD pipeline and release automation

This commit is contained in:
Yar Kravtsov
2025-05-24 09:20:43 +03:00
parent 905d88e0cf
commit 398d011270
10 changed files with 593 additions and 4 deletions

84
.github/workflows/ci.yml vendored Normal file
View File

@@ -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

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

@@ -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 }}

40
.github/workflows/validate.yml vendored Normal file
View File

@@ -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

5
.gitignore vendored
View File

@@ -40,4 +40,7 @@ desktop.ini
# Temporary files
*.tmp
*.log
*.log
# GoReleaser artifacts
goreleaser/

129
.goreleaser.yml Normal file
View File

@@ -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}}"

View File

@@ -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

131
RELEASE.md Normal file
View File

@@ -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_<platform>/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`

View File

@@ -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)

126
install.sh Normal file
View File

@@ -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

View File

@@ -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()
}