Skip to content

fix(ci): comprehensive CI fixes for self-hosted runner (#328) #754

fix(ci): comprehensive CI fixes for self-hosted runner (#328)

fix(ci): comprehensive CI fixes for self-hosted runner (#328) #754

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_audit:
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
# Build, check, and test with multiple Rust versions
unit_tests_hosted:
name: Unit Tests & Golden Master (Hosted)
runs-on: ubuntu-latest
needs: [setup-whisper-dependencies]
strategy:
matrix:
rust-version: [stable] # Use stable only
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y xdotool wget unzip gcc g++ make xvfb openbox dbus-x11 wl-clipboard xclip ydotool x11-utils wmctrl pkg-config pulseaudio libasound2-dev libgtk-3-dev libatspi2.0-dev libxtst-dev python3-pip python3-venv
- name: Set up Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.rust-version }}
components: rustfmt, clippy
override: true
# Only run formatting and linting on stable
- name: Check formatting (advisory)
if: matrix.rust-version == 'stable'
run: |
set +e
cargo fmt --all -- --check
status=$?
if [ "$status" -ne 0 ]; then
echo "::warning::cargo fmt detected formatting differences. Please run 'cargo fmt --all' locally before committing."
fi
exit 0
- name: Run clippy
if: matrix.rust-version == 'stable'
run: cargo clippy --all-targets --locked
- name: Type check
run: cargo check --workspace --all-targets --locked
- name: Build
run: cargo build --workspace --locked
# Only build docs and run tests on stable
- name: Build documentation
if: matrix.rust-version == 'stable'
run: cargo doc --workspace --no-deps --locked
- name: Run unit and integration tests (skip E2E)
if: matrix.rust-version == 'stable'
env:
WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }}
WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }}
run: |
echo "=== Environment Validation ==="
echo "WHISPER_MODEL_PATH: $WHISPER_MODEL_PATH"
echo "WHISPER_MODEL_SIZE: $WHISPER_MODEL_SIZE"
echo "Model directory contents:"
ls -la "$WHISPER_MODEL_PATH" || echo "Model directory not accessible"
echo "=== Running Tests ==="
cargo test --workspace --locked --
- name: Run Golden Master pipeline test
if: matrix.rust-version == 'stable'
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 ==="
# Install Python dependencies for Golden Master
pip install faster-whisper
export PYTHONPATH=$(python3 -c "import site; print(site.getsitepackages()[0])")
cargo test -p coldvox-app --test golden_master -- --nocapture
# Moonshine E2E skipped on GitHub-hosted: PyTorch+CUDA deps (4GB+) exceed disk space
# These tests run on self-hosted via moonshine_check job instead
- name: Skip Moonshine E2E Tests (runs on self-hosted)
if: matrix.rust-version == 'stable'
run: |
echo "::notice::Moonshine E2E tests skipped on GitHub-hosted (disk space). See moonshine_check job."
# GUI groundwork check integrated here
- name: Detect and test Qt 6 GUI
if: matrix.rust-version == 'stable'
run: |
# Qt6 might not be easily available on ubuntu-latest without extra actions, skipping for now or adding if needed
echo "Skipping Qt6 check on hosted runner"
- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: test-artifacts-build-${{ matrix.rust-version }}
path: |
target/debug/deps/
target/debug/build/
retention-days: 7
text_injection_tests:
name: Hardware Integration Tests (Self-Hosted)
runs-on: [self-hosted, Linux, X64, fedora, nobara]
needs: [setup-whisper-dependencies]
timeout-minutes: 30
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
SCCACHE_DIR: $HOME/.cache/sccache
# Build optimizations
CARGO_INCREMENTAL: "1"
RUSTFLAGS: "-C link-arg=-fuse-ld=mold"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.0
- uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Setup sccache via justfile (installs if missing, enables RUSTC_WRAPPER)
- name: Setup sccache
run: |
# Install just if not available
if ! command -v just >/dev/null 2>&1; then
cargo install just --locked
fi
# Run setup-sccache recipe (idempotent - installs if missing)
just setup-sccache
# Enable sccache wrapper after installation
SCCACHE_BIN=""
if command -v sccache >/dev/null 2>&1; then
SCCACHE_BIN="$(command -v sccache)"
elif [[ -x "$HOME/.cargo/bin/sccache" ]]; then
SCCACHE_BIN="$HOME/.cargo/bin/sccache"
fi
if [[ -n "$SCCACHE_BIN" ]]; then
"$SCCACHE_BIN" --start-server || true
echo "RUSTC_WRAPPER=$SCCACHE_BIN" >> "$GITHUB_ENV"
echo "sccache enabled: $SCCACHE_BIN"
fi
- 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 test prerequisites
run: |
set -euo pipefail
echo "=== Test Environment Validation ==="
echo "DISPLAY: $DISPLAY"
echo "XAUTHORITY: ${XAUTHORITY:-not set}"
echo "WAYLAND_DISPLAY: ${WAYLAND_DISPLAY:-not set}"
# Check if XAUTHORITY file exists
if [[ -f "${XAUTHORITY:-}" ]]; then
echo "XAUTHORITY file exists: $XAUTHORITY"
else
echo "::warning::XAUTHORITY file not found at ${XAUTHORITY:-unset}"
fi
# Verify X server is accessible
if ! xset -q >/dev/null 2>&1; then
echo "::error::Cannot connect to X server on display $DISPLAY"
echo "Trying xhost diagnostics..."
xhost 2>&1 || true
exit 1
fi
echo "X server is accessible."
echo "Available text injection backends:"
command -v xdotool >/dev/null && echo " - xdotool: $(xdotool --version 2>/dev/null || echo 'available')"
command -v ydotool >/dev/null && echo " - ydotool: available"
command -v enigo >/dev/null && echo " - enigo: available (Rust crate)"
echo "GTK development libraries:"
pkg-config --exists gtk+-3.0 && echo " - GTK+ 3.0: available" || echo " - GTK+ 3.0: not found"
echo "System audio:"
command -v alsa-info >/dev/null && echo " - ALSA: available" || echo " - ALSA: not found"
echo "=== Validation Complete ==="
- name: Test with real-injection-tests feature
run: |
dbus-run-session -- bash -lc '
# Set per-test timeout to prevent hanging
export RUST_TEST_TIME_UNIT="10000" # 10 second timeout per test
export RUST_TEST_TIME_INTEGRATION="30000" # 30 second for integration tests
# Note: atspi feature not enabled - AT-SPI Collection.GetMatches requires
# a full desktop session, not available in headless Xvfb environment.
# AT-SPI tests will skip gracefully.
cargo test -p coldvox-text-injection \
--features real-injection-tests \
-- --nocapture --test-threads=1
'
- name: Build pipeline (default features)
run: |
dbus-run-session -- bash -c '
set -euo pipefail
echo "Testing default features..."
cargo test -p coldvox-text-injection --locked
echo "Testing without default features..."
cargo test -p coldvox-text-injection --no-default-features --locked
echo "Testing regex feature only..."
cargo test -p coldvox-text-injection --no-default-features --features regex --locked
'
# Build main app to ensure integration compiles
- name: Build main application
run: cargo build --locked -p coldvox-app
- name: Run Hardware Capability Checks
env:
COLDVOX_E2E_REAL_INJECTION: "1"
COLDVOX_E2E_REAL_AUDIO: "1"
run: |
echo "=== Running Hardware Capability Checks ==="
# We run the new hardware_check test file, enabling the ignored tests
cargo test -p coldvox-app --test hardware_check -- --nocapture --include-ignored
- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: test-artifacts-text-injection
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
- security_audit
- unit_tests_hosted
- text_injection_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 "- security_audit: ${{ needs.security_audit.result }}" >> report.md
echo "- unit_tests_hosted: ${{ needs.unit_tests_hosted.result }}" >> report.md
echo "- text_injection_tests: ${{ needs.text_injection_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.security_audit.result }}" != "success" ]]; then echo "::warning::Security audit failed - check for vulnerabilities."; fi
if [[ "${{ needs.unit_tests_hosted.result }}" != "success" ]]; then echo "::error::Build and check failed."; exit 1; fi
if [[ "${{ needs.text_injection_tests.result }}" != "success" ]]; then echo "::error::Text injection 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