Skip to content

fix(ci): make provider smoke registry importable from entrypoint #196

fix(ci): make provider smoke registry importable from entrypoint

fix(ci): make provider smoke registry importable from entrypoint #196

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ═══════════════════════════════════════════════════════════════════
# Docs & benchmark gates (R1/R4) — doc-link integrity + claim discipline
# + measured perf gates
# ═══════════════════════════════════════════════════════════════════
docs-gates:
name: 'Docs & Benchmark Gates'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
continue-on-error: true
- name: Check doc links resolve
run: python benchmarks/gates.py check-links
- name: Check claim discipline (no oversold performance claims)
run: python benchmarks/gates.py check-docs
- name: Real token-compaction gate (measured against production helpers)
run: python benchmarks/gates.py run --name token_compaction_ratio
- name: Code file-size gate (<=800 lines, legacy ratchet may only shrink)
run: python benchmarks/gates.py run --name code_file_size
- name: Code complexity gate (McCabe <=20, class-qualified exemptions)
run: python benchmarks/gates.py run --name code_complexity
- name: Streaming first-token (TTFT) gate (pooled local mock, < 2s per docs claim)
run: python benchmarks/gates.py run --name streaming_ttft
- name: Restore native tokenizer assets
id: tokcache
uses: actions/cache@v4
with:
path: assets/tokenizers
# Key pins the Qwen tokenizer.json sha256[:16]; bump when
# benchmarks/fetch_tokenizers.py REPOS changes.
key: cn-tokenizers-qwen-c0382117ea329cdf
- name: Provision CN tokenizer assets (HF primary + mirror fallback)
if: steps.tokcache.outputs.cache-hit != 'true'
run: |
pip install "tokenizers>=0.20,<1"
python benchmarks/fetch_tokenizers.py --provider qwen
# Network flake must not hard-block CI: the gate degrades to an
# honest SKIP when no asset exists (never a synthetic pass).
continue-on-error: true
- name: CN tokenizer billing-parity gate
env:
AGENTHUB_TOKENIZER_QWEN_PATH: ${{ github.workspace }}/assets/tokenizers/qwen
AGENTHUB_CN_TOKENIZER_PROVIDER: qwen
run: python benchmarks/gates.py run --name cn_tokenizer_precision
# MM-5 opt-in vision probe: honest SKIP unless a real channel is wired
# via repo secrets (NEWAPI_BASE_URL / AGENTHUB_TEST_CHANNEL_KEY).
- name: Multimodal vision e2e probe (skips without channel secrets)
env:
NEWAPI_BASE_URL: ${{ secrets.NEWAPI_BASE_URL }}
AGENTHUB_TEST_CHANNEL_KEY: ${{ secrets.AGENTHUB_TEST_CHANNEL_KEY }}
run: python benchmarks/gates.py run --name multimodal_e2e_probe
# ═══════════════════════════════════════════════════════════════════
# Frontend
# ═══════════════════════════════════════════════════════════════════
frontend:
name: 'Frontend · TS'
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npx eslint . --ext .ts,.tsx --max-warnings 50
continue-on-error: true
- run: npx tsc --noEmit
- run: npx vitest run --reporter=verbose
# ═══════════════════════════════════════════════════════════════════
# Go — all services + shared libraries
# ═══════════════════════════════════════════════════════════════════
go:
name: 'Go · All'
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/go
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache-dependency-path: services/go/go.sum
- run: go work sync
# vet all modules in the workspace (GOWORK=off per-module to avoid
# "directory prefix . does not contain modules listed in go.work")
- name: go vet (all modules)
run: |
set -e
for mod in $(find . -name go.mod -not -path '*/testdata/*' | sed 's|/go.mod||'); do
echo "::group::go vet $mod"
(cd "$mod" && go vet ./...)
echo "::endgroup::"
done
# test all modules (skip integration tests that need infra)
- name: go test (all modules)
run: |
set -e
for mod in $(find . -name go.mod -not -path '*/testdata/*' | sed 's|/go.mod||'); do
echo "::group::go test $mod"
(cd "$mod" && go test -short -race -count=1 ./...)
echo "::endgroup::"
done
# ═══════════════════════════════════════════════════════════════════
# Rust — all crates
# ═══════════════════════════════════════════════════════════════════
rust:
name: 'Rust · All'
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/rust
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
workspaces: services/rust
- run: cargo fmt --all --check
continue-on-error: true
- run: cargo clippy --all-targets -- -D warnings
continue-on-error: true
- run: cargo test --workspace
# ═══════════════════════════════════════════════════════════════════
# Python — offline services
# ═══════════════════════════════════════════════════════════════════
python:
name: 'Python · All'
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/python
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
- run: pip install ruff mypy pytest pytest-cov
- run: ruff check .
continue-on-error: true
- run: mypy --ignore-missing-imports --check-untyped-defs .
continue-on-error: true
# services/python currently ships no test files; bare pytest would exit 5
# (no tests collected) and fail the job. Skip honestly until tests land —
# the coverage gate already SKIPs without a coverage.xml artifact.
- name: Run pytest (honest SKIP while services/python has no tests)
run: |
if find . -name 'test_*.py' -not -path '*/node_modules/*' | grep -q .; then
pytest --cov=services --cov-report=xml --tb=short -q
else
echo "[SKIP] services/python has no test files yet; add tests to enforce coverage."
fi
- name: Coverage gate (line-rate >= 60%)
run: python ../../benchmarks/gates.py run --name test_coverage
workspace-runner-integration:
name: 'Python - Workspace Runner integration'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: agenthub_test
POSTGRES_PASSWORD: agenthub_test
POSTGRES_DB: agenthub_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U agenthub_test -d agenthub_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
AGENTHUB_TEST_POSTGRES_DSN: >-
postgresql://agenthub_test:agenthub_test@127.0.0.1:5432/agenthub_test
# tests/integration imports the `app` package by its repo-root path;
# without this pytest cannot resolve it on the runner.
PYTHONPATH: ${{ github.workspace }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache-dependency-path: requirements.txt
- run: pip install -r requirements-dev.txt
- run: ruff check tests/integration
- run: pytest tests/integration --tb=short -q
# ═══════════════════════════════════════════════════════════════════
# Developer CLI — unit tests + headless exec --json smoke (north-star M1)
# The exec smoke proves the exit-code contract on CI: the mock provider
# cannot create files, so the VERIFY: gate must veto the mission and
# `agenthub exec --json` must exit 1 (never a fake success).
# ═══════════════════════════════════════════════════════════════════
cli:
name: 'Developer CLI'
runs-on: ubuntu-latest
env:
PYTHONPATH: ${{ github.workspace }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache-dependency-path: requirements.txt
- run: pip install -r requirements.txt pytest
- name: CLI unit tests
run: pytest tests/cli/test_cli_main.py tests/cli/test_cli_chat.py tests/cli/test_cli_chat_compact.py tests/cli/test_cli_tui.py tests/cli/test_cli_frozen.py tests/cli/test_cli_review.py tests/cli/test_stack_installer.py tests/scripts/test_make_stack_manifest.py tests/core/test_project_root.py tests/services/test_web_search_tool.py tests/services/test_web_fetch_tool.py tests/services/test_tool_permissions.py tests/services/test_desktop_skill_tools.py tests/desktop/test_bootstrap_wizard_ui.py tests/desktop/test_release_signing.py tests/npm/test_npm_cli_package.py --tb=short -q
- name: CLI e2e (mock provider, honest-failure contract)
env:
AGENTHUB_CLI_E2E: '1'
run: pytest tests/cli/test_cli_e2e.py --tb=short -q
- name: Headless exec smoke (exit code must be 1, verifier veto)
working-directory: ${{ runner.temp }}
run: |
mkdir -p agenthub-cli-smoke && cd agenthub-cli-smoke
OBJ="创建 hello.py 并打印 hello world。"$'\n'"VERIFY: python hello.py"
set +e
python -m app.cli exec "$OBJ" --json --mission-timeout 240 > result.json
code=$?
set -e
echo "exit code: $code"
cat result.json
test "$code" -eq 1
grep -q '"status": "FAILED"' result.json
# ═══════════════════════════════════════════════════════════════════
# Docker — build verification (key images only, parallel)
# ═══════════════════════════════════════════════════════════════════
docker-build:
name: 'Docker · Build'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
image:
- { name: gateway, file: services/go/gateway-service/Dockerfile }
- { name: orchestrator, file: services/go/realtime-orchestrator/Dockerfile }
- { name: session, file: services/go/session-service/Dockerfile }
- { name: stream-delivery, file: services/go/stream-delivery-service/Dockerfile }
- { name: sandbox, file: services/go/sandbox-service/Dockerfile }
- { name: mcp-gateway, file: services/go/mcp-gateway/Dockerfile }
- { name: iam, file: services/go/iam-service/Dockerfile }
- { name: audit-log, file: services/go/audit-log-service/Dockerfile }
- { name: model-adapter, file: services/python/model_adapter_service/Dockerfile }
- { name: frontend, file: frontend/Dockerfile }
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build ${{ matrix.image.name }}
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.image.file }}
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
# ═══════════════════════════════════════════════════════════════════
# Smoke test — starts minimal infra + key services, verifies health
# ═══════════════════════════════════════════════════════════════════
smoke-test:
name: 'Smoke · Docker Compose'
runs-on: ubuntu-latest
needs: [frontend, go, rust, python, docker-build]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Run smoke test
run: bash scripts/ci-smoke-test.sh
- name: Collect logs on failure
if: failure()
run: docker compose -f deploy/docker-compose.ci.yml logs --tail=200 2>/dev/null || true
# ═══════════════════════════════════════════════════════════════════
# Frontend E2E smoke test — Playwright
# ═══════════════════════════════════════════════════════════════════
frontend-e2e:
name: 'Frontend · E2E'
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npx playwright install chromium --with-deps
- run: npm run build
env:
API_BACKEND: go
GO_GATEWAY_URL: http://localhost:8081
- run: npx playwright test --reporter=list
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-traces
path: frontend/test-results/