Fix CLI stop for KeepAlive Kanata service #2374
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: KeyPath CI | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| branches: [ main, master ] | |
| paths-ignore: | |
| - 'docs/**' | |
| - '*.md' | |
| - '.github/workflows/claude.yml' | |
| - '.github/workflows/claude-code-review.yml' | |
| - 'Sources/KeyPathAppKit/Resources/*.md' | |
| - 'Sources/KeyPathAppKit/Resources/*.css' | |
| - 'Sources/KeyPathAppKit/Resources/*.png' | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-test: | |
| # macOS 27 AppSSO traps SwiftPM binary-artifact requests from a headless | |
| # LaunchDaemon. Only the interactive runner carries swiftpm-safe. | |
| runs-on: [self-hosted, macOS, keypath, swiftpm-safe] | |
| # Warm runs normally finish in 2-3 minutes. A measured cold run spent about | |
| # 3 minutes on setup/Kanata and was still compiling Swift tests after another | |
| # 12 minutes, before test execution. Keep a finite bound with real headroom. | |
| timeout-minutes: 25 | |
| # Separate from the workflow-level concurrency group above (which cancels | |
| # stale runs of the *same* PR): this queues jobs from *different* PRs onto | |
| # the single physical runner instead of letting them execute in parallel. | |
| # Concurrent swift test runs share unsandboxed local state (e.g. | |
| # RuleCollectionsManager's config file), which caused spurious | |
| # CLIPackCRUDTests "could not enable associated rule collection" failures | |
| # when two PRs' CI happened to overlap. | |
| concurrency: | |
| group: keypath-self-hosted-runner | |
| cancel-in-progress: false | |
| steps: | |
| - name: Clean stale git credentials | |
| run: | | |
| git config --global --unset-all http.https://github.com/.extraheader 2>/dev/null || true | |
| git config --global --unset-all url.https://github.com/.insteadof 2>/dev/null || true | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| persist-credentials: false | |
| - name: Verify runner disk reserve | |
| run: ./Scripts/lab/host-disk-reserve | |
| - name: Ensure Metal toolchain | |
| run: ./Scripts/ensure-metal-toolchain.sh | |
| - name: Add Homebrew and Cargo to PATH | |
| run: | | |
| echo "/opt/homebrew/bin" >> $GITHUB_PATH | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| - name: Compute kanata cache key | |
| id: kanata-cache-key | |
| run: | | |
| KANATA_SHA=$(git rev-parse HEAD:External/kanata) | |
| CARGO_LOCK_HASH=$(shasum -a 256 External/kanata/Cargo.lock | awk '{print $1}') | |
| RUSTC_HASH=$(rustc --version | shasum -a 256 | awk '{print $1}') | |
| echo "key=kanata-${{ runner.os }}-${{ runner.arch }}-${KANATA_SHA}-${CARGO_LOCK_HASH}-${RUSTC_HASH}" >> "$GITHUB_OUTPUT" | |
| echo "Kanata submodule: $KANATA_SHA" | |
| - name: Restore kanata binary cache | |
| id: kanata-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: build/ci-kanata-cache | |
| key: ${{ steps.kanata-cache-key.outputs.key }} | |
| - name: Build and install kanata fork | |
| run: | | |
| mkdir -p build/ci-kanata-cache | |
| # On the self-hosted runner the workspace persists between runs, so | |
| # binaries from a previous run can survive even when actions/cache | |
| # missed. Gate the "hit" on a stamp matching the checked-out submodule | |
| # SHA — otherwise a stale-pin run poisons every later run with an old | |
| # engine (stale kanata-simulator broke RemapEndToEndTests; see #891). | |
| EXPECTED_ENGINE_SHA=$(git rev-parse HEAD:External/kanata) | |
| CACHED_ENGINE_SHA=$(cat build/ci-kanata-cache/engine-sha 2>/dev/null || echo "none") | |
| if [[ -x build/ci-kanata-cache/kanata && -x build/ci-kanata-cache/kanata-simulator && "$CACHED_ENGINE_SHA" == "$EXPECTED_ENGINE_SHA" ]]; then | |
| echo "✅ Restored kanata artifacts from cache (engine $CACHED_ENGINE_SHA)" | |
| echo "KANATA_CACHE_STATUS=hit" >> "$GITHUB_ENV" | |
| else | |
| echo "🔨 Building kanata artifacts (cache miss: cached=$CACHED_ENGINE_SHA expected=$EXPECTED_ENGINE_SHA)" | |
| cd External/kanata | |
| cargo build --release --target aarch64-apple-darwin 2>&1 | tail -20 | |
| cargo build --release --target aarch64-apple-darwin -p kanata-sim 2>&1 | tail -20 | |
| cd ../.. | |
| cp External/kanata/target/aarch64-apple-darwin/release/kanata build/ci-kanata-cache/kanata | |
| cp External/kanata/target/aarch64-apple-darwin/release/kanata_simulated_input build/ci-kanata-cache/kanata-simulator | |
| chmod 755 build/ci-kanata-cache/kanata build/ci-kanata-cache/kanata-simulator | |
| echo "$EXPECTED_ENGINE_SHA" > build/ci-kanata-cache/engine-sha | |
| echo "KANATA_CACHE_STATUS=miss" >> "$GITHUB_ENV" | |
| fi | |
| cp build/ci-kanata-cache/kanata /opt/homebrew/bin/kanata | |
| cp build/ci-kanata-cache/kanata-simulator /opt/homebrew/bin/kanata-simulator | |
| kanata --version | |
| kanata-simulator --version | |
| - name: Lint WizardAutoFixer for forbidden subprocess usage | |
| run: | | |
| chmod +x ./Scripts/lint-no-subprocess-in-autofixer.sh | |
| ./Scripts/lint-no-subprocess-in-autofixer.sh | |
| - name: Run Test Lane - full | |
| env: | |
| KP_SIGN_DRY_RUN: "1" | |
| KEYPATH_BUNDLED_SIMULATOR_OVERRIDE: /opt/homebrew/bin/kanata-simulator | |
| # SwiftPM can wrap XCTest post-run SIGABRT as exit 1 on the self-hosted runner; | |
| # run-tests-safe.sh still fails if it parses any test failures and emits a warning when this path is used. | |
| KEYPATH_ALLOW_TEST_RUNNER_CRASH_SUCCESS: "1" | |
| KEYPATH_TEST_ENFORCE_CLEAN_SUMMARY: "1" | |
| KEYPATH_TEST_RESET_MODULE_CACHE: "0" | |
| KEYPATH_SWIFT_BUILD_JOBS: "4" | |
| SCRATCH_PATH: ${{ runner.temp }}/keypath-build | |
| run: | | |
| echo "Running full named test lane..." | |
| export CI_ENVIRONMENT=true | |
| export SKIP_EVENT_TAP_TESTS=1 | |
| export TIMEOUT_SECONDS=300 | |
| chmod +x ./Scripts/test-lane.sh | |
| chmod +x ./Scripts/run-tests-safe.sh | |
| if ./Scripts/test-lane.sh full; then | |
| echo "TEST_STATUS=passed" >> $GITHUB_ENV | |
| else | |
| echo "TEST_STATUS=failed" >> $GITHUB_ENV | |
| exit 1 | |
| fi | |
| - name: Parse Installer Reliability Matrix from Test Log | |
| if: always() | |
| run: | | |
| echo "Parsing installer reliability matrix from test output..." | |
| chmod +x ./Scripts/parse-installer-matrix.sh | |
| ./Scripts/parse-installer-matrix.sh test_output.safe.txt | |
| - name: Verify SMAppService plist wrapper | |
| run: | | |
| echo "Verifying com.keypath.kanata.plist points at kanata-launcher" | |
| ./Scripts/verify-kanata-plist.sh Sources/KeyPathApp/com.keypath.kanata.plist | |
| - name: Upload Test Results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: | | |
| test_output.safe.txt | |
| test-results/installer-reliability/ | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Generate Test Summary | |
| if: always() | |
| run: | | |
| echo "## Test Results" >> $GITHUB_STEP_SUMMARY | |
| if [ "${TEST_STATUS:-unknown}" = "failed" ]; then | |
| echo "**Tests Failed** - Check the test-results artifact for details." >> $GITHUB_STEP_SUMMARY | |
| elif [ "${TEST_STATUS:-unknown}" = "passed" ]; then | |
| echo "**All Tests Passed**" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "**Test Status Unknown** (${TEST_STATUS:-unknown})" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Fast Smoke Lane" >> $GITHUB_STEP_SUMMARY | |
| if [ "${SMOKE_STATUS:-unknown}" = "failed" ]; then | |
| echo "- \`smoke\`: failed" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${SMOKE_STATUS:-unknown}" = "passed" ]; then | |
| echo "- \`smoke\`: passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- \`smoke\`: status unknown" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Installer Reliability Matrix" >> $GITHUB_STEP_SUMMARY | |
| if [ -f "test-results/installer-reliability/latest/matrix-summary.md" ]; then | |
| cat test-results/installer-reliability/latest/matrix-summary.md >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- Installer matrix summary not found." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Kanata Artifacts" >> $GITHUB_STEP_SUMMARY | |
| echo "- Cache status: \`${KANATA_CACHE_STATUS:-unknown}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Coverage" >> $GITHUB_STEP_SUMMARY | |
| echo "- Narrow coverage enforcement moved to the scheduled/manual \`KeyPath Coverage\` workflow." >> $GITHUB_STEP_SUMMARY | |
| code-quality: | |
| runs-on: [self-hosted, macOS, keypath] | |
| timeout-minutes: 5 | |
| # No concurrency group: this job never runs `swift test` or touches | |
| # RuleCollectionsManager's config file, so it doesn't share the state | |
| # that caused the race — no need to queue it behind build-and-test. | |
| steps: | |
| - name: Clean stale git credentials | |
| run: | | |
| git config --global --unset-all http.https://github.com/.extraheader 2>/dev/null || true | |
| git config --global --unset-all url.https://github.com/.insteadof 2>/dev/null || true | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Add Homebrew to PATH | |
| run: echo "/opt/homebrew/bin" >> $GITHUB_PATH | |
| - name: Get changed Swift files | |
| id: changed | |
| run: | | |
| BASE=${{ github.event.pull_request.base.sha }} | |
| CHANGED=$(git diff --name-only --diff-filter=d "$BASE"...HEAD -- '*.swift' || true) | |
| if [ -z "$CHANGED" ]; then | |
| echo "swift_files=" >> $GITHUB_OUTPUT | |
| echo "No Swift files changed" | |
| else | |
| echo "swift_files<<EOF" >> $GITHUB_OUTPUT | |
| echo "$CHANGED" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| echo "Changed Swift files:" | |
| echo "$CHANGED" | |
| fi | |
| - name: SwiftLint Analysis | |
| if: steps.changed.outputs.swift_files != '' | |
| run: | | |
| echo "Linting changed Swift files only..." | |
| echo "${{ steps.changed.outputs.swift_files }}" | xargs swiftlint --reporter github-actions-logging | |
| echo "SWIFTLINT_STATUS=passed" >> $GITHUB_ENV | |
| - name: Verify SwiftFormat version matches pin | |
| if: steps.changed.outputs.swift_files != '' | |
| run: | | |
| PINNED=$(sed -nE 's/^[[:space:]]*swiftformat[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' mise.toml) | |
| ACTUAL=$(swiftformat --version | tr -d '[:space:]') | |
| echo "SwiftFormat — pinned (mise.toml): '$PINNED', installed: '$ACTUAL'" | |
| if [ -z "$PINNED" ]; then | |
| echo "::error::Could not read the swiftformat pin from mise.toml" | |
| exit 1 | |
| fi | |
| if [ "$PINNED" != "$ACTUAL" ]; then | |
| echo "::error::SwiftFormat version drift — runner has '$ACTUAL' but mise.toml pins '$PINNED'." | |
| echo "::error::master is a formatted fixed-point for the pinned version; a different version produces spurious lint results. Install the pinned version on the runner (mise install / brew) or update mise.toml + reformat." | |
| exit 1 | |
| fi | |
| - name: SwiftFormat Check | |
| if: steps.changed.outputs.swift_files != '' | |
| run: | | |
| echo "Checking formatting of changed Swift files (must match the pinned fixed-point)..." | |
| echo "${{ steps.changed.outputs.swift_files }}" | xargs swiftformat --lint || { | |
| echo "::error::SwiftFormat found formatting issues. Run 'swiftformat Sources Tests' (at the pinned version) and commit." | |
| exit 1 | |
| } | |
| - name: Wizard Sleep Lint | |
| run: | | |
| chmod +x ./Scripts/lint-no-sleep.sh | |
| ./Scripts/lint-no-sleep.sh | |
| - name: Accessibility Check | |
| run: | | |
| python3 Scripts/check-accessibility.py | |
| - name: Critical Pattern Analysis | |
| run: | | |
| if grep -r 'TODO.*CRITICAL\|FIXME.*URGENT' Sources/ --include='*.swift'; then | |
| echo "::error::CRITICAL TODO/FIXME markers found. Resolve them or downgrade the marker before merging." | |
| echo "CRITICAL_ISSUES=found" >> $GITHUB_ENV | |
| exit 1 | |
| else | |
| echo "No critical TODOs/FIXMEs found" | |
| echo "CRITICAL_ISSUES=none" >> $GITHUB_ENV | |
| fi | |
| - name: Code Smell Detection | |
| run: | | |
| FORCE_UNWRAPS=0 | |
| PRINT_STATEMENTS=0 | |
| python3 - <<'PY' > /tmp/keypath-force-unwraps.txt | |
| from pathlib import Path | |
| import re | |
| pattern = re.compile(r"(?:\btry\s*!|(?:[\w\)\]\}\"'`])\s*!(?=\s*(?:[\)\]\}\.,;:]|$)))") | |
| for path in Path("Sources/KeyPathAppKit/Managers").rglob("*.swift"): | |
| try: | |
| lines = path.read_text(encoding="utf-8").splitlines() | |
| except UnicodeDecodeError: | |
| continue | |
| for idx, line in enumerate(lines, start=1): | |
| stripped = line.strip() | |
| if stripped.startswith("//") or "// swiftlint:disable" in line: | |
| continue | |
| if pattern.search(line): | |
| print(f"{path}:{idx}: {stripped}") | |
| PY | |
| if [ -s /tmp/keypath-force-unwraps.txt ]; then | |
| echo "::warning::Force unwraps found in manager files" | |
| head -10 /tmp/keypath-force-unwraps.txt | |
| FORCE_UNWRAPS=1 | |
| fi | |
| if grep -r "print(" Sources/ --include="*.swift" | grep -v "// debug" >/dev/null 2>&1; then | |
| echo "::warning::print() statements found (consider using Logger)" | |
| grep -r "print(" Sources/ --include="*.swift" | grep -v "// debug" | head -10 | |
| PRINT_STATEMENTS=1 | |
| fi | |
| echo "FORCE_UNWRAPS=$FORCE_UNWRAPS" >> $GITHUB_ENV | |
| echo "PRINT_STATEMENTS=$PRINT_STATEMENTS" >> $GITHUB_ENV | |
| - name: Generate Code Quality Summary | |
| if: always() | |
| run: | | |
| echo "## Code Quality Analysis" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${SWIFTLINT_STATUS:-unknown}" = "passed" ]; then | |
| echo "- SwiftLint: **No violations**" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${SWIFTLINT_STATUS:-unknown}" = "issues_found" ]; then | |
| echo "- SwiftLint: **Issues detected** - See job logs" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${CRITICAL_ISSUES:-unknown}" = "found" ]; then | |
| echo "- **CRITICAL issues found** - Review urgently" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| SMELLS_FOUND=0 | |
| if [ "${FORCE_UNWRAPS:-0}" = "1" ]; then | |
| echo "- Force unwraps found in manager files" >> $GITHUB_STEP_SUMMARY | |
| SMELLS_FOUND=1 | |
| fi | |
| if [ "${PRINT_STATEMENTS:-0}" = "1" ]; then | |
| echo "- print() statements found (consider using Logger)" >> $GITHUB_STEP_SUMMARY | |
| SMELLS_FOUND=1 | |
| fi | |
| if [ "$SMELLS_FOUND" = "0" ] && [ "${CRITICAL_ISSUES:-unknown}" = "none" ]; then | |
| echo "- No code smells detected" >> $GITHUB_STEP_SUMMARY | |
| fi |