Skip to content

feat(branches): site branches with preview links, three-way merge, and version restore - #484

Draft
DavidBabinec wants to merge 5 commits into
mainfrom
feat/site-branches
Draft

feat(branches): site branches with preview links, three-way merge, and version restore#484
DavidBabinec wants to merge 5 commits into
mainfrom
feat/site-branches

Conversation

@DavidBabinec

@DavidBabinec DavidBabinec commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What

Site branches: a private fork of the whole site (shell, tables, rows, files) that is edited in the same editor, reviewed, and merged back into main.

  • Every content row, table, and the site shell carry a branch_id; main is the live site. Admin requests name the branch in the X-Instatic-Branch header, collab documents in their doc ids.
  • Toolbar chip + context strip: create, switch, rename, delete, share a revocable preview link, update from main. Spotlight commands for all of it.
  • Merge review page (/admin/branches/:id/review): every planned change with its detail, pages rendered before and after with the changed, added, and removed nodes outlined from the plan's tree diff, field and schema diffs, file line diffs, a comment thread per change, and a request → review → merge flow. Editors without merge rights request a merge with a note; branch managers decline with a note or merge with a step-up, deciding each conflict inline. Merging deletes the branch when asked and writes only main's draft; nothing is published.
  • Version history: list a page's published versions and restore one into the draft.
  • Publishing, scheduling, public routes, forms, plugins, dashboard, and MCP reads stay pinned to main. Site export follows the branch.

Feature doc: docs/features/branches.md (including the "Merge review" section).

Why

Editors could not stage a redesign or a content campaign without it going live piecemeal, and had no way to hand a draft to someone outside the admin for review. Without the review page a merge was a blind dialog, so the feature ships with the review as its merge path.

Impact

  • Users: new branch controls in the Site, Content, and Data workspaces; publish controls disable on a branch with the reason inline; merges go through the review page.
  • Developers: repositories on site, data_tables, and data_rows take a BranchScope; site files are merge entities with path-collision conflicts; the collab relay refuses documents of deleted branches and reseeds a revived branch from its rows; rows written outside the editor parse through parsePageNode on load. Migrations 026 and 027 are additive (027 adds site_branch_merge_requests and site_branch_review_comments).
  • Client provider: local updates are held until the server names the document lineage, which removes a stale-reset race on row creation.
  • Review render responses are text/plain with a sandbox CSP and load into sandboxed srcdoc frames; plan reads redact rows the reader cannot see.

Verification

bun run build
bun test
bun run lint
bun run doctor
bunx playwright test tests/e2e/branches.e2e.ts tests/e2e/version-history.e2e.ts tests/e2e/branch-review.e2e.ts

The branch-review spec runs the whole flow across two accounts (owner and an editor persona without merge rights) on a fresh database.

…d version restore

Every content row, table, and the site shell now carry a branch: main is
the live site, and a branch is a private fork edited through the same
editor, addressed by the X-Instatic-Branch header on every admin request
and by branch-qualified collab doc ids. Branches are created, switched,
renamed, and deleted from a toolbar chip and a context strip; a branch can
be shared through a revocable preview link, updated from main, and merged
into main through a three-way review that surfaces conflicts per field.
Published versions of a page can be listed and restored into the draft.

Publishing, scheduling, public routes, forms, plugins, the dashboard, and
MCP headless reads stay pinned to main. The collab relay refuses documents
for deleted branches, keeps its invalidation bookkeeping per branch, and
reseeds a branch from its rows if a delete fails after it was tombstoned.

Verification: bunx tsc -b, bun test, bun run build, bun run lint, and
Playwright tests/e2e/branches.e2e.ts + tests/e2e/version-history.e2e.ts.
…ads and requests

Review a branch before merging: files become merge entities, the plan carries per-change detail (fields, page tree diffs, file text), merge requests and comments live on the branch, and /admin/branches/:id/review shows one timeline per change with before/after page renders whose highlights come from the tree diff.
Highlight labels read as Changed/Added/Removed with the node name when it has one, short pages get short frames, the old merge dialog is now UpdateBranchDialog (merging lives on the review page), BRANCH-005 merges from the page, branches and audit docs describe the review.
- Plan reads redact rows the reader cannot see; file path collisions become
  conflicts; render responses are text/plain with a sandbox CSP.
- One open request per branch (partial unique index), ISO timestamps, closed
  states behave the same for everyone.
- Page rows written outside the editor parse through parsePageNode, so the
  collab seeder never meets a node without its maps (the 'change reverted'
  storm), and the review compares nodes as the editor loads them; node labels
  come from the editor name.
- Review frames re-measure on mark changes without touching refs in render;
  the layout is imported through its barrel (one chunk).
import { afterEach, describe, expect, it } from 'bun:test'
import { MAIN_SCOPE } from '../../../server/branches/scope'
import { applyBranchMerge, planBranchMerge } from '../../../server/branches/merge'
import { getDataRow, listDataRows, saveDataRowDraft, upsertDataRowDraft } from '../../../server/repositories/data'
- Rows of borderless surface tiles on the workspace canvas, card radius,
  1px-gap tile groups instead of dividers; state and kind badges are
  TagPills; the facts sit in one row on wide screens.
- TagPill gains a state tone (success, warning, danger) that keeps the
  gradient tint but colours it from the semantic tokens.
- The E2E home seed is exact, so a reused database cannot skew the diff.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant