From 2babbafb6ba15abf2739ca463f87a88c042aa0c8 Mon Sep 17 00:00:00 2001 From: malkuthro <166633396+malkuthro@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:02:37 +0200 Subject: [PATCH] chore(security): pin Actions and harden test lock (#273) * chore(security): pin Actions and update aiohttp * fix(security): generate universal test lock * fix(security): harden relock safeguards --- .github/dependabot.yml | 11 +++++ .github/workflows/ci.yml | 23 +++++---- .github/workflows/codeql.yml | 6 +-- .github/workflows/publish.yml | 4 +- .github/workflows/registry-metrics.yml | 4 +- .github/workflows/release.yml | 6 +-- CHANGELOG.md | 6 +++ CLAUDE.md | 11 +++-- constraints-test.txt | 49 ++++++++----------- docs/maintainers/dependency-security.md | 27 +++++++++-- pyproject.toml | 7 ++- scripts/bootstrap_test_env.ps1 | 54 +++++++++------------ scripts/bootstrap_test_env.sh | 54 +++++++++------------ tests/scripts/test_bootstrap_constraints.py | 48 ++++++++++++++---- 14 files changed, 180 insertions(+), 130 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f43099e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + open-pull-requests-limit: 1 + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7498bf4..964b801 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,13 +17,13 @@ jobs: name: Lint & Smoke runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-python@v6.2.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" - - uses: actions/setup-node@v6.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "22" @@ -46,9 +46,9 @@ jobs: name: Security Gate runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-python@v6.2.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" @@ -61,13 +61,18 @@ jobs: - name: Dependency audit run: pip-audit -r constraints-test.txt + - name: Audit all platform dependency pins + run: | + sed -E 's/[[:space:]]*;.*$//' constraints-test.txt > /tmp/constraints-all-platforms.txt + pip-audit --no-deps -r /tmp/constraints-all-platforms.txt + preflight: name: Pre-flight Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-python@v6.2.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" @@ -89,9 +94,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-python@v6.2.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 394171d..0a6e6a3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,16 +41,16 @@ jobs: build-mode: none steps: - name: Checkout - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@c4dd10e44af883a891fe31ced449bcb4a6728b9b # v3.37.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@c4dd10e44af883a891fe31ced449bcb4a6728b9b # v3.37.6 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f193c61..5c5d10b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,11 +12,11 @@ jobs: if: ${{ github.repository_owner == 'malkuthro' }} steps: - name: Check out code - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true - name: Publish Custom Node - uses: Comfy-Org/publish-node-action@v1 + uses: Comfy-Org/publish-node-action@0eb6cd742945443bee7f79eeddd4f732780029a1 # v1 branch head with: ## Add your own personal access token to your Github Repository secrets and reference it here. personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} diff --git a/.github/workflows/registry-metrics.yml b/.github/workflows/registry-metrics.yml index a6e770c..75c8e0c 100644 --- a/.github/workflows/registry-metrics.yml +++ b/.github/workflows/registry-metrics.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out source - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index deb5c17..df350d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: issues: write # publish-node-action opens an issue on failure steps: - name: Check out the tag - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true fetch-depth: 0 # full history so the tag-on-main ancestry check resolves @@ -51,7 +51,7 @@ jobs: exit 1 fi - name: Publish Custom Node - uses: Comfy-Org/publish-node-action@v1 + uses: Comfy-Org/publish-node-action@0eb6cd742945443bee7f79eeddd4f732780029a1 # v1 branch head with: personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} @@ -64,7 +64,7 @@ jobs: contents: write # create the GitHub Release steps: - name: Check out the tag - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Extract CHANGELOG section run: | set -euo pipefail diff --git a/CHANGELOG.md b/CHANGELOG.md index d51c577..a0b929c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is inspired by Keep a Changelog and SemVer. ## [Unreleased] +### Security +- **Supply-chain hardening.** Raised the test-dependency floors to patched + `aiohttp` and Pillow releases, regenerated the audited universal Python 3.11 + test lock with a forced upgrade, pinned every GitHub Action to an immutable + commit SHA, and enabled one grouped weekly Dependabot update for those pins. + ### Added - **Independent output type for published setups.** A `Koolook_PublishRouter` is auto-detected as the setup's **output selector**: whichever writer branches diff --git a/CLAUDE.md b/CLAUDE.md index 87c4650..28c90ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,8 @@ resolve of `.[test]` + its full transitive closure), then **audit** it with `pip-audit`; a known CVE fails the bootstrap. Idempotent (no-op if `.venv` exists) — pass `-Force` / `--force` to recreate, `-Relock` / - `--relock` to re-resolve and rewrite the lock, `-NoAudit` / `--no-audit` + `--relock` to re-resolve and upgrade the lock (requires `uv`, which is + checked before an existing `.venv` is replaced), `-NoAudit` / `--no-audit` to skip the audit (e.g. offline). Do not install into system Python or user-site packages. If `.venv` cannot be used, stop and explain the blocker before installing anywhere broader. @@ -47,7 +48,7 @@ Running the bootstrap script above is **pre-approved** — run it automatically whenever `.venv` is missing or `pytest` isn't importable, -without asking first. It is safe by construction: it installs only the +without asking first. Normal bootstrap is safe by construction: it installs only the already-reviewed, pinned `constraints-test.txt` set and `pip-audit`-gates the result, so it can never silently pull an unreviewed package or version. This **supersedes the global `/warmup` skill's `MISSING_VENV` hard-stop**: @@ -56,9 +57,9 @@ and continue — do not surface it as a blocker. Two things still need your attention, and only these: -- **Bootstrap exits non-zero** (pip-audit flagged a CVE, or no Python was - found) — surface *that* as the blocker. A failed audit is the one signal - that genuinely warrants a human. +- **Bootstrap exits non-zero** (pip-audit flagged a CVE, no Python was found, + or a requested relock cannot find `uv`) — surface *that* as the blocker. A + failed audit is the one signal that genuinely warrants a human. - **Dependency changes** — editing the `[test]` extras in `pyproject.toml` or regenerating `constraints-test.txt` (`--relock`) — still go through a reviewed PR diff. The lock file *is* the review surface; never relock diff --git a/constraints-test.txt b/constraints-test.txt index 64002fe..747f204 100644 --- a/constraints-test.txt +++ b/constraints-test.txt @@ -1,28 +1,17 @@ -# Locked test dependency set for ComfyUI-Koolook. -# -# Pinned resolve of the `[test]` extras in pyproject.toml plus their full -# transitive closure. The bootstrap scripts install with `-c -# constraints-test.txt`, so every fresh .venv is reproducible and -# pip-audit-verifiable. -# -# DO NOT hand-edit the version pins. To change the set: edit the `[test]` -# extras in pyproject.toml, then regenerate this file with -# bash scripts/bootstrap_test_env.sh --force --relock (POSIX) -# scripts\bootstrap_test_env.ps1 -Force -Relock (Windows) -# and commit the diff -- that diff is the dependency-change review surface. -# -aiohappyeyeballs==2.6.2 -aiohttp==3.14.1 +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --extra test --universal --python-version 3.11 --no-annotate --no-emit-package pip --output-file constraints-test.txt +aiohappyeyeballs==2.7.1 +aiohttp==3.14.3 aiosignal==1.4.0 attrs==26.1.0 -boolean.py==5.0 -CacheControl==0.14.4 -certifi==2026.6.17 -charset-normalizer==3.4.7 -colorama==0.4.6 +boolean-py==5.0 +cachecontrol==0.14.4 +certifi==2026.7.22 +charset-normalizer==3.4.9 +colorama==0.4.6 ; sys_platform == 'win32' cyclonedx-python-lib==11.11.0 defusedxml==0.7.1 -filelock==3.29.4 +filelock==3.32.2 frozenlist==1.8.0 idna==3.18 iniconfig==2.3.0 @@ -32,22 +21,26 @@ mdurl==0.1.2 msgpack==1.2.1 multidict==6.7.1 packageurl-python==0.17.6 -packaging==26.2 +packaging==26.3 pillow==12.3.0 pip-api==0.0.34 -pip_audit==2.10.1 +pip-audit==2.10.1 pip-requirements-parser==32.0.1 -platformdirs==4.10.0 +platformdirs==4.11.0 pluggy==1.6.0 propcache==0.5.2 py-serializable==2.1.0 -Pygments==2.20.0 +pygments==2.20.0 pyparsing==3.3.2 -pytest==9.1.0 +pytest==9.1.1 requests==2.34.2 rich==15.0.0 sortedcontainers==2.4.0 tomli==2.4.1 -tomli_w==1.2.0 +tomli-w==1.2.0 +typing-extensions==4.16.0 ; python_full_version < '3.13' urllib3==2.7.0 -yarl==1.24.2 +yarl==1.24.5 + +# The following packages were excluded from the output: +# pip diff --git a/docs/maintainers/dependency-security.md b/docs/maintainers/dependency-security.md index 440ead1..c33fcc1 100644 --- a/docs/maintainers/dependency-security.md +++ b/docs/maintainers/dependency-security.md @@ -12,13 +12,33 @@ check `constraints-test.txt` with `pip-audit`. `pip-audit` on the installed environment. - CI audits the committed lock with `pip-audit -r constraints-test.txt` on each pull request and on a weekly schedule, so newly disclosed CVEs in old pins are - caught even when nobody has touched the dependency files. + caught even when nobody has touched the dependency files. It also strips + environment markers into a temporary manifest and audits that manifest, so + Windows-only and Linux-only pins are each checked. - Bandit remains a separate first-party-code scan. A green Bandit job does not mean the dependency lock is clean. +## GitHub Actions + +Every third-party `uses:` reference in `.github/workflows/` is pinned to an +immutable commit SHA, with its source release or branch reference left in a +comment for readability. When updating an Action, resolve that reference to a +commit with the GitHub API, review the change in the PR, then update both the +SHA and its comment together. Do not use mutable major-version tags such as +`@v3` or `@v7` in committed workflows. + +Dependabot groups all GitHub Action updates into one weekly pull request. Python +test dependencies are updated through the deliberate universal relock below, so +the complete cross-platform lock diff stays one reviewable security unit. + ## Regenerating the lock -Regenerate the lock only as an intentional dependency-change PR: +Regenerate the lock only as an intentional dependency-change PR. The bootstrap +uses `uv` to resolve a universal Python 3.11 lock, including the conditional +dependencies needed by both CI platforms. The scripts check that `uv` is +available before replacing an existing `.venv`, then use `--upgrade` so the +relock can move stale vulnerable pins. Install `uv` before relocking if it is +not already available. ```powershell scripts\bootstrap_test_env.ps1 -Force -Relock @@ -29,8 +49,7 @@ bash scripts/bootstrap_test_env.sh --force --relock ``` Review and commit the resulting `constraints-test.txt` diff. Do not hand-edit -individual pins unless you are immediately re-running the relock command to -prove the full resolved set. +individual pins. ## Responding to a CVE diff --git a/pyproject.toml b/pyproject.toml index f2e4972..d670319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ addopts = ["--import-mode=importlib"] # ComfyUI custom node is not installed as a package in the workflow. # Locally use ``.venv/bin/pip install -e '.[test]'`` from the repo root. # ``aiohttp`` is a runtime dep of ``koolook_routes`` and must be importable -# even when only a pure helper is under test. The lower bound is pinned to -# the CVE-2026-54273..54280 fix line (>=3.14.1) so the declared range can't -# resolve a vulnerable aiohttp; the lock (constraints-test.txt) matches. -test = ["pytest>=8", "aiohttp>=3.14.1", "pip-audit>=2.7", "Pillow>=10"] +# even when only a pure helper is under test. Minimum versions exclude known +# vulnerabilities; constraints-test.txt pins the exact audited test set. +test = ["pytest>=8", "aiohttp>=3.14.3", "pip-audit>=2.7", "Pillow>=12.3.0"] diff --git a/scripts/bootstrap_test_env.ps1 b/scripts/bootstrap_test_env.ps1 index 0b56752..93d4a23 100644 --- a/scripts/bootstrap_test_env.ps1 +++ b/scripts/bootstrap_test_env.ps1 @@ -19,14 +19,26 @@ $ErrorActionPreference = "Stop" $Constraints = "constraints-test.txt" -if (Test-Path .venv) { - if (-not $Force) { - if ($Relock) { - Write-Host "-Relock requires -Force (the lock is rewritten from a fresh resolve); nothing was changed." - } - Write-Host ".venv already exists. Pass -Force to recreate." - exit 0 +if ((Test-Path .venv) -and (-not $Force)) { + if ($Relock) { + Write-Host "-Relock requires -Force (the lock is rewritten from a fresh resolve); nothing was changed." } + Write-Host ".venv already exists. Pass -Force to recreate." + exit 0 +} + +if ($Relock) { + if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + throw "-Relock requires uv to generate the universal Python 3.11 lock; nothing was changed." + } + Write-Host "Resolving upgraded universal Python 3.11 test lock ..." + uv pip compile pyproject.toml --extra test --universal --python-version 3.11 --upgrade --no-annotate --no-emit-package pip --output-file $Constraints + if ($LASTEXITCODE -ne 0) { + throw "uv could not generate $Constraints." + } +} + +if (Test-Path .venv) { Write-Host "Removing existing .venv ..." Remove-Item -Recurse -Force .venv } @@ -37,31 +49,13 @@ python -m venv .venv Write-Host "Upgrading pip + setuptools ..." .\.venv\Scripts\python -m pip install --quiet --upgrade pip setuptools -if ((Test-Path $Constraints) -and (-not $Relock)) { - Write-Host "Installing project + test extras (locked via $Constraints) ..." - .\.venv\Scripts\python -m pip install --quiet -e ".[test]" -c $Constraints -} else { - Write-Host "Resolving + installing project + test extras ..." - .\.venv\Scripts\python -m pip install --quiet -e ".[test]" - Write-Host "Writing locked set to $Constraints ..." - @' -# Locked test dependency set for ComfyUI-Koolook. -# -# Pinned resolve of the `[test]` extras in pyproject.toml plus their full -# transitive closure. The bootstrap scripts install with `-c -# constraints-test.txt`, so every fresh .venv is reproducible and -# pip-audit-verifiable. -# -# DO NOT hand-edit the version pins. To change the set: edit the `[test]` -# extras in pyproject.toml, then regenerate this file with -# bash scripts/bootstrap_test_env.sh --force --relock (POSIX) -# scripts\bootstrap_test_env.ps1 -Force -Relock (Windows) -# and commit the diff -- that diff is the dependency-change review surface. -# -'@ | Out-File -Encoding ascii $Constraints - .\.venv\Scripts\python -m pip list --format=freeze --exclude pip --exclude setuptools --exclude wheel --exclude koolook | Out-File -Encoding ascii -Append $Constraints +if (-not (Test-Path $Constraints)) { + throw "$Constraints is missing. Run: scripts\\bootstrap_test_env.ps1 -Force -Relock" } +Write-Host "Installing project + test extras (locked via $Constraints) ..." +.\.venv\Scripts\python -m pip install --quiet -e ".[test]" -c $Constraints + if (-not $NoAudit) { Write-Host "Auditing installed set (pip-audit) ..." .\.venv\Scripts\pip-audit --skip-editable diff --git a/scripts/bootstrap_test_env.sh b/scripts/bootstrap_test_env.sh index 6e967b8..ce411dd 100755 --- a/scripts/bootstrap_test_env.sh +++ b/scripts/bootstrap_test_env.sh @@ -41,14 +41,25 @@ if [ -z "$PYTHON_BIN" ]; then fi fi -if [ -d .venv ]; then - if [ -z "$FORCE" ]; then - if [ -n "$RELOCK" ]; then - echo "--relock requires --force (the lock is rewritten from a fresh resolve); nothing was changed." >&2 - fi - echo ".venv already exists. Pass --force to recreate." - exit 0 +if [ -d .venv ] && [ -z "$FORCE" ]; then + if [ -n "$RELOCK" ]; then + echo "--relock requires --force (the lock is rewritten from a fresh resolve); nothing was changed." >&2 fi + echo ".venv already exists. Pass --force to recreate." + exit 0 +fi + +if [ -n "$RELOCK" ]; then + if ! command -v uv >/dev/null 2>&1; then + echo "--relock requires uv to generate the universal Python 3.11 lock; nothing was changed." >&2 + exit 1 + fi + echo "Resolving upgraded universal Python 3.11 test lock ..." + uv pip compile pyproject.toml --extra test --universal --python-version 3.11 \ + --upgrade --no-annotate --no-emit-package pip --output-file "$CONSTRAINTS" +fi + +if [ -d .venv ]; then echo "Removing existing .venv ..." rm -rf .venv fi @@ -59,31 +70,14 @@ echo "Creating .venv ..." echo "Upgrading pip + setuptools ..." .venv/bin/python -m pip install --quiet --upgrade pip setuptools -if [ -f "$CONSTRAINTS" ] && [ -z "$RELOCK" ]; then - echo "Installing project + test extras (locked via $CONSTRAINTS) ..." - .venv/bin/python -m pip install --quiet -e '.[test]' -c "$CONSTRAINTS" -else - echo "Resolving + installing project + test extras ..." - .venv/bin/python -m pip install --quiet -e '.[test]' - echo "Writing locked set to $CONSTRAINTS ..." - cat > "$CONSTRAINTS" <<'EOF' -# Locked test dependency set for ComfyUI-Koolook. -# -# Pinned resolve of the `[test]` extras in pyproject.toml plus their full -# transitive closure. The bootstrap scripts install with `-c -# constraints-test.txt`, so every fresh .venv is reproducible and -# pip-audit-verifiable. -# -# DO NOT hand-edit the version pins. To change the set: edit the `[test]` -# extras in pyproject.toml, then regenerate this file with -# bash scripts/bootstrap_test_env.sh --force --relock (POSIX) -# scripts\bootstrap_test_env.ps1 -Force -Relock (Windows) -# and commit the diff -- that diff is the dependency-change review surface. -# -EOF - .venv/bin/python -m pip list --format=freeze --exclude pip --exclude setuptools --exclude wheel --exclude koolook >> "$CONSTRAINTS" +if [ ! -f "$CONSTRAINTS" ]; then + echo "$CONSTRAINTS is missing. Run: bash scripts/bootstrap_test_env.sh --force --relock" >&2 + exit 1 fi +echo "Installing project + test extras (locked via $CONSTRAINTS) ..." +.venv/bin/python -m pip install --quiet -e '.[test]' -c "$CONSTRAINTS" + if [ -n "$AUDIT" ]; then echo "Auditing installed set (pip-audit) ..." if .venv/bin/pip-audit --skip-editable; then diff --git a/tests/scripts/test_bootstrap_constraints.py b/tests/scripts/test_bootstrap_constraints.py index 49750b2..909863d 100644 --- a/tests/scripts/test_bootstrap_constraints.py +++ b/tests/scripts/test_bootstrap_constraints.py @@ -35,7 +35,7 @@ def _pinned_names() -> dict[str, str]: continue assert "==" in line, f"constraint not pinned with '==': {line!r}" name, version = line.split("==", 1) - pins[_canonical(name)] = version.strip() + pins[_canonical(name)] = version.split(";", 1)[0].strip() return pins @@ -66,12 +66,12 @@ def test_every_top_level_test_extra_is_pinned(): def test_no_duplicate_pins(): - names: list[str] = [] + pins: list[str] = [] for raw in CONSTRAINTS.read_text(encoding="utf-8").splitlines(): line = raw.strip() if line and not line.startswith("#"): - names.append(_canonical(line.split("==", 1)[0])) - dupes = sorted({name for name in names if names.count(name) > 1}) + pins.append(re.sub(r"\s+", " ", line)) + dupes = sorted({pin for pin in pins if pins.count(pin) > 1}) assert not dupes, f"duplicate pins in constraints-test.txt: {dupes}" @@ -84,15 +84,41 @@ def test_bootstrap_upgrades_setuptools_before_audit(): assert "--upgrade pip setuptools" in sh -def test_bootstrap_relock_does_not_inspect_editable_git_metadata(): - """Relock should work from cross-drive git worktrees on Windows.""" +def test_bootstrap_relock_checks_uv_before_replacing_venv(): + """A missing relock prerequisite must not discard a usable test env.""" ps1 = (REPO_ROOT / "scripts" / "bootstrap_test_env.ps1").read_text(encoding="utf-8") sh = (REPO_ROOT / "scripts" / "bootstrap_test_env.sh").read_text(encoding="utf-8") - assert "pip list --format=freeze" in ps1 - assert "pip list --format=freeze" in sh - assert "pip freeze --exclude-editable" not in ps1 - assert "pip freeze --exclude-editable" not in sh + assert ps1.index("Get-Command uv") < ps1.index("Remove-Item -Recurse -Force .venv") + assert sh.index("command -v uv") < sh.index("rm -rf .venv") + assert "pip list --format=freeze" not in ps1 + assert "pip list --format=freeze" not in sh + + +def test_bootstrap_relock_generates_a_universal_ci_lock(): + """Relocks include conditional dependencies for both CI platforms.""" + ps1 = (REPO_ROOT / "scripts" / "bootstrap_test_env.ps1").read_text(encoding="utf-8") + sh = (REPO_ROOT / "scripts" / "bootstrap_test_env.sh").read_text(encoding="utf-8") + + for script in (ps1, sh): + assert "uv pip compile pyproject.toml --extra test --universal" in script + assert "--python-version 3.11" in script + assert "--upgrade" in script + assert "--no-emit-package pip" in script + + +def test_constraints_cover_conditional_ci_dependencies(): + pins = _pinned_names() + + assert "typing-extensions" in pins + assert "colorama" in pins + + +def test_marker_versions_are_parsed_without_environment_markers(): + pins = _pinned_names() + + assert pins["typing-extensions"] == "4.16.0" + assert pins["colorama"] == "0.4.6" def test_ci_audits_committed_lock_on_prs_and_schedule(): @@ -100,6 +126,8 @@ def test_ci_audits_committed_lock_on_prs_and_schedule(): assert "schedule:" in ci assert "pip-audit -r constraints-test.txt" in ci + assert "pip-audit --no-deps -r /tmp/constraints-all-platforms.txt" in ci + assert "sed -E 's/[[:space:]]*;.*$//' constraints-test.txt" in ci def test_ci_pytest_installs_from_committed_lock():