Separate cmux NIGHTLY as standalone app with its own bundle ID (#164)
The nightly build is now a distinct app called "cmux NIGHTLY" with bundle ID com.cmuxterm.app.nightly, allowing side-by-side installation with the stable release. The nightly appcast URL is baked into the app's Info.plist by CI, so no in-app channel switching is needed. - Nightly workflow: rename app to "cmux NIGHTLY", set bundle ID to com.cmuxterm.app.nightly, hardcode nightly Sparkle feed URL, publish DMG as cmux-nightly-macos.dmg - Remove "Receive Nightly Builds" toggle from settings - Remove UpdateChannelSettings enum and simplify feed URL resolution to just use SUFeedURL from Info.plist - Remove UpdateChannelSettingsTests (no longer applicable)
This commit is contained in:
parent
c11453fa6c
commit
707be44aaf
5 changed files with 37 additions and 109 deletions
37
.github/workflows/nightly.yml
vendored
37
.github/workflows/nightly.yml
vendored
|
|
@ -151,16 +151,29 @@ jobs:
|
|||
run: |
|
||||
xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build
|
||||
|
||||
- name: Inject Sparkle keys and nightly metadata
|
||||
- name: Inject nightly identity and metadata
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APP_PLIST="build/Build/Products/Release/cmux.app/Contents/Info.plist"
|
||||
APP_DIR="build/Build/Products/Release"
|
||||
APP_PLIST="${APP_DIR}/cmux.app/Contents/Info.plist"
|
||||
SHORT_SHA="${{ needs.decide.outputs.short_sha }}"
|
||||
|
||||
# --- 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/latest/download/appcast.xml" "$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")
|
||||
|
|
@ -175,6 +188,8 @@ jobs:
|
|||
/usr/libexec/PlistBuddy -c "Delete :CMUXCommit" "$APP_PLIST" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Add :CMUXCommit string ${SHORT_SHA}" "$APP_PLIST"
|
||||
|
||||
echo "Nightly app name: cmux NIGHTLY"
|
||||
echo "Nightly bundle ID: com.cmuxterm.app.nightly"
|
||||
echo "Nightly marketing version: ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}"
|
||||
echo "Nightly build number: ${NIGHTLY_BUILD}"
|
||||
echo "Commit SHA: ${SHORT_SHA}"
|
||||
|
|
@ -210,7 +225,7 @@ jobs:
|
|||
echo "Missing APPLE_SIGNING_IDENTITY secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
APP_PATH="build/Build/Products/Release/cmux.app"
|
||||
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
|
||||
|
|
@ -230,9 +245,9 @@ 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.app"
|
||||
APP_PATH="build/Build/Products/Release/cmux NIGHTLY.app"
|
||||
ZIP_SUBMIT="cmux-nightly-notary.zip"
|
||||
DMG_RELEASE="cmux-macos.dmg"
|
||||
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")"
|
||||
|
|
@ -250,7 +265,7 @@ jobs:
|
|||
--identity="$APPLE_SIGNING_IDENTITY" \
|
||||
"$APP_PATH" \
|
||||
./
|
||||
mv ./cmux*.dmg "$DMG_RELEASE"
|
||||
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")"
|
||||
|
|
@ -270,7 +285,7 @@ jobs:
|
|||
echo "Missing SPARKLE_PRIVATE_KEY secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
./scripts/sparkle_generate_appcast.sh cmux-macos.dmg nightly appcast.xml
|
||||
./scripts/sparkle_generate_appcast.sh cmux-nightly-macos.dmg nightly appcast.xml
|
||||
|
||||
- name: Move nightly tag to built commit
|
||||
run: |
|
||||
|
|
@ -289,8 +304,12 @@ jobs:
|
|||
make_latest: false
|
||||
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.
|
||||
|
||||
[Download cmux-nightly-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
files: |
|
||||
cmux-macos.dmg
|
||||
cmux-nightly-macos.dmg
|
||||
appcast.xml
|
||||
overwrite_files: true
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ class UpdateController {
|
|||
"SUEnableAutomaticChecks": false,
|
||||
"SUSendProfileInfo": false,
|
||||
"SUAutomaticallyUpdate": false,
|
||||
UpdateChannelSettings.includeNightlyBuildsKey: UpdateChannelSettings.defaultIncludeNightlyBuilds,
|
||||
])
|
||||
|
||||
let hostBundle = Bundle.main
|
||||
|
|
|
|||
|
|
@ -1,26 +1,6 @@
|
|||
import Sparkle
|
||||
import Cocoa
|
||||
|
||||
enum UpdateChannelSettings {
|
||||
static let includeNightlyBuildsKey = "cmux.includeNightlyBuilds"
|
||||
static let defaultIncludeNightlyBuilds = false
|
||||
|
||||
static let stableFeedURL = "https://github.com/manaflow-ai/cmux/releases/latest/download/appcast.xml"
|
||||
static let nightlyFeedURL = "https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast.xml"
|
||||
|
||||
static func resolvedFeedURLString(
|
||||
infoFeedURL: String?,
|
||||
defaults: UserDefaults = .standard
|
||||
) -> (url: String, isNightly: Bool, usedFallback: Bool) {
|
||||
let stableURL = (infoFeedURL?.isEmpty == false) ? infoFeedURL! : stableFeedURL
|
||||
let includeNightlyBuilds = defaults.bool(forKey: includeNightlyBuildsKey)
|
||||
if includeNightlyBuilds {
|
||||
return (url: nightlyFeedURL, isNightly: true, usedFallback: false)
|
||||
}
|
||||
return (url: stableURL, isNightly: false, usedFallback: stableURL == stableFeedURL)
|
||||
}
|
||||
}
|
||||
|
||||
extension UpdateDriver: SPUUpdaterDelegate {
|
||||
func feedURLString(for updater: SPUUpdater) -> String? {
|
||||
#if DEBUG
|
||||
|
|
@ -31,11 +11,15 @@ extension UpdateDriver: SPUUpdaterDelegate {
|
|||
return override
|
||||
}
|
||||
#endif
|
||||
let infoURL = Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") as? String
|
||||
let resolved = UpdateChannelSettings.resolvedFeedURLString(infoFeedURL: infoURL)
|
||||
UpdateLogStore.shared.append("update channel: \(resolved.isNightly ? "nightly" : "stable")")
|
||||
recordFeedURLString(resolved.url, usedFallback: resolved.usedFallback)
|
||||
return resolved.url
|
||||
// The feed URL is baked into Info.plist at build time:
|
||||
// - Stable releases use the stable appcast URL
|
||||
// - cmux NIGHTLY has the nightly appcast URL injected by CI
|
||||
let feedURL = Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") as? String
|
||||
let isNightly = feedURL?.contains("/nightly/") == true
|
||||
UpdateLogStore.shared.append("update channel: \(isNightly ? "nightly" : "stable")")
|
||||
let usedFallback = feedURL == nil || feedURL?.isEmpty == true
|
||||
recordFeedURLString(feedURL ?? "https://github.com/manaflow-ai/cmux/releases/latest/download/appcast.xml", usedFallback: usedFallback)
|
||||
return feedURL
|
||||
}
|
||||
|
||||
/// Called when an update is scheduled to install silently,
|
||||
|
|
|
|||
|
|
@ -2239,7 +2239,6 @@ struct SettingsView: View {
|
|||
@AppStorage(BrowserSearchSettings.searchEngineKey) private var browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
|
||||
@AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
@AppStorage(NotificationBadgeSettings.dockBadgeEnabledKey) private var notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled
|
||||
@AppStorage(UpdateChannelSettings.includeNightlyBuildsKey) private var includeNightlyBuilds = UpdateChannelSettings.defaultIncludeNightlyBuilds
|
||||
@AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
@State private var shortcutResetToken = UUID()
|
||||
@State private var topBlurOpacity: Double = 0
|
||||
|
|
@ -2317,25 +2316,6 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
SettingsSectionHeader(title: "Updates")
|
||||
SettingsCard {
|
||||
SettingsCardRow(
|
||||
"Receive Nightly Builds",
|
||||
subtitle: includeNightlyBuilds
|
||||
? "Using nightly update channel. Builds may be less stable."
|
||||
: "Using stable update channel."
|
||||
) {
|
||||
Toggle("", isOn: $includeNightlyBuilds)
|
||||
.labelsHidden()
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("SettingsIncludeNightlyBuildsToggle")
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardNote("Nightly builds are published from the latest main branch commit when available.")
|
||||
}
|
||||
|
||||
SettingsSectionHeader(title: "Automation")
|
||||
SettingsCard {
|
||||
SettingsCardRow(
|
||||
|
|
@ -2551,7 +2531,6 @@ struct SettingsView: View {
|
|||
browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
|
||||
browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled
|
||||
includeNightlyBuilds = UpdateChannelSettings.defaultIncludeNightlyBuilds
|
||||
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
KeyboardShortcutSettings.resetAll()
|
||||
shortcutResetToken = UUID()
|
||||
|
|
|
|||
|
|
@ -339,59 +339,6 @@ final class WorkspacePlacementSettingsTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
final class UpdateChannelSettingsTests: XCTestCase {
|
||||
func testDefaultNightlyPreferenceIsDisabled() {
|
||||
XCTAssertFalse(UpdateChannelSettings.defaultIncludeNightlyBuilds)
|
||||
}
|
||||
|
||||
func testResolvedFeedFallsBackToStableWhenInfoFeedMissing() {
|
||||
let suiteName = "UpdateChannelSettingsTests.MissingInfo.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
XCTFail("Failed to create isolated UserDefaults suite")
|
||||
return
|
||||
}
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let resolved = UpdateChannelSettings.resolvedFeedURLString(infoFeedURL: nil, defaults: defaults)
|
||||
XCTAssertEqual(resolved.url, UpdateChannelSettings.stableFeedURL)
|
||||
XCTAssertFalse(resolved.isNightly)
|
||||
XCTAssertTrue(resolved.usedFallback)
|
||||
}
|
||||
|
||||
func testResolvedFeedUsesInfoFeedForStableChannel() {
|
||||
let suiteName = "UpdateChannelSettingsTests.InfoFeed.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
XCTFail("Failed to create isolated UserDefaults suite")
|
||||
return
|
||||
}
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let infoFeed = "https://example.com/custom/appcast.xml"
|
||||
let resolved = UpdateChannelSettings.resolvedFeedURLString(infoFeedURL: infoFeed, defaults: defaults)
|
||||
XCTAssertEqual(resolved.url, infoFeed)
|
||||
XCTAssertFalse(resolved.isNightly)
|
||||
XCTAssertFalse(resolved.usedFallback)
|
||||
}
|
||||
|
||||
func testResolvedFeedUsesNightlyWhenPreferenceEnabled() {
|
||||
let suiteName = "UpdateChannelSettingsTests.Nightly.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
XCTFail("Failed to create isolated UserDefaults suite")
|
||||
return
|
||||
}
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
defaults.set(true, forKey: UpdateChannelSettings.includeNightlyBuildsKey)
|
||||
let resolved = UpdateChannelSettings.resolvedFeedURLString(
|
||||
infoFeedURL: "https://example.com/custom/appcast.xml",
|
||||
defaults: defaults
|
||||
)
|
||||
XCTAssertEqual(resolved.url, UpdateChannelSettings.nightlyFeedURL)
|
||||
XCTAssertTrue(resolved.isNightly)
|
||||
XCTAssertFalse(resolved.usedFallback)
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkspaceReorderTests: XCTestCase {
|
||||
@MainActor
|
||||
func testReorderWorkspaceMovesWorkspaceToRequestedIndex() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue