# RFC: GitOps Branching & Versioning Strategy > **Status:** ๐Ÿงช Experimental โ€” requesting feedback before implementation > **Author:** @trek-e > **Date:** 2026-03-19 ## Problem The current workflow is trunk-based: all PRs target `main`, the pipeline auto-bumps version on merge, and `@dev`/`@next`/`@latest` dist-tags promote through stages. This works but has gaps: 1. **No stable release branch.** If v2.33 ships a regression, the only fix path is forward โ€” merge to main, wait for the full pipeline. There's no branch to cherry-pick a hotfix onto. 2. **No batched releases.** Every merge to main triggers a version bump. Contributors can't group related features into a coordinated release. 3. **Ad-hoc branch naming.** Branch prefixes (`fix/`, `feat/`, `docs/`, `refactor/`) are conventional but not enforced. No automated integration branches collect work-in-progress. 4. **No pre-release channel for breaking changes.** Major version bumps (v3.0.0) have no staging area โ€” they'd need to land on main directly. ## Proposal: Git-Flow Lite with Automated Integration Branches A lightweight adaptation of git-flow that preserves our trunk-based CI speed while adding release stability. Three branch tiers: ``` main โ† production-ready, tagged releases only โ”œโ”€โ”€ release/2.34 โ† stabilization branch, created when 2.34 is feature-complete โ”œโ”€โ”€ release/2.33 โ† maintenance branch for hotfixes to the current stable โ””โ”€โ”€ next โ† integration branch for the next minor release โ”œโ”€โ”€ feat/1325-user-prefs โ”œโ”€โ”€ feat/1340-parallel-v2 โ””โ”€โ”€ fix/1326-silent-commit ``` ### Branch Roles | Branch | Purpose | Merges Into | Auto-Created | |--------|---------|-------------|--------------| | `main` | Production releases only. Every commit is a tagged release. | โ€” | โ€” | | `next` | Integration branch for the next minor version. PRs target here. | `main` (via release branch) | Yes, on version bump | | `release/X.Y` | Stabilization branch. Created when `next` is feature-complete. Only bugfixes allowed. | `main` + back-merged to `next` | Yes, via `/release` command or workflow | | `hotfix/X.Y.Z` | Emergency fixes for production. Cherry-picked from `next` or created fresh. | `release/X.Y` + `main` | No, manual | | `feat/-` | Feature work. Targets `next`. | `next` | No, developer creates | | `fix/-` | Bug fix. Targets `next` (or `release/X.Y` for hotfixes). | `next` or `release/X.Y` | No, developer creates | ### Version Scheme Semantic versioning with automated bump logic based on conventional commits (already implemented in `generate-changelog.mjs`): | Commit Type | Bump | Example | |-------------|------|---------| | `fix:` | Patch | 2.33.0 โ†’ 2.33.1 | | `feat:` | Minor | 2.33.0 โ†’ 2.34.0 | | `feat!:` / `BREAKING CHANGE` | Major | 2.33.0 โ†’ 3.0.0 | Pre-release versions on `next`: ``` 2.34.0-next.1 โ† first merge to next after 2.33.0 release 2.34.0-next.2 โ† second merge to next 2.34.0-next.N โ† continues until release/2.34 is cut ``` ### Lifecycle ``` 1. Development Developer creates feat/1325-user-prefs from next Developer opens PR targeting next CI runs on PR (build, test, typecheck, windows) PR is reviewed and merged to next Pipeline publishes gsd-pi@2.34.0-next.N with @next tag 2. Stabilization Maintainer runs: gh workflow dispatch create-release -- version=2.34 Workflow creates release/2.34 from next Only fix: commits allowed on release/2.34 (enforced by branch protection) Pipeline publishes gsd-pi@2.34.0-rc.N with @rc tag Back-merges fixes to next automatically 3. Production Release Maintainer approves prod-release for release/2.34 Pipeline merges release/2.34 โ†’ main, tags v2.34.0, publishes @latest release/2.34 branch is kept alive for patch releases (2.34.1, 2.34.2) 4. Hotfix Critical bug found in v2.34.0 Developer creates fix/1400-critical from release/2.34 PR targets release/2.34 Pipeline publishes 2.34.1-rc.1 for verification Merged to release/2.34 โ†’ auto-merged to main โ†’ back-merged to next ``` ## Automation: Workflow Additions ### 1. `create-release.yml` โ€” Release Branch Creation ```yaml name: Create Release Branch on: workflow_dispatch: inputs: version: description: "Release version (e.g., 2.34)" required: true type: string jobs: create-release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 with: ref: next fetch-depth: 0 token: ${{ secrets.RELEASE_PAT }} - name: Validate version format run: | if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+$'; then echo "::error::Version must be X.Y format (e.g., 2.34)" exit 1 fi - name: Create release branch run: | BRANCH="release/${{ inputs.version }}" git checkout -b "$BRANCH" git push origin "$BRANCH" echo "Created $BRANCH from next" - name: Configure branch protection env: GH_TOKEN: ${{ secrets.RELEASE_PAT }} run: | # Require PR reviews, block force-push, restrict to fix: commits gh api repos/${{ github.repository }}/branches/release%2F${{ inputs.version }}/protection \ -X PUT \ -f required_pull_request_reviews='{"required_approving_review_count":1}' \ -F enforce_admins=true \ -F allow_force_pushes=false \ || echo "::warning::Branch protection setup requires admin permissions" - name: Open tracking issue env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh issue create \ --title "Release v${{ inputs.version }}.0" \ --label "release" \ --body "## Release v${{ inputs.version }}.0 Release branch: \`release/${{ inputs.version }}\` Created from: \`next\` at $(git rev-parse --short HEAD) ### Checklist - [ ] All targeted fixes merged to release/${{ inputs.version }} - [ ] RC published and smoke-tested - [ ] CHANGELOG reviewed - [ ] Production deployment approved" ``` ### 2. `sync-next.yml` โ€” Auto-Create/Maintain `next` Branch ```yaml name: Sync Next Branch on: push: tags: - "v*" jobs: sync-next: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PAT }} - name: Ensure next branch exists and is up to date run: | git fetch origin next 2>/dev/null || true if git show-ref --verify --quiet refs/remotes/origin/next; then # next exists โ€” merge main into it (fast-forward if possible) git checkout next git merge origin/main --no-edit || { echo "::warning::Merge conflict merging main into next. Manual resolution required." exit 1 } else # next doesn't exist โ€” create from main git checkout -b next fi git push origin next ``` ### 3. `backmerge.yml` โ€” Auto Back-Merge Release Fixes to `next` ```yaml name: Back-merge Release Fixes on: push: branches: - "release/**" jobs: backmerge: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PAT }} - name: Back-merge to next run: | RELEASE_BRANCH="${GITHUB_REF#refs/heads/}" git fetch origin next git checkout next git merge "origin/${RELEASE_BRANCH}" --no-edit || { # Conflict โ€” open a PR instead of failing git merge --abort gh pr create \ --base next \ --head "${RELEASE_BRANCH}" \ --title "backmerge: ${RELEASE_BRANCH} โ†’ next (conflict)" \ --body "Automated back-merge from ${RELEASE_BRANCH} to next has a conflict. Please resolve manually." exit 0 } git push origin next env: GH_TOKEN: ${{ secrets.RELEASE_PAT }} ``` ## Pipeline Changes The existing `pipeline.yml` needs minor adjustments: | Current | Proposed | |---------|----------| | Pipeline triggers on `main` CI success | Pipeline triggers on `main`, `next`, and `release/*` CI success | | Dev publish uses `-dev.` | `next` uses `-next.N`, `release/*` uses `-rc.N` | | Prod release auto-bumps version | Prod release reads version from release branch | | Single `@latest` promotion | `@next` from `next` branch, `@rc` from release branches, `@latest` from main | ## Migration Path This can be adopted incrementally: 1. **Phase 1 (low risk):** Create `next` branch as an alias for `main`. PRs can target either. Pipeline handles both. Zero behavioral change. 2. **Phase 2:** Start targeting `next` for new feature PRs. `main` receives only merges from release branches. 3. **Phase 3:** Add `create-release.yml` workflow. Cut first release branch for the next minor. 4. **Phase 4:** Add back-merge automation. Enforce branch protection on release branches. ## What This Doesn't Change - **Conventional commits** โ€” same `feat:`, `fix:`, `refactor:` prefixes - **CI workflow** โ€” same build/test/typecheck gates on every PR - **Dev publish** โ€” still publishes on every merge (just to `next` instead of `main`) - **Prod approval** โ€” still requires manual environment approval - **Changelog generation** โ€” same script, just reads from release branch instead of main - **Docker images** โ€” same multi-stage build, same GHCR tags ## Open Questions 1. **Is the `next` branch worth the overhead?** Trunk-based is simpler. The main benefit is batched releases and a stable `main`. 2. **Should release branches be long-lived or ephemeral?** Long-lived enables patch releases (2.34.1, 2.34.2). Ephemeral (delete after merge) is simpler. 3. **How many simultaneous release branches?** Maintaining 2+ releases (current + previous) adds backport burden. Is `current + hotfix` enough? 4. **Should `next` branch get its own npm dist-tag?** Currently `@next` is promoted from `@dev`. With this model, `@next` would come from the `next` branch directly. 5. **Branch protection on `next`?** Require PR reviews? Or allow direct push for maintainers? ## Alternatives Considered ### Trunk-Based (Current) Pros: Simple, fast. Cons: No release stabilization, no hotfix path. ### Full Git-Flow Pros: Maximum control. Cons: Heavy โ€” `develop`, `release`, `hotfix`, `feature` branches with strict merge rules. Overkill for a team this size. ### GitHub Flow + Release Tags Pros: Simple branching, release via tags only. Cons: No stabilization period, same forward-only problem as current. ### Release Please / Semantic Release Pros: Fully automated versioning. Cons: Less control over release timing, doesn't solve the hotfix branch problem. ## Feedback Requested - Does this match how you want to manage releases? - Is the `next` branch overhead justified for this project's pace? - Which open questions have strong opinions? - Any workflows or automation missing?