From e1ce24e09633b1ad04386c7f68dba66514e21242 Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Tue, 27 Oct 2026 16:39:59 +0200 Subject: [PATCH 1/5] CI: swift test and a release build on every pull request Both on macOS 26 (the launcher uses its Liquid Glass), with the SwiftPM build cached. ci-ok sums the jobs up and is the check main requires. --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..37c0930 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + name: swift test + # Liquid Glass (glassEffect) needs the macOS 26 SDK. + runs-on: macos-26 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - run: swift --version + - uses: actions/cache@v6 + with: + path: .build + key: spm-debug-${{ runner.os }}-${{ hashFiles('Package.resolved') }}-${{ github.sha }} + restore-keys: spm-debug-${{ runner.os }}-${{ hashFiles('Package.resolved') }}- + - run: swift test + + release: + name: swift build -c release + runs-on: macos-26 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/cache@v6 + with: + path: .build + key: spm-release-${{ runner.os }}-${{ hashFiles('Package.resolved') }}-${{ github.sha }} + restore-keys: spm-release-${{ runner.os }}-${{ hashFiles('Package.resolved') }}- + - run: swift build -c release + + ci-ok: + name: ci-ok + if: always() + needs: [test, release] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - env: + NEEDS: ${{ toJSON(needs) }} + run: | + echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"' + bad=$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result != "success" and .value.result != "skipped") | .key] | join(", ")') + if [ -n "$bad" ]; then echo "::error::Not passed: $bad"; exit 1; fi From ed9a2881be2775deb6489fd4151987774dfc280f Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Tue, 27 Oct 2026 16:45:16 +0200 Subject: [PATCH 2/5] CI: build with the newest Xcode on the runner (the default Swift 6.3 crashes) --- .github/workflows/ci.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37c0930..f7bc3f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,13 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - - run: swift --version + # The newest Xcode on the runner: the default one's Swift 6.3 crashes on the launcher (local builds use Swift 6.4). + - name: Select the newest Xcode + run: | + ls -d /Applications/Xcode*.app + xcode=$(ls -d /Applications/Xcode_*.app | grep -iv beta | sort -V | tail -1) + sudo xcode-select -s "$xcode" + swift --version - uses: actions/cache@v6 with: path: .build @@ -39,6 +45,13 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false + # The newest Xcode on the runner: the default one's Swift 6.3 crashes on the launcher (local builds use Swift 6.4). + - name: Select the newest Xcode + run: | + ls -d /Applications/Xcode*.app + xcode=$(ls -d /Applications/Xcode_*.app | grep -iv beta | sort -V | tail -1) + sudo xcode-select -s "$xcode" + swift --version - uses: actions/cache@v6 with: path: .build From 7996b7a5e438603de3f87fff08391db8513321b3 Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Tue, 27 Oct 2026 16:54:18 +0200 Subject: [PATCH 3/5] Profiles: self.profile(_:) in add, so Swift 6.3 (Xcode 26) compiles it The local 'profile' declared two lines below shadowed the method, and Swift 6.3 reports a circular reference there; Swift 6.4 accepts it. --- Sources/AnyPS5Core/Profiles/LauncherProfiles.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AnyPS5Core/Profiles/LauncherProfiles.swift b/Sources/AnyPS5Core/Profiles/LauncherProfiles.swift index 65bd9b2..2f03c0d 100644 --- a/Sources/AnyPS5Core/Profiles/LauncherProfiles.swift +++ b/Sources/AnyPS5Core/Profiles/LauncherProfiles.swift @@ -158,7 +158,7 @@ public final class ProfileStore: @unchecked Sendable { let used = Set(book.profiles.map(\.avatar.color)) let color = (0 ..< LauncherProfile.paletteSize).first { !used.contains($0) } ?? book.profiles.count % LauncherProfile.paletteSize let first = book.profiles.isEmpty - let made = id.flatMap { candidate in profile(candidate) == nil ? candidate : nil } + let made = id.flatMap { candidate in self.profile(candidate) == nil ? candidate : nil } ?? (first && book.legacyOwner == nil ? LauncherProfile.legacyID : UUID().uuidString.lowercased()) let profile = LauncherProfile(id: made, name: name, avatar: .init(color: color), mode: mode, account: account) book.profiles.append(profile) From 6a81e1a4b39b5784e5407a0a12a8925583257c6d Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Tue, 27 Oct 2026 17:05:15 +0200 Subject: [PATCH 4/5] Build with Swift 6.3 (Xcode 26) too: two spots only Swift 6.4 accepted - The prepare timer's assumeIsolated closure dropped stepPrepare()'s result implicitly; 6.3 inferred the closure's type from it. - SocialPreviews' local leaderboard entry 'me' shadowed the static user 'me' an earlier closure uses; it is 'robin' now. --- .github/workflows/ci.yml | 4 ++-- Sources/AnyPS5/AppModel.swift | 2 +- Sources/AnyPS5/SocialPreviews.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7bc3f0..e042769 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - # The newest Xcode on the runner: the default one's Swift 6.3 crashes on the launcher (local builds use Swift 6.4). + # The newest Xcode on the runner (its SDK has Liquid Glass; the launcher must also build with Xcode 26's Swift 6.3). - name: Select the newest Xcode run: | ls -d /Applications/Xcode*.app @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - # The newest Xcode on the runner: the default one's Swift 6.3 crashes on the launcher (local builds use Swift 6.4). + # The newest Xcode on the runner (its SDK has Liquid Glass; the launcher must also build with Xcode 26's Swift 6.3). - name: Select the newest Xcode run: | ls -d /Applications/Xcode*.app diff --git a/Sources/AnyPS5/AppModel.swift b/Sources/AnyPS5/AppModel.swift index 67fd23f..c988207 100644 --- a/Sources/AnyPS5/AppModel.swift +++ b/Sources/AnyPS5/AppModel.swift @@ -1417,7 +1417,7 @@ final class AppModel: ObservableObject { if !waiting { prepareTimer?.invalidate(); prepareTimer = nil; return } guard prepareTimer == nil else { return } prepareTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in - MainActor.assumeIsolated { self?.stepPrepare() } + MainActor.assumeIsolated { _ = self?.stepPrepare() } } } diff --git a/Sources/AnyPS5/SocialPreviews.swift b/Sources/AnyPS5/SocialPreviews.swift index 160d7d7..a478c03 100644 --- a/Sources/AnyPS5/SocialPreviews.swift +++ b/Sources/AnyPS5/SocialPreviews.swift @@ -46,8 +46,8 @@ enum SocialPreviews { let person = { (id: String, name: String) in TogetherPerson(userId: id, handle: name.lowercased(), displayName: name, avatar: nil) } let names = ["Mira", "Kenji", "Sol", "Ines", "Theo", "Noor", "Lena", "Felix"] let top = names.enumerated().map { index, name in TogetherLeaderboard.Entry(rank: index + 1, user: person("p\(index)", name), value: 9_400 - index * 870) } - let me = TogetherLeaderboard.Entry(rank: 41, user: person("u1", "Robin"), value: 1_830) - let around = [TogetherLeaderboard.Entry(rank: 40, user: person("x1", "Lin"), value: 1_905), me, TogetherLeaderboard.Entry(rank: 42, user: person("x2", "Ada"), value: 1_790)] + let robin = TogetherLeaderboard.Entry(rank: 41, user: person("u1", "Robin"), value: 1_830) + let around = [TogetherLeaderboard.Entry(rank: 40, user: person("x1", "Lin"), value: 1_905), robin, TogetherLeaderboard.Entry(rank: 42, user: person("x2", "Ada"), value: 1_790)] render("leaderboard-game") { model in model.previewLeaderboard(TogetherLeaderboard(board: "points", scope: .global, game: nil, entries: Array(top.prefix(4)), me: .init(rank: 41, value: 1_830, listed: true), around: around, total: 312), game: found.first, index: 6) From 3e0aded7fa09c3e986ae43745a73c97ae0aaad81 Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Tue, 27 Oct 2026 17:12:35 +0200 Subject: [PATCH 5/5] CI: skip the issue form check outside AnyPS5-ARM, and the real-time Opus test on runners The crash issue form the check compares with lives in AnyPS5-ARM's .github; this repository has none, so the test skips itself when the file is not there. The stereo Opus test measures a live WebRTC jitter buffer and packet rate, which a shared runner cannot keep; CI leaves it out through .github/ci/swift-test-skips.txt. --- .github/ci/swift-test-skips.txt | 5 +++++ .github/workflows/ci.yml | 7 ++++++- Tests/AnyPS5CoreTests/IssueDraftTests.swift | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .github/ci/swift-test-skips.txt diff --git a/.github/ci/swift-test-skips.txt b/.github/ci/swift-test-skips.txt new file mode 100644 index 0000000..873d984 --- /dev/null +++ b/.github/ci/swift-test-skips.txt @@ -0,0 +1,5 @@ +# Tests CI leaves out of `swift test` (.github/workflows/ci.yml), one per line: +# +# Only for tests that depend on the runner rather than the code (real-time timing, devices). + +AnyPS5SharePlayTests.SharePlayOpusTests/testAViewerSayingHelloHearsStereoOpus Real-time WebRTC audio: the jitter buffer and packet rate it measures go past its limits on a shared runner. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e042769..abc3450 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,12 @@ jobs: path: .build key: spm-debug-${{ runner.os }}-${{ hashFiles('Package.resolved') }}-${{ github.sha }} restore-keys: spm-debug-${{ runner.os }}-${{ hashFiles('Package.resolved') }}- - - run: swift test + - name: swift test (without the runner-dependent tests in .github/ci/swift-test-skips.txt) + run: | + skips=() + while read -r pattern _; do skips+=(--skip "$pattern"); done < <(grep -Ev '^(#|[[:space:]]*$)' .github/ci/swift-test-skips.txt) + echo "swift test ${skips[*]}" + swift test "${skips[@]}" release: name: swift build -c release diff --git a/Tests/AnyPS5CoreTests/IssueDraftTests.swift b/Tests/AnyPS5CoreTests/IssueDraftTests.swift index 7a25f09..74e1821 100644 --- a/Tests/AnyPS5CoreTests/IssueDraftTests.swift +++ b/Tests/AnyPS5CoreTests/IssueDraftTests.swift @@ -80,7 +80,12 @@ final class IssueDraftTests: XCTestCase { // tools/macos/launcher/Tests/AnyPS5CoreTests → the repository root. let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() - let template = try String(contentsOf: root.appendingPathComponent(".github/ISSUE_TEMPLATE/crash.yml"), encoding: .utf8) + let file = root.appendingPathComponent(".github/ISSUE_TEMPLATE/crash.yml") + // The issue forms are AnyPS5-ARM's; the standalone AnyLauncher repository has none to check. + guard FileManager.default.fileExists(atPath: file.path) else { + throw XCTSkip("no .github/ISSUE_TEMPLATE/crash.yml: run this in AnyPS5-ARM's tools/macos/launcher") + } + let template = try String(contentsOf: file, encoding: .utf8) for field in IssueDraft.crash(report()).fields { XCTAssertTrue(template.contains("id: \(field.id)\n"), "crash.yml has no field \(field.id)") }