You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/09 06:50:17 UTC
(hugegraph-toolchain) 02/05: fix(ci): verify current ordinary PR merges
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git
commit a9d01deab1ddade87cc518de71668078b7b21ff3
Author: imbajin <ji...@apache.org>
AuthorDate: Thu Oct 8 22:30:30 2026 +0800
fix(ci): verify current ordinary PR merges
- recover stale event bases using the current PR merge
- verify canonical branch, merge parents and source head
- preserve native stack checks and final gate freshness
- cover ordinary snapshots and rejected stale inputs
---
.github/scripts/ci-policy.py | 40 +++++++++++++++----
.github/scripts/test_ci_policy.py | 83 +++++++++++++++++++++++++++++++++++++++
2 files changed, 116 insertions(+), 7 deletions(-)
diff --git a/.github/scripts/ci-policy.py b/.github/scripts/ci-policy.py
index 193efcc41..46e6b60c0 100644
--- a/.github/scripts/ci-policy.py
+++ b/.github/scripts/ci-policy.py
@@ -279,14 +279,38 @@ def require_current_pr(plan, fetch):
or live["head"]["ref"] != plan["branch"]
or live["base"]["repo"]["full_name"] != plan["repository"]):
raise StaleInputError("PR inputs changed; refresh the branch and start a new PR run")
- if "stack" in plan:
- # Native stacks can leave REST base.sha at an older snapshot. The actual
- # branch ref, rather than that snapshot, identifies the tested base.
+ if "baseBranch" in plan:
+ # Event and REST base.sha can be older snapshots. The actual branch
+ # ref, rather than those snapshots, identifies the tested base.
ref = fetch(f"repos/{plan['repository']}/git/ref/heads/{quote(plan['baseBranch'], safe='')}")
- if (live.get("stack") != plan["stack"] or live["base"].get("ref") != plan["baseBranch"]
+ if (live.get("stack") != plan.get("stack") or live["base"].get("ref") != plan["baseBranch"]
or ref.get("object", {}).get("sha") != plan["base"]
or live.get("merge_commit_sha") != plan["testedMergeSHA"]):
- raise StaleInputError("PR stack or canonical base changed; start a new PR run")
+ raise StaleInputError("PR merge or canonical base changed; start a new PR run")
+
+
+def record_current_merge(plan, event_pr, parents, fetch):
+ """Recover an older event base only from GitHub's current merge identity."""
+ try:
+ live = fetch(f"repos/{plan['repository']}/pulls/{plan['pr']}")
+ if live.get("stack") is not None:
+ record_stack_merge(plan, event_pr, parents, fetch)
+ return
+ branch = live["base"]["ref"]
+ if not branch or event_pr["base"]["ref"] != branch:
+ raise StaleInputError("PR target branch changed; start a new PR run")
+ ref = fetch(f"repos/{plan['repository']}/git/ref/heads/{quote(branch, safe='')}")
+ canonical = ref["object"]["sha"]
+ if (event_pr.get("stack") is not None
+ or ref["object"].get("type") != "commit" or not canonical
+ or live.get("merge_commit_sha") != plan["testedMergeSHA"]
+ or parents != [canonical, plan["head"]]
+ or git("merge-base", plan["base"], canonical) != plan["base"]):
+ raise StaleInputError("checkout lacks a valid current PR merge identity")
+ plan.update(base=canonical, baseBranch=branch)
+ require_current_pr(plan, fetch)
+ except (ValueError, KeyError, TypeError, AttributeError, subprocess.SubprocessError, OSError) as error:
+ raise StaleInputError("current PR merge identity could not be verified") from error
def record_stack_merge(plan, event_pr, parents, fetch):
@@ -343,7 +367,7 @@ def create_plan(project, event, repository, fetch=api, external_inputs=None):
raise StaleInputError("PR event has an empty input identity")
parents = git("show", "-s", "--format=%P", plan["testedMergeSHA"]).split()
if parents != [plan["base"], plan["head"]]:
- record_stack_merge(plan, pr, parents, fetch)
+ record_current_merge(plan, pr, parents, fetch)
else:
require_current_pr(plan, fetch)
# head/base objects must exist locally; workflow fetches both before planning.
@@ -424,8 +448,10 @@ def write_summary(plan, results=None):
def result_report(plan, results):
report = {key: plan[key] for key in ["schema", "repository", "project", "pr", "source", "branch", "base",
"head", "testedMergeSHA", "externalInputs"]}
+ if "baseBranch" in plan:
+ report["baseBranch"] = plan["baseBranch"]
if "stack" in plan:
- report.update(baseBranch=plan["baseBranch"], stack=plan["stack"])
+ report["stack"] = plan["stack"]
report.update(runID=int(os.environ.get("GITHUB_RUN_ID", "0")),
runAttempt=int(os.environ.get("GITHUB_RUN_ATTEMPT", "1")),
executed=plan["expected"], results=results)
diff --git a/.github/scripts/test_ci_policy.py b/.github/scripts/test_ci_policy.py
index 77e22fd97..2f722f6bc 100644
--- a/.github/scripts/test_ci_policy.py
+++ b/.github/scripts/test_ci_policy.py
@@ -421,6 +421,89 @@ class PolicyTest(unittest.TestCase):
finally:
os.chdir(old)
+ def test_regular_pr_recovers_stale_event_base_with_real_git(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ def git(*args):
+ return subprocess.check_output(["git", *args], cwd=root, text=True).strip()
+ git("init", "-q")
+ git("config", "user.email", "ci@example.invalid")
+ git("config", "user.name", "CI")
+ (root / "README.md").write_text("base")
+ (root / "hugegraph-hubble").mkdir()
+ (root / "hugegraph-hubble/README.md").write_text("packaged docs")
+ git("add", ".")
+ git("commit", "-qm", "base")
+ anchor = git("rev-parse", "HEAD")
+ git("checkout", "-qb", "feature")
+ (root / "hugegraph-loader").mkdir()
+ (root / "hugegraph-loader/A.java").write_text("source")
+ git("add", ".")
+ git("commit", "-qm", "feature")
+ head = git("rev-parse", "HEAD")
+ old_merge = git("commit-tree", git("rev-parse", "HEAD^{tree}"),
+ "-p", anchor, "-p", head, "-m", "old merge")
+ git("checkout", "-qb", "target", anchor)
+ (root / "README.md").write_text("advanced master")
+ git("commit", "-qam", "advance target")
+ canonical = git("rev-parse", "HEAD")
+ git("merge", "--no-ff", "-qm", "current merge", "feature")
+ merge = git("rev-parse", "HEAD")
+ live = self.live_pr()
+ live["head"]["sha"] = head
+ live["base"].update(sha=anchor, ref="target")
+ event = {"pull_request": json.loads(json.dumps(dict(live, number=7)))}
+ live.update(merge_commit_sha=merge)
+ ref = {"object": {"type": "commit", "sha": canonical}}
+ def fetch(path):
+ if path == "repos/apache/t/pulls/7":
+ return live
+ self.assertEqual("repos/apache/t/git/ref/heads/target", path)
+ return ref
+ old = os.getcwd()
+ try:
+ os.chdir(root)
+ # Neither event nor REST snapshot has stack metadata. The real
+ # checkout merges the current canonical target with the same head.
+ plan = policy.create_plan("toolchain", event, "apache/t", fetch)
+ self.assertEqual(canonical, plan["base"])
+ self.assertEqual("target", plan["baseBranch"])
+ self.assertNotIn("stack", plan)
+ self.assertEqual(["hubble", "loader"], plan["selected"])
+ results = {suite: {"result": "success"} for suite in plan["expected"]}
+ results.update(plan={"result": "success"}, fixture={"result": "success"},
+ **{"hubble-fixture": {"result": "success"}})
+ report = policy.gate(plan, results, fetch)
+ self.assertEqual(canonical, report["base"])
+ self.assertEqual("target", report["baseBranch"])
+ for key, value in [("merge_commit_sha", old_merge),
+ ("state", "closed"),
+ ("head", dict(live["head"], sha=anchor)),
+ ("base", dict(live["base"], ref="other"))]:
+ original = live[key]
+ live[key] = value
+ with self.subTest(field=key), self.assertRaises(policy.StaleInputError):
+ policy.create_plan("toolchain", event, "apache/t", fetch)
+ with self.subTest(gate=key), self.assertRaises(policy.StaleInputError):
+ policy.gate(plan, results, fetch)
+ live[key] = original
+ ref["object"]["sha"] = anchor
+ with self.assertRaises(policy.StaleInputError):
+ policy.create_plan("toolchain", event, "apache/t", fetch)
+ with self.assertRaises(policy.StaleInputError):
+ policy.gate(plan, results, fetch)
+ ref["object"]["sha"] = canonical
+ # A rerun of an old checkout cannot claim the fresh merge.
+ git("checkout", "--detach", "-q", old_merge)
+ with self.assertRaises(policy.StaleInputError):
+ policy.gate(plan, results, lambda _: dict(live, head=dict(live["head"], sha=anchor)))
+ stale_event = json.loads(json.dumps(event))
+ stale_event["pull_request"]["base"]["sha"] = canonical
+ with self.assertRaises(policy.StaleInputError):
+ policy.create_plan("toolchain", stale_event, "apache/t", fetch)
+ finally:
+ os.chdir(old)
+
def test_native_stack_canonical_base_and_gate_freshness(self):
# Reproduce GitHub native stacks: REST base.sha can remain at the old
# head, while the merge's first parent wraps the actual branch head.