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
64 changes: 31 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,9 @@ jobs:
- name: Validate universal nightly workflow
run: bash ./tests/test_nightly_universal_build.sh

- name: Validate test compilation cache seeding
run: bash ./tests/test_ci_test_compilation_cache_seed.sh

- name: Validate cmux-tui client installation
run: bash ./tests/test_install_cmux_tui_client.sh

Expand Down Expand Up @@ -2248,7 +2251,11 @@ jobs:
persist-credentials: false

- name: Prepare isolated admission DerivedData
run: echo "CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission" >> "$GITHUB_ENV"
run: |
set -euo pipefail
echo "CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission" >> "$GITHUB_ENV"
# Outside DerivedData: the resolve step below recreates that directory.
echo "CMUX_COMPILE_ADMISSION_CAS=$RUNNER_TEMP/cmux-compile-admission-cas" >> "$GITHUB_ENV"

- name: Select Xcode
run: ./scripts/select-ci-xcode.sh
Expand Down Expand Up @@ -2284,44 +2291,35 @@ jobs:
- name: Resolve Swift packages
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
rm -rf "$CMUX_COMPILE_ADMISSION_DERIVED_DATA"
mkdir -p "$SOURCE_PACKAGES_DIR" "$CMUX_COMPILE_ADMISSION_DERIVED_DATA"
scripts/ci/compile-app-host-test-product.sh resolve \
"$CMUX_COMPILE_ADMISSION_DERIVED_DATA" "$PWD/.ci-source-packages"

for attempt in 1 2 3; do
if xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-resolvePackageDependencies; then
if [ -d "$SOURCE_PACKAGES_DIR/artifacts/sparkle/Sparkle/Sparkle.xcframework" ] \
&& [ -d "$SOURCE_PACKAGES_DIR/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" ]; then
exit 0
fi
echo "Resolve succeeded but binary artifacts are missing; clearing and retrying" >&2
rm -rf "$SOURCE_PACKAGES_DIR"
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to resolve Swift packages after 3 attempts" >&2
exit 1
fi
echo "Package resolution failed on attempt $attempt, retrying..."
sleep $((attempt * 5))
done
- name: Compute test compilation cache key
id: compilation-cache-key
run: |
set -euo pipefail
echo "fingerprint=$(scripts/ci/compile-app-host-test-product.sh fingerprint "$CMUX_COMPILE_ADMISSION_DERIVED_DATA")" >> "$GITHUB_OUTPUT"

# Read-only on purpose: nightly.yml `refresh-test-compilation-cache` is the
# only writer. A cache saved here would be scoped to this pull request and
# would spend the cache budget that keeps the main seed alive.
- name: Restore test compilation cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.CMUX_COMPILE_ADMISSION_CAS }}
key: xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-${{ github.event.pull_request.base.sha || github.sha }}
restore-keys: |
xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-

- name: Compile app-host test product
run: |
set -euo pipefail
# shellcheck disable=SC2016 # Xcode expands $(inherited), not the shell
for scheme in cmux-unit cmux-numeric-locale; do
xcodebuild -project cmux.xcodeproj -scheme "$scheme" -configuration Debug \
-derivedDataPath "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \
-clonedSourcePackagesDirPath "$PWD/.ci-source-packages" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) CMUX_CI_APP_HOST_ISOLATION_REQUIRED' \
'LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/../Frameworks /private/tmp/cmux-app-host-package-frameworks' \
build-for-testing 2>&1 | tee -a "$RUNNER_TEMP/cmux-compile-admission.txt"
done
scripts/ci/compile-app-host-test-product.sh build \
"$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \
"$PWD/.ci-source-packages" \
"$CMUX_COMPILE_ADMISSION_CAS" \
"$RUNNER_TEMP/cmux-compile-admission.txt"

- name: Package compiled app-host test product
id: package-products
Expand Down
126 changes: 126 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,132 @@ jobs:
path: build-universal/CompilationCache.noindex
key: xcode-compilation-release-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.toolchain }}-${{ needs.decide.outputs.head_sha }}

refresh-test-compilation-cache:
needs: decide
# The only writer of the Debug compilation cache that ci.yml
# `macos-compile-admission` restores. Scheduled, not per push: the seed
# mostly saves the package layer, which changes slowly, and a build per
# merge would take macOS slots from the pull request queue.
if: github.event_name == 'schedule' && github.event.schedule == '17 */6 * * *'
# Match the admission job's runner and Xcode. The cache key carries the
# toolchain and the build paths, so a mismatch is a miss, not a wrong hit.
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
timeout-minutes: 75
env:
CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_15 }}
CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26"
CMUX_SKIP_ZIG_BUILD: "1"
steps:
- name: Clear stale git locks (self-hosted reused workspace)
shell: bash
run: |
ws="${GITHUB_WORKSPACE:-$PWD}"
rm -f "$ws/.git/index.lock" 2>/dev/null || true
if [ -d "$ws/.git/modules" ]; then
find "$ws/.git/modules" -type f -name "*.lock" -delete 2>/dev/null || true
fi

- name: Checkout cache ref
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ needs.decide.outputs.head_sha }}
submodules: recursive

- name: Prepare admission build paths
run: |
set -euo pipefail
# The same paths as ci.yml `macos-compile-admission`: they are part of
# every cache entry this job writes.
echo "CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission" >> "$GITHUB_ENV"
echo "CMUX_COMPILE_ADMISSION_CAS=$RUNNER_TEMP/cmux-compile-admission-cas" >> "$GITHUB_ENV"

- name: Select Xcode
run: ./scripts/select-ci-xcode.sh

- name: Compute test compilation cache key
id: compilation-cache-key
run: |
set -euo pipefail
echo "fingerprint=$(scripts/ci/compile-app-host-test-product.sh fingerprint "$CMUX_COMPILE_ADMISSION_DERIVED_DATA")" >> "$GITHUB_OUTPUT"

- name: Restore test compilation cache
id: compilation-cache-restore
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.CMUX_COMPILE_ADMISSION_CAS }}
key: xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-${{ needs.decide.outputs.head_sha }}
restore-keys: |
xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-

- name: Install compilation dependencies
if: steps.compilation-cache-restore.outputs.cache-hit != 'true'
run: |
./scripts/install-rust-ci.sh
./scripts/download-prebuilt-ghosttykit.sh

- name: Cache Swift packages
if: steps.compilation-cache-restore.outputs.cache-hit != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .ci-source-packages
key: spm-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-

- name: Sanitize Swift package cache
if: steps.compilation-cache-restore.outputs.cache-hit != 'true'
run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .ci-source-packages

- name: Resolve Swift packages
if: steps.compilation-cache-restore.outputs.cache-hit != 'true'
run: |
set -euo pipefail
scripts/ci/compile-app-host-test-product.sh resolve \
"$CMUX_COMPILE_ADMISSION_DERIVED_DATA" "$PWD/.ci-source-packages"

- name: Refresh test compilation cache
if: steps.compilation-cache-restore.outputs.cache-hit != 'true'
run: |
set -euo pipefail
scripts/ci/compile-app-host-test-product.sh build \
"$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \
"$PWD/.ci-source-packages" \
"$CMUX_COMPILE_ADMISSION_CAS"

- name: Bound test compilation cache size
id: compilation-cache-bound
if: steps.compilation-cache-restore.outputs.cache-hit != 'true'
run: |
set -euo pipefail
cache_path="$CMUX_COMPILE_ADMISSION_CAS"
max_cache_kib=$((5 * 1024 * 1024))
if [ -d "$cache_path" ]; then
# See `refresh-compilation-cache`: a warm build leaves a dead CAS
# generation behind, and pruning it keeps the seed to one build.
python3 scripts/ci/prune-xcode-compilation-cache.py "$cache_path" \
|| echo "::warning::Xcode compilation cache pruning failed; measuring it unpruned"
cache_kib=$(du -sk "$cache_path" | awk '{print $1}')
else
cache_kib=0
fi
echo "Test compilation cache size: ${cache_kib} KiB (limit: ${max_cache_kib} KiB)"
save=false
if [ "$cache_kib" -gt "$max_cache_kib" ]; then
echo "::warning::Test compilation cache exceeds 5 GiB; skipping cache save"
elif [ "$cache_kib" -gt 0 ]; then
save=true
else
echo "Test compilation cache is empty; skipping cache save"
fi
echo "save=${save}" >> "$GITHUB_OUTPUT"

- name: Save test compilation cache
if: steps.compilation-cache-restore.outputs.cache-hit != 'true' && steps.compilation-cache-bound.outputs.save == 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.CMUX_COMPILE_ADMISSION_CAS }}
key: xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-${{ needs.decide.outputs.head_sha }}

build-nightly-ghostty-cli-helper:
needs: decide
if: needs.decide.outputs.should_build == 'true' && (github.event_name != 'schedule' || github.event.schedule == '47 8 * * *') && needs.decide.outputs.build_only != 'true'
Expand Down
99 changes: 99 additions & 0 deletions scripts/ci/compile-app-host-test-product.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# compile-app-host-test-product.sh fingerprint <derived-data>
# compile-app-host-test-product.sh resolve <derived-data> <source-packages>
# compile-app-host-test-product.sh build <derived-data> <source-packages> <cas-path> [log]
#
# Compiles the app-host test product with Xcode's compilation cache on. ci.yml
# `macos-compile-admission` restores that cache read-only and nightly.yml
# `refresh-test-compilation-cache` writes it. A cache entry is keyed on the
# whole compiler invocation and on absolute paths, so both jobs must build
# through this script or they stop sharing hits without anything failing.
#
# `fingerprint` hashes the toolchain and the build paths into the cache key.
# Runner pools lay the workspace out differently, and a seed built under
# another layout cannot hit, so it should be a cache miss and not a download.
set -euo pipefail

usage() {
echo "usage: $0 fingerprint <derived-data>" >&2
echo " $0 resolve <derived-data> <source-packages>" >&2
echo " $0 build <derived-data> <source-packages> <cas-path> [log]" >&2
exit 64
}

# Same limit as the Release seed in nightly.yml.
cache_limit_bytes=3221225472

fingerprint() {
local derived_data="$1"
{
xcodebuild -version
printf 'workspace=%s\n' "$PWD"
printf 'derived-data=%s\n' "$derived_data"
} | shasum -a 256 | cut -c1-32
}

# `build` disables package resolution, so a resolve that reports success
# without the Sparkle and Sentry binary artifacts would fail it. A restored
# source-packages cache can do that, and a failed resolve can leave a partial
# clone behind, so every retry starts from an empty package directory.
resolve() {
local derived_data="$1" source_packages="$2" attempt
for attempt in 1 2 3; do
mkdir -p "$source_packages" "$derived_data"
if xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$derived_data" \
-clonedSourcePackagesDirPath "$source_packages" \
-resolvePackageDependencies; then
if [ -d "$source_packages/artifacts/sparkle/Sparkle/Sparkle.xcframework" ] \
&& [ -d "$source_packages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" ]; then
return 0
fi
echo "Resolve succeeded but binary artifacts are missing" >&2
fi
Comment thread
teamleaderleo marked this conversation as resolved.
[ "$attempt" -lt 3 ] || break
echo "Package resolution failed on attempt $attempt; clearing packages and retrying" >&2
rm -rf "$source_packages"
done
echo "Failed to resolve Swift packages after 3 attempts" >&2
return 1
}
Comment thread
teamleaderleo marked this conversation as resolved.

build() {
local derived_data="$1" source_packages="$2" cas_path="$3" log="${4:-/dev/null}"
mkdir -p "$cas_path"

# shellcheck disable=SC2016 # Xcode expands $(inherited), not the shell
for scheme in cmux-unit cmux-numeric-locale; do
xcodebuild -project cmux.xcodeproj -scheme "$scheme" -configuration Debug \
-derivedDataPath "$derived_data" \
-clonedSourcePackagesDirPath "$source_packages" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) CMUX_CI_APP_HOST_ISOLATION_REQUIRED' \
'LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/../Frameworks /private/tmp/cmux-app-host-package-frameworks' \
COMPILATION_CACHE_ENABLE_CACHING=YES \
"COMPILATION_CACHE_CAS_PATH=$cas_path" \
"COMPILATION_CACHE_LIMIT_SIZE=$cache_limit_bytes" \
build-for-testing 2>&1 | tee -a "$log"
done
}

case "${1:-}" in
fingerprint)
[ "$#" -eq 2 ] || usage
fingerprint "$2"
;;
resolve)
[ "$#" -eq 3 ] || usage
resolve "$2" "$3"
;;
build)
[ "$#" -ge 4 ] && [ "$#" -le 5 ] || usage
shift
build "$@"
;;
*)
usage
;;
esac
9 changes: 6 additions & 3 deletions tests/test_ci_change_areas.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,9 +988,12 @@ def test_macos_compile_admission_precedes_expensive_shards() -> None:
assert "name: macOS compile admission" in admission
assert " - changes" in admission
assert " - linux-preflight" in admission
assert "build-for-testing" in admission
assert "cmux-unit" in admission
assert "cmux-numeric-locale" in admission
# The compile lives in one script so the nightly cache seeder runs the same
# invocation; see tests/test_ci_test_compilation_cache_seed.sh.
assert "scripts/ci/compile-app-host-test-product.sh build" in admission
compile_script = (ROOT / "scripts/ci/compile-app-host-test-product.sh").read_text(encoding="utf-8")
assert "build-for-testing" in compile_script
assert "for scheme in cmux-unit cmux-numeric-locale; do" in compile_script
assert "actions/cache@27d5ce7" in admission
assert "steps.upload-products.outputs.artifact-id" in admission
assert "app_host_test_products.py stamp" in admission
Expand Down
Loading
Loading