mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* test(webui): add guarded SSO provider E2E seam * test(e2e): cover WebUI SSO multi-user isolation * fix(test): address SSO E2E review feedback
362 lines
13 KiB
YAML
362 lines
13 KiB
YAML
# Code Coverage Workflow
|
|
#
|
|
# This workflow runs test coverage analysis and uploads reports to Codecov.
|
|
# Coverage reports help identify untested code paths and maintain code quality.
|
|
#
|
|
# What it does:
|
|
# - Runs unit and integration tests with coverage instrumentation
|
|
# - Runs E2E tests with coverage instrumentation
|
|
# - Uploads coverage reports to Codecov (https://codecov.io/gh/nearai/ironclaw)
|
|
#
|
|
# Viewing coverage reports:
|
|
# - PRs automatically get coverage comments showing changes in coverage
|
|
# - Visit https://codecov.io/gh/nearai/ironclaw for detailed coverage reports
|
|
# - Coverage reports are generated for two configurations:
|
|
# 1. all-features: Full feature set
|
|
# 2. default: Default features
|
|
# - E2E coverage tracks end-to-end test coverage separately
|
|
#
|
|
# Coverage files:
|
|
# - Unit/integration: lcov.info (uploaded to Codecov with "unit" flag)
|
|
# - E2E: e2e-coverage.info (uploaded to Codecov with "e2e" flag)
|
|
#
|
|
# Requirements:
|
|
# - Uses cargo-llvm-cov for coverage instrumentation
|
|
# - Requires PostgreSQL for integration tests (pgvector/pgvector:pg16)
|
|
# - E2E tests require Python 3.12 and Playwright
|
|
|
|
name: Code Coverage
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
# Tests must never touch the real OS keychain (macOS Keychain auth dialog /
|
|
# Linux Secret Service). Guarded by
|
|
# crates/ironclaw_secrets/src/keychain.rs::os_keychain_suppressed:
|
|
# cfg!(test) covers unit tests; this covers integration/e2e that link the
|
|
# non-cfg(test) library.
|
|
IRONCLAW_DISABLE_OS_KEYCHAIN: "1"
|
|
|
|
jobs:
|
|
coverage:
|
|
name: Coverage (${{ matrix.name }})
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
CARGO_PROFILE_DEV_DEBUG: 0
|
|
CARGO_PROFILE_TEST_DEBUG: 0
|
|
# `--workspace` below runs BOTH root-package test tiers in one job: the
|
|
# Reborn integration suites (tests/integration/*) and the root QA suites
|
|
# (tests/reborn_qa_*). Both overrun libtest's 2 MiB default test-thread
|
|
# stack and abort the whole run with `has overflowed its stack` (SIGABRT)
|
|
# instead of failing a test:
|
|
# * integration tier — llvm-cov inflates async frames past the harness's
|
|
# own depth (group build -> submit_turn -> composition). reborn-tests.
|
|
# yml's reborn-integration-coverage lane carries 8 MiB for this.
|
|
# * root QA tier — reborn_qa_smoke_scenarios_e2e drives whole turns on
|
|
# the libtest stack and measures ~10 MiB *uninstrumented*, so 8 MiB is
|
|
# not enough. reborn-tests.yml's root-reborn-parity-tests lane carries
|
|
# 64 MiB for this.
|
|
# This job's scope is the union, so it takes the larger of the two. The
|
|
# depth lives in shared harness code, so per-test Box::pin only moves the
|
|
# crash to the next-deepest test.
|
|
# Pinned by tests/reborn_coverage_lane_stack_headroom.rs.
|
|
RUST_MIN_STACK: "67108864"
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: all-features
|
|
flags: "--all-features"
|
|
has_postgres: true
|
|
- name: default
|
|
flags: ""
|
|
has_postgres: true
|
|
services:
|
|
postgres:
|
|
image: pgvector/pgvector:pg16
|
|
env:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: postgres
|
|
POSTGRES_DB: ironclaw_test
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd "pg_isready -U postgres"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Free runner disk space
|
|
run: |
|
|
df -h
|
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/local/share/boost /opt/hostedtoolcache/CodeQL || true
|
|
docker system prune -af || true
|
|
df -h
|
|
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
|
with:
|
|
components: llvm-tools-preview
|
|
targets: wasm32-wasip2
|
|
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
key: coverage-${{ matrix.name }}
|
|
|
|
- name: Install cargo-llvm-cov
|
|
uses: taiki-e/install-action@62b0f2dec647a8e604c6a0fda0e38530180dce20 # cargo-llvm-cov
|
|
|
|
- name: Install cargo-component
|
|
uses: ./.github/actions/install-cargo-component
|
|
|
|
- name: Build WASM channels (for integration tests)
|
|
run: ./scripts/build-wasm-extensions.sh --channels
|
|
|
|
- name: Run database migrations
|
|
if: matrix.has_postgres
|
|
run: |
|
|
set -euo pipefail
|
|
readarray -t migration_files < <(printf '%s\n' migrations/V*.sql | sort -V)
|
|
for f in "${migration_files[@]}"; do
|
|
echo "Applying $f..."
|
|
psql -v ON_ERROR_STOP=1 -f "$f"
|
|
done
|
|
env:
|
|
PGHOST: localhost
|
|
PGUSER: postgres
|
|
PGPASSWORD: postgres
|
|
PGDATABASE: ironclaw_test
|
|
|
|
- name: Set DATABASE_URL for postgres configs
|
|
if: matrix.has_postgres
|
|
run: echo "DATABASE_URL=postgres://postgres:postgres@localhost/ironclaw_test" >> "$GITHUB_ENV"
|
|
|
|
- name: Enable pnpm for setup-node cache
|
|
if: contains(matrix.flags, '--all-features')
|
|
run: corepack enable pnpm
|
|
|
|
- name: Install Node.js for WebUI bundle build
|
|
if: contains(matrix.flags, '--all-features')
|
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "pnpm"
|
|
cache-dependency-path: crates/ironclaw_webui/frontend/pnpm-lock.yaml
|
|
|
|
- name: Enable pnpm
|
|
if: contains(matrix.flags, '--all-features')
|
|
run: corepack enable pnpm
|
|
|
|
- name: Install WebUI frontend dependencies
|
|
if: contains(matrix.flags, '--all-features')
|
|
run: |
|
|
cd crates/ironclaw_webui/frontend
|
|
pnpm install --frozen-lockfile
|
|
|
|
- name: Generate coverage
|
|
run: cargo llvm-cov ${{ matrix.flags }} --workspace --lcov --output-path lcov.info
|
|
|
|
- name: Upload to Codecov
|
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
with:
|
|
files: lcov.info
|
|
flags: ${{ matrix.name }}
|
|
disable_search: true
|
|
use_oidc: true
|
|
fail_ci_if_error: true
|
|
|
|
e2e-coverage:
|
|
name: E2E Coverage
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Free runner disk space
|
|
run: |
|
|
df -h
|
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/local/share/boost /opt/hostedtoolcache/CodeQL || true
|
|
docker system prune -af || true
|
|
df -h
|
|
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
|
with:
|
|
components: llvm-tools-preview
|
|
targets: wasm32-wasip2
|
|
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
key: e2e-coverage
|
|
|
|
- name: Install cargo-llvm-cov
|
|
uses: taiki-e/install-action@62b0f2dec647a8e604c6a0fda0e38530180dce20 # cargo-llvm-cov
|
|
|
|
- name: Set up coverage instrumentation
|
|
run: |
|
|
# show-env outputs shell-quoted values (KEY='value') but GITHUB_ENV
|
|
# expects unquoted KEY=value. Strip only the wrapping single quotes
|
|
# from KEY='value' lines without altering any internal characters.
|
|
cargo llvm-cov show-env | sed -E "s/^([A-Za-z_][A-Za-z0-9_]*)='(.*)'$/\1=\2/" >> "$GITHUB_ENV"
|
|
|
|
- name: Clean coverage workspace
|
|
run: cargo llvm-cov clean --workspace
|
|
|
|
- name: Enable pnpm for setup-node cache
|
|
run: corepack enable pnpm
|
|
|
|
- name: Install Node.js for WebUI bundle build
|
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "pnpm"
|
|
cache-dependency-path: crates/ironclaw_webui/frontend/pnpm-lock.yaml
|
|
|
|
- name: Enable pnpm
|
|
run: corepack enable pnpm
|
|
|
|
- name: Install WebUI frontend dependencies
|
|
run: |
|
|
cd crates/ironclaw_webui/frontend
|
|
pnpm install --frozen-lockfile
|
|
|
|
# Pre-build the reborn binary under the same llvm-cov env so the E2E
|
|
# fixtures find it cached instead of doing a cold instrumented build
|
|
# inside a pytest timeout. The WebChat v2 and OpenAI-compatible route
|
|
# surfaces are both unconditional. The test-support feature adds only
|
|
# the guarded loopback SSO-provider seam used by the selected SSO E2E.
|
|
- name: Build instrumented ironclaw-reborn
|
|
run: |
|
|
set -euo pipefail
|
|
cargo build -p ironclaw --bin ironclaw
|
|
cargo build -p ironclaw --bin ironclaw \
|
|
--features test-support \
|
|
--target-dir "${CARGO_TARGET_DIR:-target}/e2e-sso"
|
|
|
|
- name: Mark OpenAI-compatible binary build
|
|
run: touch target/debug/.ironclaw-reborn-openai-compat.stamp
|
|
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: tests/e2e/pyproject.toml
|
|
|
|
# Cache the Playwright browser binaries (chromium ~150MB) keyed on the e2e
|
|
# deps file; on a hit `playwright install` skips the download and only runs
|
|
# the apt `--with-deps` step. Avoids a cold browser download every run.
|
|
- name: Cache Playwright browsers
|
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
|
with:
|
|
path: ~/.cache/ms-playwright
|
|
key: ${{ runner.os }}-playwright-${{ hashFiles('tests/e2e/pyproject.toml') }}
|
|
restore-keys: ${{ runner.os }}-playwright-
|
|
- name: Install E2E dependencies
|
|
run: |
|
|
cd tests/e2e
|
|
pip install -e .
|
|
playwright install --with-deps chromium
|
|
|
|
- name: Check Reborn Responses API E2E inventory
|
|
run: python3 scripts/ci/check-reborn-responses-e2e-manifest.py
|
|
|
|
- name: Run Reborn E2E coverage tests
|
|
run: |
|
|
mapfile -t reborn_e2e_tests < <(
|
|
{
|
|
sed -e 's/#.*//' -e '/^[[:space:]]*$/d' tests/e2e/reborn_coverage_tests.txt
|
|
sed -e 's/#.*//' -e '/^[[:space:]]*$/d' tests/e2e/reborn_responses_e2e_tests.txt
|
|
} | sort -u
|
|
)
|
|
if ((${#reborn_e2e_tests[@]} == 0)); then
|
|
echo "No Reborn coverage tests selected"
|
|
exit 1
|
|
fi
|
|
printf 'Selected Reborn E2E coverage tests:\n'
|
|
printf ' %s\n' "${reborn_e2e_tests[@]}"
|
|
pytest "${reborn_e2e_tests[@]}" -v --timeout=120
|
|
env:
|
|
RUST_LOG: ironclaw=info
|
|
RUST_BACKTRACE: "1"
|
|
|
|
- name: Verify profraw files exist
|
|
if: always()
|
|
run: |
|
|
echo "LLVM_PROFILE_FILE=${LLVM_PROFILE_FILE}"
|
|
echo "CARGO_LLVM_COV_TARGET_DIR=${CARGO_LLVM_COV_TARGET_DIR}"
|
|
profraw_count=$(find target/ -name '*.profraw' 2>/dev/null | wc -l)
|
|
echo "Found ${profraw_count} .profraw files under target/"
|
|
find target/ -name '*.profraw' 2>/dev/null || true
|
|
if [ "$profraw_count" -eq 0 ]; then
|
|
echo "::warning::No .profraw files found — coverage report will fail"
|
|
fi
|
|
|
|
- name: Generate coverage report
|
|
if: always()
|
|
run: cargo llvm-cov report --lcov --output-path e2e-coverage.info
|
|
|
|
- name: Upload to Codecov
|
|
if: always()
|
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
with:
|
|
files: e2e-coverage.info
|
|
flags: e2e
|
|
disable_search: true
|
|
use_oidc: true
|
|
fail_ci_if_error: true
|
|
|
|
- name: Upload screenshots on failure
|
|
if: failure()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: e2e-screenshots
|
|
path: tests/e2e/screenshots/
|
|
if-no-files-found: ignore
|
|
|
|
coverage-gate:
|
|
name: Coverage
|
|
runs-on: ubuntu-latest
|
|
if: always()
|
|
needs: [coverage, e2e-coverage]
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- run: |
|
|
# shellcheck disable=SC1091
|
|
if ! source .github/scripts/ci-job-result-ok.sh; then
|
|
echo "Unable to source .github/scripts/ci-job-result-ok.sh"
|
|
exit 1
|
|
fi
|
|
|
|
failures=0
|
|
|
|
if ! job_result_ok "coverage" "${{ needs.coverage.result }}" false "superseded_only"; then
|
|
echo "Coverage job failed: ${{ needs.coverage.result }}"
|
|
failures=$((failures + 1))
|
|
fi
|
|
if ! job_result_ok "e2e-coverage" "${{ needs.e2e-coverage.result }}" false "superseded_only"; then
|
|
echo "E2E coverage job failed: ${{ needs.e2e-coverage.result }}"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
if [[ "${failures}" -gt 0 ]]; then
|
|
echo "One or more coverage jobs failed"
|
|
exit 1
|
|
fi
|