mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
852 lines
30 KiB
YAML
852 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
|
|
|
|
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 }}
|
|
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.*'
|
|
- 'scripts/ci-cancel-aware.sh'
|
|
i18n:
|
|
- '.github/workflows/pr-ci.yml'
|
|
- 'app/src/**'
|
|
- 'scripts/ci-cancel-aware.sh'
|
|
rust-core:
|
|
- '.github/workflows/pr-ci.yml'
|
|
- 'Cargo.toml'
|
|
- 'Cargo.lock'
|
|
- 'rust-toolchain.toml'
|
|
- 'src/**'
|
|
- 'tests/**'
|
|
- 'scripts/ci-cancel-aware.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-quality:
|
|
name: Frontend Quality (typecheck, lint, format)
|
|
needs: [changes]
|
|
if: needs.changes.outputs.frontend == 'true'
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 15
|
|
container:
|
|
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Cache pnpm store
|
|
id: pnpm-cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
pnpm-store-${{ runner.os }}-
|
|
|
|
- name: Install dependencies
|
|
run: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile
|
|
|
|
- name: Save pnpm store cache
|
|
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
|
|
|
|
- name: Type check TypeScript files
|
|
run: bash scripts/ci-cancel-aware.sh pnpm --filter openhuman-app compile
|
|
env:
|
|
NODE_ENV: test
|
|
|
|
- name: Check Prettier formatting
|
|
run: bash scripts/ci-cancel-aware.sh pnpm --filter openhuman-app format:check
|
|
env:
|
|
NODE_ENV: test
|
|
|
|
- name: Run ESLint
|
|
run: bash scripts/ci-cancel-aware.sh pnpm --filter openhuman-app lint
|
|
env:
|
|
NODE_ENV: test
|
|
|
|
i18n-coverage:
|
|
name: i18n Coverage (parity)
|
|
needs: [changes]
|
|
if: needs.changes.outputs.i18n == 'true'
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 10
|
|
container:
|
|
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Cache pnpm store
|
|
id: pnpm-cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
pnpm-store-${{ runner.os }}-
|
|
|
|
- name: Install dependencies
|
|
run: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile
|
|
|
|
- name: Save pnpm store cache
|
|
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
|
|
|
|
- name: Verify i18n coverage (missing / extra / drifted keys across locales)
|
|
run: bash scripts/ci-cancel-aware.sh pnpm i18n:check
|
|
|
|
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: bash scripts/ci-cancel-aware.sh 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@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@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: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile
|
|
|
|
- name: Save pnpm store cache
|
|
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@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
|
|
bash scripts/ci-cancel-aware.sh 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@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@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
|
|
|
|
frontend-coverage:
|
|
name: Frontend Coverage (Vitest)
|
|
needs: [changes, frontend-quality]
|
|
if: always() && needs.changes.outputs.frontend == 'true' && needs['frontend-quality'].result == 'success'
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 30
|
|
container:
|
|
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Cache pnpm store
|
|
id: pnpm-cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
pnpm-store-${{ runner.os }}-
|
|
|
|
- name: Install dependencies
|
|
run: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile
|
|
|
|
- name: Save pnpm store cache
|
|
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
|
|
|
|
- name: Run Vitest with coverage
|
|
run: bash ../scripts/ci-cancel-aware.sh pnpm test:coverage
|
|
working-directory: app
|
|
env:
|
|
NODE_ENV: test
|
|
|
|
- name: Normalize lcov source paths to repo root
|
|
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
|
|
uses: actions/upload-artifact@v5
|
|
with:
|
|
name: lcov-frontend
|
|
path: app/coverage/lcov.info
|
|
retention-days: 7
|
|
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: true
|
|
shared-key: pr-rust-core-cov
|
|
|
|
- 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: Upload core lcov
|
|
uses: actions/upload-artifact@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@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@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@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@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: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile
|
|
|
|
- name: Save pnpm store cache
|
|
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@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: bash scripts/ci-cancel-aware.sh pnpm test:rust:e2e
|
|
env:
|
|
RUST_BACKTRACE: "1"
|
|
|
|
playwright-e2e:
|
|
name: E2E (Playwright / web lane ${{ matrix.shard }}/4)
|
|
needs: [changes, frontend-quality, rust-quality, build-playwright-e2e-artifact]
|
|
if: |
|
|
always() &&
|
|
needs.changes.outputs.playwright == 'true' &&
|
|
needs['build-playwright-e2e-artifact'].result == 'success' &&
|
|
contains(fromJSON('["success", "skipped"]'), needs['frontend-quality'].result) &&
|
|
contains(fromJSON('["success", "skipped"]'), needs['rust-quality'].result)
|
|
runs-on: ubuntu-22.04
|
|
container:
|
|
image: ghcr.io/tinyhumansai/openhuman_ci:latest
|
|
timeout-minutes: 30
|
|
# Sharded 4 ways: each shard is a separate runner that boots its OWN backend
|
|
# stack (mock + core + web on fixed ports) and runs playwright --shard=i/4.
|
|
# Because each shard is an isolated machine there is no shared mock-state
|
|
# race, so this parallelises safely despite the suite's workers:1 design.
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
shard: [1, 2, 3, 4]
|
|
# TODO(ci-flaky, #3615): shard 1/4 consistently hits 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 ~7 others. Shards 2-4 pass reliably.
|
|
# 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@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: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile
|
|
|
|
- name: Save pnpm store cache
|
|
if: always() && steps.pnpm-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .pnpm-store
|
|
key: ${{ steps.pnpm-cache.outputs.cache-primary-key }}
|
|
|
|
- name: Download Playwright E2E artifact
|
|
uses: actions/download-artifact@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: bash scripts/ci-cancel-aware.sh pnpm --filter openhuman-app exec playwright install chromium-headless-shell
|
|
|
|
- name: Run Playwright web E2E suite (shard ${{ matrix.shard }}/4)
|
|
env:
|
|
OPENHUMAN_WORKSPACE: ${{ runner.temp }}/openhuman-playwright-workspace
|
|
run: |
|
|
mkdir -p "$OPENHUMAN_WORKSPACE"
|
|
bash scripts/ci-cancel-aware.sh bash app/scripts/e2e-web-session.sh \
|
|
--shard=${{ matrix.shard }}/4
|
|
|
|
- name: Upload Playwright E2E failure artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@v5
|
|
with:
|
|
name: e2e-playwright-failure-logs-${{ github.run_id }}-shard${{ matrix.shard }}
|
|
path: |
|
|
${{ runner.temp }}/openhuman-playwright-workspace/**
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
playwright-e2e-gate:
|
|
name: Playwright E2E Gate
|
|
# Fixed-name aggregator over the 4 dynamic shard checks. Branch protection
|
|
# can require this single job instead of the per-shard matrix names (which
|
|
# change if the shard count changes). if: always() so it runs even when a
|
|
# shard is failed/skipped and can report the real aggregate result.
|
|
needs: [playwright-e2e]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Require all Playwright shards to pass
|
|
run: |
|
|
result="${{ needs['playwright-e2e'].result }}"
|
|
echo "Playwright web lane aggregate result: ${result}"
|
|
# NOTE: the shards currently carry continue-on-error: true, so this
|
|
# aggregate is 'success' even when a shard hit a known flake — the
|
|
# gate passes today. Once continue-on-error is removed from the shards
|
|
# (after the flakes are fixed) this same check enforces a real pass.
|
|
if [ "${result}" = "success" ] || [ "${result}" = "skipped" ]; then
|
|
echo "Playwright shards passed or were intentionally skipped."
|
|
exit 0
|
|
fi
|
|
echo "::error::Playwright web lane did not succeed (result=${result})."
|
|
exit 1
|
|
|
|
coverage-gate:
|
|
name: Coverage Gate (diff-cover >= 80%)
|
|
needs: [changes, frontend-coverage, rust-core-coverage, rust-tauri-coverage]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Verify coverage lanes completed
|
|
run: |
|
|
set -euo pipefail
|
|
coverage="${{ needs.changes.outputs.coverage }}"
|
|
frontend="${{ needs['frontend-coverage'].result }}"
|
|
core="${{ needs['rust-core-coverage'].result }}"
|
|
tauri="${{ needs['rust-tauri-coverage'].result }}"
|
|
|
|
echo "Coverage needed: ${coverage}"
|
|
echo "Frontend coverage result: ${frontend}"
|
|
echo "Rust core coverage result: ${core}"
|
|
echo "Rust Tauri coverage result: ${tauri}"
|
|
|
|
if [ "${coverage}" != "true" ]; then
|
|
echo "No coverage-relevant changes detected; skipping diff-cover."
|
|
exit 0
|
|
fi
|
|
|
|
for result in "${frontend}" "${core}" "${tauri}"; do
|
|
if [ "${result}" != "success" ] && [ "${result}" != "skipped" ]; then
|
|
echo "::error::A required coverage lane did not succeed or skip cleanly (result=${result})."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- 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@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@v5
|
|
with:
|
|
name: diff-coverage-report
|
|
path: |
|
|
diff-coverage.html
|
|
diff-coverage.md
|
|
retention-days: 14
|
|
if-no-files-found: warn
|
|
|
|
pr-ci-gate:
|
|
name: PR CI Gate
|
|
needs:
|
|
- changes
|
|
- frontend-quality
|
|
- i18n-coverage
|
|
- rust-quality
|
|
- build-playwright-e2e-artifact
|
|
- frontend-coverage
|
|
- rust-core-coverage
|
|
- rust-tauri-coverage
|
|
- rust-e2e
|
|
- playwright-e2e-gate
|
|
- coverage-gate
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Require PR CI jobs to pass
|
|
run: |
|
|
set -euo pipefail
|
|
declare -A results=(
|
|
["Detect Changed Areas"]="${{ needs.changes.result }}"
|
|
["Frontend Quality"]="${{ needs['frontend-quality'].result }}"
|
|
["i18n Coverage"]="${{ needs['i18n-coverage'].result }}"
|
|
["Rust Quality"]="${{ needs['rust-quality'].result }}"
|
|
["Build Playwright E2E Artifact"]="${{ needs['build-playwright-e2e-artifact'].result }}"
|
|
["Frontend Coverage"]="${{ needs['frontend-coverage'].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 Gate"]="${{ needs['playwright-e2e-gate'].result }}"
|
|
["Coverage Gate"]="${{ needs['coverage-gate'].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
|