Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 72 additions & 38 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ permissions:
concurrency:
# A merged PR's closed event uses main as github.ref; keep its original PR lane.
group: python-tests-${{ github.event_name }}-${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('pr-{0}', github.event.number) || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || github.event_name == 'push' }}
# Let active main qualification finish; the default single pending slot keeps
# only its latest successor. PR updates still retire that PR's obsolete head.
queue: single
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}

env:
LOOPX_USAGE_PING: "0"
Expand Down Expand Up @@ -105,8 +108,20 @@ jobs:
retention-days: 7

chat-bundle:
needs: changes
if: needs.changes.outputs.core_tests == 'true' || needs.changes.outputs.python_tests == 'true' || needs.changes.outputs.presentation_tests == 'true'
needs: [changes, kernel-static-checks]
# This is the sole admission path to expensive qualification. A client-only
# exemption requires the static lane to be skipped; failed full candidates
# continue only in the existing scheduled/manual diagnostic runs.
if: >-
${{ !cancelled() && needs.changes.result == 'success' &&
(needs.changes.outputs.core_tests == 'true' ||
needs.changes.outputs.python_tests == 'true' ||
needs.changes.outputs.presentation_tests == 'true') &&
((needs.changes.outputs.backend_tests == 'false' && needs.kernel-static-checks.result == 'skipped') ||
(needs.changes.outputs.backend_tests == 'true' &&
(needs.kernel-static-checks.result == 'success' ||
((github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
needs.kernel-static-checks.result == 'failure')))) }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -132,7 +147,7 @@ jobs:

chat-bundle-browser:
needs: [changes, chat-bundle]
if: needs.changes.outputs.core_tests == 'true'
if: needs.changes.outputs.core_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: ubuntu-latest
# The packaged scenario catalog outgrew one 15-minute runner, so the
# catalog is partitioned like dashboard-browser and the companion smokes
Expand Down Expand Up @@ -194,9 +209,43 @@ jobs:
node examples/zcode-persistent-browser-smoke.mjs

kernel-static-checks:
needs: [changes, chat-bundle]
# Reject cheap repository failures before any frontend build or test matrix.
needs: changes
if: needs.changes.outputs.backend_tests == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip

- name: Install test dependencies
run: python -m pip install --disable-pip-version-check -e ".[test]"

- name: Check repository invariants before long tests
run: >-
python -m pytest -q --no-cov
tests/test_stage2c_dependency_lock.py
tests/architecture/test_top_level_module_budget.py

- name: Lint test suite
run: >-
python -m ruff check
tests
loopx/canary
loopx/control_plane
loopx/domain_packs
loopx/presentation

kernel-contracts:
needs: [changes, chat-bundle]
if: needs.changes.outputs.backend_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out repository
Expand Down Expand Up @@ -225,28 +274,13 @@ jobs:
- name: Install test dependencies
run: python -m pip install --disable-pip-version-check -e ".[test]"

- name: Check repository invariants before long tests
run: >-
python -m pytest -q --no-cov
tests/test_stage2c_dependency_lock.py
tests/architecture/test_top_level_module_budget.py

- name: Verify file authority on the core runtime
run: >-
node --no-warnings --experimental-strip-types --test
tests/control_plane_ts/authority_store.test.ts
tests/control_plane_ts/local_authority_runtime.test.ts
tests/control_plane_ts/sqlite_runtime_admission.test.ts

- name: Lint test suite
run: >-
python -m ruff check
tests
loopx/canary
loopx/control_plane
loopx/domain_packs
loopx/presentation

- name: Type-check kernel contracts
run: python -m mypy

Expand All @@ -261,7 +295,7 @@ jobs:
typescript-core:
name: typescript-core (${{ matrix.shard }}/3)
needs: [changes, chat-bundle]
if: needs.changes.outputs.backend_tests == 'true'
if: needs.changes.outputs.backend_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
Expand Down Expand Up @@ -317,7 +351,7 @@ jobs:

typescript-coverage:
needs: [changes, typescript-core]
if: needs.changes.outputs.backend_tests == 'true'
if: needs.changes.outputs.backend_tests == 'true' && !cancelled() && needs.typescript-core.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down Expand Up @@ -356,7 +390,7 @@ jobs:

dashboard-browser:
needs: [changes, chat-bundle]
if: needs.changes.outputs.core_tests == 'true'
if: needs.changes.outputs.core_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
Expand Down Expand Up @@ -431,7 +465,7 @@ jobs:

dashboard-acceptance:
needs: [changes, dashboard-browser]
if: needs.changes.outputs.core_tests == 'true'
if: needs.changes.outputs.core_tests == 'true' && !cancelled() && needs.dashboard-browser.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -458,7 +492,7 @@ jobs:
# Preserve the required check name while exposing independent failure lanes.
# Cancelled main pushes must not retain runner-backed aggregate work.
if: always() && needs.changes.outputs.core_tests == 'true' && (github.event_name != 'push' || !cancelled())
needs: [changes, kernel-static-checks, typescript-coverage, dashboard-acceptance, chat-bundle-browser]
needs: [changes, kernel-static-checks, kernel-contracts, typescript-coverage, dashboard-acceptance, chat-bundle-browser]
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
Expand All @@ -472,8 +506,8 @@ jobs:
run: python scripts/ci/review_gate.py verify-core

node-minimum-compatibility:
needs: changes
if: needs.changes.outputs.backend_tests == 'true'
needs: [changes, chat-bundle]
if: needs.changes.outputs.backend_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: ubuntu-latest
# The full suite runs under this exact Node version in typescript-core.
timeout-minutes: 10
Expand All @@ -497,10 +531,10 @@ jobs:
tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts

node-forward-compatibility:
needs: changes
needs: [changes, chat-bundle]
# Probe the future runtime once per night or on demand, outside the PR/main
# critical path. The supported runtime still runs on every full candidate.
if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && needs.changes.outputs.backend_tests == 'true'
if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && needs.changes.outputs.backend_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
continue-on-error: true
runs-on: ubuntu-latest
# This runs the full core conformance suite. Allow forward-runtime/runner
Expand All @@ -520,14 +554,14 @@ jobs:
npm run test:control-plane

test-shard:
# Keep known-red PRs out of the long-test queue. Non-PR runs still collect
# complete Python diagnostics after checks finish, even when checks fail.
needs: [changes, chat-bundle, checks]
# The bundle admits only statically qualified candidates or explicit
# diagnostics. Python can now overlap TS/browser qualification while its
# six repository-wide slots still bound long-test concurrency.
needs: [changes, chat-bundle]
if: >-
${{ !cancelled() && needs.changes.result == 'success' &&
needs.changes.outputs.python_tests == 'true' &&
needs.chat-bundle.result == 'success' &&
(github.event_name != 'pull_request' || needs.checks.result == 'success') }}
needs.chat-bundle.result == 'success' }}
runs-on: ubuntu-latest
# Share six long-test lanes across current PRs and other workflow runs.
# Do not replace another candidate's pending shard or cancel its active head.
Expand Down Expand Up @@ -667,7 +701,7 @@ jobs:

stage2c-suite:
needs: [changes, chat-bundle]
if: needs.changes.outputs.stage2c_tests == 'true'
if: needs.changes.outputs.stage2c_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
name: stage2c (${{ matrix.suite }} ${{ matrix.shard }})
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -766,7 +800,7 @@ jobs:

windows-powershell:
needs: [changes, chat-bundle]
if: needs.changes.outputs.python_tests == 'true'
if: needs.changes.outputs.python_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: windows-latest
timeout-minutes: 30
steps:
Expand Down Expand Up @@ -812,7 +846,7 @@ jobs:
python -m pytest -q
tests/control_plane/test_cli_output_budget.py
tests/control_plane/test_cli_output_differential.py
-k "stable_budget_fixture or probe_process_failure_preserves_utf8_diagnostics"
-k "stable_budget_fixture or long_path_budget or probe_process_failure_preserves_utf8_diagnostics"

- name: Qualify the native Windows CLI output differential
timeout-minutes: 12
Expand Down Expand Up @@ -941,7 +975,7 @@ jobs:

presentation:
needs: [changes, chat-bundle]
if: needs.changes.outputs.presentation_tests == 'true'
if: needs.changes.outputs.presentation_tests == 'true' && !cancelled() && needs.chat-bundle.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand Down
72 changes: 48 additions & 24 deletions docs/development/ci-impact-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,43 @@ No tests are removed. Without timing history the splitter uses equal weights;
six shards do not establish perfect duration balancing. Each numbered shard
shares its queue across workflow runs without replacing another current head.

Long Python shards now wait for the existing `checks` aggregate. An ordinary
PR enters those queues only after core qualification succeeds, so a known-red
candidate does not occupy the long-test lanes. This changes scheduling, not
qualification: skipped shards after a failed prerequisite still fail `pytest`
and `merge-gate`; a passing full candidate still requires every shard and the
same coverage floor. Main, scheduled, manual and merge-queue runs retain full
Python diagnostics after `checks` finishes, even if it failed, provided the
classification and source bundle succeeded and the workflow was not cancelled.
The tradeoff is extra serial latency for a healthy run in an idle queue, in
exchange for less work behind known failures under contention. `ci:full` still
forces the full profile; use a manual run for complete diagnostics of a failing
PR revision. This does not cancel previously queued runs of older workflows.

长 Python 分片现在先等待现有 `checks` 汇总;普通 PR 仅在核心资格检查成功后
进入长队列,避免已知失败的候选继续占用分片。前置失败导致的跳过仍使 `pytest`
和 `merge-gate` 失败,完整候选仍须通过全部分片与原覆盖率门槛。主干、定时、
手动和 merge queue 在汇总结束后仍执行完整 Python 诊断,即使该汇总失败;
分类和源码构建必须成功,取消的 workflow 不再启动分片。代价是空闲队列中健康
候选的串行等待增加;收益是拥塞时不再给已知失败的 PR 排长测试。`ci:full` 仍强制
完整 profile;需要失败候选的全部诊断时可手动运行其分支。旧 workflow 已排队的
任务不会因此自动取消。
The existing `kernel-static-checks` lane now follows classification directly:
the dependency lock, top-level module budget and existing Ruff scope run before
the Chat build or any heavy matrix. Full PR, main-push and merge-queue candidates
must pass that lane before the source bundle admits downstream work. A client-only
exemption requires its static lane to be explicitly skipped. Scheduled and manual
full runs may continue after a static failure to collect complete diagnostics;
cancelled, missing or unexpectedly skipped prerequisites never grant admission.
The downstream jobs retain explicit cancellation predicates so GitHub's implicit
`success()` does not discard requested diagnostics after a failed ancestor.

Python shards start after the admitted bundle, alongside TypeScript and browser
qualification. The six repository-wide slots and two workers per shard remain.
File-authority tests, mypy and CLI output budgets move to `kernel-contracts` and
remain required by `checks`, as does the static lane. The existing `pytest` and
`merge-gate` still reject any required failure, skip or missing coverage. This
removes the full browser/TypeScript wait from Python's critical path, but allows
up to six already-bounded Python jobs to overlap those lanes; a later browser
failure may now coexist with running Python tests. It neither reserves hosted
capacity nor proves a throughput improvement. `ci:full` selects all qualification,
without overriding failed admission; use a manual run for complete diagnostics
of a failing revision. Older queued workflow versions keep their own policy.

既有 `kernel-static-checks` 现在直接等待分类:依赖锁、顶层模块预算和原有 Ruff
范围在 Chat 构建及全部重型矩阵之前检查。全量 PR、main push 和 merge queue
必须先通过此通道,源码 bundle 才准入下游;纯前端豁免要求静态通道明确 skipped。
定时与手动全量运行可在静态 failure 后继续收集完整诊断;取消、缺失或意外 skipped
均不放行。下游显式保留取消条件,避免 GitHub 隐含的 `success()` 因祖先失败丢弃
本次请求的诊断。

Python 分片在已准入的 bundle 完成后即可与 TypeScript/浏览器并行,仍受六个
仓库级槽位、每片两个 worker 限制。File authority、mypy 与 CLI 输出预算移到
`kernel-contracts`,与静态通道一起继续作为 `checks` 必过项。既有 `pytest` 和
`merge-gate` 仍拒绝必需项失败、跳过或 coverage 缺失。这样缩短 Python 等待全套
前端/TS 的关键路径,但最多六个已有上限的 Python job 会与这些通道重叠;稍后
发生浏览器失败时,Python 可能已经运行。这不预留 hosted 容量,也不直接证明吞吐
改善。`ci:full` 选择全部资格检查,不绕过准入失败;失败版本的完整诊断仍使用手动
运行。旧版 workflow 已排队的任务保留其原有规则。

The shard command explicitly loads `scripts.ci.pytest_timing`. Its controller
streams pytest's setup/call/teardown durations to Actions logs as each report
Expand Down Expand Up @@ -240,13 +256,21 @@ to one and background shard occupancy from five runners to two; the complete
sweep may take longer. The existing fleet-health readback still checks receipts.

Python Tests also runs daily at `20:17 UTC`. Its event-scoped concurrency keeps
that full run separate from main pushes and PRs. New PR heads and main pushes
cancel superseded runs; scheduled, manual and merge-queue qualification is not
cancelled by that policy. Release-artifact, desktop-artifact, signed-desktop,
that full run separate from main pushes and PRs. New PR heads cancel their
superseded runs. Active main qualification finishes; `queue: single` replaces
only the pending main successor with the latest push instead of interrupting
the running candidate or accumulating every intermediate commit. Scheduled,
manual and merge-queue qualification is not cancelled by that policy.
Release-artifact, desktop-artifact, signed-desktop,
DCO and optional Ark PR checks cancel only superseded validation for the same PR.
Signed-desktop PRs use separate groups from each other and from channel publication.
Release publication remains serialized and is never cancelled by a PR update.

Python Tests 每日 `20:17 UTC` 的完整运行与 main push、PR 保持不同并发组。
PR 新 head 仍取消本 PR 的旧运行;正在执行的 main 资格验证会完成,`queue: single`
仅用最新 push 替换等待中的主干后继,不中断运行中的候选,也不积压每个中间提交。
该策略不取消定时、手动和 merge queue 的资格验证;发布仍保持原有串行规则。

Trusted obsolete-PR cleanup runs in the separate **CI Maintenance** workflow on
`pull_request_target` close/synchronize events. It never checks out PR content
and retains the same PR/repository/head checks before cancellation. A green
Expand Down
Loading
Loading