feat(ci): bifurcate CI into hosted and self-hosted jobs #758
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 | |
| - "release/*" | |
| - "feature/*" | |
| - "feat/*" | |
| - "fix/*" | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 0 * * *" | |
| permissions: | |
| contents: read | |
| actions: read | |
| security-events: write | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| CARGO_TERM_COLOR: always | |
| WHISPER_MODEL_SIZE: tiny | |
| MIN_FREE_DISK_GB: 10 | |
| MAX_LOAD_AVERAGE: 5 | |
| jobs: | |
| validate-workflows: | |
| name: Validate Workflow Definitions | |
| runs-on: [self-hosted, Linux, X64, fedora, nobara] | |
| continue-on-error: true # Optional validation | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Validate with gh | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if ! command -v gh >/dev/null 2>&1; | |
| then | |
| echo "gh CLI not found on runner, skipping workflow validation" | |
| exit 0 | |
| fi | |
| shopt -s nullglob | |
| files=(.github/workflows/*.yml .github/workflows/*.yaml) | |
| if [[ ${#files[@]} -eq 0 ]] | |
| then | |
| echo "No workflow files found" | |
| exit 0 | |
| fi | |
| echo "Validating ${{ github.sha }} against ${#files[@]} workflow files..." | |
| failed=0 | |
| for wf in "${files[@]}" | |
| do | |
| echo "-- $wf" | |
| # gh workflow view requires the workflow to exist on default branch first. | |
| # For new workflows in PRs, this will fail - skip them gracefully. | |
| if ! gh workflow view "$wf" --ref "$GITHUB_SHA" --yaml >/dev/null 2>&1 | |
| then | |
| # Check if workflow exists on default branch | |
| if gh workflow view "$wf" --yaml >/dev/null 2>&1; then | |
| echo "ERROR: Failed to render $wf via gh (exists on default branch but fails to render)" >&2 | |
| failed=1 | |
| else | |
| echo "SKIP: $wf is new (not yet on default branch), will validate after merge" | |
| fi | |
| fi | |
| done | |
| if [[ $failed -ne 0 ]] | |
| then | |
| echo "One or more workflows failed server-side validation via gh." >&2 | |
| exit 1 | |
| fi | |
| echo "All workflows validated (new workflows skipped)." | |
| setup-whisper-dependencies: | |
| name: Setup Whisper Dependencies | |
| runs-on: [self-hosted, Linux, X64, fedora, nobara] | |
| outputs: | |
| model_path: ${{ steps.setup.outputs.model_path }} | |
| model_size: ${{ steps.setup.outputs.model_size }} | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Setup Whisper Model | |
| id: setup | |
| run: bash scripts/ci/setup-whisper-cache.sh | |
| # Security scanning for vulnerabilities and license compliance | |
| security: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Set up Rust toolchain | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| - name: Install security tools | |
| run: | | |
| cargo install cargo-audit --locked || true | |
| cargo install cargo-deny --locked || true | |
| - name: Run cargo audit | |
| run: cargo audit | |
| - name: Run cargo deny | |
| run: cargo deny check | |
| lint: | |
| name: Lint (fmt + clippy) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Set up Rust toolchain | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| components: rustfmt, clippy | |
| - name: Check formatting | |
| run: cargo fmt --all -- --check | |
| - name: Run clippy | |
| run: cargo clippy --all-targets --locked | |
| docs: | |
| name: Documentation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Set up Rust toolchain | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| - name: Build documentation | |
| run: cargo doc --workspace --no-deps --locked | |
| # Build and unit tests on GitHub-hosted (stateless, parallelizable) | |
| build_and_unit_tests: | |
| name: Build & Unit Tests (Hosted) | |
| runs-on: ubuntu-latest | |
| needs: [setup-whisper-dependencies] | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y pkg-config libasound2-dev libgtk-3-dev python3-pip python3-venv | |
| - name: Set up Rust toolchain | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| - name: Type check | |
| run: cargo check --workspace --all-targets --locked | |
| - name: Build | |
| run: cargo build --workspace --locked | |
| - name: Run unit tests | |
| env: | |
| WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} | |
| WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} | |
| run: | | |
| echo "=== Running Workspace Unit Tests ===" | |
| cargo test --workspace --locked | |
| - name: Run Golden Master pipeline test | |
| env: | |
| WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} | |
| WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} | |
| run: | | |
| echo "=== Running Golden Master Test ===" | |
| pip install faster-whisper | |
| export PYTHONPATH=$(python3 -c "import site; print(site.getsitepackages()[0])") | |
| cargo test -p coldvox-app --test golden_master -- --nocapture | |
| - name: Upload test artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: test-artifacts-build | |
| path: | | |
| target/debug/deps/ | |
| target/debug/build/ | |
| retention-days: 7 | |
| # Hardware tests ONLY - requires live display (self-hosted laptop) | |
| hardware_tests: | |
| name: Hardware Tests (Self-Hosted) | |
| runs-on: [self-hosted, Linux, X64, fedora, nobara] | |
| timeout-minutes: 15 | |
| env: | |
| # Runner's live KDE Wayland session with XWayland | |
| DISPLAY: ":0" | |
| WAYLAND_DISPLAY: "wayland-0" | |
| RUST_LOG: debug | |
| RUST_TEST_TIME_UNIT: 10000 | |
| RUST_TEST_TIME_INTEGRATION: 30000 | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 | |
| with: | |
| toolchain: stable | |
| - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 | |
| - name: Setup ColdVox | |
| uses: ./.github/actions/setup-coldvox | |
| - name: Setup D-Bus Session | |
| run: | | |
| set -euo pipefail | |
| eval "$(dbus-launch --sh-syntax)" | |
| echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> $GITHUB_ENV | |
| echo "DBUS_SESSION_BUS_PID=$DBUS_SESSION_BUS_PID" >> $GITHUB_ENV | |
| echo "D-Bus session started." | |
| - name: Setup X11 access | |
| run: | | |
| set -euo pipefail | |
| echo "=== Setting up X11 access ===" | |
| # Find XWayland auth file (KDE Wayland uses /run/user/UID/xauth_*) | |
| XAUTH_FILE=$(ls /run/user/1000/xauth_* 2>/dev/null | head -1 || true) | |
| if [[ -n "$XAUTH_FILE" && -f "$XAUTH_FILE" ]]; then | |
| echo "Found XAUTHORITY: $XAUTH_FILE" | |
| echo "XAUTHORITY=$XAUTH_FILE" >> $GITHUB_ENV | |
| export XAUTHORITY="$XAUTH_FILE" | |
| else | |
| echo "::warning::No xauth file found in /run/user/1000/" | |
| fi | |
| # Verify X11 access | |
| if xset -q >/dev/null 2>&1; then | |
| echo "X11 accessible via XWayland" | |
| else | |
| echo "::warning::X11 not accessible via xset" | |
| fi | |
| - name: Validate hardware prerequisites | |
| run: | | |
| set -euo pipefail | |
| echo "=== Hardware Test Environment Validation ===" | |
| echo "DISPLAY: $DISPLAY" | |
| echo "XAUTHORITY: ${XAUTHORITY:-not set}" | |
| echo "WAYLAND_DISPLAY: ${WAYLAND_DISPLAY:-not set}" | |
| # Verify X server is accessible | |
| if ! xset -q >/dev/null 2>&1; then | |
| echo "::error::Cannot connect to X server on display $DISPLAY" | |
| exit 1 | |
| fi | |
| echo "X server is accessible." | |
| echo "Available backends:" | |
| command -v xdotool >/dev/null && echo " - xdotool: available" | |
| command -v ydotool >/dev/null && echo " - ydotool: available" | |
| echo "=== Validation Complete ===" | |
| - name: Run real injection tests | |
| run: | | |
| dbus-run-session -- bash -lc ' | |
| export RUST_TEST_TIME_UNIT="10000" | |
| export RUST_TEST_TIME_INTEGRATION="30000" | |
| cargo test -p coldvox-text-injection \ | |
| --features real-injection-tests \ | |
| -- --nocapture --test-threads=1 | |
| ' | |
| - name: Run Hardware Capability Checks | |
| env: | |
| COLDVOX_E2E_REAL_INJECTION: "1" | |
| COLDVOX_E2E_REAL_AUDIO: "1" | |
| run: | | |
| echo "=== Running Hardware Capability Checks ===" | |
| cargo test -p coldvox-app --test hardware_check -- --nocapture --include-ignored | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| if [[ -n "${DBUS_SESSION_BUS_PID:-}" ]]; then | |
| kill "$DBUS_SESSION_BUS_PID" 2>/dev/null || true | |
| fi | |
| name: test-artifacts-build-and-test | |
| path: | | |
| target/debug/deps/ | |
| target/debug/build/ | |
| models/ | |
| retention-days: 7 | |
| - name: Cleanup background processes | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| echo "Cleaning up background processes..." | |
| # Kill dbus-daemon if it was started by this session | |
| if [[ -n "${DBUS_SESSION_BUS_PID:-}" ]]; then | |
| kill "$DBUS_SESSION_BUS_PID" 2>/dev/null || true | |
| fi | |
| echo "Cleanup completed." | |
| # Moonshine STT plugin build verification (optional - requires Python deps) | |
| moonshine_check: | |
| name: Moonshine STT Check (Optional) | |
| runs-on: [self-hosted, Linux, X64, fedora, nobara] | |
| continue-on-error: true | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0 | |
| - name: Set up Rust toolchain | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| - name: Check Python dependencies | |
| id: python-deps | |
| run: | | |
| set -euo pipefail | |
| if python3 -c "import transformers, torch, librosa" 2>/dev/null; then | |
| echo "available=true" >> $GITHUB_OUTPUT | |
| echo "Python dependencies available" | |
| else | |
| echo "available=false" >> $GITHUB_OUTPUT | |
| echo "::warning::Moonshine Python deps not installed, skipping" | |
| fi | |
| - name: Build with moonshine feature | |
| if: steps.python-deps.outputs.available == 'true' | |
| run: cargo build -p coldvox-stt --features moonshine --locked | |
| - name: Run moonshine unit tests | |
| if: steps.python-deps.outputs.available == 'true' | |
| run: cargo test -p coldvox-stt --features moonshine --lib -- --nocapture | |
| ci_success: | |
| name: CI Success Summary | |
| runs-on: ubuntu-latest | |
| needs: | |
| - validate-workflows | |
| - setup-whisper-dependencies | |
| - lint | |
| - security | |
| - docs | |
| - build_and_unit_tests | |
| - hardware_tests | |
| - moonshine_check | |
| if: always() | |
| steps: | |
| - uses: actions/[email protected] | |
| - name: Generate CI Report | |
| run: | | |
| echo "## CI Report" > report.md | |
| echo "- validate-workflows: ${{ needs.validate-workflows.result }}" >> report.md | |
| echo "- setup-whisper-dependencies: ${{ needs.setup-whisper-dependencies.result }}" >> report.md | |
| echo "- lint: ${{ needs.lint.result }}" >> report.md | |
| echo "- security: ${{ needs.security.result }}" >> report.md | |
| echo "- docs: ${{ needs.docs.result }}" >> report.md | |
| echo "- build_and_unit_tests: ${{ needs.build_and_unit_tests.result }}" >> report.md | |
| echo "- hardware_tests: ${{ needs.hardware_tests.result }}" >> report.md | |
| echo "- moonshine_check: ${{ needs.moonshine_check.result }} (optional)" >> report.md | |
| if [[ "${{ needs.setup-whisper-dependencies.result }}" != "success" ]]; then echo "::error::Setup Whisper dependencies failed."; exit 1; fi | |
| if [[ "${{ needs.lint.result }}" != "success" ]]; then echo "::error::Lint checks failed."; exit 1; fi | |
| if [[ "${{ needs.security.result }}" != "success" ]]; then echo "::warning::Security audit failed - check for vulnerabilities."; fi | |
| if [[ "${{ needs.docs.result }}" != "success" ]]; then echo "::warning::Documentation build failed."; fi | |
| if [[ "${{ needs.build_and_unit_tests.result }}" != "success" ]]; then echo "::error::Build and Unit Tests failed."; exit 1; fi | |
| if [[ "${{ needs.hardware_tests.result }}" != "success" ]]; then echo "::error::Hardware tests failed."; exit 1; fi | |
| if [[ "${{ needs.moonshine_check.result }}" != "success" ]]; then echo "::warning::Moonshine check failed (optional)."; fi | |
| echo "All critical stages passed successfully." | |
| - name: Upload CI Report | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ci-report | |
| path: report.md |