Bug Scenario
Given the apexyard framework uses a release-cut branch model (dev → main via release PRs per AgDR-0007)
When a release PR is squash-merged into main, the squash commit on main has a different SHA than the equivalent dev-branch history
Then dev accumulates "phantom" un-synced commits that look like they're ahead of main but were actually already shipped via the squash. A future dev → main release PR will conflict with the squashed deltas already on main.
Expected every release PR is followed by a deterministic main→dev sync PR that reconciles the squash divergence within the same release cycle.
Repro Steps
git fetch upstream main dev --tags
git log upstream/dev..upstream/main --oneline
Currently returns 5 squash commits on main that are NOT on dev:
eb41cfe release(#401): v2.0.2 (#402)
b29b266 fix(#399): site/ — GA4 + consent banner on all 4 pages (#400)
189f86a release(#395): v2.0.1 (#396)
d1c2115 release(#391): v2.0.0 (#392)
15516a8 release(#278): v1.3.0 (#279)
52f220c release(#160): v1.2.0 (#172)
(Three of these — v1.2.0, v1.3.0, v2.0.0 — date back months. v2.0.1 + v2.0.2 added two more, plus the v2.0.2 underlying fix b29b266 because the v2.0.1 cherry-pick path landed it directly on main, not dev.)
Inverse direction (git log upstream/main..upstream/dev) shows 124 commits on dev that aren't on main — these are the legitimate "dev is ahead" deltas that will land in the next release.
Severity
P2 — not blocking any current work, but compounds with every release. Each unaddressed release cycle adds one more divergence point. The next dev → main release PR will conflict at a higher rate the longer this is left; we already paid the cost on v2.0.0 (99-conflict mega-merge) and worked around it on v2.0.1 + v2.0.2 by branching releases directly from main.
Mitigation
Workaround that's been used recently: branch release PRs directly from main (cherry-pick path) instead of dev, sidestepping the divergence cost per-release. Works for single-commit PATCH releases but doesn't scale — any release containing >1 commit that's only on dev can't use the workaround.
Real fix candidates (pick one):
/release-sync skill — automation that runs as the final step of /release: after the release PR merges to main, file a sync-back PR from main into dev that just merges main with -X theirs (dev wins everywhere the squash didn't change content, the squash commits become merge ancestors of dev). One PR per release.
- Branch-protect main with a "must be ancestor of dev" rule — won't work for squash-merge releases by design; mostly a non-starter.
- Switch from squash to merge-commit on release PRs — preserves dev's commit history into main, eliminates divergence at the source. Costs: main's log gets noisier; release PR ergonomics change. Worth considering but a bigger framework decision.
Option 1 is the lowest-friction; recommend filing as a separate [Feature] /release-sync skill ticket and then closing this bug when that ships.
Investigation Notes
The divergence pattern is:
dev: A — B — C — D (4 daily commits)
\
main: ... — (squash:A+B+C+D as one commit X) ... — tag v1.X.0
After release: dev still has A, B, C, D as separate commits; main has only X. They share NO commits between them since the last fully-synced point. A future PR dev → main containing A — B — C — D — E (new commit E) will conflict on everything in A — D's diffs that X also touched.
The sync-back direction is much friendlier: git merge -X theirs from main into dev tells git "where there's a conflict, the dev version wins" — which is correct because dev already has those changes via the unsquashed commits. The merge commit then makes the squash commits an ancestor of dev, so future dev → main PRs only see the genuinely-new commits.
Glossary
| Term |
Definition |
| Release-cut branch model |
apexyard-framework convention (AgDR-0007): daily PRs land on dev; main only receives release PRs from dev (tagged with semver). Framework-only — managed projects stay trunk-based. |
| Squash divergence |
When a release PR is squash-merged to main, the resulting commit has a different SHA than the equivalent dev history. dev still carries the unsquashed commits unless a sync-back PR is filed. |
| Cherry-pick release path |
Workaround used for v2.0.1 + v2.0.2: branch the release from upstream/main (not dev), pick or apply the single fix, PR to main. Only works for releases containing 1 commit. |
Merge -X theirs |
Git merge strategy option that resolves conflicts in favour of "their" side — used on main→dev syncs where dev already has the un-squashed equivalents and "wins" by default. |
Bug Scenario
Given the apexyard framework uses a release-cut branch model (
dev→mainvia release PRs per AgDR-0007)When a release PR is squash-merged into
main, the squash commit onmainhas a different SHA than the equivalent dev-branch historyThen
devaccumulates "phantom" un-synced commits that look like they're ahead ofmainbut were actually already shipped via the squash. A futuredev → mainrelease PR will conflict with the squashed deltas already onmain.Expected every release PR is followed by a deterministic main→dev sync PR that reconciles the squash divergence within the same release cycle.
Repro Steps
Currently returns 5 squash commits on main that are NOT on dev:
(Three of these — v1.2.0, v1.3.0, v2.0.0 — date back months. v2.0.1 + v2.0.2 added two more, plus the v2.0.2 underlying fix
b29b266because the v2.0.1 cherry-pick path landed it directly on main, not dev.)Inverse direction (
git log upstream/main..upstream/dev) shows 124 commits on dev that aren't on main — these are the legitimate "dev is ahead" deltas that will land in the next release.Severity
P2 — not blocking any current work, but compounds with every release. Each unaddressed release cycle adds one more divergence point. The next
dev → mainrelease PR will conflict at a higher rate the longer this is left; we already paid the cost on v2.0.0 (99-conflict mega-merge) and worked around it on v2.0.1 + v2.0.2 by branching releases directly from main.Mitigation
Workaround that's been used recently: branch release PRs directly from
main(cherry-pick path) instead ofdev, sidestepping the divergence cost per-release. Works for single-commit PATCH releases but doesn't scale — any release containing >1 commit that's only ondevcan't use the workaround.Real fix candidates (pick one):
/release-syncskill — automation that runs as the final step of/release: after the release PR merges to main, file a sync-back PR from main into dev that just merges main with-X theirs(dev wins everywhere the squash didn't change content, the squash commits become merge ancestors of dev). One PR per release.Option 1 is the lowest-friction; recommend filing as a separate
[Feature] /release-sync skillticket and then closing this bug when that ships.Investigation Notes
The divergence pattern is:
After release:
devstill has A, B, C, D as separate commits;mainhas only X. They share NO commits between them since the last fully-synced point. A future PRdev → maincontainingA — B — C — D — E(new commit E) will conflict on everything in A — D's diffs that X also touched.The sync-back direction is much friendlier:
git merge -X theirsfrom main into dev tells git "where there's a conflict, the dev version wins" — which is correct because dev already has those changes via the unsquashed commits. The merge commit then makes the squash commits an ancestor of dev, so futuredev → mainPRs only see the genuinely-new commits.Glossary
dev;mainonly receives release PRs fromdev(tagged with semver). Framework-only — managed projects stay trunk-based.upstream/main(notdev), pick or apply the single fix, PR to main. Only works for releases containing 1 commit.-X theirs