Skip to content

fix(sandbox): forward Windows env keys so allow_network works (#4937) #10563

fix(sandbox): forward Windows env keys so allow_network works (#4937)

fix(sandbox): forward Windows env keys so allow_network works (#4937) #10563

Workflow file for this run

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