Skip to content

Implement automated release.yml workflow with cross-platform builds and Go installers #2

@2bigO

Description

@2bigO

Complete release.yml Workflow Implementation Guide

📋 Installer Architecture Reference: installer/README.md - Read this first for complete context on installation system, versioned paths, and build process.

Context & Problem

dotvibe is a developer toolbox that provides coding agents with superpowers. Currently, our release process is manual and painful:

  • Users want simple GitHub releases with platform-specific installers that just work
  • Developers want version bumps to automatically trigger releases
  • Maintainers are manually building cross-platform binaries and installers

Installation Architecture

Two-Layer System:

  1. Deno Binary: Core CLI compiled with deno compile (handles all logic)
  2. Go Installer: Downloads binary + WASM assets, sets up environment

User Installation Flow:

# What users do:
# 1. Visit https://github.com/vhybzOS/dotvibe/releases
# 2. Download install-dotvibe-{platform}-{arch} for their OS
# 3. Run the installer

./install-dotvibe-linux-amd64

# Behind the scenes (installer does):
# 1. Downloads vibe-{version}-{platform}-{arch} from same release
# 2. Downloads tree-sitter-typescript.wasm from unpkg.com
# 3. Sets up ~/.vibe/data/ directory structure  
# 4. Creates symlinks, adds to PATH

Developer Experience:

# Local development & testing:
./install-dotvibe-linux-amd64 --dev # Symlinks local ./vibe to PATH
vibe index src/                     # Now uses local development binary

# What should trigger everything:
git commit -m "bump: v0.7.29" deno.json
git push

# Should automatically result in:
# - Cross-platform builds (Linux, macOS, Windows)  
# - Go installers for all platforms
# - GitHub release with all assets
# - Users can immediately download new version

Solution Overview

Implement automated release workflow that detects version changes in deno.json, builds cross-platform binaries, creates Go installers, and publishes GitHub releases with comprehensive error handling and validation.

🔗 Integration with Issue #1: This release pipeline provides the vibe-{version}-{platform}-{arch} binaries that the modern installer architecture (#1) expects to download from GitHub releases. The two issues are complementary:

📁 Unified Installation Structure: Both issues now use consistent versioned directories where vibe and data/ are co-located, enabling simple ./data/ relative path resolution in all environments (development and production).

Workflow Architecture

flowchart TD
    A[Version Change in deno.json] --> B[detect-version]
    B --> C{Version Changed?}
    C -->|No| D[Skip Release]
    C -->|Yes| E[build-binaries]
    
    E --> F[Linux Build]
    E --> G[macOS Build] 
    E --> H[Windows Build]
    
    F --> I[Upload Linux Artifact]
    G --> J[Upload macOS Artifact]
    H --> K[Upload Windows Artifact]
    
    I --> L[build-installers]
    J --> L
    K --> L
    
    L --> M[Download All Artifacts]
    M --> N[Build Linux Installer]
    M --> O[Build macOS Installer]
    M --> P[Build Windows Installer]
    
    N --> Q[create-release]
    O --> Q
    P --> Q
    
    Q --> R[Create GitHub Release]
    R --> S[Upload All Assets]
    S --> T[Validate Release]
    T --> U[Notify Success]
Loading

Directory Structure

dotvibe/
├── .github/workflows/
│   ├── release.yml          # 🎯 Main implementation target
│   └── docs.yml             # Reference for action patterns
├── scripts/
│   └── build-all-platforms.sh  # Cross-platform Deno builds (existing)
├── installer/
│   ├── Taskfile.yml         # Go installer build tasks (updated for new architecture)
│   ├── main.go              # Modern binary download installer (rewritten)
│   ├── elevate.go           # Privilege detection and elevation logic
│   ├── paths.go             # Cross-platform path management
│   ├── download.go          # Binary download with SHA256 validation
│   ├── go.mod               # Go dependencies
│   ├── installer_test.go    # Updated tests for new architecture
│   └── dist/                # Generated installers
├── tests/
│   ├── integration/         # End-to-end tests
│   └── unit/                # Component tests
├── data/                    # Assets included in builds
│   ├── .gitignore           # Ignores everything but README.md
│   └── README.md            # Installation docs
├── deno.json               # Version source & build config
└── build/                  # Generated binaries
    ├── vibe-linux-amd64
    ├── vibe-darwin-amd64
    └── vibe-windows-amd64.exe

Job Breakdown & Implementation

Job 1: detect-version

Purpose: Extract version from deno.json, compare with latest release, set outputs
Outputs: version, should_release
Dependencies: None

detect-version:
  runs-on: ubuntu-latest
  outputs:
    version: ${{ steps.version.outputs.version }}
    should_release: ${{ steps.check.outputs.should_release }}
  steps:
    - uses: actions/checkout@v4
    - name: Extract version
      id: version
      run: echo "version=$(jq -r '.version' deno.json)" >> $GITHUB_OUTPUT
    - name: Check if release needed
      id: check
      run: |
        # Compare with latest GitHub release
        # Set should_release=true if version is new

Job 2: build-binaries

Purpose: Cross-platform binary compilation using deno compile
Strategy: Matrix build (Linux, macOS, Windows)
Dependencies: detect-version
Artifacts: Platform-specific binaries

build-binaries:
  needs: detect-version
  if: needs.detect-version.outputs.should_release == 'true'
  strategy:
    matrix:
      include:
        - os: ubuntu-latest
          target: x86_64-unknown-linux-gnu
          artifact: vibe-linux-amd64
        - os: macos-latest
          target: x86_64-apple-darwin
          artifact: vibe-darwin-amd64
        - os: windows-latest
          target: x86_64-pc-windows-msvc
          artifact: vibe-windows-amd64.exe

Job 3: build-installers

Purpose: Generate Go-based installers for all platforms
Dependencies: detect-version, build-binaries
Artifacts: Platform-specific installers

Job 4: create-release

Purpose: Create GitHub release with all assets
Dependencies: All previous jobs
Assets: Binaries + Installers + Checksums

Required Implementation Details

1. Workflow Structure

name: Release
on:
  push:
    branches: [main]
    paths: ['deno.json']
  workflow_dispatch:

jobs:
  detect-version:
    # Extract version from deno.json, set as output
    # Skip if version unchanged from latest release
    
  build-binaries:
    needs: detect-version
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        include:
          - os: ubuntu-latest
            target: linux-amd64
          - os: macos-latest  
            target: darwin-amd64
          - os: windows-latest
            target: windows-amd64
    # Use deno compile commands from scripts/build.ts
    # Upload artifacts with consistent naming
    
  build-installers:
    needs: [detect-version, build-binaries]
    # Use go run installer/cmd/build.go commands
    # Download all binary artifacts first
    # Build installers for all platforms
    
  create-release:
    needs: [detect-version, build-binaries, build-installers]
    # Create GitHub release with all assets
    # Use version from detect-version job

2. Integration Points

Deno Build System: Reference deno.json tasks and scripts/build-all-platforms.sh

  • Use existing deno task build:cross-platform command
  • Deno compile commands already configured with correct flags
  • Data directory inclusion patterns already set

Go Installer System: Reference installer/Taskfile.yml (post-Issue #1 rewrite)

  • Use task build:all command from installer directory
  • Modern binary download architecture (no more Rust/Cargo dependencies)
  • Privilege-aware installation with elevate.go and paths.go
  • SHA256 validation via download.go
  • Downloads vibe binaries from GitHub releases + external dependencies

Testing: Reference tests/ directory and deno.json tasks

  • Pre-build validation steps
  • Binary smoke tests
  • Installation verification

3. Critical Implementation Specifics

Version Detection:

# Extract version from deno.json
jq -r '.version' deno.json
# Compare with latest GitHub release tag
# Set skip=true if versions match

Artifact Naming Convention:

Binaries: vibe-v{version}-{platform}-{arch}
Installers: install-dotvibe-{platform}-{arch}

🔗 Note: The binary naming pattern vibe-{version}-{platform}-{arch} matches exactly what the modern installer architecture (Issue #1) expects to download. The installer will use GitHub API to detect the latest version and download the appropriate binary for the user's platform.

📁 Installation Structure: The installer places vibe and data/ in the same versioned directory (e.g., /usr/local/dotvibe/0.9.0/), ensuring consistent ./data/ relative paths in both development and production environments.

Environment Requirements:

  • Deno 2.1.4+ (from deno.json)
  • Go 1.21+ (from installer/go.mod)
  • GitHub token with release permissions

Cross-Platform Build Commands:

# Linux/macOS
deno compile --allow-all --target x86_64-unknown-linux-gnu

# Windows  
deno compile --allow-all --target x86_64-pc-windows-msvc

# Include data directory for all platforms
--include data/

4. Error Handling Strategy

  • Build failures: Fail fast, don't create partial releases
  • Version conflicts: Skip if version already released
  • Asset validation: Verify file sizes and executability
  • Rollback: Delete release if any step fails after creation

5. Dependencies & Actions

Required GitHub Actions:

  • actions/checkout@v4
  • denoland/setup-deno@v2
  • actions/setup-go@v5
  • actions/upload-artifact@v4
  • actions/download-artifact@v4
  • gh CLI for release management

Caching Strategy:

  • Deno dependencies: ~/.cache/deno
  • Go modules: ~/go/pkg/mod

6. Security Considerations

  • Use GITHUB_TOKEN for releases
  • Validate all artifacts before upload
  • Generate checksums for verification
  • Sign binaries if possible

7. Testing Integration

Pre-release validation:

# Run full test suite
deno task test

# Verify build system
deno task build

# Test installer generation
task build:installers

Post-build verification:

# Test binary execution
./build/vibe --version

# Verify installer functionality
./installer/dist/install-dotvibe-linux-amd64 --help

Implementation Checklist

  • Create data/.gitignore with *.wasm pattern (prevent binary assets in git)
  • Create .github/workflows/release.yml
  • Implement version detection logic
  • Set up cross-platform build matrix
  • Configure artifact upload/download
  • Integrate Go installer build
  • Add release creation step
  • Test installer --dev mode workflow (symlinks local binary)
  • Test with manual workflow dispatch
  • Validate with version bump

Critical Setup: Data Directory

Why data/.gitignore is essential:

  • WASM files are downloaded by Go installer at install time (installer/modules.go)
  • Binary assets (like tree-sitter-typescript.wasm) shouldn't be committed to git
  • Prevents accidental commits of large downloaded files during development

Required content for data/.gitignore:

# Ignore everything in data/ directory
*

# But keep README.md for documentation
!README.md

Development Workflow Integration

Developer --dev Mode:
The Go installer supports a --dev flag that enables local development:

# Setup: Point system to use local development binary
./install-dotvibe-linux-amd64 --dev

# What --dev does:
# 1. Detects ./vibe executable in current directory (repo clone)
# 2. Downloads tree-sitter-typescript.wasm to ./data/ (in repo)
# 3. Symlinks ./vibe (local repo binary) to ~/.local/bin/vibe
# 4. Adds ~/.local/bin to PATH if needed

# Result: Developers use vibe normally but execute local code
vibe index src/           # Uses ./vibe from repo location via symlink
vibe query "functions"    # Same local binary, ./data/ assets available

Developer Mode (--dev) Implementation

Purpose: Enable developers to use local development builds with production installer infrastructure. Assumes knowledge of installer architecture.

Prerequisites Validation

// main.go - Dev mode detection
func validateDevMode() error {
    // Check for ./vibe executable
    if _, err := os.Stat("./vibe"); os.IsNotExist(err) {
        return fmt.Errorf("./vibe not found. Run: deno task build")
    }
    
    // Verify repository structure
    requiredPaths := []string{"deno.json", "src/", "installer/"}
    for _, path := range requiredPaths {
        if _, err := os.Stat(path); os.IsNotExist(err) {
            return fmt.Errorf("not in dotvibe repository: missing %s", path)
        }
    }
    
    // Test executable works
    cmd := exec.Command("./vibe", "--version")
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("./vibe executable failed: %v", err)
    }
    return nil
}

WASM Asset Management

// Download only tree-sitter WASM to ./data/ (skip other binaries)
func setupDevAssets() error {
    dataDir := "./data"
    os.MkdirAll(dataDir, 0755)
    
    wasmPath := filepath.Join(dataDir, "tree-sitter-typescript.wasm")
    
    // Skip if exists and valid checksum
    if isValidWASM(wasmPath) {
        log.Printf("WASM file already exists: %s", wasmPath)
        return nil
    }
    
    // Download with same security validation as production
    return downloadAndValidate(
        "https://unpkg.com/tree-sitter-typescript@0.23.2/tree-sitter-typescript.wasm",
        wasmPath,
        TREE_SITTER_WASM_SHA256,
    )
}

Symlink Strategy Differences

// Dev mode uses absolute paths to current repo
func createDevSymlinks() error {
    currentDir, _ := os.Getwd()
    vibeSource := filepath.Join(currentDir, "vibe")
    
    // Unix: ~/.local/bin/vibe → /absolute/path/to/repo/vibe
    homeDir, _ := os.UserHomeDir()
    localBin := filepath.Join(homeDir, ".local", "bin")
    vibeTarget := filepath.Join(localBin, "vibe")
    
    // Backup existing if production install detected
    if isProductionInstall(vibeTarget) {
        backupPath := fmt.Sprintf("%s.backup.%d", vibeTarget, time.Now().Unix())
        os.Rename(vibeTarget, backupPath)
        log.Printf("Backed up production install: %s", backupPath)
    }
    
    os.MkdirAll(localBin, 0755)
    return os.Symlink(vibeSource, vibeTarget)
}

Conflict Resolution

// Detect and handle existing installations
func detectInstallConflicts() error {
    conflicts := []string{}
    
    // Check for production installs
    productionPaths := []string{
        "/usr/local/dotvibe/",
        filepath.Join(os.Getenv("HOME"), ".local/dotvibe/"),
    }
    
    for _, path := range productionPaths {
        if _, err := os.Stat(path); err == nil {
            conflicts = append(conflicts, path)
        }
    }
    
    if len(conflicts) > 0 {
        fmt.Printf("⚠️  Existing installations detected:\n")
        for _, path := range conflicts {
            fmt.Printf("   - %s\n", path)
        }
        fmt.Printf("Continue with dev mode? [y/N]: ")
        // Handle user input...
    }
    return nil
}

Error Handling & Recovery

# Common dev mode errors and solutions
./install-dotvibe --dev --verbose  # Debug mode
./install-dotvibe --dev --force    # Override conflicts
./install-dotvibe --dev --dry-run  # Preview actions

# Error scenarios:
# - Missing ./vibe → "Run: deno task build"
# - Permission denied → "Try: chmod +x ./vibe"
# - Symlink exists → "Backup and replace? [y/N]"
# - PATH missing → "Add ~/.local/bin to PATH"

CI/CD Integration Requirements

# .github/workflows/release.yml integration
test-dev-mode:
  strategy:
    matrix:
      os: [ubuntu-latest, macos-latest, windows-latest]
  steps:
    - name: Test dev mode installation
      run: |
        deno task build
        ./install-dotvibe-${{ matrix.platform }} --dev --verbose
        vibe --version  # Should use local build
        vibe index src/ --limit 1  # Smoke test WASM
        
    - name: Test dev mode uninstall
      run: |
        ./install-dotvibe-${{ matrix.platform }} --uninstall
        ! which vibe  # Should be removed from PATH

Uninstall Behavior

// Dev mode uninstall preserves repo, restores backups
func uninstallDevMode() error {
    // Remove symlinks only, preserve ./data/ and ./vibe
    homeDir, _ := os.UserHomeDir()
    vibeLink := filepath.Join(homeDir, ".local", "bin", "vibe")
    
    // Restore backup if it exists
    backupPattern := vibeLink + ".backup.*"
    if backups, _ := filepath.Glob(backupPattern); len(backups) > 0 {
        latest := findLatestBackup(backups)
        os.Rename(latest, vibeLink)
        log.Printf("Restored backup: %s", latest)
    } else {
        os.Remove(vibeLink)
    }
    
    // Don't touch ./data/ - developer might be using it
    log.Printf("Dev mode uninstalled. Local ./data/ preserved.")
    return nil
}

References

  • Deno Build: deno.json tasks, scripts/build-all-platforms.sh
  • Go Installer: installer/Taskfile.yml, installer/main.go (post-Issue Modern Installer Architecture: Privilege-Aware Binary Distribution #1 modern architecture)
  • Test Suite: tests/, deno.json tasks
  • Configuration: deno.json, installer/go.mod
  • Current CI: .github/workflows/docs.yml (for action patterns)

Success Criteria

  1. Automated: Triggers on version changes in deno.json
  2. Complete: Builds all platforms + installers
  3. Reliable: Handles errors gracefully
  4. Fast: Parallel builds, efficient caching
  5. Validated: All assets tested before release

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions