# ============================================================ # APS 构建矩阵 CI(矩阵 118 · CI 构建矩阵门禁) # push / PR 触发:lint(ruff,基线有存量告警,informational) # × Windows + Ubuntu 后端黄金测试(隔离 APS_KNOWLEDGE_PATH) # × Windows + Ubuntu 前端 type+build # × E2E(preview 生产构建 + Chromium,JMS 登录依赖外网) # × SBOM(npm run sbom,CycloneDX 1.6) # × node-tests(Ubuntu + Windows):updater/sidecar/packaging-smoke(node --test,npm run test:node) # Windows 额外:sidecar 本地冒烟 + offline install verify(产物存在时)+ Defender best-effort # 签名等外部项不在本工作流(需签名证书,见 docs/development/e2e.md);Defender 无 MpCmdRun 时跳过(退出码 2) # ============================================================ name: CI on: push: pull_request: concurrency: group: ci-${{ github.ref }} cancel-in-progress: true env: PYTHONUTF8: '1' NODE_VERSION: '22' PYTHON_VERSION: '3.13' jobs: lint: name: lint (ruff) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install ruff run: python -m pip install --upgrade pip ruff # 基线:server/tests/scripts 存量 55 项 ruff 告警(round-41 记录)。 # 本步 informational(continue-on-error),不阻断构建;清零后改回硬门禁。 - name: ruff check(informational,不阻断) continue-on-error: true run: ruff check --output-format=github server/ tests/ scripts/ || true frontend-build: name: frontend build (tsc + vite) · ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: npm ci working-directory: apps/web run: npm ci - name: build(tsc -b && vite build) working-directory: apps/web run: npm run build backend-tests: name: backend golden tests · ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} env: # 隔离运行数据:绝不触碰仓库 server/data 或开发者 ~/.aps APS_HOME: ${{ runner.temp }}/aps-ci/home APS_DB_PATH: ${{ runner.temp }}/aps-ci/master.db APS_KNOWLEDGE_PATH: ${{ runner.temp }}/aps-ci/knowledge APS_APPROVAL_PATH: ${{ runner.temp }}/aps-ci/approval APS_WORLD_PATH: ${{ runner.temp }}/aps-ci/world.json APS_AUTH_PROVIDER: unconfigured APS_LICENSE_PROVIDER: unconfigured LLM_PROVIDER: '' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install deps run: python -m pip install -r requirements.txt pytest pytest-asyncio - name: golden tests(隔离 APS_KNOWLEDGE_PATH) run: python -m pytest tests/golden -q -p no:cacheprovider e2e: name: frontend E2E smoke (preview + Chromium) runs-on: ubuntu-latest env: E2E_MODE: preview E2E_FRONTEND_PORT: '43117' E2E_BASE_URL: http://localhost:43117 E2E_API_TARGET: http://127.0.0.1:8100 E2E_OUTPUT_DIR: ${{ runner.temp }}/aps-e2e-artifacts APS_HOME: ${{ runner.temp }}/aps-e2e/home APS_DB_PATH: ${{ runner.temp }}/aps-e2e/master.db APS_KNOWLEDGE_PATH: ${{ runner.temp }}/aps-e2e/knowledge APS_APPROVAL_PATH: ${{ runner.temp }}/aps-e2e/approval APS_WORLD_PATH: ${{ runner.temp }}/aps-e2e/world.json APS_AUTH_PROVIDER: jms APS_AUTH_COOKIE_SECURE: '0' APS_AUTH_TTL_SECONDS: '28800' APS_APPROVAL_BACKEND: file APS_CORS_ORIGINS: http://localhost:43117,http://127.0.0.1:43117 JMS_AUTH_BASE_URL: ${{ vars.APS_E2E_JMS_BASE_URL }} JMS_AUTH_SESSION_SECRET: ${{ secrets.APS_JMS_SESSION_SECRET }} JMS_AUTH_TIMEOUT_SECONDS: '15' LLM_PROVIDER: '' # 若 GitHub 运行器无法访问 JMS(外网受限),设置仓库变量 APS_E2E_SKIP_JMS=1 只跑离线访客冒烟 APS_E2E_SKIP_JMS: ${{ vars.APS_E2E_SKIP_JMS }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install backend deps run: python -m pip install -r requirements.txt - name: frontend deps working-directory: apps/web run: npm ci - name: build frontend(preview 模式依赖 dist) working-directory: apps/web run: npm run build - name: install chromium working-directory: apps/web run: npx playwright install --with-deps chromium - name: start backend(独立端口 8100,隔离数据) run: | python -m uvicorn server.main:app --host 127.0.0.1 --port 8100 > /tmp/aps-e2e-backend.log 2>&1 & for i in $(seq 1 30); do if curl -sf http://127.0.0.1:8100/api/health > /dev/null; then break; fi sleep 2 done curl -sf http://127.0.0.1:8100/api/health || (echo 'backend failed to start'; tail -200 /tmp/aps-e2e-backend.log; exit 1) - name: run E2E smoke working-directory: apps/web run: npx playwright test --project=chromium - name: upload playwright artifacts if: failure() uses: actions/upload-artifact@v4 with: name: playwright-e2e-artifacts path: ${{ runner.temp }}/aps-e2e-artifacts if-no-files-found: ignore sbom: name: SBOM(CycloneDX 1.6 + SHA-256) runs-on: ubuntu-latest env: PYTHONUTF8: '1' steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install uv(提供 uvx 供 sidecar-python BOM) run: python -m pip install uv - name: npm run sbom run: npm run sbom - name: verify SBOM outputs run: | ls -la dist/sbom/ test -s dist/sbom/web-node.cdx.json test -s dist/sbom/desktop-node.cdx.json test -s dist/sbom/sidecar-python.cdx.json test -s dist/sbom/release-manifest.json node-tests: name: node tests(updater/sidecar/packaging-smoke + Win 冒烟)· ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} # 根目录 npm run test:node = node --test apps/desktop/test/*.test.cjs + tests/node/packaging-smoke.test.mjs # (updater 22 项 + sidecar 7 项 + packaging-smoke 12 项),纯 node 内置模块,无需 npm install - name: node --test(updater + sidecar + packaging-smoke) run: npm run test:node # Windows 额外门禁:sidecar 本地冒烟(server/sidecar.py 纯 stdlib,~72s) - name: setup python(sidecar 冒烟用) if: runner.os == 'Windows' uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: sidecar 本地冒烟(20 冷启动/端口冲突/崩溃重启/中文路径) if: runner.os == 'Windows' run: node scripts/smoke-sidecar-local.mjs # offline install verify:本 job 不构建安装包,产物存在时才校验;缺失则跳过 - name: offline install verify(产物存在时) if: runner.os == 'Windows' shell: pwsh run: | if (Test-Path "build/offline-check/offline-install-manifest.json") { node scripts/offline-install-check.mjs --verify } else { Write-Host "offline manifest 不存在(本 job 不构建安装包)— 跳过 verify" } # Defender 扫描 best-effort:无 MpCmdRun(退出码 2)或无可扫描产物(退出码 3)视为跳过; # 检出威胁/扫描失败(退出码 1)才阻断。产物由桌面构建流程生成,本 job 通常跳过。 - name: Defender 扫描(best-effort,退出码 2/3 跳过) if: runner.os == 'Windows' shell: pwsh run: | node scripts/defender-scan.mjs $code = $LASTEXITCODE if ($code -eq 0) { Write-Host "Defender: PASS"; exit 0 } if ($code -eq 2) { Write-Host "Defender: SKIP(MpCmdRun 不可用)"; exit 0 } if ($code -eq 3) { Write-Host "Defender: SKIP(无可扫描产物)"; exit 0 } Write-Host "Defender: FAIL(exit $code)" exit $code