Files
proxycast/.github/workflows/quality.yml
T
2026-08-12 12:28:58 +08:00

592 lines
22 KiB
YAML

name: Quality
on:
pull_request:
paths-ignore:
- ".github/workflows/build-windows-test.yml"
push:
branches:
- main
paths-ignore:
- ".github/workflows/build-windows-test.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
changed:
name: Detect changed areas
runs-on: ubuntu-latest
outputs:
bridge: ${{ steps.detect.outputs.bridge }}
bridge_reasons: ${{ steps.detect.outputs.bridge_reasons }}
changed_count: ${{ steps.detect.outputs.changed_count }}
docs: ${{ steps.detect.outputs.docs }}
docs_only: ${{ steps.detect.outputs.docs_only }}
fallback: ${{ steps.detect.outputs.fallback }}
frontend: ${{ steps.detect.outputs.frontend }}
gui_smoke: ${{ steps.detect.outputs.gui_smoke }}
integrity: ${{ steps.detect.outputs.integrity }}
rust: ${{ steps.detect.outputs.rust }}
workflow: ${{ steps.detect.outputs.workflow }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Detect tasks
id: detect
shell: bash
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
args=(--format github)
if [[ "${{ github.event_name }}" == "push" && -n "${BEFORE_SHA}" && "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]]; then
args+=(--base "${BEFORE_SHA}")
fi
node scripts/quality-task-selector.mjs "${args[@]}" >> "$GITHUB_OUTPUT"
# PR 侧保留快速反馈门禁;main / 手动触发继续跑全量前端与 Rust。
architecture_confirmation:
name: Architecture Confirmation
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Verify architecture confirmation
run: npm run governance:architecture-confirmation
integrity:
name: Integrity
runs-on: ubuntu-latest
needs: changed
if: ${{ needs.changed.outputs.integrity == 'true' || needs.changed.outputs.fallback == 'true' || needs.changed.outputs.workflow == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Verify app version consistency
run: npm run verify:app-version
frontend:
name: Frontend Quick
runs-on: ubuntu-latest
needs: changed
if: ${{ github.event_name == 'pull_request' && (needs.changed.outputs.frontend == 'true' || needs.changed.outputs.fallback == 'true' || needs.changed.outputs.workflow == 'true') }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test layer budget
run: node scripts/check-vitest-layer-budget.mjs --max-component-candidates 37
- name: Unit tests
run: npm run test:unit
- name: Contract layer tests
run: npm run test:contract
frontend_full:
name: Frontend Full
runs-on: ubuntu-latest
needs: changed
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changed.outputs.docs_only != 'true') || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test layer budget
run: node scripts/check-vitest-layer-budget.mjs --max-component-candidates 37
- name: Full frontend Vitest
run: npm run test:frontend:all
bridge_contracts:
name: Bridge & Contracts
runs-on: ubuntu-latest
needs: changed
if: ${{ needs.changed.outputs.bridge == 'true' || needs.changed.outputs.fallback == 'true' || needs.changed.outputs.workflow == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Bridge tests
run: npm run test:bridge
- name: Command contracts
run: npm run test:contracts
gui_smoke:
name: GUI Smoke
runs-on: macos-latest
timeout-minutes: 90
needs: changed
if: ${{ needs.changed.outputs.gui_smoke == 'true' || needs.changed.outputs.fallback == 'true' || needs.changed.outputs.workflow == 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changed.outputs.docs_only != 'true') || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: lime-rs
shared-key: quality-gui-smoke
cache-on-failure: true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Prepare sherpa-onnx runtime
shell: bash
run: |
set -euo pipefail
TARGET="$(rustc -vV | sed -n 's/^host: //p')"
test -n "$TARGET"
node scripts/prepare-sherpa-onnx-runtime.mjs --target "$TARGET"
- name: Run headless GUI smoke
run: npm run verify:gui-smoke -- --timeout-ms 1800000
rust:
name: Rust Quick
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changed
if: ${{ github.event_name == 'pull_request' && (needs.changed.outputs.rust == 'true' || needs.changed.outputs.fallback == 'true' || needs.changed.outputs.workflow == 'true') }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: lime-rs
shared-key: quality-rust-quick
cache-on-failure: true
- name: Rust layer budget
run: node scripts/check-rust-layer-budget.mjs --max-e2e-runnable 0
- name: Unit Rust tests
run: npm run test:rust:unit
rust_full:
name: Rust Full
runs-on: macos-latest
timeout-minutes: 30
needs: changed
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changed.outputs.docs_only != 'true') || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: lime-rs
shared-key: quality-rust-full
cache-on-failure: true
- name: Prepare sherpa-onnx runtime
shell: bash
run: |
set -euo pipefail
TARGET="$(rustc -vV | sed -n 's/^host: //p')"
test -n "$TARGET"
node scripts/prepare-sherpa-onnx-runtime.mjs --target "$TARGET"
- name: Test Rust workspace
env:
TMPDIR: ${{ runner.temp }}/rust-full
TEMP: ${{ runner.temp }}/rust-full
TMP: ${{ runner.temp }}/rust-full
run: |
mkdir -p "$TMPDIR"
npm run test:rust
windows_shell_runtime:
name: Windows Shell Runtime
runs-on: windows-2022
timeout-minutes: 20
needs: changed
if: ${{ needs.changed.outputs.rust == 'true' || needs.changed.outputs.fallback == 'true' || needs.changed.outputs.workflow == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: lime-rs
shared-key: quality-windows-shell-runtime
cache-on-failure: true
- name: Configure sandboxed rusty_v8 artifacts
run: node scripts/lib/rusty-v8-artifacts.mjs --github-env
- name: Check Windows app-server and code-mode-host sidecars
run: cargo check --manifest-path lime-rs/Cargo.toml -p app-server --bin app-server -p tool-runtime --bin code-mode-host
- name: Test Windows shell runtime fallback
run: cargo test --manifest-path lime-rs/Cargo.toml -p lime-agent --test windows_shell_runtime -- --test-threads=1
- name: Test Windows file browser directory creation
run: cargo test --manifest-path lime-rs/Cargo.toml -p lime-services create_directory_creates_nested_directory_from_platform_path -- --test-threads=1
- name: Test Windows Agent Plugin MCP parity
run: cargo test --manifest-path lime-rs/Cargo.toml -p lime-mcp agent_plugin_config --lib -- --test-threads=1
results:
name: Quality results
runs-on: ubuntu-latest
needs:
- changed
- architecture_confirmation
- integrity
- frontend
- frontend_full
- bridge_contracts
- gui_smoke
- rust
- rust_full
- windows_shell_runtime
if: always()
steps:
- name: Summarize
shell: bash
run: |
CHANGED_COUNT='${{ needs.changed.outputs.changed_count }}'
ARCHITECTURE_CONFIRMATION_RESULT='${{ needs.architecture_confirmation.result }}'
DOCS_ONLY='${{ needs.changed.outputs.docs_only }}'
BRIDGE_REASONS='${{ needs.changed.outputs.bridge_reasons }}'
INTEGRITY_RESULT='${{ needs.integrity.result }}'
FRONTEND_RESULT='${{ needs.frontend.result }}'
FRONTEND_FULL_RESULT='${{ needs.frontend_full.result }}'
BRIDGE_RESULT='${{ needs.bridge_contracts.result }}'
GUI_SMOKE_RESULT='${{ needs.gui_smoke.result }}'
RUST_RESULT='${{ needs.rust.result }}'
RUST_FULL_RESULT='${{ needs.rust_full.result }}'
WINDOWS_SHELL_RESULT='${{ needs.windows_shell_runtime.result }}'
FALLBACK='${{ needs.changed.outputs.fallback }}'
WORKFLOW='${{ needs.changed.outputs.workflow }}'
INTEGRITY_REQUIRED='${{ needs.changed.outputs.integrity }}'
FRONTEND_REQUIRED='${{ needs.changed.outputs.frontend }}'
BRIDGE_REQUIRED='${{ needs.changed.outputs.bridge }}'
GUI_SMOKE_REQUIRED='${{ needs.changed.outputs.gui_smoke }}'
RUST_REQUIRED='${{ needs.changed.outputs.rust }}'
IS_MAIN_PUSH="false"
IS_WORKFLOW_DISPATCH="false"
if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${GITHUB_REF:-}" == "refs/heads/main" ]]; then
IS_MAIN_PUSH="true"
fi
if [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then
IS_WORKFLOW_DISPATCH="true"
fi
FRONTEND_QUICK_REQUIRED="false"
RUST_QUICK_REQUIRED="false"
MAIN_FULL_REQUIRED="false"
if [[ "${DOCS_ONLY}" != "true" ]]; then
if [[ "${IS_MAIN_PUSH}" == "true" || "${IS_WORKFLOW_DISPATCH}" == "true" ]]; then
MAIN_FULL_REQUIRED="true"
else
if [[ "${FRONTEND_REQUIRED}" == "true" || "${FALLBACK}" == "true" || "${WORKFLOW}" == "true" ]]; then
FRONTEND_QUICK_REQUIRED="true"
fi
if [[ "${RUST_REQUIRED}" == "true" || "${FALLBACK}" == "true" || "${WORKFLOW}" == "true" ]]; then
RUST_QUICK_REQUIRED="true"
fi
fi
fi
BRIDGE_REASONS_DISPLAY="${BRIDGE_REASONS//,/、}"
if [[ -z "${BRIDGE_REASONS_DISPLAY}" ]]; then
BRIDGE_REASONS_DISPLAY="无"
fi
echo "changed_count: ${CHANGED_COUNT}"
echo "architecture_confirmation: ${ARCHITECTURE_CONFIRMATION_RESULT}"
echo "docs_only : ${DOCS_ONLY}"
echo "bridge_reasons: ${BRIDGE_REASONS}"
echo "integrity : ${INTEGRITY_RESULT}"
echo "frontend_quick: ${FRONTEND_RESULT}"
echo "frontend_full : ${FRONTEND_FULL_RESULT}"
echo "bridge : ${BRIDGE_RESULT}"
echo "gui_smoke : ${GUI_SMOKE_RESULT}"
echo "rust_quick : ${RUST_RESULT}"
echo "rust_full : ${RUST_FULL_RESULT}"
echo "windows_shell_runtime: ${WINDOWS_SHELL_RESULT}"
FINAL_STATUS="success"
FAILURE_REASON=""
NEXT_ACTION="No extra action required."
if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" && "${ARCHITECTURE_CONFIRMATION_RESULT}" != "success" ]]; then
SUMMARY_NOTE="Architecture confirmation is required for every pull request."
FINAL_STATUS="failed"
FAILURE_REASON="architecture confirmation failed"
elif [[ "${DOCS_ONLY}" == 'true' ]]; then
SUMMARY_NOTE="Only docs/markdown changes detected -> quality checks not required."
NEXT_ACTION="No action required for docs-only changes."
else
SUMMARY_NOTE="Quality checks evaluated against changed areas."
if [[ -z "${FAILURE_REASON}" && ("${INTEGRITY_REQUIRED}" == 'true' || "${FALLBACK}" == 'true' || "${WORKFLOW}" == 'true') ]]; then
if [[ "${INTEGRITY_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="integrity failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && "${FRONTEND_QUICK_REQUIRED}" == 'true' ]]; then
if [[ "${FRONTEND_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="frontend quick failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && ("${BRIDGE_REQUIRED}" == 'true' || "${FALLBACK}" == 'true' || "${WORKFLOW}" == 'true') ]]; then
if [[ "${BRIDGE_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="bridge/contracts failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && ("${GUI_SMOKE_REQUIRED}" == 'true' || "${FALLBACK}" == 'true' || "${WORKFLOW}" == 'true') ]]; then
if [[ "${GUI_SMOKE_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="gui smoke failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && "${RUST_QUICK_REQUIRED}" == 'true' ]]; then
if [[ "${RUST_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="rust quick failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && "${MAIN_FULL_REQUIRED}" == 'true' ]]; then
if [[ "${FRONTEND_FULL_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="frontend full failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && "${MAIN_FULL_REQUIRED}" == 'true' ]]; then
if [[ "${RUST_FULL_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="rust full failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && "${MAIN_FULL_REQUIRED}" == 'true' ]]; then
if [[ "${GUI_SMOKE_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="gui smoke failed"
fi
fi
if [[ -z "${FAILURE_REASON}" && ("${RUST_REQUIRED}" == 'true' || "${FALLBACK}" == 'true' || "${WORKFLOW}" == 'true') ]]; then
if [[ "${WINDOWS_SHELL_RESULT}" != 'success' ]]; then
FINAL_STATUS="failed"
FAILURE_REASON="windows shell runtime failed"
fi
fi
if [[ -n "${FAILURE_REASON}" ]]; then
if [[ "${FAILURE_REASON}" == "integrity failed" ]]; then
NEXT_ACTION='Run `npm run verify:app-version` and sync package / Cargo / Electron version files.'
elif [[ "${FAILURE_REASON}" == "architecture confirmation failed" ]]; then
NEXT_ACTION='Complete the architecture section in the PR description and update `internal/aiprompts/architecture.md` when the change is major.'
elif [[ "${FAILURE_REASON}" == "frontend quick failed" ]]; then
NEXT_ACTION='Run `npm run lint && npm run typecheck && node scripts/check-vitest-layer-budget.mjs --max-component-candidates 37 && npm run test:unit && npm run test:contract` to reproduce the frontend quick failure locally.'
elif [[ "${FAILURE_REASON}" == "frontend full failed" ]]; then
NEXT_ACTION='Run `node scripts/check-vitest-layer-budget.mjs --max-component-candidates 37 && npm run test:frontend:all` after `npm run lint && npm run typecheck` to reproduce the full frontend failure locally.'
elif [[ "${FAILURE_REASON}" == "bridge/contracts failed" ]]; then
if [[ ",${BRIDGE_REASONS}," == *",harness_cleanup_contract,"* ]]; then
NEXT_ACTION='Run `npm run harness:cleanup-report:check` first, then `npm run test:contracts`.'
elif [[ ",${BRIDGE_REASONS}," == *",bridge_runtime,"* ]]; then
NEXT_ACTION='Run `npm run test:bridge` first, then `npm run test:contracts`.'
else
NEXT_ACTION='Run `npm run verify:local` or `npm run test:bridge && npm run test:contracts` to reproduce the bridge/contracts failure.'
fi
elif [[ "${FAILURE_REASON}" == "gui smoke failed" ]]; then
NEXT_ACTION='Run `npm run verify:gui-smoke -- --timeout-ms 900000` and inspect Electron Desktop Host / DevBridge readiness.'
elif [[ "${FAILURE_REASON}" == "rust quick failed" ]]; then
NEXT_ACTION='Run `node scripts/check-rust-layer-budget.mjs --max-e2e-runnable 0 && npm run test:rust:unit` to reproduce the Rust unit-layer failure locally.'
elif [[ "${FAILURE_REASON}" == "windows shell runtime failed" ]]; then
NEXT_ACTION='Run `cargo test --manifest-path lime-rs/Cargo.toml -p lime-agent --test windows_shell_runtime -- --test-threads=1` on Windows and inspect PowerShell PATH fallback.'
elif [[ "${FAILURE_REASON}" == "rust full failed" ]]; then
NEXT_ACTION='Run `npm run test:rust` to reproduce the Rust workspace failure locally.'
fi
fi
fi
{
echo "## Quality Summary"
echo
echo "**Final status:** ${FINAL_STATUS}"
echo
echo "### Scope"
echo
echo "- changed_count: ${CHANGED_COUNT}"
echo "- docs_only: ${DOCS_ONLY}"
echo "- bridge_reasons: ${BRIDGE_REASONS_DISPLAY}"
echo
echo "### Required Gates"
echo
echo "| Gate | Required | Result |"
echo "| --- | --- | --- |"
echo "| architecture_confirmation | PR only | ${ARCHITECTURE_CONFIRMATION_RESULT} |"
echo "| integrity | ${INTEGRITY_REQUIRED} | ${INTEGRITY_RESULT} |"
echo "| frontend_quick | ${FRONTEND_QUICK_REQUIRED} | ${FRONTEND_RESULT} |"
echo "| frontend_full | ${MAIN_FULL_REQUIRED} | ${FRONTEND_FULL_RESULT} |"
echo "| bridge_contracts | ${BRIDGE_REQUIRED} | ${BRIDGE_RESULT} |"
echo "| gui_smoke | ${GUI_SMOKE_REQUIRED} / main_full=${MAIN_FULL_REQUIRED} | ${GUI_SMOKE_RESULT} |"
echo "| rust_quick | ${RUST_QUICK_REQUIRED} | ${RUST_RESULT} |"
echo "| rust_full | ${MAIN_FULL_REQUIRED} | ${RUST_FULL_RESULT} |"
echo "| windows_shell_runtime | ${RUST_REQUIRED} | ${WINDOWS_SHELL_RESULT} |"
echo
echo "### Notes"
echo
echo "- ${SUMMARY_NOTE}"
echo
echo "### Recommended Next Action"
echo
echo "- ${NEXT_ACTION}"
if [[ -n "${FAILURE_REASON}" ]]; then
echo
echo "### Failure"
echo
echo "- ${FAILURE_REASON}"
fi
} >> "$GITHUB_STEP_SUMMARY"
if [[ "${DOCS_ONLY}" == 'true' && -z "${FAILURE_REASON}" ]]; then
echo "${SUMMARY_NOTE}"
exit 0
fi
if [[ -n "${FAILURE_REASON}" ]]; then
echo "${FAILURE_REASON}"
exit 1
fi