@@ -49,17 +49,13 @@ jobs:
4949
5050 release-mac :
5151 needs : build
52- strategy :
53- fail-fast : false
54- matrix :
55- include :
56- - arch : arm64
57- runner : macos-14
58- dist_command : pnpm dist:mac:arm64
59- - arch : x64
60- runner : macos-15-intel
61- dist_command : pnpm dist:mac:x64
62- runs-on : ${{ matrix.runner }}
52+ # Single job builds BOTH arm64 and x64 in one electron-builder invocation.
53+ # Reason: when arches are split into separate jobs each running `--publish always`,
54+ # each upload OVERWRITES latest-mac.yml — so the final yml only lists one arch
55+ # and electron-updater silently routes the wrong-arch dmg to users (arm64 users
56+ # get x64 → Rosetta penalty). Building both in one invocation produces a single
57+ # latest-mac.yml that lists both arches, so auto-update routes correctly.
58+ runs-on : macos-14 # arm64 runner; macOS supports x64 cross-build natively
6359
6460 steps :
6561 - name : Checkout
@@ -93,24 +89,82 @@ jobs:
9389 VERSION="${GITHUB_REF#refs/tags/v}"
9490 pnpm pkg set version="$VERSION"
9591
96- - name : Build app (macOS ${{ matrix.arch }})
92+ - name : Build app
9793 run : pnpm build
9894
99- - name : Verify packaged inputs (macOS ${{ matrix.arch }})
95+ - name : Verify packaged inputs
10096 run : |
10197 test -f dist-electron/main/index.cjs
10298 test -f dist-electron/preload/index.js
10399 test -f out/renderer/index.html
104100
105- - name : Package & Release (macOS ${{ matrix.arch }} )
101+ - name : Package & Release (macOS arm64 + x64 )
106102 env :
107103 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
108104 CSC_LINK : ${{ secrets.CSC_LINK }}
109105 CSC_KEY_PASSWORD : ${{ secrets.CSC_KEY_PASSWORD }}
110106 APPLE_ID : ${{ secrets.APPLE_ID }}
111107 APPLE_APP_SPECIFIC_PASSWORD : ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
112108 APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
113- run : ${{ matrix.dist_command }}
109+ run : pnpm dist:mac
110+
111+ - name : Assert arch suffix in artifact names
112+ run : |
113+ set -euo pipefail
114+ shopt -s nullglob
115+ BAD=()
116+ for f in release/*.dmg release/*.zip; do
117+ base=$(basename "$f")
118+ case "$base" in
119+ *blockmap*|*-arm64.dmg|*-arm64.zip|*-arm64-mac.zip|*-x64.dmg|*-x64.zip|*-x64-mac.zip)
120+ ;;
121+ *)
122+ BAD+=("$base")
123+ ;;
124+ esac
125+ done
126+ if [ ${#BAD[@]} -gt 0 ]; then
127+ echo "::error::Found macOS artifacts without '-arm64' or '-x64' suffix. Apple Silicon users would download x64 by default and run under Rosetta. Fix package.json build.mac.artifactName."
128+ printf ' bad: %s\n' "${BAD[@]}"
129+ exit 1
130+ fi
131+ echo "OK: all macOS artifacts have arch suffix"
132+ ls -la release/ | grep -E '\.(dmg|zip)$' || true
133+
134+ - name : Verify latest-mac.yml lists BOTH arm64 and x64
135+ run : |
136+ set -euo pipefail
137+ YML=release/latest-mac.yml
138+ test -f "$YML" || { echo "::error::missing $YML — electron-updater cannot route auto-updates"; exit 1; }
139+ echo "--- latest-mac.yml ---"
140+ cat "$YML"
141+ echo "----------------------"
142+ # Both arches must appear in the yml, otherwise electron-updater will route
143+ # the wrong dmg to one arch's users (the regression that caused #186).
144+ grep -q -- '-arm64\.dmg' "$YML" || { echo "::error::latest-mac.yml missing arm64 dmg entry — arm64 users will receive x64 build"; exit 1; }
145+ grep -q -- '-x64\.dmg' "$YML" || { echo "::error::latest-mac.yml missing x64 dmg entry — x64 users will receive arm64 build"; exit 1; }
146+ echo "OK: latest-mac.yml lists both arm64 and x64"
147+
148+ - name : Verify each dmg's binary matches its arch suffix
149+ run : |
150+ set -euo pipefail
151+ fail=0
152+ for dmg in release/*-arm64.dmg release/*-x64.dmg; do
153+ [ -e "$dmg" ] || continue
154+ expected=$([[ "$dmg" == *"-arm64.dmg" ]] && echo arm64 || echo x86_64)
155+ mount=$(hdiutil attach "$dmg" -nobrowse -readonly | tail -1 | awk '{print $NF}')
156+ bin="$mount/claude-devtools.app/Contents/MacOS/claude-devtools"
157+ if [ -f "$bin" ]; then
158+ actual=$(file "$bin")
159+ echo "$dmg -> $actual"
160+ echo "$actual" | grep -q "$expected" || { echo "::error::$dmg has wrong binary arch (expected $expected)"; fail=1; }
161+ else
162+ echo "::warning::no binary at $bin"
163+ fi
164+ hdiutil detach "$mount" -quiet || true
165+ done
166+ [ "$fail" = 0 ] || exit 1
167+ echo "OK: each dmg's binary matches its arch suffix"
114168
115169 release-win :
116170 needs : build
0 commit comments