Repository navigation
chore(deps,ci): bump the actions-minor-patch group across 1 directory with 4 updates #1045
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies (frozen lock profile) | |
| run: bash scripts/ci_sync.sh dev-tools | |
| - name: Ruff check | |
| run: ruff check src/ tests/ scripts/ sdk/ integrations/ warehouse/ | |
| - name: Ruff format check | |
| run: ruff format --check src/ tests/ scripts/ sdk/ integrations/ warehouse/ | |
| - name: Type check | |
| run: mypy src/ --ignore-missing-imports | |
| - name: Validate machine-readable project claims | |
| run: python scripts/validate_project_claims.py | |
| - name: Build documentation with strict link and warning checks | |
| run: mkdocs build --strict | |
| # Audit P1-3: the lock chain stays honest end to end — uv.lock matches | |
| # pyproject.toml, requirements-docker.lock (what Dockerfile.api actually | |
| # installs) matches uv.lock, and a fresh hash-verified install from it is | |
| # a consistent environment per pip check. Runs independently of lint: a | |
| # dependency drift should be visible even while lint is red. | |
| lock-check: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install uv | |
| # Exact pin: a newer uv may rewrite the lock format and turn this | |
| # gate into noise. Bump deliberately, together with uv.lock. | |
| run: pip install uv==0.8.23 | |
| - name: uv.lock matches pyproject.toml | |
| run: uv lock --check | |
| - name: requirements-docker.lock matches uv.lock | |
| run: | | |
| uv export --format requirements-txt --extra cloud --extra postgres \ | |
| --no-emit-project -o requirements-docker.lock | |
| git diff --exit-code requirements-docker.lock | |
| - name: Locked install is consistent | |
| run: | | |
| python -m venv /tmp/lockenv | |
| /tmp/lockenv/bin/pip install --quiet --require-hashes -r requirements-docker.lock | |
| /tmp/lockenv/bin/pip check | |
| python-compat: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.11", "3.12", "3.13"] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Test SDK and core compatibility on the frozen project lock | |
| run: | | |
| python -m pip install uv==0.8.23 | |
| uv sync --frozen --extra cloud --extra dev | |
| uv pip install --no-deps --editable ./sdk | |
| uv run pytest tests/unit/test_sdk_client.py tests/unit/test_sdk_async_client.py tests/unit/test_event_schemas.py -q | |
| - name: Build runtime and SDK wheels | |
| run: | | |
| uv run python -m build --wheel --no-isolation | |
| uv run python -m build --wheel --no-isolation --outdir dist sdk | |
| - name: Wheel top-level namespace policy (audit F-09) | |
| # The runtime wheel must ship exactly agentflow_runtime + the one-file | |
| # deprecated src shim; any other top-level entry is an undeclared | |
| # package leaking into the distribution. | |
| run: uv run python scripts/wheel_smoke.py 'dist/agentflow_runtime-*.whl' | |
| - name: Install production dependencies and wheel in a clean environment | |
| run: | | |
| python -m venv /tmp/agentflow-clean | |
| /tmp/agentflow-clean/bin/pip install --quiet --require-hashes -r requirements-docker.lock | |
| /tmp/agentflow-clean/bin/pip install --quiet --no-deps dist/agentflow_runtime-*.whl | |
| /tmp/agentflow-clean/bin/pip install --quiet --no-deps dist/agentflow_client-*.whl | |
| /tmp/agentflow-clean/bin/pip check | |
| - name: Smoke quickstart, materializer and deprecated-shim imports | |
| # Run outside the checkout so the runtime must come from the installed | |
| # wheel rather than the repository working tree on sys.path. The src.* | |
| # surface must keep working through the wheel's deprecated shim for | |
| # one deprecation window (removed in the next major release). | |
| working-directory: /tmp | |
| run: | | |
| /tmp/agentflow-clean/bin/python - <<'PY' | |
| import warnings | |
| from agentflow.client import AgentFlowClient | |
| from agentflow_runtime.processing.lake_consumer import ValidatedLakeConsumer | |
| from agentflow_runtime.processing.local_pipeline import main | |
| from agentflow_runtime.serving.api.main import app | |
| assert AgentFlowClient and app and main and ValidatedLakeConsumer | |
| with warnings.catch_warnings(record=True) as caught: | |
| warnings.simplefilter("always") | |
| import src.serving.api.main as shim_main | |
| assert any( | |
| issubclass(w.category, DeprecationWarning) for w in caught | |
| ), "src shim import did not raise DeprecationWarning" | |
| assert shim_main.app is app, "src shim aliased a different module object" | |
| PY | |
| local-duckdb-smoke: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| SERVING_BACKEND: clickhouse | |
| DUCKDB_PATH: ci-must-ignore.duckdb | |
| AGENTFLOW_LOCAL_ONLY: "false" | |
| AGENTFLOW_SERVING_BRIDGE_ENABLED: "true" | |
| AGENTFLOW_CONTROLPLANE_PG_DSN: postgresql://ci-must-not-connect.invalid/agentflow | |
| AGENTFLOW_NODE_CENTER_URL: https://ci-must-not-connect.invalid | |
| AGENTFLOW_NODE_TOKEN: ci-must-ignore | |
| AGENTFLOW_ICEBERG_CONFIG: s3://ci-must-not-connect.invalid/catalog | |
| KAFKA_BOOTSTRAP_SERVERS: ci-must-not-connect.invalid:9092 | |
| FLINK_JOBMANAGER_URL: http://ci-must-not-connect.invalid:8081 | |
| REDIS_URL: redis://ci-must-not-connect.invalid:6379/0 | |
| OTEL_EXPORTER_OTLP_ENDPOINT: https://ci-must-not-connect.invalid:4317 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install core runtime dependencies (frozen lock profile) | |
| run: bash scripts/ci_sync.sh runtime | |
| - name: Prepare the local-only DuckDB database | |
| run: | | |
| test ! -e .env | |
| python scripts/demo_local.py --db-path "${RUNNER_TEMP}/local-duckdb-smoke.duckdb" --burst 20 --prepare-only | |
| - name: Smoke health, entity, and NL query | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| log_path="${RUNNER_TEMP}/local-duckdb-smoke.log" | |
| port="$(python -c 'import socket; sock = socket.socket(); sock.bind(("127.0.0.1", 0)); print(sock.getsockname()[1]); sock.close()')" | |
| server_pid="" | |
| cleanup() { | |
| local status="$?" | |
| trap - EXIT | |
| if [[ -n "${server_pid}" ]]; then | |
| kill -TERM -- "-${server_pid}" 2>/dev/null || true | |
| for _ in $(seq 1 50); do | |
| if ! kill -0 -- "-${server_pid}" 2>/dev/null; then | |
| break | |
| fi | |
| sleep 0.1 | |
| done | |
| if kill -0 -- "-${server_pid}" 2>/dev/null; then | |
| kill -KILL -- "-${server_pid}" 2>/dev/null || true | |
| fi | |
| wait "${server_pid}" 2>/dev/null || true | |
| fi | |
| if [[ "${status}" -ne 0 ]]; then | |
| cat "${log_path}" | |
| fi | |
| exit "${status}" | |
| } | |
| trap cleanup EXIT | |
| export AGENTFLOW_CI_DB_PATH="${RUNNER_TEMP}/local-duckdb-smoke.duckdb" | |
| export AGENTFLOW_CI_PORT="${port}" | |
| PYTHONUNBUFFERED=1 setsid python -c \ | |
| 'import os; from pathlib import Path; from scripts.demo_local import build_environment, serve_demo; serve_demo(build_environment(Path(os.environ["AGENTFLOW_CI_DB_PATH"])), host="127.0.0.1", port=int(os.environ["AGENTFLOW_CI_PORT"]))' \ | |
| >"${log_path}" 2>&1 & | |
| server_pid="$!" | |
| base_url="http://127.0.0.1:${port}" | |
| curl_options=( | |
| --fail | |
| --silent | |
| --show-error | |
| --retry 60 | |
| --retry-all-errors | |
| --retry-connrefused | |
| --retry-delay 1 | |
| --max-time 5 | |
| ) | |
| curl "${curl_options[@]}" "${base_url}/v1/health" > /dev/null | |
| curl "${curl_options[@]}" \ | |
| "${base_url}/v1/entity/order/ORD-20260404-1001" > /dev/null | |
| curl "${curl_options[@]}" \ | |
| -H "Content-Type: application/json" \ | |
| --data '{"question":"Show me top 3 products"}' \ | |
| "${base_url}/v1/query" > /dev/null | |
| schema-check: | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 2 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies (frozen lock profile) | |
| run: bash scripts/ci_sync.sh dev-tools | |
| - name: Check schema evolution | |
| run: python scripts/check_schema_evolution.py | |
| test-unit: | |
| runs-on: ubuntu-latest | |
| needs: [lint, python-compat] | |
| timeout-minutes: 25 | |
| # Audit F-06: this job used to hold id-token: write solely for a Codecov | |
| # OIDC upload that failed with "Repository not found" (the repo was never | |
| # enabled in the external service). The broken upload and the badge are | |
| # removed, so the job that executes repository-owned test code no longer | |
| # carries an OIDC capability it cannot use. Coverage floors below remain | |
| # the blocking gates. Reintroduce upload in a separate job consuming a | |
| # .artifacts/coverage/coverage.xml artifact if external reporting is ever | |
| # enabled. | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies | |
| id: install | |
| run: | | |
| bash scripts/ci_sync.sh test-integrations | |
| - name: Verify SDK/integrations joint installability | |
| run: python scripts/check_cross_package_install.py | |
| - name: Prepare pytest temp directory | |
| run: mkdir -p .tmp | |
| - name: Run unit and property tests with coverage | |
| run: | | |
| # Full src/sdk baseline floor; changed-code coverage is enforced locally below. | |
| # --cov-branch turns the floor into a combined line+branch metric — local baseline | |
| # 2026-05-25 is 62% (7716 lines / 2010 branches measured on HEAD `22b1be9`), so the | |
| # 60% gate stays passing with a 2pp cushion. Raise the gate once that cushion grows. | |
| mkdir -p .artifacts/coverage | |
| python -m pytest tests/unit/ tests/property/ -v --tb=short --cov=src/agentflow_runtime --cov=sdk --cov-branch --cov-report=xml:.artifacts/coverage/coverage.xml --cov-report=term-missing --cov-fail-under=60 | |
| - name: Enforce changed-code coverage | |
| run: diff-cover .artifacts/coverage/coverage.xml --compare-branch=origin/main --fail-under=80 | |
| - name: Run quality validators coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| python -m pytest tests/unit/test_validators.py -v --tb=short --cov=agentflow_runtime.quality.validators --cov-report=term-missing --cov-fail-under=90 | |
| - name: Run freshness monitor coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| python -m pytest tests/unit/test_freshness_monitor.py -v --tb=short --cov=agentflow_runtime.quality.monitors.freshness_monitor --cov-report=term-missing --cov-fail-under=90 | |
| - name: Run event producer coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| python -m pytest tests/unit/test_event_producer.py -v --tb=short --cov=agentflow_runtime.ingestion.producers.event_producer --cov-report=term-missing --cov-fail-under=90 | |
| - name: Run SQL guard coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| # Security-critical NL->SQL guard: validate_nl_sql (SELECT-only, no DML, | |
| # tenant table allow-list, recursive-CTE shadow reject). test_sql_guard | |
| # plus test_sql_guard_mutation give full module coverage (100% local); | |
| # the 90% gate keeps a 10pp regression cushion. | |
| python -m pytest tests/unit/test_sql_guard.py tests/unit/test_sql_guard_mutation.py -v --tb=short --cov=agentflow_runtime.serving.semantic_layer.sql_guard --cov-report=term-missing --cov-fail-under=90 | |
| - name: Run rate limiter coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| # Security-critical sliding-window rate limiter (Redis + in-memory | |
| # fail-closed fallback); local module coverage is 98% (only the optional | |
| # redis auto-construct line is env-gated), so the 90% gate keeps a | |
| # cushion on a mutmut target. | |
| # | |
| # Uses `coverage run` + `coverage report --include` (auth/outbox pattern). | |
| # Scope is pure RateLimiter tests only (`-k "not auth_middleware"`): the | |
| # three middleware+TestClient cases still run in the main unit suite, | |
| # but under coverage they tear down redis.asyncio + Starlette and have | |
| # SIGSEGV'd (exit 139) on ubuntu-latest after a green 12/12 report | |
| # (PR #174). Pure module coverage stays ≥90% without those three. | |
| # Also disable the pytest-cov plugin so it cannot double-instrument. | |
| python -m coverage run -m pytest tests/unit/test_rate_limiter.py \ | |
| -k "not auth_middleware" -v --tb=short -p no:schemathesis -p no:cov | |
| python -m coverage report --include="*/serving/api/rate_limiter.py" --show-missing --fail-under=90 | |
| - name: Run auth manager coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| # Security-critical auth manager (key match/verify, tenant isolation, | |
| # rate-limit/failed-auth windows, rotation grace) and a mutmut target; | |
| # the gate runs its dedicated unit files. Module coverage is 97% so the | |
| # 90% gate keeps a cushion; the remaining gap is the platform-divergent | |
| # SIGHUP handler, the two read-only-store branches load() reaches only | |
| # when the store flips between probe and write, and the thin | |
| # KeyRotator/usage-table delegations the integration/e2e auth suites | |
| # cover. | |
| # | |
| # This list IS the gate: a dedicated unit file that is not named here | |
| # buys the module nothing. test_key_store_readonly.py arrived with the | |
| # read-only Secret mount work (F-02 B, 2026-08-23) and was never added, | |
| # which by itself took the module from 94% to 82% -- unnoticed, because | |
| # an earlier step in this job was failing and this one never ran. | |
| # | |
| # NOTE: unlike the other per-module gates this uses `coverage run` + | |
| # `coverage report --include`, NOT `pytest --cov=<module>`. The auth | |
| # manager pulls in duckdb (usage table), and pytest-cov's source | |
| # instrumentation of a duckdb-importing module trips duckdb's lazy | |
| # `_duckdb._sqltypes` import at COLLECTION time, both locally and on CI | |
| # runners. `coverage run` imports duckdb normally and avoids the break. | |
| python -m coverage run -m pytest \ | |
| tests/unit/test_auth.py \ | |
| tests/unit/test_auth_manager_pure_logic.py \ | |
| tests/unit/test_auth_manager_memory_bounds.py \ | |
| tests/unit/test_auth_hashed_key_guidance.py \ | |
| tests/unit/test_auth_argon2_lookup.py \ | |
| tests/unit/test_auth_key_store_probe.py \ | |
| tests/unit/test_auth_manager_key_resolution.py \ | |
| tests/unit/test_key_store_readonly.py \ | |
| -p no:schemathesis | |
| python -m coverage report --include="*/serving/api/auth/manager.py" --show-missing --fail-under=90 | |
| - name: Run key rotation coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| # Security-critical key-rotation lifecycle (create/rotate/revoke, | |
| # grace-period scheduling, rotation status) and a mutmut target. Like | |
| # the auth manager gate it pulls in duckdb, so it uses coverage run + | |
| # coverage report --include (not pytest --cov) to avoid the | |
| # duckdb _duckdb._sqltypes collection break. | |
| # | |
| # Module coverage is 100%. It was 89.6% on test_key_rotation.py alone | |
| # -- passing only because coverage rounds to 90 before comparing, so | |
| # one more defensive line would have turned this gate red without | |
| # anyone changing its behaviour. test_key_rotation.py pins the happy | |
| # lifecycle; the revoke_and_failures file adds what it never reached: | |
| # revoke_key_by_id (the only revoke path the admin router calls since | |
| # F-02 A), the read-only vs merely-broken key-store split, and the | |
| # grace-period cleanup failure that runs on a timer thread with no | |
| # caller left to see it. | |
| python -m coverage run -m pytest \ | |
| tests/unit/test_key_rotation.py \ | |
| tests/unit/test_key_rotation_revoke_and_failures.py \ | |
| -p no:schemathesis | |
| python -m coverage report --include="*/serving/api/auth/key_rotation.py" --show-missing --fail-under=90 | |
| - name: Run outbox coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| # Security/reliability-critical at-least-once outbox dispatch loop | |
| # (delivery, retry/backoff, poison-to-failed, mark-sent transactions) | |
| # and a mutmut target. Imports duckdb, so it uses coverage run + | |
| # coverage report --include like the auth gates. Module coverage is | |
| # 92% across the two dedicated unit files. | |
| python -m coverage run -m pytest tests/unit/test_outbox_processor.py tests/unit/test_outbox_connection_guard.py -p no:schemathesis | |
| python -m coverage report --include="*/processing/outbox.py" --show-missing --fail-under=90 | |
| - name: Run query package coverage gate | |
| if: ${{ !cancelled() && steps.install.outcome == 'success' }} | |
| run: | | |
| # The NL->SQL orchestration surface (engine, entity/metric/NL query | |
| # mixins, SQL builder) and a mutmut target set; the old single-file | |
| # query_engine.py is a re-export shim, so the gate spans the whole | |
| # query package. The engine imports duckdb, so it uses coverage run + | |
| # coverage report --include like the auth/outbox gates. Package | |
| # coverage is 97% across the six dedicated unit files; the gap is | |
| # the OTel span-recording branches the integration suites cover. | |
| # test_pipeline_events_scan.py covers QueryEngine.fetch_pipeline_events | |
| # (the backend event scan the webhook dispatcher and SSE delegate to). | |
| python -m coverage run -m pytest tests/unit/test_query_engine.py tests/unit/test_query_engine_injection.py tests/unit/test_query_engine_mixin_contracts.py tests/unit/test_paginated_nl_query.py tests/unit/test_query_package_logic.py tests/unit/test_pipeline_events_scan.py -p no:schemathesis | |
| python -m coverage report --include="*/serving/semantic_layer/query/*" --show-missing --fail-under=90 | |
| test-integration: | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 25 | |
| services: | |
| kafka: | |
| image: confluentinc/cp-kafka:7.7.0 | |
| ports: | |
| - 9092:9092 | |
| env: | |
| KAFKA_NODE_ID: 1 | |
| KAFKA_PROCESS_ROLES: broker,controller | |
| KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:29093 | |
| KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:29093 | |
| KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 | |
| KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER | |
| KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT | |
| KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 | |
| CLUSTER_ID: "CITestCluster01" | |
| clickhouse: | |
| # Live coverage for the ClickHouse serving backend's sqlglot | |
| # transpile path (H-C2); test_clickhouse_backend_live.py skips | |
| # itself when CLICKHOUSE_LIVE_HOST is absent. | |
| image: clickhouse/clickhouse-server:25.3 | |
| ports: | |
| - 8123:8123 | |
| env: | |
| CLICKHOUSE_USER: agentflow | |
| CLICKHOUSE_PASSWORD: agentflow | |
| CLICKHOUSE_DB: agentflow | |
| postgres: | |
| # Live coverage for PostgresControlPlaneStore (ADR 0010 slice 5); | |
| # test_control_plane_postgres_live.py skips itself when | |
| # AGENTFLOW_TEST_PG_DSN is absent. | |
| image: postgres:17 | |
| ports: | |
| - 5432:5432 | |
| env: | |
| POSTGRES_USER: agentflow | |
| POSTGRES_PASSWORD: agentflow | |
| POSTGRES_DB: agentflow | |
| options: >- | |
| --health-cmd "pg_isready -U agentflow" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies | |
| run: | | |
| bash scripts/ci_sync.sh test-sdk | |
| - name: Prepare pytest temp directory | |
| run: mkdir -p .tmp | |
| - name: Wait for Kafka | |
| run: | | |
| timeout 30 bash -c 'until nc -z localhost 9092; do sleep 1; done' | |
| - name: Wait for ClickHouse | |
| run: | | |
| timeout 60 bash -c 'until curl -sf http://localhost:8123/ping; do sleep 1; done' | |
| - name: Run integration tests | |
| env: | |
| CLICKHOUSE_LIVE_HOST: localhost | |
| CLICKHOUSE_LIVE_PORT: "8123" | |
| CLICKHOUSE_LIVE_USER: agentflow | |
| CLICKHOUSE_LIVE_PASSWORD: agentflow | |
| CLICKHOUSE_LIVE_DATABASE: agentflow | |
| AGENTFLOW_TEST_PG_DSN: postgresql://agentflow:agentflow@localhost:5432/agentflow | |
| run: pytest tests/integration/ -v --tb=short | |
| - name: Control-plane critical-set coverage gate (audit F-12) | |
| # The repository floor is one number over hundreds of files, so a | |
| # surface can sit at 21% while the aggregate reads 78%. These adapters | |
| # carry the lease / SKIP LOCKED / one-transaction semantics whose | |
| # regressions are silent in production, so they get their own floors | |
| # (scripts/check_control_plane_coverage.py holds the table). | |
| # | |
| # Unit and live coverage go into ONE data file on purpose: most | |
| # branches in these modules only execute against a real server, so a | |
| # unit-only number measures the absence of a database rather than the | |
| # absence of tests. -p no:cov keeps pytest-cov from double-instrumenting | |
| # the `coverage run` (the auth/outbox gate pattern). | |
| env: | |
| AGENTFLOW_TEST_PG_DSN: postgresql://agentflow:agentflow@localhost:5432/agentflow | |
| run: | | |
| coverage erase | |
| coverage run --append -m pytest -q -p no:cov tests/unit/test_control_plane_store.py tests/unit/test_control_plane_capabilities.py tests/unit/test_postgres_enqueue_lease_contract.py tests/unit/test_query_analytics_retention.py tests/unit/test_analytics_middleware.py tests/unit/test_embedded_usage_analytics.py tests/unit/test_audit_publisher.py tests/unit/test_usage_db_connection_reuse.py tests/unit/test_usage_write_off_request_path.py tests/unit/test_node_ingest.py | |
| coverage run --append -m pytest -q -p no:cov tests/integration/test_control_plane_postgres_live.py tests/integration/test_exceptions_inbox.py tests/integration/test_stuck_orders.py tests/integration/test_tenant_isolation.py tests/integration/test_node_topology.py | |
| mkdir -p .artifacts/coverage | |
| coverage xml -o .artifacts/coverage/coverage-control-plane.xml --include="*/serving/control_plane/*,*/routers/ops.py,*/semantic_layer/reconciliation.py,*/serving/node/ingest.py" | |
| python scripts/check_control_plane_coverage.py | |
| - name: Publish control-plane coverage | |
| # Separate from the repository-wide report on purpose (audit F-12 asks | |
| # for integration coverage to be published without folding it into the | |
| # general floor, which would raise the aggregate without covering | |
| # anything new). | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-control-plane | |
| path: .artifacts/coverage/coverage-control-plane.xml | |
| if-no-files-found: warn | |
| helm-schema-live: | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 8 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies (frozen lock profile) | |
| run: bash scripts/ci_sync.sh dev-tools | |
| - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 | |
| with: | |
| version: v3.16.3 | |
| - name: Report Helm version | |
| run: helm version --short | |
| - uses: helm/kind-action@06c1ae10762d3b9c1644e7fe69596ae519e015a2 # v1.15.0 | |
| with: | |
| install_only: true | |
| - name: Prepare pytest temp directory | |
| run: mkdir -p .tmp | |
| - name: Run Helm schema live validation | |
| run: python -m pytest tests/integration/test_helm_values_live_validation.py -v -m integration --tb=short | |
| perf-check: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - test-unit | |
| - test-integration | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies | |
| run: bash scripts/ci_sync.sh perf | |
| - name: Run benchmark | |
| # DuckDB-profile benchmark: the pipeline runs on the runner host with no | |
| # ClickHouse service, and the perf history compares against DuckDB-era | |
| # numbers. The shipped ClickHouse path is exercised by the E2E lane. | |
| env: | |
| SERVING_BACKEND: duckdb | |
| run: python scripts/run_benchmark.py | |
| - name: Convert benchmark report to JSON | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import re | |
| from pathlib import Path | |
| report_path = Path(".artifacts/benchmark/benchmark.md") | |
| report = report_path.read_text(encoding="utf-8") | |
| lines = [line.strip() for line in report.splitlines() if line.startswith("|")] | |
| if len(lines) < 3: | |
| raise SystemExit( | |
| "Benchmark results table not found in " | |
| ".artifacts/benchmark/benchmark.md" | |
| ) | |
| generated_at_match = re.search(r"Generated: `([^`]+)`", report) | |
| endpoints = {} | |
| for line in lines[2:]: | |
| columns = [column.strip() for column in line.strip("|").split("|")] | |
| if len(columns) != 8: | |
| continue | |
| endpoint, requests, failures, failure_rate, rps, p50, p95, p99 = columns | |
| endpoints[endpoint] = { | |
| "request_count": int(requests), | |
| "failure_count": int(failures), | |
| "failure_rate_percent": float(failure_rate.removesuffix("%")), | |
| "requests_per_second": float(rps), | |
| "p50_latency_ms": float(p50.removesuffix(" ms")), | |
| "p95_latency_ms": float(p95.removesuffix(" ms")), | |
| "p99_latency_ms": float(p99.removesuffix(" ms")), | |
| } | |
| aggregate = endpoints.pop("ALL", None) | |
| if aggregate is None: | |
| raise SystemExit("Missing ALL aggregate row in benchmark report.") | |
| current_report = { | |
| "generated_at": generated_at_match.group(1) if generated_at_match else None, | |
| "source": str(report_path), | |
| "aggregate": aggregate, | |
| "endpoints": endpoints, | |
| } | |
| Path("/tmp/current.json").write_text( | |
| json.dumps(current_report, indent=2) + "\n", | |
| encoding="utf-8", | |
| ) | |
| PY | |
| - name: Compare to baseline | |
| run: python scripts/check_performance.py docs/benchmark-baseline.json /tmp/current.json | |
| terraform-validate: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 | |
| with: | |
| terraform_version: "1.15.4" | |
| - name: Terraform fmt check | |
| run: terraform fmt -check -recursive infrastructure/terraform/ | |
| - name: Terraform init | |
| run: | | |
| cd infrastructure/terraform | |
| terraform init -backend=false | |
| - name: Terraform validate | |
| run: | | |
| cd infrastructure/terraform | |
| terraform validate | |
| # h1: hashes in the lock file carry no platform label. This step is the | |
| # guard that linux_amd64 (ubuntu-latest) is actually covered; it uses the | |
| # same three-platform command the module README documents (re-run | |
| # 2026-09-05: no lock rewrite). Three provider zips (~0.7 GB) plus | |
| # terraform init is why this job's timeout-minutes is 20, not 10. | |
| # Observed green on ubuntu-latest 2026-09-08 (run 34213482386): the step | |
| # regenerates all three platforms and the diff comes back empty, so the | |
| # tracked lock genuinely covers the runner it is used on. | |
| - name: Provider lock covers linux_amd64 | |
| run: | | |
| cd infrastructure/terraform | |
| terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64 | |
| git ls-files --error-unmatch .terraform.lock.hcl | |
| git diff --exit-code .terraform.lock.hcl | |
| sdk-ts: | |
| # Audit P1-5: typecheck/tests/build for the TypeScript SDK previously ran | |
| # only in publish-npm.yml (tag pushes) and mutation.yml; every PR only got | |
| # `npm audit` (security.yml). This job gives PRs the same build-shaped | |
| # gate the publish workflow already trusts, so a broken SDK cannot merge. | |
| # `sdk-ts` is one of the 15 required status checks on `main` (confirmed | |
| # 2026-09-08 via gh api repos/{owner}/{repo}/branches/main/protection), so | |
| # renaming this job silently removes a gate: the context stops reporting, | |
| # and a required check that never reports blocks merges instead of failing | |
| # them. Rename here and in branch protection together, or not at all. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| defaults: | |
| run: | |
| working-directory: sdk-ts | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "22" | |
| - name: Install dependencies from lockfile | |
| run: npm ci | |
| - name: Typecheck | |
| run: npm run typecheck | |
| - name: Test | |
| run: npm test | |
| - name: Build | |
| run: npm run build | |
| - name: Verify package is publishable | |
| run: npm pack --dry-run | |
| # Audit F-11: CI must exercise every Node version the package claims, and the | |
| # claimed floor has to be one the toolchain can actually run. That has bitten | |
| # twice now. First: Vitest 4 imports util.styleText, absent on Node 18, so the | |
| # ">=18" floor was untestable and rose to ">=20". Now: Node 20 reached | |
| # end-of-life on 2026-04-30 and Vitest 5 declares engines | |
| # "^22.12.0 || ^24.0.0 || >=26.0.0", which the Node 20 this lane pinned does | |
| # not satisfy -- npm downgraded that to a warning and PR #252 went green | |
| # anyway. The floor is ">=22": the required "sdk-ts" job covers 22, this lane | |
| # covers the next LTS (24), sdk-ts/.npmrc turns the warning back into a | |
| # failure, and tests/unit/test_sdk_ts_node_floor.py keeps the three in step. | |
| # A matrix inside the required job would rename its check context and break | |
| # branch protection. | |
| sdk-ts-compat: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node-version: ["24"] | |
| defaults: | |
| run: | |
| working-directory: sdk-ts | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| - name: Install dependencies from lockfile | |
| run: npm ci | |
| - name: Typecheck | |
| run: npm run typecheck | |
| - name: Test | |
| run: npm test | |
| - name: Build | |
| run: npm run build |