Skip to content

chore(deps,ci): bump the actions-minor-patch group across 1 directory with 4 updates #1045

chore(deps,ci): bump the actions-minor-patch group across 1 directory with 4 updates

chore(deps,ci): bump the actions-minor-patch group across 1 directory with 4 updates #1045

Workflow file for this run

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