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
95 changes: 70 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Validate self-hosted runner guards
- name: Validate Depot runner guards
run: ./tests/test_ci_self_hosted_guard.sh

- name: Validate create-dmg version pinning
Expand Down Expand Up @@ -44,12 +44,9 @@ jobs:
run: bun tsc --noEmit

tests:
# Never run self-hosted jobs for fork pull requests.
# Never run Depot jobs for fork pull requests (avoid billing on external PRs).
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
concurrency:
group: self-hosted-ci
cancel-in-progress: true
runs-on: depot-macos-latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check line 49 of .github/workflows/ci.yml =="
sed -n '45,55p' .github/workflows/ci.yml

echo
echo "== Find actionlint config files =="
find . -type f -name "*actionlint*" -o -name ".actionlint*" 2>/dev/null | head -20

echo
echo "== Check if actionlint.yaml exists in .github =="
ls -la .github/ | grep -i actionlint || echo "No actionlint config found in .github"

echo
echo "== Search for runner labels in workflows =="
rg -n "runs-on:" .github/workflows/ci.yml | head -20

Repository: manaflow-ai/cmux

Length of output: 768


🌐 Web query:

actionlint custom self-hosted runner labels configuration

💡 Result:

To make actionlint accept your custom self-hosted runner labels (so it won’t warn label "X" is unknown), add an actionlint config file (typically .github/actionlint.yaml) and list the allowed labels under self-hosted-runner.labels [1].

# .github/actionlint.yaml
self-hosted-runner:
  labels:
    - gpu
    - ubuntu-22.04-4core
    - my-team-runner

actionlint also supports glob patterns for these labels (so you don’t have to enumerate everything), e.g. [2]:

self-hosted-runner:
  labels:
    - private-linux-*

Background (GitHub-side): self-hosted runners always have self-hosted plus OS/arch labels (like linux, windows, x64, ARM64), and you can add your own custom labels (e.g. gpu) to route jobs via runs-on [3][4].


Add actionlint configuration for the custom Depot runner label.

Line 49 uses depot-macos-latest, a custom runner label from Depot. actionlint requires explicit configuration to recognize custom self-hosted runner labels; without it, the workflow will fail linting checks.

Add .github/actionlint.yaml:

Suggested fix
self-hosted-runner:
  labels:
    - depot-macos-latest
🧰 Tools
🪛 actionlint (1.7.11)

[error] 49-49: label "depot-macos-latest" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file

(runner-label)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 49, The workflow uses a custom self-hosted
runner label 'depot-macos-latest' (seen in the runs-on value) but actionlint
doesn't recognize custom labels; add an actionlint configuration file that
defines a self-hosted-runner with a labels list containing 'depot-macos-latest'
so actionlint will accept the custom runner label (create an actionlint YAML
config with keys self-hosted-runner -> labels -> ['depot-macos-latest']).

steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand All @@ -75,25 +72,48 @@ jobs:
xcodebuild -version
xcrun --sdk macosx --show-sdk-path

- name: Download Metal Toolchain
run: xcodebuild -downloadComponent MetalToolchain

- name: Build GhosttyKit.xcframework
- name: Download pre-built GhosttyKit.xcframework
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if ! command -v zig >/dev/null 2>&1; then
if command -v brew >/dev/null 2>&1; then
brew install zig
else
echo "zig is required to build GhosttyKit.xcframework. Install zig and retry." >&2
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
TAG="xcframework-$GHOSTTY_SHA"
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
MAX_RETRIES=30
RETRY_DELAY=20
for i in $(seq 1 $MAX_RETRIES); do
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
echo "Download succeeded on attempt $i"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
exit 1
fi
fi
(cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false)
rm -rf GhosttyKit.xcframework
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
done
Comment on lines +75 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/ci.yml | sed -n '75,97p'

Repository: manaflow-ai/cmux

Length of output: 1202


🏁 Script executed:

# Check for job timeout or step timeout in the workflow
rg -A 5 -B 5 "timeout" .github/workflows/ci.yml

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check what job this step belongs to and see overall workflow structure
cat -n .github/workflows/ci.yml | sed -n '50,100p'

Repository: manaflow-ai/cmux

Length of output: 2407


🏁 Script executed:

# Check for the job definition that contains this step
head -n 50 .github/workflows/ci.yml

Repository: manaflow-ai/cmux

Length of output: 1404


🏁 Script executed:

# Check the full "tests" job definition for timeout-minutes
rg -A 30 "jobs:" .github/workflows/ci.yml | grep -A 30 "tests:"

Repository: manaflow-ai/cmux

Length of output: 949


🏁 Script executed:

# Get lines around the "tests" job definition to check for timeout-minutes
sed -n '45,120p' .github/workflows/ci.yml

Repository: manaflow-ai/cmux

Length of output: 3088


Add explicit curl timeouts in the xcframework download loop.

The curl command on line 87 has no connection or operation timeouts. If a network stall occurs, curl will hang indefinitely and the retry logic becomes ineffective. Add --connect-timeout 15 --max-time 300 to ensure each attempt times out and the loop can proceed to the next retry.

Suggested fix
-            if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
+            if curl -fSL --connect-timeout 15 --max-time 300 \
+              -o GhosttyKit.xcframework.tar.gz "$URL"; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 75 - 97, The curl invocation inside
the download loop lacks connection/operation timeouts so a stalled network can
hang the retry loop; update the curl command that currently runs as curl -fSL -o
GhosttyKit.xcframework.tar.gz "$URL" (inside the for loop that uses MAX_RETRIES,
RETRY_DELAY and URL) to include explicit timeouts like --connect-timeout 15
--max-time 300 so each attempt fails fast and the retry logic can proceed; keep
the existing flags (-fSL -o ...) and error handling intact.

tar xzf GhosttyKit.xcframework.tar.gz
rm GhosttyKit.xcframework.tar.gz
test -d GhosttyKit.xcframework

- name: Create virtual display
run: |
set -euo pipefail
echo "=== Display before ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
echo ""
clang -framework Foundation -framework CoreGraphics \
-o /tmp/create-virtual-display scripts/create-virtual-display.m
/tmp/create-virtual-display &
VDISPLAY_PID=$!
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
Comment on lines +110 to +112

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 Fail fast if virtual-display helper dies at startup

This step starts create-virtual-display in the background and proceeds without checking that the process is still alive, so if the helper exits immediately (for example when CGVirtualDisplay is unavailable on a runner image) the step still reports success and UI tests continue headless. Because the same workflow notes headless UI runs can hang, this turns a setup failure into a later flaky timeout instead of an immediate actionable failure.

Useful? React with 👍 / 👎.

sleep 3
echo "=== Display after ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"

Comment on lines +102 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast when virtual display process exits early.

This step can pass even if /tmp/create-virtual-display dies immediately, which shifts failures into UI tests and increases flakiness.

Proposed hardening
       - name: Create virtual display
         run: |
           set -euo pipefail
           echo "=== Display before ==="
           system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
           echo ""
           clang -framework Foundation -framework CoreGraphics \
             -o /tmp/create-virtual-display scripts/create-virtual-display.m
           /tmp/create-virtual-display &
           VDISPLAY_PID=$!
           echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
           sleep 3
+          if ! kill -0 "$VDISPLAY_PID" 2>/dev/null; then
+            echo "Virtual display process exited before tests started" >&2
+            exit 1
+          fi
           echo "=== Display after ==="
           system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Create virtual display
run: |
set -euo pipefail
echo "=== Display before ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
echo ""
clang -framework Foundation -framework CoreGraphics \
-o /tmp/create-virtual-display scripts/create-virtual-display.m
/tmp/create-virtual-display &
VDISPLAY_PID=$!
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
sleep 3
echo "=== Display after ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
- name: Create virtual display
run: |
set -euo pipefail
echo "=== Display before ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
echo ""
clang -framework Foundation -framework CoreGraphics \
-o /tmp/create-virtual-display scripts/create-virtual-display.m
/tmp/create-virtual-display &
VDISPLAY_PID=$!
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
sleep 3
if ! kill -0 "$VDISPLAY_PID" 2>/dev/null; then
echo "Virtual display process exited before tests started" >&2
exit 1
fi
echo "=== Display after ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"

- name: Clean DerivedData
run: |
# Remove stale build cache to avoid incremental build errors
Expand Down Expand Up @@ -138,7 +158,7 @@ jobs:
EXIT_CODE=$?
set -e

# SwiftPM binary artifact resolution can occasionally fail on self-hosted
# 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
Expand Down Expand Up @@ -167,8 +187,33 @@ jobs:
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
-only-testing:cmuxUITests test
# SidebarResizeUITests hangs on headless Depot runners (mouse drag
# simulation doesn't work without a physical display, even with virtual
# display). Skip it in CI; it runs fine on local machines.
run_ui_tests() {
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
-maximum-test-execution-time-allowance 120 \
-only-testing:cmuxUITests \
-skip-testing:cmuxUITests/SidebarResizeUITests test 2>&1
}

# xcodebuild exits 65 even for expected failures (XCTExpectFailure).
# Capture output and fail only if there are unexpected failures.
set +e
OUTPUT=$(run_ui_tests)
EXIT_CODE=$?
set -e

echo "$OUTPUT"
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
44 changes: 26 additions & 18 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,7 @@ jobs:
build-sign-notarize-nightly:
needs: decide
if: needs.decide.outputs.should_build == 'true'
runs-on: self-hosted
concurrency:
group: self-hosted-nightly
cancel-in-progress: false
runs-on: depot-macos-latest

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 Reintroduce single-flight gating for nightly publish job

Dropping the concurrency group here allows multiple nightly runs to execute at the same time, but this workflow later force-moves the nightly tag (git tag -f/git push --force) and uploads release assets with overwrite_files: true, so the run that finishes last wins even if it built an older commit; when scheduled and manually-triggered runs overlap, the nightly channel can roll back to stale binaries/appcast unexpectedly. Please serialize this job again (or add an equivalent freshness check before publishing).

Useful? React with 👍 / 👎.

steps:
- name: Checkout main
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down Expand Up @@ -113,23 +110,34 @@ jobs:

- name: Install build deps
run: |
brew update
brew install zig
npm install --global "create-dmg@${CREATE_DMG_VERSION}"

- name: Build GhosttyKit.xcframework
run: |
cd ghostty
zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=native -Doptimize=ReleaseFast
cd ..
rm -rf GhosttyKit.xcframework
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework

- name: Clear SPM cache
- name: Download pre-built GhosttyKit.xcframework
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -rf ~/Library/Caches/org.swift.swiftpm
mkdir -p ~/Library/Caches/org.swift.swiftpm
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
set -euo pipefail
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
TAG="xcframework-$GHOSTTY_SHA"
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
MAX_RETRIES=30
RETRY_DELAY=20
for i in $(seq 1 $MAX_RETRIES); do
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
echo "Download succeeded on attempt $i"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
exit 1
fi
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
done
tar xzf GhosttyKit.xcframework.tar.gz
rm GhosttyKit.xcframework.tar.gz
test -d GhosttyKit.xcframework

- name: Configure SwiftPM cache
run: |
Expand Down
56 changes: 25 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ env:

jobs:
build-sign-notarize:
runs-on: self-hosted
concurrency:
group: self-hosted-release
cancel-in-progress: false
runs-on: depot-macos-latest

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 Serialize release workflow executions for same tag

Removing per-workflow concurrency makes the release asset guard non-atomic under overlap: two runs for the same tag can both pass the early guard before assets exist, then race to softprops/action-gh-release where overwrite_files: false causes the later run to fail (or leave a confusing partial state) once the first upload completes. This is most visible when a manual rerun starts while a tag-triggered release is still running.

Useful? React with 👍 / 👎.

steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down Expand Up @@ -102,38 +99,35 @@ jobs:
- name: Install build deps
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
brew update
brew install zig
npm install --global "create-dmg@${CREATE_DMG_VERSION}"

- name: Download Metal Toolchain
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: xcodebuild -downloadComponent MetalToolchain

- name: Build GhosttyKit.xcframework
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
cd ghostty
zig build -Demit-xcframework=true -Demit-macos-app=false -Doptimize=ReleaseFast
cd ..
rm -rf GhosttyKit.xcframework
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework

- name: Clear SPM cache
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
rm -rf ~/Library/Caches/org.swift.swiftpm
mkdir -p ~/Library/Caches/org.swift.swiftpm
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*

- name: Configure SwiftPM cache
- name: Download pre-built GhosttyKit.xcframework
if: steps.guard_release_assets.outputs.skip_all != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
CACHE_DIR="${RUNNER_TEMP}/swiftpm-cache/${GITHUB_RUN_ID}"
rm -rf "$CACHE_DIR"
mkdir -p "$CACHE_DIR"
echo "SWIFTPM_CACHE_PATH=$CACHE_DIR" >> "$GITHUB_ENV"
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
TAG="xcframework-$GHOSTTY_SHA"
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
MAX_RETRIES=30
RETRY_DELAY=20
for i in $(seq 1 $MAX_RETRIES); do
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
echo "Download succeeded on attempt $i"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
exit 1
fi
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
done
tar xzf GhosttyKit.xcframework.tar.gz
rm GhosttyKit.xcframework.tar.gz
test -d GhosttyKit.xcframework

- name: Derive Sparkle public key from private key
if: steps.guard_release_assets.outputs.skip_all != 'true'
Expand Down
12 changes: 6 additions & 6 deletions tests/test_ci_self_hosted_guard.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Regression test for https://github.com/manaflow-ai/cmux/issues/385.
# Ensures self-hosted UI tests are never run for fork pull requests.
# Ensures Depot-hosted UI tests are never run for fork pull requests.
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
Expand All @@ -9,7 +9,7 @@ WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci.yml"
EXPECTED_IF="if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository"

if ! grep -Fq "$EXPECTED_IF" "$WORKFLOW_FILE"; then
echo "FAIL: Missing fork pull_request guard for ui-tests in $WORKFLOW_FILE"
echo "FAIL: Missing fork pull_request guard for tests in $WORKFLOW_FILE"
echo "Expected line:"
echo " $EXPECTED_IF"
exit 1
Expand All @@ -18,12 +18,12 @@ fi
if ! awk '
/^ tests:/ { in_tests=1; next }
in_tests && /^ [^[:space:]]/ { in_tests=0 }
in_tests && /runs-on: self-hosted/ { saw_self_hosted=1 }
in_tests && /runs-on: depot-macos-latest/ { saw_depot=1 }
in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 }
END { exit !(saw_self_hosted && saw_guard) }
END { exit !(saw_depot && saw_guard) }
' "$WORKFLOW_FILE"; then
echo "FAIL: tests block must keep both self-hosted and fork guard"
echo "FAIL: tests block must keep both depot-macos-latest runner and fork guard"
exit 1
fi

echo "PASS: tests self-hosted fork guard is present"
echo "PASS: tests Depot runner fork guard is present"