Skip to content

Redesign Permit Join sheet + restructure Settings Logging (#7) #25

Redesign Permit Join sheet + restructure Settings Logging (#7)

Redesign Permit Join sheet + restructure Settings Logging (#7) #25

Workflow file for this run

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