# Run the automated test gate when package or runtime inputs change on a PR # or on push to main. Privileged self-hosted backends live in the manually # dispatched pre-release-test workflow. name: test on: push: branches: - main paths: - 'bot_bottle/**' - 'tests/**/*.py' - 'cli.py' - 'install.sh' - 'setup.py' - 'MANIFEST.in' - 'flake.nix' - 'nix/firecracker-netpool.nix' - 'scripts/coverage.sh' - 'scripts/critical-modules.txt' - 'scripts/**/*.py' - 'scripts/firecracker-netpool.sh' - 'Dockerfile*' - 'pyproject.toml' - 'requirements-dev.txt' - 'requirements.gateway.*' - '.coveragerc' - '.dockerignore' - '.gitea/workflows/test.yml' - '.gitea/workflows/refresh-image-locks.yml' - '.gitea/workflows/pre-release-test.yml' pull_request: paths: - 'bot_bottle/**' - 'tests/**/*.py' - 'cli.py' - 'install.sh' - 'setup.py' - 'MANIFEST.in' - 'flake.nix' - 'nix/firecracker-netpool.nix' - 'scripts/coverage.sh' - 'scripts/critical-modules.txt' - 'scripts/**/*.py' - 'scripts/firecracker-netpool.sh' - 'Dockerfile*' - 'pyproject.toml' - 'requirements-dev.txt' - 'requirements.gateway.*' - '.coveragerc' - '.dockerignore' - '.gitea/workflows/test.yml' - '.gitea/workflows/refresh-image-locks.yml' - '.gitea/workflows/pre-release-test.yml' jobs: unit: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install dev requirements run: python3 -m pip install --break-system-packages -r requirements-dev.txt - name: Run unit tests with coverage env: COVERAGE_FILE: ${{ github.workspace }}/.coverage.unit run: python3 -m coverage run -m unittest discover -t . -s tests/unit -v - name: Report unit coverage env: COVERAGE_FILE: ${{ github.workspace }}/.coverage.unit run: python3 -m coverage report -m - name: Stage unit coverage for upload run: cp .coverage.unit coverage-unit.dat - name: Upload unit coverage artifact uses: actions/upload-artifact@v3 with: name: coverage-unit path: coverage-unit.dat integration-docker: runs-on: ubuntu-latest concurrency: group: integration-docker-infra cancel-in-progress: false steps: - name: Checkout uses: actions/checkout@v4 - name: Install coverage run: python3 -m pip install --break-system-packages coverage - name: Preflight — Docker backend is ready run: | python3 --version python3 cli.py backend status --backend=docker - name: Run integration tests (docker) with coverage env: BOT_BOTTLE_BACKEND: docker COVERAGE_FILE: ${{ github.workspace }}/.coverage.docker run: | set -euo pipefail # act_runner executes this job in a container while sharing the host # Docker socket. Attach control-plane siblings to the job's network, # and use named volumes for state the host daemon must mount. DOCKER_CLIENT_NETWORK=$( docker inspect "$(hostname)" | python3 -c 'import json,sys; n=json.load(sys.stdin)[0]["NetworkSettings"]["Networks"]; print(next(iter(n)))' ) test -n "$DOCKER_CLIENT_NETWORK" RUN_KEY="${GITHUB_RUN_ID:-${GITHUB_RUN_NUMBER:-0}}" export NO_PROXY="*" export no_proxy="*" export BOT_BOTTLE_DOCKER_CLIENT_NETWORK="$DOCKER_CLIENT_NETWORK" export BOT_BOTTLE_DOCKER_ROOT_MOUNT="bot-bottle-ci-root-$RUN_KEY" export BOT_BOTTLE_DOCKER_CA_MOUNT="bot-bottle-ci-ca-$RUN_KEY" python3 -m coverage run -m scripts.unittest_gate \ -t . -s tests/integration -v \ --minimum-executed 22 --fail-on-skip - name: Clean Docker integration volumes if: always() run: | RUN_KEY="${GITHUB_RUN_ID:-${GITHUB_RUN_NUMBER:-0}}" docker volume rm --force \ "bot-bottle-ci-root-$RUN_KEY" \ "bot-bottle-ci-ca-$RUN_KEY" 2>/dev/null || true - name: Stage docker coverage for upload run: cp .coverage.docker coverage-docker.dat - name: Upload docker coverage artifact uses: actions/upload-artifact@v3 with: name: coverage-docker path: coverage-docker.dat image-input-builds: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Verify shared bases cover supported architectures run: | python_ref='python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de' node_ref='node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba' for ref in "$python_ref" "$node_ref"; do docker buildx imagetools inspect --raw "$ref" | python3 -c ' import json import sys manifest = json.load(sys.stdin) platforms = { (item["platform"]["os"], item["platform"]["architecture"]) for item in manifest["manifests"] if item.get("platform", {}).get("os") != "unknown" } required = {("linux", "amd64"), ("linux", "arm64")} missing = required - platforms if missing: raise SystemExit(f"base manifest lacks supported platforms: {missing}") ' done - name: Build and smoke-test all supported images run: | set -euo pipefail suffix="${GITHUB_RUN_ID:-${GITHUB_RUN_NUMBER:-image-inputs}}" orchestrator="bot-bottle-orchestrator-inputs:${suffix}" gateway="bot-bottle-gateway-inputs:${suffix}" orchestrator_fc="bot-bottle-orchestrator-fc-inputs:${suffix}" claude="bot-bottle-claude-inputs:${suffix}" codex="bot-bottle-codex-inputs:${suffix}" pi="bot-bottle-pi-inputs:${suffix}" docker build -t "$orchestrator" -f Dockerfile.orchestrator . orchestrator_id=$(docker image inspect --format '{{.Id}}' "$orchestrator") case "$orchestrator_id" in sha256:*) ;; *) exit 1 ;; esac docker build -t "$gateway" -f Dockerfile.gateway . docker build \ --build-arg "ORCHESTRATOR_BASE_IMAGE=$orchestrator_id" \ -t "$orchestrator_fc" -f Dockerfile.orchestrator.fc . docker build -t "$claude" -f bot_bottle/contrib/claude/Dockerfile . docker build -t "$codex" -f bot_bottle/contrib/codex/Dockerfile . docker build -t "$pi" -f bot_bottle/contrib/pi/Dockerfile . docker run --rm --entrypoint python3 "$orchestrator" -c \ 'import bot_bottle.orchestrator' docker run --rm --entrypoint mitmdump "$gateway" --version docker run --rm "$claude" claude --version docker run --rm "$codex" codex --version docker run --rm "$pi" pi --version coverage: needs: [unit, integration-docker] timeout-minutes: 15 runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install coverage run: python3 -m pip install --break-system-packages coverage - name: Download unit coverage artifact uses: actions/download-artifact@v3 with: name: coverage-unit path: ${{ github.workspace }} - name: Download docker coverage artifact uses: actions/download-artifact@v3 with: name: coverage-docker path: ${{ github.workspace }} - name: Reassemble coverage data files run: | mv coverage-unit.dat .coverage.unit mv coverage-docker.dat .coverage.docker - name: Combined coverage (unit + docker integration) run: PYTHON=python3 bash scripts/coverage.sh aggregate critical - name: Diff-coverage gate (changed lines >= 90%) run: | git fetch --no-tags origin main:refs/remotes/origin/main python3 scripts/diff_coverage.py --base origin/main --min 90