Skip to content

fix(audio): mic-first HUD, stop-path drain, silence pad, hangover tuning #224

fix(audio): mic-first HUD, stop-path drain, silence pad, hangover tuning

fix(audio): mic-first HUD, stop-path drain, silence pad, hangover tuning #224

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# ---------------------------------------------------------------------------
# Job: build-and-test
# Blocks PR merge. Must pass before any code lands on main.
# ---------------------------------------------------------------------------
jobs:
build-and-test:
name: Build & Test (macOS)
runs-on: [self-hosted, macOS]
steps:
- name: Runner info
run: |
echo "Hostname: $(hostname)"
echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)"
echo "Xcode path: $(xcode-select -p 2>/dev/null || echo 'not set')"
echo "Xcode: $(xcodebuild -version 2>/dev/null | head -1 || echo 'not found')"
echo "Swift: $(swift --version 2>/dev/null | head -1 || echo 'not found')"
echo "CPU: $(sysctl -n machdep.cpu.brand_string)"
echo "Cores: $(sysctl -n hw.logicalcpu)"
echo "RAM: $(($(sysctl -n hw.memsize) / 1073741824)) GB"
- name: Checkout (with submodules)
uses: actions/checkout@v4
with:
submodules: false
- name: Restore vendor submodules
# Avoids network fetches to GitHub for whisper.cpp and llama.cpp on every run.
# The UDM Pro NAT drops long-lived connections that cause recursive submodule
# checkout to hang indefinitely. Instead we cache the source trees on the runner
# at ~/.whiskey-vendor-cache/ and restore from disk, only falling back to the
# network when the expected commit has changed (i.e. after a Vendor bump).
run: |
VENDOR_CACHE="${HOME}/.whiskey-vendor-cache"
mkdir -p "$VENDOR_CACHE"
for MODULE in whisper.cpp llama.cpp; do
EXPECTED=$(git ls-tree HEAD "Vendor/${MODULE}" | awk '{print $3}')
CACHE_COMMIT_FILE="${VENDOR_CACHE}/${MODULE}.commit"
CACHED_COMMIT=""
if [ -f "$CACHE_COMMIT_FILE" ]; then
CACHED_COMMIT=$(cat "$CACHE_COMMIT_FILE")
fi
echo "[vendor] ${MODULE}: expected=${EXPECTED} cached=${CACHED_COMMIT:-none}"
if [ -d "${VENDOR_CACHE}/${MODULE}" ] && [ "$EXPECTED" = "$CACHED_COMMIT" ]; then
echo "[vendor] Cache hit for ${MODULE} — restoring from disk"
rsync -a "${VENDOR_CACHE}/${MODULE}/" "Vendor/${MODULE}/"
else
echo "[vendor] Cache miss for ${MODULE} — fetching from GitHub"
git submodule update --init --recursive "Vendor/${MODULE}"
rsync -a "Vendor/${MODULE}/" "${VENDOR_CACHE}/${MODULE}/"
echo "$EXPECTED" > "$CACHE_COMMIT_FILE"
fi
done
git submodule init
- name: Restore local SPM cache
# ~/whiskey-spm-cache lives outside the workspace so it survives
# git clean from concurrent SwiftLint/Code Quality jobs. On the first
# run the directory won't exist and the build is cold; subsequent runs
# are incremental (~30s instead of 14+ min).
#
# After cache restore, touch all vendor C/C++ sources to match build.db
# mtime. git clean resets source timestamps to "now" which makes SPM
# think vendor files are dirty and recompiles all 185 of them (~10 min).
# Aligning timestamps tells SPM the vendor files haven't changed.
run: |
CACHE_DIR="${HOME}/.whiskey-spm-cache"
if [ -d "$CACHE_DIR" ]; then
echo "Restoring local SPM cache from $CACHE_DIR"
rsync -a "$CACHE_DIR/" .build/
BUILD_DB=".build/arm64-apple-macosx/debug/build.db"
if [ -f "$BUILD_DB" ]; then
echo "Aligning vendor source timestamps to build.db to prevent full recompile"
find Vendor/ -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.m" | xargs touch -r "$BUILD_DB"
fi
else
echo "No local SPM cache — cold build"
fi
- name: Install SQLCipher
run: /opt/homebrew/bin/brew install sqlcipher 2>&1 || /opt/homebrew/bin/brew upgrade sqlcipher 2>&1 || true
- name: Build (debug + tests)
# Reserve 2 cores for the runner's .NET renewal timer. Using all cores
# starves the timer thread, causing token renewal to fire after the
# 10-min window expires — the server then rejects CompleteJob with 404.
run: |
NCPU=$(sysctl -n hw.logicalcpu)
BUILD_JOBS=$(( NCPU > 2 ? NCPU - 2 : 1 ))
while true; do echo "[heartbeat] $(date -u +%H:%M:%SZ)"; sleep 30; done &
HPID=$!
swift build -c debug --build-tests --jobs "$BUILD_JOBS" 2>&1
STATUS=$?
kill $HPID 2>/dev/null || true
exit $STATUS
- name: Save local SPM cache
# --update skips files where the destination is already up-to-date,
# avoiding re-copying 700MB+ of timestamp-only changes after each build.
if: success() || failure()
run: |
CACHE_DIR="${HOME}/.whiskey-spm-cache"
echo "Saving SPM cache to $CACHE_DIR"
rsync -a --update .build/ "$CACHE_DIR/"
- name: Save vendor cache
# Persist any newly-fetched submodule source trees back to disk so the
# next run can skip the network entirely. Runs on failure too so a
# partial submodule fetch still warms the cache for subsequent runs.
if: success() || failure()
run: |
VENDOR_CACHE="${HOME}/.whiskey-vendor-cache"
mkdir -p "$VENDOR_CACHE"
for MODULE in whisper.cpp llama.cpp; do
if [ -d "Vendor/${MODULE}/.git" ] || [ -f "Vendor/${MODULE}/.git" ]; then
echo "[vendor] Saving ${MODULE} to cache"
rsync -a "Vendor/${MODULE}/" "${VENDOR_CACHE}/${MODULE}/"
git ls-tree HEAD "Vendor/${MODULE}" | awk '{print $3}' > "${VENDOR_CACHE}/${MODULE}.commit"
fi
done
- name: Run tests
# --skip-build: binary was compiled by "Build (debug + tests)" above.
# Without this, swift test rebuilds all test targets (~11 min) even
# though --build-tests already compiled everything.
run: swift test --skip-build -c debug --filter WhisKeyCoreTests 2>&1
# ---------------------------------------------------------------------------
# Job: build-release
# Depends on build-and-test. Runs in its own job so the release build
# gets a fresh runner session — separating long jobs reduces the chance
# of a broker reconnect overlapping with job teardown.
# Reuses ~/.whiskey-spm-cache saved by build-and-test (same machine).
# ---------------------------------------------------------------------------
build-release:
name: Build (release)
runs-on: [self-hosted, macOS]
needs: build-and-test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout (with submodules)
uses: actions/checkout@v4
with:
submodules: false
- name: Restore vendor submodules
# Release builds always run on main where the cache should be warm from
# build-and-test. Restore unconditionally from disk; fall back to network
# only if the cache is absent (first ever run or manual cache purge).
run: |
VENDOR_CACHE="${HOME}/.whiskey-vendor-cache"
mkdir -p "$VENDOR_CACHE"
for MODULE in whisper.cpp llama.cpp; do
EXPECTED=$(git ls-tree HEAD "Vendor/${MODULE}" | awk '{print $3}')
CACHE_COMMIT_FILE="${VENDOR_CACHE}/${MODULE}.commit"
CACHED_COMMIT=""
if [ -f "$CACHE_COMMIT_FILE" ]; then
CACHED_COMMIT=$(cat "$CACHE_COMMIT_FILE")
fi
echo "[vendor] ${MODULE}: expected=${EXPECTED} cached=${CACHED_COMMIT:-none}"
if [ -d "${VENDOR_CACHE}/${MODULE}" ] && [ "$EXPECTED" = "$CACHED_COMMIT" ]; then
echo "[vendor] Cache hit for ${MODULE} — restoring from disk"
rsync -a "${VENDOR_CACHE}/${MODULE}/" "Vendor/${MODULE}/"
else
echo "[vendor] Cache miss for ${MODULE} — fetching from GitHub (cold)"
git submodule update --init --recursive "Vendor/${MODULE}"
rsync -a "Vendor/${MODULE}/" "${VENDOR_CACHE}/${MODULE}/"
echo "$EXPECTED" > "$CACHE_COMMIT_FILE"
fi
done
git submodule init
- name: Restore release SPM cache
# ~/.whiskey-spm-cache-release persists release-configuration artifacts
# (.build/release/) across runs. Debug artifacts (from build-and-test)
# are in a separate cache and not useful here. First run is cold (~11 min);
# subsequent runs are warm (~2 min) and complete well within the
# 10-minute broker session window.
run: |
CACHE_DIR="${HOME}/.whiskey-spm-cache-release"
if [ -d "$CACHE_DIR" ]; then
echo "Restoring release SPM cache from $CACHE_DIR"
rsync -a "$CACHE_DIR/" .build/
else
echo "No release SPM cache — cold build"
fi
- name: Install SQLCipher
run: /opt/homebrew/bin/brew install sqlcipher 2>&1 || /opt/homebrew/bin/brew upgrade sqlcipher 2>&1 || true
- name: Build (release)
run: |
NCPU=$(sysctl -n hw.logicalcpu)
while true; do echo "[heartbeat] $(date -u +%H:%M:%SZ)"; sleep 30; done &
HPID=$!
swift build -c release --jobs "$NCPU" 2>&1
STATUS=$?
kill $HPID 2>/dev/null || true
exit $STATUS
- name: Save release SPM cache
if: success() || failure()
run: |
CACHE_DIR="${HOME}/.whiskey-spm-cache-release"
echo "Saving release SPM cache to $CACHE_DIR"
rsync -a .build/ "$CACHE_DIR/"
- name: Save vendor cache
if: success() || failure()
run: |
VENDOR_CACHE="${HOME}/.whiskey-vendor-cache"
mkdir -p "$VENDOR_CACHE"
for MODULE in whisper.cpp llama.cpp; do
if [ -d "Vendor/${MODULE}/.git" ] || [ -f "Vendor/${MODULE}/.git" ]; then
echo "[vendor] Saving ${MODULE} to cache"
rsync -a "Vendor/${MODULE}/" "${VENDOR_CACHE}/${MODULE}/"
git ls-tree HEAD "Vendor/${MODULE}" | awk '{print $3}' > "${VENDOR_CACHE}/${MODULE}.commit"
fi
done
# ---------------------------------------------------------------------------
# Job: swiftlint
# Blocks PR merge. Runs SwiftLint with .swiftlint.yml config.
# Annotates violations inline on the PR diff.
# ---------------------------------------------------------------------------
swiftlint:
name: SwiftLint
runs-on: [self-hosted, macOS]
steps:
- uses: actions/checkout@v4
- name: Install SwiftLint
run: /opt/homebrew/bin/swiftlint --version 2>/dev/null || /opt/homebrew/bin/brew install swiftlint
- name: Run SwiftLint
# --strict turns warnings into errors, blocking the PR.
run: /opt/homebrew/bin/swiftlint lint --strict --config .swiftlint.yml 2>&1
# ---------------------------------------------------------------------------
# Job: code-quality
# Advisory (non-blocking on push to main, blocking on PRs).
# Checks TODO/FIXME density and file organization.
# ---------------------------------------------------------------------------
code-quality:
name: Code Quality Checks
runs-on: [self-hosted, macOS]
steps:
- uses: actions/checkout@v4
- name: TODO/FIXME density
run: |
TODO_COUNT=$(grep -rn "TODO\|FIXME" Sources/ --include="*.swift" | wc -l | tr -d ' ')
echo "Found $TODO_COUNT TODO/FIXME markers in Sources/"
if [ "$TODO_COUNT" -gt 30 ]; then
echo "::error::TODO/FIXME count ($TODO_COUNT) exceeds threshold (30). Triage before merging."
exit 1
elif [ "$TODO_COUNT" -gt 15 ]; then
echo "::warning::TODO/FIXME count ($TODO_COUNT) is elevated. Consider triaging soon."
fi
echo "Quality check passed ($TODO_COUNT markers)."
- name: No force-push markers
# Catch leftover conflict markers that escape SwiftLint
run: |
if grep -rn "<<<<<<\|>>>>>>\|=======" Sources/ Tests/ --include="*.swift"; then
echo "::error::Merge conflict markers found in source files."
exit 1
fi
echo "No conflict markers found."
- name: Test file parity check
# Warn if a Sources/ module has no corresponding test file.
run: |
MISSING=0
for dir in Sources/WhisKeyCore/*/; do
module=$(basename "$dir")
test_file="Tests/WhisKeyCoreTests/${module}Tests.swift"
if [ ! -f "$test_file" ]; then
echo "::warning::No test file for module: $module (expected $test_file)"
MISSING=$((MISSING + 1))
fi
done
echo "Modules without test files: $MISSING"
# Don't fail — some modules (e.g. Models) legitimately share test files.