fix(sandbox): forward Windows env keys so allow_network works (#4937) #10563
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] | |
| paths: | |
| - "surfaces/**" | |
| - "config/**" | |
| - "core/**" | |
| - "integrations/**" | |
| - "infrastructure/**" | |
| - "tools/**" | |
| - "gateway/**" | |
| - "tests/**" | |
| - "pyproject.toml" | |
| - "uv.lock" | |
| - "pytest.ini" | |
| - "ruff.toml" | |
| - "mypy.ini" | |
| - "Makefile" | |
| - ".importlinter" | |
| - ".importlinter.strict" | |
| - ".github/ci/**" | |
| - ".github/workflows/ci.yml" | |
| - ".github/workflows/release.yml" | |
| # No path filter: a filtered workflow never starts, so its gate never | |
| # reports and a PR that needs it can wait forever. The `changes` job | |
| # below decides what actually runs. | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| PYTHON_SOURCE_PATHS: "bootstrap config core gateway integrations infrastructure surfaces tools" | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: Detect relevant changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| source: ${{ steps.filter.outputs.source }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # Pin to immutable SHA (dorny/paths-filter@v4.0.2). A movable tag here | |
| # could repoint and skip quality/standard/thorough while CI Gate still passes. | |
| - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 | |
| id: filter | |
| with: | |
| filters: | | |
| source: | |
| - "surfaces/**" | |
| - "config/**" | |
| - "core/**" | |
| - "integrations/**" | |
| - "infrastructure/**" | |
| - "tools/**" | |
| - "gateway/**" | |
| - "tests/**" | |
| - "pyproject.toml" | |
| - "uv.lock" | |
| - "pytest.ini" | |
| - "ruff.toml" | |
| - "mypy.ini" | |
| - "Makefile" | |
| - ".importlinter" | |
| - ".importlinter.strict" | |
| - ".github/ci/**" | |
| - ".github/workflows/ci.yml" | |
| - ".github/workflows/release.yml" | |
| quality: | |
| needs: [changes] | |
| if: needs.changes.outputs.source == 'true' | |
| 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@v10.0.1 | |
| with: | |
| version: "latest-known" # avoid live ndjson "latest" fetch flakes | |
| 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: Verify integration registry (smoke) | |
| run: make verify-integrations-smoke | |
| - 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: | |
| needs: [changes] | |
| if: needs.changes.outputs.source == 'true' | |
| 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/infrastructure | |
| 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/benchmarks | |
| tests/chaos_engineering | |
| tests/shared | |
| tests/config | |
| tests/github_ci | |
| tests/packaging | |
| tests/scheduler | |
| gateway/tests | |
| # install/ + quickstart/ are opt-in live (CDN/brew/LLM); not PR CI. | |
| - 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 | |
| --ignore=tests/e2e/install | |
| --ignore=tests/e2e/quickstart | |
| - 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@v10.0.1 | |
| with: | |
| version: "latest-known" # avoid live ndjson "latest" fetch flakes | |
| 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=infrastructure.deployment.ec2 | |
| --cov=integrations | |
| --cov=infrastructure | |
| --cov=surfaces | |
| --cov=tools | |
| --cov=gateway | |
| --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] | |
| # Do not use always(): when test is cancelled (concurrency) or failed, | |
| # there are no coverage-data-* artifacts and ``coverage combine`` exits 1 | |
| # with "No data to combine". Skip instead; CI Gate treats skipped as OK. | |
| if: >- | |
| github.event_name == 'push' && | |
| github.ref == 'refs/heads/main' && | |
| needs.test.result == 'success' | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: "latest-known" # avoid live ndjson "latest" fetch flakes | |
| 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@v10.0.1 | |
| with: | |
| version: "latest-known" # avoid live ndjson "latest" fetch flakes | |
| 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 | |
| # Same pin as Detect relevant changes — see comment there. | |
| - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 | |
| id: filter | |
| with: | |
| filters: | | |
| thorough: | |
| - ".github/workflows/ci.yml" | |
| - "pyproject.toml" | |
| - "uv.lock" | |
| - "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@v10.0.1 | |
| with: | |
| version: "latest-known" # avoid live ndjson "latest" fetch flakes | |
| 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 | |
| ci-gate: | |
| name: CI Gate | |
| needs: [changes, quality, test, coverage-report] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| # Single required check: branch protection points here instead of at every | |
| # matrix leg. `always()` is what lets this run when an upstream job failed | |
| # or was skipped — without it the gate would be skipped too, and a skipped | |
| # required check reads as passing. | |
| - name: Require green upstream jobs | |
| run: | | |
| set -euo pipefail | |
| changes='${{ needs.changes.result }}' | |
| quality='${{ needs.quality.result }}' | |
| test_result='${{ needs.test.result }}' | |
| coverage='${{ needs.coverage-report.result }}' | |
| echo "changes=$changes quality=$quality test=$test_result coverage-report=$coverage" | |
| # If the decision job itself failed we cannot tell whether the suite | |
| # was needed, so refuse rather than pass a PR nothing checked. | |
| [ "$changes" = success ] || { echo "::error::changes is $changes"; exit 1; } | |
| # quality/test are skipped when a PR touches no source path (docs | |
| # only). That is not a failure — but it must be the only reason. | |
| case "$quality" in | |
| success) ;; | |
| skipped) echo "quality skipped: no source paths changed" ;; | |
| *) echo "::error::quality is $quality"; exit 1 ;; | |
| esac | |
| case "$test_result" in | |
| success) ;; | |
| skipped) echo "test skipped: no source paths changed" ;; | |
| *) echo "::error::test is $test_result"; exit 1 ;; | |
| esac | |
| # coverage-report only runs on pushes to main; on pull requests it is | |
| # skipped, which is not a failure. | |
| case "$coverage" in | |
| success|skipped) ;; | |
| *) echo "::error::coverage-report is $coverage"; exit 1 ;; | |
| esac |