fix(ci): make provider smoke registry importable from entrypoint #196
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] | |
| 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/ |