From 7c5bb833167afaae2ac1315b9d4f5afd09d8e0f7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 18 Oct 2026 18:39:56 -0500 Subject: [PATCH 1/2] CI(chore[macos]): Stop running macOS in CI why: macOS is supported and the library works there, but the hosted macOS cells have not been a reliable signal. They gated every pull request and master push through the report job, and a single macOS cell failing in Run the complete cell reddened runs whose Linux cells all passed: master runs 39163864246 and 39016247120, and pull request runs 39125933378 and 38231812946. Bringing it back is #47. what: - Remove the macOS entry from the compatibility cell matrix, along with the macOS dependency step and the Homebrew and jemalloc branch of the tmux build - Remove the debug_enabled and native_only dispatch inputs, the native child retirement trace, the tmate debug steps and .github/scripts/trace-process.rb, which only the macOS debug cell used - Drop macos from scripts/matrix so --require-all stops demanding macOS cells, and keep the Linux-only shape with assertions in the matrix and workflow tests - Say in the READMEs and CONTRIBUTING that macOS is supported on a best-effort basis and CI coverage for it is highly limited Closes #46 --- .github/CONTRIBUTING.md | 7 +- .github/scripts/trace-process.rb | 94 ------------------------ .github/workflows/compatibility.yml | 86 +--------------------- README.md | 5 +- gems/libtmux-async/README.md | 5 +- gems/libtmux-mcp/README.md | 3 +- gems/libtmux-workspace/README.md | 3 +- gems/libtmux/README.md | 5 +- scripts/matrix | 4 +- test/unit/compatibility_workflow_test.rb | 13 ++++ test/unit/matrix_test.rb | 5 +- 11 files changed, 40 insertions(+), 190 deletions(-) delete mode 100644 .github/scripts/trace-process.rb diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e43d122..1a28b51 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,7 +2,10 @@ This repository contains the libtmux gem suite. Installed-import tests establish package boundaries and execute consumer APIs and documentation -recipes. Compatibility CI records the exact Ruby, tmux and operating-system cells. +recipes. Compatibility CI records the exact Ruby and tmux cells on Linux. +macOS is supported on a best-effort basis, and CI coverage for it is highly +limited: no job runs there, so a macOS-only regression is not caught before +merge. Read [AGENTS.md](../AGENTS.md) for change discipline and [WRITING.md](WRITING.md) for prose and commit conventions. @@ -22,7 +25,7 @@ which tmux 3.2 clients cannot start; it also needs variable to `refused` or `supported` for the other version check tests, and they fail in CI when it is missing. The authored-shell tests use zsh's ZLE, `zsh/net/socket` and `zsh/system` -modules. The Linux CI job installs zsh; the macOS runner supplies it. +modules. The Linux CI job installs zsh. Install the pinned tool with mise: diff --git a/.github/scripts/trace-process.rb b/.github/scripts/trace-process.rb deleted file mode 100644 index 663b320..0000000 --- a/.github/scripts/trace-process.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -# Diagnostic preload and timer comparison; installed gems never require it. -require "json" - -if ARGV.delete("--compare-waits") - requested = 0.05 - measurements = [] - 6.times do |sample| - backends = sample.even? ? [:condition_variable, :select] : [:select, :condition_variable] - backends.each do |backend| - reader, writer = IO.pipe - mutex, changed = Mutex.new, ConditionVariable.new - worker = Thread.new do - Thread.handle_interrupt(Exception => :never) do - started = Process.clock_gettime(Process::CLOCK_MONOTONIC) - if backend == :condition_variable - mutex.synchronize { changed.wait(mutex, requested) } - else - IO.select([reader], nil, nil, requested) - end - Process.clock_gettime(Process::CLOCK_MONOTONIC) - started - end - end - begin - completed = !!worker.join(0.5) - measurements << {sample: sample + 1, backend: backend, requested_seconds: requested, - completed: completed, elapsed_seconds: completed ? worker.value : nil} - ensure - worker.kill if worker.alive? - worker.join(0.5) - reader.close - writer.close - end - end - end - puts JSON.pretty_generate(ruby: RUBY_DESCRIPTION, platform: RUBY_PLATFORM, - clock: "CLOCK_MONOTONIC", measurements: measurements) - exit -end - -require "libtmux/child" -require "minitest/autorun" - -module NativeChildTrace - STARTED = Process.clock_gettime(Process::CLOCK_MONOTONIC) - LOCK = Mutex.new - EVENTS = [] - - def self.record(phase, owner, detail = nil) - event = {seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - STARTED, - thread: Thread.current.object_id, owner: owner.object_id, phase: phase, detail: detail} - LOCK.synchronize { EVENTS << event } - end -end - -LibTmux::Internal::OwnedChild.prepend(Module.new do - [:spawned, :signal, :wait_observed, :finish_signalling, :join].each do |name| - define_method(name) do |*arguments, &block| - NativeChildTrace.record("#{name}:enter", self, arguments) - result = super(*arguments, &block) - NativeChildTrace.record("#{name}:leave", self, !!result) - result - rescue Exception => error - NativeChildTrace.record("#{name}:error", self, error.class.name) - raise - end - end -end) - -LibTmux::Internal::ProcessWait.prepend(Module.new do - def observe(pid) - NativeChildTrace.record("waitid:enter", self, pid) - super - ensure - NativeChildTrace.record("waitid:leave", self, pid) - end -end) - -Process.singleton_class.prepend(Module.new do - def wait2(*arguments) - NativeChildTrace.record("wait2:enter", self, arguments) - super - ensure - NativeChildTrace.record("wait2:leave", self, arguments) - end -end) - -Minitest.after_run do - report = {ruby: RUBY_DESCRIPTION, platform: RUBY_PLATFORM, clock: "CLOCK_MONOTONIC", - elapsed_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - NativeChildTrace::STARTED, - events: NativeChildTrace::LOCK.synchronize { NativeChildTrace::EVENTS.dup }} - File.write(ENV.fetch("CHILD_TRACE"), JSON.pretty_generate(report)) -end diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 689812f..1861e55 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -5,15 +5,6 @@ on: branches: [master] pull_request: workflow_dispatch: - inputs: - debug_enabled: - type: boolean - description: Open tmate on the macOS Ruby 4.0.7 / tmux 3.7c cell - default: false - native_only: - type: boolean - description: Run only the macOS Ruby 4.0.7 / tmux 3.7c cell without tmate - default: false permissions: contents: read @@ -55,9 +46,9 @@ jobs: strategy: fail-fast: false matrix: - platform: ${{ fromJSON(github.event_name == 'workflow_dispatch' && (inputs.debug_enabled || inputs.native_only) && '[{"system":"macos","runner":"macos-15"}]' || '[{"system":"linux","runner":"ubuntu-24.04"},{"system":"macos","runner":"macos-15"}]') }} - ruby: ${{ fromJSON(github.event_name == 'workflow_dispatch' && (inputs.debug_enabled || inputs.native_only) && '["4.0.7"]' || '["3.3.12","3.4.10","4.0.7"]') }} - tmux: ${{ fromJSON(github.event_name == 'workflow_dispatch' && (inputs.debug_enabled || inputs.native_only) && '["3.7c"]' || '["3.3a","3.4","3.5a","3.6","3.7c","3.8"]') }} + platform: [{system: linux, runner: ubuntu-24.04}] + ruby: ['3.3.12', '3.4.10', '4.0.7'] + tmux: ['3.3a', '3.4', '3.5a', '3.6', '3.7c', '3.8'] env: BUNDLE_FROZEN: 'true' LIBTMUX_VERSION_CHECK_EXPECT: supported @@ -78,9 +69,6 @@ jobs: run: | sudo apt-get update sudo apt-get install --yes libevent-dev libncurses-dev bison pkg-config zsh - - name: Install macOS build dependencies - if: runner.os == 'macOS' - run: brew install libevent ncurses bison pkg-config jemalloc - name: Build the exact tmux release shell: bash run: | @@ -92,47 +80,11 @@ jobs: ruby -rjson -rdigest -e 'expected = JSON.parse(File.read("scripts/tmux-releases.json")).fetch(ENV.fetch("TMUX_VERSION")); abort "tmux archive checksum mismatch" unless Digest::SHA256.file(ARGV.fetch(0)).hexdigest == expected' "$archive" tar -xzf "$archive" -C "$RUNNER_TEMP" configure_options=(--disable-utf8proc) - if [ "$RUNNER_OS" = macOS ]; then - PATH="$(brew --prefix bison)/bin:$PATH" - PKG_CONFIG_PATH="$(brew --prefix libevent)/lib/pkgconfig:$(brew --prefix ncurses)/lib/pkgconfig" - CPPFLAGS="-I$(brew --prefix libevent)/include -I$(brew --prefix ncurses)/include" - LDFLAGS="-L$(brew --prefix libevent)/lib -L$(brew --prefix ncurses)/lib" - if [ "$TMUX_VERSION" = 3.7c ]; then - PKG_CONFIG_PATH="$(brew --prefix jemalloc)/lib/pkgconfig:$PKG_CONFIG_PATH" - CPPFLAGS="-I$(brew --prefix jemalloc)/include $CPPFLAGS" - LDFLAGS="-L$(brew --prefix jemalloc)/lib $LDFLAGS" - configure_options+=(--enable-jemalloc) - fi - export PATH PKG_CONFIG_PATH CPPFLAGS LDFLAGS - fi cd "$RUNNER_TEMP/tmux-$TMUX_VERSION" ./configure --prefix="$RUNNER_TEMP/tmux-install" "${configure_options[@]}" make -j2 make install test "$("$RUNNER_TEMP/tmux-install/bin/tmux" -V)" = "tmux $TMUX_VERSION" - - name: Trace native child retirement samples - if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} - continue-on-error: true - timeout-minutes: 2 - shell: bash - run: | - mkdir -p build/native-process-trace - bundle exec ruby .github/scripts/trace-process.rb --compare-waits \ - > build/native-process-trace/wait-comparison.json - # Keep every sample and failure; these diagnostics are not a test retry gate. - for sample in 1 2 3 4 5 6; do - if CHILD_TRACE="build/native-process-trace/sample-$sample.json" \ - bundle exec ruby -Igems/libtmux/lib -r./.github/scripts/trace-process.rb \ - -Itest test/unit/process_test.rb \ - --name '/test_cancellation_escalates_and_reaps_a_child_ignoring_term|test_repeated_thread_cancellation_does_not_replace_the_first_failure/' \ - > "build/native-process-trace/sample-$sample.log" 2>&1; then - status=0 - else - status=$? - fi - printf '{"sample":%d,"exit_status":%d}\n' "$sample" "$status" \ - >> build/native-process-trace/samples.jsonl - done - name: Run the complete cell timeout-minutes: 5 run: >- @@ -148,36 +100,6 @@ jobs: name: matrix-${{ matrix.platform.system }}-${{ matrix.ruby }}-${{ matrix.tmux }} path: build/matrix/ if-no-files-found: warn - - name: Retain every native child retirement sample - if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} - continue-on-error: true - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: native-process-trace - path: build/native-process-trace/ - retention-days: 7 - if-no-files-found: error - - name: Debug the selected macOS cell - id: debug - if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled && matrix.platform.system == 'macos' && matrix.ruby == '4.0.7' && matrix.tmux == '3.7c' }} - uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3 - timeout-minutes: 10 - with: - detached: true - limit-access-to-actor: true - - name: Save the restricted debug SSH address - if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled && steps.debug.outputs.ssh-address != '' }} - env: - TMATE_SSH_ADDRESS: ${{ steps.debug.outputs.ssh-address }} - run: printf '%s\n' "$TMATE_SSH_ADDRESS" > "$RUNNER_TEMP/tmate-ssh-address.txt" - - name: Retain the restricted debug connection - if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled && steps.debug.outputs.ssh-address != '' }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: tmate-connection - path: ${{ runner.temp }}/tmate-ssh-address.txt - retention-days: 1 - if-no-files-found: error unsupported: name: Refuse tmux ${{ matrix.tmux }} before effects @@ -276,7 +198,7 @@ jobs: report: name: All required cells - if: ${{ always() && !(github.event_name == 'workflow_dispatch' && (inputs.debug_enabled || inputs.native_only)) }} + if: ${{ always() }} needs: [cell, unsupported, development] runs-on: ubuntu-24.04 timeout-minutes: 5 diff --git a/README.md b/README.md index d06c9b7..183c6f9 100644 --- a/README.md +++ b/README.md @@ -354,8 +354,9 @@ isolated installation and runs the recipes outside the checkout. minimum. A [server handle][server] checks the connected daemon's release, not the client's, and refuses an older daemon before any requested command runs. - **Linux, including WSL, and macOS.** The [compatibility workflow][compat] - runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04 and - macOS 15. + runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04. + macOS is supported on a best-effort basis, and CI coverage for it is highly + limited. - **tmux master and old daemons.** On Ubuntu the workflow also runs the suite against a pinned tmux master, and checks that tmux 3.0a, 3.1c, 3.2a and 3.3 daemons are refused. diff --git a/gems/libtmux-async/README.md b/gems/libtmux-async/README.md index 05be3e6..4d35c08 100644 --- a/gems/libtmux-async/README.md +++ b/gems/libtmux-async/README.md @@ -241,8 +241,9 @@ The contract is in [Control connections][behavior-control]. - **Async 2.46.** The gem depends on `async` ~> 2.46.0 and the same version of `libtmux`. - **Linux, including WSL, and macOS.** The [compatibility workflow][compat] - runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04 and - macOS 15, and retains per-revision results. + runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04 and retains + per-revision results. macOS is supported on a best-effort basis, and CI + coverage for it is highly limited. ## Documentation diff --git a/gems/libtmux-mcp/README.md b/gems/libtmux-mcp/README.md index 2d9da8e..3dde8f5 100644 --- a/gems/libtmux-mcp/README.md +++ b/gems/libtmux-mcp/README.md @@ -335,7 +335,8 @@ pass the invitation to [`accept_shell`][mcp-application-accept_shell]. and says to upgrade tmux and restart the server. - **Linux, including WSL, and macOS.** The core [compatibility workflow][compat] runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a - through 3.8 on Ubuntu 24.04 and macOS 15. + through 3.8 on Ubuntu 24.04. macOS is supported on a best-effort basis, and + CI coverage for it is highly limited. ## Documentation diff --git a/gems/libtmux-workspace/README.md b/gems/libtmux-workspace/README.md index 453d755..0e119ac 100644 --- a/gems/libtmux-workspace/README.md +++ b/gems/libtmux-workspace/README.md @@ -380,7 +380,8 @@ values by design. `error.kind` is `tmux_unsupported`. - **Linux, including WSL, and macOS.** The library and the command share the core [compatibility workflow][compat]: Ruby 3.3, 3.4 and 4.0 with tmux 3.3a - through 3.8 on Ubuntu 24.04 and macOS 15, with per-revision results. + through 3.8 on Ubuntu 24.04, with per-revision results. macOS is supported on + a best-effort basis, and CI coverage for it is highly limited. - **History limit.** On tmux 3.3a to 3.6 the reused initial pane keeps the global `history-limit`. Set the server's global value before you apply to get one history size. diff --git a/gems/libtmux/README.md b/gems/libtmux/README.md index 003bccb..85205f4 100644 --- a/gems/libtmux/README.md +++ b/gems/libtmux/README.md @@ -732,8 +732,9 @@ server and lists every option. [server handle][server] checks the connected daemon's release before it sends a command, and refuses an older daemon even when its client is newer. - **Linux, including WSL, and macOS.** The [compatibility workflow][compat] - runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04 and - macOS 15. + runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04. + macOS is supported on a best-effort basis, and CI coverage for it is highly + limited. - **tmux master and old daemons.** On Ubuntu the workflow also runs the suite against a pinned tmux master, and checks that tmux 3.0a, 3.1c, 3.2a and 3.3 daemons are refused. diff --git a/scripts/matrix b/scripts/matrix index 503e3da..62a9ed2 100755 --- a/scripts/matrix +++ b/scripts/matrix @@ -10,7 +10,7 @@ require "open3" module CompatibilityMatrix RUBIES = %w[3.3.12 3.4.10 4.0.7].freeze TMUXES = %w[3.3a 3.4 3.5a 3.6 3.7c 3.8].freeze - SYSTEMS = %w[linux macos].freeze + SYSTEMS = %w[linux].freeze ROOT = File.expand_path("..", __dir__) def self.cells @@ -34,7 +34,7 @@ module CompatibilityMatrix end def self.execute(cell, rubies, tmuxes, directory) - host = RUBY_PLATFORM.include?("linux") ? "linux" : RUBY_PLATFORM.include?("darwin") ? "macos" : "unsupported" + host = RUBY_PLATFORM.include?("linux") ? "linux" : "unsupported" return cell.merge("status" => "MISSING_RUNNER") unless host == cell.fetch("os") ruby, tmux = rubies[cell.fetch("ruby")], tmuxes[cell.fetch("tmux")] diff --git a/test/unit/compatibility_workflow_test.rb b/test/unit/compatibility_workflow_test.rb index 5498b6d..9564e77 100644 --- a/test/unit/compatibility_workflow_test.rb +++ b/test/unit/compatibility_workflow_test.rb @@ -22,4 +22,17 @@ def test_supported_cells_and_the_development_build_name_their_lane assert_equal "supported", WORKFLOW.fetch("jobs").fetch(name).fetch("env").fetch("LIBTMUX_VERSION_CHECK_EXPECT"), name end end + + def test_no_job_runs_on_macos + runners = WORKFLOW.fetch("jobs").flat_map do |_name, job| + [job.fetch("runs-on"), *job.dig("strategy", "matrix", "platform")&.map { |platform| platform.fetch("runner") }] + end + refute_empty runners + runners.each { |runner| refute_match(/macos/i, runner.to_s) } + assert_equal ["linux"], CompatibilityWorkflowTest.platform_systems + end + + def self.platform_systems + WORKFLOW.dig("jobs", "cell", "strategy", "matrix", "platform").map { |platform| platform.fetch("system") } + end end diff --git a/test/unit/matrix_test.rb b/test/unit/matrix_test.rb index 7f1c8f5..a3a4978 100644 --- a/test/unit/matrix_test.rb +++ b/test/unit/matrix_test.rb @@ -20,6 +20,7 @@ def test_plan_preserves_all_required_cells_and_cannot_claim_missing_cells_pass assert_equal "PARTIAL", report.fetch("status") assert report.fetch("cells").all? { |cell| cell.fetch("status") == "UNVERIFIED" } assert_equal %w[3.3a 3.4 3.5a 3.6 3.7c 3.8], report.fetch("cells").map { |cell| cell.fetch("tmux") }.uniq + assert_equal ["linux"], report.fetch("cells").map { |cell| cell.fetch("os") }.uniq assert_equal ["positive_native_identity"], report.fetch("cells").map { |cell| cell["process_bound_mcp"] }.uniq output, _, status = Open3.capture3(Gem.ruby, path, "--require-all") refute status.success? @@ -83,8 +84,8 @@ def test_failed_identity_probe_cannot_dispatch_the_suite File.write(ruby, "#!/bin/sh\nif [ \"$1\" = --disable=gems ]; then printf 4.0.7; else touch '#{marker}'; exit 7; fi\n") File.write(tmux, "#!/bin/sh\nprintf 'tmux 3.7c\\n'\nexit 1\n") [ruby, tmux].each { |path| File.chmod(0o700, path) } - os = RUBY_PLATFORM.include?("darwin") ? "macos" : "linux" - cell = CompatibilityMatrix.cells.find { |item| item.fetch("id") == "#{os}/4.0.7/3.7c" } + skip "the matrix runs on Linux only" unless RUBY_PLATFORM.include?("linux") + cell = CompatibilityMatrix.cells.find { |item| item.fetch("id") == "linux/4.0.7/3.7c" } result = CompatibilityMatrix.execute(cell, {"4.0.7" => ruby}, {"3.7c" => tmux}, directory) assert_equal "WRONG_TOOLCHAIN", result.fetch("status") refute File.exist?(marker) From 22bccdc53dc596441f8feff736c915d5e1b7560d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 18 Oct 2026 18:39:56 -0500 Subject: [PATCH 2/2] Changelog(docs): Record macOS leaving CI why: Pull requests stop waiting on the macOS cells, and a macOS-only regression is no longer caught before merge, so the entry says what CI no longer covers and that macOS stays supported. what: - Add an unreleased entry for removing macOS from CI - State that macOS is supported best-effort with limited CI coverage --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d981955..f91b702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.1.0.alpha.3 (unreleased) +- CI no longer runs macOS. macOS stays supported on a best-effort basis, and CI + coverage for it is highly limited, so a macOS-only regression is not caught + before merge. (#49) + ## 0.1.0.alpha.2 (2026-10-18) Requires tmux 3.3a or newer, checked against the running daemon, and runs on