@@ -53,12 +53,27 @@ permissions:
5353
5454jobs :
5555 build :
56- runs-on : macos-14
56+ strategy :
57+ matrix :
58+ include :
59+ - runner : macos-14
60+ arch : arm64
61+ - runner : macos-15-intel
62+ arch : x64
63+ runs-on : ${{ matrix.runner }}
5764
5865 steps :
5966 - name : Checkout
6067 uses : actions/checkout@v4
6168
69+ - name : Validate channel input
70+ shell : bash
71+ run : |
72+ case "${{ inputs.channel }}" in
73+ stable|beta|nightly) ;;
74+ *) echo "Invalid channel: ${{ inputs.channel }}" >&2; exit 1 ;;
75+ esac
76+
6277 - name : Setup pnpm
6378 uses : pnpm/action-setup@v4
6479 with :
7489 - name : Resolve build metadata
7590 id : meta
7691 shell : bash
92+ env :
93+ ARCH : ${{ matrix.arch }}
7794 run : |
7895 set -euo pipefail
7996
@@ -103,7 +120,7 @@ jobs:
103120 echo "build_date=$build_date"
104121 echo "short_sha=$short_sha"
105122 echo "channel=$channel"
106- echo "artifact_name=desktop-${channel}-${build_date}-${short_sha}"
123+ echo "artifact_name=desktop-${channel}-${ARCH}-${ build_date}-${short_sha}"
107124 } >> "$GITHUB_OUTPUT"
108125
109126 - name : Prepare Apple signing certificate
@@ -142,6 +159,7 @@ jobs:
142159 BUILD_SOURCE : ${{ inputs.build_source }}
143160 BUILD_BRANCH : ${{ github.ref_name }}
144161 BUILD_COMMIT : ${{ github.sha }}
162+ BUILD_ARCH : ${{ matrix.arch }}
145163 SENTRY_DSN_TEST : ${{ secrets.SENTRY_DSN_NEXU_DESKTOP_TEST }}
146164 SENTRY_DSN_PROD : ${{ secrets.SENTRY_DSN_NEXU_DESKTOP_PROD }}
147165 run : |
@@ -158,13 +176,14 @@ jobs:
158176 NEXU_LINK_URL: process.env.LINK_URL === "null" ? null : process.env.LINK_URL,
159177 NEXU_SENTRY_ENV: process.env.SENTRY_ENV,
160178 NEXU_DESKTOP_APP_VERSION: version,
179+ NEXU_DESKTOP_UPDATE_CHANNEL: "${{ inputs.channel }}",
161180 NEXU_DESKTOP_BUILD_SOURCE: process.env.BUILD_SOURCE,
162181 NEXU_DESKTOP_BUILD_BRANCH: process.env.BUILD_BRANCH,
163182 NEXU_DESKTOP_BUILD_COMMIT: process.env.BUILD_COMMIT,
164183 NEXU_DESKTOP_BUILD_TIME: process.env.BUILT_AT,
165184 };
166185 if (process.env.UPDATE_FEED_URL) {
167- config.NEXU_UPDATE_FEED_URL = process.env.UPDATE_FEED_URL;
186+ config.NEXU_UPDATE_FEED_URL = `${ process.env.UPDATE_FEED_URL.replace(/\/$/, "")}/${process.env.BUILD_ARCH}` ;
168187 }
169188 if (sentryDsn) {
170189 config.NEXU_DESKTOP_SENTRY_DSN = sentryDsn;
@@ -183,7 +202,10 @@ jobs:
183202 APPLE_ID : ${{ secrets.APPLE_ID }}
184203 APPLE_APP_SPECIFIC_PASSWORD : ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
185204 APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
205+ NEXU_DESKTOP_TARGET_ARCH : ${{ matrix.arch }}
186206 SENTRY_AUTH_TOKEN : ${{ secrets.SENTRY_AUTH_TOKEN }}
207+ VITE_AMPLITUDE_API_KEY : ${{ secrets.AMPLITUDE_API_KEY }}
208+ AMPLITUDE_API_KEY : ${{ secrets.AMPLITUDE_API_KEY }}
187209 run : pnpm --filter @nexu/desktop dist:mac
188210
189211 - name : Prepare artifacts
@@ -192,6 +214,8 @@ jobs:
192214 env :
193215 CHANNEL : ${{ inputs.channel }}
194216 VERSION : ${{ steps.meta.outputs.desktop_version }}
217+ SHORT_SHA : ${{ steps.meta.outputs.short_sha }}
218+ ARCH : ${{ matrix.arch }}
195219 run : |
196220 set -euo pipefail
197221 shopt -s nullglob
@@ -207,11 +231,21 @@ jobs:
207231 exit 1
208232 fi
209233
210- # New naming: nexu-{version}-{channel}-{arch}.{ext}
211- versioned_dmg="nexu-${VERSION}-${CHANNEL}-arm64.dmg"
212- versioned_zip="nexu-${VERSION}-${CHANNEL}-arm64.zip"
213- latest_dmg="nexu-latest-${CHANNEL}-arm64.dmg"
214- latest_zip="nexu-latest-${CHANNEL}-arm64.zip"
234+ artifact_version="$VERSION"
235+ if [ "$CHANNEL" != "stable" ]; then
236+ artifact_version="${VERSION}.${SHORT_SHA}"
237+ fi
238+
239+ latest_name_prefix="nexu-latest-mac-${ARCH}"
240+ if [ "$CHANNEL" != "stable" ]; then
241+ latest_name_prefix="nexu-latest-${CHANNEL}-mac-${ARCH}"
242+ fi
243+
244+ versioned_dmg="nexu-${artifact_version}-mac-${ARCH}.dmg"
245+ versioned_zip="nexu-${artifact_version}-mac-${ARCH}.zip"
246+ latest_dmg="${latest_name_prefix}.dmg"
247+ latest_zip="${latest_name_prefix}.zip"
248+ checksum_file="desktop-${ARCH}-sha256.txt"
215249
216250 cp "${dmg_files[0]}" "apps/desktop/channel-artifacts/${versioned_dmg}"
217251 cp "${zip_files[0]}" "apps/desktop/channel-artifacts/${versioned_zip}"
@@ -221,13 +255,14 @@ jobs:
221255 shasum -a 256 \
222256 "apps/desktop/channel-artifacts/${versioned_dmg}" \
223257 "apps/desktop/channel-artifacts/${versioned_zip}" \
224- > apps/desktop/channel-artifacts/desktop-sha256.txt
258+ > " apps/desktop/channel-artifacts/${checksum_file}"
225259
226260 {
227261 echo "versioned_dmg=$versioned_dmg"
228262 echo "versioned_zip=$versioned_zip"
229263 echo "latest_dmg=$latest_dmg"
230264 echo "latest_zip=$latest_zip"
265+ echo "checksum_file=$checksum_file"
231266 } >> "$GITHUB_OUTPUT"
232267
233268 - name : Upload workflow artifacts
@@ -237,7 +272,7 @@ jobs:
237272 path : |
238273 apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.versioned_dmg }}
239274 apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.versioned_zip }}
240- apps/desktop/channel-artifacts/desktop-sha256.txt
275+ apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.checksum_file }}
241276 retention-days : 7
242277 if-no-files-found : error
243278
@@ -266,13 +301,14 @@ jobs:
266301 files : |
267302 apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.versioned_dmg }}
268303 apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.versioned_zip }}
269- apps/desktop/channel-artifacts/desktop-sha256.txt
304+ apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.checksum_file }}
270305
271306 - name : Patch latest-mac.yml for channel naming
272307 if : inputs.update_feed_url != ''
273308 env :
274309 VERSION : ${{ steps.meta.outputs.desktop_version }}
275310 CHANNEL : ${{ inputs.channel }}
311+ ARCH : ${{ matrix.arch }}
276312 VERSIONED_DMG : ${{ steps.artifacts.outputs.versioned_dmg }}
277313 VERSIONED_ZIP : ${{ steps.artifacts.outputs.versioned_zip }}
278314 shell : bash
@@ -284,11 +320,11 @@ jobs:
284320 exit 0
285321 fi
286322
287- # electron-builder generates filenames without channel, we need to patch them
288- # Original: nexu-0.1.3-arm64.dmg
289- # Target: nexu-0.1.3-nightly-arm64.dmg
290- original_dmg="nexu-${VERSION}-arm64 .dmg"
291- original_zip="nexu-${VERSION}-arm64 .zip"
323+ # electron-builder generates filenames without the final public naming format.
324+ # Original: nexu-0.1.3-nightly.20260325- arm64.dmg
325+ # Target: nexu-0.1.3-nightly.20260325.ab12cd3-mac -arm64.dmg
326+ original_dmg="nexu-${VERSION}-${ARCH} .dmg"
327+ original_zip="nexu-${VERSION}-${ARCH} .zip"
292328
293329 echo "Patching latest-mac.yml:"
294330 echo " ${original_dmg} → ${VERSIONED_DMG}"
@@ -309,6 +345,7 @@ jobs:
309345 CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
310346 UPDATE_FEED_URL : ${{ inputs.update_feed_url }}
311347 CHANNEL : ${{ inputs.channel }}
348+ ARCH : ${{ matrix.arch }}
312349 VERSIONED_DMG : ${{ steps.artifacts.outputs.versioned_dmg }}
313350 VERSIONED_ZIP : ${{ steps.artifacts.outputs.versioned_zip }}
314351 LATEST_DMG : ${{ steps.artifacts.outputs.latest_dmg }}
@@ -317,34 +354,42 @@ jobs:
317354 run : |
318355 set -euo pipefail
319356
320- # Extract R2 prefix from feed URL path (e.g. https://desktop-releases.nexu.io/nightly → nightly)
321- r2_prefix=$(node -e "process.stdout.write(new URL(process.env.UPDATE_FEED_URL).pathname.slice(1))")
357+ upload_prefix() {
358+ local prefix="$1"
322359
323- echo "[r2] uploading to prefix: ${r2_prefix }/"
360+ echo "[r2] uploading to prefix: ${prefix }/"
324361
325- # Upload patched latest-mac.yml for auto-update
326- if [ -f "apps/desktop/release/latest-mac.yml" ]; then
327- echo "Uploading latest-mac.yml → ${r2_prefix}/latest-mac.yml"
328- npx wrangler r2 object put "nexu-desktop-releases/${r2_prefix}/latest-mac.yml" --file "apps/desktop/release/latest-mac.yml" --remote
329- fi
362+ if [ -f "apps/desktop/release/latest-mac.yml" ]; then
363+ echo "Uploading latest-mac.yml → ${prefix}/latest-mac.yml"
364+ npx wrangler r2 object put "nexu-desktop-releases/${prefix}/latest-mac.yml" --file "apps/desktop/release/latest-mac.yml" --remote
365+ fi
366+
367+ echo "Uploading ${VERSIONED_DMG} → ${prefix}/${VERSIONED_DMG}"
368+ npx wrangler r2 object put "nexu-desktop-releases/${prefix}/${VERSIONED_DMG}" --file "apps/desktop/channel-artifacts/${VERSIONED_DMG}" --remote
369+
370+ echo "Uploading ${VERSIONED_ZIP} → ${prefix}/${VERSIONED_ZIP}"
371+ npx wrangler r2 object put "nexu-desktop-releases/${prefix}/${VERSIONED_ZIP}" --file "apps/desktop/channel-artifacts/${VERSIONED_ZIP}" --remote
372+
373+ echo "Uploading ${LATEST_DMG} → ${prefix}/${LATEST_DMG}"
374+ npx wrangler r2 object put "nexu-desktop-releases/${prefix}/${LATEST_DMG}" --file "apps/desktop/channel-artifacts/${LATEST_DMG}" --remote
330375
331- # Upload versioned artifacts
332- echo "Uploading ${VERSIONED_DMG} → ${r2_prefix}/${VERSIONED_DMG}"
333- npx wrangler r2 object put "nexu-desktop-releases/${r2_prefix}/${VERSIONED_DMG}" --file "apps/desktop/channel-artifacts/${VERSIONED_DMG}" --remote
376+ echo "Uploading ${LATEST_ZIP} → ${prefix}/${LATEST_ZIP}"
377+ npx wrangler r2 object put "nexu-desktop-releases/${prefix}/${LATEST_ZIP}" --file "apps/desktop/channel-artifacts/${LATEST_ZIP}" --remote
334378
335- echo "Uploading ${VERSIONED_ZIP} → ${r2_prefix}/${VERSIONED_ZIP}"
336- npx wrangler r2 object put "nexu-desktop-releases/${r2_prefix}/${VERSIONED_ZIP}" --file "apps/desktop/channel-artifacts/${VERSIONED_ZIP}" --remote
379+ echo "Uploading ${{ steps.artifacts.outputs.checksum_file }} → ${prefix}/${{ steps.artifacts.outputs.checksum_file }}"
380+ npx wrangler r2 object put "nexu-desktop-releases/${prefix}/${{ steps.artifacts.outputs.checksum_file }}" --file "apps/desktop/channel-artifacts/${{ steps.artifacts.outputs.checksum_file }}" --remote
381+ }
337382
338- # Upload latest artifacts (overwrite each time )
339- echo "Uploading ${LATEST_DMG} → ${r2_prefix}/${LATEST_DMG}"
340- npx wrangler r2 object put "nexu-desktop-releases/${r2_prefix }/${LATEST_DMG}" --file "apps/desktop/channel-artifacts/${LATEST_DMG}" --remote
383+ # Extract R2 prefix from feed URL path (e.g. https://desktop-releases.nexu.io/nightly → nightly )
384+ channel_prefix=$(node -e 'const path = new URL(process.env.UPDATE_FEED_URL).pathname.replace(/^\//, "").replace(/\/$/, ""); process.stdout.write(path);')
385+ r2_prefix="${channel_prefix }/${ARCH}"
341386
342- echo "Uploading ${LATEST_ZIP} → ${r2_prefix}/${LATEST_ZIP}"
343- npx wrangler r2 object put "nexu-desktop-releases/${r2_prefix}/${LATEST_ZIP}" --file "apps/desktop/channel-artifacts/${LATEST_ZIP}" --remote
387+ upload_prefix "$r2_prefix"
344388
345- # Upload checksum
346- echo "Uploading desktop-sha256.txt → ${r2_prefix}/desktop-sha256.txt"
347- npx wrangler r2 object put "nexu-desktop-releases/${r2_prefix}/desktop-sha256.txt" --file "apps/desktop/channel-artifacts/desktop-sha256.txt" --remote
389+ if [ "${ARCH}" = "arm64" ]; then
390+ echo "[r2] uploading legacy arm64 compatibility feed to prefix: ${channel_prefix}/"
391+ upload_prefix "$channel_prefix"
392+ fi
348393
349394 - name : Purge Cloudflare CDN cache for latest artifacts
350395 if : inputs.update_feed_url != '' && env.CLOUDFLARE_ZONE_ID != ''
@@ -354,17 +399,27 @@ jobs:
354399 UPDATE_FEED_URL : ${{ inputs.update_feed_url }}
355400 LATEST_DMG : ${{ steps.artifacts.outputs.latest_dmg }}
356401 LATEST_ZIP : ${{ steps.artifacts.outputs.latest_zip }}
402+ ARCH : ${{ matrix.arch }}
357403 shell : bash
358404 run : |
359405 set -euo pipefail
360406
361- base_url="${UPDATE_FEED_URL%/}"
407+ base_url="${UPDATE_FEED_URL%/}/${ARCH} "
362408 urls=(
363409 "${base_url}/${LATEST_DMG}"
364410 "${base_url}/${LATEST_ZIP}"
365411 "${base_url}/latest-mac.yml"
366412 )
367413
414+ if [ "${ARCH}" = "arm64" ]; then
415+ legacy_base_url="${UPDATE_FEED_URL%/}"
416+ urls+=(
417+ "${legacy_base_url}/${LATEST_DMG}"
418+ "${legacy_base_url}/${LATEST_ZIP}"
419+ "${legacy_base_url}/latest-mac.yml"
420+ )
421+ fi
422+
368423 files_json=$(printf '%s\n' "${urls[@]}" | jq -R . | jq -sc '.')
369424 echo "[cf-purge] Purging: ${files_json}"
370425
@@ -383,11 +438,12 @@ jobs:
383438 VERSIONED_ZIP : ${{ steps.artifacts.outputs.versioned_zip }}
384439 LATEST_DMG : ${{ steps.artifacts.outputs.latest_dmg }}
385440 LATEST_ZIP : ${{ steps.artifacts.outputs.latest_zip }}
441+ ARCH : ${{ matrix.arch }}
386442 shell : bash
387443 run : |
388444 set -euo pipefail
389445
390- base_url="${UPDATE_FEED_URL%/}"
446+ base_url="${UPDATE_FEED_URL%/}/${ARCH} "
391447 latest_yml_url="${base_url}/latest-mac.yml"
392448 versioned_dmg_url="${base_url}/${VERSIONED_DMG}"
393449 versioned_zip_url="${base_url}/${VERSIONED_ZIP}"
0 commit comments