name: CI on: push: branches: [main] paths: - "surfaces/**" - "config/**" - "core/**" - "platform/deployment/**" - "integrations/**" - "platform/**" - "tools/**" - "tests/**" - "pyproject.toml" - "uv.lock" - "pytest.ini" - "ruff.toml" - "mypy.ini" - "Makefile" - ".importlinter" - ".importlinter.strict" - ".github/ci/**" - ".github/workflows/ci.yml" pull_request: branches: [main] paths: - "surfaces/**" - "config/**" - "core/**" - "platform/deployment/**" - "integrations/**" - "platform/**" - "tools/**" - "tests/**" - "pyproject.toml" - "uv.lock" - "pytest.ini" - "ruff.toml" - "mypy.ini" - "Makefile" - ".importlinter" - ".importlinter.strict" - ".github/ci/**" - ".github/workflows/ci.yml" permissions: contents: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" PYTHON_SOURCE_PATHS: "config core integrations platform surfaces tools" concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: quality: name: quality (${{ matrix.os }}) strategy: fail-fast: false matrix: os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 15 continue-on-error: ${{ matrix.os == 'windows-latest' }} steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v8.1.0 with: enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: uv sync --frozen --extra dev - name: Lint run: uv run python -m ruff check $PYTHON_SOURCE_PATHS tests/ - name: Format check run: uv run python -m ruff format --check $PYTHON_SOURCE_PATHS tests/ - name: Typecheck run: uv run python -m mypy $PYTHON_SOURCE_PATHS - name: Import graph run: uv run python .github/ci/check_imports.py --strict # Full layered contracts (.importlinter.strict), not the minimal default # .importlinter config used by ``make check-imports``. - name: Import boundaries run: uv run lint-imports --config .importlinter.strict test: name: test (${{ matrix.shard }}) strategy: fail-fast: false max-parallel: 5 matrix: include: - os: ubuntu-latest shard: tools-runtime # ``tests/core/agent`` is excluded here; it runs in cli-runtime, # which pins openai for its live action-planning oracles. extra_pytest_args: >- --ignore=tests/core/agent pytest_paths: >- tests/tools tests/core tests/platform tests/utils tests/masking tests/watch_dog - os: ubuntu-latest shard: cli-runtime # Live shell action-agent contracts are validated on openai, # matching interactive-shell-live.yml. The # default anthropic tool-call model (haiku) cannot reliably perform # compound action planning, so pin this shard to openai. # ``tests/core/agent`` runs here because it contains the live # action-planning oracles (``test_live_action_planning`` and # ``test_live_turn_execution_oracle``) that need the openai pin — # plus the static ``test_import_boundaries`` guard the surfaces # restructure (#3299) added. llm_provider: openai pytest_paths: >- tests/cli tests/interactive_shell tests/agent tests/core/agent tests/fleet_monitoring tests/analytics tests/tools/investigation tests/delivery tests/sandbox tests/hermes - os: ubuntu-latest shard: integrations-and-misc pytest_paths: >- tests/integrations tests/deployment tests/benchmarks tests/chaos_engineering tests/shared tests/config tests/github_ci tests/packaging tests/scheduler gateway/tests - os: ubuntu-latest shard: e2e-general pytest_paths: >- tests/e2e extra_pytest_args: >- --ignore=tests/e2e/kubernetes --ignore=tests/e2e/openclaw --ignore=tests/e2e/upstream_lambda --ignore=tests/e2e/upstream_prefect_ecs_fargate --ignore=tests/e2e/upstream_apache_flink_ecs - os: ubuntu-latest shard: e2e-provider-and-openclaw pytest_paths: >- tests/e2e/openclaw tests/e2e/upstream_lambda tests/e2e/upstream_prefect_ecs_fargate tests/e2e/upstream_apache_flink_ecs tests/e2e/kubernetes runs-on: ${{ matrix.os }} timeout-minutes: 30 env: # Per-shard LLM provider for live_llm contracts; defaults to anthropic. LLM_PROVIDER: ${{ matrix.llm_provider || 'anthropic' }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} JWT_TOKEN: ${{ secrets.JWT_TOKEN }} GRAFANA_READ_TOKEN: ${{ secrets.GRAFANA_READ_TOKEN }} GCLOUD_HOSTED_METRICS_ID: ${{ secrets.GCLOUD_HOSTED_METRICS_ID }} GCLOUD_HOSTED_METRICS_URL: ${{ secrets.GCLOUD_HOSTED_METRICS_URL }} GCLOUD_HOSTED_LOGS_ID: ${{ secrets.GCLOUD_HOSTED_LOGS_ID }} GCLOUD_HOSTED_LOGS_URL: ${{ secrets.GCLOUD_HOSTED_LOGS_URL }} GCLOUD_RW_API_KEY: ${{ secrets.GCLOUD_RW_API_KEY }} GCLOUD_OTLP_ENDPOINT: ${{ secrets.GCLOUD_OTLP_ENDPOINT }} GCLOUD_OTLP_AUTH_HEADER: ${{ secrets.GCLOUD_OTLP_AUTH_HEADER }} PYTHONUTF8: "1" steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v8.1.0 with: enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: uv sync --frozen --extra dev - name: Validate pytest marker env: # Fork PRs do not receive repository secrets; skip live_llm contracts # (see interactive-shell-live.yml). Index a JSON string array so the # value is always a marker string — never a bare boolean from # ``a && 'x' || 'y'`` (boolean false → ``-m false`` → xdist # ``N workers [0 items]``, which can still exit 0). PYTEST_MARKER_EXPR: ${{ fromJSON('["not synthetic", "not (synthetic or live_llm)"]')[github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true] }} run: | echo "PYTEST_MARKER_EXPR=${PYTEST_MARKER_EXPR}" case "${PYTEST_MARKER_EXPR}" in "not synthetic"|"not (synthetic or live_llm)") ;; *) echo "Refusing unexpected PYTEST_MARKER_EXPR: '${PYTEST_MARKER_EXPR}'" >&2 exit 1 ;; esac - name: Run tests env: COVERAGE_FILE: .coverage.${{ matrix.shard }} PYTEST_MARKER_EXPR: ${{ fromJSON('["not synthetic", "not (synthetic or live_llm)"]')[github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true] }} # Keep ``run: >`` (folded) so ``matrix.pytest_paths`` stays one argv list. # A ``run: |`` + ``\`` rewrite can drop/split those paths and yield # ``N workers [0 items]`` even when the marker is correct. run: > uv run python -m pytest -n auto -v ${{ matrix.pytest_paths }} --cov=config --cov=core --cov=platform.deployment.aws --cov=platform.deployment --cov=integrations --cov=platform --cov=surfaces --cov=tools --cov-report= --ignore=tests/e2e/kubernetes_local_alert_simulation --ignore=tests/synthetic ${{ matrix.extra_pytest_args }} -m "${PYTEST_MARKER_EXPR}" - name: Upload shard coverage data if: >- always() && github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: coverage-data-${{ matrix.shard }} path: .coverage.${{ matrix.shard }}* if-no-files-found: error include-hidden-files: true retention-days: 7 coverage-report: name: coverage-report runs-on: ubuntu-latest needs: [test] if: >- always() && github.event_name == 'push' && github.ref == 'refs/heads/main' timeout-minutes: 15 steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v8.1.0 with: enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: uv sync --frozen --extra dev - name: Download shard coverage artifacts uses: actions/download-artifact@v4 with: pattern: coverage-data-* path: ./ merge-multiple: true - name: Combine coverage and build report run: | uv run python -m coverage combine . uv run python -m coverage html - name: Upload combined coverage uses: actions/upload-artifact@v4 with: name: coverage-report-ubuntu-latest path: | htmlcov/ .coverage retention-days: 7 test-kubernetes: runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 15 if: >- github.repository == 'Tracer-Cloud/opensre' && (github.event_name == 'push' || ( github.event_name == 'pull_request' && ( contains(github.event.pull_request.title, 'k8s') || contains(github.event.pull_request.title, 'kubernetes') ) )) steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v8.1.0 with: enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: uv sync --frozen --extra dev - name: Run Kubernetes tests env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: uv run python -m tests.e2e.kubernetes.test_local should-run-thorough: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'Tracer-Cloud/opensre' outputs: thorough: ${{ steps.filter.outputs.thorough }} steps: - uses: actions/checkout@v5 - uses: dorny/paths-filter@v4 id: filter with: filters: | thorough: - ".github/workflows/ci.yml" - "pyproject.toml" - "uv.lock" - "platform/deployment/**" - "integrations/**" - "tools/investigation/**" - "tools/**" - "tests/e2e/cloudwatch_demo/**" - "tests/e2e/upstream_lambda/**" - "tests/e2e/upstream_prefect_ecs_fargate/**" - "tests/e2e/upstream_apache_flink_ecs/**" test-thorough: runs-on: ubuntu-latest needs: [test, should-run-thorough] if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'Tracer-Cloud/opensre' && needs.should-run-thorough.outputs.thorough == 'true' timeout-minutes: 30 strategy: fail-fast: false matrix: include: - name: cloudwatch-demo command: python3 -m tests.e2e.cloudwatch_demo.test_aws - name: upstream-lambda command: python3 -m tests.e2e.upstream_lambda.test_agent_e2e - name: prefect-ecs-fargate command: python3 -m tests.e2e.upstream_prefect_ecs_fargate.test_agent_e2e - name: flink-ecs command: python3 -m tests.e2e.upstream_apache_flink_ecs.test_agent_e2e name: test-thorough (${{ matrix.name }}) env: # Thorough e2e suites make live LLM calls; run them on openai because the # default anthropic account is credit-exhausted. Requires a valid, funded # OPENAI_API_KEY secret. LLM_PROVIDER: openai OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} JWT_TOKEN: ${{ secrets.JWT_TOKEN }} TRACER_API_URL: ${{ secrets.TRACER_API_URL }} TRACER_INGEST_TOKEN: ${{ secrets.TRACER_INGEST_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1 GRAFANA_READ_TOKEN: ${{ secrets.GRAFANA_READ_TOKEN }} GCLOUD_HOSTED_METRICS_ID: ${{ secrets.GCLOUD_HOSTED_METRICS_ID }} GCLOUD_HOSTED_METRICS_URL: ${{ secrets.GCLOUD_HOSTED_METRICS_URL }} GCLOUD_HOSTED_LOGS_ID: ${{ secrets.GCLOUD_HOSTED_LOGS_ID }} GCLOUD_HOSTED_LOGS_URL: ${{ secrets.GCLOUD_HOSTED_LOGS_URL }} GCLOUD_RW_API_KEY: ${{ secrets.GCLOUD_RW_API_KEY }} CLOUDWATCH_VERIFY_LOGS: ${{ matrix.name == 'cloudwatch-demo' && '0' || '1' }} steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v8.1.0 with: enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: uv sync --frozen --extra dev - name: Install tracer CLI run: | curl -sSL https://install.tracer.cloud | sh -s user_36fbN6K6FwEgJFHsQv1pUByo8K6 - name: Initialize tracer (optional) run: | sudo tracer init --token ${{ secrets.JWT_TOKEN }} continue-on-error: true timeout-minutes: 2 - name: Run test run: uv run ${{ matrix.command }} timeout-minutes: 30