ci.yml
1 name: CI 2 3 on: 4 push: 5 branches: [main, 'epic/*', 'feature/*'] 6 pull_request: 7 branches: [main] 8 9 # Path filters for conditional job execution 10 # Note: GitHub Actions doesn't support per-job path filters natively, 11 # so we use dorny/paths-filter action within jobs that need it 12 13 jobs: 14 # ============================================================================ 15 # Core Checks - Run on every commit (fast: ~2 min) 16 # ============================================================================ 17 check-all: 18 strategy: 19 fail-fast: false 20 matrix: 21 os: [ubuntu-latest, macos-latest, windows-latest] 22 23 runs-on: ${{ matrix.os }} 24 timeout-minutes: 15 25 26 steps: 27 - name: Checkout 28 uses: actions/checkout@v4 29 timeout-minutes: 2 30 31 - name: Setup Node.js 32 uses: actions/setup-node@v4 33 with: 34 node-version: 20 35 cache: 'npm' 36 timeout-minutes: 3 37 38 - name: Install dependencies 39 run: npm ci 40 timeout-minutes: 5 41 42 - name: Run lint 43 run: npm run lint 44 timeout-minutes: 2 45 46 - name: Run typecheck 47 run: npm run typecheck 48 timeout-minutes: 3 49 50 - name: Run tests 51 run: npm run test 52 timeout-minutes: 5 53 54 # ============================================================================ 55 # Path Filter - Determine which jobs need to run 56 # ============================================================================ 57 changes: 58 runs-on: ubuntu-latest 59 outputs: 60 install-scripts: ${{ steps.filter.outputs.install-scripts }} 61 steps: 62 - uses: actions/checkout@v4 63 - uses: dorny/paths-filter@v3 64 id: filter 65 with: 66 filters: | 67 install-scripts: 68 - 'install.sh' 69 - 'install.ps1' 70 - '.github/workflows/ci.yml' 71 72 # ============================================================================ 73 # Install Script Syntax Validation (only when install scripts change) 74 # ============================================================================ 75 install-script-syntax: 76 needs: changes 77 if: ${{ needs.changes.outputs.install-scripts == 'true' }} 78 runs-on: ubuntu-latest 79 timeout-minutes: 5 80 steps: 81 - uses: actions/checkout@v4 82 83 - name: Verify install.sh syntax 84 run: bash -n install.sh 85 86 - name: Verify install.ps1 syntax 87 run: | 88 # Use pwsh to check PowerShell syntax 89 pwsh -Command "[System.Management.Automation.Language.Parser]::ParseFile('install.ps1', [ref]\$null, [ref]\$null)" > /dev/null 90 echo "install.ps1 syntax OK" 91 92 - name: Test --ci flag is recognized 93 run: | 94 # Test that --ci flag prevents the non-interactive exit 95 output=$(timeout 5 bash install.sh --ci 2>&1 || true) 96 97 if echo "$output" | grep -q "NON-INTERACTIVE MODE NOT SUPPORTED"; then 98 echo "ERROR: --ci flag not working - script still requires interactive mode" 99 exit 1 100 fi 101 102 if echo "$output" | grep -q "Running in CI mode"; then 103 echo "SUCCESS: --ci flag recognized" 104 else 105 echo "WARNING: CI mode message not found, but non-interactive error also not present" 106 fi 107 108 echo "Output preview:" 109 echo "$output" | head -20 110 111 # ============================================================================ 112 # Group A: Installation & Tool Verification (only when install scripts change) 113 # ============================================================================ 114 install-verify-linux: 115 needs: changes 116 if: ${{ needs.changes.outputs.install-scripts == 'true' }} 117 runs-on: ubuntu-latest 118 timeout-minutes: 25 119 outputs: 120 rad-version: ${{ steps.verify.outputs.rad-version }} 121 steps: 122 - uses: actions/checkout@v4 123 124 - name: Cache Radicle 125 id: cache-radicle 126 uses: actions/cache@v4 127 with: 128 path: ~/.radicle 129 key: radicle-linux-${{ hashFiles('install.sh') }}-v2 130 restore-keys: radicle-linux- 131 132 - name: Cache Ollama models 133 id: cache-ollama 134 uses: actions/cache@v4 135 with: 136 path: ~/.ollama/models 137 key: ollama-nomic-embed-text-v1 138 139 - name: Run install script 140 run: | 141 bash install.sh --ci --alias "CI-Linux-${{ github.run_id }}" --passphrase "ci-test-pass" 142 env: 143 RAD_PASSPHRASE: ci-test-pass 144 145 - name: Verify installed tools 146 id: verify 147 run: | 148 echo "=== Group A: Tool Verification ===" 149 150 # A.1: Git 151 echo "A.1 Git:" 152 git --version || { echo "FAIL: Git not found"; exit 1; } 153 154 # A.2: GitHub CLI (check presence, auth tested separately) 155 echo "A.2 GitHub CLI:" 156 gh --version || { echo "FAIL: GitHub CLI not found"; exit 1; } 157 158 # A.3: Radicle CLI 159 echo "A.3 Radicle CLI:" 160 export PATH="$HOME/.radicle/bin:$PATH" 161 RAD_VERSION=$(rad --version 2>/dev/null || echo "NOT FOUND") 162 echo "rad-version=$RAD_VERSION" >> $GITHUB_OUTPUT 163 if [ "$RAD_VERSION" = "NOT FOUND" ]; then 164 echo "FAIL: Radicle CLI not found" 165 exit 1 166 fi 167 echo "Radicle: $RAD_VERSION" 168 169 # A.4: Radicle Identity 170 echo "A.4 Radicle Identity:" 171 RAD_DID=$(rad self --did 2>/dev/null || echo "NOT FOUND") 172 if [ "$RAD_DID" = "NOT FOUND" ]; then 173 echo "FAIL: Radicle identity not found" 174 exit 1 175 fi 176 echo "Identity: $RAD_DID" 177 178 # A.5: Ollama 179 echo "A.5 Ollama:" 180 if command -v ollama &> /dev/null; then 181 ollama --version || true 182 echo "Ollama installed" 183 else 184 echo "WARNING: Ollama not installed (optional)" 185 fi 186 187 # A.6: Obsidian (Linux - check Flatpak/Snap) 188 echo "A.6 Obsidian:" 189 if flatpak list 2>/dev/null | grep -q "md.obsidian.Obsidian"; then 190 echo "Obsidian installed via Flatpak" 191 elif snap list 2>/dev/null | grep -q "obsidian"; then 192 echo "Obsidian installed via Snap" 193 elif command -v obsidian &> /dev/null; then 194 echo "Obsidian found in PATH" 195 else 196 echo "FAIL: Obsidian not found" 197 exit 1 198 fi 199 200 echo "" 201 echo "=== All required tools verified ===" 202 203 install-verify-macos: 204 needs: changes 205 if: ${{ needs.changes.outputs.install-scripts == 'true' }} 206 runs-on: macos-latest 207 timeout-minutes: 30 208 outputs: 209 rad-version: ${{ steps.verify.outputs.rad-version }} 210 steps: 211 - uses: actions/checkout@v4 212 213 - name: Cache Radicle 214 id: cache-radicle 215 uses: actions/cache@v4 216 with: 217 path: ~/.radicle 218 key: radicle-macos-${{ hashFiles('install.sh') }}-v2 219 restore-keys: radicle-macos- 220 221 - name: Cache Ollama models 222 id: cache-ollama 223 uses: actions/cache@v4 224 with: 225 path: ~/.ollama/models 226 key: ollama-nomic-embed-text-v1 227 228 - name: Run install script 229 run: | 230 bash install.sh --ci --alias "CI-macOS-${{ github.run_id }}" --passphrase "ci-test-pass" 231 env: 232 RAD_PASSPHRASE: ci-test-pass 233 234 - name: Verify installed tools 235 id: verify 236 run: | 237 echo "=== Group A: Tool Verification ===" 238 239 # A.1: Git 240 echo "A.1 Git:" 241 git --version || { echo "FAIL: Git not found"; exit 1; } 242 243 # A.2: GitHub CLI 244 echo "A.2 GitHub CLI:" 245 gh --version || { echo "FAIL: GitHub CLI not found"; exit 1; } 246 247 # A.3: Radicle CLI 248 echo "A.3 Radicle CLI:" 249 export PATH="$HOME/.radicle/bin:$PATH" 250 RAD_VERSION=$(rad --version 2>/dev/null || echo "NOT FOUND") 251 echo "rad-version=$RAD_VERSION" >> $GITHUB_OUTPUT 252 if [ "$RAD_VERSION" = "NOT FOUND" ]; then 253 echo "FAIL: Radicle CLI not found" 254 exit 1 255 fi 256 echo "Radicle: $RAD_VERSION" 257 258 # A.4: Radicle Identity 259 echo "A.4 Radicle Identity:" 260 RAD_DID=$(rad self --did 2>/dev/null || echo "NOT FOUND") 261 if [ "$RAD_DID" = "NOT FOUND" ]; then 262 echo "FAIL: Radicle identity not found" 263 exit 1 264 fi 265 echo "Identity: $RAD_DID" 266 267 # A.5: Ollama 268 echo "A.5 Ollama:" 269 if command -v ollama &> /dev/null; then 270 ollama --version || true 271 echo "Ollama installed" 272 else 273 echo "WARNING: Ollama not installed (optional)" 274 fi 275 276 # A.6: Obsidian (macOS - check /Applications) 277 echo "A.6 Obsidian:" 278 if [ -d "/Applications/Obsidian.app" ]; then 279 echo "Obsidian installed at /Applications/Obsidian.app" 280 else 281 echo "FAIL: Obsidian not found" 282 exit 1 283 fi 284 285 echo "" 286 echo "=== All required tools verified ===" 287 288 install-verify-windows: 289 needs: changes 290 if: ${{ needs.changes.outputs.install-scripts == 'true' }} 291 runs-on: windows-latest 292 timeout-minutes: 30 293 steps: 294 - uses: actions/checkout@v4 295 296 - name: Cache Ollama models 297 id: cache-ollama 298 uses: actions/cache@v4 299 with: 300 path: ~\.ollama\models 301 key: ollama-nomic-embed-text-windows-v1 302 303 - name: Run install script 304 shell: pwsh 305 run: | 306 .\install.ps1 -CI 307 308 - name: Verify installed tools 309 shell: pwsh 310 run: | 311 $ErrorActionPreference = "Continue" 312 313 Write-Host "=== Group A: Tool Verification (Windows) ===" 314 315 # A.1: Git 316 Write-Host "A.1 Git:" 317 try { 318 git --version 319 } catch { 320 Write-Host "FAIL: Git not found" 321 exit 1 322 } 323 324 # A.2: GitHub CLI 325 Write-Host "A.2 GitHub CLI:" 326 try { 327 gh --version | Select-Object -First 1 328 } catch { 329 Write-Host "FAIL: GitHub CLI not found" 330 exit 1 331 } 332 333 # A.3: Radicle CLI (Windows has CLI but not node) 334 Write-Host "A.3 Radicle CLI:" 335 # Check if rad is in PATH or common locations 336 $radPaths = @( 337 "$env:USERPROFILE\.radicle\bin\rad.exe", 338 "$env:LOCALAPPDATA\Programs\Radicle\rad.exe" 339 ) 340 $radFound = $false 341 foreach ($radPath in $radPaths) { 342 if (Test-Path $radPath) { 343 $radVersion = & $radPath --version 2>&1 344 Write-Host "Radicle: $radVersion" 345 $radFound = $true 346 break 347 } 348 } 349 if (-not $radFound) { 350 # Try PATH 351 try { 352 $radVersion = rad --version 2>&1 353 Write-Host "Radicle: $radVersion" 354 $radFound = $true 355 } catch { 356 Write-Host "INFO: Radicle CLI not found (expected - Windows native build optional)" 357 } 358 } 359 360 # A.4: Radicle Identity (only if rad found) 361 if ($radFound) { 362 Write-Host "A.4 Radicle Identity:" 363 try { 364 $radDid = rad self --did 2>&1 365 Write-Host "Identity: $radDid" 366 } catch { 367 Write-Host "INFO: No Radicle identity (expected if rad not fully set up)" 368 } 369 } 370 371 # A.5: Ollama 372 Write-Host "A.5 Ollama:" 373 try { 374 ollama --version 375 Write-Host "Ollama installed" 376 } catch { 377 Write-Host "INFO: Ollama not installed (optional)" 378 } 379 380 # A.6: Obsidian (Windows - check common locations) 381 Write-Host "A.6 Obsidian:" 382 $obsidianPaths = @( 383 "$env:LOCALAPPDATA\Obsidian\Obsidian.exe", 384 "$env:LOCALAPPDATA\Programs\Obsidian\Obsidian.exe", 385 "$env:PROGRAMFILES\Obsidian\Obsidian.exe" 386 ) 387 $obsidianFound = $false 388 foreach ($path in $obsidianPaths) { 389 if (Test-Path $path) { 390 Write-Host "Obsidian installed at $path" 391 $obsidianFound = $true 392 break 393 } 394 } 395 if (-not $obsidianFound) { 396 Write-Host "FAIL: Obsidian not found" 397 exit 1 398 } 399 400 Write-Host "" 401 Write-Host "=== Windows tool verification complete ===" 402 Write-Host "NOTE: P2P features not available on Windows (pending Radicle support)" 403 404 # ============================================================================ 405 # Group B: DreamNode Operations (placeholder - needs Node.js test harness) 406 # ============================================================================ 407 # TODO: Create scripts/ci/test-dreamnode-ops.ts 408 # - Test DreamNodeConversionService.convertToDreamNode() 409 # - Test SubmoduleManagerService.importSubmodule() 410 # - Verify platform-specific submodule URL handling 411 412 # ============================================================================ 413 # DROPPED GROUPS (December 2025) 414 # ============================================================================ 415 # Group C (GitHub Publishing) - Dropped: gh CLI is straightforward, manual testing sufficient 416 # Group D (Semantic Search) - Dropped: Vitest covers logic, Ollama is install-script concern 417 # See p2p-collaboration.yml for Group E (P2P tests with Tailscale) 418 419 # ============================================================================ 420 # Summary Job 421 # ============================================================================ 422 ci-summary: 423 if: always() 424 needs: [check-all, changes, install-script-syntax, install-verify-linux, install-verify-macos, install-verify-windows] 425 runs-on: ubuntu-latest 426 steps: 427 - name: CI Summary 428 run: | 429 echo "=== CI Summary ===" 430 echo "" 431 echo "Core Checks: ${{ needs.check-all.result }}" 432 echo "" 433 echo "Install Script Tests (run when install.sh/install.ps1 change):" 434 echo " Path filter: ${{ needs.changes.outputs.install-scripts == 'true' && 'triggered' || 'skipped (no changes)' }}" 435 echo " Syntax Check: ${{ needs.install-script-syntax.result }}" 436 echo " Linux Install: ${{ needs.install-verify-linux.result }}" 437 echo " macOS Install: ${{ needs.install-verify-macos.result }}" 438 echo " Windows Install: ${{ needs.install-verify-windows.result }}" 439 echo "" 440 441 # Only show Radicle versions if install jobs ran 442 if [ "${{ needs.changes.outputs.install-scripts }}" == "true" ]; then 443 echo "Radicle Versions:" 444 echo " Linux: ${{ needs.install-verify-linux.outputs.rad-version || 'N/A' }}" 445 echo " macOS: ${{ needs.install-verify-macos.outputs.rad-version || 'N/A' }}" 446 echo "" 447 fi 448 449 # Fail if check-all failed (always required) 450 if [ "${{ needs.check-all.result }}" != "success" ]; then 451 echo "FAIL: Core checks failed" 452 exit 1 453 fi 454 455 # Fail if install jobs ran and failed (skipped is OK) 456 if [ "${{ needs.changes.outputs.install-scripts }}" == "true" ]; then 457 if [ "${{ needs.install-script-syntax.result }}" == "failure" ] || \ 458 [ "${{ needs.install-verify-linux.result }}" == "failure" ] || \ 459 [ "${{ needs.install-verify-macos.result }}" == "failure" ] || \ 460 [ "${{ needs.install-verify-windows.result }}" == "failure" ]; then 461 echo "FAIL: Install verification failed" 462 exit 1 463 fi 464 fi 465 466 echo "SUCCESS: All CI checks passed"