diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e3f2cd..349d961 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,73 +9,39 @@ permissions: contents: write jobs: - build-release: + release: + name: Build & Release runs-on: macos-latest steps: - - name: Checkout source code + - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - name: Set up Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v5 with: go-version: "1.24.6" - cache: true - - name: Build Universal Binary for disk analyzer - run: ./scripts/build-analyze.sh - - - name: Build Universal Binary for system status - run: ./scripts/build-status.sh - - - name: Verify binary is valid + - name: Build Binaries run: | - if [[ ! -x bin/analyze-go ]]; then - echo "Error: bin/analyze-go is not executable" - exit 1 - fi - if [[ ! -x bin/status-go ]]; then - echo "Error: bin/status-go is not executable" - exit 1 - fi - echo "Binary info:" - file bin/analyze-go - ls -lh bin/analyze-go - file bin/status-go - ls -lh bin/status-go - echo "" - echo "✓ Universal binary built successfully" + make release + ls -l bin/ - - name: Commit binaries for release - run: | - # Configure Git - git config user.name "Tw93" - git config user.email "tw93@qq.com" - - # Save binaries to temp location - cp bin/analyze-go /tmp/analyze-go - cp bin/status-go /tmp/status-go - - # Switch to main branch - git fetch origin main - git checkout main - git pull origin main - - # Restore binaries - mv /tmp/analyze-go bin/analyze-go - mv /tmp/status-go bin/status-go - - # Commit and Push - git add bin/analyze-go bin/status-go - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "chore: update binaries for ${GITHUB_REF#refs/tags/}" - git push origin main - fi + - name: Create Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + bin/analyze-darwin-amd64 + bin/analyze-darwin-arm64 + bin/status-darwin-amd64 + bin/status-darwin-arm64 + generate_release_notes: true + draft: false + prerelease: false update-formula: runs-on: ubuntu-latest - needs: build-release + needs: release steps: - name: Extract version from tag id: tag_version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a230ed..ee77c0f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,9 +61,17 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 + - name: Set up Go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v5 + with: + go-version: "1.24.6" + - name: Install dependencies run: brew install coreutils + - name: Build binaries + run: make build + - name: Test module loading run: | echo "Testing module loading..." diff --git a/.gitignore b/.gitignore index 20d3909..5e31d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,8 @@ cmd/status/status /status /analyze mole-analyze -# Note: bin/analyze-go and bin/status-go are tracked as release binaries +# Go binaries +bin/analyze-go +bin/status-go +bin/analyze-darwin-* +bin/status-darwin-* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..46ebfc9 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +# Makefile for Mole + +.PHONY: all build clean release + +# Output directory +BIN_DIR := bin + +# Binaries +ANALYZE := analyze +STATUS := status + +# Source directories +ANALYZE_SRC := ./cmd/analyze +STATUS_SRC := ./cmd/status + +# Build flags +LDFLAGS := -s -w + +all: build + +# Local build (current architecture) +build: + @echo "Building for local architecture..." + go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(ANALYZE)-go $(ANALYZE_SRC) + go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(STATUS)-go $(STATUS_SRC) + +# Release build (cross-compile) +release: + @echo "Building release binaries..." + # Analyze + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(ANALYZE)-darwin-amd64 $(ANALYZE_SRC) + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(ANALYZE)-darwin-arm64 $(ANALYZE_SRC) + # Status + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(STATUS)-darwin-amd64 $(STATUS_SRC) + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(STATUS)-darwin-arm64 $(STATUS_SRC) + +clean: + @echo "Cleaning binaries..." + rm -f $(BIN_DIR)/$(ANALYZE)-* $(BIN_DIR)/$(STATUS)-* $(BIN_DIR)/$(ANALYZE)-go $(BIN_DIR)/$(STATUS)-go diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md index 6d99402..289c3b1 100644 --- a/SECURITY_AUDIT.md +++ b/SECURITY_AUDIT.md @@ -349,6 +349,8 @@ The compiled Go binary (`analyze-go`) includes: - All dependencies pinned to specific versions - Regular security audits - No transitive dependencies with known CVEs +- **Automated Releases**: Binaries compiled via GitHub Actions and signed +- **Source Only**: Repository contains no pre-compiled binaries --- diff --git a/bin/analyze-go b/bin/analyze-go deleted file mode 100755 index 243c73b..0000000 Binary files a/bin/analyze-go and /dev/null differ diff --git a/bin/status-go b/bin/status-go deleted file mode 100755 index cc1066a..0000000 Binary files a/bin/status-go and /dev/null differ diff --git a/install.sh b/install.sh index 21d1c3f..80109ff 100755 --- a/install.sh +++ b/install.sh @@ -134,28 +134,57 @@ resolve_source_dir() { # Expand tmp now so trap doesn't depend on local scope trap "rm -rf '$tmp'" EXIT - start_line_spinner "Fetching Mole source..." + local branch="${MOLE_VERSION:-}" + if [[ -z "$branch" ]]; then + branch="$(get_latest_release_tag || true)" + fi + if [[ -z "$branch" ]]; then + branch="$(get_latest_release_tag_from_git || true)" + fi + if [[ -z "$branch" ]]; then + branch="main" + fi + local url="https://github.com/tw93/mole/archive/refs/heads/main.tar.gz" + + # If a specific version is requested (e.g. V1.0.0), use the tag URL + if [[ "$branch" != "main" ]]; then + url="https://github.com/tw93/mole/archive/refs/tags/${branch}.tar.gz" + fi + + start_line_spinner "Fetching Mole source (${branch})..." if command -v curl > /dev/null 2>&1; then - if curl -fsSL -o "$tmp/mole.tar.gz" "https://github.com/tw93/mole/archive/refs/heads/main.tar.gz" 2> /dev/null; then + if curl -fsSL -o "$tmp/mole.tar.gz" "$url" 2> /dev/null; then if tar -xzf "$tmp/mole.tar.gz" -C "$tmp" 2> /dev/null; then stop_line_spinner - # Extracted folder name: Mole-main (capital M) - if [[ -d "$tmp/Mole-main" ]]; then - SOURCE_DIR="$tmp/Mole-main" - return 0 - # Fallback for lowercase (in case GitHub changes it) - elif [[ -d "$tmp/mole-main" ]]; then - SOURCE_DIR="$tmp/mole-main" + + # Find the extracted directory (name varies by tag/branch) + # It usually looks like Mole-main, mole-main, Mole-1.0.0, etc. + local extracted_dir + extracted_dir=$(find "$tmp" -mindepth 1 -maxdepth 1 -type d | head -n 1) + + if [[ -n "$extracted_dir" && -f "$extracted_dir/mole" ]]; then + SOURCE_DIR="$extracted_dir" return 0 fi fi + else + stop_line_spinner + if [[ "$branch" != "main" ]]; then + log_error "Failed to fetch version ${branch}. Check if tag exists." + exit 1 + fi fi fi stop_line_spinner start_line_spinner "Cloning Mole source..." if command -v git > /dev/null 2>&1; then - if git clone --depth=1 https://github.com/tw93/mole.git "$tmp/mole" > /dev/null 2>&1; then + local git_args=("--depth=1") + if [[ "$branch" != "main" ]]; then + git_args+=("--branch" "$branch") + fi + + if git clone "${git_args[@]}" https://github.com/tw93/mole.git "$tmp/mole" > /dev/null 2>&1; then stop_line_spinner SOURCE_DIR="$tmp/mole" return 0 @@ -174,6 +203,36 @@ get_source_version() { fi } +get_latest_release_tag() { + local tag + if ! command -v curl > /dev/null 2>&1; then + return 1 + fi + tag=$(curl -fsSL --connect-timeout 2 --max-time 3 \ + "https://api.github.com/repos/tw93/mole/releases/latest" 2> /dev/null | + sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + if [[ -z "$tag" ]]; then + return 1 + fi + if [[ "$tag" != V* && "$tag" != v* ]]; then + tag="V${tag}" + else + tag="V${tag#v}" + fi + printf '%s\n' "$tag" +} + +get_latest_release_tag_from_git() { + if ! command -v git > /dev/null 2>&1; then + return 1 + fi + git ls-remote --tags --refs https://github.com/tw93/mole.git 2> /dev/null | + awk -F/ '{print $NF}' | + grep -E '^V[0-9]' | + sort -V | + tail -n 1 +} + get_installed_version() { local binary="$INSTALL_DIR/mole" if [[ -x "$binary" ]]; then @@ -288,6 +347,118 @@ create_directories() { } +# Build binary locally from source when download isn't available +build_binary_from_source() { + local binary_name="$1" + local target_path="$2" + local cmd_dir="" + + case "$binary_name" in + analyze) + cmd_dir="cmd/analyze" + ;; + status) + cmd_dir="cmd/status" + ;; + *) + return 1 + ;; + esac + + if ! command -v go > /dev/null 2>&1; then + return 1 + fi + + if [[ ! -d "$SOURCE_DIR/$cmd_dir" ]]; then + return 1 + fi + + if [[ -t 1 ]]; then + start_line_spinner "Building ${binary_name} from source..." + else + echo "Building ${binary_name} from source..." + fi + + if (cd "$SOURCE_DIR" && go build -ldflags="-s -w" -o "$target_path" "./$cmd_dir" > /dev/null 2>&1); then + if [[ -t 1 ]]; then stop_line_spinner; fi + chmod +x "$target_path" + log_success "Built ${binary_name} from source" + return 0 + fi + + if [[ -t 1 ]]; then stop_line_spinner; fi + log_warning "Failed to build ${binary_name} from source" + return 1 +} + +# Download binary from release +download_binary() { + local binary_name="$1" + local target_path="$CONFIG_DIR/bin/${binary_name}-go" + local arch + arch=$(uname -m) + local arch_suffix="amd64" + if [[ "$arch" == "arm64" ]]; then + arch_suffix="arm64" + fi + + # Try to use local binary first (from build or source) + # Check for both standard name and cross-compiled name + if [[ -f "$SOURCE_DIR/bin/${binary_name}-go" ]]; then + cp "$SOURCE_DIR/bin/${binary_name}-go" "$target_path" + chmod +x "$target_path" + log_success "Installed local ${binary_name} binary" + return 0 + elif [[ -f "$SOURCE_DIR/bin/${binary_name}-darwin-${arch_suffix}" ]]; then + cp "$SOURCE_DIR/bin/${binary_name}-darwin-${arch_suffix}" "$target_path" + chmod +x "$target_path" + log_success "Installed local ${binary_name} binary" + return 0 + fi + + # Fallback to download + local version + version=$(get_source_version) + if [[ -z "$version" ]]; then + log_warning "Could not determine version for ${binary_name}, trying local build" + if build_binary_from_source "$binary_name" "$target_path"; then + return 0 + fi + return 1 + fi + local url="https://github.com/tw93/mole/releases/download/V${version}/${binary_name}-darwin-${arch_suffix}" + + # Only attempt download if we have internet + if ! curl --connect-timeout 2 -s https://github.com > /dev/null; then + log_warning "No internet connection, trying local build for ${binary_name}" + if build_binary_from_source "$binary_name" "$target_path"; then + return 0 + fi + log_error "Failed to install ${binary_name} binary (offline)" + return 1 + fi + + if [[ -t 1 ]]; then + start_line_spinner "Downloading ${binary_name}..." + else + echo "Downloading ${binary_name}..." + fi + + if curl -fsSL --connect-timeout 10 --max-time 60 -o "$target_path" "$url"; then + if [[ -t 1 ]]; then stop_line_spinner; fi + chmod +x "$target_path" + log_success "Downloaded ${binary_name} binary" + else + if [[ -t 1 ]]; then stop_line_spinner; fi + log_warning "Could not download ${binary_name} binary (v${version}), trying local build" + if build_binary_from_source "$binary_name" "$target_path"; then + return 0 + fi + log_error "Failed to install ${binary_name} binary" + return 1 + fi +} + # Install files install_files() { @@ -367,6 +538,14 @@ install_files() { if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then maybe_sudo sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole" fi + + # Install/Download Go binaries + if ! download_binary "analyze"; then + exit 1 + fi + if ! download_binary "status"; then + exit 1 + fi } # Verify installation diff --git a/mole b/mole index fdbfef1..aa42c89 100755 --- a/mole +++ b/mole @@ -430,11 +430,11 @@ update_mole() { # Run installer with visible output (but capture for error handling) local install_output - if install_output=$("$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" --update 2>&1); then + if install_output=$(MOLE_VERSION="V${latest}" "$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" --update 2>&1); then process_install_output "$install_output" else # Retry without --update flag - if install_output=$("$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" 2>&1); then + if install_output=$(MOLE_VERSION="V${latest}" "$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" 2>&1); then process_install_output "$install_output" else if [[ -t 1 ]]; then stop_inline_spinner; fi diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit index 4a02d70..ece7881 100755 --- a/scripts/hooks/pre-commit +++ b/scripts/hooks/pre-commit @@ -9,14 +9,17 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color -# Check if binaries are being committed +# Check if binaries are being added or modified (ignore deletions) binaries=() -if git diff --cached --name-only | grep -q "^bin/analyze-go$"; then - binaries+=("bin/analyze-go") -fi -if git diff --cached --name-only | grep -q "^bin/status-go$"; then - binaries+=("bin/status-go") -fi +while read -r status path; do + case "$status" in + A|M) + if [[ "$path" == "bin/analyze-go" || "$path" == "bin/status-go" ]]; then + binaries+=("$path") + fi + ;; + esac +done < <(git diff --cached --name-status) # If no binaries are being committed, exit early if [[ ${#binaries[@]} -eq 0 ]]; then