From 235bd9b6b95bbad65186f3e72bf81c8ceee142c4 Mon Sep 17 00:00:00 2001 From: Terrace Hung Date: Sat, 20 Jun 2026 18:18:51 -0700 Subject: [PATCH] fix(app): strong-bind self for concurrent closures; add pre-tag macOS build CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.4.0 release build failed at the swiftc step on the macos-14 runner: DeviceSession's G1b self-heal closure captured a weak `self` and referenced it inside a @Sendable Task — "reference to captured var 'self' in concurrently-executing code." A weak capture is modeled as a var (ARC can zero it across a concurrency boundary), so the newer runner toolchain rejects it as a hard error; the older local toolchain only warned, so it slipped past local builds and the tag. Fix: bind a strong, immutable `self` (guard-let) before the Task in onUnexpectedExit, and re-capture self immutably in onStatusLine's DispatchQueue.main.async closure. Root cause was zero pre-tag macOS build coverage (release.yml is tag-only, the Forgejo workflow is Linux/Go). Add build-check.yml: a pull_request job on macos-14 that runs the same `make dist-swiftc` release.yml runs (plus the Go build/tests), no secrets/signing — so compile-time toolchain skew surfaces on the PR, not on a tag. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-check.yml | 39 ++++++++++++++++++++++++++ MenuBarApp/Sources/DeviceSession.swift | 16 +++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-check.yml diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 00000000..efd3fb99 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,39 @@ +# Pre-tag macOS build check. +# +# Why this exists: the release pipeline (release.yml) is tag-triggered, so +# until v0.4.0 there was ZERO macOS build coverage before a tag. The G1b +# self-heal code compiled locally (older Swift toolchain, warning only) but +# the macos-14 runner's newer toolchain rejected a weak-`self` capture as a +# hard error — discovered only after tagging. This job runs the SAME build +# step release.yml runs (`make dist-swiftc`) on the SAME runner image, on +# every PR, so compile-time skew surfaces on the PR instead of on the tag. +# +# Build-only: no secrets, no Developer ID signing, no notarization. It proves +# the tree compiles and links; the release pipeline owns sign+notarize. + +name: Build check + +on: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build dependencies + run: brew install libmtp libusb go + + - name: Go bridge — compile + vet (tree-wide vendor consistency) + run: make build-all + + - name: Go bridge — tests + run: make bridge-test + + - name: macOS app — full build (bridge + swiftc, ad-hoc signed) + # Identical to release.yml's "Build app bundle" step. This is the + # check that would have caught the Swift concurrency error pre-tag. + run: make dist-swiftc diff --git a/MenuBarApp/Sources/DeviceSession.swift b/MenuBarApp/Sources/DeviceSession.swift index e01a316b..09b9fb18 100644 --- a/MenuBarApp/Sources/DeviceSession.swift +++ b/MenuBarApp/Sources/DeviceSession.swift @@ -162,7 +162,11 @@ final class DeviceSession { // Surface key bridge-side milestones in the menu while connecting. bp.onStatusLine = { [weak self] msg in - DispatchQueue.main.async { self?.setConnectStatus(msg) } + // Same weak-var-into-concurrent-closure issue as + // onUnexpectedExit below: DispatchQueue.main.async takes a + // @Sendable closure, so re-capture self immutably in its + // capture list rather than referencing the enclosing weak var. + DispatchQueue.main.async { [weak self] in self?.setConnectStatus(msg) } } let useNFS = true @@ -216,7 +220,15 @@ final class DeviceSession { // fault, anything that isn't our own stop()), self-heal instead // of leaving Finder pointed at a dead mount (G1b). bp.onUnexpectedExit = { [weak self] in - Task { await self?.handleUnexpectedBridgeExit() } + // Bind a strong, immutable `self` before the Task. A weak + // capture is modeled as a *var* (ARC can zero it across a + // concurrency boundary), so referencing it inside the + // @Sendable Task is "reference to captured var 'self' in + // concurrently-executing code" — a hard error on the macos-14 + // runner's Swift toolchain (it only warns locally). guard-let + // makes the capture immutable. + guard let self else { return } + Task { await self.handleUnexpectedBridgeExit() } } await MainActor.run {