ci: type-check every supported platform, and fix what that surfaced

`make type-check` ran mypy for whichever platform the developer happened
to be on, and no workflow ran it at all, so nothing gated a merge on it.
The practical effect was that `sys.platform == "win32"` branches were
never type-checked by anyone, while a Windows contributor saw six errors
that did not exist for anyone else.

Add a workflow running mypy under --platform linux, darwin and win32.
All three run on Linux because --platform is what selects the branches
mypy resolves; three runner OSes would cost three times as much and each
would still check whatever platform it happened to be.

Making --platform win32 pass needed the two platform-specific spots to
resolve under every target:

- codex.py reached fcntl.flock directly, which does not exist on
  Windows. Resolve it once behind a `sys.platform != "win32"` guard,
  which mypy narrows under every --platform, with a no-op fallback.
  Behaviour is unchanged: the ImportError path it used to rely on only
  ever fired on Windows, which the guard now covers explicitly.
- session_manager.py reached os.getuid/os.getgid. Wrap the existing
  Linux check in a platform guard so the attributes resolve, keeping the
  indirection through a local that the original comment explains.

`# type: ignore[attr-defined]` was not an option: warn_unused_ignores
would then flag it on the platforms where the attribute does exist.

`make type-check` now runs the same three platforms as CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
itzzdev09 2026-09-12 13:25:49 +05:30
parent 95e085eb6c
commit 336d86af4b
4 changed files with 92 additions and 16 deletions

43
.github/workflows/typecheck.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Type check
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
mypy:
# `--platform` is what selects the branches mypy resolves, so all three run
# on Linux. Checking out on three runner OSes would cost three times as much
# and still check whatever platform each runner happened to be.
strategy:
fail-fast: false
matrix:
platform:
- linux
- darwin
- win32
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: Install dependencies
run: uv sync --frozen
- name: mypy --platform ${{ matrix.platform }}
run: uv run mypy --platform ${{ matrix.platform }} strix/

View file

@ -46,8 +46,11 @@ lint:
@echo "✅ Linting complete!"
type-check:
@echo "🔍 Type checking with mypy..."
uv run mypy strix/
@echo "🔍 Type checking with mypy (linux, darwin, win32)..."
@for platform in linux darwin win32; do \
echo " → $$platform"; \
uv run mypy --platform $$platform strix/ || exit 1; \
done
@echo "🔍 Type checking with pyright..."
uv run pyright strix/
@echo "✅ Type checking complete!"

View file

@ -16,6 +16,7 @@ import hashlib
import json
import logging
import secrets
import sys
import threading
import time
import urllib.parse
@ -103,27 +104,52 @@ def logout() -> None:
AUTH_PATH.unlink()
# `fcntl` exists only on POSIX. Resolve it once behind a platform guard, which
# mypy narrows on every `--platform` it is asked to check, rather than at the
# call sites where it would need a per-platform ignore.
if sys.platform != "win32":
import fcntl
_HAS_FILE_LOCKING = True
def _lock_exclusive(descriptor: int) -> None:
fcntl.flock(descriptor, fcntl.LOCK_EX)
def _unlock(descriptor: int) -> None:
fcntl.flock(descriptor, fcntl.LOCK_UN)
else:
_HAS_FILE_LOCKING = False
def _lock_exclusive(descriptor: int) -> None:
"""Windows has no advisory file lock; the in-process lock still holds."""
def _unlock(descriptor: int) -> None:
"""Windows has no advisory file lock; the in-process lock still holds."""
@contextlib.contextmanager
def _refresh_guard() -> Iterator[None]:
"""Serialize token refresh within (lock) and across (flock) Strix processes,
so concurrent runs can't both spend the single-use refresh token."""
with _refresh_lock:
if not _HAS_FILE_LOCKING:
yield
return
try:
import fcntl
lock_path = AUTH_PATH.with_suffix(".lock")
lock_path.parent.mkdir(parents=True, exist_ok=True)
handle = lock_path.open("w")
except (ImportError, OSError):
except OSError:
yield
return
try:
with contextlib.suppress(OSError):
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
_lock_exclusive(handle.fileno())
yield
finally:
with contextlib.suppress(OSError):
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
_unlock(handle.fileno())
handle.close()

View file

@ -40,15 +40,19 @@ _PROTECTED_METADATA_NAMES = (".git", ".agents", ".codex")
def _host_identity_env() -> dict[str, str]:
# Read the platform through a local so it is not narrowed to whichever OS is
# type-checking: comparing sys.platform directly makes one of these branches
# statically dead, and which one flips between Linux and macOS.
platform_name: str = sys.platform
if platform_name != "linux":
return {}
# Bind-mount ownership only needs mapping on Linux, where the container uid
# must match the host's.
return {"STRIX_HOST_UID": str(os.getuid()), "STRIX_HOST_GID": str(os.getgid())}
# The outer guard is a real platform check, so mypy narrows it under every
# --platform it is asked about and `os.getuid` resolves where it exists.
if sys.platform != "win32":
# Inside it, read the platform through a local so it is not narrowed to
# whichever OS is type-checking: comparing sys.platform directly makes
# one of these branches statically dead, and which one flips between
# Linux and macOS.
platform_name: str = sys.platform
if platform_name == "linux":
# Bind-mount ownership only needs mapping on Linux, where the
# container uid must match the host's.
return {"STRIX_HOST_UID": str(os.getuid()), "STRIX_HOST_GID": str(os.getgid())}
return {}
def build_bind_mounts(local_sources: list[dict[str, Any]]) -> list[dict[str, Any]]: