chore(ci): add PR CI final gate and path skips (#3878)

This commit is contained in:
Steven Enamakel
2026-06-21 15:26:14 -07:00
committed by GitHub
parent b4a2242266
commit e159c7a1cb
+179 -10
View File
@@ -23,8 +23,79 @@ defaults:
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:
@@ -71,6 +142,8 @@ jobs:
i18n-coverage:
name: i18n Coverage (parity)
needs: [changes]
if: needs.changes.outputs.i18n == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 10
container:
@@ -105,6 +178,8 @@ jobs:
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:
@@ -139,6 +214,8 @@ jobs:
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
@@ -236,7 +313,8 @@ jobs:
frontend-coverage:
name: Frontend Coverage (Vitest)
needs: [frontend-quality]
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:
@@ -289,7 +367,8 @@ jobs:
rust-core-coverage:
name: Rust Core Coverage (cargo-llvm-cov)
needs: [rust-quality]
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:
@@ -349,7 +428,8 @@ jobs:
rust-tauri-coverage:
name: Rust Tauri Coverage (cargo-llvm-cov)
needs: [rust-quality]
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:
@@ -411,7 +491,8 @@ jobs:
rust-e2e:
name: Rust E2E (mock backend)
needs: [rust-quality]
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
@@ -477,7 +558,13 @@ jobs:
playwright-e2e:
name: E2E (Playwright / web lane ${{ matrix.shard }}/4)
needs: [frontend-quality, rust-quality, build-playwright-e2e-artifact]
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
@@ -574,14 +661,14 @@ jobs:
steps:
- name: Require all Playwright shards to pass
run: |
result="${{ needs.playwright-e2e.result }}"
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" ]; then
echo "Playwright shards passed."
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})."
@@ -589,16 +676,44 @@ jobs:
coverage-gate:
name: Coverage Gate (diff-cover >= 80%)
needs: [frontend-coverage, rust-core-coverage, rust-tauri-coverage]
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
@@ -612,14 +727,17 @@ jobs:
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
@@ -627,11 +745,13 @@ jobs:
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)
@@ -646,7 +766,7 @@ jobs:
--markdown-report diff-coverage.md
- name: Upload diff-cover report
if: always()
if: always() && needs.changes.outputs.coverage == 'true'
uses: actions/upload-artifact@v5
with:
name: diff-coverage-report
@@ -655,3 +775,52 @@ jobs:
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