Skip to content

Land the press-rebound fix that raced the #1248 merge #2364

Land the press-rebound fix that raced the #1248 merge

Land the press-rebound fix that raced the #1248 merge #2364

Workflow file for this run

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