Fix residual blog metadata from Ahrefs audit #38678
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| actions: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| changes: | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| timeout-minutes: 5 | |
| outputs: | |
| macos: ${{ steps.detect.outputs.macos }} | |
| web: ${{ steps.detect.outputs.web }} | |
| go: ${{ steps.detect.outputs.go }} | |
| agent_session_web: ${{ steps.detect.outputs.agent_session_web }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect CI change areas | |
| id: detect | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| emit_all_areas() { | |
| { | |
| echo "macos=true" | |
| echo "web=true" | |
| echo "go=true" | |
| echo "agent_session_web=true" | |
| } >> "$GITHUB_OUTPUT" | |
| } | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| if ! MERGE_BASE="$(git merge-base "$BASE_SHA" "$HEAD_SHA")" \ | |
| || ! git diff --name-only "$MERGE_BASE" "$HEAD_SHA" > /tmp/cmux-ci-changed-files.txt; then | |
| echo "Could not compute PR diff; running all CI areas." >&2 | |
| emit_all_areas | |
| exit 0 | |
| fi | |
| if [ ! -s /tmp/cmux-ci-changed-files.txt ]; then | |
| echo "PR diff is empty; running all CI areas." | |
| emit_all_areas | |
| exit 0 | |
| fi | |
| # This guard lives in workflow YAML, before the PR-editable detector | |
| # runs. Router/workflow edits must fail open even if a PR breaks the | |
| # detector's own self-change classification. | |
| if grep -Eq '^(\.github/workflows/ci\.yml|scripts/ci/[^/]+\.py|tests/test_ci_change_areas\.py)$' /tmp/cmux-ci-changed-files.txt; then | |
| echo "CI router changed; running all CI areas." | |
| emit_all_areas | |
| exit 0 | |
| fi | |
| python3 scripts/ci/detect_ci_change_areas.py \ | |
| --event-name "$EVENT_NAME" \ | |
| --files-from /tmp/cmux-ci-changed-files.txt | |
| exit 0 | |
| fi | |
| python3 scripts/ci/detect_ci_change_areas.py \ | |
| --event-name "$EVENT_NAME" \ | |
| --base-sha "$BASE_SHA" \ | |
| --head-sha "$HEAD_SHA" | |
| workflow-guard-tests: | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate macOS runner guards | |
| run: ./tests/test_ci_self_hosted_guard.sh | |
| - name: Validate release SDK build lane | |
| run: ./tests/test_ci_release_sdk_lane.sh | |
| - name: Validate Python test harness syntax | |
| run: git ls-files 'tests/*.py' 'tests_v2/*.py' 'scripts/*.py' | xargs python3 -m py_compile | |
| - name: Set up Python 3.9 for nightly prune compatibility | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: "3.9" | |
| - name: Validate nightly prune Python compatibility | |
| run: PYTHON_BIN=python3.9 bash ./tests/test_ci_nightly_prune_python_compat.sh | |
| - name: Validate activation benchmark scrollback sizing | |
| run: python3 tests/test_perf_activation_scrollback_sizing.py | |
| - name: Validate create-dmg version pinning | |
| run: ./tests/test_ci_create_dmg_pinned.sh | |
| - name: Validate unit-test SwiftPM retry guard | |
| run: ./tests/test_ci_unit_test_spm_retry.sh | |
| - name: Validate xcodebuild noninteractive crash prompt guard | |
| run: python3 tests/test_ci_xcodebuild_noninteractive_helper.py | |
| - name: Validate app-host xcodebuild retry guard | |
| run: ./tests/test_ci_app_host_xcodebuild_retry.sh | |
| - name: Validate app-host xcodebuild attempt budget | |
| run: ./tests/test_ci_app_host_xcodebuild_attempts.sh | |
| - name: Validate cmux profiling support scripts | |
| run: ./tests/test_start_cmux_profiling.sh | |
| - name: Validate Xcode SourcePackages cache sanitizer | |
| run: python3 tests/test_ci_sanitize_xcode_source_packages_cache.py | |
| - name: Validate TestFlight notes generator | |
| run: python3 tests/test_ios_testflight_notes.py | |
| - name: Validate external TestFlight group assignment helper | |
| run: python3 tests/test_ios_testflight_external_distribution.py | |
| - name: Validate iOS App Store lane identity | |
| run: python3 tests/test_ios_appstore_lane_identity.py | |
| - name: Validate cmux scheme test configuration | |
| run: ./tests/test_ci_scheme_testaction_debug.sh | |
| - name: Validate cmuxTests sharding | |
| run: | | |
| python3 scripts/ci/cmux_unit_test_shard.py --validate | |
| python3 tests/test_ci_cmux_unit_test_shard.py | |
| - name: Validate GhosttyKit checksum verification | |
| run: ./tests/test_ci_ghosttykit_checksum_verification.sh | |
| - name: Validate Zig install without sudo | |
| run: ./tests/test_install_zig_ci_no_sudo.sh | |
| - name: Validate virtual display lock | |
| run: ./tests/test_ci_virtual_display_lock.sh | |
| - name: Validate nightly tag push auth | |
| run: ./tests/test_ci_nightly_tag_push_auth.sh | |
| - name: Validate nightly Xcode selection | |
| run: ./tests/test_ci_nightly_xcode_selection.sh | |
| - name: Validate CI Xcode selection fast path | |
| run: ./tests/test_ci_xcode_selection_fast_path.sh | |
| - name: Validate universal nightly workflow | |
| run: bash ./tests/test_nightly_universal_build.sh | |
| - name: Validate release asset guard | |
| run: node scripts/release_asset_guard.test.js | |
| - name: Validate CI change area filter | |
| run: python3 tests/test_ci_change_areas.py | |
| - name: Validate release attestation retry guard | |
| run: ./tests/test_ci_attestation_retry.sh | |
| - name: Validate Python R2 appcast upload guard | |
| run: ./tests/test_ci_r2_upload_python.sh | |
| - name: Validate release-build timeout guard | |
| run: ./tests/test_ci_release_build_timeout.sh | |
| - name: Validate markdown viewer asset compression | |
| run: ./tests/test_compress_markdown_viewer_assets.sh | |
| - name: Validate release bundle stripping | |
| run: ./tests/test_strip_release_bundle.sh | |
| - name: Validate current GhosttyKit checksum pin | |
| run: ./tests/test_ci_ghosttykit_checksum_present.sh | |
| - name: Validate Swift warning budget guard | |
| run: ./tests/test_ci_swift_warning_budget.sh | |
| - name: Validate Swift file length budget guard | |
| run: ./tests/test_ci_swift_file_length_budget.sh | |
| - name: Validate auxiliary window close shortcut lint | |
| run: ./tests/test_ci_auxiliary_window_close_shortcuts.sh | |
| - name: Validate pbxproj test-wiring lint | |
| run: ./tests/test_ci_pbxproj_test_wiring.sh | |
| - name: Validate pbxproj objectVersion pin and normalization | |
| run: ./scripts/check-pbxproj.sh | |
| - name: Validate workspace SPM package grouping | |
| run: python3 scripts/check-workspace-package-groups.py --check | |
| - name: Validate SwiftPM lockfile policy | |
| run: python3 scripts/check-package-resolved-policy.py | |
| - name: Validate bash shell integration job control | |
| run: python3 tests/test_bash_integration_no_done_notifications.py | |
| - name: Validate sidebar lazy-layout guard | |
| run: python3 tests/test_ci_sidebar_lazy_layout_guard.py | |
| - name: Validate bash prompt bootstrap composes with user PROMPT_COMMAND (starship) | |
| run: python3 tests/test_issue_5164_starship_prompt_composition.py | |
| - name: Validate Swift file length budget | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| BASE_BRANCH: ${{ github.event.pull_request.base.ref }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| BASE_REF="$BASE_SHA" | |
| if MERGE_BASE="$(git merge-base "$BASE_SHA" "$HEAD_SHA")"; then | |
| BASE_REF="$MERGE_BASE" | |
| fi | |
| python3 scripts/swift_file_length_budget.py \ | |
| --budget .github/swift-file-length-budget.tsv \ | |
| --base-ref "$BASE_REF" \ | |
| --merge-ref "origin/${BASE_BRANCH:-main}" \ | |
| --merge-head "$HEAD_SHA" | |
| elif [ "$EVENT_NAME" = "push" ] && [ -n "$BEFORE_SHA" ] && ! [[ "$BEFORE_SHA" =~ ^0+$ ]]; then | |
| python3 scripts/swift_file_length_budget.py \ | |
| --budget .github/swift-file-length-budget.tsv \ | |
| --base-ref "$BEFORE_SHA" | |
| elif git rev-parse --verify --quiet origin/main >/dev/null && MERGE_BASE="$(git merge-base origin/main HEAD)"; then | |
| python3 scripts/swift_file_length_budget.py \ | |
| --budget .github/swift-file-length-budget.tsv \ | |
| --base-ref "$MERGE_BASE" | |
| else | |
| python3 scripts/swift_file_length_budget.py \ | |
| --budget .github/swift-file-length-budget.tsv | |
| fi | |
| - name: Validate feature flags (naming, owners, expiry, single use, no reuse) | |
| run: python3 scripts/lint-feature-flags.py | |
| - name: Validate test determinism gate | |
| run: python3 scripts/check-test-determinism.py --strict | |
| remote-daemon-tests: | |
| needs: changes | |
| if: ${{ needs.changes.outputs.go == 'true' }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: daemon/remote/go.mod | |
| cache-dependency-path: daemon/remote/go.sum | |
| - name: Run remote daemon tests | |
| working-directory: daemon/remote | |
| run: go test ./... | |
| - name: Validate remote daemon release assets | |
| run: ./tests/test_remote_daemon_release_assets.sh | |
| web-typecheck: | |
| needs: changes | |
| if: ${{ needs.changes.outputs.web == 'true' }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| defaults: | |
| run: | |
| working-directory: web | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Typecheck | |
| run: bun run typecheck | |
| - name: Web tests | |
| # Explicit sorted file list: bun discovers test files in filesystem | |
| # readdir order, which differs between Linux runners and local macOS, | |
| # so an unpinned run exercises a file order no developer can | |
| # reproduce. Sorted order makes CI failures replayable locally. | |
| run: bun test $(ls tests/*.test.ts tests/*.test.tsx | sort) | |
| # Checks for in-app React webviews (currently the diff viewer; more cmux React | |
| # surfaces will live alongside it). | |
| react-apps-check: | |
| needs: changes | |
| if: ${{ needs.changes.outputs.web == 'true' }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| - name: Verify generated webviews assets | |
| run: ./scripts/build-webviews-app.sh --check | |
| - name: Verify React Compiler for webviews | |
| run: ./scripts/check-webviews-react-compiler.mjs | |
| - name: Typecheck webviews | |
| working-directory: webviews | |
| run: bun run typecheck | |
| - name: Test webviews | |
| working-directory: webviews | |
| run: bun run test | |
| - name: Oxlint webviews | |
| working-directory: webviews | |
| run: bun run lint:ci | |
| - name: React Doctor | |
| working-directory: webviews | |
| run: bun run react-doctor:ci | |
| web-db-migrations: | |
| needs: changes | |
| if: ${{ needs.changes.outputs.web == 'true' }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| defaults: | |
| run: | |
| working-directory: web | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_DB: cmux_test | |
| POSTGRES_PASSWORD: cmux | |
| POSTGRES_USER: cmux | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U cmux -d cmux_test" | |
| --health-interval 1s | |
| --health-timeout 5s | |
| --health-retries 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Apply migrations | |
| env: | |
| DATABASE_URL: postgres://cmux:cmux@localhost:5432/cmux_test | |
| DIRECT_DATABASE_URL: postgres://cmux:cmux@localhost:5432/cmux_test | |
| run: bunx drizzle-kit migrate --config drizzle.config.ts | |
| - name: Re-apply migrations | |
| env: | |
| DATABASE_URL: postgres://cmux:cmux@localhost:5432/cmux_test | |
| DIRECT_DATABASE_URL: postgres://cmux:cmux@localhost:5432/cmux_test | |
| run: bunx drizzle-kit migrate --config drizzle.config.ts | |
| - name: Database behavior tests | |
| env: | |
| CMUX_DB_TEST: "1" | |
| DATABASE_URL: postgres://cmux:cmux@localhost:5432/cmux_test | |
| DIRECT_DATABASE_URL: postgres://cmux:cmux@localhost:5432/cmux_test | |
| run: bun run test:db:behavior | |
| app-host-unit-tests: | |
| needs: | |
| - changes | |
| - linux-preflight | |
| # !cancelled() disables the implicit success() gate, which GitHub evaluates | |
| # over the transitive needs chain: linux-preflight runs behind routed linux | |
| # jobs that legitimately skip (web/go/agent-session paths), and that | |
| # transitive skip otherwise marks every macOS job skipped even when | |
| # linux-preflight itself succeeds. Require the direct needs explicitly. | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && needs.linux-preflight.result == 'success' && needs.changes.outputs.macos == 'true' }} | |
| name: app-host unit tests (${{ matrix.shard }}/4) | |
| # App-host XCTest needs a runner that can broker testmanagerd control | |
| # sessions. Validated head-to-head that warp-macos-15-arm64-6x runs the | |
| # full app-host suite with 0 unexpected failures, so route through the | |
| # shared MACOS_RUNNER_15 var like the other macOS jobs. | |
| runs-on: ${{ vars.MACOS_RUNNER_15 || 'warp-macos-15-arm64-6x' }} | |
| timeout-minutes: 75 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| env: | |
| CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_15 }} | |
| CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26" | |
| CMUX_SKIP_ZIG_BUILD: "1" | |
| # Keep one-off focused gates on the lightest measured shard so shard 1 no | |
| # longer carries the full shard plus every extra regression guard. | |
| CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD: "4" | |
| # XCTest app-host crashes can leave xcodebuild waiting in Swift's crash | |
| # backtracer until the job timeout. Keep crash handling non-interactive | |
| # and cheap so xcodebuild can restart/finish the suite. | |
| SWIFT_BACKTRACE: "interactive=no,timeout=0s,symbolicate=off,color=no" | |
| CMUX_XCODEBUILD_NONINTERACTIVE_POST_TEST_TIMEOUT_SECONDS: "45" | |
| steps: | |
| - name: Clear stale git locks (self-hosted reused workspace) | |
| shell: bash | |
| run: | | |
| # Self-hosted macOS runners reuse the workspace. A job cancelled or | |
| # killed mid-checkout can leave a stale .git/modules/*/index.lock that | |
| # fails every later submodule checkout (e.g. ghostty). Clear them first. | |
| 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 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| submodules: recursive | |
| - name: Select Xcode | |
| run: | | |
| set -euo pipefail | |
| ./scripts/select-ci-xcode.sh | |
| - name: Capture Ghostty revision | |
| id: ghostty-revision | |
| run: | | |
| echo "sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Cache GhosttyKit.xcframework | |
| id: cache-ghosttykit | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: GhosttyKit.xcframework | |
| key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }} | |
| - name: Download pre-built GhosttyKit.xcframework | |
| if: steps.cache-ghosttykit.outputs.cache-hit != 'true' | |
| run: | | |
| ./scripts/download-prebuilt-ghosttykit.sh | |
| - name: Install Rust | |
| run: | | |
| ./scripts/install-rust-ci.sh | |
| - name: Cache Swift packages | |
| 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 | |
| run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .ci-source-packages | |
| - name: Prepare isolated DerivedData | |
| run: | | |
| set -euo pipefail | |
| DERIVED_DATA_PATH="${RUNNER_TEMP}/cmux-derived-data-tests-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-shard-${{ matrix.shard }}" | |
| rm -rf "$DERIVED_DATA_PATH" | |
| mkdir -p "$DERIVED_DATA_PATH" | |
| echo "CMUX_DERIVED_DATA_PATH=$DERIVED_DATA_PATH" >> "$GITHUB_ENV" | |
| - name: Resolve Swift packages | |
| run: | | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| mkdir -p "$SOURCE_PACKAGES_DIR" | |
| for attempt in 1 2 3; do | |
| if xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -resolvePackageDependencies; then | |
| # Guard against a stale/poisoned Swift-package cache: a restored | |
| # .ci-source-packages can make resolve report success without the | |
| # binary artifacts (Sparkle/Sentry) actually present, which then | |
| # fails the build. Verify they materialized; if not, clear and retry. | |
| 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 (stale cache); clearing and retrying" >&2 | |
| rm -rf "$SOURCE_PACKAGES_DIR" # whole dir — resolve will not re-materialize artifacts into a partial tree | |
| 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: Run Ghostty split-theme appearance regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| set -euo pipefail | |
| DERIVED_DATA_PATH="${RUNNER_TEMP:-/tmp}/cmux-terminal-core-split-theme" | |
| rm -rf "$DERIVED_DATA_PATH" | |
| ( | |
| cd Packages/macOS/CmuxTerminalCore | |
| xcodebuild \ | |
| -scheme CmuxTerminalCore-Package \ | |
| -destination "platform=macOS" \ | |
| -derivedDataPath "$DERIVED_DATA_PATH" \ | |
| build-for-testing | |
| ) | |
| xcrun xctest "$DERIVED_DATA_PATH/Build/Products/Debug/CmuxTerminalCoreTests.xctest" | |
| - name: Enable XCTest automation mode | |
| run: | | |
| set -euo pipefail | |
| if ! command -v automationmodetool >/dev/null 2>&1; then | |
| echo "::warning::automationmodetool is unavailable; XCTest will use its default automation-mode setup" | |
| exit 0 | |
| fi | |
| if sudo -n true 2>/dev/null; then | |
| sudo -n automationmodetool enable-automationmode-without-authentication | |
| else | |
| echo "::warning::Passwordless sudo unavailable; XCTest will use its default automation-mode setup" | |
| fi | |
| - name: Run browser system proxy mirror regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| # Focused gate for https://github.com/manaflow-ai/cmux/issues/5888. | |
| # The full "Run unit tests" step tolerates app-host crashes by | |
| # parsing only the last test summary, so suites that run late can | |
| # otherwise be skipped by an app-host abort that still reports | |
| # "(0 unexpected)". Keep this targeted only-testing invocation as a | |
| # non-tolerant guard for the system proxy mirror regression. | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| scripts/ci/run-in-console-session.sh \ | |
| scripts/ci/run-app-host-xcodebuild.sh \ | |
| -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" \ | |
| CMUX_SKIP_ZIG_BUILD=1 \ | |
| -only-testing:cmuxTests/BrowserSystemProxyMirrorTests \ | |
| test | |
| - name: Run Option/Alt sided-modifier regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| scripts/ci/run-in-console-session.sh \ | |
| scripts/ci/run-app-host-xcodebuild.sh \ | |
| -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" \ | |
| CMUX_SKIP_ZIG_BUILD=1 \ | |
| -only-testing:cmuxTests/GhosttyOptionAsAltModsTests \ | |
| test | |
| - name: Run omnibar suggestion click regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| # Focused gate for https://github.com/manaflow-ai/cmux/issues/7380. | |
| # The tolerant full-suite step can crash-skip late suites (see #5888 | |
| # note above), so this regression gets a non-tolerant focused | |
| # invocation. | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| scripts/ci/run-in-console-session.sh \ | |
| scripts/ci/run-app-host-xcodebuild.sh \ | |
| -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" \ | |
| CMUX_SKIP_ZIG_BUILD=1 \ | |
| -only-testing:cmuxTests/BrowserOmnibarSuggestionClickRoutingTests \ | |
| test | |
| - name: Run remote tmux mirror active-pane regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| # Focused gates for https://github.com/manaflow-ai/cmux/issues/7372. | |
| # The tolerant full-suite step can crash-skip late suites (see #5888 | |
| # note above), so the seed and immediate-update paths get a | |
| # non-tolerant invocation. | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| scripts/ci/run-in-console-session.sh \ | |
| scripts/ci/run-app-host-xcodebuild.sh \ | |
| -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" \ | |
| CMUX_SKIP_ZIG_BUILD=1 \ | |
| -only-testing:cmuxTests/RemoteTmuxWindowMirrorFocusSeedTests \ | |
| test | |
| - name: Run remote tmux mirror layout identity regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| # The tolerant full-suite step may accept expected failures or stop | |
| # after an app-host crash. Keep #7833's identity suite non-tolerant | |
| # so a missing incremental-reconcile regression cannot pass the shard. | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| scripts/ci/run-in-console-session.sh \ | |
| scripts/ci/run-app-host-xcodebuild.sh \ | |
| -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" \ | |
| CMUX_SKIP_ZIG_BUILD=1 \ | |
| -only-testing:cmuxTests/RemoteTmuxMirrorLayoutIdentityTests \ | |
| test | |
| - name: Run unit tests | |
| run: | | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| SHARD_ARGS="$RUNNER_TEMP/cmux-unit-shard-${{ matrix.shard }}.args" | |
| python3 scripts/ci/cmux_unit_test_shard.py \ | |
| --shard-index "${{ matrix.shard }}" \ | |
| --shard-total 4 \ | |
| --output "$SHARD_ARGS" | |
| ONLY_TESTING_ARGS=() | |
| while IFS= read -r arg; do | |
| [ -n "$arg" ] && ONLY_TESTING_ARGS+=("$arg") | |
| done < "$SHARD_ARGS" | |
| run_unit_tests() { | |
| # These app-host tests create real SwiftUI/WebKit/Ghostty windows and | |
| # intermittently crash inside XCTest's post-test memory checker or | |
| # leave native display/WebKit work alive on GitHub macOS runners. | |
| # When that happens, Swift's crash/backtrace handling or the stale | |
| # app-host process keeps xcodebuild alive until the job-level timeout, | |
| # hiding the actual unit test summary. | |
| # The app-host wrapper also serializes GUI XCTest ownership per Mac, | |
| # while still allowing shards on separate Macs to run in parallel. | |
| scripts/ci/run-in-console-session.sh \ | |
| scripts/ci/run-app-host-xcodebuild.sh \ | |
| -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" \ | |
| "${ONLY_TESTING_ARGS[@]}" \ | |
| CMUX_SKIP_ZIG_BUILD=1 \ | |
| test 2>&1 & | |
| local xcodebuild_pid=$! | |
| local timeout_seconds="${CMUX_UNIT_TEST_TIMEOUT_SECONDS:-900}" | |
| local deadline=$((SECONDS + timeout_seconds)) | |
| while kill -0 "$xcodebuild_pid" 2>/dev/null; do | |
| if [ "$SECONDS" -ge "$deadline" ]; then | |
| echo "xcodebuild unit test timeout after ${timeout_seconds}s; terminating" | |
| kill -TERM "$xcodebuild_pid" 2>/dev/null || true | |
| sleep 5 | |
| kill -KILL "$xcodebuild_pid" 2>/dev/null || true | |
| wait "$xcodebuild_pid" 2>/dev/null || true | |
| return 124 | |
| fi | |
| sleep 5 | |
| done | |
| wait "$xcodebuild_pid" | |
| } | |
| # Stream output via tee so CI logs are visible in real time, while still | |
| # capturing for post-run analysis of expected vs unexpected failures. | |
| TEST_OUTPUT="$RUNNER_TEMP/cmux-unit-output-shard-${{ matrix.shard }}.txt" | |
| set +e | |
| run_unit_tests | tee "$TEST_OUTPUT" | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| OUTPUT=$(cat "$TEST_OUTPUT") | |
| set -e | |
| # SwiftPM binary artifact resolution can occasionally fail on ephemeral | |
| # runners with "Could not resolve package dependencies". Retry once after | |
| # clearing SwiftPM/DerivedData caches to recover from transient corruption. | |
| if [ "$EXIT_CODE" -ne 0 ] && echo "$OUTPUT" | grep -q "Could not resolve package dependencies"; then | |
| echo "SwiftPM package resolution failed, clearing caches and retrying once" | |
| rm -rf ~/Library/Caches/org.swift.swiftpm | |
| mkdir -p ~/Library/Caches/org.swift.swiftpm | |
| rm -rf "$CMUX_DERIVED_DATA_PATH" | |
| mkdir -p "$CMUX_DERIVED_DATA_PATH" | |
| set +e | |
| run_unit_tests | tee "$TEST_OUTPUT" | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| OUTPUT=$(cat "$TEST_OUTPUT") | |
| set -e | |
| fi | |
| if [ "$EXIT_CODE" -ne 0 ]; then | |
| SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1) | |
| if echo "$SUMMARY" | grep -q "(0 unexpected)"; then | |
| echo "All failures are expected, treating as pass" | |
| else | |
| echo "Unexpected test failures detected" | |
| exit 1 | |
| fi | |
| fi | |
| - name: Run bundled Ghostty theme picker helper regression | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| set -euo pipefail | |
| CMUX_SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" \ | |
| ./tests/test_bundled_ghostty_theme_picker_helper.sh | |
| - name: Ensure node for wrapper regressions | |
| id: detect-node | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| set -euo pipefail | |
| # The claude wrapper regressions below exec `node` from fake claude | |
| # binaries. The tart-macos-15 runner image shipped without node on | |
| # the runner PATH (2026-07-10), which failed every branch. Reuse an | |
| # image-provided node when present; otherwise install one below. | |
| for candidate in "$(command -v node || true)" /opt/homebrew/bin/node /usr/local/bin/node; do | |
| if [ -n "$candidate" ] && [ -x "$candidate" ]; then | |
| dirname "$candidate" >> "$GITHUB_PATH" | |
| "$candidate" --version | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| done | |
| echo "no node runtime in the runner image; installing via setup-node" | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| - name: Install node (runner image has none) | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) && steps.detect-node.outputs.found == 'false' }} | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "20" | |
| - name: Run CLI no-socket regressions | |
| if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }} | |
| run: | | |
| set -euo pipefail | |
| CLI_BIN="$( | |
| find "$CMUX_DERIVED_DATA_PATH" -path "*/Build/Products/Debug/cmux" -exec stat -f '%m %N' {} \; \ | |
| | sort -nr \ | |
| | head -1 \ | |
| | cut -d' ' -f2- | |
| )" | |
| if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then | |
| echo "cmux CLI binary not found in DerivedData" >&2 | |
| exit 1 | |
| fi | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_contract_help.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_layout_focus_contract.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_socket_operation_deadline.py | |
| python3 tests/test_stress_cli_socket_api.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omo_openagent_plugin_migration.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_socket_autodiscovery.py | |
| python3 tests/test_claude_wrapper_hooks.py | |
| python3 tests/test_claude_wrapper_mutual_shim_loop.py | |
| python3 tests/test_claude_wrapper_user_binary_resolution.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_claude_teams_fallback_path.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_claude_teams_env.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_claude_teams_trust_optin.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omo_fallback_path.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omx_fallback_path.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omc_fallback_path.py | |
| python3 tests/test_issue_2448_shell_claude_wrapper_dispatch.py | |
| python3 tests/test_issue_6714_zsh_shim_noclobber.py | |
| python3 tests/test_shell_git_branch_stale_cwd.py | |
| python3 tests/test_shell_git_config_remote_url_parsing.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_stop_last_assistant.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_clear_running_status.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_push_notification.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_pi_extension_install.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_omp_extension_install.py | |
| CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_campfire_extension_install.py | |
| tests: | |
| name: tests | |
| # Aggregate gate for the test/build suites in this workflow. Required by | |
| # branch protection under the stable name "tests", and references each suite | |
| # by its job KEY via needs:, so renaming/sharding a job's display name never | |
| # desyncs the required-checks list (the failure mode from the #6464 sharding | |
| # that stranded the old "tests" check). Add new ci.yml test/build jobs here. | |
| needs: | |
| - changes | |
| - linux-preflight | |
| - app-host-unit-tests | |
| - swift-package-tests | |
| - agent-session-web-resources | |
| if: ${{ always() }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check app-host unit test routing | |
| env: | |
| TESTS_NEEDS: ${{ toJSON(needs) }} | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| needs = json.loads(os.environ["TESTS_NEEDS"]) | |
| changes = needs["changes"] | |
| tests = needs["app-host-unit-tests"] | |
| macos = changes.get("outputs", {}).get("macos") | |
| if changes["result"] != "success": | |
| print(f"changes: {changes['result']}", file=sys.stderr) | |
| sys.exit(1) | |
| preflight = needs["linux-preflight"] | |
| if preflight["result"] != "success": | |
| print(f"linux preflight did not pass: {preflight['result']}", file=sys.stderr) | |
| sys.exit(1) | |
| if macos == "true" and tests["result"] != "success": | |
| print(f"app-host unit tests were required but did not pass: {tests['result']}", file=sys.stderr) | |
| sys.exit(1) | |
| if macos != "true" and tests["result"] not in {"success", "skipped"}: | |
| print(f"app-host unit tests had unexpected result for macos={macos}: {tests['result']}", file=sys.stderr) | |
| sys.exit(1) | |
| # The remaining test/build suites in this workflow gate too. A job that | |
| # opts out via its own path filter reports "skipped", which is fine; a | |
| # job that actually ran and failed must block the merge. | |
| allowed = {"success", "skipped"} | |
| for name in ("swift-package-tests", "agent-session-web-resources"): | |
| result = needs[name]["result"] | |
| if result not in allowed: | |
| print(f"{name} did not pass: {result}", file=sys.stderr) | |
| sys.exit(1) | |
| print(f"changes.macos={macos}") | |
| print(f"linux-preflight={preflight['result']}") | |
| print(f"app-host unit tests={tests['result']}") | |
| for name in ("swift-package-tests", "agent-session-web-resources"): | |
| print(f"{name}={needs[name]['result']}") | |
| PY | |
| swift-package-tests: | |
| needs: | |
| - changes | |
| - linux-preflight | |
| # !cancelled() disables the implicit success() gate, which GitHub evaluates | |
| # over the transitive needs chain: linux-preflight runs behind routed linux | |
| # jobs that legitimately skip (web/go/agent-session paths), and that | |
| # transitive skip otherwise marks every macOS job skipped even when | |
| # linux-preflight itself succeeds. Require the direct needs explicitly. | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && needs.linux-preflight.result == 'success' && needs.changes.outputs.macos == 'true' }} | |
| # Build the release helper with SDK 15, then run package tests with SDK 26. | |
| runs-on: ${{ vars.MACOS_RUNNER_DUAL_XCODE || 'blacksmith-6vcpu-macos-15' }} | |
| timeout-minutes: 40 | |
| env: | |
| CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_15 }} | |
| CMUX_CI_HELPER_XCODE_APP: ${{ vars.CMUX_CI_HELPER_XCODE_APP_MACOS_15 }} | |
| CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26" | |
| steps: | |
| - name: Clear stale git locks (self-hosted reused workspace) | |
| shell: bash | |
| run: | | |
| # Self-hosted macOS runners reuse the workspace. A job cancelled or | |
| # killed mid-checkout can leave a stale .git/modules/*/index.lock that | |
| # fails every later submodule checkout (e.g. ghostty). Clear them first. | |
| 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 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| submodules: recursive | |
| - name: Select helper Xcode | |
| run: | | |
| set -euo pipefail | |
| CMUX_CI_XCODE_APP="$CMUX_CI_HELPER_XCODE_APP" \ | |
| CMUX_CI_REQUIRED_MACOS_SDK_MAJOR=15 \ | |
| ./scripts/select-ci-xcode.sh | |
| - name: Install zig | |
| run: ./scripts/install-zig-ci.sh | |
| - name: Cache Zig packages | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.cache/zig | |
| key: zig-packages-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }} | |
| restore-keys: zig-packages- | |
| - name: Build universal Ghostty CLI helper | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ghostty-cli-helper | |
| ./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty | |
| lipo ghostty-cli-helper/ghostty -verify_arch arm64 x86_64 | |
| for arch in arm64 x86_64; do | |
| thin="ghostty-cli-helper/ghostty-$arch" | |
| lipo ghostty-cli-helper/ghostty -thin "$arch" -output "$thin" | |
| HELPER_SDK_VERSION="$(otool -l "$thin" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')" | |
| echo "Ghostty helper $arch SDK version: $HELPER_SDK_VERSION" | |
| [[ "$HELPER_SDK_VERSION" == 15.* ]] | |
| done | |
| - name: Upload universal Ghostty CLI helper | |
| id: upload-ghostty-cli-helper | |
| continue-on-error: true | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cmux-ghostty-cli-helper | |
| path: ghostty-cli-helper/ghostty | |
| if-no-files-found: error | |
| - name: Retry universal Ghostty CLI helper upload | |
| if: steps.upload-ghostty-cli-helper.outcome == 'failure' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cmux-ghostty-cli-helper | |
| path: ghostty-cli-helper/ghostty | |
| if-no-files-found: error | |
| overwrite: true | |
| - name: Select Xcode | |
| run: | | |
| set -euo pipefail | |
| ./scripts/select-ci-xcode.sh | |
| - name: Capture Ghostty revision | |
| id: ghostty-revision | |
| run: | | |
| echo "sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Cache GhosttyKit.xcframework | |
| id: cache-ghosttykit-package-tests | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: GhosttyKit.xcframework | |
| key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }} | |
| - name: Validate cached GhosttyKit.xcframework | |
| id: validate-ghosttykit-package-tests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -f GhosttyKit.xcframework/Info.plist ]; then | |
| echo "valid=true" >> "$GITHUB_OUTPUT" | |
| else | |
| rm -rf GhosttyKit.xcframework | |
| echo "valid=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Download pre-built GhosttyKit.xcframework | |
| if: steps.validate-ghosttykit-package-tests.outputs.valid != 'true' | |
| run: | | |
| ./scripts/download-prebuilt-ghosttykit.sh | |
| - name: Install Rust | |
| run: | | |
| ./scripts/install-rust-ci.sh | |
| - name: Run Swift package unit tests | |
| run: | | |
| set -euo pipefail | |
| # CmuxCommandPalette's nucleo FFI tests load the Rust dylib through | |
| # CMUX_NUCLEO_FFI_LIB (they skip when it is absent, so build it here | |
| # to keep the FFI parity suite a real gate). | |
| cargo build --manifest-path Native/CommandPaletteNucleoFFI/Cargo.toml --release | |
| export CMUX_NUCLEO_FFI_LIB="$PWD/Native/CommandPaletteNucleoFFI/target/release/libcmux_command_palette_nucleo_ffi.dylib" | |
| # The cmux-unit scheme only runs the cmuxTests app-host suite; it does | |
| # not execute the SPM package test targets. Run them here so package | |
| # tests (settings stores, secret-file migration, socket-control | |
| # convergence, etc.) are a real CI gate, not just compiled. | |
| # Scoped to packages that build headlessly via SwiftPM (no GhosttyKit / | |
| # app-target dependency). Add a package here once its `swift test` | |
| # is confirmed to resolve standalone. The GhosttyKit-referencing | |
| # packages (CmuxTerminalCore and the terminal packages stacked on | |
| # it) are the exception: their binaryTarget only needs the | |
| # xcframework present at the repo root (downloaded earlier in this | |
| # job), and their test runners link a C stub for the @_silgen_name | |
| # symbol instead of the GhosttyKit archive. | |
| PACKAGES=( | |
| CMUXAuthCore | |
| CmuxCanvasUI | |
| CmuxCore | |
| CmuxRemoteDaemon | |
| CmuxRemoteWorkspace | |
| CmuxRemoteSession | |
| CmuxAuthRuntime | |
| CmuxCommandPalette | |
| CmuxControlSocket | |
| CmuxFoundation | |
| CmuxGit | |
| CmuxSettings | |
| CmuxSettingsUI | |
| CmuxTerminal | |
| CmuxTerminalCore | |
| CmuxUpdater | |
| CMUXAgentLaunch | |
| ) | |
| # SwiftPM emits an error-severity diagnostic while planning the | |
| # GhosttyKit binaryTarget (the xcframework's static archive is not | |
| # lib-prefixed: "unexpected binary name"/"unexpected binary | |
| # framework"). The build and every test still succeed, but the | |
| # diagnostic poisons the process exit code on a fresh .build. For | |
| # the GhosttyKit-referencing packages only, tolerate exactly that | |
| # case: a nonzero exit passes only when the all-tests-passed summary | |
| # is present, no test failures are reported, and the only error | |
| # lines are that known diagnostic. Everything else (compile errors, | |
| # test failures, crashes) still fails the job. | |
| for pkg in "${PACKAGES[@]}"; do | |
| # Packages live under group folders (Packages/{Shared,iOS,macOS}/); | |
| # resolve the actual directory so this list stays group-agnostic. | |
| pkgdir="$(find Packages -mindepth 2 -maxdepth 2 -type d -name "$pkg" -print -quit)" | |
| if [ -z "$pkgdir" ]; then | |
| echo "::error::package '$pkg' not found under Packages/*/ (renamed or moved?)" | |
| exit 1 | |
| fi | |
| echo "::group::swift test $pkgdir" | |
| case "$pkg" in | |
| CmuxAuthRuntime) | |
| suite_list="$( | |
| find "$pkgdir/Tests" -name '*.swift' -print0 \ | |
| | xargs -0 perl -ne 'print "$1\n" if /^\s*@Suite(?:\([^)]*\))?\s+struct\s+([A-Za-z0-9_]+)/' \ | |
| | sort -u | |
| )" | |
| if [ -z "$suite_list" ]; then | |
| echo "::error::no Swift Testing suites found for $pkg" | |
| exit 1 | |
| fi | |
| while IFS= read -r suite; do | |
| [ -n "$suite" ] || continue | |
| echo "swift test $pkgdir --filter $suite" | |
| swift test --package-path "$pkgdir" --filter "$suite" | |
| done <<< "$suite_list" | |
| ;; | |
| CmuxTerminal|CmuxTerminalCore) | |
| test_status=0 | |
| output="$(swift test --package-path "$pkgdir" 2>&1)" || test_status=$? | |
| printf '%s\n' "$output" | |
| if [ "$test_status" -ne 0 ]; then | |
| if printf '%s\n' "$output" | grep -Eq 'Test run with [0-9]+ tests( in [0-9]+ suites)? passed' \ | |
| && ! printf '%s\n' "$output" | grep -Eq 'with [1-9][0-9]* failures?' \ | |
| && ! printf '%s\n' "$output" | grep -v 'unexpected binary' | grep -Eq '(^|[^a-zA-Z])error:'; then | |
| echo "Tolerated cosmetic GhosttyKit binaryTarget diagnostic; all tests passed." | |
| else | |
| exit "$test_status" | |
| fi | |
| fi | |
| ;; | |
| *) | |
| swift test --package-path "$pkgdir" | |
| ;; | |
| esac | |
| echo "::endgroup::" | |
| done | |
| agent-session-web-resources: | |
| needs: changes | |
| if: ${{ needs.changes.outputs.agent_session_web == 'true' }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| - name: Build and test Agent Session web resources | |
| run: | | |
| set -euo pipefail | |
| bun install --frozen-lockfile | |
| bun run agent-session-web:build | |
| bun run agent-session-web:test | |
| git diff --exit-code -- Resources/agent-session-react Resources/agent-session-solid | |
| linux-preflight: | |
| name: linux-preflight | |
| needs: | |
| - changes | |
| - workflow-guard-tests | |
| - remote-daemon-tests | |
| - web-typecheck | |
| - react-apps-check | |
| - web-db-migrations | |
| - agent-session-web-resources | |
| if: ${{ always() }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check cheap CI layer before macOS runners | |
| env: | |
| PREFLIGHT_NEEDS: ${{ toJSON(needs) }} | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| needs = json.loads(os.environ["PREFLIGHT_NEEDS"]) | |
| required = ("changes", "workflow-guard-tests") | |
| allowed_routed = { | |
| "remote-daemon-tests", | |
| "web-typecheck", | |
| "react-apps-check", | |
| "web-db-migrations", | |
| "agent-session-web-resources", | |
| } | |
| bad = {} | |
| for name in required: | |
| result = needs[name]["result"] | |
| if result != "success": | |
| bad[name] = result | |
| outputs = needs["changes"].get("outputs", {}) | |
| routed_outputs = { | |
| "remote-daemon-tests": "go", | |
| "web-typecheck": "web", | |
| "react-apps-check": "web", | |
| "web-db-migrations": "web", | |
| "agent-session-web-resources": "agent_session_web", | |
| } | |
| for name in sorted(allowed_routed): | |
| result = needs[name]["result"] | |
| route = routed_outputs[name] | |
| if outputs.get(route) == "true": | |
| if result != "success": | |
| bad[name] = f"{result} (route {route}=true)" | |
| elif result not in {"success", "skipped"}: | |
| bad[name] = result | |
| if bad: | |
| for name, result in bad.items(): | |
| print(f"{name}: {result}", file=sys.stderr) | |
| sys.exit(1) | |
| print( | |
| "routes: " | |
| f"macos={outputs.get('macos')} " | |
| f"web={outputs.get('web')} " | |
| f"go={outputs.get('go')} " | |
| f"agent_session_web={outputs.get('agent_session_web')}" | |
| ) | |
| for name, data in sorted(needs.items()): | |
| print(f"{name}: {data['result']}") | |
| PY | |
| tests-build-and-lag: | |
| needs: | |
| - changes | |
| - linux-preflight | |
| # !cancelled() disables the implicit success() gate, which GitHub evaluates | |
| # over the transitive needs chain: linux-preflight runs behind routed linux | |
| # jobs that legitimately skip (web/go/agent-session paths), and that | |
| # transitive skip otherwise marks every macOS job skipped even when | |
| # linux-preflight itself succeeds. Require the direct needs explicitly. | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && needs.linux-preflight.result == 'success' && needs.changes.outputs.macos == 'true' }} | |
| # Build the full cmux scheme once, then run the required display/runtime | |
| # regressions from the same DerivedData instead of queuing a second display | |
| # runner for UI-only checks. | |
| runs-on: ${{ vars.MACOS_RUNNER_DISPLAY || 'warp-macos-15-arm64-6x' }} | |
| # A cold DerivedData cache (any project.pbxproj or Package.resolved change | |
| # mints a new cache key with no restore-keys fallback) forces a full | |
| # cmux build whose Swift codegen alone can run 20+ min. Project/package | |
| # changes from the sidebar extension kit pushed this full build plus the | |
| # CA, lag, and UI regressions beyond 35 min before the cache could repopulate. | |
| timeout-minutes: 75 | |
| env: | |
| CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_15 }} | |
| CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26" | |
| steps: | |
| - name: Validate display runner identity | |
| env: | |
| REQUESTED_RUNNER: ${{ vars.MACOS_RUNNER_DISPLAY || 'warp-macos-15-arm64-6x' }} | |
| RUNNER_CONTEXT_NAME: ${{ runner.name }} | |
| run: | | |
| set -euo pipefail | |
| echo "Requested runner: $REQUESTED_RUNNER" | |
| case "$REQUESTED_RUNNER" in | |
| depot-*) | |
| case "$RUNNER_CONTEXT_NAME" in | |
| depot-*) | |
| echo "Resolved runner matches depot-*? yes" | |
| ;; | |
| *) | |
| echo "Resolved runner matches depot-*? no" | |
| echo "::error::$REQUESTED_RUNNER resolved outside Depot. Remove $REQUESTED_RUNNER from non-Depot self-hosted runners or choose a different runner." | |
| exit 1 | |
| ;; | |
| esac | |
| ;; | |
| *) | |
| echo "Display runner is not Depot; skipping Depot identity guard" | |
| ;; | |
| esac | |
| - name: Clear stale git locks (self-hosted reused workspace) | |
| shell: bash | |
| run: | | |
| # Self-hosted macOS runners reuse the workspace. A job cancelled or | |
| # killed mid-checkout can leave a stale .git/modules/*/index.lock that | |
| # fails every later submodule checkout (e.g. ghostty). Clear them first. | |
| 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 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| submodules: recursive | |
| - name: Select Xcode | |
| run: | | |
| set -euo pipefail | |
| ./scripts/select-ci-xcode.sh | |
| - name: Prepare isolated DerivedData | |
| run: | | |
| set -euo pipefail | |
| DERIVED_DATA_PATH="$RUNNER_TEMP/cmux-deriveddata-tests-build-and-lag" | |
| rm -rf "$DERIVED_DATA_PATH" | |
| mkdir -p "$DERIVED_DATA_PATH" | |
| echo "CMUX_DERIVED_DATA_PATH=$DERIVED_DATA_PATH" >> "$GITHUB_ENV" | |
| - name: Capture Ghostty revision | |
| id: ghostty-revision | |
| run: | | |
| echo "sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Cache GhosttyKit.xcframework | |
| id: cache-ghosttykit-lag | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: GhosttyKit.xcframework | |
| key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }} | |
| - name: Download pre-built GhosttyKit.xcframework | |
| if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true' | |
| run: | | |
| ./scripts/download-prebuilt-ghosttykit.sh | |
| - name: Install zig | |
| run: | | |
| ./scripts/install-zig-ci.sh | |
| - name: Install Rust | |
| run: | | |
| ./scripts/install-rust-ci.sh | |
| - name: Cache Zig packages | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.cache/zig | |
| key: zig-packages-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }} | |
| restore-keys: zig-packages- | |
| - name: Cache DerivedData | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ${{ runner.temp }}/cmux-deriveddata-tests-build-and-lag | |
| key: deriveddata-build-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('cmux.xcodeproj/project.pbxproj') }}-${{ steps.ghostty-revision.outputs.sha }} | |
| - name: Cache Swift packages | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: .ci-source-packages | |
| key: spm-build-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} | |
| restore-keys: spm-build- | |
| - name: Sanitize Swift package cache | |
| run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .ci-source-packages | |
| - name: Resolve Swift packages | |
| run: | | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| mkdir -p "$CMUX_DERIVED_DATA_PATH" | |
| mkdir -p "$SOURCE_PACKAGES_DIR" | |
| for attempt in 1 2 3; do | |
| if xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -resolvePackageDependencies; then | |
| # Guard against a stale/poisoned Swift-package cache: a restored | |
| # .ci-source-packages can make resolve report success without the | |
| # binary artifacts (Sparkle/Sentry) actually present, which then | |
| # fails the build. Verify they materialized; if not, clear and retry. | |
| 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 (stale cache); clearing and retrying" >&2 | |
| rm -rf "$SOURCE_PACKAGES_DIR" # whole dir — resolve will not re-materialize artifacts into a partial tree | |
| 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: Build for runtime regressions | |
| run: | | |
| set -euo pipefail | |
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | |
| xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Debug \ | |
| -derivedDataPath "$CMUX_DERIVED_DATA_PATH" \ | |
| -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ | |
| -disableAutomaticPackageResolution \ | |
| -destination "platform=macOS" build-for-testing 2>&1 | tee /tmp/cmux-build-output.txt | |
| - name: Validate Swift warning budget | |
| run: python3 scripts/swift_warning_budget.py --log /tmp/cmux-build-output.txt | |
| - name: Run display UI regressions | |
| run: scripts/ci/run-display-ui-regressions.sh | |
| - name: Create virtual display | |
| run: | | |
| set -euo pipefail | |
| HELPER_PATH="$RUNNER_TEMP/create-virtual-display" | |
| VDISPLAY_READY="$RUNNER_TEMP/cmux-build-lag-vdisplay.ready" | |
| VDISPLAY_ID_PATH="$RUNNER_TEMP/cmux-build-lag-vdisplay.id" | |
| VDISPLAY_LOG="$RUNNER_TEMP/cmux-build-lag-vdisplay.log" | |
| VDISPLAY_PID="" | |
| clang -framework Foundation -framework CoreGraphics \ | |
| -o "$HELPER_PATH" scripts/create-virtual-display.m | |
| cleanup_display_attempt() { | |
| if [ -n "${VDISPLAY_PID:-}" ]; then | |
| kill "$VDISPLAY_PID" >/dev/null 2>&1 || true | |
| for _ in $(seq 1 50); do | |
| kill -0 "$VDISPLAY_PID" >/dev/null 2>&1 || break | |
| sleep 0.1 | |
| done | |
| if kill -0 "$VDISPLAY_PID" >/dev/null 2>&1; then | |
| kill -9 "$VDISPLAY_PID" >/dev/null 2>&1 || true | |
| wait "$VDISPLAY_PID" >/dev/null 2>&1 || true | |
| fi | |
| VDISPLAY_PID="" | |
| fi | |
| scripts/ci/virtual-display-lock.sh reap-strays || true | |
| scripts/ci/virtual-display-lock.sh release || true | |
| } | |
| for attempt in 1 2 3; do | |
| rm -f "$VDISPLAY_READY" "$VDISPLAY_ID_PATH" "$VDISPLAY_LOG" | |
| LOCK_ENV="$(scripts/ci/virtual-display-lock.sh acquire)" | |
| eval "$LOCK_ENV" | |
| export CMUX_VDISPLAY_LOCK_DIR CMUX_VDISPLAY_LOCK_TOKEN | |
| { | |
| echo "CMUX_VDISPLAY_LOCK_DIR=$CMUX_VDISPLAY_LOCK_DIR" | |
| echo "CMUX_VDISPLAY_LOCK_TOKEN=$CMUX_VDISPLAY_LOCK_TOKEN" | |
| } >> "$GITHUB_ENV" | |
| # Now that we hold the lock, reap any leaked display helper so a | |
| # CGVirtualDisplay orphaned by a crashed/cancelled job cannot block | |
| # this create on persistent self-hosted runners. | |
| scripts/ci/virtual-display-lock.sh reap-strays || true | |
| "$HELPER_PATH" \ | |
| --ready-path "$VDISPLAY_READY" \ | |
| --display-id-path "$VDISPLAY_ID_PATH" \ | |
| >"$VDISPLAY_LOG" 2>&1 & | |
| VDISPLAY_PID=$! | |
| scripts/ci/virtual-display-lock.sh set-owner "$VDISPLAY_PID" | |
| { | |
| echo "VDISPLAY_PID=$VDISPLAY_PID" | |
| echo "VDISPLAY_HELPER_PATH=$HELPER_PATH" | |
| echo "VDISPLAY_READY=$VDISPLAY_READY" | |
| echo "VDISPLAY_ID_PATH=$VDISPLAY_ID_PATH" | |
| echo "VDISPLAY_LOG=$VDISPLAY_LOG" | |
| } >> "$GITHUB_ENV" | |
| for _ in $(seq 1 100); do | |
| if [ -s "$VDISPLAY_READY" ] && [ -s "$VDISPLAY_ID_PATH" ]; then | |
| break | |
| fi | |
| if ! kill -0 "$VDISPLAY_PID" 2>/dev/null; then | |
| echo "Virtual display helper exited before readiness on attempt $attempt" >&2 | |
| cat "$VDISPLAY_LOG" >&2 || true | |
| break | |
| fi | |
| sleep 0.1 | |
| done | |
| if [ -s "$VDISPLAY_READY" ] && [ -s "$VDISPLAY_ID_PATH" ]; then | |
| echo "Virtual display ready: $(tr -d '\n' < "$VDISPLAY_ID_PATH")" | |
| cat "$VDISPLAY_LOG" | |
| exit 0 | |
| fi | |
| echo "Virtual display not ready on attempt $attempt" >&2 | |
| cat "$VDISPLAY_LOG" >&2 || true | |
| cleanup_display_attempt | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "Failed to create virtual display after 3 attempts" >&2 | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| - name: Run CoreAnimation main-thread startup regression | |
| run: | | |
| set -euo pipefail | |
| APP="$( | |
| find "$CMUX_DERIVED_DATA_PATH" -path "*/Build/Products/Debug/cmux DEV.app" -exec stat -f '%m %N' {} \; \ | |
| | sort -nr \ | |
| | head -1 \ | |
| | cut -d' ' -f2- | |
| )" | |
| if [ -z "${APP:-}" ] || [ ! -d "$APP" ]; then | |
| echo "cmux DEV.app not found in DerivedData" >&2 | |
| exit 1 | |
| fi | |
| CMUX_ALLOW_UNTAGGED_CA_REGRESSION=1 \ | |
| CMUX_CA_ASSERT_HOLD_SECONDS=15 \ | |
| CMUX_TAG="ci-ca-main-thread" \ | |
| ./scripts/verify-main-thread-ca-transactions.sh "$APP" | |
| - name: Run workspace churn typing-lag regression | |
| run: | | |
| set -euo pipefail | |
| APP="$(find "$CMUX_DERIVED_DATA_PATH" -path "*/Build/Products/Debug/cmux DEV.app" -print -quit)" | |
| if [ -z "${APP:-}" ] || [ ! -d "$APP" ]; then | |
| echo "cmux DEV.app not found in DerivedData" >&2 | |
| exit 1 | |
| fi | |
| TAG="ci-lag" | |
| SOCK="/tmp/cmux-debug-${TAG}.sock" | |
| BUNDLE_ID="$( | |
| /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist" 2>/dev/null \ | |
| || echo 'com.cmuxterm.app.debug' | |
| )" | |
| pkill -x "cmux DEV" || true | |
| rm -f "$SOCK" "/tmp/cmux-${TAG}.sock" || true | |
| defaults write "$BUNDLE_ID" socketControlMode -string full >/dev/null 2>&1 || true | |
| CMUX_TAG="$TAG" CMUX_SOCKET_PATH="$SOCK" CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/tmp/cmux-ci-lag.log 2>&1 & | |
| APP_PID=$! | |
| trap 'kill "$APP_PID" >/dev/null 2>&1 || true' EXIT | |
| for _ in {1..240}; do | |
| [ -S "$SOCK" ] && break | |
| sleep 0.25 | |
| done | |
| [ -S "$SOCK" ] || { echo "Socket not ready at $SOCK" >&2; exit 1; } | |
| CMUX_SOCKET_PATH="$SOCK" \ | |
| CMUX_LAG_MAX_P95_RATIO=1.70 \ | |
| CMUX_LAG_MAX_AVG_RATIO=1.70 \ | |
| CMUX_LAG_MIN_BASELINE_P95_MS_FOR_RATIO=6.0 \ | |
| CMUX_LAG_MIN_BASELINE_AVG_MS_FOR_RATIO=4.0 \ | |
| CMUX_LAG_MAX_P95_DELTA_MS=20.0 \ | |
| CMUX_LAG_MAX_AVG_DELTA_MS=12.0 \ | |
| CMUX_LAG_MAX_CHURN_P95_MS=35 \ | |
| CMUX_LAG_KEY_EVENTS=180 \ | |
| python3 tests/test_workspace_churn_up_arrow_lag.py | |
| - name: Cleanup virtual display | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${VDISPLAY_PID:-}" ]; then | |
| kill "$VDISPLAY_PID" >/dev/null 2>&1 || true | |
| for _ in $(seq 1 50); do | |
| kill -0 "$VDISPLAY_PID" >/dev/null 2>&1 || break | |
| sleep 0.1 | |
| done | |
| if kill -0 "$VDISPLAY_PID" >/dev/null 2>&1; then | |
| kill -9 "$VDISPLAY_PID" >/dev/null 2>&1 || true | |
| wait "$VDISPLAY_PID" >/dev/null 2>&1 || true | |
| fi | |
| fi | |
| scripts/ci/virtual-display-lock.sh reap-strays || true | |
| scripts/ci/virtual-display-lock.sh release || true | |
| rm -f "${VDISPLAY_HELPER_PATH:-}" "${VDISPLAY_READY:-}" "${VDISPLAY_ID_PATH:-}" "${VDISPLAY_LOG:-}" | |
| release-build: | |
| needs: | |
| - changes | |
| - linux-preflight | |
| - swift-package-tests | |
| # See app-host-unit-tests: explicit direct-needs gate instead of the | |
| # implicit success() so skipped routed linux jobs upstream of | |
| # linux-preflight do not skip this job transitively. | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && needs.linux-preflight.result == 'success' && needs.swift-package-tests.result == 'success' && needs.changes.outputs.macos == 'true' }} | |
| # Compile the same unsigned universal Release app that nightly builds before | |
| # signing, notarization, and publishing. This catches DEBUG/Release boundary | |
| # mistakes before they reach main. | |
| # Keep the app build on macOS 26 so SDK-gated SwiftUI Liquid Glass code | |
| # compiles into the same artifact shape as nightly and stable releases. | |
| # Release builds need enough disk for a universal Release build plus the | |
| # restored SwiftPM cache. Default to a clean paid macOS 26 runner instead | |
| # of the generic persistent macOS 26 pool. | |
| runs-on: ${{ vars.MACOS_RUNNER_26_RELEASE || 'blacksmith-6vcpu-macos-26' }} | |
| timeout-minutes: 60 | |
| env: | |
| CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_26 }} | |
| CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26" | |
| steps: | |
| - name: Clear stale git locks (self-hosted reused workspace) | |
| shell: bash | |
| run: | | |
| # Self-hosted macOS runners reuse the workspace. A job cancelled or | |
| # killed mid-checkout can leave a stale .git/modules/*/index.lock that | |
| # fails every later submodule checkout (e.g. ghostty). Clear them first. | |
| 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 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| submodules: recursive | |
| - name: Select Xcode | |
| run: | | |
| set -euo pipefail | |
| ./scripts/select-ci-xcode.sh | |
| - name: Capture Ghostty revision | |
| id: ghostty-revision | |
| run: | | |
| echo "sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Reclaim release runner disk | |
| run: | | |
| set -euo pipefail | |
| echo "Disk before cleanup:" | |
| df -h / | |
| rm -rf build-universal .spm-cache | |
| echo "Disk after cleanup:" | |
| df -h / | |
| - name: Cache GhosttyKit.xcframework | |
| id: cache-ghosttykit-release | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: GhosttyKit.xcframework | |
| key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }} | |
| - name: Download pre-built GhosttyKit.xcframework | |
| if: steps.cache-ghosttykit-release.outputs.cache-hit != 'true' | |
| run: | | |
| ./scripts/download-prebuilt-ghosttykit.sh | |
| - name: Install Rust | |
| run: | | |
| ./scripts/install-rust-ci.sh | |
| - name: Cache DerivedData | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: build-universal | |
| key: deriveddata-release-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('cmux.xcodeproj/project.pbxproj') }}-${{ steps.ghostty-revision.outputs.sha }} | |
| - name: Cache Swift packages | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: .spm-cache | |
| key: spm-release-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} | |
| restore-keys: | | |
| spm-release- | |
| spm- | |
| - name: Sanitize Swift package cache | |
| run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .spm-cache | |
| - name: Build universal app (Release) | |
| run: | | |
| set -euo pipefail | |
| CMUX_SKIP_ZIG_BUILD=1 xcodebuild -project cmux.xcodeproj -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: Download universal Ghostty CLI helper | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: cmux-ghostty-cli-helper | |
| path: ghostty-cli-helper | |
| - name: Install universal Ghostty CLI helper | |
| run: | | |
| set -euo pipefail | |
| ./scripts/install-prebuilt-ghostty-cli-helper.sh \ | |
| ghostty-cli-helper/ghostty \ | |
| build-universal/Build/Products/Release/cmux.app | |
| - name: Validate Release artifact slices | |
| 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" | |
| test -x "$APP_BINARY" | |
| test -x "$CLI_BINARY" | |
| test -x "$HELPER_BINARY" | |
| file "$APP_BINARY" "$CLI_BINARY" "$HELPER_BINARY" | |
| SDK_VERSION="$(otool -l "$APP_BINARY" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')" | |
| echo "App SDK version: $SDK_VERSION" | |
| lipo "$APP_BINARY" -verify_arch arm64 x86_64 | |
| lipo "$CLI_BINARY" -verify_arch arm64 x86_64 | |
| lipo "$HELPER_BINARY" -verify_arch arm64 x86_64 | |
| [[ "$SDK_VERSION" == 26.* ]] | |
| ci-status: | |
| needs: | |
| - changes | |
| - workflow-guard-tests | |
| - remote-daemon-tests | |
| - web-typecheck | |
| - react-apps-check | |
| - web-db-migrations | |
| - linux-preflight | |
| - app-host-unit-tests | |
| - tests | |
| - swift-package-tests | |
| - agent-session-web-resources | |
| - tests-build-and-lag | |
| - release-build | |
| if: ${{ always() }} | |
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | |
| steps: | |
| - name: Check routed CI jobs | |
| env: | |
| CI_NEEDS: ${{ toJSON(needs) }} | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| needs = json.loads(os.environ["CI_NEEDS"]) | |
| allowed = {"success", "skipped"} | |
| bad = { | |
| name: data["result"] | |
| for name, data in sorted(needs.items()) | |
| if data["result"] not in allowed | |
| } | |
| if bad: | |
| for name, result in bad.items(): | |
| print(f"{name}: {result}", file=sys.stderr) | |
| sys.exit(1) | |
| for name, data in sorted(needs.items()): | |
| print(f"{name}: {data['result']}") | |
| PY |