Require proof from every Rust CI shard (#9)

## Summary
- emit one run/attempt-scoped completion artifact per Rust test shard
- require the exact five-shard marker set before candidate proof, main
source proof, or Required CI can succeed
- document full-workflow rerun recovery and cover missing, extra, and
malformed marker cases

## Evidence
- real run 31501024780 completed successfully while the Jobs API
retained integration-a as queued
- 85 Python workflow/tooling tests
- build/release static safety suites (31 + 8 + 4)
- actionlint for CI, auto-merge, and Release workflows
- Release shell contracts and git diff check

This changes the CI control plane, so the trusted merger must
intentionally refuse privileged auto-merge; merge only through the
documented trusted manual squash path after Required CI succeeds.

Co-authored-by: gd-ops <gd-ops@xmfunny.com>
This commit is contained in:
xjxandgd-ops authored and GitHub committed 2026-08-11 23:07:36 +08:00
1 parent 4bc69f63e3
commit c79f73ae93
7 files changed
+153 -17

No files matched your search

+47 -10
View File
@@ -382,7 +382,41 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-apc-rust-
- name: Run inventory-checked Rust shard
run: ./script/validate_rust_test_shards.py --shard "${{ matrix.shard }}"
run: |
./script/validate_rust_test_shards.py \
--shard "${{ matrix.shard }}" \
--completion-dir "$RUNNER_TEMP/rust-shard-proof"
- name: Upload exact Rust shard completion
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ci-rust-shard-proof-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard }}
if-no-files-found: error
retention-days: 3
path: ${{ runner.temp }}/rust-shard-proof/${{ matrix.shard }}.json
rust_test_proof:
name: Verify every Rust test shard
needs: [scope, promotion, rust_tests]
if: >-
always() &&
needs.scope.result == 'success' &&
needs.promotion.outputs.promoted != '1' &&
(needs.scope.outputs.full_candidate == '1' || needs.scope.outputs.rust_mode != 'none')
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download this attempt's Rust shard completions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: ci-rust-shard-proof-${{ github.run_id }}-${{ github.run_attempt }}-*
merge-multiple: true
path: ${{ runner.temp }}/rust-shard-proof
- name: Require the exact Rust shard completion set
run: ./script/validate_rust_test_shards.py --verify-completion-dir "$RUNNER_TEMP/rust-shard-proof"
swift_interaction:
name: Swift and interaction proof
@@ -573,7 +607,7 @@ jobs:
candidate_proof:
name: Create promotable PR source proof
needs: [scope, promotion, static, contracts, rust_lint, rust_tests, swift_interaction, stress, overlay, bundle]
needs: [scope, promotion, static, contracts, rust_lint, rust_tests, rust_test_proof, swift_interaction, stress, overlay, bundle]
if: always() && github.event_name == 'pull_request' && needs.scope.outputs.full_candidate == '1'
runs-on: macos-26
timeout-minutes: 10
@@ -584,12 +618,13 @@ jobs:
CONTRACTS_RESULT: ${{ needs.contracts.result }}
RUST_LINT_RESULT: ${{ needs.rust_lint.result }}
RUST_TESTS_RESULT: ${{ needs.rust_tests.result }}
RUST_TEST_PROOF_RESULT: ${{ needs.rust_test_proof.result }}
SWIFT_RESULT: ${{ needs.swift_interaction.result }}
STRESS_RESULT: ${{ needs.stress.result }}
OVERLAY_RESULT: ${{ needs.overlay.result }}
BUNDLE_RESULT: ${{ needs.bundle.result }}
run: |
for result in "$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
for result in "$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$RUST_TEST_PROOF_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
case "$result" in
success) ;;
*) echo "candidate source dependency did not succeed: $result" >&2; exit 1 ;;
@@ -638,7 +673,7 @@ jobs:
source_proof:
name: Bind release source proof to main commit
needs: [scope, promotion, static, contracts, rust_lint, rust_tests, swift_interaction, stress, overlay, bundle, candidate_proof]
needs: [scope, promotion, static, contracts, rust_lint, rust_tests, rust_test_proof, swift_interaction, stress, overlay, bundle, candidate_proof]
if: always() && needs.scope.outputs.release_source == '1'
runs-on: macos-26
timeout-minutes: 15
@@ -654,20 +689,21 @@ jobs:
CONTRACTS_RESULT: ${{ needs.contracts.result }}
RUST_LINT_RESULT: ${{ needs.rust_lint.result }}
RUST_TESTS_RESULT: ${{ needs.rust_tests.result }}
RUST_TEST_PROOF_RESULT: ${{ needs.rust_test_proof.result }}
SWIFT_RESULT: ${{ needs.swift_interaction.result }}
STRESS_RESULT: ${{ needs.stress.result }}
OVERLAY_RESULT: ${{ needs.overlay.result }}
BUNDLE_RESULT: ${{ needs.bundle.result }}
run: |
if [[ "$PROMOTED" == "1" ]]; then
for result in "$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
for result in "$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$RUST_TEST_PROOF_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
case "$result" in
skipped) ;;
*) echo "promoted main CI unexpectedly scheduled a complete gate: $result" >&2; exit 1 ;;
esac
done
else
for result in "$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
for result in "$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$RUST_TEST_PROOF_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
case "$result" in
success) ;;
*) echo "release source dependency did not succeed: $result" >&2; exit 1 ;;
@@ -763,7 +799,7 @@ jobs:
required:
name: Required CI
needs: [scope, promotion, static, contracts, rust_lint, rust_tests, swift_interaction, stress, overlay, bundle, candidate_proof, source_proof]
needs: [scope, promotion, static, contracts, rust_lint, rust_tests, rust_test_proof, swift_interaction, stress, overlay, bundle, candidate_proof, source_proof]
if: always()
runs-on: macos-26
timeout-minutes: 10
@@ -775,6 +811,7 @@ jobs:
CONTRACTS_RESULT: ${{ needs.contracts.result }}
RUST_LINT_RESULT: ${{ needs.rust_lint.result }}
RUST_TESTS_RESULT: ${{ needs.rust_tests.result }}
RUST_TEST_PROOF_RESULT: ${{ needs.rust_test_proof.result }}
SWIFT_RESULT: ${{ needs.swift_interaction.result }}
STRESS_RESULT: ${{ needs.stress.result }}
OVERLAY_RESULT: ${{ needs.overlay.result }}
@@ -787,7 +824,7 @@ jobs:
run: |
for result in \
"$SCOPE_RESULT" "$STATIC_RESULT" "$CONTRACTS_RESULT" \
"$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$SWIFT_RESULT" \
"$RUST_LINT_RESULT" "$RUST_TESTS_RESULT" "$RUST_TEST_PROOF_RESULT" "$SWIFT_RESULT" \
"$STRESS_RESULT" "$OVERLAY_RESULT" "$BUNDLE_RESULT" \
"$CANDIDATE_PROOF_RESULT" "$SOURCE_PROOF_RESULT"; do
case "$result" in
@@ -798,7 +835,7 @@ jobs:
if [[ "$FULL_CANDIDATE" == "1" && "$PROMOTED" != "1" ]]; then
for result in \
"$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" \
"$RUST_TESTS_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" \
"$RUST_TESTS_RESULT" "$RUST_TEST_PROOF_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" \
"$OVERLAY_RESULT" "$BUNDLE_RESULT"; do
case "$result" in
success) ;;
@@ -813,7 +850,7 @@ jobs:
test "$RELEASE_SOURCE" = "1"
for result in \
"$STATIC_RESULT" "$CONTRACTS_RESULT" "$RUST_LINT_RESULT" \
"$RUST_TESTS_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" \
"$RUST_TESTS_RESULT" "$RUST_TEST_PROOF_RESULT" "$SWIFT_RESULT" "$STRESS_RESULT" \
"$OVERLAY_RESULT" "$BUNDLE_RESULT" "$CANDIDATE_PROOF_RESULT"; do
test "$result" = "skipped"
done
+2 -2
View File
@@ -18,11 +18,11 @@ Keep each user-visible change in its own bullet. Write the English text first, t
- Local pre-push validation is now a bounded formatting, syntax, contract, and compile gate, while required GitHub CI runs complete source checks in parallel across Rust shards, Swift interaction proof, integration contracts, overlay validation, stress, and App assembly. Agents choose either a direct hotfix/small PR to protected `main` or coordinated task PRs to one shared train followed by a final train PR; repository-owned ready PRs are squash-merged by a trusted workflow only after `Required CI`. A successful trusted `main` validation emits an exact-commit source proof that GitHub Release reuses before dual-architecture packaging, and an explicit Release dispatch can bind a missing tag only after that proof succeeds. Fail-closed ruleset automation protects both `main` and integration trains after the new required check is proven remotely.
Complete direct and final train PR proofs are now promoted to the squash-merged `main` commit when their repository, PR run, ordered parents, artifact, and exact Git tree all match. Run/attempt-bound merge tickets prevent a completed check from being reused after a PR changes base or lane. The replay-safe post-CI merger explicitly dispatches validation for the exact main SHA, avoiding GitHub's suppression of ordinary events produced by `GITHUB_TOKEN`; control-plane PRs fail closed to trusted manual merge so they cannot rewrite and prove their own gate. This removes the duplicate post-merge Rust, Swift, integration, stress, overlay, and App-assembly run while preserving a main-commit-bound Release proof; promotion uncertainty automatically falls back to complete main CI, and task PRs merged into a train still run only their path-scoped checks.
Complete direct and final train PR proofs are now promoted to the squash-merged `main` commit when their repository, PR run, ordered parents, artifact, and exact Git tree all match. Run/attempt-bound merge tickets prevent a completed check from being reused after a PR changes base or lane. Each Rust shard now emits its own run/attempt completion marker, and a separate exact-set gate blocks candidate, main, and `Required CI` proof issuance if GitHub's matrix summary omits a shard. The replay-safe post-CI merger explicitly dispatches validation for the exact main SHA, avoiding GitHub's suppression of ordinary events produced by `GITHUB_TOKEN`; control-plane PRs fail closed to trusted manual merge so they cannot rewrite and prove their own gate. This removes the duplicate post-merge Rust, Swift, integration, stress, overlay, and App-assembly run while preserving a main-commit-bound Release proof; promotion uncertainty automatically falls back to complete main CI, and task PRs merged into a train still run only their path-scoped checks.
本地预推送验证现收敛为有界的格式、语法、合同与编译门禁;必选 GitHub CI 则通过 Rust 分片、Swift 交互证明、集成合同、悬浮层验证、压力与 App 组装并行完成全量源码检查。Agent 会在热修复/小范围 direct PR 与“多个任务 PR 汇入共享 train、再由 train PR 至 main”之间选择;仓库内 ready PR 仅在 `Required CI` 成功后由受信任 workflow 执行 squash 合并。受信任的 `main` 验证会生成绑定精确 commit 的源码证明,GitHub Release 在双架构打包前复用该证明,显式 Release dispatch 也只有在证明成功后才能绑定缺失 tag。fail-closed ruleset 自动化会在新必选检查已由远端证明后保护 `main` 与集成 train。
direct PR 与最终 train PR 的完整证明现可在仓库、PR run、有序父提交、artifact 与精确 Git tree 全部匹配时晋升到 squash 合并后的 `main` commit。绑定 run/attempt 的 merge ticket 会阻止 PR 更换 base 或通道后复用旧检查。可重放恢复的 CI 后合并器会为精确 main SHA 显式派发验证,从而避开 GitHub 对 `GITHUB_TOKEN` 所产生普通事件的抑制;控制面 PR 会 fail closed 转为受信任人工合并,不能重写并自证门禁。此机制删除合并后重复的 Rust、Swift、集成、压力、悬浮层与 App 组装任务,同时保留绑定 main commit 的 Release 证明;晋升存在任何不确定性时都会自动回退完整 main CI,合入 train 的 task PR 仍只运行按路径检查。
direct PR 与最终 train PR 的完整证明现可在仓库、PR run、有序父提交、artifact 与精确 Git tree 全部匹配时晋升到 squash 合并后的 `main` commit。绑定 run/attempt 的 merge ticket 会阻止 PR 更换 base 或通道后复用旧检查。每个 Rust 分片现会分别生成绑定 run/attempt 的完成标记,独立的精确集合门禁会在 GitHub 矩阵汇总遗漏任一分片时阻止候选、main 与 `Required CI` 证明签发。可重放恢复的 CI 后合并器会为精确 main SHA 显式派发验证,从而避开 GitHub 对 `GITHUB_TOKEN` 所产生普通事件的抑制;控制面 PR 会 fail closed 转为受信任人工合并,不能重写并自证门禁。此机制删除合并后重复的 Rust、Swift、集成、压力、悬浮层与 App 组装任务,同时保留绑定 main commit 的 Release 证明;晋升存在任何不确定性时都会自动回退完整 main CI,合入 train 的 task PR 仍只运行按路径检查。
## [0.3.6] - 2026-08-11
+1 -1
View File
@@ -45,7 +45,7 @@ The local default is deliberately bounded: it checks diff and source syntax, loc
./script/test_all.sh --clear-cache
```
Authoritative CI never consumes local checkpoints or calls the local fast gate. One classifier fans work out into static contracts, integration contracts, Rust lint, five inventory-checked Rust test shards, complete Swift interaction proof, bounded stress, offline overlay proof, and App assembly. Every successful PR `Required CI` emits a short-lived merge ticket bound to repository, PR, base/head refs and SHAs, tested merge parents, run/attempt, and lane. Ready direct or final train PRs to `main` also emit a candidate proof bound to the tested merge commit, exact tree, gate inventory, toolchains, and interaction bytes. Task PRs to `gd-ops/train/*` remain path-scoped, create no Release candidate, and have no duplicate train-push run. / 权威 CI 不消费本地检查点,也不调用本地快速门禁。统一分类结果会并行路由到静态合同、集成合同、Rust lint、5 个带清单校验的 Rust 测试分片、完整 Swift 交互证明、压力、离线悬浮层证明与 App 组装。每个成功的 PR `Required CI` 都会生成短期 merge ticket,绑定仓库、PR、base/head ref 与 SHA、测试合并父提交、run/attempt 和通道。ready 的 direct 或最终 train → `main` PR 还会生成绑定测试合并 commit、精确 tree、门禁清单、工具链与交互证明字节的候选证明。task → train PR 保持按路径验证、不生成 Release 候选,且合并后不重复运行 train push CI。
Authoritative CI never consumes local checkpoints or calls the local fast gate. One classifier fans work out into static contracts, integration contracts, Rust lint, five inventory-checked Rust test shards, complete Swift interaction proof, bounded stress, offline overlay proof, and App assembly. Each successful Rust shard uploads a run/attempt-scoped completion marker; an independent aggregate gate requires the exact five-marker set before candidate proof, main source proof, or `Required CI` may succeed, rather than trusting the matrix summary alone. Because markers never cross attempts, recover a failed aggregate with GitHub's **Re-run all jobs**, not **Re-run failed jobs**. Every successful PR `Required CI` emits a short-lived merge ticket bound to repository, PR, base/head refs and SHAs, tested merge parents, run/attempt, and lane. Ready direct or final train PRs to `main` also emit a candidate proof bound to the tested merge commit, exact tree, gate inventory, toolchains, and interaction bytes. Task PRs to `gd-ops/train/*` remain path-scoped, create no Release candidate, and have no duplicate train-push run. / 权威 CI 不消费本地检查点,也不调用本地快速门禁。统一分类结果会并行路由到静态合同、集成合同、Rust lint、5 个带清单校验的 Rust 测试分片、完整 Swift 交互证明、压力、离线悬浮层证明与 App 组装。每个成功 Rust 分片都会上传绑定 run/attempt 的完成标记;独立汇总门禁必须看到精确的 5 个标记集合,候选证明、main 源码证明与 `Required CI` 才能成功,而不会只信任矩阵汇总状态。完成标记不会跨 attempt 复用,因此汇总失败后必须使用 GitHub 的 **Re-run all jobs**,不能使用 **Re-run failed jobs**。每个成功的 PR `Required CI` 都会生成短期 merge ticket,绑定仓库、PR、base/head ref 与 SHA、测试合并父提交、run/attempt 和通道。ready 的 direct 或最终 train → `main` PR 还会生成绑定测试合并 commit、精确 tree、门禁清单、工具链与交互证明字节的候选证明。task → train PR 保持按路径验证、不生成 Release 候选,且合并后不重复运行 train push CI。
On a trusted `main` validation (`push` or exact-SHA `workflow_dispatch`), CI first attempts exact-tree proof promotion. The post-CI merger uses the dispatch form after its `GITHUB_TOKEN` squash merge because token-produced ordinary events do not start another workflow; the dispatch fails closed unless its required SHA is the checked-out `main` commit. Promotion requires exactly one same-repository managed PR for the squash commit, one successful pull-request workflow with a successful `Required CI`, one live bounded candidate artifact, a tested merge whose ordered parents are the previous main commit and PR head, and a tested tree identical to the new main tree. CI/Release workflows, proof resolvers, interaction-proof definitions, and their routing contracts must also be unchanged from the previous trusted main commit, so a control-plane PR—including the first rollout of this mechanism—must complete the full main CI once before later PRs can reuse it. Promotion rebinds only the already-proven interaction identity; any failed check leaves the promotion output unset and schedules the complete release-grade main gates instead. / 受信任的 `main` 验证(`push` 或精确 SHA 的 `workflow_dispatch`)会先尝试精确 tree 证明晋升。CI 后合并器使用 `GITHUB_TOKEN` 完成 squash 合并后会采用显式派发,因为 token 产生的普通事件不会再启动 workflow;若必填 SHA 与 checkout 的 `main` commit 不一致,派发会 fail closed。晋升必须存在唯一的同仓库受管 PR 对应 squash commit、唯一成功且含成功 `Required CI` 的 PR workflow、唯一未过期的有界候选 artifact;测试合并的有序父提交必须是前一 main commit 与 PR head,且测试 tree 必须与新 main tree 完全一致。CI/Release workflow、证明解析器、交互证明定义及其路由合同还必须相对前一个受信任 main commit 保持不变,因此控制面 PR(包括本机制的首次上线)必须先完整执行一次 main CI,之后的 PR 才能复用它。晋升只重绑已经证明的交互身份;任一检查失败都会保持晋升输出为空,并改为调度完整 Release 级 main 门禁。
@@ -995,6 +995,10 @@ class CIWorkflowContractTests(unittest.TestCase):
self.assertIn("--validation full", source)
self.assertIn("--interaction-attestation", source)
self.assertIn("shard: [core, integration-a, integration-b, integration-c, integration-d]", source)
self.assertIn("name: Verify every Rust test shard", source)
self.assertIn("ci-rust-shard-proof-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard }}", source)
self.assertIn("--verify-completion-dir", source)
self.assertIn("RUST_TEST_PROOF_RESULT: ${{ needs.rust_test_proof.result }}", source)
self.assertIn("name: Required CI", source)
self.assertIn("release-source-proof-${{ github.sha }}", source)
self.assertIn("Computer Use is recommended", source)
+33
View File
@@ -284,6 +284,39 @@ class RustTestShardTests(unittest.TestCase):
self.assertTrue(all(command[:2] == ["cargo", "test"] for command in commands))
self.assertTrue(all("--locked" in command for command in commands))
def test_completion_proof_requires_every_exact_shard(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
proof_dir = pathlib.Path(temp_dir)
for shard in rust_test_shards.ALL_SHARDS:
rust_test_shards.write_completion(proof_dir, shard)
rust_test_shards.validate_completions(proof_dir)
def test_completion_proof_rejects_a_missing_shard(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
proof_dir = pathlib.Path(temp_dir)
for shard in rust_test_shards.ALL_SHARDS[:-1]:
rust_test_shards.write_completion(proof_dir, shard)
with self.assertRaisesRegex(ValueError, "completion set mismatch"):
rust_test_shards.validate_completions(proof_dir)
def test_completion_proof_rejects_extra_or_malformed_markers(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
proof_dir = pathlib.Path(temp_dir)
for shard in rust_test_shards.ALL_SHARDS:
rust_test_shards.write_completion(proof_dir, shard)
(proof_dir / "unexpected.json").write_text("{}", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "completion set mismatch"):
rust_test_shards.validate_completions(proof_dir)
(proof_dir / "unexpected.json").unlink()
marker = proof_dir / f"{rust_test_shards.ALL_SHARDS[0]}.json"
marker.write_text('{"schema_version":"wrong"}', encoding="utf-8")
with self.assertRaisesRegex(ValueError, "invalid Rust shard completion"):
rust_test_shards.validate_completions(proof_dir)
class MainBranchRulesetTests(unittest.TestCase):
def test_ruleset_requires_pull_requests_and_the_github_actions_check(self) -> None:
+6 -1
View File
@@ -115,8 +115,13 @@ if rg -Fq "if: steps.validation_scope.outputs.docs_only != '1'" "$CI_WORKFLOW";
fi
rg -Fq -- '--validation full' "$CI_WORKFLOW"
rg -Fq -- '--interaction-attestation' "$CI_WORKFLOW"
rg -Fq 'validate_rust_test_shards.py --shard' "$CI_WORKFLOW"
rg -Fq './script/validate_rust_test_shards.py' "$CI_WORKFLOW"
rg -Fq -- '--shard "${{ matrix.shard }}"' "$CI_WORKFLOW"
rg -Fq 'shard: [core, integration-a, integration-b, integration-c, integration-d]' "$CI_WORKFLOW"
rg -Fq 'name: Verify every Rust test shard' "$CI_WORKFLOW"
rg -Fq 'ci-rust-shard-proof-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard }}' "$CI_WORKFLOW"
rg -Fq 'validate_rust_test_shards.py --verify-completion-dir' "$CI_WORKFLOW"
rg -Fq 'RUST_TEST_PROOF_RESULT: ${{ needs.rust_test_proof.result }}' "$CI_WORKFLOW"
rg -Fq 'name: Required CI' "$CI_WORKFLOW"
rg -Fq 'release-source-proof-${{ github.sha }}' "$CI_WORKFLOW"
rg -Fq 'ci-candidate-proof-${{ github.sha }}' "$CI_WORKFLOW"
+60 -3
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
import argparse
import json
import pathlib
import subprocess
import sys
@@ -47,6 +48,7 @@ SHARDS: dict[str, list[tuple[str, str]]] = {
],
}
ALL_SHARDS = ["core", *SHARDS]
COMPLETION_SCHEMA = "apc.rust-test-shard-completion.v1"
def discovered_tests(root: pathlib.Path) -> set[tuple[str, str]]:
@@ -88,11 +90,57 @@ def commands(root: pathlib.Path, shard: str) -> list[list[str]]:
]
def write_completion(proof_dir: pathlib.Path, shard: str) -> pathlib.Path:
if shard not in ALL_SHARDS:
raise ValueError(f"unknown Rust test shard: {shard}")
proof_dir.mkdir(parents=True, exist_ok=True)
if not proof_dir.is_dir() or proof_dir.is_symlink():
raise ValueError("Rust shard completion destination must be a directory")
path = proof_dir / f"{shard}.json"
path.write_text(
json.dumps(
{"schema_version": COMPLETION_SCHEMA, "shard": shard},
sort_keys=True,
separators=(",", ":"),
)
+ "\n",
encoding="utf-8",
)
return path
def validate_completions(proof_dir: pathlib.Path) -> None:
if not proof_dir.is_dir() or proof_dir.is_symlink():
raise ValueError("Rust shard completion proof must be a directory")
entries = sorted(proof_dir.iterdir())
expected_names = sorted(f"{shard}.json" for shard in ALL_SHARDS)
actual_names = [entry.name for entry in entries]
if actual_names != expected_names:
raise ValueError(
"Rust shard completion set mismatch: "
f"expected={expected_names} actual={actual_names}; "
"rerun all workflow jobs because completion markers cannot cross run attempts"
)
for path in entries:
if path.is_symlink() or not path.is_file() or path.stat().st_size > 4096:
raise ValueError(f"invalid Rust shard completion file: {path.name}")
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise ValueError(f"invalid Rust shard completion file: {path.name}") from error
shard = path.stem
if payload != {"schema_version": COMPLETION_SCHEMA, "shard": shard}:
raise ValueError(f"invalid Rust shard completion payload: {path.name}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path(__file__).resolve().parent.parent)
parser.add_argument("--shard", choices=ALL_SHARDS)
parser.add_argument("--list-shards", action="store_true")
operation = parser.add_mutually_exclusive_group(required=True)
operation.add_argument("--shard", choices=ALL_SHARDS)
operation.add_argument("--list-shards", action="store_true")
operation.add_argument("--verify-completion-dir", type=pathlib.Path)
parser.add_argument("--completion-dir", type=pathlib.Path)
parser.add_argument("--plan", action="store_true")
args = parser.parse_args()
try:
@@ -101,15 +149,24 @@ def main() -> int:
if args.list_shards:
print("\n".join(ALL_SHARDS))
return 0
if args.verify_completion_dir is not None:
if args.plan or args.completion_dir is not None:
parser.error("completion verification cannot be combined with shard output options")
validate_completions(args.verify_completion_dir)
return 0
if args.shard is None:
parser.error("--shard is required unless --list-shards is used")
parser.error("--shard is required")
planned = commands(root, args.shard)
if args.plan:
if args.completion_dir is not None:
parser.error("--completion-dir cannot be used with --plan")
for command in planned:
print(" ".join(command))
return 0
for command in planned:
subprocess.run(command, cwd=root, check=True)
if args.completion_dir is not None:
write_completion(args.completion_dir, args.shard)
except (OSError, ValueError, subprocess.CalledProcessError) as error:
print(f"Rust test shard error: {error}", file=sys.stderr)
return 1