Skip to content

ci: publish macOS arm64 TUI binary - #6

Merged
zy1233 merged 1 commit into
mainfrom
codex/macos-tui-build
Oct 1, 2026
Merged

zy1233 merged 1 commit into
mainfrom
codex/macos-tui-build

Conversation

@zy1233

@zy1233 zy1233 commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a macOS arm64 release job for the zypi TUI binary using the macos-14 runner.
  • Upload the binary as zypi-macos-arm64 and include it in GitHub Releases alongside the Linux binary.

Validation

  • Built the TUI successfully on local Apple Silicon with cargo build --release -p pi-pager-bin.
  • Confirmed the resulting binary is Mach-O arm64 and runs with --version.
  • Parsed .github/workflows/release.yml as YAML and ran git diff --check.

The workflow's release-dist profile will be exercised by the tagged release workflow.

@zy1233
zy1233 merged commit 09fc38d into main Oct 1, 2026
4 checks passed
@zy1233
zy1233 deleted the codex/macos-tui-build branch October 6, 2026 16:09
zy1233 added a commit that referenced this pull request Oct 10, 2026
…8, run on WSL2) (#9)

## Summary

The macOS half of `docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md` is merged (#6 to #8). This is the Linux half, run in WSL2 (Ubuntu 24.04, kernel 5.10 **without Landlock**, bubblewrap 0.9.0): build the TUI, run its Rust suites, drive a real `zypi` under a pseudo-terminal, and look at what it leaves behind. That turned up six things, and this PR fixes them. The plan's section 10.10 has the full record (what was run, what was found, what is not verified).

**What this PR is for in CI.** The numbers below were measured on WSL2, not on the runner. This PR's first TUI CI run is what confirms or corrects them:

- `doctor_cmd::` is no longer skipped in `pi-pager` (12 tests). The skip existed because the voice probe hung on macOS CoreAudio; the probe is gone and the tests pass here without an audio device. If they do not hold on the runner, put the skip back (one line in `scripts/tui_baseline.toml`).
- Two suites join the manifest: `pi-pager-render` (1,092 passed, 2 ignored) and `pi-pager-minimal` (79 passed).
- `pi-pager` `reference_passed` 5552 -> 5577; `pi-pager-bin` 20 -> 21.
- One known failure, `tmux_probe::tests::successful_near_deadline_exit_still_returns_captured_output`, is listed in `known_failures`. It spends 1.2 s of a 1.5 s budget in a shell script, so it fails when the machine stalls (twice on WSL2, both on the first run after linking, never in the 94 runs after). It is reported when it fails and does not block.

**CI result.** The runs on `d84d98a` are green (TUI CI [run 38052332014](https://github.com/zy1233/pi-python/actions/runs/38052332014)): 10 suites, **8,634 passed / 0 failed / 22 ignored**, each suite equal to its `reference_passed`. That is the local 8,633 plus the one known failure, which passed on the runner, so `known_failures` stays as a safety net and was not needed this time. `doctor_cmd::` held on the runner without the skip. `cargo test` took 32 m 40 s, `Gates + cargo check` 8 m 21 s, Ruff 8 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each; the release build baseline is skipped by design.

## The six findings

| # | What was wrong | Fix |
|---|---|---|
| 1 | `zypi -p` runs the Python agent as a plain child on the user's terminal, so the agent never sees EOF when only zypi is killed (`kill -9`, `kill <pid>`, an IDE's stop button). The agent and a `sleep 300` it had started kept running. | zypi names itself in `PI_AGENT_PARENT_PID`; `pi_agent_cli` polls `os.getppid()` every 0.5 s in `-p` mode and stops as it does on SIGTERM (cancel the turn, reap every tool's process group). `-p` also stops on SIGTERM / SIGHUP now (exit status 128 + signal). |
| 1b | With `--sandbox`, zypi re-executes itself under bubblewrap, and a plain bwrap only waits for its child. SIGKILL / SIGTERM / SIGHUP to bwrap alone left zypi, the agent and its tools running. | `bwrap --die-with-parent`, as its own commit. |
| 2 | `SandboxManager::apply()` degrades on purpose (no Landlock, or applying fails): it logs, returns `Ok`, and the run goes on with none of the profile's limits. The user was never told. | The reason is kept; a warning is printed on stderr at start-up, and the welcome bar and status bar show `sandbox:<profile> (not enforced)` in the error colour. |
| 3 | The agent's stderr went to `/dev/null` once the TUI was up, so a Python traceback went nowhere. | `<home>/logs/agent.stderr.log`: appended, one marker line per spawn, `0600`, rotated at 1 MiB at spawn, and a log that cannot be opened does not stop the agent. `-p` keeps the terminal. |
| 4 | Two tests failed intermittently on WSL2. | Test-only: a 25 ms gap between two writes (the 100 Hz kernel clock makes ctime coarse), and `theme_cache::test_lock()` in a test that raced the terminal-native tests. |
| 5 | The manifest skipped `doctor_cmd::` and had no `pi-pager-render` / `pi-pager-minimal`. | See the CI section above. |
| 6 | The Linux-only checks (what is left running after each way of leaving zypi, the sandbox label, `-p` shutdown) had no repeatable form. | `scripts/tui_pty/` drives a built `zypi` under a pty (a `pyte` screen reads what it draws) and inspects the process tree. They need a built binary, so they are **not** part of pytest or CI. |

## Decision for you

**`--die-with-parent` has a cost.** The sandboxed child is SIGKILLed when bwrap dies, so when only the outer bwrap gets SIGTERM or SIGHUP, zypi cannot restore the terminal (alternate screen, raw mode). Before the flag, the whole tree stayed up instead. I judged an unrestorable terminal better than a running agent nobody can see, but it is a trade, and the commit is only the flag and its test, so it is easy to revert. If you revert it, the CHANGELOG line and the plan's section 10.10 need the same edit.

## Not verified

- **Landlock itself.** It needs kernel 5.13 or later and the WSL2 kernel is 5.10, so the "applied" side of the sandbox is covered by unit tests only (applying it would confine the test process for good).
- **`-p` with a wrapper script as `PI_AGENT_COMMAND`.** The watch stays off when the process that started the agent is not the one named, so killing only zypi still leaves that agent behind. Windows has no counterpart for the watch.
- Section 10.10 of the plan lists the rest.

## Review guide

Nine commits. The first is not part of the Linux work: it was on local `main` and never pushed, so it rides along. If you squash-merge, it goes in under this PR's title; a rebase-merge keeps the nine as they are.

| Commit | What |
|---|---|
| `eafc578` | Not mine: phase7 fix (project-trust for saved workflows, a bad meta description no longer breaks the session) |
| `c13f7cc` | Finding 1: `-p` parent watch (Rust passes the pid; 15 pytest cases, POSIX only; 1 Rust test) |
| `d14e6f8` | Finding 1b: `--die-with-parent` and the test that pins its position before `--` |
| `2d499ad` | Finding 2: not-enforced notice (`pi-sandbox`, `pi-shell`, two render sites; 5 tests) |
| `91b5f62` | Finding 3: agent stderr log (6 tests) |
| `9489975` | Finding 4: two flaky tests, no production code |
| `478aa8a` | Finding 5: manifest |
| `15d2d85` | Finding 6: `scripts/tui_pty/` and `scripts/tests/test_tui_pty.py` |
| `4cf989f` | Docs: plan 10.10 and r8, `docs/baselines/tui.md`, CHANGELOG, AGENTS files |

## Verification

On WSL2, the whole manifest (`python scripts/tui_baseline.py test`): exit 0, 10 suites, 8,633 passed, 1 known failure, 22 ignored. pytest: 2,160 passed / 30 skipped on Linux (WSL), 2,156 passed / 34 skipped on Windows. `ruff check` and `ruff format --check` are clean. With a built `zypi` under a pty: `smoke.py` 23 checks (26 with `--sandbox workspace`), `exit_matrix.py` 12/12 (14/14 with the sandbox), `print_exit.py` 4/4 (the tool survived before the `-p` fix).
zy1233 added a commit that referenced this pull request Oct 12, 2026
…s nothing (#14)

## Summary

`release.yml` runs only for a `v*` tag, so the only way to test a release is to make one. This PR adds `workflow_dispatch`. A manual run is a dry run: `test`, `build`, `build-tui` and `build-tui-macos-arm` run and keep their artifacts. `publish-pypi` and `github-release` do not run.

```yaml
on:
  push:
    tags:
      - "v*"
  workflow_dispatch:

# publish-pypi and github-release:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
```

The `if` tests the event, not only the ref. A manual run can name a tag as its ref, and then `startsWith(github.ref, 'refs/tags/v')` alone is true.

Seven lines change, in one file. Nothing else changes: the jobs, the artifacts and the tag trigger are the same.

## Why now

This is exit condition 3 of the V0.5 milestone ([v0.5.0](https://github.com/zy1233/pi-python/milestone/1)).

- The macOS arm64 job (PR #6) has never run in a release. `github-release` waits for it. `publish-pypi` does not wait: if the macOS job fails, the Python packages are already on PyPI and the GitHub Release is missing.
- Linux CI cannot compile code that is only for macOS. 75 files under `tui/crates` hold `cfg(target_os = "macos")`. After the last macOS build, 88 files of `tui/` changed, and 8 of them hold such code.
- The release workflow of v0.3.0 needed three runs before it passed.

## Verification

- `actionlint` 1.7.12: no findings.
- The YAML parses, and the two `if` lines are the only changed conditions.
- The dry run ran from this branch (`gh workflow run release.yml --ref ci/release-dry-run`; GitHub accepts a manual run from a branch that is not on `main`). Run [38223353755](https://github.com/zy1233/pi-python/actions/runs/38223353755) succeeded in 27 minutes: `test` 97 s, the Python packages 29 s, the Linux x86_64 binary 16 m 26 s, the macOS arm64 binary 25 m 27 s. `publish-pypi` and `github-release` were skipped, so nothing was published. The artifacts (`dist`, `tui-binary`, `tui-binary-macos-arm64`) are kept until 2027-01-10.
- Not verified: the true side of the two `if` lines. It runs only for a real tag push.

## For the reviewer

- The three build jobs need `test`, so a dry run first runs the full pytest suite and Ruff. A dry run costs as much as a release run. The repository is public, so the minutes are free.
- A dry run does not use the `pypi` environment: the job that needs it is skipped.
- Creating the tag and the release stays a step for the maintainer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant