From 493d4b66eb8bbb99c1e334e230190155dd365a98 Mon Sep 17 00:00:00 2001 From: David Fitzsimmons Date: Mon, 17 Aug 2026 16:55:52 -0400 Subject: [PATCH 1/3] ci: add comprehensive verification gates --- .github/dependabot.yml | 27 ++++++++ .github/workflows/ci.yml | 101 +++++++++++++++++++++++++++++ loopgain/core.py | 1 - loopgain/telemetry.py | 7 +- pyproject.toml | 16 +++++ tests/test_classifier_synthetic.py | 1 - tests/test_core.py | 1 - tests/test_funnel.py | 3 +- tests/test_stress.py | 4 +- tests/test_telemetry.py | 11 ++-- tests/test_termination_safety.py | 1 - 11 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a9cebb7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 3 + groups: + routine-python: + patterns: + - "*" + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 3 + groups: + routine-actions: + patterns: + - "*" + update-types: + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8115765 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: Tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Install package and test dependencies + run: python -m pip install --disable-pip-version-check ".[test]" + - name: Run test suite + run: python -m pytest -q + + framework-integrations: + name: Integration (${{ matrix.framework }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - framework: langgraph + extra: langgraph + test-file: tests/integration/test_langgraph_smoke.py + - framework: crewai + extra: crewai + test-file: tests/integration/test_crewai_smoke.py + - framework: autogen + extra: autogen + test-file: tests/integration/test_autogen_smoke.py + - framework: langchain + extra: langchain + test-file: tests/integration/test_langchain_smoke.py + - framework: openai-agents + extra: openai-agents + test-file: tests/integration/test_openai_agents_smoke.py + - framework: claude-agent-sdk + extra: claude-agent-sdk + test-file: tests/integration/test_claude_agent_sdk_smoke.py + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + - name: Install framework integration + run: python -m pip install --disable-pip-version-check ".[test,${{ matrix.extra }}]" + - name: Run real-framework offline smoke test + run: python -m pytest -q "${{ matrix.test-file }}" -m integration + + quality-and-package: + name: Lint, security, and package + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: pip + - name: Install quality and packaging tools + run: python -m pip install --disable-pip-version-check ".[dev]" + - name: Lint source and tests + run: python -m ruff check loopgain tests + - name: Scan application source for security issues + run: python -m bandit -q -r loopgain -c pyproject.toml -ll + - name: Build source and wheel distributions + run: python -m build + - name: Validate package metadata + run: python -m twine check dist/* + - name: Smoke-test the built wheel + run: | + smoke_venv="$(mktemp -d)/venv" + python -m venv "$smoke_venv" + "$smoke_venv/bin/python" -m pip install --disable-pip-version-check dist/*.whl + cd /tmp + "$smoke_venv/bin/python" -c "import loopgain; print(loopgain.__version__)" + "$smoke_venv/bin/loopgain" --help diff --git a/loopgain/core.py b/loopgain/core.py index 622799c..98c7462 100644 --- a/loopgain/core.py +++ b/loopgain/core.py @@ -24,7 +24,6 @@ from loopgain.classifier import ( TrajectoryThresholds, classify_trajectory, - extract_features, ) diff --git a/loopgain/telemetry.py b/loopgain/telemetry.py index 1caf94b..a3d5903 100644 --- a/loopgain/telemetry.py +++ b/loopgain/telemetry.py @@ -31,6 +31,8 @@ from typing import TYPE_CHECKING, Any, Optional from urllib.parse import urlparse +from loopgain._version import __version__ as LIBRARY_VERSION + def _safe_float(x: Any) -> Any: """Coerce inf / -inf / NaN to None so the payload stays strict JSON. @@ -128,11 +130,6 @@ def resolve_telemetry_config( return ep, tok -# Library version sourced from loopgain._version so there's exactly one -# string to bump per release. _version.py has no project imports, so this -# is safe to import at module load. -from loopgain._version import __version__ as LIBRARY_VERSION - # Cap on per-iteration trajectory length sent to telemetry. Loops longer than # this are truncated to the first PER_ITERATION_CAP entries with a # ``truncated: true`` flag in the payload. 256 is well above the typical diff --git a/pyproject.toml b/pyproject.toml index 3e2c454..8f95681 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,13 @@ loopgain = "loopgain.cli:main" [project.optional-dependencies] test = ["pytest>=7.0"] +dev = [ + "bandit[toml]>=1.7.10,<2", + "build>=1.2,<2", + "pytest>=7,<10", + "ruff>=0.11,<1", + "twine>=5,<7", +] # Framework adapter extras. Install with e.g. `pip install loopgain[langgraph]`. # Versions are floors only — any reasonably recent release should work since # the adapters duck-type the framework's public stream/callback surface. @@ -117,3 +124,12 @@ testpaths = ["tests"] markers = [ "integration: tests that import a real framework (langgraph, crewai, autogen, langchain, openai-agents, claude-agent-sdk). Skipped automatically when the framework isn't installed.", ] + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] + +[tool.bandit] +exclude_dirs = ["tests"] diff --git a/tests/test_classifier_synthetic.py b/tests/test_classifier_synthetic.py index 3eebcfd..6f5c123 100644 --- a/tests/test_classifier_synthetic.py +++ b/tests/test_classifier_synthetic.py @@ -28,7 +28,6 @@ FAST_CONVERGE, OSCILLATING, STALLING, - TrajectoryFeatures, TrajectoryThresholds, classify_trajectory, extract_features, diff --git a/tests/test_core.py b/tests/test_core.py index 70ec1e2..c95625c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,7 +7,6 @@ from __future__ import annotations -import math import pytest diff --git a/tests/test_funnel.py b/tests/test_funnel.py index d23ebaa..25b4c94 100644 --- a/tests/test_funnel.py +++ b/tests/test_funnel.py @@ -21,7 +21,8 @@ from loopgain.funnel import Funnel -FIXED_CLOCK = lambda: datetime(2026, 5, 30, 9, 37, 12, 500, tzinfo=timezone.utc) +def FIXED_CLOCK(): + return datetime(2026, 5, 30, 9, 37, 12, 500, tzinfo=timezone.utc) class _Capture: diff --git a/tests/test_stress.py b/tests/test_stress.py index 0b60ade..6b63d86 100644 --- a/tests/test_stress.py +++ b/tests/test_stress.py @@ -10,9 +10,7 @@ from __future__ import annotations -import math -import pytest from loopgain import ( LoopGain, @@ -280,7 +278,7 @@ def test_observe_rejects_string(): lg = LoopGain() # We choose to accept strings (len semantics) but it's an unexpected path. # If we ever tighten this, update the test. - state = lg.observe("abc") # len("abc") = 3 + lg.observe("abc") # len("abc") = 3 assert lg.result.error_history[0] == 3.0 diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 2697ccb..7db0436 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -9,13 +9,15 @@ import inspect import json import re -import socket +import socket as _socket +import urllib.error as _uerr from datetime import datetime, timezone from pathlib import Path import pytest -from loopgain import LoopGain, build_telemetry_payload +from loopgain import telemetry as _tele +from loopgain import LoopGain from loopgain.telemetry import ( SCHEMA_VERSION, LIBRARY_VERSION, @@ -687,11 +689,6 @@ def test_send_payload_refuses_redirects(): # ----- send_payload retry behavior (transient failures) ----- -import socket as _socket -import urllib.error as _uerr - -from loopgain import telemetry as _tele - class _OkResp: status = 202 diff --git a/tests/test_termination_safety.py b/tests/test_termination_safety.py index ba3e52f..334626d 100644 --- a/tests/test_termination_safety.py +++ b/tests/test_termination_safety.py @@ -21,7 +21,6 @@ from __future__ import annotations -import pytest from loopgain import CONVERGING, FAST_CONVERGE, LoopGain, classify_trajectory From 1c330a6c2410d031354cb9671ea9aaf406325a67 Mon Sep 17 00:00:00 2001 From: David Fitzsimmons Date: Mon, 17 Aug 2026 17:35:32 -0400 Subject: [PATCH 2/3] ci: close reproducibility gaps --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8115765..9aa7377 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,8 @@ jobs: matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} cache: pip @@ -61,23 +61,38 @@ jobs: extra: claude-agent-sdk test-file: tests/integration/test_claude_agent_sdk_smoke.py steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" cache: pip - name: Install framework integration run: python -m pip install --disable-pip-version-check ".[test,${{ matrix.extra }}]" - name: Run real-framework offline smoke test - run: python -m pytest -q "${{ matrix.test-file }}" -m integration + run: | + results_file="$(mktemp)" + python -m pytest -q "${{ matrix.test-file }}" -m integration --junitxml="$results_file" + python - "$results_file" <<'PY' + import sys + import xml.etree.ElementTree as ET + + suites = ET.parse(sys.argv[1]).getroot().findall(".//testsuite") + executed = sum(int(suite.get("tests", "0")) for suite in suites) + skipped = sum(int(suite.get("skipped", "0")) for suite in suites) + if executed == 0 or skipped: + raise SystemExit( + f"integration gate requires executed tests with zero skips; " + f"executed={executed}, skipped={skipped}" + ) + PY quality-and-package: name: Lint, security, and package runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" cache: pip From 395742132eabc19e3936597362b9043bb0d73ea1 Mon Sep 17 00:00:00 2001 From: David Fitzsimmons Date: Mon, 17 Aug 2026 18:40:16 -0400 Subject: [PATCH 3/3] ci: upgrade action runtimes --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9aa7377..c8a2a8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,8 @@ jobs: matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: pip @@ -61,8 +61,8 @@ jobs: extra: claude-agent-sdk test-file: tests/integration/test_claude_agent_sdk_smoke.py steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip @@ -91,8 +91,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" cache: pip