From e23eb285cdb099abcf55362aa4e70c9c1975c870 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 8 Mar 2026 03:09:49 -0700 Subject: [PATCH] Publish separate universal nightly track --- .github/workflows/nightly.yml | 218 +++++++++++++++++++++------------- 1 file changed, 134 insertions(+), 84 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index da15f45f..e90ecc03 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -151,9 +151,16 @@ jobs: echo "Derived Sparkle public key: $DERIVED_PUBLIC_KEY" echo "SPARKLE_PUBLIC_KEY=$DERIVED_PUBLIC_KEY" >> "$GITHUB_ENV" - - name: Build app (Release) + - name: Build Apple Silicon app (Release) run: | - xcodebuild -scheme cmux -configuration Release -derivedDataPath build \ + xcodebuild -scheme cmux -configuration Release -derivedDataPath build-arm \ + -destination 'platform=macOS,arch=arm64' \ + -clonedSourcePackagesDirPath .spm-cache \ + CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build + + - name: Build universal app (Release) + run: | + xcodebuild -scheme cmux -configuration Release -derivedDataPath build-universal \ -destination 'generic/platform=macOS' \ -clonedSourcePackagesDirPath .spm-cache \ ARCHS="arm64 x86_64" \ @@ -163,8 +170,8 @@ jobs: - name: Verify universal binaries run: | set -euo pipefail - APP_BINARY="build/Build/Products/Release/cmux.app/Contents/MacOS/cmux" - CLI_BINARY="build/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" + APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" + CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" APP_ARCHS="$(lipo -archs "$APP_BINARY")" CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" echo "App binary architectures: $APP_ARCHS" @@ -172,34 +179,15 @@ jobs: [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] - - name: Inject nightly identity and metadata + - name: Inject nightly identities and metadata run: | set -euo pipefail - APP_DIR="build/Build/Products/Release" - APP_PLIST="${APP_DIR}/cmux.app/Contents/Info.plist" SHORT_SHA="${{ needs.decide.outputs.short_sha }}" + ARM_APP_DIR="build-arm/Build/Products/Release" + UNIVERSAL_APP_DIR="build-universal/Build/Products/Release" - # --- Separate app identity: "cmux NIGHTLY" with its own bundle ID --- - /usr/libexec/PlistBuddy -c "Set :CFBundleName cmux NIGHTLY" "$APP_PLIST" - /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName cmux NIGHTLY" "$APP_PLIST" - /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.cmuxterm.app.nightly" "$APP_PLIST" - - # Rename the .app bundle to match the display name - mv "${APP_DIR}/cmux.app" "${APP_DIR}/cmux NIGHTLY.app" - - # Update plist path after rename - APP_PLIST="${APP_DIR}/cmux NIGHTLY.app/Contents/Info.plist" - - # --- Sparkle: point at the nightly appcast --- - /usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$APP_PLIST" >/dev/null 2>&1 || true - /usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$APP_PLIST" >/dev/null 2>&1 || true - /usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string ${SPARKLE_PUBLIC_KEY}" "$APP_PLIST" - /usr/libexec/PlistBuddy -c "Add :SUFeedURL string https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast.xml" "$APP_PLIST" - - # Marketing version: append -nightly.YYYYMMDD so users can identify the channel and date - BASE_MARKETING=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PLIST") + BASE_MARKETING=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${ARM_APP_DIR}/cmux.app/Contents/Info.plist") NIGHTLY_DATE=$(date -u +%Y%m%d) - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}" "$APP_PLIST" # Build number: unique/monotonic per workflow run attempt so same-day # nightlies and reruns still compare as newer in Sparkle. @@ -209,23 +197,49 @@ jobs: else NIGHTLY_BUILD="${NIGHTLY_DATE}000000" fi - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${NIGHTLY_BUILD}" "$APP_PLIST" - - # Use an immutable DMG filename in appcast URLs so old appcasts keep - # pointing at matching archives while nightly assets roll forward. - NIGHTLY_DMG_IMMUTABLE="cmux-nightly-macos-${NIGHTLY_BUILD}.dmg" echo "NIGHTLY_BUILD=${NIGHTLY_BUILD}" >> "$GITHUB_ENV" - echo "NIGHTLY_DMG_IMMUTABLE=${NIGHTLY_DMG_IMMUTABLE}" >> "$GITHUB_ENV" - # Embed commit SHA for bug reports - /usr/libexec/PlistBuddy -c "Delete :CMUXCommit" "$APP_PLIST" >/dev/null 2>&1 || true - /usr/libexec/PlistBuddy -c "Add :CMUXCommit string ${SHORT_SHA}" "$APP_PLIST" + ARM_DMG_IMMUTABLE="cmux-nightly-macos-${NIGHTLY_BUILD}.dmg" + UNIVERSAL_DMG_IMMUTABLE="cmux-nightly-universal-macos-${NIGHTLY_BUILD}.dmg" + echo "NIGHTLY_DMG_IMMUTABLE=${ARM_DMG_IMMUTABLE}" >> "$GITHUB_ENV" + echo "NIGHTLY_UNIVERSAL_DMG_IMMUTABLE=${UNIVERSAL_DMG_IMMUTABLE}" >> "$GITHUB_ENV" + + prepare_variant() { + local app_dir="$1" + local bundle_id="$2" + local feed_url="$3" + local app_plist="$app_dir/cmux.app/Contents/Info.plist" + + /usr/libexec/PlistBuddy -c "Set :CFBundleName cmux NIGHTLY" "$app_plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName cmux NIGHTLY" "$app_plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${bundle_id}" "$app_plist" + /usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$app_plist" >/dev/null 2>&1 || true + /usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$app_plist" >/dev/null 2>&1 || true + /usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string ${SPARKLE_PUBLIC_KEY}" "$app_plist" + /usr/libexec/PlistBuddy -c "Add :SUFeedURL string ${feed_url}" "$app_plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}" "$app_plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${NIGHTLY_BUILD}" "$app_plist" + /usr/libexec/PlistBuddy -c "Delete :CMUXCommit" "$app_plist" >/dev/null 2>&1 || true + /usr/libexec/PlistBuddy -c "Add :CMUXCommit string ${SHORT_SHA}" "$app_plist" + mv "$app_dir/cmux.app" "$app_dir/cmux NIGHTLY.app" + } + + prepare_variant \ + "$ARM_APP_DIR" \ + "com.cmuxterm.app.nightly" \ + "https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast.xml" + prepare_variant \ + "$UNIVERSAL_APP_DIR" \ + "com.cmuxterm.app.nightly.universal" \ + "https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast-universal.xml" echo "Nightly app name: cmux NIGHTLY" - echo "Nightly bundle ID: com.cmuxterm.app.nightly" + echo "Nightly arm64 bundle ID: com.cmuxterm.app.nightly" + echo "Nightly universal bundle ID: com.cmuxterm.app.nightly.universal" echo "Nightly marketing version: ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}" echo "Nightly build number: ${NIGHTLY_BUILD}" - echo "Nightly immutable DMG: ${NIGHTLY_DMG_IMMUTABLE}" + echo "Nightly arm64 immutable DMG: ${ARM_DMG_IMMUTABLE}" + echo "Nightly universal immutable DMG: ${UNIVERSAL_DMG_IMMUTABLE}" echo "Commit SHA: ${SHORT_SHA}" - name: Import signing cert @@ -251,7 +265,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain security list-keychains -d user -s build.keychain - - name: Codesign app + - name: Codesign apps env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | @@ -259,16 +273,20 @@ jobs: echo "Missing APPLE_SIGNING_IDENTITY secret" >&2 exit 1 fi - APP_PATH="build/Build/Products/Release/cmux NIGHTLY.app" ENTITLEMENTS="cmux.entitlements" - CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" - if [ -f "$CLI_PATH" ]; then - /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH" - fi - /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH" - /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH" + for APP_PATH in \ + "build-arm/Build/Products/Release/cmux NIGHTLY.app" \ + "build-universal/Build/Products/Release/cmux NIGHTLY.app" + do + CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" + if [ -f "$CLI_PATH" ]; then + /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH" + fi + /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH" + /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH" + done - - name: Notarize app and dmg + - name: Notarize apps and dmgs env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} @@ -279,41 +297,62 @@ jobs: echo "Missing notarization secrets (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID)" >&2 exit 1 fi - APP_PATH="build/Build/Products/Release/cmux NIGHTLY.app" - ZIP_SUBMIT="cmux-nightly-notary.zip" - DMG_RELEASE="cmux-nightly-macos.dmg" - ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_SUBMIT" - APP_SUBMIT_JSON="$(xcrun notarytool submit "$ZIP_SUBMIT" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" - APP_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$APP_SUBMIT_JSON")" - APP_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$APP_SUBMIT_JSON")" - if [ "$APP_STATUS" != "Accepted" ]; then - echo "App notarization failed with status: $APP_STATUS" >&2 - xcrun notarytool log "$APP_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true - exit 1 - fi - xcrun stapler staple "$APP_PATH" - xcrun stapler validate "$APP_PATH" - spctl -a -vv --type execute "$APP_PATH" - rm -f "$ZIP_SUBMIT" - create-dmg \ - --identity="$APPLE_SIGNING_IDENTITY" \ - "$APP_PATH" \ - ./ - mv ./"cmux NIGHTLY"*.dmg "$DMG_RELEASE" 2>/dev/null || mv ./cmux*.dmg "$DMG_RELEASE" - DMG_SUBMIT_JSON="$(xcrun notarytool submit "$DMG_RELEASE" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" - DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")" - DMG_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$DMG_SUBMIT_JSON")" - if [ "$DMG_STATUS" != "Accepted" ]; then - echo "DMG notarization failed with status: $DMG_STATUS" >&2 - xcrun notarytool log "$DMG_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true - exit 1 - fi - xcrun stapler staple "$DMG_RELEASE" - xcrun stapler validate "$DMG_RELEASE" + notarize_and_package() { + local app_path="$1" + local dmg_release="$2" + local dmg_immutable="$3" + local zip_submit="${dmg_release%.dmg}-notary.zip" + local dmg_tmp_dir + local created_dmg - # Keep a stable filename for humans and an immutable filename used - # by appcast URLs to prevent signature/asset mismatch races. - cp "$DMG_RELEASE" "$NIGHTLY_DMG_IMMUTABLE" + ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_submit" + APP_SUBMIT_JSON="$(xcrun notarytool submit "$zip_submit" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" + APP_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$APP_SUBMIT_JSON")" + APP_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$APP_SUBMIT_JSON")" + if [ "$APP_STATUS" != "Accepted" ]; then + echo "App notarization failed for $app_path with status: $APP_STATUS" >&2 + xcrun notarytool log "$APP_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true + exit 1 + fi + xcrun stapler staple "$app_path" + xcrun stapler validate "$app_path" + spctl -a -vv --type execute "$app_path" + rm -f "$zip_submit" + + dmg_tmp_dir="$(mktemp -d)" + create-dmg \ + --identity="$APPLE_SIGNING_IDENTITY" \ + "$app_path" \ + "$dmg_tmp_dir" + created_dmg="$(find "$dmg_tmp_dir" -maxdepth 1 -name '*.dmg' | head -n 1)" + if [ -z "$created_dmg" ]; then + echo "Failed to locate created DMG for $app_path" >&2 + exit 1 + fi + mv "$created_dmg" "$dmg_release" + rm -rf "$dmg_tmp_dir" + + DMG_SUBMIT_JSON="$(xcrun notarytool submit "$dmg_release" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" + DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")" + DMG_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$DMG_SUBMIT_JSON")" + if [ "$DMG_STATUS" != "Accepted" ]; then + echo "DMG notarization failed for $dmg_release with status: $DMG_STATUS" >&2 + xcrun notarytool log "$DMG_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true + exit 1 + fi + xcrun stapler staple "$dmg_release" + xcrun stapler validate "$dmg_release" + cp "$dmg_release" "$dmg_immutable" + } + + notarize_and_package \ + "build-arm/Build/Products/Release/cmux NIGHTLY.app" \ + "cmux-nightly-macos.dmg" \ + "$NIGHTLY_DMG_IMMUTABLE" + notarize_and_package \ + "build-universal/Build/Products/Release/cmux NIGHTLY.app" \ + "cmux-nightly-universal-macos.dmg" \ + "$NIGHTLY_UNIVERSAL_DMG_IMMUTABLE" - name: Upload dSYMs to Sentry env: @@ -326,9 +365,11 @@ jobs: exit 0 fi brew install getsentry/tools/sentry-cli || true - sentry-cli debug-files upload --include-sources build/Build/Products/Release/ + sentry-cli debug-files upload --include-sources \ + build-arm/Build/Products/Release/ \ + build-universal/Build/Products/Release/ - - name: Generate Sparkle appcast (nightly) + - name: Generate Sparkle appcasts (nightly) env: SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} run: | @@ -337,6 +378,7 @@ jobs: exit 1 fi ./scripts/sparkle_generate_appcast.sh "$NIGHTLY_DMG_IMMUTABLE" nightly appcast.xml + ./scripts/sparkle_generate_appcast.sh "$NIGHTLY_UNIVERSAL_DMG_IMMUTABLE" nightly appcast-universal.xml - name: Upload branch nightly artifacts if: needs.decide.outputs.should_publish != 'true' @@ -345,7 +387,9 @@ jobs: name: cmux-nightly-${{ needs.decide.outputs.short_sha }} path: | cmux-nightly-macos*.dmg + cmux-nightly-universal-macos*.dmg appcast.xml + appcast-universal.xml if-no-files-found: error - name: Move nightly tag to built commit @@ -368,13 +412,19 @@ jobs: body: | Automated nightly build for `${{ needs.decide.outputs.short_sha }}`. - **cmux NIGHTLY** is a separate app (bundle ID `com.cmuxterm.app.nightly`) that can be installed alongside the stable release. It receives nightly updates automatically via its own Sparkle feed. + **cmux NIGHTLY** has two update tracks: + - Apple Silicon: bundle ID `com.cmuxterm.app.nightly`, feed `appcast.xml` + - Universal: bundle ID `com.cmuxterm.app.nightly.universal`, feed `appcast-universal.xml` [Download cmux-nightly-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg) + [Download cmux-nightly-universal-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-universal-macos.dmg) files: | cmux-nightly-macos-${{ github.run_id }}*.dmg cmux-nightly-macos.dmg + cmux-nightly-universal-macos-${{ github.run_id }}*.dmg + cmux-nightly-universal-macos.dmg appcast.xml + appcast-universal.xml overwrite_files: true - name: Cleanup keychain