name: Nightly macOS build on: push: branches: [main] workflow_dispatch: inputs: force: description: Force a nightly build even if main has no new commits required: false default: false type: boolean concurrency: group: nightly-build-${{ github.ref_name }} # Queue concurrent runs instead of canceling them so no build is lost. cancel-in-progress: false permissions: contents: write attestations: write id-token: write env: CREATE_DMG_VERSION: 8.0.0 jobs: decide: runs-on: ubuntu-latest outputs: should_build: ${{ steps.decide.outputs.should_build }} head_sha: ${{ steps.decide.outputs.head_sha }} short_sha: ${{ steps.decide.outputs.short_sha }} should_publish: ${{ steps.decide.outputs.should_publish }} steps: - name: Decide whether a nightly build is needed id: decide uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: FORCE_BUILD: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force == 'true' && 'true' || 'false' }} with: script: | const forceBuild = process.env.FORCE_BUILD === 'true'; const { owner, repo } = context.repo; const requestedRef = context.ref.startsWith('refs/heads/') ? context.ref.replace('refs/heads/', '') : 'main'; const isMainRef = requestedRef === 'main'; let headSha = context.sha; if (isMainRef) { const branch = await github.rest.repos.getBranch({ owner, repo, branch: 'main', }); headSha = branch.data.commit.sha; } let nightlySha = null; if (isMainRef) { try { const ref = await github.rest.git.getRef({ owner, repo, ref: 'tags/nightly', }); if (ref.data.object.type === 'commit') { nightlySha = ref.data.object.sha; } else if (ref.data.object.type === 'tag') { const tagObject = await github.rest.git.getTag({ owner, repo, tag_sha: ref.data.object.sha, }); nightlySha = tagObject.data.object.sha; } } catch (error) { if (error.status !== 404) throw error; } } const shouldBuild = !isMainRef || forceBuild || nightlySha !== headSha; core.setOutput('should_build', shouldBuild ? 'true' : 'false'); core.setOutput('head_sha', headSha); core.setOutput('short_sha', headSha.slice(0, 7)); core.setOutput('should_publish', isMainRef ? 'true' : 'false'); core.summary .addHeading('Nightly build decision') .addTable([ [{data: 'requested ref', header: true}, requestedRef], [{data: 'build HEAD', header: true}, headSha], [{data: 'nightly tag', header: true}, nightlySha ?? '(missing)'], [{data: 'force build', header: true}, String(forceBuild)], [{data: 'should build', header: true}, String(shouldBuild)], [{data: 'should publish', header: true}, String(isMainRef)], ]) .write(); build-sign-notarize-nightly: needs: decide if: needs.decide.outputs.should_build == 'true' runs-on: warp-macos-26-arm64-6x timeout-minutes: 20 steps: - name: Checkout build ref uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ needs.decide.outputs.head_sha }} submodules: recursive - name: Check whether build commit is still current main HEAD before build if: needs.decide.outputs.should_publish == 'true' id: current_head_prebuild run: | set -euo pipefail CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')" BUILD_SHA="${{ needs.decide.outputs.head_sha }}" if [ "$CURRENT_MAIN_SHA" = "$BUILD_SHA" ]; then STILL_CURRENT=true else STILL_CURRENT=false fi echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT" { echo "### Pre-build publish guard" echo echo "- build sha: \`$BUILD_SHA\`" echo "- current main sha: \`$CURRENT_MAIN_SHA\`" echo "- continue build/sign/publish: \`$STILL_CURRENT\`" } >> "$GITHUB_STEP_SUMMARY" - name: Select Xcode if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | set -euo pipefail if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then XCODE_DIR="/Applications/Xcode.app/Contents/Developer" else XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)" if [ -n "$XCODE_APP" ]; then XCODE_DIR="$XCODE_APP/Contents/Developer" else echo "No Xcode.app found under /Applications" >&2 exit 1 fi fi echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV" export DEVELOPER_DIR="$XCODE_DIR" xcodebuild -version xcrun --sdk macosx --show-sdk-path - name: Install build deps if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | ZIG_REQUIRED="0.15.2" if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then echo "zig ${ZIG_REQUIRED} already installed" else echo "Installing zig ${ZIG_REQUIRED} from tarball" curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz tar xf /tmp/zig.tar.xz -C /tmp sudo mkdir -p /usr/local/bin /usr/local/lib sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig export PATH="/usr/local/bin:$PATH" zig version fi npm install --global "create-dmg@${CREATE_DMG_VERSION}" - name: Download pre-built GhosttyKit.xcframework if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | ./scripts/download-prebuilt-ghosttykit.sh - name: Cache Swift packages if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 with: path: .spm-cache key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} restore-keys: spm- - name: Setup Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: daemon/remote/go.mod - name: Derive Sparkle public key from private key if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' env: SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} run: | if [ -z "$SPARKLE_PRIVATE_KEY" ]; then echo "Missing SPARKLE_PRIVATE_KEY secret" >&2 exit 1 fi DERIVED_PUBLIC_KEY=$(swift scripts/derive_sparkle_public_key.swift "$SPARKLE_PRIVATE_KEY") echo "Derived Sparkle public key: $DERIVED_PUBLIC_KEY" echo "SPARKLE_PUBLIC_KEY=$DERIVED_PUBLIC_KEY" >> "$GITHUB_ENV" - name: Build universal nightly app (Release) if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | xcodebuild -scheme cmux -configuration Release -derivedDataPath build-universal \ -destination 'generic/platform=macOS' \ -clonedSourcePackagesDirPath .spm-cache \ ARCHS="arm64 x86_64" \ ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build - name: Verify nightly binary architectures if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | set -euo pipefail 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" HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty" APP_ARCHS="$(lipo -archs "$APP_BINARY")" CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")" echo "App binary architectures: $APP_ARCHS" echo "CLI binary architectures: $CLI_ARCHS" echo "Ghostty helper architectures: $HELPER_ARCHS" [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] [[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]] - name: Run CLI version memory guard regression if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | set -euo pipefail CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" [ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; } CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py - name: Check whether build commit is still current main HEAD after build if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' id: current_head_postbuild run: | set -euo pipefail CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')" BUILD_SHA="${{ needs.decide.outputs.head_sha }}" if [ "$CURRENT_MAIN_SHA" = "$BUILD_SHA" ]; then STILL_CURRENT=true else STILL_CURRENT=false fi echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT" { echo "### Post-build publish guard" echo echo "- build sha: \`$BUILD_SHA\`" echo "- current main sha: \`$CURRENT_MAIN_SHA\`" echo "- continue signing/publish: \`$STILL_CURRENT\`" } >> "$GITHUB_STEP_SUMMARY" - name: Inject nightly identities and metadata if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') run: | set -euo pipefail SHORT_SHA="${{ needs.decide.outputs.short_sha }}" APP_DIR="build-universal/Build/Products/Release" BASE_MARKETING=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${APP_DIR}/cmux.app/Contents/Info.plist") NIGHTLY_DATE=$(date -u +%Y%m%d) # Build number: unique/monotonic per workflow run attempt so same-day # nightlies and reruns still compare as newer in Sparkle. if [ -n "${GITHUB_RUN_ID:-}" ]; then RUN_ATTEMPT="$(printf '%02d' "${GITHUB_RUN_ATTEMPT:-1}")" NIGHTLY_BUILD="${GITHUB_RUN_ID}${RUN_ATTEMPT}" else NIGHTLY_BUILD="${NIGHTLY_DATE}000000" fi NIGHTLY_MARKETING_VERSION="${BASE_MARKETING}-nightly.${NIGHTLY_BUILD}" echo "NIGHTLY_BUILD=${NIGHTLY_BUILD}" >> "$GITHUB_ENV" echo "NIGHTLY_MARKETING_VERSION=${NIGHTLY_MARKETING_VERSION}" >> "$GITHUB_ENV" echo "NIGHTLY_REMOTE_DAEMON_VERSION=${NIGHTLY_MARKETING_VERSION}" >> "$GITHUB_ENV" NIGHTLY_DMG_IMMUTABLE="cmux-nightly-macos-${NIGHTLY_BUILD}.dmg" echo "NIGHTLY_DMG_IMMUTABLE=${NIGHTLY_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 ${NIGHTLY_MARKETING_VERSION}" "$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 \ "$APP_DIR" \ "com.cmuxterm.app.nightly" \ "https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast.xml" echo "Nightly app name: cmux NIGHTLY" echo "Nightly bundle ID: com.cmuxterm.app.nightly" echo "Nightly marketing version: ${NIGHTLY_MARKETING_VERSION}" echo "Nightly build number: ${NIGHTLY_BUILD}" echo "Nightly immutable DMG: ${NIGHTLY_DMG_IMMUTABLE}" echo "Commit SHA: ${SHORT_SHA}" - name: Build remote daemon nightly assets and inject manifest if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') run: | set -euo pipefail # Build with --asset-suffix so manifest download URLs point to # immutable, build-specific asset names (e.g. cmuxd-remote-darwin-arm64-2362248028801). # This prevents checksum mismatches when a newer nightly overwrites # the shared "latest" assets on the release. ./scripts/build_remote_daemon_release_assets.sh \ --version "$NIGHTLY_REMOTE_DAEMON_VERSION" \ --release-tag "nightly" \ --repo "manaflow-ai/cmux" \ --output-dir "remote-daemon-assets" \ --asset-suffix "$NIGHTLY_BUILD" MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json")" APP_PLIST="build-universal/Build/Products/Release/cmux NIGHTLY.app/Contents/Info.plist" if [ ! -f "$APP_PLIST" ]; then echo "Missing nightly app Info.plist at $APP_PLIST" >&2 exit 1 fi plutil -remove CMUXRemoteDaemonManifestJSON "$APP_PLIST" >/dev/null 2>&1 || true plutil -insert CMUXRemoteDaemonManifestJSON -string "$MANIFEST_JSON" "$APP_PLIST" # Also create unsuffixed "latest" copies for the release page and # any tooling that fetches the generic asset names. The manifest's # downloadURLs still point to the versioned filenames (intentional: # the live manifest is used by the client-side checksum fallback # which only reads sha256, not downloadURL). The unsuffixed copies # are convenience aliases and don't carry build-provenance # attestation (attested versioned files are canonical). for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \ "remote-daemon-assets/cmuxd-remote-${platform}" done # Regenerate unsuffixed checksums with generic filenames so # `shasum -c cmuxd-remote-checksums.txt` works against the aliases. ( cd remote-daemon-assets shasum -a 256 \ cmuxd-remote-darwin-arm64 \ cmuxd-remote-darwin-amd64 \ cmuxd-remote-linux-arm64 \ cmuxd-remote-linux-amd64 \ > cmuxd-remote-checksums.txt ) cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \ "remote-daemon-assets/cmuxd-remote-manifest.json" - name: Import signing cert if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') env: APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | if [ -z "$APPLE_CERTIFICATE_BASE64" ]; then echo "Missing APPLE_CERTIFICATE_BASE64 secret" >&2 exit 1 fi if [ -z "$APPLE_CERTIFICATE_PASSWORD" ]; then echo "Missing APPLE_CERTIFICATE_PASSWORD secret" >&2 exit 1 fi KEYCHAIN_PASSWORD="$(uuidgen)" echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > /tmp/cert.p12 security delete-keychain build.keychain >/dev/null 2>&1 || true security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -lut 21600 build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security import /tmp/cert.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security 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 apps if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | if [ -z "$APPLE_SIGNING_IDENTITY" ]; then echo "Missing APPLE_SIGNING_IDENTITY secret" >&2 exit 1 fi ENTITLEMENTS="cmux.entitlements" for APP_PATH in \ "build-universal/Build/Products/Release/cmux NIGHTLY.app" do CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty" if [ -f "$CLI_PATH" ]; then /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH" fi if [ -f "$HELPER_PATH" ]; then /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$HELPER_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 apps and dmgs if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | if [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then echo "Missing notarization secrets (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID)" >&2 exit 1 fi 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 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-universal/Build/Products/Release/cmux NIGHTLY.app" \ "cmux-nightly-macos.dmg" \ "$NIGHTLY_DMG_IMMUTABLE" - name: Upload dSYMs to Sentry if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: manaflow SENTRY_PROJECT: cmuxterm-macos run: | if [ -z "$SENTRY_AUTH_TOKEN" ]; then echo "SENTRY_AUTH_TOKEN not set, skipping dSYM upload" exit 0 fi brew install getsentry/tools/sentry-cli || true sentry-cli debug-files upload --include-sources \ build-universal/Build/Products/Release/ - name: Generate Sparkle appcasts (nightly) if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') env: SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} run: | if [ -z "$SPARKLE_PRIVATE_KEY" ]; then echo "Missing SPARKLE_PRIVATE_KEY secret" >&2 exit 1 fi ./scripts/sparkle_generate_appcast.sh "$NIGHTLY_DMG_IMMUTABLE" nightly appcast.xml # Keep the legacy universal feed alive long enough for older nightly # installs to migrate onto the unified nightly appcast. cp appcast.xml appcast-universal.xml - name: Attest remote daemon nightly assets if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: | remote-daemon-assets/cmuxd-remote-darwin-arm64-${{ env.NIGHTLY_BUILD }} remote-daemon-assets/cmuxd-remote-darwin-amd64-${{ env.NIGHTLY_BUILD }} remote-daemon-assets/cmuxd-remote-linux-arm64-${{ env.NIGHTLY_BUILD }} remote-daemon-assets/cmuxd-remote-linux-amd64-${{ env.NIGHTLY_BUILD }} remote-daemon-assets/cmuxd-remote-checksums-${{ env.NIGHTLY_BUILD }}.txt remote-daemon-assets/cmuxd-remote-manifest-${{ env.NIGHTLY_BUILD }}.json - name: Upload branch nightly artifacts if: needs.decide.outputs.should_publish != 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: cmux-nightly-${{ needs.decide.outputs.short_sha }} path: | cmux-nightly-macos*.dmg appcast.xml remote-daemon-assets/cmuxd-remote-* appcast-universal.xml if-no-files-found: error - name: Move nightly tag to built commit if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true' run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git tag -f nightly "${{ needs.decide.outputs.head_sha }}" git push origin refs/tags/nightly --force - name: Publish nightly release assets if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true' uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: tag_name: nightly name: Nightly prerelease: true make_latest: false body: | Automated nightly build for `${{ needs.decide.outputs.short_sha }}`. **cmux NIGHTLY** is published as a universal app: - bundle ID `com.cmuxterm.app.nightly` - feed `appcast.xml` - compatibility feed `appcast-universal.xml` for older universal nightlies [Download cmux-nightly-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg) files: | cmux-nightly-macos-${{ github.run_id }}*.dmg cmux-nightly-macos.dmg appcast.xml remote-daemon-assets/cmuxd-remote-* appcast-universal.xml overwrite_files: true - name: Cleanup keychain if: always() run: | security delete-keychain build.keychain >/dev/null 2>&1 || true rm -f /tmp/cert.p12