Skip to content

fix: restore Windows CLI and preserve controller locks - #129

Merged
adithya-s-k merged 2 commits into
huggingface:mainfrom
KNambiarDJsc:fix/windows-fcntl-crash
Sep 15, 2026
Merged

adithya-s-k merged 2 commits into
huggingface:mainfrom
KNambiarDJsc:fix/windows-fcntl-crash

Conversation

@KNambiarDJsc

@KNambiarDJsc KNambiarDJsc commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Windows installations of 0.9.0 crash on every CLI command, including --version and --help, because parser registration reaches four modules that import POSIX-only fcntl.

Use a shared standard-library lock with lazy platform imports: retain flock on POSIX and lock byte zero through msvcrt on Windows. Controller locks remain nonblocking; the history checkout lock still waits for its owner. Closing the handle releases either lock, and the lock path is retained.

Add fresh-wheel Windows CI for Python 3.12–3.14, including CLI discovery, native UTF-8 task emission/static validation and real subprocess lock contention. Publication depends on the same Windows gate. Document the current host support scope. Thanks to @KNambiarDJsc for the report, initial fix and import regression test.

Test plan

  • Full macOS all-extras suite: 1,874 passed, 4 optional live checks skipped.
  • Ruff lint and formatting.
  • Fresh base-only wheel: CLI entrypoint, help, all 14 recipe descriptions, structured errors and native task validation.
  • Real subprocess tests: conflicting locks, blocking acquisition, normal/abrupt owner exit and POSIX compatibility with earlier releases.
  • Native Windows wheel checks on Python 3.12, 3.13 and 3.14.
  • Linux CI suite, runtime SDK contracts and strict docs build.

Out of scope

Full native Windows Tasksmith, research-recipe and quality-controller execution is tracked in #130. Use Linux, macOS or WSL for those controllers; choosing a remote sandbox does not remove host artifact/process requirements. No generated datasets need rewriting for this CLI/locking fix.

Closes #128

repo2rlenv is completely non-functional on Windows right now, in the
current v0.9.0 PyPI release: `repo2rlenv --version` raises
`ModuleNotFoundError: No module named 'fcntl'` before argument parsing
even completes. Verified from a fresh `pip install repo2rlenv` on a
clean venv, from a from-scratch git clone + uv sync, and from
upstream/main directly — not specific to any one install path.

Root cause: cli.py's dispatcher imports every subsystem's argparse
registration unconditionally, before routing to any subcommand, so
this runs on every invocation including --version and --help. Four
files reachable from that chain do `import fcntl` at module level —
fcntl is POSIX-only and has no stdlib equivalent on Windows at all
(msvcrt.locking byte-ranges a file rather than advisory-locking a
whole handle, so it isn't a drop-in swap):

  quality/loop/runner.py, tasksmith/runner.py, tasksmith/batch.py,
  pipelines/recipes/history/worker.py

Each uses it identically, for the same thing: an advisory, mostly
non-blocking flock() on a ".lock" file so two concurrent
controllers/loops don't run against the same output directory. Guard
both the import and the flock() call by `sys.platform`, preserving
each site's exact existing behavior (return type, exception handling)
on POSIX unchanged. Locking becomes a documented no-op on Windows
rather than a crash — a real gap, but not a regression, since locking
was never functional there before this either.

Added tests/test_posix_only_imports.py: an AST-based static check
(same shape as test_subprocess_encoding.py) that fails if any file
under src/repo2rlenv imports fcntl/pwd/grp/termios/tty/pty/posix/
resource/crypt/nis/spwd unconditionally at module level. Verified it
flags all 4 original offenders when the fix is reverted, and passes
clean with it applied — CI runs Linux-only, where none of these
modules are missing, so nothing else would have caught a regression
here.

Verified on Windows 11 / Python 3.12: `repo2rlenv --version` and
`--help` both now exit 0 and build the full subcommand list. Verified
on Linux (WSL Ubuntu, uv run --all-extras pytest -q — CI's exact
command): 1866 passed, 0 failed, both before and after this patch —
no regressions.

Refs huggingface#128
@hf-security-analysis

hf-security-analysis Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

⚠️ Workflow security review — 20 high, 14 medium, 3 low, 3 informational

Scanned 3 workflow file(s) with zizmor, pinact, OSV/GHSA, Claude, in full, as they stand on the default branch.

.github/workflows/ci.yml

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:35 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:64 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:65 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7

    Fix: Replace with - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:82 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:100 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:151 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:155 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7

    Fix: Replace with - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:169 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/ci.yml:188 (via pinact)
    Action is not pinned to an immutable commit SHA: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7

    Fix: Replace with uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:1 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:27 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:31 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:60 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:74 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:96 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:147 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/ci.yml:164 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🔵 LOW self-repository — .github/workflows/ci.yml:29 (via zizmor)
    use GitHub's dedicated self-repository syntax — use '$/...' instead of './...'

    Fix: use GitHub's dedicated self-repository syntax

  • ℹ️ INFORMATIONAL undetermined-permissions — .github/workflows/ci.yml (via Claude)
    Job windows: minimum permissions could not be determined — This job only calls the reusable workflow ./.github/workflows/windows.yml, which is not included in this file, so its token needs cannot be read here.

    Fix: Add GitHubSecurityLab/actions-permissions/monitor to this job, run the workflow, and apply the minimum it reports.

.github/workflows/release.yml

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:49 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:65 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:72 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7

    Fix: Replace with - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:88 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

  • ⚠️ HIGH template-injection — .github/workflows/release.yml:107 (via zizmor)
    code injection via template expansion — may expand into attacker-controllable code

    Fix: Map the expression into the step's env: block and reference the shell variable instead of interpolating it into run:.

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:126 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7

    Fix: Replace with - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:140 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8

    Fix: Replace with - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:146 (via pinact)
    Action is not pinned to an immutable commit SHA: uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1

    Fix: Replace with uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

  • ⚠️ HIGH unpinned-action — .github/workflows/release.yml:166 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8

    Fix: Replace with - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

  • ⚠️ HIGH template-injection — .github/workflows/release.yml:175 (via zizmor)
    code injection via template expansion — may expand into attacker-controllable code

    Fix: Map the expression into the step's env: block and reference the shell variable instead of interpolating it into run:.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/release.yml:1 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/release.yml:34 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/release.yml:41 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/release.yml:61 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/release.yml:81 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🟡 MEDIUM excessive-permissions — .github/workflows/release.yml:132 (via zizmor)
    overly broad permissions — default permissions used due to no permissions: block

    Fix: Declare an explicit permissions: block with the narrowest scopes the job needs, at job level rather than workflow level.

  • 🔵 LOW self-repository — .github/workflows/release.yml:36 (via zizmor)
    use GitHub's dedicated self-repository syntax — use '$/...' instead of './...'

    Fix: use GitHub's dedicated self-repository syntax

  • 🔵 LOW advisory — .github/workflows/release.yml:146 (via OSV/GHSA)
    pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 has a published advisory: GHSA-vxmw-7h4f-hqxh — PyPI publish GitHub Action vulnerable to injectable expression expansions in action steps

    Fix: Upgrade to a fixed release or remove the action. Details: GHSA-vxmw-7h4f-hqxh

  • ℹ️ INFORMATIONAL use-trusted-publishing — .github/workflows/release.yml:146 (via zizmor)
    prefer trusted publishing for authentication — this step

    Fix: prefer trusted publishing for authentication

  • ℹ️ INFORMATIONAL undetermined-permissions — .github/workflows/release.yml (via Claude)
    Job windows: minimum permissions could not be determined — This job only calls the reusable workflow ./.github/workflows/windows.yml, which is not in this file, so its token needs cannot be read here.

    Fix: Add GitHubSecurityLab/actions-permissions/monitor to this job, run the workflow, and apply the minimum it reports.

.github/workflows/windows.yml

  • ⚠️ HIGH unpinned-action — .github/workflows/windows.yml:26 (via pinact)
    Action is not pinned to an immutable commit SHA: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

    Fix: Replace with - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

🔀 A fix PR is open against this branch: #131

Pinning and static findings are deterministic. Findings marked (via Claude) are model judgements — check them before acting.

@adithya-s-k

Copy link
Copy Markdown
Collaborator

thanks for catching this and putting up the fix. i added real Windows locks so parallel controllers cannot write the same run, plus Windows wheel checks in CI and the release gate. local focused tests are passing; waiting on the native Windows jobs before merging. broader controller portability will be tracked separately.

@adithya-s-k adithya-s-k changed the title fix: guard fcntl imports so the CLI doesn't crash on Windows fix: restore Windows CLI and preserve controller locks Sep 15, 2026

@adithya-s-k adithya-s-k left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verified the final changes: locks remain exclusive on both platforms, the shared checkout still waits correctly, and all CI checks pass including Windows 3.12–3.14. thanks for the report and initial fix. the wider portability work is tracked in #130.

@adithya-s-k
adithya-s-k merged commit 9892bbe into huggingface:main Sep 15, 2026
11 checks passed
@KNambiarDJsc

Copy link
Copy Markdown
Contributor Author

Nice — msvcrt.locking + a manual retry loop for the blocking case is the right call, and the subprocess-based tests are exactly the hard cases that matter here (real cross-process contention, not mocked).

Pulled 640fbc into a clean worktree and ran it on a separate Windows 11 machine, independent of CI:

  • repo2rlenv --version / --help: exit 0, full subcommand list ✓
  • tests/test_locking.py: 4 passed, 1 skipped (the POSIX-interop test, correctly platform-gated) — including the blocking-wait/acquire handoff and lock-released-on-process-death cases
  • tests/test_posix_only_imports.py, tests/check_cli_install.py: pass

All green independently of the CI run. Looks ready from my side.

@adithya-s-k

Copy link
Copy Markdown
Collaborator

merged and released in 0.9.1. Windows CI and the fresh PyPI install checks passed. thanks for catching this and getting the fix started!

adithya-s-k added a commit that referenced this pull request Sep 15, 2026
* fix(ci): harden workflow files flagged on #129

* Complete workflow defaults and release credential hardening

---------

Co-authored-by: hf-security-analysis[bot] <265538906+hf-security-analysis[bot]@users.noreply.github.com>
Co-authored-by: adithya-s-k <adithyaskolavi@gmail.com>
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.

v0.9.0 / main: CLI is completely broken on Windows (ModuleNotFoundError: fcntl) — includes --version

2 participants