chore: require admin approval to merge the main→dev sync PR - #9491
Conversation
The auto-sync PR was being squash-merged, which rewrites the synced commits into new SHAs on dev. Git then no longer recognizes that dev already contains hotfixes applied directly to main (e.g. #9466, #9460), so the next release→main merge re-conflicts on those exact changes (as seen in #9490). Enable auto-merge with --merge so the sync PR merges as a merge commit once approved and checks pass, keeping main's commits as real ancestors of dev. Also update the PR body to state the merge behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
decentraland-bot
left a comment
There was a problem hiding this comment.
STEP 2 — Root-cause check: PASS ✅
The PR correctly identifies that squash-merging the chore/sync PR rewrites commit SHAs, preventing Git from recognizing dev as containing hotfixes applied directly to main. Enforcing merge commits preserves the original SHAs and eliminates the divergence. This addresses the root cause, not a symptom.
STEP 3 — Design & integration: N/A
This PR modifies only a CI workflow file (.github/workflows/auto-sync-main-to-dev.yml). No application code, ECS systems, or runtime components are changed. No lifecycle owner analysis required.
STEP 4 — Member audit: N/A
No public properties or accessors changed (workflow YAML only).
STEP 5 — Line-level review
P1 — PR title uses invalid type ci:
The semantic / title-matches-convention CI check is failing because ci is not in the repo's allowed type list:
break,chore,docs,feat,fix,refactor,revert,style,test,release
The branch name is chore/enforce-merge-commit-on-main-dev-sync, so the title should match:
chore: force merge commit on main→dev sync PR
This blocks merge — please update the PR title.
P2 — Missing trailing newline at EOF
See inline comment on line 35.
Security review
No security issues found. The workflow uses ${{ secrets.GITHUB_TOKEN }} (standard GitHub Actions practice), no hardcoded secrets, and no injection vectors — all gh CLI arguments are fixed literals with no user-controlled interpolation.
STEP 6 — Complexity: SIMPLE
Workflow YAML change, 1 file, +7/−2 lines. No runtime code, no ECS, no async patterns.
STEP 7 — QA assessment: NO
CI-only change — no user-facing runtime behavior affected.
STEP 8 — Non-blocking warnings: None
STEP 9 — Verdict
REVIEW_RESULT: FAIL ❌
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Single CI workflow file with a small YAML change — no runtime code touched
QA_REQUIRED: NO
Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub
decentraland-bot
left a comment
There was a problem hiding this comment.
PR Review — ci: force merge commit on main→dev sync PR
STEP 2 — Root-cause check: ✅ PASS
The PR correctly identifies the root cause: squash-merging the chore/sync PR rewrites commit SHAs on dev, so Git no longer recognizes that dev already contains hotfixes from main. The fix — forcing merge commits via gh pr merge --merge --auto — preserves shared commit ancestry and prevents future release conflicts. Evidence cited in #9490 (duplicate SHAs from squashed sync) confirms this is the cause, not a symptom.
STEP 3 — Design & integration: N/A
CI workflow file only — no runtime code, no ECS systems, no new long-lived units.
STEP 4 — Member audit: N/A
No code members introduced or modified.
STEP 5 — Line-level review
| # | Sev | File | Issue |
|---|---|---|---|
| 1 | P2 | auto-sync-main-to-dev.yml:30 |
gh pr create failure blocks auto-merge enablement (see inline) |
| 2 | P2 | auto-sync-main-to-dev.yml:35 |
Missing newline at end of file |
Security review
No new security issues introduced:
GITHUB_TOKENis correctly passed viaenv:block (no script injection risk)- No untrusted input flows into
run:blocks — all values are static strings - Workflow triggers (
pushtomain,workflow_dispatch) are trusted
Pre-existing items (out of scope for this PR):
actions/checkout@v6uses a mutable tag instead of a pinned SHA- No explicit
permissionsblock (inherits repo defaults)
STEP 6 — Complexity: SIMPLE
STEP 7 — QA: NO
CI-only change — no runtime code affected, nothing ships in the Unity player.
STEP 8 — Non-blocking warnings: None
REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Single CI workflow file change (+7/−2) with no runtime code impact
QA_REQUIRED: NO
Reviewed by Jarvis 🤖 · Requested by Juan Ignacio Molteni [Dalkia] (<@U03JSUQ5Z7U>) via Slack
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
|
Thanks for the deep dive — the
The last three sync PRs were all authored by
So on these PRs the Net: approve → required checks go green (as they always have) → auto-merge merges as a merge commit. The "require approval before merge" behavior is exactly what Note on I'll keep an eye on the first real run since auto-merge itself is new here, but no fix needed for this finding — leaving as-is. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Great catch on the root cause — I independently verified the diagnosis and it's correct: #9479 (the sync merged right before the #9490 conflicts) was squash-merged (its commit has a single parent), which rewrites the synced commits and makes That said, I'm requesting changes — as written the new step will fail on every run, because of a branch-protection rule the prerequisites don't account for. 🚫 Blocker:
|
Replace the auto-merge approach (impossible under dev's required linear history — the bot cannot create a merge commit) with an enforcement gate: the enforce-approvals required check now fails on PRs from chore/sync until at least one approver has admin permission on the repo. The admin then merges the sync PR as a merge commit, bypassing linear history. Also make the sync PR creation idempotent so runs no longer fail when a chore/sync PR is already open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in-dev-sync' into chore/enforce-merge-commit-on-main-dev-sync
|
@charly-bg — you were right, and the PR has pivoted accordingly (see the updated description). Point-by-point: 🚫 Linear-history blocker — confirmed, and it killed the auto-merge approach. Removing
Minor notes: |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Re-reviewed at 🚫 The admin gate would not have prevented #9479 — the incident it was built forI ran the new logic against the last five sync PRs. It gates who approves, but the divergence was caused by which merge button was clicked — and those are independent:
Two things fall out of that table: On #9479 the gate is a no-op. An admin approved it at 09:53 and the same admin merged it at 11:02, choosing Squash. #9433 and #9458 show the gate is bypassed by exactly the population it targets. Both landed with zero approvals, so I don't think that makes the PR wrong — the accountability signal and the documented rule in the body are real improvements, and the idempotency fix is a genuine bug fix worth landing on its own. But the body currently reads as if the mechanism closes the hole, and it doesn't. I'd reframe it as a speed bump and move the escalation path off "designed and ready" onto the actual backlog. 💡 Cheap mechanical alternative: assert the invariant post-mergeRather than trying to control the merge method pre-merge (which needs the ruleset + bypass-actor work), detect the actual invariant right after it happens — no branch-protection change, no bypass actor, no new token: # on: push: branches: [dev]
- run: |
AHEAD=$(gh api repos/${{ github.repository }}/compare/dev...main --jq '.ahead_by')
if [ "$AHEAD" -ne 0 ]; then
echo "::error::main has $AHEAD commit(s) not reachable from dev — sync diverged"
exit 1
fi
|
A 404 on the collaborator-permission lookup (bot or offboarded reviewer) or a transient failure fetching reviews aborted the step under bash -e before MISSING_MSG was written, so the status step blamed missing QA/DEV approvals instead. Treat those as "no permission" / "no approvers" and keep evaluating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The admin gate alone did not close the hole: on #9479 the same admin approved and then squash-merged, so the gate was green on the exact incident it was built for. Remove the merge button from the loop instead — when a repo admin approves the chore/sync PR, the new auto-merge-sync-pr workflow waits for dev's required checks and merges it with `gh pr merge --merge --admin` (merge method hardcoded, squash unreachable; --admin bypasses the linear-history rule with the same privilege admins use manually today). The sync PR body now leads with "approve, don't merge" and documents the bot flow, and the bot comments recovery steps on check failures or conflicts. Also narrow the pr-create fallback: an explicit open-PR check replaces the blanket `|| echo`, so real create failures fail the run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in-dev-sync' into chore/enforce-merge-commit-on-main-dev-sync
This comment has been minimized.
This comment has been minimized.
|
@charly-bg — the five-PR table convinced us, especially #9479 being green under the gate. The PR has pivoted again (updated description, new commits): instead of gating who can approve and hoping the right button gets clicked, automation now performs the merge itself. Point by point: The pivot:
|
|
Re-reviewed at Still requesting changes — the checklist item you flagged comes back negative, and it's fatal to the merge step. 🚫 Blocker 1 —
|
…ions Revert the auto-merge workflow: ORG_ACCESS_TOKEN belongs to decentraland-bot (write permission), which cannot create merge commits under dev's classic linear-history protection, and the alternative — an admin-owned token — bypasses all of dev's protection, not just linear history. The mechanical enforcement design (ruleset with a scoped bot bypass) is documented in #9566 and will be applied if a squash recurs. The manual procedure stays: the enforce-approvals required check demands an admin approval on chore/sync PRs, and the sync PR body instructs merging as a merge commit, now linking #9566. The approver API-failure tolerance and the idempotent pr-create guard are kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
|
Update — the auto-merge pivot announced above is reverted ( What changed our mind: we verified the token. So the strategy is: land the speed bump now — your reframing is adopted in the description, it's explicitly not claimed to be enforcement — and the mechanical fix is fully designed and parked in #9566 (ruleset with |
This comment has been minimized.
This comment has been minimized.
|
Re-reviewed at First, though, a correction I owe you. 🔄 I was wrong that the gate is a no-op — it addresses 2 of the 3 recent incidentsMy #9479 table was built from the five most recent sync PRs. I've now audited all 118 Six squashes, all-time (1-parent merge commits): #2507 (2024-10-21), #2531 (2024-10-22), #2769 (2024-11-12), then #9014 (2026-06-19), #9060 (2026-06-30), #9479 (2026-07-27). In all three recent cases the approver and the merger were the same person. The permission split is what matters:
A non-admin's approval doesn't satisfy the new gate, and a non-admin can't merge past a red required check ( To keep it precise: the gate forces an admin into the loop, it doesn't make Squash unreachable. Once any admin approves, all 55 write-access collaborators can merge, and non-admins still see only the Squash button. So the residual is broader than "an admin squashes it": it's "an admin approves, then anyone squashes it."
|
This comment has been minimized.
This comment has been minimized.
|
@charly-bg on the already-open sync PR body refresh ( There's no open If a long-lived sync PR ever becomes a real pattern, adding the Separately, I've updated #9566 with the full picture per your audit: three squashes in seven weeks (#9014, #9060, #9479), the per-incident gate table, and the self-healing window note. I also reframed its trigger to "an admin-approved squash", since the gate already blocks the non-admin #9014/#9060 pattern. Thanks for the thorough review 🙏 |
|
Claude finished @dalkia's task in 2m 51s —— View job Post-merge re-review —
|
| dependency / file | version | source | type | scope | risk | confidence |
|---|---|---|---|---|---|---|
.github/workflows/enforce-group-approvals.yml |
n/a | in-repo | CI config | CI-only | LOW | HIGH |
.github/workflows/auto-sync-main-to-dev.yml |
n/a | in-repo | CI config | CI-only | LOW | HIGH |
The new-dependency label is still attached per the PR metadata, and is still a labeler misfire.
Summary
No diff change on this event, and the merged state on dev is byte-identical to the approved revision. Security posture unchanged and clean: no HIGH on W.1–W.10, no bypass primitive, no new secret consumer.
The one thing I'd act on is operational, not a code change: the gate is enforcing on dev now, but the explanatory body text won't reach a sync PR until this lands on main. Until then, whoever merges the next chore/sync PR sees a red required check with no instructions — worth a heads-up so the merge-commit procedure is followed in that gap.
Everything else is carried-forward minors, plus @charly-bg's tracking-side asks (which @dalkia has since applied to #9566: three squashes recorded, trigger narrowed to admin-approved) and dropping the new-dependency label.
DEPENDENCY_REVIEW: NEEDS_ATTENTION
I made no changes — GitHub App permissions don't allow modifying files under .github/workflows/, and there's nothing here I'd change in code anyway. gh is outside my tool allowlist in this environment, and the clone is shallow with fetch unavailable, so #9566, the 118-PR squash audit, dev's live branch protection, and main's current copy of auto-sync-main-to-dev.yml are reasoned from the review discussion and trigger semantics rather than fetched. Everything stated as "verified mechanically" was checked against local refs.
· Branch ·
Pull Request Description
What does this PR change?
The
Create PR for syncing main to devworkflow (.github/workflows/auto-sync-main-to-dev.yml) only opened thechore/syncPR and left the merge method up to whoever clicked the button. That PR has been getting squash-merged, which is what caused the release conflicts in #9490.Why squash breaks the sync: squashing rewrites the synced commits into new SHAs on
dev, so Git no longer recognizes thatdevalready contains hotfixes applied directly tomain. The originals stop being ancestors ofdev, and the nextrelease → mainmerge re-conflicts on those exact same changes.Concrete evidence from #9490 (
release/2026-07-27 → main):mainbut missing fromdev/release, so all 6 conflicts were its files (including anadd/addonDeepLinkAllowlist.cs).537f9fcf0eonmainvs481ff99a23on release — the fingerprint of a squashed sync rewriting shared commits.The strategy: keep it simple — admin approval + explicit instructions
The manual admin merge-commit procedure is not a new invention: it kept
mainanddevin sync for years while admins were aware of it (#9433, #9458, #9504, #9523 were all merged correctly by hand). What failed in #9479 was awareness in the moment, not the mechanism. So this PR restores and reinforces exactly that, without adding new machinery to the repo:enforce-group-approvals.yml— theenforce-approvalscheck (already required ondev) now fails on PRs whose head branch ischore/syncuntil at least one approver has admin permission (checked live via the collaborators API; no team to maintain). All other PRs keep the existing QA/DEV logic untouched, including theauto-prskip. The approver scan now tolerates API failures: a 404 on the permission lookup (bot or offboarded reviewer) or a transient failure fetching reviews no longer aborts the step with a misleading "Waiting on: QA approval, DEV approval" status.auto-sync-main-to-dev.yml— the sync PR body now states the procedure explicitly (admin merges as a merge commit, never squash, and why), and links the escalation plan ([TECH DEBT] CI/Branch protection | Enforce merge-commit syncs via ruleset with bot bypass (if squash recurs) #9566) so future admins have the context in front of them.gh pr createis now guarded by an explicit open-PR check instead of|| echo, so a bad token or a failed create fails the run instead of logging a reassuring message.No repo settings changes are required — this works with
dev's branch protection exactly as configured today.What this deliberately is — and isn't
This is an accountability speed bump, not mechanical enforcement: GitHub cannot restrict merge methods per-PR, admins bypass required checks (
enforce_admins: false), and the gate would have been green on #9479 itself (same admin approved and squashed). We know that and accept it — the gate guarantees an aware admin is in the loop, and the instructions make the right action unambiguous at the moment of merge.An auto-merge bot was prototyped on this branch and reverted after verification:
ORG_ACCESS_TOKENbelongs todecentraland-bot(permission:write), which cannot create merge commits underdev's linear-history protection — and the alternative, an admin-owned token, would bypass all ofdev's protection (checks, reviews), an over-powered credential reachable by any workflow in the repo.If a squash slips through again, the escalation is designed, pre-analyzed, and tracked in #9566: move
required_linear_historyinto a repository ruleset with a narrowly-scoped bypass (the sync bot, PR-merges only, still bound by checks and the admin-approval gate) and re-land the reverted auto-merge workflow. We're not applying it preemptively — we don't want to complicate the repo's governance more than it already is for a failure mode the simple procedure historically prevented.Test Instructions
This is a CI workflow change; there is no in-app behavior to run.
Steps:
enforce-approvalschange applies to sync PRs once this lands ondev. On the nextchore/syncPR, verify the check stays red with only non-admin approvals and flips green after an admin approves.auto-sync-main-to-dev.ymlchange (PR body text, idempotent create) takes effect once it reachesmain, since that workflow runs frommain.gh api repos/decentraland/unity-explorer/compare/dev...main --jq .ahead_byreturns0once the sync PR is merged.Is it QA-able? No — CI-only change.