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.