Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c28227b
ci: guard reusable-workflow permission grants (red on main's shape)
austinywang Sep 8, 2026
5794b17
ci: stop ios-screenshots.yml requesting actions: write (fixes release…
austinywang Sep 8, 2026
629a7dc
release: do not gate build-sign-notarize on iOS screenshot capture
austinywang Sep 8, 2026
e36bab6
release: give the screenshot capture job only contents: read and no s…
austinywang Sep 8, 2026
90577c9
release: let the Sparkle monotonic guard warn on non-tag dry runs
austinywang Sep 8, 2026
b1a422d
ci: give Gatekeeper twenty minutes to see a fresh notarization ticket
austinywang Sep 8, 2026
a8d7391
release: raise build-sign-notarize timeout to 90 minutes
austinywang Sep 8, 2026
bc9ef33
Merge remote-tracking branch 'origin/main' into issue-12149-release-w…
austinywang Sep 8, 2026
8357225
release: name the tunnel extension by its bundle identifier, not its …
austinywang Sep 8, 2026
844c17a
Merge remote-tracking branch 'origin/main' into issue-12149-release-w…
austinywang Sep 8, 2026
75cbd92
Fix main's package-test compile error and Swift warning-budget violat…
austinywang Sep 8, 2026
2137e3d
Normalize project.pbxproj (main bypassed the pre-commit hook in #12145)
austinywang Sep 8, 2026
2e7e15e
tests: drive sparkle_generate_appcast.sh through the no-delta release…
austinywang Sep 8, 2026
a1c2feb
release: generate the appcast when there are no previous archives (ba…
austinywang Sep 8, 2026
d6c2bd3
release: fail the Sparkle monotonic guard closed when the appcast is …
austinywang Sep 8, 2026
e7849bb
tests: assert the journal-carried pane clear that #11976 replaced cle…
austinywang Sep 8, 2026
33e849e
ci: make app-host hangs fail in minutes instead of the 75-minute job …
austinywang Sep 8, 2026
a6e13f8
ci: keep the canonical OUTPUT capture line the SPM-retry guard pins
austinywang Sep 8, 2026
7c133be
ci: keep a watchdog-killed app-host batch terminal; drop the wall-clo…
austinywang Sep 8, 2026
7cf004e
Merge remote-tracking branch 'origin/main' into issue-12149-release-w…
austinywang Sep 8, 2026
47823dc
chore: normalize project file after main merge
austinywang Sep 8, 2026
25eb4e4
test: remove timing dependency from terminal lane test
austinywang Sep 9, 2026
6683e48
ci: accept completed app-host summaries after launcher timeout
austinywang Sep 9, 2026
5d0075d
test: make idle watchdog coverage scheduler tolerant
austinywang Sep 9, 2026
91054a1
ci: require Swift Testing completion before accepting launcher timeout
austinywang Sep 9, 2026
b1d5ef5
ci: fail fast on known broad app-host hangs
austinywang Sep 9, 2026
7bd12a8
Merge remote-tracking branch 'origin/main' into issue-12149-release-w…
austinywang Sep 9, 2026
f01f68e
test: allowlist virtual retry delay fixtures
austinywang Sep 9, 2026
f7d9b92
ci: preserve app-host lock queue headroom
austinywang Sep 9, 2026
8eab135
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 9, 2026
d237e17
ci: restore terminal creation CLI regression coverage
austinywang Sep 9, 2026
c656274
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 9, 2026
7c350e3
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 9, 2026
103e897
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 9, 2026
3f10479
ci: retain release guard coverage after main merge
austinywang Sep 9, 2026
6a07983
ci: remove obsolete app-host idle override
austinywang Sep 9, 2026
3880eb0
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 9, 2026
f6233ac
cmuxTests: run the Coderouter no-socket tests without an unwaited exp…
austinywang Sep 9, 2026
e4847d5
ci: rerun a remote tmux mirror suite once after an app-host crash
austinywang Sep 9, 2026
6515679
test(iroh): promote the replacement connection deterministically
austinywang Sep 9, 2026
9b8a443
cmuxTests: serialize the stdin pump suite and bound its blocking waits
austinywang Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/test-determinism-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/OnboardingMacDiscove
cmuxTests/WorkspaceForkConversationContextMenuTests.swift assert-on-duration grandfathered
cmuxTests/WorkspaceForkConversationContextMenuTests.swift sleep-then-assert grandfathered
tests/test_ci_sparkle_build_monotonic.sh live-network-host release pretag guard intentionally probes the published appcast and soft-passes offline
Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxRetryAfterPolicyTests.swift sleep-then-assert upstream retry policy timing assertions
Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxRetryAfterPolicyTests.swift sleep-then-assert virtual-clock injection makes these large delays non-blocking and deterministic
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileTerminalLaneCoordinatorTests.swift sleep-then-assert upstream coordinator readiness timing assertion
58 changes: 48 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,21 @@ jobs:
- name: Validate release-build timeout guard
run: ./tests/test_ci_release_build_timeout.sh

- name: Validate reusable workflow permission grants
run: python3 tests/test_ci_reusable_workflow_permissions.py

- name: Validate release does not gate on iOS screenshot capture
run: ./tests/test_ci_release_ios_screenshots_decoupled.sh

- name: Validate Sparkle monotonic guard modes
run: ./tests/test_sparkle_build_monotonic_modes.sh

- name: Validate release tunnel extension identifiers
run: ./tests/test_ci_release_tunnel_identifiers.sh

- name: Validate Sparkle appcast generation without previous archives
run: ./tests/test_sparkle_generate_appcast_no_deltas.sh

- name: Validate markdown viewer asset compression
run: ./tests/test_compress_markdown_viewer_assets.sh

Expand Down Expand Up @@ -916,22 +931,45 @@ jobs:
# Ghostty-backed panels. Run the close and placement suites in separate
# app-host processes so renderer teardown from one topology workload
# cannot crash the next before its assertions execute.
#
# The app host still crashes intermittently mid-suite here
# (https://github.com/manaflow-ai/cmux/issues/9348): the crash point
# moves between tests and the relaunched host passes every remaining
# test. Rerun a suite once, and only when xcodebuild reports that the
# host exited, crashed, or timed out. An assertion failure never earns
# a rerun, and a second crash still fails the shard.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
for suite in \
RemoteTmuxMirrorCloseDetachTests \
RemoteTmuxMirrorDedicatedPlacementTests
do
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/$suite" \
test
for attempt in 1 2; do
suite_output="$RUNNER_TEMP/cmux-remote-tmux-mirror-${suite}-attempt-${attempt}.txt"
set +e
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/$suite" \
test 2>&1 | tee "$suite_output"
suite_status="${PIPESTATUS[0]}"
set -e
if [ "$suite_status" -eq 0 ]; then
break
fi
if [ "$attempt" -eq 1 ] \
&& grep -Fq 'Restarting after unexpected exit, crash, or test timeout' "$suite_output"; then
echo "::warning::app host crashed while running cmuxTests/${suite} (https://github.com/manaflow-ai/cmux/issues/9348); rerunning the suite once"
continue
fi
echo "cmuxTests/${suite} failed with status ${suite_status} on attempt ${attempt}"
exit "$suite_status"
done
done

- name: Run browser system proxy mirror regression
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/ios-screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,18 @@ on:
required: false
default: false
type: boolean
# Capture-only needs nothing beyond reading the repository: checkout runs with
# persist-credentials disabled, and artifact uploads use the runner's artifact
# token rather than GITHUB_TOKEN. Nothing here dispatches, cancels or deletes
# runs, so `actions: write` was never exercised, and declaring it broke every
# caller that grants less: GitHub validates a reusable workflow's permissions
# against the calling job at parse time, and release.yml failed at startup
# with "is requesting 'actions: write', but is only allowed 'actions: none'"
# (https://github.com/manaflow-ai/cmux/issues/12149). Keep this block at the
# minimum the steps use; tests/test_ci_reusable_workflow_permissions.py checks
# every local caller against it.
permissions:
contents: read
actions: write

jobs:
screenshots:
Expand Down
47 changes: 40 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,26 @@ env:
jobs:
generate-ios-screenshots:
name: Generate iOS App Store screenshots
# Runs alongside the macOS build so every stable tag still ships screenshot
# artifacts captured at the exact release ref. It is deliberately not a
# dependency of build-sign-notarize: the DMG consumes nothing from it, and a
# slow or flaky simulator capture (up to 300 minutes) must neither delay nor
# fail a macOS release (https://github.com/manaflow-ai/cmux/issues/12149).
# A failed capture still turns the run red, so it stays visible.
#
# Least privilege: capture-only reads the repository and nothing else, so
# this job hands the callee neither the release token scopes nor the
# repository secrets. The App Store Connect upload path inside the called
# workflow is gated to workflow_dispatch from main and is unreachable from
# a release run, so `secrets: inherit` would only have exposed the signing,
# notarization, Sparkle and R2 secrets to a UI-test job.
permissions:
contents: read
uses: ./.github/workflows/ios-screenshots.yml
with:
ref: ${{ github.ref }}
languages: "en-US,de-DE,fr-FR,ar-SA,es-ES,zh-Hant,zh-Hans,ko,ja"
upload: false
secrets: inherit

build-ghostty-cli-helper:
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
Expand Down Expand Up @@ -72,9 +86,7 @@ jobs:
retention-days: 3

build-sign-notarize:
needs:
- build-ghostty-cli-helper
- generate-ios-screenshots
needs: build-ghostty-cli-helper
# Build the app on macOS 26 so SDK-gated SwiftUI Liquid Glass code compiles
# into stable releases. The real universal Ghostty CLI helper is built on
# macOS 15 above because Zig 0.15.2 cannot link it on macOS 26.
Expand All @@ -84,7 +96,11 @@ jobs:
# Notarization wait times vary on Apple's side. The standalone Computer Use
# helper starts as soon as the unsigned app exists and overlaps the remaining
# release preparation; the outer app and final DMG still serialize afterward.
timeout-minutes: 60
# v0.64.22 took 40 minutes before the Cloud tunnel extension, its Go engine
# build and the universal diff sidecar joined this job, and the helper's
# Gatekeeper propagation wait alone can now take twenty minutes, so 60 left
# no headroom. Keep at least 25 percent above the measured duration.
timeout-minutes: 90
steps:
- name: Clear stale git locks (self-hosted reused workspace)
shell: bash
Expand All @@ -104,6 +120,12 @@ jobs:
submodules: recursive

- name: Validate Sparkle build number is monotonic
# A tag push is about to publish, so a stale build number must fail.
# A non-tag workflow_dispatch is the built-in dry run: it publishes
# nothing and normally runs from a branch that has not been bumped yet,
# so the same condition only warns there.
env:
CMUX_SPARKLE_MONOTONIC_MODE: ${{ startsWith(github.ref, 'refs/tags/') && 'enforce' || 'warn' }}
run: ./tests/test_ci_sparkle_build_monotonic.sh

- name: Guard immutable release assets
Expand Down Expand Up @@ -339,7 +361,11 @@ jobs:
run: |
set -euo pipefail
APP="build-universal/Build/Products/Release/cmux.app"
./scripts/normalize-system-extension-bundle.sh "$APP" "7WLXT3NR37.com.cmuxterm.app.tunnel"
# The Release build's tunnel extension is com.cmuxterm.app.tunnel: the
# bundle identifier carries no team prefix (only NEMachServiceName and
# the profile's App ID do), exactly as nightly ships <bundle id>.tunnel.
# tests/test_ci_release_tunnel_identifiers.sh pins this to the project.
./scripts/normalize-system-extension-bundle.sh "$APP" "com.cmuxterm.app.tunnel"
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"
Expand All @@ -355,7 +381,7 @@ jobs:
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
[[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]]
TUNNEL_BINARY="$APP/Contents/Library/SystemExtensions/7WLXT3NR37.com.cmuxterm.app.tunnel.systemextension/Contents/MacOS/cmuxTunnel"
TUNNEL_BINARY="$APP/Contents/Library/SystemExtensions/com.cmuxterm.app.tunnel.systemextension/Contents/MacOS/cmuxTunnel"
[ -x "$TUNNEL_BINARY" ] || { echo "Cloud tunnel extension binary not found at $TUNNEL_BINARY" >&2; exit 1; }
TUNNEL_ARCHS="$(lipo -archs "$TUNNEL_BINARY")"
echo "Tunnel extension architectures: $TUNNEL_ARCHS"
Expand Down Expand Up @@ -563,6 +589,13 @@ jobs:
exit 1
fi
./scripts/sparkle_generate_appcast.sh cmux-macos.dmg "$GITHUB_REF_NAME" appcast.xml
# macOS /bin/bash 3.2 can report "unbound variable" from a script with
# an EXIT trap and still exit 0; dry run 34227505375 (2026-09-08)
# uploaded a DMG with no appcast that way. The feed must exist, carry
# an EdDSA signature and point at this DMG before anything uploads it.
test -s appcast.xml
grep -q 'sparkle:edSignature' appcast.xml
grep -q 'cmux-macos.dmg' appcast.xml

- name: Upload build artifacts (dry-run)
if: steps.guard_release_assets.outputs.skip_upload != 'true' && github.event_name == 'workflow_dispatch'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,28 +340,35 @@ struct CmxIrohEndpointServerTests {
_ = try await supervisor.activate()
let blocker = EndpointServerHandlerBlocker()
let recorder = EndpointServerRecorder()
let first = TestIrohConnection(
remoteIdentity: remoteIdentity,
bidirectionalStreams: []
)
let replacement = TestIrohConnection(
remoteIdentity: remoteIdentity,
bidirectionalStreams: []
)
let server = CmxIrohEndpointServer(supervisor: supervisor) {
connection,
generation,
admission in
// Admit before recording so the test's `recorder.next()` below
// proves `first` is already active when `replacement` arrives,
// and promote only the replacement. Keying the promotion off the
// recorded count let the first handler observe the second record
// before its own check and promote `first` instead, which
// superseded the replacement and failed its markUsable (CI run
// 34414741413, swift-package-tests).
#expect(await admission())
await recorder.record(
identity: await connection.remoteIdentity(),
generation: generation
)
#expect(await admission())
if await recorder.recordedCount() == 2 {
if (connection as? TestIrohConnection) === replacement {
#expect(await admission.markUsable())
}
await blocker.wait()
}
let first = TestIrohConnection(
remoteIdentity: remoteIdentity,
bidirectionalStreams: []
)
let replacement = TestIrohConnection(
remoteIdentity: remoteIdentity,
bidirectionalStreams: []
)
var firstCloses = await first.closeEvents().makeAsyncIterator()

await server.start()
Expand Down
16 changes: 14 additions & 2 deletions cmuxTests/CLICoderouterCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
unlink(socketPath)
}

let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in
let respond: @Sendable (String) -> String = { line in
guard let payload = self.jsonObject(line),
let id = payload["id"] as? String,
let method = payload["method"] as? String else {
Expand All @@ -81,6 +81,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
error: ["code": "unexpected", "message": "Unexpected method \(method)"]
)
}
// A test that expects the CLI to stay off the socket must not hold a
// case-bound expectation it never waits on: the shared accept loop
// fulfills it when the listener closes below, and XCTest reports a
// fulfilled-but-unwaited expectation as an unexpected failure, which
// the app-host batch classifier turns into a red shard.
let serverHandled: XCTestExpectation?
if waitForSocket {
serverHandled = startMockServer(listenerFD: listenerFD, state: state, handler: respond)
} else {
serverHandled = nil
startDetachedMockServer(listenerFD: listenerFD, state: state, handler: respond)
}

var environment = ProcessInfo.processInfo.environment
environment["CMUX_SOCKET_PATH"] = socketPath
Expand All @@ -98,7 +110,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: standardInput,
timeout: 5
)
if waitForSocket {
if let serverHandled {
wait(for: [serverHandled], timeout: 5)
}
return (result, state)
Expand Down
10 changes: 10 additions & 0 deletions cmuxTests/CodexAppServerSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,11 @@ struct CodexAppServerSessionTests {
func testClaudeStreamJSONAccumulatorTracksDeltaTextPerAssistantMessage() {
var accumulator = ClaudeStreamJSONAccumulator()

expectEqual(
accumulator.consumeLine(
#"{"type":"message_start","message":{"id":"msg_1","role":"assistant"}}"#),
[]
)
expectEqual(
accumulator.consumeLine(
#"{"type":"content_block_delta","delta":{"type":"text_delta","text":"first"}}"#),
Expand All @@ -733,6 +738,11 @@ struct CodexAppServerSessionTests {
),
[" done"]
)
expectEqual(
accumulator.consumeLine(
#"{"type":"message_start","message":{"id":"msg_2","role":"assistant"}}"#),
[]
)
expectEqual(
accumulator.consumeLine(
#"{"type":"content_block_delta","delta":{"type":"text_delta","text":"second"}}"#),
Expand Down
40 changes: 36 additions & 4 deletions cmuxTests/SSHPTYAttachReconnectInputFilterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import Darwin
import Foundation
import Testing

@Suite struct SSHPTYAttachReconnectInputFilterTests {
// Serialized: each stdin-pump test parks a Swift Testing cooperative thread in
// a blocking wait while the pump under test is a detached task that needs one
// of those same threads. Running the five pump tests concurrently can leave
// no thread for any pump, and the suite then hangs until CI's idle timeout
// (run 34414741413, shard 3, three attempts). Every wait below is also
// bounded so a starved pump fails the test instead of the batch.
@Suite(.serialized) struct SSHPTYAttachReconnectInputFilterTests {
@Test func deadlineFlushesPendingAndStopsStripping() {
var expired = false
let filter = SSHPTYAttachReconnectInputFilter(
Expand Down Expand Up @@ -69,7 +75,7 @@ import Testing
let lateProbeReply = Data("\u{1B}[1;1R".utf8)
let forwardedInput = Data("printf keep\n".utf8)
try writeAll(fd: inputPipe[1], data: lateProbeReply + forwardedInput)
control?.stopFiltering()
#expect(control?.stopFiltering(timeoutMilliseconds: Self.waitTimeoutMilliseconds) == true)
Darwin.close(inputPipe[1])
inputPipe[1] = -1

Expand All @@ -95,7 +101,7 @@ import Testing
)
#expect(control != nil)

control?.stopFiltering()
#expect(control?.stopFiltering(timeoutMilliseconds: Self.waitTimeoutMilliseconds) == true)
let liveProbeReply = Data("\u{1B}[2;2R".utf8)
try writeAll(fd: inputPipe[1], data: liveProbeReply)
Darwin.close(inputPipe[1])
Expand Down Expand Up @@ -127,7 +133,7 @@ import Testing
try writeAll(fd: inputPipe[1], data: normalInput)
#expect(try readExactly(fd: bridgePair[1], count: normalInput.count) == normalInput)

control?.stopFiltering()
#expect(control?.stopFiltering(timeoutMilliseconds: Self.waitTimeoutMilliseconds) == true)
let liveProbeReply = Data("\u{1B}[3;3R".utf8)
try writeAll(fd: inputPipe[1], data: liveProbeReply)
Darwin.close(inputPipe[1])
Expand Down Expand Up @@ -197,10 +203,35 @@ import Testing
}
}

/// Longer than the pump's reconnect probe deadline, which the EOF reads
/// below legitimately wait out, and short enough that a pump that never
/// got scheduled fails the test well inside CI's 300s idle timeout.
private static let waitTimeoutMilliseconds: Int32 = 30_000

private struct WaitTimedOut: Error {}

private func waitReadable(fd: Int32) throws {
let events = Int16(POLLIN | POLLHUP | POLLERR | POLLNVAL)
var pollFD = pollfd(fd: fd, events: events, revents: 0)
while true {
let result = Darwin.poll(&pollFD, 1, Self.waitTimeoutMilliseconds)
if result > 0 {
return
}
if result == 0 {
throw WaitTimedOut()
}
if errno != EINTR {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
}
}

private func readUntilEOF(fd: Int32) throws -> Data {
var output = Data()
var buffer = [UInt8](repeating: 0, count: 1024)
while true {
try waitReadable(fd: fd)
let count = Darwin.read(fd, &buffer, buffer.count)
if count > 0 {
output.append(contentsOf: buffer.prefix(count))
Expand All @@ -217,6 +248,7 @@ import Testing
var buffer = [UInt8](repeating: 0, count: 1024)
while output.count < expectedCount {
let remaining = expectedCount - output.count
try waitReadable(fd: fd)
let count = Darwin.read(fd, &buffer, min(buffer.count, remaining))
if count > 0 {
output.append(contentsOf: buffer.prefix(count))
Expand Down
Loading
Loading