Skip to content

feat(ci): bifurcate CI into hosted and self-hosted jobs #758

feat(ci): bifurcate CI into hosted and self-hosted jobs

feat(ci): bifurcate CI into hosted and self-hosted jobs #758

Workflow file for this run

---
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