/ .github / workflows / ci.yml
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"