Skip to content

Commit e11e859

Browse files
authored
fix(ci): project shard count from the executing workflow (#6568)
1 parent ddde334 commit e11e859

4 files changed

Lines changed: 84 additions & 7 deletions

File tree

‎docs/development/ci-impact-selection.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,18 @@ full qualification. A PR is classified from its merge base through its complete
170170
head, including changes from earlier commits, not just its latest commit.
171171

172172
The `ci-impact-plan` artifact and job summary report exact revisions, change kind,
173-
per-job execution flags, reason and coverage scope. The merge gate requires
173+
per-job execution flags, reason and coverage scope. `python_shards` reads the
174+
static `test-shard.strategy.matrix.shard` axis from the immutable workflow at
175+
`checkout_sha`, currently six, rather than retaining a separate historical count.
176+
Unsupported or noncontiguous topology fails classification instead of publishing
177+
a fallback count. Docs/presentation exemptions still report zero. The number
178+
describes the workflow topology, not how many jobs actually ran.
179+
180+
`python_shards` 从 `checkout_sha` 对应的不可变 workflow 读取静态分片轴,当前为六片,
181+
不另存历史计数;不支持或不连续的拓扑直接报错,不输出猜测值。文档/前端豁免仍为零。
182+
它描述 workflow 拓扑,不能证明任何分片实际执行或 JUnit/coverage 已完整。
183+
184+
The merge gate requires
174185
success for required jobs and an explicit skip for exempt ones; failure,
175186
cancellation, missing outputs, contradictory flags or unexpected skips fail.
176187

‎scripts/ci/impact_plan.py‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ def trusted_policy(base: str, head: str) -> bool:
7878
return True
7979

8080

81+
def declared_python_shards(checkout_sha: str) -> int:
82+
# Only classification needs the parser already installed in the changes
83+
# job. Aggregate verification must remain usable with the standard library.
84+
import yaml
85+
86+
workflow = yaml.load(git("show", f"{checkout_sha}:{POLICY_PATHS[-1]}"), Loader=yaml.BaseLoader)
87+
message = "test-shard.strategy.matrix must declare only a static contiguous shard axis [1..N]"
88+
try:
89+
matrix = workflow["jobs"]["test-shard"]["strategy"]["matrix"]
90+
except (KeyError, TypeError):
91+
raise ValueError(message) from None
92+
if not isinstance(matrix, dict) or set(matrix) != {"shard"}:
93+
raise ValueError(message)
94+
shards = matrix["shard"]
95+
if not isinstance(shards, list) or not shards or shards != [str(i) for i in range(1, len(shards) + 1)]:
96+
raise ValueError(message)
97+
return len(shards)
98+
99+
81100
def plan(base: str, head: str, *, pull_request: bool = True, force_full: bool = False) -> dict:
82101
base_sha, head_sha = revision(base), revision(head)
83102
merge_base = git("merge-base", base_sha, head_sha).decode().strip()
@@ -96,12 +115,13 @@ def plan(base: str, head: str, *, pull_request: bool = True, force_full: bool =
96115
elif kind == "presentation" and not trusted_policy(base_sha, head_sha):
97116
kind, reason = "full", "presentation exemption policy is not already trusted on the target branch"
98117
flags = job_flags(kind, presentation=presentation or policy_change)
118+
checkout_sha = revision("HEAD")
99119
return {"schema_version": SCHEMA, "base_sha": base_sha, "head_sha": head_sha,
100-
"merge_base_sha": merge_base, "checkout_sha": revision("HEAD"), "change_kind": kind,
120+
"merge_base_sha": merge_base, "checkout_sha": checkout_sha, "change_kind": kind,
101121
"force_full": force_full, "reason": reason, **flags,
102122
"changes": [{"status": item.status, "path": item.path} for item in changes],
103123
"coverage_scope": "full Python suite" if flags["python_tests"] else "no Python coverage claim",
104-
"python_shards": 4 if flags["python_tests"] else 0}
124+
"python_shards": declared_python_shards(checkout_sha) if flags["python_tests"] else 0}
105125

106126

107127
def write_plan(packet: dict, output: str) -> None:

‎scripts/ci/test_impact_plan.py‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def write(path, content):
5555
target = root / path
5656
target.parent.mkdir(parents=True, exist_ok=True)
5757
target.write_text(content)
58-
def classify(base, *extra):
59-
result = subprocess.run([sys.executable, script, "classify", "--base", base, "--head", "HEAD",
58+
def classify(base, *extra, head="HEAD"):
59+
result = subprocess.run([sys.executable, script, "classify", "--base", base, "--head", head,
6060
"--plan", str(root / "plan.json"), *extra], cwd=root, env=env, capture_output=True, text=True)
6161
self.assertEqual(result.returncode, 0, result.stderr)
6262
return json.loads((root / "plan.json").read_text()), result.stdout
@@ -67,7 +67,8 @@ def classify(base, *extra):
6767
path = "loopx/web/chat/index.html"
6868
write(path, "old")
6969
for policy in POLICY_PATHS:
70-
write(policy, "reviewed policy")
70+
write(policy, "jobs:\n test-shard:\n strategy:\n matrix:\n shard: [1, 2, 3, 4, 5, 6]\n"
71+
if policy.endswith(".yml") else "reviewed policy")
7172
git("add", ".")
7273
git("commit", "-qm", "base")
7374
base = git("rev-parse", "HEAD")
@@ -78,14 +79,15 @@ def classify(base, *extra):
7879
self.assertFalse(packet["python_tests"])
7980
self.assertFalse(packet["backend_tests"])
8081
self.assertFalse(packet["stage2c_tests"])
82+
self.assertEqual(packet["python_shards"], 0)
8183
self.assertTrue(packet["presentation_tests"])
8284
self.assertEqual(packet["head_sha"], git("rev-parse", "HEAD"))
8385
for flag in ("--force-full", "--non-pr"):
8486
forced, _ = classify(base, flag)
8587
self.assertTrue(forced["python_tests"])
8688
self.assertTrue(forced["backend_tests"])
8789
self.assertTrue(forced["stage2c_tests"])
88-
self.assertEqual(forced["python_shards"], 4)
90+
self.assertEqual(forced["python_shards"], 6)
8991
# A policy change cannot authorize its own exemption.
9092
write(POLICY_PATHS[0], "changed policy")
9193
git("commit", "-qam", "policy")
@@ -116,6 +118,39 @@ def classify(base, *extra):
116118
failed = subprocess.run([sys.executable, script, "classify", "--base", "missing", "--head", "HEAD"],
117119
cwd=root, env=env, capture_output=True)
118120
self.assertNotEqual(failed.returncode, 0)
121+
# The workflow executing this checkout owns topology, even when
122+
# the candidate head differs or an uncommitted file is misleading.
123+
published = git("rev-parse", "HEAD")
124+
workflow = POLICY_PATHS[-1]
125+
prefix = "jobs:\n test-shard:\n strategy:\n matrix:\n"
126+
write(workflow, prefix + " shard: [1, 2, 3, 4, 5, 6, 7, 8]\n")
127+
self.assertEqual(classify(published, "--non-pr")[0]["python_shards"], 6)
128+
git("commit", "-qam", "executing workflow has eight shards")
129+
packet, _ = classify(published, "--non-pr", head=published)
130+
self.assertEqual(packet["python_shards"], 8)
131+
self.assertEqual(packet["head_sha"], published)
132+
self.assertEqual(packet["checkout_sha"], git("rev-parse", "HEAD"))
133+
for axis in ("shard: []", "shard: [1, 1]", "shard: [1, 3]", "shard: [1, '02']",
134+
"shard: '${{ inputs.shards }}'", "shard: [1, 2]\n python: ['3.11']", None):
135+
with self.subTest(axis=axis):
136+
write(workflow, prefix + " " + axis + "\n" if axis else "jobs: {}\n")
137+
git("commit", "-qam", "unsupported execution topology")
138+
output = root / "rejected.json"
139+
result = subprocess.run([sys.executable, script, "classify", "--base", "HEAD",
140+
"--head", "HEAD", "--non-pr", "--plan", str(output)],
141+
cwd=root, env=env, capture_output=True, text=True)
142+
self.assertNotEqual(result.returncode, 0)
143+
self.assertIn("test-shard.strategy.matrix", result.stderr)
144+
self.assertFalse(output.exists(), "invalid topology must not publish a fallback plan")
145+
# The docs exemption still reports no Python coverage or shards.
146+
docs_base = git("rev-parse", "HEAD")
147+
write("README.md", "fixture documentation")
148+
git("add", "--", "README.md")
149+
git("commit", "-qm", "docs-only change")
150+
packet, _ = classify(docs_base)
151+
self.assertEqual(packet["change_kind"], "docs")
152+
self.assertFalse(packet["python_tests"])
153+
self.assertEqual(packet["python_shards"], 0)
119154

120155

121156
if __name__ == "__main__":

‎scripts/ci/test_review_gate.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77
import re
88
import subprocess
9+
import sys
910
import tempfile
1011
from types import SimpleNamespace
1112
import unittest
@@ -37,6 +38,16 @@ def needs(kind="full", *, presentation=False, jobs=JOB_OUTPUT):
3738

3839

3940
class GateTests(unittest.TestCase):
41+
def test_real_aggregate_entrypoints_do_not_need_the_classification_yaml_parser(self):
42+
script = Path(__file__).with_name("review_gate.py")
43+
for command, jobs in (("verify", JOB_OUTPUT), ("verify-core", CHECK_OUTPUT)):
44+
with self.subTest(command=command):
45+
result = subprocess.run([sys.executable, "-S", str(script), command],
46+
env={**os.environ, "PYTHONPATH": "", "NEEDS_JSON": json.dumps(needs(jobs=jobs))},
47+
capture_output=True, text=True)
48+
self.assertEqual(result.returncode, 0, result.stderr)
49+
self.assertIn("qualification complete", result.stdout)
50+
4051
def test_closed_pr_cancels_same_lane_and_skips_qualification(self):
4152
workflow = yaml.load(
4253
(Path(__file__).resolve().parents[2] / ".github/workflows/python-tests.yml").read_text(),

0 commit comments

Comments
 (0)