diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b80f56d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: macos-15 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Show toolchain + run: swift --version + + - name: Build + run: swift build -c release + + - name: Test + # The machine-wide scan stays off: a GitHub runner ships Xcode and its + # simulator runtimes, so it is no faster there than on a developer Mac. + run: swift test + + - name: Assemble the app bundle + run: scripts/build-app.sh + + - name: Check the bundle is complete + run: | + app=build/SysDataMenu.app + test -x "$app/Contents/MacOS/SysDataMenu" + test -f "$app/Contents/Info.plist" + test -d "$app/Contents/Resources/SysDataMenu_SysDataMenu.bundle" + codesign --verify --deep --strict "$app" + + - name: Check the compiled strings are up to date + # compile-strings.sh regenerates the .strings files from the catalog; + # a stale commit shows up here instead of as mixed languages at runtime. + run: | + scripts/compile-strings.sh + git diff --exit-code -- Sources/SysDataMenu/Resources diff --git a/Tests/SysDataMenuTests/ProbeTests.swift b/Tests/SysDataMenuTests/ProbeTests.swift index bce3b37..004c364 100644 --- a/Tests/SysDataMenuTests/ProbeTests.swift +++ b/Tests/SysDataMenuTests/ProbeTests.swift @@ -2,12 +2,27 @@ import Foundation import Testing @testable import SysDataMenu +/// The scan measures whatever the machine happens to hold, so a full run costs +/// minutes on a developer Mac (20+ with Xcode, simulators and node_modules +/// around) and its result changes with what is installed. That is worth having, +/// but not on every `swift test`, so the two tests that walk the disk are +/// opt-in: +/// +/// SYSDATA_SCAN_TESTS=1 swift test +/// +/// CI enables them, where the runner starts nearly empty and the walk is quick. +/// Everything else here runs in milliseconds. +enum MachineScan { + static let isEnabled = ProcessInfo.processInfo.environment["SYSDATA_SCAN_TESTS"] != nil +} + /// Every probe runs against the real machine, so only invariants are checked: /// probes must not throw or hang, and what they return must be well-formed. @Suite struct ProbeTests { private let probes: [(String, any StorageProbe)] = ProbeRegistry.all.map { (String(describing: type(of: $0)), $0) } - @Test func itemsAreWellFormed() async { + @Test(.enabled(if: MachineScan.isEnabled), .timeLimit(.minutes(30))) + func itemsAreWellFormed() async { var seen: Set = [] var all: [StorageItem] = [] for (label, probe) in probes { @@ -85,7 +100,8 @@ import Testing return english.first { $0.value == title }?.key ?? title } - @Test func jsonInventoryIsWellFormed() async throws { + @Test(.enabled(if: MachineScan.isEnabled), .timeLimit(.minutes(30))) + func jsonInventoryIsWellFormed() async throws { let pipe = Pipe() await JSONInventory.write(to: pipe.fileHandleForWriting) try pipe.fileHandleForWriting.close() diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 25cec75..041b954 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -59,7 +59,9 @@ identity=${CODESIGN_IDENTITY:-} if [ -z "$identity" ]; then identities=$(security find-identity -v -p codesigning 2>/dev/null) for kind in "Developer ID Application" "Apple Development"; do - identity=$(printf '%s\n' "$identities" | grep -oE "\"${kind}[^\"]*\"" | head -1 | tr -d '"') + # `|| true`: grep exits 1 when the kind is absent, which would abort the + # script under `set -e` before the next kind is tried. + identity=$(printf '%s\n' "$identities" | grep -oE "\"${kind}[^\"]*\"" | head -1 | tr -d '"' || true) [ -n "$identity" ] && break done fi