diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 639d267c4..6c22d74e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,8 +73,6 @@ jobs: tests/test_architecture_egress.py \ tests/test_architecture_event_facts.py \ tests/test_architecture_event_policy.py -q - uv run --locked --no-sync python \ - scripts/architecture/event_policy.py - name: Check host architecture snapshot run: | @@ -84,6 +82,30 @@ jobs: uv run --locked --no-sync python \ scripts/architecture/baseline.py --check-host + # 与 architecture job 并行执行,两者合计即完整的宿主架构门禁 + architecture-ratchets: + needs: reuse + if: needs.reuse.outputs.reused != 'true' + runs-on: ubuntu-latest + name: Architecture Ratchet Gate + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Set up uv + uses: astral-sh/setup-uv@v10.0.1 + with: + python-version: '3.14' + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + + - name: Install dependencies + run: uv sync --locked + - name: Check governed Python types run: uv run --locked --no-sync mypy --config-file mypy.ini @@ -215,8 +237,8 @@ jobs: proof: name: CI proof (${{ github.sha }}) - needs: [architecture, coverage-shard, coverage-report] - if: github.event_name == 'pull_request' && needs.architecture.result == 'success' && needs.coverage-shard.result == 'success' && needs.coverage-report.result == 'success' + needs: [architecture, architecture-ratchets, coverage-shard, coverage-report] + if: github.event_name == 'pull_request' && needs.architecture.result == 'success' && needs.architecture-ratchets.result == 'success' && needs.coverage-shard.result == 'success' && needs.coverage-report.result == 'success' runs-on: ubuntu-latest timeout-minutes: 1 steps: diff --git a/AGENTS.md b/AGENTS.md index af23d6f9f..9b639520a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,7 +155,6 @@ uv run --locked --no-sync pytest \ tests/test_architecture_egress.py \ tests/test_architecture_event_facts.py \ tests/test_architecture_event_policy.py -q -uv run --locked --no-sync python scripts/architecture/event_policy.py uv run --locked --no-sync pytest \ tests/test_architecture_contract_baseline.py \ tests/test_architecture_baseline_cli.py -q diff --git a/docs/testing.md b/docs/testing.md index bc0d9a9a1..a77b0bdce 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -186,7 +186,7 @@ def test_recognize_prefers_explicit_identity(sample_meta, monkeypatch): - **合并检查复用**:单测/架构与 Pylint 工作流各自保留 PR、push 和手动入口。PR 完整通过全部门禁后,末尾 `CI proof ()` job 记录实际验证的模拟合并提交。合并 push 只在同一工作流的最新 PR 运行完整成功、证明成功、代码树完全相同、模拟合并父提交分别等于 push 前的目标分支和 PR head 时跳过重复门禁。直接 push、强制 push、基线变化、旧工作流缺少证明、失败/未完成运行或 API 异常都执行全量;不依赖提交消息,也不把 PR 的 head SHA 当作测试的合并 SHA。标准 merge/squash/rebase 仅在上述证据一致时复用。GitHub 构建与发布工作流保持独立。 - **去重脚本验证**:`node --test .github/scripts/reuse.test.mjs` 离线覆盖成功复用及保守回退,两个检查工作流均在判定前执行。复用 job 仅持有 contents/actions/pull-requests 读取权限;没有修改分支保护设置。 -- **门禁**:`.github/workflows/test.yml` 在指向 `v3` 的 `pull_request` / `push` 及手动触发时,从 `uv.lock` 同步环境。独立 `architecture` job 先运行宿主依赖、运行契约和基线 CLI 快速门禁;其余测试通过 `coverage run --parallel-mode tests/run.py --exclude-architecture-gate --shard N/6` 分到 6 个 job,一次执行同时验证单测并采集覆盖率;门禁已运行的架构测试不在分片中重复执行。每个分片都有独立进程和临时 `CONFIG_DIR`,不共用 SQLite 或进程级状态,由单一报告 job 合并后检查 Application 与 Domain 的固定 80% 基线。 +- **门禁**:`.github/workflows/test.yml` 在指向 `v3` 的 `pull_request` / `push` 及手动触发时,从 `uv.lock` 同步环境。宿主架构门禁分为两个并行 job,`architecture` 运行宿主依赖、运行契约、事件政策测试和基线快照,`architecture-ratchets` 运行类型、复杂度、并发、Ruff/mypy ratchet 与启动性能检查;其余测试通过 `coverage run --parallel-mode tests/run.py --exclude-architecture-gate --shard N/6` 分到 6 个 job,一次执行同时验证单测并采集覆盖率;门禁已运行的架构测试不在分片中重复执行。每个分片都有独立进程和临时 `CONFIG_DIR`,不共用 SQLite 或进程级状态,由单一报告 job 合并后检查 Application 与 Domain 的固定 80% 基线。 - **跨仓观察**:`.github/workflows/architecture-observe.yml` 每周或手工检出官方插件仓最新 `main`,使用 `--check-plugins` 比较公开导入、Hook 和动态 API 契约。它只上传 `official-plugin-architecture-report.json`,不会自动刷新 fixture;语义变化必须人工审查后显式执行 `--write-plugins`。 - **静态检查**:`.github/workflows/pylint.yml` 对指向 `v3` 的 PR、推送和手工触发运行 Pylint。PR/推送改动到的 Python 文件是硬门禁;`app/` 全量扫描保留为建议性 JSON 构建工件,存量告警不会掩盖或阻塞本次增量治理。 - **PR 本地验证**:按上文「验证范围与维护者安排」选择 contributor 默认检查或已记录的维护者安排,统一处理证据复用与失败归属;需要断点、输出顺序或测试污染诊断时使用 `--serial`。确认受影响路径与零真实出站,准确标注验证范围;本地执行安排不改变 CI 的全量验证与复用合同。 diff --git a/tests/test_architecture_ci.py b/tests/test_architecture_ci.py index b1f499539..caac867b1 100644 --- a/tests/test_architecture_ci.py +++ b/tests/test_architecture_ci.py @@ -28,7 +28,9 @@ def _step_commands(workflow: dict, job_name: str) -> str: def test_unit_test_workflow_has_independent_host_architecture_gate(): """主仓 PR 与推送必须在全量分片外快速执行宿主架构门禁。""" workflow = _load_workflow("test.yml") - commands = _step_commands(workflow, "architecture") + commands = "\n".join( + _step_commands(workflow, job_name) for job_name in ("architecture", "architecture-ratchets") + ) steps = workflow["jobs"]["architecture"]["steps"] semantic_step = next( step for step in steps if step.get("name") == "Check event semantic policy" @@ -47,7 +49,6 @@ def test_unit_test_workflow_has_independent_host_architecture_gate(): assert "tests/test_architecture_dependencies.py" in semantic_step["run"] assert "tests/test_architecture_adapter_imports.py" in semantic_step["run"] assert "tests/test_architecture_egress.py" in semantic_step["run"] - assert "scripts/architecture/event_policy.py" in semantic_step["run"] assert "scripts/architecture/baseline.py" not in semantic_step["run"] assert not { "tests/test_architecture_dependencies.py", @@ -178,7 +179,7 @@ def test_coverage_jobs_parallelize_data_and_keep_one_global_ratchet() -> None: def test_ci_reuse_requires_successful_proof_for_every_gate() -> None: """证明必须依赖全部硬门禁,推送只能通过保守判定跳过昂贵任务。""" for filename, gates in { - "test.yml": ["architecture", "coverage-shard", "coverage-report"], + "test.yml": ["architecture", "architecture-ratchets", "coverage-shard", "coverage-report"], "pylint.yml": ["pylint"], }.items(): jobs = _load_workflow(filename)["jobs"]