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
13 changes: 12 additions & 1 deletion docs/development/ci-impact-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,18 @@ full qualification. A PR is classified from its merge base through its complete
head, including changes from earlier commits, not just its latest commit.

The `ci-impact-plan` artifact and job summary report exact revisions, change kind,
per-job execution flags, reason and coverage scope. The merge gate requires
per-job execution flags, reason and coverage scope. `python_shards` reads the
static `test-shard.strategy.matrix.shard` axis from the immutable workflow at
`checkout_sha`, currently six, rather than retaining a separate historical count.
Unsupported or noncontiguous topology fails classification instead of publishing
a fallback count. Docs/presentation exemptions still report zero. The number
describes the workflow topology, not how many jobs actually ran.

`python_shards` 从 `checkout_sha` 对应的不可变 workflow 读取静态分片轴,当前为六片,
不另存历史计数;不支持或不连续的拓扑直接报错,不输出猜测值。文档/前端豁免仍为零。
它描述 workflow 拓扑,不能证明任何分片实际执行或 JUnit/coverage 已完整。

The merge gate requires
success for required jobs and an explicit skip for exempt ones; failure,
cancellation, missing outputs, contradictory flags or unexpected skips fail.

Expand Down
24 changes: 22 additions & 2 deletions scripts/ci/impact_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ def trusted_policy(base: str, head: str) -> bool:
return True


def declared_python_shards(checkout_sha: str) -> int:
# Only classification needs the parser already installed in the changes
# job. Aggregate verification must remain usable with the standard library.
import yaml

workflow = yaml.load(git("show", f"{checkout_sha}:{POLICY_PATHS[-1]}"), Loader=yaml.BaseLoader)
message = "test-shard.strategy.matrix must declare only a static contiguous shard axis [1..N]"
try:
matrix = workflow["jobs"]["test-shard"]["strategy"]["matrix"]
except (KeyError, TypeError):
raise ValueError(message) from None
if not isinstance(matrix, dict) or set(matrix) != {"shard"}:
raise ValueError(message)
shards = matrix["shard"]
if not isinstance(shards, list) or not shards or shards != [str(i) for i in range(1, len(shards) + 1)]:
raise ValueError(message)
return len(shards)


def plan(base: str, head: str, *, pull_request: bool = True, force_full: bool = False) -> dict:
base_sha, head_sha = revision(base), revision(head)
merge_base = git("merge-base", base_sha, head_sha).decode().strip()
Expand All @@ -96,12 +115,13 @@ def plan(base: str, head: str, *, pull_request: bool = True, force_full: bool =
elif kind == "presentation" and not trusted_policy(base_sha, head_sha):
kind, reason = "full", "presentation exemption policy is not already trusted on the target branch"
flags = job_flags(kind, presentation=presentation or policy_change)
checkout_sha = revision("HEAD")
return {"schema_version": SCHEMA, "base_sha": base_sha, "head_sha": head_sha,
"merge_base_sha": merge_base, "checkout_sha": revision("HEAD"), "change_kind": kind,
"merge_base_sha": merge_base, "checkout_sha": checkout_sha, "change_kind": kind,
"force_full": force_full, "reason": reason, **flags,
"changes": [{"status": item.status, "path": item.path} for item in changes],
"coverage_scope": "full Python suite" if flags["python_tests"] else "no Python coverage claim",
"python_shards": 4 if flags["python_tests"] else 0}
"python_shards": declared_python_shards(checkout_sha) if flags["python_tests"] else 0}


def write_plan(packet: dict, output: str) -> None:
Expand Down
43 changes: 39 additions & 4 deletions scripts/ci/test_impact_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def write(path, content):
target = root / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content)
def classify(base, *extra):
result = subprocess.run([sys.executable, script, "classify", "--base", base, "--head", "HEAD",
def classify(base, *extra, head="HEAD"):
result = subprocess.run([sys.executable, script, "classify", "--base", base, "--head", head,
"--plan", str(root / "plan.json"), *extra], cwd=root, env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads((root / "plan.json").read_text()), result.stdout
Expand All @@ -67,7 +67,8 @@ def classify(base, *extra):
path = "loopx/web/chat/index.html"
write(path, "old")
for policy in POLICY_PATHS:
write(policy, "reviewed policy")
write(policy, "jobs:\n test-shard:\n strategy:\n matrix:\n shard: [1, 2, 3, 4, 5, 6]\n"
if policy.endswith(".yml") else "reviewed policy")
git("add", ".")
git("commit", "-qm", "base")
base = git("rev-parse", "HEAD")
Expand All @@ -78,14 +79,15 @@ def classify(base, *extra):
self.assertFalse(packet["python_tests"])
self.assertFalse(packet["backend_tests"])
self.assertFalse(packet["stage2c_tests"])
self.assertEqual(packet["python_shards"], 0)
self.assertTrue(packet["presentation_tests"])
self.assertEqual(packet["head_sha"], git("rev-parse", "HEAD"))
for flag in ("--force-full", "--non-pr"):
forced, _ = classify(base, flag)
self.assertTrue(forced["python_tests"])
self.assertTrue(forced["backend_tests"])
self.assertTrue(forced["stage2c_tests"])
self.assertEqual(forced["python_shards"], 4)
self.assertEqual(forced["python_shards"], 6)
# A policy change cannot authorize its own exemption.
write(POLICY_PATHS[0], "changed policy")
git("commit", "-qam", "policy")
Expand Down Expand Up @@ -116,6 +118,39 @@ def classify(base, *extra):
failed = subprocess.run([sys.executable, script, "classify", "--base", "missing", "--head", "HEAD"],
cwd=root, env=env, capture_output=True)
self.assertNotEqual(failed.returncode, 0)
# The workflow executing this checkout owns topology, even when
# the candidate head differs or an uncommitted file is misleading.
published = git("rev-parse", "HEAD")
workflow = POLICY_PATHS[-1]
prefix = "jobs:\n test-shard:\n strategy:\n matrix:\n"
write(workflow, prefix + " shard: [1, 2, 3, 4, 5, 6, 7, 8]\n")
self.assertEqual(classify(published, "--non-pr")[0]["python_shards"], 6)
git("commit", "-qam", "executing workflow has eight shards")
packet, _ = classify(published, "--non-pr", head=published)
self.assertEqual(packet["python_shards"], 8)
self.assertEqual(packet["head_sha"], published)
self.assertEqual(packet["checkout_sha"], git("rev-parse", "HEAD"))
for axis in ("shard: []", "shard: [1, 1]", "shard: [1, 3]", "shard: [1, '02']",
"shard: '${{ inputs.shards }}'", "shard: [1, 2]\n python: ['3.11']", None):
with self.subTest(axis=axis):
write(workflow, prefix + " " + axis + "\n" if axis else "jobs: {}\n")
git("commit", "-qam", "unsupported execution topology")
output = root / "rejected.json"
result = subprocess.run([sys.executable, script, "classify", "--base", "HEAD",
"--head", "HEAD", "--non-pr", "--plan", str(output)],
cwd=root, env=env, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("test-shard.strategy.matrix", result.stderr)
self.assertFalse(output.exists(), "invalid topology must not publish a fallback plan")
# The docs exemption still reports no Python coverage or shards.
docs_base = git("rev-parse", "HEAD")
write("README.md", "fixture documentation")
git("add", "--", "README.md")
git("commit", "-qm", "docs-only change")
packet, _ = classify(docs_base)
self.assertEqual(packet["change_kind"], "docs")
self.assertFalse(packet["python_tests"])
self.assertEqual(packet["python_shards"], 0)


if __name__ == "__main__":
Expand Down
11 changes: 11 additions & 0 deletions scripts/ci/test_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
Expand Down Expand Up @@ -37,6 +38,16 @@ def needs(kind="full", *, presentation=False, jobs=JOB_OUTPUT):


class GateTests(unittest.TestCase):
def test_real_aggregate_entrypoints_do_not_need_the_classification_yaml_parser(self):
script = Path(__file__).with_name("review_gate.py")
for command, jobs in (("verify", JOB_OUTPUT), ("verify-core", CHECK_OUTPUT)):
with self.subTest(command=command):
result = subprocess.run([sys.executable, "-S", str(script), command],
env={**os.environ, "PYTHONPATH": "", "NEEDS_JSON": json.dumps(needs(jobs=jobs))},
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("qualification complete", result.stdout)

def test_closed_pr_cancels_same_lane_and_skips_qualification(self):
workflow = yaml.load(
(Path(__file__).resolve().parents[2] / ".github/workflows/python-tests.yml").read_text(),
Expand Down
Loading