Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-ghosttykit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Version GhosttyKit release key when changing target

Changing the GhosttyKit build to -Dxcframework-target=universal without changing the artifact identity means xcframework-${sha} can still resolve to an older single-arch release for the same SHA, because the workflow skips rebuilding when that tag already exists and downstream downloads are keyed by SHA. In that case nightly/release universal builds continue consuming a stale non-universal xcframework and can fail when x86_64 slices are required; include the target in the release key (or force a rebuild/migration) so the artifact identity matches its contents.

Useful? React with 👍 / 👎.


- name: Package xcframework
if: steps.check-release.outputs.exists == 'false'
Expand Down
314 changes: 207 additions & 107 deletions .github/workflows/nightly.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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=";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-sign-upload.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

@cubic-dev-ai cubic-dev-ai Bot Mar 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The cache key/stamp does not include xcframework target, so existing same-SHA artifacts can bypass this new universal build setting and keep serving stale architecture output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup.sh, line 61:

<comment>The cache key/stamp does not include xcframework target, so existing same-SHA artifacts can bypass this new universal build setting and keep serving stale architecture output.</comment>

<file context>
@@ -58,7 +58,7 @@ else
         (
             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
</file context>
Fix with Cubic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate setup cache when switching to universal xcframework

This switches local GhosttyKit builds to universal, but setup.sh cache reuse is still keyed only by GHOSTTY_SHA, so a previously cached/native xcframework for the same SHA will be reused and never rebuilt. Developers who already cached the old artifact can silently keep using a non-universal framework, which breaks or masks universal release behavior locally; include the build target in the cache key/stamp so old native caches are not reused.

Useful? React with 👍 / 👎.

)
# Stamp the build output with the SHA it was built from
echo "$GHOSTTY_SHA" > "$LOCAL_SHA_STAMP"
Expand Down
21 changes: 15 additions & 6 deletions scripts/sparkle_generate_appcast.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,31 @@ 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

# Check if generate_appcast added the edSignature. If not, use sign_update
# 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")
Expand All @@ -95,20 +104,20 @@ 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
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
Expand Down
29 changes: 29 additions & 0 deletions tests/test_ci_universal_release_settings.sh
Original file line number Diff line number Diff line change
@@ -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

@cubic-dev-ai cubic-dev-ai Bot Mar 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This regression test relies on grep/awk checks of source text instead of verifying executable behavior, which violates the repository’s test quality policy.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test_ci_universal_release_settings.sh, line 12:

<comment>This regression test relies on grep/awk checks of source text instead of verifying executable behavior, which violates the repository’s test quality policy.</comment>

<file context>
@@ -0,0 +1,29 @@
+  "$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
</file context>
Fix with Cubic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace source-text assertions with executable regression checks

This test validates behavior by grepping/awking repository files instead of exercising runtime build behavior, so it is brittle to refactors and can still pass when the actual pipeline/build output is wrong. The repository policy in /workspace/cmux/AGENTS.md explicitly forbids source-text-only tests, so this should be rewritten to assert observable behavior through an executable path.

Useful? React with 👍 / 👎.

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 changes: 99 additions & 0 deletions tests/test_nightly_universal_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash

@cubic-dev-ai cubic-dev-ai Bot Mar 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This entire test file verifies source-code text patterns (awk/grep on the YAML workflow file) rather than observable runtime behavior, which the project's test quality policy explicitly prohibits.

If the goal is to lock in universal-build and publish-guard behavior, consider a test that actually exercises the workflow outputs — e.g., a small script that invokes the build with the expected flags and verifies the resulting binary is universal via lipo -archs, or a unit test for the publish-decision logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test_nightly_universal_build.sh, line 8:

<comment>This entire test file verifies source-code text patterns (awk/grep on the YAML workflow file) rather than observable runtime behavior, which the project's test quality policy explicitly prohibits.

If the goal is to lock in universal-build and publish-guard behavior, consider a test that actually exercises the workflow outputs — e.g., a small script that invokes the build with the expected flags and verifies the resulting binary is universal via `lipo -archs`, or a unit test for the publish-decision logic.</comment>

<file context>
@@ -0,0 +1,67 @@
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+WORKFLOW_FILE="$ROOT_DIR/.github/workflows/nightly.yml"
+
+if ! awk '
+  /^      - name: Build app \(Release\)/ { in_build=1; next }
+  in_build && /^      - name:/ { in_build=0 }
</file context>
Fix with Cubic

# 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 }
Comment on lines +8 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace grep-style workflow assertions with executable checks

This test only inspects .github/workflows/nightly.yml text via awk/grep patterns, so it can pass even when the workflow behavior is broken (or fail on harmless refactors), which gives brittle, non-runtime coverage for a release pipeline change. The repository policy in /workspace/cmux/AGENTS.md explicitly forbids this pattern (“Do not add tests that only verify source code text...”), so this should be rewritten to validate observable behavior (e.g., by exercising the workflow logic through an executable seam) rather than matching YAML strings.

Useful? React with 👍 / 👎.

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) }

@cubic-dev-ai cubic-dev-ai Bot Mar 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The branch-upload regression check omits appcast.xml, so it can falsely pass when only the universal appcast is uploaded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test_nightly_universal_build.sh, line 64:

<comment>The branch-upload regression check omits `appcast.xml`, so it can falsely pass when only the universal appcast is uploaded.</comment>

<file context>
@@ -38,9 +58,12 @@ if ! awk '
+  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 artifacts instead of publishing the official nightly release"
</file context>
Fix with Cubic

' "$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"