ci-hygiene guard: lockfile-only installs, pinned images, no host-port services (warn-only)
All checks were successful
check / gate (push) Successful in 19s

House rule 6 (09-23). The bridge now also posts windy-git/ci-hygiene on
every PR head (added lines) and default branch (whole files), scanning CI
workflows and Dockerfiles for: floating pip / uv pip installs (not -r,
not --no-deps, not exact pins), uv sync without --locked/--frozen,
npm install instead of npm ci (unless every package is exact-pinned),
yarn/pnpm without a frozen lockfile, :latest images and COPY lock* globs
(Windy Mail #147), and CI services publishing a HOST port (every job
shares one dind: Windy Mind runs 147/176 died on 5432). Warn-only;
CI_HYGIENE_MODE=block later. Allow-list ci/ci-hygiene-allow.yml (empty).

compute_guard's walker is now parameterised (line_fn / path_ok /
prefilter) so both guards share one scanner, cache and allow loader; the
bridge posts both through one _post_guard. Today: 95 issues in 21 repos;
windy-git, calendar, traveler, traveler-site clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kit OC5
2026-09-23 15:30:51 -04:00
parent e64a1b5fcb
commit 255aa58b35
6 changed files with 371 additions and 21 deletions

View File

@@ -0,0 +1,118 @@
"""CI hygiene guard (house rule 6): lockfile-only installs, no host-port services."""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
_spec = importlib.util.spec_from_file_location("ci_hygiene", ROOT / "scripts" / "ci_hygiene.py")
hy = importlib.util.module_from_spec(_spec)
sys.modules["ci_hygiene"] = hy
_spec.loader.exec_module(hy)
WF = ".github/workflows/ci.yml"
@pytest.mark.parametrize("path, text", [
(WF, " .venv/bin/pip install -e \".[dev]\""), # windy-git's own, before e64a1b5
("Dockerfile", "RUN pip install --no-cache-dir -e ."), # windy-git image, before e64a1b5
(WF, " - run: uv pip install -e \".[dev]\""), # WindyCloud #109's CI
(WF, " run: pip install fastapi uvicorn"),
(WF, " - run: uv sync --all-extras"), # windy-mind style, not locked
(WF, " - run: npm install"), # windy-drops / windytalk
(WF, " - run: npm install --no-save --no-audit --no-fund jsdom"), # windy-pro reality-check
(WF, " - run: yarn install"),
(WF, " - run: cd web && pnpm install"),
("docker/api.Dockerfile", "RUN apt-get update && pip install requests"),
])
def test_floating_installs_are_flagged(path, text):
assert [k for k, _ in hy.scan_line(path, text)] == ["floating install"]
@pytest.mark.parametrize("path, text", [
(WF, " python3 -m pip install -q uv==0.12.5"), # exact tool pin
(WF, " uv sync --locked --extra dev"),
(WF, " - run: uv sync --frozen"),
(WF, " - run: npm ci"),
(WF, " - run: npm install --no-save jsdom@24.1.0"),
(WF, " - run: pip install -r requirements.lock --require-hashes"),
(WF, " - run: pip install -r requirements.txt"),
("Dockerfile", " && pip install --no-cache-dir --require-hashes -r /tmp/requirements.txt \\\\"),
("Dockerfile", "RUN pip install --no-cache-dir --no-deps -e ."), # project only, deps from the lock
(WF, " .venv/bin/pip install -q --upgrade pip"),
(WF, " - run: yarn install --frozen-lockfile"),
(WF, " # - run: npm install (commented out)"),
(WF, " - run: echo 'pip is great'"),
])
def test_locked_or_pinned_installs_pass(path, text):
assert hy.scan_line(path, text) == []
@pytest.mark.parametrize("text, port", [
(" - 5432:5432", "5432"), # windy-mind / eternitas (collided 09-23)
(" - '15432:5432'", "15432"), # WindyCloud
(' - "6379:6379"', "6379"),
])
def test_services_publishing_a_host_port_are_flagged(text, port):
[(kind, match)] = hy.scan_line(WF, text)
assert kind == "host port" and port in match
def test_host_port_rule_is_for_workflows_only():
assert hy.scan_line("docker-compose.yml", " - 5432:5432") == []
@pytest.mark.parametrize("path, ok", [
(".github/workflows/ci.yml", True), (".gitea/workflows/check.yaml", True),
("Dockerfile", True), ("api/Dockerfile.prod", True), ("docker/web.Dockerfile", True),
("scripts/setup.sh", False), ("README.md", False), ("node_modules/x/Dockerfile", False),
(".github/lint/x.yml", False),
])
def test_scope_is_ci_workflows_and_dockerfiles(path, ok):
assert hy.path_ok(path) is ok
def test_warn_mode_never_turns_red(monkeypatch):
monkeypatch.setattr(hy, "MODE", "warn")
state, desc, f = hy.status_for([hy.cg.Finding(WF, 12, "floating install", "npm install (use npm ci)")], True)
assert state == "success" and desc.startswith("⚠ WARN (not blocking): 1 CI hygiene issue in CI/Dockerfiles")
def test_allow_file_loads_and_is_empty_today():
assert hy.cg.load_allow(hy.ALLOW_FILE) == []
@pytest.mark.parametrize("path, text, want", [
("Dockerfile", "COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv", "ghcr.io/astral-sh/uv:latest"), # Mail #147
("Dockerfile", "FROM python:latest", "python:latest"),
("Dockerfile", "FROM --platform=linux/amd64 node:latest AS web", "node:latest"),
(WF, " image: postgres:latest", "postgres:latest"),
(WF, " - uses: docker://ghcr.io/foo/bar:latest", "ghcr.io/foo/bar:latest"),
])
def test_latest_images_are_flagged(path, text, want):
hits = hy.scan_line(path, text)
assert ("floating image", want) in hits
@pytest.mark.parametrize("text", [
"COPY pyproject.toml uv.lock* ./", # Windy Mail #147
"COPY package.json package-lock.json* ./",
])
def test_optional_lock_globs_are_flagged(text):
assert [k for k, _ in hy.scan_line("Dockerfile", text)] == ["optional lock"]
@pytest.mark.parametrize("path, text", [
("Dockerfile", "COPY --from=ghcr.io/astral-sh/uv:0.12.5 /uv /usr/local/bin/uv"),
("Dockerfile", "FROM python:3.12-slim"),
("Dockerfile", "COPY pyproject.toml uv.lock ./"),
("Dockerfile", "COPY src/*.py ./src/"),
("Dockerfile", "RUN echo latest release notes"),
])
def test_pinned_images_and_real_locks_pass(path, text):
assert hy.scan_line(path, text) == []

View File

@@ -382,3 +382,11 @@ def test_guard_that_cannot_run_posts_nothing(fake, monkeypatch):
monkeypatch.setitem(sys.modules, "compute_guard", _Guard(None))
bridge.post_compute_guard("windy-chat", SHA, "main", True)
assert f.posted == []
def test_ci_hygiene_posts_under_its_own_context(fake, monkeypatch):
f = fake(statuses=[{"context": "windy-git/compute-guard", "state": "success", "description": "WARN 1"}])
monkeypatch.setitem(sys.modules, "ci_hygiene", _Guard([_F()]))
bridge.post_ci_hygiene("windy-chat", SHA, "main", True)
# the compute-guard status with the same description must not suppress it
assert [(p["context"], p["description"]) for p in f.posted] == [("windy-git/ci-hygiene", "WARN 1")]