Skip to content

Merge queue squash-merge can revert submodule pointers from concurrent PRs #559

@khatchad

Description

@khatchad

Summary

ponder-lab/ML's merge queue is configured to squash-merge, and the merge queue silently reverts submodule pointers when two PRs touch master in sequence and the second PR was branched from a pre-first-PR base. This isn't a hypothetical—it just happened on 2026-05-27.

Concrete incident

  1. Bump jython3 to 0.0.2-SNAPSHOT; drop redundant ProxyDeserialization shade-exclude ponder-lab/ML#341 (closing Slim-jar consumption blocked by jython3 default-package class (ProxyDeserialization.class) #557) merged at commit baeddd93, bumping the jython3 submodule pointer to b81600db (the post-Slim-jar consumption blocked by jython3 default-package class (ProxyDeserialization.class) #557 build that strips ProxyDeserialization.class from jython-dev.jar).
  2. Hash PythonInvokeInstruction on iIndex() for consistency with inherited equals ponder-lab/ML#343 was branched from PRE-Bump jep from 4.2.2 to 4.3.0 #341 master, where the submodule pointer was still at 580ea6ba. The PR didn't intentionally touch the submodule.
  3. When Bump black from 25.9.0 to 25.11.0 #343 was squash-merged at commit 64f60021, GitHub replayed the entire PR diff against the post-Bump jep from 4.2.2 to 4.3.0 #341 tree. The replay included the (now-divergent) submodule pointer, silently reverting Bump jep from 4.2.2 to 4.3.0 #341's bump.

git show 64f60021 -- jython3 confirms:

diff --git a/jython3 b/jython3
index b81600db..580ea6ba 160000
--- a/jython3
+++ b/jython3
@@ -1 +1 @@
-Subproject commit b81600db6c254d7fc60603af2e33c57b98093dd8
+Subproject commit 580ea6ba9f03f78987b278b21177b8da0d3f4e1f

#343 didn't intentionally touch the submodule, but the squash made it look like it did.

Why CI Didn't Catch This

#341 also dropped the local <exclude>ProxyDeserialization.class</exclude> shade-filter on the assumption that the upstream jython3 build no longer ships the class. With the submodule reverted, the upstream ant build does ship it again, but the fat-JAR build doesn't fail—shade just packages the class at the default-package level. CI only smoke-tests the fat JAR's existence and Main-Class manifest entry; it doesn't validate slim-JAR consumability. So both #341 and #343 passed CI cleanly.

The downstream symptom: Hybridize via Tycho 5+'s bnd auto-wrap hits the same The default package '.' is not permitted by the Import-Package syntax failure that #557 was supposed to close.

ponder-lab#346 repairs the regression with a one-line submodule SHA bump, but the underlying foot-gun remains.

Mitigation Options

Each has a cost; this issue tracks the discussion, not a chosen path.

  1. Require rebase-before-merge. Force every PR to be at master HEAD before the merge queue admits it. Eliminates the squash-replay issue entirely. Costs a rebase per PR; on a busy week with concurrent PRs this is meaningful friction.

  2. Switch from squash-merge to merge-commit. Merge commits preserve the actual file states from each parent rather than replaying a diff. Submodule pointer survives the merge correctly. Costs a noisier commit graph and breaks the "one PR = one commit" pattern many automation hooks rely on.

  3. CI check: reject PRs whose submodule diff would revert master. Cheap to add (a small shell snippet in continuous-integration.yml). Catches the regression at PR time before it reaches the queue. Doesn't prevent the squash-replay mechanism, just guards against its visible effect.

  4. Slim-JAR smoke test in CI. Independent of (3): exercising the slim-consumption path (e.g., via a minimal Tycho target build that pulls the published slim JAR) would catch Slim-jar consumption blocked by jython3 default-package class (ProxyDeserialization.class) #557-class regressions regardless of cause. Larger lift than (3).

I'd lean (3) as the lowest-cost mitigation worth adopting; (4) as a longer-term hardening; (1)/(2) as policy decisions if the foot-gun recurs.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions