Skip to content

CI runs only on ubuntu-latest, so POSIX-only regressions reach PyPI before anyone notices 🤖🤖🤖 #95

Description

@Hotragn

The gap

Every job in .github/workflows/ci.yml and publish.yml is runs-on: ubuntu-latest. Nothing verifies that import nooa works on any other platform, so a module-level POSIX-only import reaches PyPI without anything failing first.

That is not hypothetical — it already happened, three times, and each time a user found it rather than CI:

Two independent reporters, three issues, all in the same class. I am not proposing to take any of them#92's reporter says they have a PR in progress, and #84/#85's reporter offered to submit both. This issue is about the missing guard that lets the class recur.

What I measured

I ran a ladder on native Windows 11 (Python 3.13, no WSL), applying the minimum shim for each blocker to see what the next one is:

STEP 1  bare `import nooa`                  ModuleNotFoundError: fcntl                    <- #84
STEP 2  + msvcrt-backed fcntl shim          AttributeError: signal has no SIGUSR2         <- #85
STEP 3  + signal.SIGUSR2 = 0                imported OK, version 0.0.1.dev162
STEP 4  SandboxedExecutor(require=False)    ModuleNotFoundError: resource                 <- #92
STEP 5  ShellTools.read()/write_file()      OK
STEP 6  ShellTools.run()                    AssertionError: pass_fds not supported on Windows
STEP 7  eval_pipeline._memory_monitor       ModuleNotFoundError: resource

Three things in there are worth separating out from the known issues:

STEP 3 is the important one. Once #84 and #85 are fixed, import nooa succeeds on native Windows today. The two blockers are both one-line platform guards, and nothing deeper is preventing importability. That makes an import smoke test a genuinely cheap gate rather than the first step of a long port.

STEP 6 and 7 are not reported in any open issue.

  • src/nooa/tools/_bash_session.py:173 passes pass_fds=(ctrl_w,) to create_subprocess_exec, which asserts on Windows. Note this is a different failure from the os.killpg/os.getpgid calls at lines 79-80 and 642-661 — those are downstream and never reached, so an audit that only greps for killpg misses the real blocker.
  • util/eval_pipeline/src/eval_pipeline/_memory_monitor.py:19 imports resource unconditionally.

STEP 5 is a useful negative result: the ShellTools file surface (read, replace, write_file) is already platform-clean. Only the shell-session surface is POSIX-bound.

Why a gate, and not just the fixes

The repo already has the right convention — hasattr(signal, "SIGUSR1") guards in packages/nooa-cli/src/nooa_cli/commands/eval.py:47 and util/eval_pipeline/src/eval_pipeline/__main__.py:13. It is applied in two places and missed in four. That is exactly the pattern that a per-PR check fixes and code review does not: nobody reviewing a diff on sqlite.py is thinking about win32.

There's also a contributor cost, which is how I ran into this. On a Windows checkout the test suite cannot start at all — conftest.py imports nooa.storage.sqlite — so uv run pytest fails at collection before a single test runs. Any Windows-based contributor is blocked on their first command.

Proposed design

Deliberately narrow, because full Windows support is a much bigger project and I don't think it's what's being asked for here. Running the whole suite on windows-latest is not viable: BashSession needs bash and pass_fds, and the Landlock/seccomp/RLIMIT sandbox is Linux-only by design.

Tier 1 — import smoke test (this issue). A windows-latest job that installs the built wheels and asserts each distribution imports:

  import-smoke:
    strategy:
      matrix:
        os: [windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
        with:
          python-version: "3.12"
      - run: uv run --no-project --with . python -c "import nooa; print(nooa.__version__)"

Seconds of runtime, no credentials, and it fails on exactly the bug class that reached 0.0.8. I'd suggest including macos-latest in the same matrix: macOS is POSIX so it wouldn't have caught #84, but it would catch a Linux-only assumption (/proc, landlock), and it's the platform most likely to be used by contributors after Linux.

Tier 2 — a posix marker, later and incrementally. pyproject.toml already carries integration, stress, and sandbox markers, so posix fits the existing convention. POSIX-bound tests get marked, and the Windows job grows to run the unmarked remainder. This should follow the fixes rather than lead them, and is a separate PR.

Sequencing

The Tier 1 job goes red until #84 and #85 land — that's the point of it, but it also means it can't merge before those do. Three options, and I'd like a maintainer call rather than guessing:

  1. Land the job after import nooa fails on native Windows: unconditional import fcntl in nooa/storage/sqlite.py #84/install_debug_handler() crashes at import on Windows: signal.SIGUSR2 AttributeError escapes the platform guard #85 merge. Cleanest, but depends on other contributors' PRs.
  2. Land it now with continue-on-error: true, then flip it once the fixes are in. Visible immediately, no red required check.
  3. Fold the two one-line guards into the same PR as the job. Fastest to green, but duplicates work import nooa fails on native Windows: unconditional import fcntl in nooa/storage/sqlite.py #84/install_debug_handler() crashes at import on Windows: signal.SIGUSR2 AttributeError escapes the platform guard #85's reporter already offered to do — which is why I'm not proposing it.

I'd suggest (1), with (2) as a fallback if #84/#85 stall.

What I'm offering

Happy to submit Tier 1 — the workflow job, plus a short "Supported platforms" note in CONTRIBUTING.md recording that the suite currently requires a POSIX host, so the next Windows contributor isn't surprised by a collection error. I'll also file the two unreported findings (STEP 6, STEP 7) as their own issue if you'd rather track them separately from this one; say the word and I'll split them out.

Not filing this as a PR first because CONTRIBUTING asks for an issue on anything substantial, and adding runners to CI is a maintainer cost decision rather than a code question.

🤖🤖🤖

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions