Redesign Permit Join sheet + restructure Settings Logging (#7) #25
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: CI (Fast) | |
| # Lightweight gate that runs on every PR push and every push to main. | |
| # Builds the app and runs unit tests only. UI tests are intentionally | |
| # excluded here — see ci-full.yml for the complete suite. | |
| # | |
| # Typical duration: 5-8 min. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - '**/*.md' | |
| - 'docker/**' | |
| - 'scripts/**' | |
| - 'docs/**' | |
| - '.gitignore' | |
| - 'PRIVACY.md' | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - '**/*.md' | |
| - 'docker/**' | |
| - 'scripts/**' | |
| - 'docs/**' | |
| - '.gitignore' | |
| - 'PRIVACY.md' | |
| concurrency: | |
| group: ci-fast-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-unit-test: | |
| name: Build & Unit Tests | |
| runs-on: macos-15 | |
| timeout-minutes: 20 | |
| env: | |
| SCHEME: Shellbee | |
| # Which tests run is defined by the test plan, not the workflow. | |
| # See Shellbee.xcodeproj/xcshareddata/xctestplans/README.md for the | |
| # rationale behind the current skip list. | |
| TEST_PLAN: Shellbee-CI | |
| DERIVED_DATA: ${{ github.workspace }}/DerivedData | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache SwiftPM dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| # Only cache the source checkouts and SwiftPM's global cache — not | |
| # the artifacts directory. Artifacts contain prebuilt xcframeworks | |
| # that xcodebuild re-validates on every run (~2 min), which negated | |
| # the cache win when we included them. | |
| path: | | |
| ${{ github.workspace }}/DerivedData/SourcePackages/checkouts | |
| ~/Library/Caches/org.swift.swiftpm | |
| key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }} | |
| restore-keys: | | |
| spm-${{ runner.os }}- | |
| - name: Select latest Xcode | |
| run: | | |
| LATEST=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -n1) | |
| echo "Selecting $LATEST" | |
| sudo xcode-select -s "$LATEST" | |
| xcodebuild -version | |
| - name: Pick simulator | |
| id: sim | |
| run: | | |
| DEVICE_ID=$(xcrun simctl list devices available --json \ | |
| | jq -r '[.devices | to_entries[] | |
| | select(.key | test("iOS|com.apple.CoreSimulator.SimRuntime.iOS")) | |
| | .value[] | |
| | select(.isAvailable and (.name | startswith("iPhone")))] | |
| | sort_by(.name) | reverse | .[0].udid') | |
| if [ -z "$DEVICE_ID" ] || [ "$DEVICE_ID" = "null" ]; then | |
| echo "No iPhone simulator available on this runner" >&2 | |
| exit 1 | |
| fi | |
| NAME=$(xcrun simctl list devices --json \ | |
| | jq -r --arg id "$DEVICE_ID" '[.devices | to_entries[].value[] | select(.udid==$id)][0].name') | |
| echo "Using: $NAME ($DEVICE_ID)" | |
| echo "device_id=$DEVICE_ID" >> "$GITHUB_OUTPUT" | |
| echo "device_name=$NAME" >> "$GITHUB_OUTPUT" | |
| - name: Boot simulator and wait for readiness | |
| timeout-minutes: 4 | |
| run: | | |
| # `simctl bootstatus -b` boots (if not already) and BLOCKS until the | |
| # device is actually ready (springboard up, services responding) — | |
| # not just "Booted" state. Previous runs used a plain state-check | |
| # loop, which returned too early and caused xcodebuild to hang | |
| # waiting for a not-yet-ready simulator. | |
| DEVICE_ID="${{ steps.sim.outputs.device_id }}" | |
| xcrun simctl bootstatus "$DEVICE_ID" -b | |
| echo "Simulator is fully ready." | |
| - name: Start simulator log stream | |
| run: | | |
| # If xcodebuild hangs again, this log tells us what the host app or | |
| # test runner is actually doing on the device. Written to a file we | |
| # upload on failure. | |
| DEVICE_ID="${{ steps.sim.outputs.device_id }}" | |
| mkdir -p "$DERIVED_DATA" | |
| xcrun simctl spawn "$DEVICE_ID" log stream \ | |
| --level=info \ | |
| --predicate 'process == "Shellbee" OR process == "xctest" OR subsystem == "com.apple.dt.XCTest"' \ | |
| > "$DERIVED_DATA/simulator.log" 2>&1 & | |
| echo $! > "$DERIVED_DATA/simulator-log.pid" | |
| sleep 1 | |
| echo "Log stream PID: $(cat "$DERIVED_DATA/simulator-log.pid")" | |
| - name: Resolve Swift packages | |
| run: | | |
| xcodebuild -resolvePackageDependencies \ | |
| -project Shellbee.xcodeproj \ | |
| -scheme "$SCHEME" \ | |
| -derivedDataPath "$DERIVED_DATA" | |
| - name: Build for testing | |
| env: | |
| DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }} | |
| run: | | |
| set -o pipefail | |
| xcodebuild build-for-testing \ | |
| -project Shellbee.xcodeproj \ | |
| -scheme "$SCHEME" \ | |
| -testPlan "$TEST_PLAN" \ | |
| -destination "$DESTINATION" \ | |
| -derivedDataPath "$DERIVED_DATA" \ | |
| CODE_SIGNING_ALLOWED=NO | tee build.log | |
| - name: Run unit tests | |
| timeout-minutes: 12 | |
| env: | |
| DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }} | |
| run: | | |
| set -o pipefail | |
| # `script` forces line-buffered output so the Actions log shows | |
| # progress in real time instead of a 2-minute silence followed by | |
| # a flood. -verbose tells us which phase xcodebuild is in | |
| # (install / launch / attach-runner / run-tests) if it hangs. | |
| script -q /dev/null \ | |
| xcodebuild test-without-building \ | |
| -project Shellbee.xcodeproj \ | |
| -scheme "$SCHEME" \ | |
| -testPlan "$TEST_PLAN" \ | |
| -destination "$DESTINATION" \ | |
| -derivedDataPath "$DERIVED_DATA" \ | |
| -resultBundlePath "$DERIVED_DATA/UnitTests.xcresult" \ | |
| CODE_SIGNING_ALLOWED=NO | tee test.log | |
| - name: Summarize results | |
| if: always() | |
| env: | |
| DEVICE_NAME: ${{ steps.sim.outputs.device_name }} | |
| run: | | |
| XCRESULT="$DERIVED_DATA/UnitTests.xcresult" | |
| { | |
| echo "## CI (Fast) — Unit Tests" | |
| echo | |
| echo "| Field | Value |" | |
| echo "| --- | --- |" | |
| echo "| Simulator | $DEVICE_NAME |" | |
| echo "| Xcode | $(xcodebuild -version | head -n1) |" | |
| echo "| Scheme | $SCHEME |" | |
| echo "| Target | $UNIT_TEST_TARGET |" | |
| echo | |
| if [ -d "$XCRESULT" ]; then | |
| SUMMARY=$(xcrun xcresulttool get --path "$XCRESULT" --format json 2>/dev/null \ | |
| | jq -r '.metrics | "Tests: \(.testsCount._value // 0) • Failed: \(.testsFailedCount._value // 0) • Skipped: \(.testsSkippedCount._value // 0)"' 2>/dev/null || echo "Results bundle present but could not be parsed.") | |
| echo "**$SUMMARY**" | |
| else | |
| echo "_No result bundle produced (build may have failed before tests ran)._" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Stop simulator log stream | |
| if: always() | |
| run: | | |
| PID_FILE="$DERIVED_DATA/simulator-log.pid" | |
| if [ -f "$PID_FILE" ]; then | |
| kill "$(cat "$PID_FILE")" 2>/dev/null || true | |
| fi | |
| - name: Upload logs and result bundle on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ci-fast-logs | |
| path: | | |
| build.log | |
| test.log | |
| ${{ env.DERIVED_DATA }}/UnitTests.xcresult | |
| ${{ env.DERIVED_DATA }}/simulator.log | |
| if-no-files-found: ignore | |
| retention-days: 7 |