Files
openhuman/.github/workflows/pr-ci.yml
T

796 lines
30 KiB
YAML

---
name: PR CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
packages: read
pull-requests: read
env:
NPM_CONFIG_STORE_DIR: .pnpm-store
# Keep only the newest run per PR/branch. Long Rust/Tauri build commands must
# run through scripts/ci-cancel-aware.sh so cancellation reaches descendants.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
changes:
name: Detect Changed Areas
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
i18n: ${{ steps.filter.outputs.i18n }}
docs: ${{ steps.filter.outputs.docs }}
rust-core: ${{ steps.filter.outputs['rust-core'] }}
rust-tauri: ${{ steps.filter.outputs['rust-tauri'] }}
playwright: ${{ steps.filter.outputs.playwright }}
coverage: ${{ steps.filter.outputs.frontend == 'true' || steps.filter.outputs['rust-core'] == 'true' || steps.filter.outputs['rust-tauri'] == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Detect changed paths
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
frontend:
- '.github/workflows/pr-ci.yml'
- 'package.json'
- 'pnpm-lock.yaml'
- 'app/package.json'
- 'app/index.html'
- 'app/public/**'
- 'app/src/**'
- 'app/test/vitest.config.ts'
- 'app/tsconfig*.json'
- 'app/vite.config.*'
- 'app/tailwind.config.*'
- 'app/postcss.config.*'
i18n:
- '.github/workflows/pr-ci.yml'
- 'app/src/**'
docs:
- '.github/workflows/pr-ci.yml'
- 'app/src/App.tsx'
- 'scripts/generate-architecture-docs.mjs'
- 'scripts/__tests__/generate-architecture-docs.test.mjs'
- 'gitbooks/developing/architecture/frontend.md'
- 'package.json'
rust-core:
- '.github/workflows/pr-ci.yml'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'src/**'
- 'tests/**'
- 'scripts/ci-cancel-aware.sh'
- 'scripts/test-rust-e2e.sh'
- 'scripts/test-rust-with-mock.sh'
rust-tauri:
- '.github/workflows/pr-ci.yml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'app/src-tauri/**'
- 'scripts/ci-cancel-aware.sh'
playwright:
- '.github/workflows/pr-ci.yml'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'package.json'
- 'pnpm-lock.yaml'
- 'src/**'
- 'app/package.json'
- 'app/index.html'
- 'app/public/**'
- 'app/src/**'
- 'app/test/e2e/**'
- 'app/scripts/e2e-web-*.sh'
- 'scripts/ci-cancel-aware.sh'
frontend-checks:
name: Frontend Checks (quality, i18n, docs, coverage)
needs: [changes]
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.i18n == 'true' || needs.changes.outputs.docs == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 60
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
steps:
- name: Checkout code
# Pinned to the v5 commit SHA for supply-chain hardening (CodeRabbit
# review on #3892). The combined frontend lane keeps that hardening
# from the former docs-drift job.
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 1
persist-credentials: false
- name: Cache pnpm store
id: pnpm-cache
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.i18n == 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Install dependencies
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.i18n == 'true'
run: pnpm install --frozen-lockfile
- name: Save pnpm store cache
if: always() && (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.i18n == 'true') && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
- name: Type check TypeScript files
if: needs.changes.outputs.frontend == 'true'
run: pnpm --filter openhuman-app compile
env:
NODE_ENV: test
- name: Check Prettier formatting
if: needs.changes.outputs.frontend == 'true'
run: pnpm --filter openhuman-app format:check
env:
NODE_ENV: test
- name: Run ESLint
if: needs.changes.outputs.frontend == 'true'
run: pnpm --filter openhuman-app lint
env:
NODE_ENV: test
- name: Verify i18n coverage (missing / extra / drifted keys across locales)
if: needs.changes.outputs.i18n == 'true'
run: pnpm i18n:check
# The generator + its tests use only Node built-ins (no workspace deps),
# so docs-only changes still skip `pnpm install`.
- name: Generator unit tests
if: needs.changes.outputs.docs == 'true'
run: pnpm docs:test
- name: Verify generated architecture docs are not stale
if: needs.changes.outputs.docs == 'true'
run: pnpm docs:check
- name: Run Vitest with coverage
if: needs.changes.outputs.frontend == 'true'
run: pnpm test:coverage
working-directory: app
env:
NODE_ENV: test
- name: Normalize lcov source paths to repo root
if: needs.changes.outputs.frontend == 'true'
run: |
set -euo pipefail
test -f app/coverage/lcov.info
sed -i -E 's#^SF:(src/)#SF:app/\1#' app/coverage/lcov.info
sed -i -E 's#^SF:(\./src/)#SF:app/src/#' app/coverage/lcov.info
- name: Upload frontend lcov
if: needs.changes.outputs.frontend == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: lcov-frontend
path: app/coverage/lcov.info
retention-days: 7
if-no-files-found: error
rust-quality:
name: Rust Quality (fmt, clippy)
needs: [changes]
if: needs.changes.outputs['rust-core'] == 'true' || needs.changes.outputs['rust-tauri'] == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 20
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
env:
CARGO_INCREMENTAL: "0"
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
persist-credentials: false
submodules: recursive
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
cache-on-failure: true
# shared-key (not key) so the cache name is stable and not suffixed
# with the job id. Swatinem caches the full target/, which is why we
# no longer layer sccache on top (it logged 0% hits under a warm
# rust-cache and just doubled the cache footprint).
shared-key: pr-rust-quality
- name: Check Rust formatting
run: cargo fmt --all -- --check
- name: Run clippy (core crate)
run: bash scripts/ci-cancel-aware.sh cargo clippy -p openhuman
build-playwright-e2e-artifact:
name: Build Playwright E2E Artifact
needs: [changes]
if: needs.changes.outputs.playwright == 'true'
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:latest
timeout-minutes: 30
env:
CARGO_INCREMENTAL: "0"
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
persist-credentials: false
submodules: recursive
- name: Cache pnpm store
id: pnpm-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: . -> target
cache-on-failure: true
# Shared with rust-e2e (same image + env): both compile the openhuman
# crate's non-instrumented dependency tree, so one cache entry serves
# both instead of two ~1GB copies competing for the repo cache budget.
shared-key: pr-rust-core
- name: Restore cached Playwright E2E artifact
id: playwright-artifact-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .ci/artifacts/e2e-playwright-linux.tar.gz
key: e2e-playwright-linux-${{ hashFiles('src/**', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', 'app/src/**', 'app/public/**', 'app/index.html', 'app/vite.config.*', 'app/tailwind.config.*', 'app/postcss.config.*', 'app/package.json', 'pnpm-lock.yaml', 'app/scripts/e2e-web-build.sh') }}
- name: Install dependencies
if: steps.playwright-artifact-cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Save pnpm store cache
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
- name: Ensure .env exists for E2E build
if: steps.playwright-artifact-cache.outputs.cache-hit != 'true'
run: |
touch .env
touch app/.env
- name: Build and package Playwright E2E artifact
if: steps.playwright-artifact-cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
pnpm --filter openhuman-app test:e2e:web:build
RUST_HOST_TRIPLE="$(rustc -vV | awk '/^host: / { print $2 }')"
WEB_CORE_TARGET_DIR="target/e2e-web-${RUST_HOST_TRIPLE}"
STAGE="$(mktemp -d)"
mkdir -p "$STAGE/repo/app/dist-web"
mkdir -p "$STAGE/repo/$WEB_CORE_TARGET_DIR/debug"
mkdir -p .ci/artifacts
cp -a app/dist-web/. "$STAGE/repo/app/dist-web/"
cp -a "$WEB_CORE_TARGET_DIR/debug/openhuman-core" "$STAGE/repo/$WEB_CORE_TARGET_DIR/debug/"
tar -czf .ci/artifacts/e2e-playwright-linux.tar.gz -C "$STAGE" repo
ls -lh .ci/artifacts/e2e-playwright-linux.tar.gz
- name: Verify build artifact exists
run: |
set -euo pipefail
test -f .ci/artifacts/e2e-playwright-linux.tar.gz
- name: Save Playwright E2E artifact cache
if: always() && steps.playwright-artifact-cache.outputs.cache-hit != 'true' && hashFiles('.ci/artifacts/e2e-playwright-linux.tar.gz') != ''
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .ci/artifacts/e2e-playwright-linux.tar.gz
key: ${{ steps.playwright-artifact-cache.outputs.cache-primary-key }}
- name: Upload Playwright E2E artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: e2e-playwright-linux-${{ github.run_id }}
path: .ci/artifacts/e2e-playwright-linux.tar.gz
retention-days: 1
if-no-files-found: error
rust-core-coverage:
name: Rust Core Coverage (cargo-llvm-cov)
needs: [changes, rust-quality]
if: always() && needs.changes.outputs['rust-core'] == 'true' && needs['rust-quality'].result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 30
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
env:
CARGO_INCREMENTAL: "0"
# Coverage-instrumented test binaries are dominated by DWARF debug info,
# which exhausts the runner disk and SIGBUSes the linker. Drop debug info
# entirely for this job — llvm-cov line/region coverage is read from the
# embedded __llvm_covmap/__llvm_covfun sections (filenames + line numbers
# included), NOT from DWARF, so coverage numbers are unaffected. This is
# the maximum-shrink setting; line-tables-only was still too large for the
# instrumented build.
CARGO_PROFILE_DEV_DEBUG: "0"
steps:
- name: Free disk space
run: |
# Coverage-instrumented builds produce huge binaries that exhaust the
# runner's ~14GB free disk during linking (SIGBUS). Inside a container,
# host paths are bind-mounted:
# /opt/hostedtoolcache → /__t
# /home/runner/work → /__w
# Deleting /__t frees ~8GB on the same partition the build uses.
rm -rf /__t/* || true
echo "Disk after cleanup:"
df -h /__w || true
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
persist-credentials: false
submodules: recursive
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: . -> target
cache-on-failure: false
shared-key: pr-rust-core-cov
- name: Prune stale coverage test executables
run: |
# The restored target cache can contain prior llvm-cov integration-test
# executables. Those are enormous after coverage instrumentation and
# can exhaust the runner before the current cargo invocation finishes.
find target/llvm-cov-target -type f -path '*/debug/deps/*' -perm -111 -delete 2>/dev/null || true
echo "Disk after coverage target prune:"
df -h /__w || true
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run cargo llvm-cov for openhuman core
run: bash scripts/ci-cancel-aware.sh cargo llvm-cov --no-fail-fast -p openhuman --lcov --output-path lcov-core.info
env:
CARGO_BUILD_JOBS: "1"
- name: Prune coverage test executables before cache save
if: always()
run: |
find target/llvm-cov-target -type f -path '*/debug/deps/*' -perm -111 -delete 2>/dev/null || true
echo "Disk after post-coverage prune:"
df -h /__w || true
- name: Upload core lcov
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: lcov-rust-core
path: lcov-core.info
retention-days: 7
if-no-files-found: error
rust-tauri-coverage:
name: Rust Tauri Coverage (cargo-llvm-cov)
needs: [changes, rust-quality]
if: always() && needs.changes.outputs['rust-tauri'] == 'true' && needs['rust-quality'].result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 30
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
env:
CARGO_INCREMENTAL: "0"
# The instrumented Tauri shell binary (whole tauri/zbus/CEF graph) is
# dominated by DWARF debug info, which exhausts the runner disk and
# SIGBUSes the linker (collect2: ld terminated with signal 7) during the
# final link. Drop debug info — llvm-cov reads coverage from the embedded
# __llvm_covmap/__llvm_covfun sections, NOT from DWARF, so coverage
# numbers are unaffected. Mirrors rust-core-coverage.
CARGO_PROFILE_DEV_DEBUG: "0"
steps:
- name: Free disk space
run: |
# Coverage-instrumented builds produce huge binaries that exhaust the
# runner's ~14GB free disk during linking (SIGBUS). Inside a container,
# host paths are bind-mounted:
# /opt/hostedtoolcache → /__t
# /home/runner/work → /__w
# Deleting /__t frees ~8GB on the same partition the build uses.
# Mirrors rust-core-coverage.
rm -rf /__t/* || true
echo "Disk after cleanup:"
df -h /__w || true
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
persist-credentials: false
submodules: recursive
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
app/src-tauri -> target
cache-on-failure: true
# Tauri shell coverage (instrumented) — its own stable cache; reuses
# its warm dependency tree run-over-run.
shared-key: pr-rust-tauri-cov
- name: Cache CEF binary distribution
id: cef-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/Library/Caches/tauri-cef
~/.cache/tauri-cef
key: cef-x86_64-unknown-linux-gnu-v2-${{ hashFiles('app/src-tauri/Cargo.toml') }}
restore-keys: |
cef-x86_64-unknown-linux-gnu-v2-
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run cargo llvm-cov for Tauri shell
run: bash scripts/ci-cancel-aware.sh cargo llvm-cov --manifest-path app/src-tauri/Cargo.toml --lcov --output-path lcov-tauri.info
env:
# Serialize codegen/link to cap peak memory + disk during the
# instrumented Tauri-shell link (SIGBUS otherwise). Mirrors
# rust-core-coverage.
CARGO_BUILD_JOBS: "1"
- name: Save CEF binary distribution cache
if: always() && steps.cef-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/Library/Caches/tauri-cef
~/.cache/tauri-cef
key: ${{ steps.cef-cache.outputs.cache-primary-key }}
- name: Upload tauri lcov
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: lcov-rust-tauri
path: lcov-tauri.info
retention-days: 7
if-no-files-found: error
rust-e2e:
name: Rust E2E (mock backend)
needs: [changes, rust-quality]
if: always() && needs.changes.outputs['rust-core'] == 'true' && needs['rust-quality'].result == 'success'
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:latest
timeout-minutes: 30
env:
CARGO_INCREMENTAL: "0"
# Trim DWARF debug info so test-binary linking doesn't exhaust runner disk
# (SIGBUS). Tests don't need full debug info; backtraces keep file/line.
CARGO_PROFILE_DEV_DEBUG: line-tables-only
steps:
- name: Free disk space
run: |
# Reclaim space on the build partition before compiling/linking the
# Rust test binaries (host toolcache is bind-mounted at /__t inside the
# container). Mirrors the rust-core-coverage cleanup.
rm -rf /__t/* || true
df -h /__w || true
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
persist-credentials: false
submodules: recursive
- name: Cache pnpm store
id: pnpm-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: . -> target
cache-on-failure: true
# Shared with build-playwright (same image + env): one non-instrumented
# core cache serves both lanes instead of two competing copies.
shared-key: pr-rust-core
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Save pnpm store cache
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
- name: Ensure .env exists for tests
run: |
touch .env
touch app/.env
- name: Run Rust E2E suite
run: pnpm test:rust:e2e
env:
RUST_BACKTRACE: "1"
playwright-e2e:
name: E2E (Playwright / web lane)
needs: [changes, build-playwright-e2e-artifact]
if: |
always() &&
needs.changes.outputs.playwright == 'true' &&
needs['build-playwright-e2e-artifact'].result == 'success'
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:latest
timeout-minutes: 90
# Run the Playwright suite in one container to reduce PR CI fanout and stay
# under GitHub concurrency limits. This is intentionally slower than the
# previous shard matrix but avoids four separate runner/container slots.
# TODO(ci-flaky, #3615): several specs consistently hit toBeVisible
# timeouts under CI resource contention — chat-tool-error-recovery,
# connector-gmail-composio, connector-session-guard-matrix,
# chat-harness-send-stream, chat-management-functional and others.
# Playwright config bumped to 90s test / 15s expect timeout + 2 retries.
# Run the lane but don't block merges until infra stabilises.
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
persist-credentials: false
submodules: recursive
- name: Cache pnpm store
id: pnpm-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Install JS dependencies (test harness)
run: pnpm install --frozen-lockfile
- name: Save pnpm store cache
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .pnpm-store
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
- name: Download Playwright E2E artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: e2e-playwright-linux-${{ github.run_id }}
path: .
- name: Restore Playwright E2E artifact
run: |
set -euo pipefail
tar -xzf e2e-playwright-linux.tar.gz
cp -a repo/app/dist-web app/
mkdir -p target
cp -a repo/target/. target/
rm -rf repo e2e-playwright-linux.tar.gz
chmod +x target/e2e-web-*/debug/openhuman-core
- name: Install Playwright Chromium headless shell
run: pnpm --filter openhuman-app exec playwright install chromium-headless-shell
- name: Run Playwright web E2E suite
env:
OPENHUMAN_WORKSPACE: ${{ runner.temp }}/openhuman-playwright-workspace
run: |
mkdir -p "$OPENHUMAN_WORKSPACE"
bash app/scripts/e2e-web-session.sh
- name: Upload Playwright E2E failure artifacts
if: failure()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: e2e-playwright-failure-logs-${{ github.run_id }}
path: |
${{ runner.temp }}/openhuman-playwright-workspace/**
retention-days: 7
if-no-files-found: ignore
pr-ci-gate:
name: PR CI Gate
needs:
- changes
- frontend-checks
- rust-quality
- build-playwright-e2e-artifact
- rust-core-coverage
- rust-tauri-coverage
- rust-e2e
- playwright-e2e
if: always()
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Require PR CI jobs to pass
run: |
set -euo pipefail
declare -A results=(
["Detect Changed Areas"]="${{ needs.changes.result }}"
["Frontend Checks"]="${{ needs['frontend-checks'].result }}"
["Rust Quality"]="${{ needs['rust-quality'].result }}"
["Build Playwright E2E Artifact"]="${{ needs['build-playwright-e2e-artifact'].result }}"
["Rust Core Coverage"]="${{ needs['rust-core-coverage'].result }}"
["Rust Tauri Coverage"]="${{ needs['rust-tauri-coverage'].result }}"
["Rust E2E"]="${{ needs['rust-e2e'].result }}"
["Playwright E2E"]="${{ needs['playwright-e2e'].result }}"
)
failed=0
for name in "${!results[@]}"; do
result="${results[$name]}"
echo "${name}: ${result}"
if [ "${result}" != "success" ] && [ "${result}" != "skipped" ]; then
echo "::error::${name} did not pass (result=${result})."
failed=1
fi
done
if [ "${failed}" -ne 0 ]; then
exit 1
fi
- name: Verify coverage lanes completed
run: |
set -euo pipefail
coverage="${{ needs.changes.outputs.coverage }}"
frontend_needed="${{ needs.changes.outputs.frontend }}"
core_needed="${{ needs.changes.outputs['rust-core'] }}"
tauri_needed="${{ needs.changes.outputs['rust-tauri'] }}"
frontend="${{ needs['frontend-checks'].result }}"
core="${{ needs['rust-core-coverage'].result }}"
tauri="${{ needs['rust-tauri-coverage'].result }}"
echo "Coverage needed: ${coverage}"
echo "Frontend changed: ${frontend_needed} (job=${frontend})"
echo "Rust core changed: ${core_needed} (job=${core})"
echo "Rust Tauri changed: ${tauri_needed} (job=${tauri})"
if [ "${coverage}" != "true" ]; then
echo "No coverage-relevant changes detected; skipping diff-cover."
exit 0
fi
if [ "${frontend_needed}" = "true" ] && [ "${frontend}" != "success" ]; then
echo "::error::Frontend coverage was required but Frontend Checks did not succeed (result=${frontend})."
exit 1
fi
if [ "${core_needed}" = "true" ] && [ "${core}" != "success" ]; then
echo "::error::Rust core coverage was required but the lane did not succeed (result=${core})."
exit 1
fi
if [ "${tauri_needed}" = "true" ] && [ "${tauri}" != "success" ]; then
echo "::error::Rust Tauri coverage was required but the lane did not succeed (result=${tauri})."
exit 1
fi
- name: Checkout code
if: needs.changes.outputs.coverage == 'true'
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve coverage compare ref
if: needs.changes.outputs.coverage == 'true'
id: coverage-compare
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
git fetch origin "${{ github.base_ref }}" --depth=200
echo "ref=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
elif [ -n "${{ github.event.before }}" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
echo "ref=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
else
echo "ref=HEAD^" >> "$GITHUB_OUTPUT"
fi
- name: Setup Python
if: needs.changes.outputs.coverage == 'true'
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install diff-cover
if: needs.changes.outputs.coverage == 'true'
run: pip install 'diff-cover>=9.2.0'
- name: Download all lcov artifacts
if: needs.changes.outputs.coverage == 'true'
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
path: lcov-artifacts
pattern: lcov-*
merge-multiple: false
- name: List collected lcov files
if: needs.changes.outputs.coverage == 'true'
run: |
set -euo pipefail
find lcov-artifacts -type f -name '*.info' -print
- name: Enforce >= 80% coverage on changed lines
if: needs.changes.outputs.coverage == 'true'
run: |
set -euo pipefail
mapfile -t LCOV_FILES < <(find lcov-artifacts -type f -name '*.info' | sort)
if [ "${#LCOV_FILES[@]}" -eq 0 ]; then
echo "::error::No lcov files found; coverage gate cannot run"
exit 1
fi
diff-cover "${LCOV_FILES[@]}" \
--compare-branch="${{ steps.coverage-compare.outputs.ref }}" \
--fail-under=80 \
--html-report diff-coverage.html \
--markdown-report diff-coverage.md
- name: Upload diff-cover report
if: always() && needs.changes.outputs.coverage == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: diff-coverage-report
path: |
diff-coverage.html
diff-coverage.md
retention-days: 14
if-no-files-found: warn