Merge pull request #1067 from manaflow-ai/task-universal-macos-build
Publish separate universal nightly track
This commit is contained in:
commit
0f29735635
9 changed files with 361 additions and 122 deletions
2
.github/workflows/build-ghosttykit.yml
vendored
2
.github/workflows/build-ghosttykit.yml
vendored
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Doptimize=ReleaseFast
|
||||
cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
|
||||
- name: Package xcframework
|
||||
if: steps.check-release.outputs.exists == 'false'
|
||||
|
|
|
|||
318
.github/workflows/nightly.yml
vendored
318
.github/workflows/nightly.yml
vendored
|
|
@ -12,7 +12,7 @@ on:
|
|||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: nightly-build
|
||||
group: nightly-build-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
|
|
@ -28,6 +28,7 @@ jobs:
|
|||
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
|
||||
|
|
@ -38,46 +39,58 @@ jobs:
|
|||
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';
|
||||
|
||||
const branch = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: 'main',
|
||||
});
|
||||
const headSha = branch.data.commit.sha;
|
||||
|
||||
let nightlySha = null;
|
||||
try {
|
||||
const ref = await github.rest.git.getRef({
|
||||
let headSha = context.sha;
|
||||
if (isMainRef) {
|
||||
const branch = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
ref: 'tags/nightly',
|
||||
branch: 'main',
|
||||
});
|
||||
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;
|
||||
headSha = branch.data.commit.sha;
|
||||
}
|
||||
|
||||
const shouldBuild = forceBuild || nightlySha !== headSha;
|
||||
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: 'main HEAD', header: true}, headSha],
|
||||
[{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();
|
||||
|
||||
|
|
@ -86,7 +99,7 @@ jobs:
|
|||
if: needs.decide.outputs.should_build == 'true'
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout main
|
||||
- name: Checkout build ref
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ needs.decide.outputs.head_sha }}
|
||||
|
|
@ -138,40 +151,53 @@ 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 \
|
||||
ARCHS="arm64" \
|
||||
ONLY_ACTIVE_ARCH=YES \
|
||||
CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build
|
||||
|
||||
- name: Inject nightly identity and metadata
|
||||
- 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" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build
|
||||
|
||||
- name: Verify nightly binary architectures
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ARM_APP_BINARY="build-arm/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
|
||||
ARM_CLI_BINARY="build-arm/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"
|
||||
ARM_APP_ARCHS="$(lipo -archs "$ARM_APP_BINARY")"
|
||||
ARM_CLI_ARCHS="$(lipo -archs "$ARM_CLI_BINARY")"
|
||||
APP_ARCHS="$(lipo -archs "$APP_BINARY")"
|
||||
CLI_ARCHS="$(lipo -archs "$CLI_BINARY")"
|
||||
echo "Arm app binary architectures: $ARM_APP_ARCHS"
|
||||
echo "Arm CLI binary architectures: $ARM_CLI_ARCHS"
|
||||
echo "App binary architectures: $APP_ARCHS"
|
||||
echo "CLI binary architectures: $CLI_ARCHS"
|
||||
[[ "$ARM_APP_ARCHS" == "arm64" ]]
|
||||
[[ "$ARM_CLI_ARCHS" == "arm64" ]]
|
||||
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
|
||||
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
|
||||
|
||||
- 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.
|
||||
|
|
@ -181,23 +207,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
|
||||
|
|
@ -223,7 +275,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: |
|
||||
|
|
@ -231,16 +283,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 }}
|
||||
|
|
@ -251,41 +307,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:
|
||||
|
|
@ -298,9 +375,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: |
|
||||
|
|
@ -309,8 +388,22 @@ 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'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
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
|
||||
if: needs.decide.outputs.should_publish == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
|
|
@ -319,6 +412,7 @@ jobs:
|
|||
git push origin refs/tags/nightly --force
|
||||
|
||||
- name: Publish nightly release assets
|
||||
if: needs.decide.outputs.should_publish == 'true'
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
tag_name: nightly
|
||||
|
|
@ -328,13 +422,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
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -des
|
|||
When rebuilding GhosttyKit.xcframework, always use Release optimizations:
|
||||
|
||||
```bash
|
||||
cd ghostty && zig build -Demit-xcframework=true -Doptimize=ReleaseFast
|
||||
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
When rebuilding cmuxd for release/bundling, always use ReleaseFast:
|
||||
|
|
|
|||
|
|
@ -784,6 +784,7 @@
|
|||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
|
|
@ -859,7 +860,7 @@
|
|||
"-framework",
|
||||
Carbon,
|
||||
);
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app;
|
||||
PRODUCT_NAME = cmux;
|
||||
SPARKLE_PUBLIC_KEY = "avjcgKibf1FTvhIjLBxhd+0HSpsXU4D0IGlVk8cgqRc=";
|
||||
|
|
@ -901,6 +902,7 @@
|
|||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_NAME = cmux;
|
||||
PRODUCT_MODULE_NAME = cmux_cli;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -932,7 +934,7 @@
|
|||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.61.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -968,7 +970,7 @@
|
|||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.61.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ echo "Pre-flight checks passed"
|
|||
# --- Build GhosttyKit (if needed) ---
|
||||
if [ ! -d "GhosttyKit.xcframework" ]; then
|
||||
echo "Building GhosttyKit..."
|
||||
cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=native -Doptimize=ReleaseFast && cd ..
|
||||
cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast && cd ..
|
||||
rm -rf GhosttyKit.xcframework
|
||||
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
|
||||
else
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ else
|
|||
echo "==> Building GhosttyKit.xcframework (this may take a few minutes)..."
|
||||
(
|
||||
cd ghostty
|
||||
zig build -Demit-xcframework=true -Doptimize=ReleaseFast
|
||||
zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
)
|
||||
# Stamp the build output with the SHA it was built from
|
||||
echo "$GHOSTTY_SHA" > "$LOCAL_SHA_STAMP"
|
||||
|
|
|
|||
|
|
@ -70,14 +70,23 @@ while (( ${#padded_key} % 4 != 0 )); do
|
|||
done
|
||||
printf "%s" "$padded_key" > "$key_file"
|
||||
|
||||
generated_appcast_path="$archives_dir/$(basename "$OUT_PATH")"
|
||||
|
||||
"$generate_appcast" \
|
||||
--ed-key-file "$key_file" \
|
||||
--download-url-prefix "$DOWNLOAD_URL_PREFIX" \
|
||||
--full-release-notes-url "$RELEASE_NOTES_URL" \
|
||||
"$archives_dir"
|
||||
|
||||
if [[ ! -f "$archives_dir/appcast.xml" ]]; then
|
||||
echo "appcast.xml not generated." >&2
|
||||
if [[ ! -f "$generated_appcast_path" ]]; then
|
||||
fallback_generated_appcast="$(find "$archives_dir" -maxdepth 1 -name '*.xml' | head -n 1)"
|
||||
if [[ -n "$fallback_generated_appcast" ]]; then
|
||||
generated_appcast_path="$fallback_generated_appcast"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$generated_appcast_path" ]]; then
|
||||
echo "Expected appcast was not generated." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -85,7 +94,7 @@ fi
|
|||
# to sign the DMG and inject the signature. generate_appcast silently skips
|
||||
# signing when the public key derived from the private key doesn't match the
|
||||
# SUPublicEDKey in the app's Info.plist.
|
||||
if ! grep -q 'sparkle:edSignature' "$archives_dir/appcast.xml"; then
|
||||
if ! grep -q 'sparkle:edSignature' "$generated_appcast_path"; then
|
||||
echo "Warning: generate_appcast did not add edSignature. Using sign_update fallback..."
|
||||
SIGNATURE=$("$sign_update" -p --ed-key-file "$key_file" "$DMG_PATH")
|
||||
DMG_LENGTH=$(stat -f%z "$DMG_PATH")
|
||||
|
|
@ -95,7 +104,7 @@ if ! grep -q 'sparkle:edSignature' "$archives_dir/appcast.xml"; then
|
|||
# Inject sparkle:edSignature and correct length into the enclosure element
|
||||
python3 -c "
|
||||
import sys
|
||||
xml = open('$archives_dir/appcast.xml').read()
|
||||
xml = open('$generated_appcast_path').read()
|
||||
sig = '$SIGNATURE'
|
||||
length = '$DMG_LENGTH'
|
||||
# Add edSignature to enclosure
|
||||
|
|
@ -103,12 +112,12 @@ xml = xml.replace(
|
|||
'type=\"application/octet-stream\"',
|
||||
'sparkle:edSignature=\"' + sig + '\" length=\"' + length + '\" type=\"application/octet-stream\"'
|
||||
)
|
||||
open('$archives_dir/appcast.xml', 'w').write(xml)
|
||||
open('$generated_appcast_path', 'w').write(xml)
|
||||
print(' Injected edSignature into appcast.xml')
|
||||
"
|
||||
fi
|
||||
|
||||
cp "$archives_dir/appcast.xml" "$OUT_PATH"
|
||||
cp "$generated_appcast_path" "$OUT_PATH"
|
||||
echo "Generated appcast at $OUT_PATH"
|
||||
|
||||
# Verify the appcast has a signature
|
||||
|
|
|
|||
29
tests/test_ci_universal_release_settings.sh
Normal file
29
tests/test_ci_universal_release_settings.sh
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
# Regression test for universal GhosttyKit and Release build settings.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
for file in \
|
||||
"$ROOT_DIR/.github/workflows/build-ghosttykit.yml" \
|
||||
"$ROOT_DIR/scripts/setup.sh" \
|
||||
"$ROOT_DIR/scripts/build-sign-upload.sh"
|
||||
do
|
||||
if ! grep -Fq -- '-Dxcframework-target=universal' "$file"; then
|
||||
echo "FAIL: $file must build GhosttyKit with -Dxcframework-target=universal"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! awk '
|
||||
/\/\* Release \*\// { in_release=1; next }
|
||||
in_release && /ONLY_ACTIVE_ARCH = YES;/ { saw_yes=1 }
|
||||
in_release && /ONLY_ACTIVE_ARCH = NO;/ { saw_no=1 }
|
||||
in_release && /name = Release;/ { in_release=0 }
|
||||
END { exit !(saw_no && !saw_yes) }
|
||||
' "$ROOT_DIR/GhosttyTabs.xcodeproj/project.pbxproj"; then
|
||||
echo "FAIL: Release configurations in project.pbxproj must use ONLY_ACTIVE_ARCH = NO"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: GhosttyKit builds universal and Release configs disable ONLY_ACTIVE_ARCH"
|
||||
99
tests/test_nightly_universal_build.sh
Normal file
99
tests/test_nightly_universal_build.sh
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env bash
|
||||
# Regression test for dual nightly macOS tracks.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORKFLOW_FILE="$ROOT_DIR/.github/workflows/nightly.yml"
|
||||
|
||||
if ! awk '
|
||||
/^ - name: Build Apple Silicon app \(Release\)/ { in_arm=1; next }
|
||||
/^ - name: Build universal app \(Release\)/ { in_universal=1; next }
|
||||
in_arm && /^ - name:/ { in_arm=0 }
|
||||
in_universal && /^ - name:/ { in_universal=0 }
|
||||
in_arm && /-destination '\''platform=macOS,arch=arm64'\''/ { saw_arm_destination=1 }
|
||||
in_arm && /ARCHS="arm64"/ { saw_arm_archs=1 }
|
||||
in_arm && /ONLY_ACTIVE_ARCH=YES/ { saw_arm_only_active_arch=1 }
|
||||
in_universal && /-destination '\''generic\/platform=macOS'\''/ { saw_universal_destination=1 }
|
||||
in_universal && /ARCHS="arm64 x86_64"/ { saw_universal_archs=1 }
|
||||
in_universal && /ONLY_ACTIVE_ARCH=NO/ { saw_universal_only_active_arch=1 }
|
||||
END {
|
||||
exit !(saw_arm_destination && saw_arm_archs && saw_arm_only_active_arch && saw_universal_destination && saw_universal_archs && saw_universal_only_active_arch)
|
||||
}
|
||||
' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: nightly workflow must force Apple Silicon nightly to arm64-only and universal nightly to both slices"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! awk '
|
||||
/^ - name: Verify nightly binary architectures/ { in_verify=1; next }
|
||||
in_verify && /^ - name:/ { in_verify=0 }
|
||||
in_verify && /lipo -archs "\$ARM_APP_BINARY"/ { saw_arm_app=1 }
|
||||
in_verify && /lipo -archs "\$ARM_CLI_BINARY"/ { saw_arm_cli=1 }
|
||||
in_verify && /lipo -archs "\$APP_BINARY"/ { saw_app=1 }
|
||||
in_verify && /lipo -archs "\$CLI_BINARY"/ { saw_cli=1 }
|
||||
in_verify && /\[\[ "\$ARM_APP_ARCHS" == "arm64" \]\]/ { saw_arm_app_assert=1 }
|
||||
in_verify && /\[\[ "\$ARM_CLI_ARCHS" == "arm64" \]\]/ { saw_arm_cli_assert=1 }
|
||||
END { exit !(saw_arm_app && saw_arm_cli && saw_app && saw_cli && saw_arm_app_assert && saw_arm_cli_assert) }
|
||||
' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: nightly workflow must verify arm-only and universal slices with lipo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'com.cmuxterm.app.nightly.universal' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: nightly workflow must set a distinct .universal bundle ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast-universal.xml' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: nightly workflow must publish a separate universal appcast feed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq './scripts/sparkle_generate_appcast.sh "$NIGHTLY_UNIVERSAL_DMG_IMMUTABLE" nightly appcast-universal.xml' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: nightly workflow must generate a separate universal appcast"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq "core.setOutput('should_publish', isMainRef ? 'true' : 'false');" "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: nightly decide step must expose should_publish based on whether the ref is main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! awk '
|
||||
/^ - name: Upload branch nightly artifacts/ { in_upload=1; next }
|
||||
in_upload && /^ - name:/ { in_upload=0 }
|
||||
in_upload && /if: needs\.decide\.outputs\.should_publish != '\''true'\''/ { saw_if=1 }
|
||||
in_upload && /uses: actions\/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4/ { saw_upload=1 }
|
||||
in_upload && /cmux-nightly-macos\*\.dmg/ { saw_arm_artifacts=1 }
|
||||
in_upload && /cmux-nightly-universal-macos\*\.dmg/ { saw_universal_artifacts=1 }
|
||||
in_upload && /appcast-universal\.xml/ { saw_universal_appcast=1 }
|
||||
END { exit !(saw_if && saw_upload && saw_arm_artifacts && saw_universal_artifacts && saw_universal_appcast) }
|
||||
' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: non-main nightly runs must upload both nightly variants and both appcasts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! awk '
|
||||
/^ - name: Move nightly tag to built commit/ { in_move=1; next }
|
||||
in_move && /^ - name:/ { in_move=0 }
|
||||
in_move && /if: needs\.decide\.outputs\.should_publish == '\''true'\''/ { saw_move_if=1 }
|
||||
END { exit !saw_move_if }
|
||||
' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: moving the nightly tag must be gated to main nightly publishes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! awk '
|
||||
/^ - name: Publish nightly release assets/ { in_publish=1; next }
|
||||
in_publish && /^ - name:/ { in_publish=0 }
|
||||
in_publish && /if: needs\.decide\.outputs\.should_publish == '\''true'\''/ { saw_publish_if=1 }
|
||||
in_publish && /cmux-nightly-universal-macos-\$\{\{ github\.run_id \}\}\*\.dmg/ { saw_universal_immutable=1 }
|
||||
in_publish && /cmux-nightly-universal-macos\.dmg/ { saw_universal_stable=1 }
|
||||
in_publish && /appcast-universal\.xml/ { saw_universal_appcast=1 }
|
||||
END { exit !(saw_publish_if && saw_universal_immutable && saw_universal_stable && saw_universal_appcast) }
|
||||
' "$WORKFLOW_FILE"; then
|
||||
echo "FAIL: main nightly publish must include the universal assets and appcast"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: nightly workflow keeps separate Apple Silicon and universal nightly tracks"
|
||||
Loading…
Add table
Add a link
Reference in a new issue