decentraland / decentraland/unity-explorer
[TECH DEBT] CI/Branch protection | Enforce merge-commit syncs via ruleset with bot bypass (if squash recurs)
- Dominant language
- C#
- Stars
- 23
- Forks
- 17
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 101
Description
### π **Priority Level:**
Low β apply only if an **admin-approved** sync PR gets squash-merged again despite the admin-approval gate from #9491. (The gate already blocks the non-admin squash pattern β see the incident history below.)
### π **Area/Component:**
CI / branch protection β `dev` branch, `chore/sync` (mainβdev) PR flow. Workflows: `auto-sync-main-to-dev.yml`, `enforce-group-approvals.yml`.
### π **Description:**
The mainβdev sync PR must be merged **as a merge commit** to keep every `main` commit an ancestor of `dev` (`ahead_by == 0` on `compare/dev...main`). Squashing it rewrites the synced commits into new SHAs, `main` and `dev` diverge silently, and the damage surfaces later as release-merge conflicts (#9479 β the six conflicts in #9490).
The current mitigation (#9491) is procedural: the `enforce-approvals` required check demands an admin approval on `chore/sync` PRs, and the PR body instructs "merge as a merge commit, never squash". This issue records the mechanical enforcement design, pre-analyzed and ready to apply if the residual failure mode recurs.
**Squash history (all 118 `chore/sync` PRs audited):** six squashes all-time β #2507 (2024-10-21), #2531 (2024-10-22), #2769 (2024-11-12), then a cluster of three in seven weeks: **#9014 (2026-06-19), #9060 (2026-06-30), #9479 (2026-07-27)**. After #2769 there were ~19 months clean (~75 sync PRs, zero squashes), so "the manual procedure worked for years while admins were aware of it" (#9433, #9458, #9504, #9523 all merged correctly by hand) is genuinely true of that stretch β but the current regime is degrading, not stable.
**What the #9491 admin-approval gate covers, per recent incident:**
| PR | self-approved + merged by | permission | gate verdict |
|---|---|---|---|
| #9014 | davidejensen | write | β red β merge blocked |
| #9060 | NickKhalow | write | β red β merge blocked |
| #9479 | mikhail-dcl | admin | β
green β not blocked |
A non-admin's approval doesn't satisfy the gate, and a non-admin can't merge past a red required check (`restrictions: none`, `enforce_admins: false` β only admins bypass), so the gate would have **blocked #9014 and #9060**. It does **not** stop #9479's shape: 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 this issue exists to close is narrower than "a squash recurs": it is **"an admin approves, then anyone squashes it."**
**Why two of the three squashes went unnoticed:** a squash's divergence is self-healing at the next merge-commit sync β merging `main` into `dev` makes `main`'s tip (and its whole history, including the pre-squash originals) an ancestor of `dev` again (`ahead_by: 0` holds right now despite six historical squashes). A squash is therefore only damaging if a `release β main` merge lands inside the window before the next correct sync. #9014 and #9060 were healed by #9103 (2026-07-01) with no release merge in the window and went unnoticed; #9479's #9490 release merge landed **inside** the window β six conflicts. That sharpens the case for the `ahead_by == 0` tripwire below: its value is catching divergence *during* the self-healing window, when repair is one merge instead of a conflicted release.
### π **Current State:**
- `dev` uses **classic branch protection** with `required_linear_history: true` and `enforce_admins: false`. Rebase-merge is disabled repo-wide.
- Consequence: non-admins only ever see the **Squash** button on PRs to `dev`; merge commits are possible only via the blanket admin exemption β which also exempts admins from required checks and reviews (all-or-nothing).
- The sync-PR merge method is therefore a human choice made under that pressure, guarded only by the admin-approval gate and PR-body instructions from #9491.
- An auto-merge bot was prototyped in #9491 and reverted: `ORG_ACCESS_TOKEN` belongs to `decentraland-bot` (permission: `write`), which cannot create merge commits under classic protection, and `gh pr merge --merge --admin` requires an admin-owned token β an over-powered credential (a stolen admin token bypasses **all** of `dev`'s protection, not just linear history).
### π‘ **Proposed Solution:**
Move the linear-history rule to a **repository ruleset**, which supports per-actor bypass β granting exactly the one exemption classic protection cannot express:
1. Create ruleset `linear-history-dev` targeting `dev`, containing only *Require linear history*, **Active**. Bypass list:
- Team `sync-automation` (sole member: `decentraland-bot`) β bypass mode **Pull requests only** (rulesets do not accept individual users as bypass actors, hence the one-member team).
- **Repository admin** role β bypass mode **Always** (preserves the manual merge path and the emergency repair: `git checkout dev && git merge main && git push`).
2. Verify the ruleset is active and targeting `dev`, **then** untick *Require linear history* in classic protection (rules stack most-restrictive, so there is no unprotected window; rollback is re-ticking the checkbox). All other classic protection stays untouched.
3. Re-land the auto-merge workflow from #9491 (`auto-merge-sync-pr.yml`, reverted there β recoverable from that branch's history): on an admin approval of a `chore/sync` PR, it verifies the approver's permission, waits for required checks (`gh pr checks --required --watch`), and merges with `gh pr merge --merge` (no `--admin` needed once the ruleset grants the bypass). Merge method hardcoded β squash unreachable.
Resulting trust model: the bot can add merge commits to `dev` **only via a PR merge**, still gated by required checks and the admin-approval required check. A stolen `ORG_ACCESS_TOKEN` cannot direct-push to `dev`, cannot skip CI, and cannot manufacture the human admin approval β it can only complete a sync an admin already approved.
Complementary (independent, ~10 lines): a divergence tripwire on `push: [dev]` asserting `ahead_by == 0` on `compare/dev...main`, alerting on transition (deduped via a tracking issue / Slack). Catches every divergence cause (squash, manual admin merges, direct pushes to `main`), not just the one the bot prevents.
### βοΈ **Impact Assessment:**
#### Performance Impact:
None β CI/settings only.
#### Maintainability Impact:
Positive for the sync flow (removes a known human trap; ruleset bypasses are audit-logged, unlike silent classic admin exemptions). Cost: one more moving part in repo governance β a ruleset alongside classic protection, a one-member team, and a bot with standing (narrow) merge power. This is exactly the complication we chose **not** to take on preemptively in #9491.
#### Risk of Refactoring:
Low with the ordered rollout (ruleset first, verify, then untick). Residual: verify on the first real sync PR that the bot's PR-only bypass satisfies the merge API; any workflow with repo `write` can wield `ORG_ACCESS_TOKEN`, but its post-change capability is bounded as described above.
### π **Effort Estimate:**
S β ~10 minutes of admin configuration + restoring the reverted workflow file (minus `--admin`).
### π **Dependencies:**
- #9491 β the admin-approval gate and instructions (the "get started" strategy); the reverted `auto-merge-sync-pr.yml` lives in that branch's history.
- #9490 / #9479 / #9014 / #9060 β the incidents motivating all of this.
- Requires a repo admin (settings + team creation); no new tokens or secrets.
### π₯οΈ **Additional Notes:**
Trigger condition: another **admin-approved** squashed sync (the gate already blocks the non-admin pattern of #9014/#9060) β detected by `gh api repos/decentraland/unity-explorer/compare/dev...main --jq .ahead_by` returning non-zero with no open `chore/sync` PR. Immediate remediation (before applying anything here): an admin runs `git checkout dev && git pull && git merge main && git push` β conflict-free if done promptly, since the squash already applied identical content.
Contributor guide
Research direction
Review auto-merge-sync-pr.yml in the reverted #9491 history alongside auto-sync-main-to-dev.yml and enforce-group-approvals.yml. Start by checking the current dev protection and the compare/dev...main ahead_by value with gh api, then configure and verify the ruleset before restoring the workflow. Done means admin-approved sync PRs merge only as merge commits through the bot, with required checks preserved and divergence reporting in place.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, github-actions
- Domain
- ci-cd, devops
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100