ci: speed up full cross-platform PR validation (#296)
Use pinned nextest for full native tests while retaining doctests, production-feature checks and both macOS release checks. Publish shared dependency snapshots from successful default-branch runs, keep compiled artifacts architecture-specific, and preserve cross-process exclusion for shared theme fixtures. Run the full platform matrix for both draft and ready pull requests without repeating it for readiness-only transitions.
This commit is contained in:
1 parent
fde914bb50
commit
33e7b1d00d
11 files changed
+368
-84
No files matched your search
@@ -0,0 +1,8 @@
|
||||
# 这些现有夹具共享真实设置文件;进程内 Mutex 在 nextest 多进程模型下不再互斥。
|
||||
# 只还原该夹具组的串行合同,其余测试继续使用 runner 的并行度。
|
||||
[test-groups]
|
||||
theme-studio = { max-threads = 1 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'test(gpui_shell::settings_pane::theme_studio_tests::)'
|
||||
test-group = 'theme-studio'
|
||||
@@ -14,6 +14,12 @@ inputs:
|
||||
extra-key:
|
||||
description: Additional inputs for native build tools stored with this target
|
||||
default: ""
|
||||
revision:
|
||||
description: Snapshot suffix; empty uses the source SHA, dependency snapshots use a fixed generation
|
||||
default: ""
|
||||
save-if:
|
||||
description: Allow publishing caches; PR consumers can restore without competing with the default branch
|
||||
default: "true"
|
||||
|
||||
outputs:
|
||||
cache-key:
|
||||
@@ -25,6 +31,9 @@ outputs:
|
||||
cache-matched-key:
|
||||
description: The compiled target cache key actually restored by actions/cache
|
||||
value: ${{ steps.target.outputs.cache-matched-key }}
|
||||
save-enabled:
|
||||
description: Whether the caller permits publishing cache snapshots
|
||||
value: ${{ inputs.save-if }}
|
||||
legacy-fallback-invoked:
|
||||
description: Whether the optional legacy cache fallback action was invoked
|
||||
value: ${{ steps.cache-summary.outputs.legacy-fallback-invoked }}
|
||||
@@ -42,7 +51,7 @@ runs:
|
||||
CACHE_OS: ${{ runner.os }}
|
||||
CACHE_ARCH: ${{ runner.arch }}
|
||||
CACHE_WORKLOAD: ${{ inputs.key }}
|
||||
CACHE_REVISION: ${{ github.sha }}
|
||||
CACHE_REVISION: ${{ inputs.revision || github.sha }}
|
||||
CACHE_MANIFESTS: ${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'rust-toolchain.toml', '**/.cargo/config.toml', '.github/ci-profile.toml') }}${{ inputs.extra-key }}
|
||||
run: |
|
||||
python - <<'PY'
|
||||
@@ -66,8 +75,8 @@ runs:
|
||||
("CACHE_OS", "CACHE_ARCH", "CACHE_WORKLOAD"))
|
||||
prefix += "-" + fingerprint + "-"
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
|
||||
# Immutable caches need a new entry after a source change, including
|
||||
# when the prior revision failed before it compiled every profile.
|
||||
# 发布任务按源码刷新;原生 CI 可固定代次,只在依赖或工具链变化后新建快照。
|
||||
# 缓存命中只复用编译输入,调用方仍须执行当前提交的全部检查。
|
||||
output.write("key=" + prefix + os.environ["CACHE_MANIFESTS"] + "-" + os.environ["CACHE_REVISION"] + "\n")
|
||||
output.write("manifest-key=" + prefix + os.environ["CACHE_MANIFESTS"] + "\n")
|
||||
output.write("restore-key=" + prefix + "\n")
|
||||
@@ -92,8 +101,11 @@ runs:
|
||||
${{ steps.identity.outputs.cargo-home }}/registry/cache
|
||||
${{ steps.identity.outputs.cargo-home }}/git/db
|
||||
${{ steps.identity.outputs.cargo-home }}/git/checkouts
|
||||
key: cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}
|
||||
restore-keys: cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-
|
||||
# 同一 OS 的两种架构共享源码,编译产物仍按架构隔离。
|
||||
key: cargo-downloads-v3-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
cargo-downloads-v3-${{ runner.os }}-
|
||||
cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-
|
||||
- name: Restore compiled targets
|
||||
id: target
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -127,6 +139,7 @@ runs:
|
||||
CACHE_WORKLOAD: ${{ inputs.key }}
|
||||
RUNNER_OS_VALUE: ${{ runner.os }}
|
||||
RUNNER_ARCH_VALUE: ${{ runner.arch }}
|
||||
CACHE_SAVE_ENABLED: ${{ inputs.save-if }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -148,6 +161,7 @@ runs:
|
||||
echo "- Target cache exact hit: \`$target_cache_hit\`"
|
||||
echo "- Target cache matched key: \`$target_cache_matched_key\`"
|
||||
echo "- Save key: \`$save_key\`"
|
||||
echo "- Cache publishing enabled: \`$CACHE_SAVE_ENABLED\`"
|
||||
echo "- Legacy fallback configured: \`$legacy_configured\`"
|
||||
echo "- Legacy fallback invoked: \`$legacy_invoked\`"
|
||||
echo "- Legacy fallback outcome: \`$legacy_outcome\`"
|
||||
@@ -159,14 +173,27 @@ runs:
|
||||
echo "legacy-fallback-outcome=$legacy_outcome"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Populate complete downloads before sharing the cache
|
||||
if: steps.downloads.outputs.cache-hit != 'true'
|
||||
if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_OS: ${{ runner.os }}
|
||||
run: |
|
||||
host_target="$(rustc -vV | sed -n 's/^host: //p')"
|
||||
test -n "$host_target"
|
||||
cargo fetch --locked --target "$host_target"
|
||||
# 缓存由先完成的架构写入,必须预取两者的源码,不能把半份下载标成精确命中。
|
||||
case "$CACHE_OS" in
|
||||
Windows)
|
||||
cargo fetch --locked --target x86_64-pc-windows-msvc --target aarch64-pc-windows-msvc
|
||||
;;
|
||||
macOS)
|
||||
cargo fetch --locked --target x86_64-apple-darwin --target aarch64-apple-darwin
|
||||
;;
|
||||
*)
|
||||
host_target="$(rustc -vV | sed -n 's/^host: //p')"
|
||||
test -n "$host_target"
|
||||
cargo fetch --locked --target "$host_target"
|
||||
;;
|
||||
esac
|
||||
- name: Save complete shared Cargo downloads
|
||||
if: steps.downloads.outputs.cache-hit != 'true'
|
||||
if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -11,14 +11,15 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Include docs, catalogs, assets and workflow edits: every PR gets test results.
|
||||
# ready_for_review starts the scarce-runner jobs that draft pushes skip.
|
||||
# 草稿已跑完整矩阵,转为 Ready 而没有新提交时不重复申请七个原生 runner。
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
types: [opened, synchronize, reopened]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: native-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||
# main 的过期验证也应释放 runner;手动诊断和发布调用不互相取消。
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -75,9 +76,7 @@ jobs:
|
||||
--output "$GITHUB_OUTPUT"
|
||||
|
||||
# The required lint job validates the matrix before heavy runners are
|
||||
# requested. Draft PRs omit deferred platforms entirely; ready PRs and
|
||||
# release callers require the full matrix. Deferred checks remain missing
|
||||
# on drafts instead of being reported as successful tests.
|
||||
# requested. Every PR, including a draft, requires the complete matrix.
|
||||
native-tests:
|
||||
needs: lint
|
||||
if: ${{ inputs.windows_diagnostics_run == '' }}
|
||||
@@ -131,6 +130,10 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0 -or $compiler -notcontains 'host: aarch64-pc-windows-msvc') {
|
||||
throw 'The Windows ARM job must compile and run ARM64 tests.'
|
||||
}
|
||||
- name: Install the pinned native test runner
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-nextest@0.9.146
|
||||
- uses: ./.github/actions/rust-cache
|
||||
id: rust-cache
|
||||
with:
|
||||
@@ -139,7 +142,9 @@ jobs:
|
||||
target: |
|
||||
target/ci
|
||||
${{ runner.os != 'macOS' && 'target/release' || '' }}
|
||||
legacy-key: ${{ runner.os == 'Windows' && 'windows' || runner.os == 'Linux' && 'linux' || 'macos' }}
|
||||
# PR merge-ref 缓存不与其他 PR 共享;只由默认分支维护可复用的依赖快照。
|
||||
revision: dependencies-v1
|
||||
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
- name: Prepare pinned Windows console runtime
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: pwsh
|
||||
@@ -148,7 +153,7 @@ jobs:
|
||||
./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost -Architecture $architecture
|
||||
./scripts/tests/windows-runtime.tests.ps1 -Architecture $architecture
|
||||
- name: Test complete workspace and native harness
|
||||
run: python scripts/ci_native_tests.py
|
||||
run: python scripts/ci_native_tests.py --runner nextest
|
||||
- name: Check source architecture
|
||||
if: ${{ !cancelled() && runner.os == 'Linux' }}
|
||||
run: python scripts/check_architecture.py
|
||||
@@ -159,7 +164,7 @@ jobs:
|
||||
if: ${{ !cancelled() && runner.os != 'macOS' }}
|
||||
run: cargo check --locked --workspace --release
|
||||
- name: Save compiled workload
|
||||
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
if: ${{ success() && steps.rust-cache.outputs.save-enabled == 'true' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
@@ -214,11 +219,12 @@ jobs:
|
||||
with:
|
||||
key: native-release-check
|
||||
target: target/release
|
||||
legacy-key: macos
|
||||
revision: dependencies-v1
|
||||
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
- name: Check the release workspace
|
||||
run: cargo check --locked --workspace --release --timings
|
||||
- name: Save compiled workload
|
||||
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
if: ${{ success() && steps.rust-cache.outputs.save-enabled == 'true' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/release
|
||||
|
||||
+13
-8
@@ -130,13 +130,18 @@ compile check into a claim that UI tests or a packaged application were run.
|
||||
`Tests (<os>)` jobs and both `Release workspace (<os>)` jobs are required checks
|
||||
on `main`, together with Code Owner approval; see the
|
||||
[activation checklist](docs/project-constraints.md#server-side-activation).
|
||||
Open pull requests as drafts while iterating: drafts run Linux, Windows x64 and
|
||||
Apple Silicon tests plus the Apple Silicon release-profile compile check. The
|
||||
required lint job validates the event and selects the matrix before requesting
|
||||
platform runners. Drafts omit Intel Mac and Windows ARM64 jobs entirely, so their
|
||||
required contexts may be missing until the PR is marked ready. Ready PRs run the
|
||||
full matrix; every required check must succeed before merging. The ready event
|
||||
still reruns core validation rather than reusing an earlier check conclusion.
|
||||
Draft and ready pull requests both run all five native platforms and both macOS
|
||||
release-profile compile checks. The required lint job validates the event and
|
||||
selects the matrix before requesting platform runners. Every required check must
|
||||
succeed before merging. Marking a draft ready without changing its commits does
|
||||
not repeat the matrix; source updates and reopen events run it again. Native CI
|
||||
uses pinned `cargo-nextest` for unit and
|
||||
integration tests, followed by `cargo test --doc` with the same workspace features;
|
||||
the production feature graph and release workspace are still checked separately.
|
||||
PRs restore Cargo caches without uploading merge-ref snapshots. The default branch
|
||||
publishes reusable snapshots per dependency/toolchain configuration; a cache hit
|
||||
never skips current-commit tests. Downloads are shared between architectures of
|
||||
the same OS, while compiled targets remain isolated by architecture and SDK.
|
||||
Local hooks are convenient, but bypassable; they are not the enforcement boundary.
|
||||
Submitting a workflow or `CODEOWNERS` file does not configure server-side rules.
|
||||
|
||||
@@ -150,7 +155,7 @@ feature work; there is no routine `--skip-architecture` option.
|
||||
|
||||
- 先读架构图、工程合同和决策记录;按职责拆分,不按行号切片。
|
||||
- 一个 PR 只做一件事;改动超过 1500 行源码(不计文档、lockfile、资源)`pr-size` 会失败,请拆分。
|
||||
- Draft PR 先运行必需的格式检查和矩阵规划,再创建 Linux / Windows x64 / Apple Silicon 测试及 Apple Silicon release 编译检查;不创建 Intel Mac / Windows ARM64 任务。Ready 后运行完整矩阵,十项必需检查全绿才能合并。
|
||||
- Draft 和 Ready PR 都先运行必需的格式检查和矩阵规划,再执行五平台原生测试及两项 macOS release 编译检查;十项必需检查全绿才能合并。提速使用共享缓存与并行测试,不省略平台或 doctest。
|
||||
- 2000 行是现有仓库的防灾上限,800 行只提示审查,不是“大厂标准”。
|
||||
- 普通功能 PR 不得增加存量债务;有问题的规则可以修订,但要有反例、测试和维护者审批。
|
||||
- 新增核心抽象、依赖方向、持久化或线程模型改变要先说明设计,不强迫每个小修复写 ADR。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Plan the native matrix before requesting runners
|
||||
|
||||
Draft coverage policy superseded by [shared native PR caches](2026-09-25-shared-native-pr-cache.md).
|
||||
|
||||
## Status
|
||||
|
||||
Implemented for review. Preserves the existing ten required check names and
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Speed up native PR checks without dropping platform coverage
|
||||
|
||||
## Status
|
||||
|
||||
Implemented for review. The five native check names and two macOS release-check
|
||||
names are unchanged. Hosted-runner measurements are pending the independent PR;
|
||||
this change does not modify GitHub rulesets or claim a fixed completion time.
|
||||
|
||||
## Context
|
||||
|
||||
PR speed must improve without omitting native architectures, doctests, production
|
||||
features or release-profile compilation. Draft PRs now have the same coverage as
|
||||
ready PRs. Existing test assertions and failure propagation remain in force.
|
||||
|
||||
## Evidence
|
||||
|
||||
In [run 36119856851](https://github.com/Kuddev/pebrel/actions/runs/36119856851),
|
||||
the Intel Mac job took 23m55s, including 12m59s of test compilation after both
|
||||
cache layers missed. In
|
||||
[run 36084876307](https://github.com/Kuddev/pebrel/actions/runs/36084876307), its
|
||||
queue alone took 109m24s. Queue time and test execution are different bottlenecks.
|
||||
|
||||
Inspected native target snapshots were approximately 1–1.37 GB. Saving a new
|
||||
snapshot for every PR commit consumes repository storage even though a sibling
|
||||
PR cannot restore that merge-ref cache. GitHub documents this scope boundary and
|
||||
the default cache limit in its
|
||||
[dependency caching reference](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching).
|
||||
Default-branch caches can be restored by PRs.
|
||||
|
||||
Nextest 0.9.146 publishes native Windows ARM64 and x64 binaries and a universal
|
||||
macOS binary. Its CLI supports the current Cargo profile and timing options.
|
||||
[Nextest](https://nexte.st/docs/running/) runs unit/integration tests separately
|
||||
but does not replace Cargo's doctest execution.
|
||||
|
||||
The first hosted nextest run, [Linux job 108135574634](https://github.com/Kuddev/pebrel/actions/runs/36154433909/job/108135574634),
|
||||
ran 2,245 tests and exposed six theme-studio fixture conflicts. Those tests already
|
||||
share `lock_theme_studio` because they read and restore the same settings file.
|
||||
An in-process mutex does not protect separate nextest processes. Nextest's
|
||||
[test groups](https://nexte.st/docs/configuration/test-groups/) provide the
|
||||
corresponding cross-process scheduling boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
- The existing validated platform catalog selects all five native platforms and
|
||||
both macOS release checks for draft and ready PRs. Keep the lint prerequisite,
|
||||
unfiltered PR triggers, native Windows ARM compiler assertion and SDK checks.
|
||||
Readiness alone no longer reruns the matrix: its commit already receives full
|
||||
coverage on `opened`/`synchronize`, and `reopened` still revalidates the PR.
|
||||
- Native CI installs pinned nextest, runs the complete workspace without test
|
||||
filters or automatic retries, and explicitly runs `cargo test --doc` with the
|
||||
same profile/features. Keep the independent production-feature check. The
|
||||
script's default remains Cargo for existing local and release callers.
|
||||
- Give the entire existing theme-studio fixture module one nextest group with
|
||||
`max-threads = 1`, including readers and writers. This restores its previous
|
||||
mutual exclusion without serializing other tests or changing assertions.
|
||||
- Native jobs restore caches on PRs but publish only on the default branch.
|
||||
Use a fixed `dependencies-v1` snapshot suffix together with the existing
|
||||
compiler, OS, architecture, workload, SDK, flags and manifest identity. Source
|
||||
changes still run Cargo and all tests; cache hits never substitute results.
|
||||
- Save the immutable native snapshot only after successful checks. A failed
|
||||
first attempt must not permanently publish an incomplete snapshot under the
|
||||
fixed key. Dependency/toolchain changes create new keys; deliberate generation
|
||||
changes can refresh an otherwise unchanged snapshot.
|
||||
- Share Cargo downloads between architectures of the same OS. Before publishing,
|
||||
fetch both supported Windows or macOS targets. Keep compiled targets separate.
|
||||
Read-only PR consumers fetch only what their actual commands require. Restore
|
||||
old per-architecture downloads during the cache transition.
|
||||
- Cancel obsolete main validation runs as well as obsolete PR/merge-group runs.
|
||||
Do not add more macOS runner jobs or change required status contexts.
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- Omit draft, docs-only or platform-specific PR tests: violates the required
|
||||
cross-platform coverage.
|
||||
- Drop doctests or the production feature graph when adopting nextest: they
|
||||
cover contracts not replaced by its unit/integration test execution.
|
||||
- Remove macOS release checks or fold them back into the long native test job:
|
||||
they remain required, and prior execution measurements motivated parallelism.
|
||||
- Cache a new full target directory under every source SHA: duplicates large
|
||||
snapshots while merge-ref scope prevents reuse by other PRs.
|
||||
- Add sccache and incremental compilation together: sccache's Rust cache requires
|
||||
incremental compilation to be disabled and does not cache executable linking.
|
||||
This change leaves the existing tested compiler profile intact.
|
||||
- Retry failing tests until green or copy earlier check conclusions: obscures
|
||||
current-commit failures rather than making validation more efficient.
|
||||
|
||||
## Consequences
|
||||
|
||||
There are still seven native runner jobs for every PR. Drafts request more jobs
|
||||
than the superseded policy, not fewer. Shared caches and parallel test execution
|
||||
target avoidable work; hosted-runner capacity still determines queue latency.
|
||||
|
||||
The first successful default-branch run seeds each new snapshot. A dependency
|
||||
change can still cause a cold build. Release/package callers retain SHA-based
|
||||
target snapshots; this PR does not change their build or publication semantics.
|
||||
|
||||
## Validation
|
||||
|
||||
The existing planner, native-workflow, cache, stable-release, platform-cfg and
|
||||
PR-size contract suites ran 58 tests locally: 57 passed, one POSIX-only summary
|
||||
execution test was skipped on Windows. Tests cover full draft/ready matrices,
|
||||
read-only PR cache behavior, cross-architecture download completeness, retained
|
||||
doctests/features and failure propagation at each command.
|
||||
|
||||
Actionlint 1.7.12 passed for the native workflow. The architecture check passed
|
||||
against the PR base without policy/budget changes. Nextest ran all 71
|
||||
`nebula-settings` tests successfully on Windows; its discovered test names match
|
||||
Cargo's inventory exactly. Rustfmt and the subset's doctest command also passed
|
||||
(that subset has zero doctests). Full native/GPUI execution,
|
||||
cache restore/save behavior and cold/warm timing comparisons are validated by
|
||||
the hosted PR runs, not inferred from the local subset.
|
||||
|
||||
## Supersedes
|
||||
|
||||
The draft coverage reduction in
|
||||
[native matrix planning](2026-09-23-native-matrix-planning.md). Its validated
|
||||
event planning and required lint ownership remain unchanged.
|
||||
|
||||
## Revisit when
|
||||
|
||||
Measured cache hit rates, archive transfer costs, nextest process overhead or
|
||||
runner queue times justify a different bounded cache or test scheduling policy.
|
||||
@@ -3,19 +3,32 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
def native_commands() -> list[list[str]]:
|
||||
def native_commands(runner: str = "cargo") -> list[list[str]]:
|
||||
profile = ["--config", ".github/ci-profile.toml", "--profile", "ci"]
|
||||
workspace = ["--locked", "--workspace", "--features", "nebula/gpui-test-support", "--timings"]
|
||||
if runner == "cargo":
|
||||
tests = [["cargo", "test", *profile, *workspace]]
|
||||
elif runner == "nextest":
|
||||
tests = [
|
||||
[
|
||||
"cargo", "nextest", "run", "--config", ".github/ci-profile.toml",
|
||||
"--cargo-profile", "ci", *workspace, "--no-fail-fast", "--retries", "0",
|
||||
],
|
||||
# nextest 不执行 doctest;显式补跑,提速不改变 workspace 的覆盖范围。
|
||||
["cargo", "test", *profile, *workspace, "--doc"],
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"unknown test runner: {runner}")
|
||||
return [
|
||||
[sys.executable, "-m", "unittest", "discover", "-s", "scripts/tests", "-v"],
|
||||
[sys.executable, "-m", "unittest", "discover", "-s", "scripts/conformance/tests", "-v"],
|
||||
[
|
||||
"cargo", "test", "--locked", *profile, "--workspace",
|
||||
"--features", "nebula/gpui-test-support", "--timings",
|
||||
],
|
||||
*tests,
|
||||
# Link the test graph first: check can reuse compatible compiled
|
||||
# dependencies, while metadata-only check output cannot link the tests.
|
||||
# Keep the actual production feature graph independently checked.
|
||||
@@ -26,8 +39,12 @@ def native_commands() -> list[list[str]]:
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for command in native_commands():
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
# 发布脚本和本地入口保留 cargo 默认值,仅原生 CI 显式选择已安装的 nextest。
|
||||
parser.add_argument("--runner", choices=("cargo", "nextest"), default="cargo")
|
||||
arguments = parser.parse_args(argv)
|
||||
for command in native_commands(arguments.runner):
|
||||
print("Running:", " ".join(command), flush=True)
|
||||
subprocess.run(command, check=True)
|
||||
return 0
|
||||
|
||||
+13
-16
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plan the native CI matrices before GitHub creates runner jobs.
|
||||
|
||||
The output file is the path normally supplied as ``GITHUB_OUTPUT``. Planning
|
||||
is deliberately separate from workflow steps: a draft pull request therefore
|
||||
does not materialize jobs for the scarce platforms.
|
||||
The output file is the path normally supplied as ``GITHUB_OUTPUT``. Validate
|
||||
the event before requesting runners; draft and ready PRs get the same coverage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,17 +18,16 @@ from typing import Any, Sequence
|
||||
@dataclass(frozen=True)
|
||||
class Platform:
|
||||
runner: str
|
||||
draft: bool
|
||||
release_check: bool = False
|
||||
|
||||
|
||||
# One platform catalog owns both draft eligibility and release compilation.
|
||||
# 所有 PR 共用平台清单,避免提速时把草稿的原生覆盖悄悄省略。
|
||||
PLATFORMS = (
|
||||
Platform("ubuntu-24.04", draft=True),
|
||||
Platform("windows-2022", draft=True),
|
||||
Platform("macos-26", draft=True, release_check=True),
|
||||
Platform("windows-11-arm", draft=False),
|
||||
Platform("macos-26-intel", draft=False, release_check=True),
|
||||
Platform("ubuntu-24.04"),
|
||||
Platform("windows-2022"),
|
||||
Platform("macos-26", release_check=True),
|
||||
Platform("windows-11-arm"),
|
||||
Platform("macos-26-intel", release_check=True),
|
||||
)
|
||||
|
||||
FULL_EVENTS = frozenset(
|
||||
@@ -41,11 +39,10 @@ class PlanError(ValueError):
|
||||
"""An event cannot be converted into a safe CI plan."""
|
||||
|
||||
|
||||
def _matrices(draft: bool) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
||||
selected = [platform for platform in PLATFORMS if not draft or platform.draft]
|
||||
def _matrices() -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
||||
return (
|
||||
[{"os": platform.runner} for platform in selected],
|
||||
[{"os": platform.runner} for platform in selected if platform.release_check],
|
||||
[{"os": platform.runner} for platform in PLATFORMS],
|
||||
[{"os": platform.runner} for platform in PLATFORMS if platform.release_check],
|
||||
)
|
||||
|
||||
|
||||
@@ -79,10 +76,10 @@ def plan_matrices(event_name: str, payload: dict[str, Any]) -> tuple[list[dict[s
|
||||
draft = pull_request.get("draft")
|
||||
if not isinstance(draft, bool):
|
||||
raise PlanError("pull_request.draft must be a boolean")
|
||||
return _matrices(draft)
|
||||
return _matrices()
|
||||
|
||||
if event_name in FULL_EVENTS:
|
||||
return _matrices(draft=False)
|
||||
return _matrices()
|
||||
|
||||
supported = sorted(FULL_EVENTS | {"pull_request"})
|
||||
raise PlanError(f"unsupported event name {event_name!r}; expected one of {', '.join(supported)}")
|
||||
|
||||
@@ -108,6 +108,7 @@ class CacheIdentityTests(unittest.TestCase):
|
||||
"Target cache exact hit",
|
||||
"Target cache matched key",
|
||||
"Save key",
|
||||
"Cache publishing enabled",
|
||||
"Legacy fallback invoked",
|
||||
"Legacy fallback outcome",
|
||||
):
|
||||
@@ -149,21 +150,51 @@ class CacheIdentityTests(unittest.TestCase):
|
||||
"GITHUB_OUTPUT": str(root / "outputs"),
|
||||
"TARGET_CACHE_HIT": hit, "TARGET_CACHE_MATCHED_KEY": matched,
|
||||
"SAVE_KEY": "new-save-key", "LEGACY_CONFIGURED": configured,
|
||||
"CACHE_SAVE_ENABLED": "false",
|
||||
"LEGACY_OUTCOME": outcome, "CACHE_WORKLOAD": workload,
|
||||
"RUNNER_OS_VALUE": "Linux", "RUNNER_ARCH_VALUE": "X64",
|
||||
}
|
||||
result = subprocess.run([shutil.which("bash"), "-c", script], env=environment,
|
||||
text=True, capture_output=True, check=False)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
summary = (root / "summary").read_text()
|
||||
outputs = (root / "outputs").read_text()
|
||||
summary = (root / "summary").read_text(encoding="utf-8")
|
||||
outputs = (root / "outputs").read_text(encoding="utf-8")
|
||||
self.assertIn(f"Target cache exact hit: `{hit or 'false'}`", summary)
|
||||
self.assertIn(f"Target cache matched key: `{matched or '<none>'}`", summary)
|
||||
self.assertIn("Cache publishing enabled: `false`", summary)
|
||||
self.assertIn(f"legacy-fallback-invoked={invoked}\n", outputs)
|
||||
self.assertIn(f"legacy-fallback-outcome={outcome}\n", outputs)
|
||||
self.assertIn(workload, summary)
|
||||
self.assertFalse(marker.exists(), "summary values must never execute as shell code")
|
||||
|
||||
def test_dependency_snapshot_revision_preserves_compatibility_boundaries(self):
|
||||
source = self.action_text()
|
||||
self.assertIn("CACHE_REVISION: ${{ inputs.revision || github.sha }}", source)
|
||||
snapshot = self.identity(CACHE_REVISION="dependencies-v1")
|
||||
changed = self.identity(CACHE_REVISION="dependencies-v1", CACHE_MANIFESTS="new-dependencies")
|
||||
self.assertNotEqual(snapshot["key"], changed["key"])
|
||||
self.assertEqual(snapshot["restore-key"], changed["restore-key"])
|
||||
|
||||
def test_source_downloads_share_architectures_but_targets_do_not(self):
|
||||
source = self.action_text()
|
||||
self.assertIn("key: cargo-downloads-v3-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}", source)
|
||||
self.assertIn("cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-", source)
|
||||
fetch = source.split("- name: Populate complete downloads before sharing the cache", 1)[1]
|
||||
for targets in (
|
||||
"--target x86_64-pc-windows-msvc --target aarch64-pc-windows-msvc",
|
||||
"--target x86_64-apple-darwin --target aarch64-apple-darwin",
|
||||
):
|
||||
self.assertIn(f"cargo fetch --locked {targets}", fetch)
|
||||
self.assertNotEqual(self.identity(CACHE_ARCH="X64")["restore-key"],
|
||||
self.identity(CACHE_ARCH="ARM64")["restore-key"])
|
||||
|
||||
def test_read_only_consumers_neither_prefetch_other_architectures_nor_save(self):
|
||||
source = self.action_text()
|
||||
self.assertIn("value: ${{ inputs.save-if }}", source)
|
||||
for step in ("Populate complete downloads before sharing the cache", "Save complete shared Cargo downloads"):
|
||||
body = source.split(f"- name: {step}", 1)[1].split("\n - name:", 1)[0]
|
||||
self.assertIn("if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'", body)
|
||||
|
||||
@unittest.skipUnless(os.name == "nt", "Windows cache migration")
|
||||
def test_windows_migration_preserves_compiler_and_workload_boundaries(self):
|
||||
before = self.identity()
|
||||
|
||||
@@ -13,7 +13,7 @@ from scripts.ci_native_tests import main, native_commands
|
||||
class NativeSuiteTests(unittest.TestCase):
|
||||
def test_required_lint_plans_runners_before_native_jobs(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
|
||||
lint = workflow.split("\n lint:\n", 1)[1].split("\n native-tests:\n", 1)[0]
|
||||
self.assertIn("name: lint", lint)
|
||||
self.assertIn("Test CI contracts before matrix planning", lint)
|
||||
@@ -61,7 +61,7 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
|
||||
def test_packages_run_after_merge_or_manual_dispatch_not_for_prs(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/preview-packages.yml").read_text()
|
||||
workflow = (root / ".github/workflows/preview-packages.yml").read_text(encoding="utf-8")
|
||||
events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
|
||||
triggers = set(re.findall(r"^ ([a-z_]+):", events, re.M))
|
||||
self.assertEqual(triggers, {"push", "workflow_dispatch"})
|
||||
@@ -71,7 +71,7 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
self.assertIn("paths:", push.group(1))
|
||||
# Packaging still needs explicit dispatch to create a public release.
|
||||
self.assertIn("github.event_name == 'workflow_dispatch' && inputs.publish == true", workflow)
|
||||
stable = (root / ".github/workflows/release.yml").read_text()
|
||||
stable = (root / ".github/workflows/release.yml").read_text(encoding="utf-8")
|
||||
stable_events = stable.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
|
||||
self.assertEqual(set(re.findall(r"^ ([a-z_]+):", stable_events, re.M)),
|
||||
{"push", "workflow_dispatch"})
|
||||
@@ -79,7 +79,7 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
|
||||
def test_every_pr_and_merge_group_runs_without_path_exclusions(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
|
||||
events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
|
||||
for event in ("pull_request", "merge_group"):
|
||||
declaration = re.search(rf"^ {event}:(.*?)(?=^ [a-z_]+:|\Z)", events, re.M | re.S)
|
||||
@@ -87,9 +87,9 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
self.assertNotIn("paths", declaration.group(1))
|
||||
self.assertNotIn("branches", declaration.group(1))
|
||||
if event == "pull_request":
|
||||
# ready_for_review starts the scarce-runner jobs that draft
|
||||
# pushes skip; the default activity types must stay listed.
|
||||
self.assertIn("types: [opened, synchronize, reopened, ready_for_review]", declaration.group(1))
|
||||
# Drafts already run the full matrix; readiness alone changes no source.
|
||||
self.assertIn("types: [opened, synchronize, reopened]", declaration.group(1))
|
||||
self.assertNotIn("ready_for_review", declaration.group(1))
|
||||
else:
|
||||
self.assertNotIn("types", declaration.group(1))
|
||||
self.assertIn("branches: [main]", events)
|
||||
@@ -100,14 +100,14 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
|
||||
def test_arm_job_requires_native_execution_and_matching_console_runtime(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("windows-11-arm", workflow)
|
||||
self.assertIn("host: aarch64-pc-windows-msvc", workflow)
|
||||
self.assertIn("OSArchitecture -ne 'Arm64'", workflow)
|
||||
self.assertIn("-Architecture $architecture", workflow)
|
||||
self.assertLess(workflow.index("Prepare pinned Windows console runtime"),
|
||||
workflow.index("Test complete workspace"))
|
||||
preview = (root / ".github/workflows/preview-packages.yml").read_text()
|
||||
preview = (root / ".github/workflows/preview-packages.yml").read_text(encoding="utf-8")
|
||||
windows = preview.split("\n windows:\n", 1)[1].split("\n aggregate:", 1)[0]
|
||||
self.assertNotIn("prepare-windows-runtime.ps1", preview.split("\njobs:", 1)[1].split("\n windows:", 1)[0])
|
||||
self.assertLess(windows.index("prepare-windows-runtime.ps1"),
|
||||
@@ -128,16 +128,75 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
self.assertNotIn("--", command)
|
||||
|
||||
def test_actual_product_feature_graph_is_also_checked(self):
|
||||
checks = [command for command in native_commands() if command[:2] == ["cargo", "check"]]
|
||||
self.assertEqual(len(checks), 1)
|
||||
command = checks[0]
|
||||
self.assertEqual(command[command.index("--features") + 1], "gpui-shell")
|
||||
self.assertEqual(command[command.index("--bin") + 1], "pebrel")
|
||||
for runner in ("cargo", "nextest"):
|
||||
with self.subTest(runner=runner):
|
||||
checks = [command for command in native_commands(runner) if command[:2] == ["cargo", "check"]]
|
||||
self.assertEqual(len(checks), 1)
|
||||
command = checks[0]
|
||||
self.assertEqual(command[command.index("--features") + 1], "gpui-shell")
|
||||
self.assertEqual(command[command.index("--bin") + 1], "pebrel")
|
||||
|
||||
def test_nextest_runs_all_native_targets_and_preserves_doctests(self):
|
||||
commands = native_commands("nextest")
|
||||
tests = [command for command in commands if command[:3] == ["cargo", "nextest", "run"]]
|
||||
docs = [command for command in commands if command[:2] == ["cargo", "test"]]
|
||||
self.assertEqual(len(tests), 1)
|
||||
self.assertEqual(len(docs), 1)
|
||||
self.assertIn("--doc", docs[0])
|
||||
self.assertNotIn("--doc", tests[0])
|
||||
for command, profile_flag in ((tests[0], "--cargo-profile"), (docs[0], "--profile")):
|
||||
self.assertIn("--locked", command)
|
||||
self.assertIn("--workspace", command)
|
||||
self.assertIn("--timings", command)
|
||||
self.assertEqual(command[command.index("--features") + 1], "nebula/gpui-test-support")
|
||||
self.assertEqual(command[command.index(profile_flag) + 1], "ci")
|
||||
self.assertEqual(command[command.index("--config") + 1], ".github/ci-profile.toml")
|
||||
for excluded in ("--exclude", "--lib", "--skip", "--filterset", "--partition", "--no-run"):
|
||||
self.assertNotIn(excluded, command)
|
||||
self.assertIn("--no-fail-fast", tests[0])
|
||||
self.assertEqual(tests[0][tests[0].index("--retries") + 1], "0")
|
||||
self.assertLess(commands.index(tests[0]), commands.index(docs[0]))
|
||||
|
||||
def test_native_workflow_installs_nextest_without_changing_release_callers(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
|
||||
native = workflow.split("\n native-tests:\n", 1)[1].split("\n macos-release-check:\n", 1)[0]
|
||||
install = native.split("- name: Install the pinned native test runner", 1)[1].split(" - uses:", 1)[0]
|
||||
self.assertIn("uses: taiki-e/install-action@v2", install)
|
||||
self.assertIn("tool: cargo-nextest@0.9.146", install)
|
||||
self.assertNotIn("if:", install)
|
||||
self.assertIn("run: python scripts/ci_native_tests.py --runner nextest", native)
|
||||
release = (root / ".github/workflows/release.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("run: python scripts/ci_native_tests.py\n", release)
|
||||
|
||||
def test_nextest_preserves_only_the_existing_shared_theme_fixture_mutex(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
config = tomllib.loads((root / ".config/nextest.toml").read_text(encoding="utf-8"))
|
||||
self.assertEqual(config["test-groups"], {"theme-studio": {"max-threads": 1}})
|
||||
self.assertEqual(config["profile"]["default"], {
|
||||
"overrides": [{
|
||||
"filter": "test(gpui_shell::settings_pane::theme_studio_tests::)",
|
||||
"test-group": "theme-studio",
|
||||
}],
|
||||
})
|
||||
|
||||
def test_native_caches_are_default_branch_snapshots_not_per_pr_uploads(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
|
||||
for job in ("native-tests", "macos-release-check"):
|
||||
body = workflow.split(f"\n {job}:\n", 1)[1]
|
||||
body = re.split(r"\n [a-z][a-z-]*:\n", body, maxsplit=1)[0]
|
||||
self.assertIn("revision: dependencies-v1", body)
|
||||
self.assertIn("save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}", body)
|
||||
save = body.split("- name: Save compiled workload", 1)[1].split(" - name:", 1)[0]
|
||||
self.assertIn("success() && steps.rust-cache.outputs.save-enabled == 'true'", save)
|
||||
self.assertNotIn("always()", save)
|
||||
self.assertIn("steps.rust-cache.outputs.cache-hit != 'true'", save)
|
||||
|
||||
def test_fast_test_profile_preserves_runtime_checks_and_resets_named_overrides(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
config = tomllib.loads((root / ".github/ci-profile.toml").read_text())
|
||||
workspace = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
config = tomllib.loads((root / ".github/ci-profile.toml").read_text(encoding="utf-8"))
|
||||
workspace = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8"))
|
||||
profile = config["profile"]["ci"]
|
||||
self.assertEqual(profile["inherits"], "dev")
|
||||
self.assertTrue(profile["debug-assertions"])
|
||||
@@ -155,17 +214,24 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
self.assertTrue(all("discover" in command for command in suites))
|
||||
|
||||
def test_success_runs_every_command_and_checks_exit_codes(self):
|
||||
with patch("scripts.ci_native_tests.subprocess.run") as run:
|
||||
self.assertEqual(main(), 0)
|
||||
self.assertEqual(run.call_count, len(native_commands()))
|
||||
self.assertTrue(all(call.kwargs["check"] for call in run.call_args_list))
|
||||
for runner in ("cargo", "nextest"):
|
||||
with self.subTest(runner=runner), patch("scripts.ci_native_tests.subprocess.run") as run:
|
||||
self.assertEqual(main(["--runner", runner]), 0)
|
||||
self.assertEqual([call.args[0] for call in run.call_args_list], native_commands(runner))
|
||||
self.assertTrue(all(call.kwargs["check"] for call in run.call_args_list))
|
||||
|
||||
def test_failure_stops_the_suite_and_is_not_reported_as_success(self):
|
||||
with patch("scripts.ci_native_tests.subprocess.run") as run:
|
||||
run.side_effect = subprocess.CalledProcessError(7, "test")
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
main()
|
||||
self.assertEqual(run.call_count, 1)
|
||||
for runner in ("cargo", "nextest"):
|
||||
for failed in range(len(native_commands(runner))):
|
||||
with self.subTest(runner=runner, failed=failed), patch("scripts.ci_native_tests.subprocess.run") as run:
|
||||
run.side_effect = [None] * failed + [subprocess.CalledProcessError(7, "test")]
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
main(["--runner", runner])
|
||||
self.assertEqual(run.call_count, failed + 1)
|
||||
|
||||
def test_unknown_runner_is_rejected_before_running_commands(self):
|
||||
with self.assertRaises(ValueError):
|
||||
native_commands("typo")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -36,7 +36,7 @@ class CiPlanCliTests(unittest.TestCase):
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
return {name: json.loads(value) for name, value in (line.split("=", 1) for line in lines)}
|
||||
|
||||
def test_draft_pr_does_not_materialize_scarce_runner_rows(self) -> None:
|
||||
def test_draft_pr_has_the_same_full_native_coverage_as_ready_pr(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "github-output"
|
||||
result = self.run_cli("pull_request", {"pull_request": {"draft": True}}, output)
|
||||
@@ -44,12 +44,15 @@ class CiPlanCliTests(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(
|
||||
output.read_text(encoding="utf-8"),
|
||||
'native_matrix=[{"os":"ubuntu-24.04"},{"os":"windows-2022"},{"os":"macos-26"}]\n'
|
||||
'release_matrix=[{"os":"macos-26"}]\n',
|
||||
'native_matrix=[{"os":"ubuntu-24.04"},{"os":"windows-2022"},{"os":"macos-26"},'
|
||||
'{"os":"windows-11-arm"},{"os":"macos-26-intel"}]\n'
|
||||
'release_matrix=[{"os":"macos-26"},{"os":"macos-26-intel"}]\n',
|
||||
)
|
||||
plan = self.outputs(output)
|
||||
self.assertNotIn({"os": "windows-11-arm"}, plan["native_matrix"])
|
||||
self.assertNotIn({"os": "macos-26-intel"}, plan["native_matrix"])
|
||||
ready_output = Path(directory) / "ready-output"
|
||||
ready = self.run_cli("pull_request", {"pull_request": {"draft": False}}, ready_output)
|
||||
self.assertEqual(ready.returncode, 0, ready.stderr)
|
||||
self.assertEqual(plan, self.outputs(ready_output))
|
||||
|
||||
def test_ready_pr_has_all_native_and_release_checks(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
Reference in new issue
Block a user