ci: add SwiftLint, coverage reporting, and dev loop improvements #348
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect which files changed to decide which jobs to run | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| app: ${{ steps.filter.outputs.app }} | |
| web: ${{ steps.filter.outputs.web }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Check paths | |
| uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| app: | |
| - 'Sources/**' | |
| - 'Tests/**' | |
| - 'Package.swift' | |
| - 'Package.resolved' | |
| - 'scripts/**' | |
| - 'Makefile' | |
| - 'VocaMac.entitlements' | |
| - '.swiftlint.yml' | |
| web: | |
| - 'web/**' | |
| # Lint Swift sources (only when app source or lint config changes) | |
| lint: | |
| name: Lint | |
| needs: changes | |
| if: needs.changes.outputs.app == 'true' | |
| runs-on: macos-15 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install SwiftLint | |
| run: brew install swiftlint | |
| - name: Run SwiftLint | |
| run: swiftlint lint --config .swiftlint.yml | |
| # Build and test Swift app (only when app source changes) | |
| build-and-test: | |
| name: Build & Test App | |
| needs: changes | |
| if: needs.changes.outputs.app == 'true' | |
| runs-on: macos-15 | |
| timeout-minutes: 30 | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Cache SPM dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: .build | |
| key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }} | |
| restore-keys: | | |
| spm-${{ runner.os }}- | |
| - name: Build (Debug) | |
| run: swift build | |
| - name: Test with coverage | |
| run: swift test --enable-code-coverage | |
| - name: Generate coverage report | |
| id: coverage | |
| run: | | |
| PROFDATA=".build/debug/codecov/default.profdata" | |
| BINARY=".build/debug/VocaMacTests.xctest/Contents/MacOS/VocaMacTests" | |
| if [ -f "$PROFDATA" ] && [ -f "$BINARY" ]; then | |
| xcrun llvm-cov report "$BINARY" \ | |
| --instr-profile="$PROFDATA" \ | |
| --ignore-filename-regex="\.build|Tests|VocaMacObjC" \ | |
| > /tmp/coverage-raw.txt 2>&1 | |
| echo "coverage_available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "coverage_available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Post coverage comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const available = '${{ steps.coverage.outputs.coverage_available }}' === 'true'; | |
| let body; | |
| if (available) { | |
| const raw = fs.readFileSync('/tmp/coverage-raw.txt', 'utf8'); | |
| const lines = raw.trim().split('\n'); | |
| const total = lines[lines.length - 1]; | |
| body = [ | |
| '## 📊 Test Coverage', | |
| '', | |
| `**Total:** \`${total}\``, | |
| '', | |
| '<details><summary>Full report</summary>', | |
| '', | |
| '```', | |
| raw, | |
| '```', | |
| '', | |
| '</details>', | |
| ].join('\n'); | |
| } else { | |
| body = '## 📊 Test Coverage\n\nCoverage data unavailable.'; | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find( | |
| c => c.user.login === 'github-actions[bot]' && c.body.includes('📊 Test Coverage') | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| - name: Build (Release) | |
| run: swift build -c release | |
| - name: Initialize Xcode tools | |
| run: sudo xcodebuild -runFirstLaunch | |
| - name: Verify app bundle | |
| run: | | |
| ./scripts/build.sh release | |
| test -d VocaMac.app || exit 1 | |
| codesign -v VocaMac.app || echo "Codesign verification note: expected for ad-hoc CI builds" | |
| test -f VocaMac.app/Contents/Resources/Assets.car || echo "⚠️ Assets.car missing" | |
| echo "App bundle verified" | |
| # Build website (only when web files change) | |
| build-website: | |
| name: Build Website | |
| needs: changes | |
| if: needs.changes.outputs.web == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Hugo | |
| uses: peaceiris/actions-hugo@v3 | |
| with: | |
| hugo-version: "latest" | |
| extended: true | |
| - name: Build Hugo site | |
| run: cd web && hugo --minify |