- Rebrand commits already in history (gsd → forge) - Sync pre-existing doc, docker, and CI config updates - All rebrand artifacts verified in place: * Native crates: forge-engine, forge-ast, forge-grep * Log prefixes: [forge] across 22+ files * Binary: ~/bin/sf-run * Workspace scopes: @sf-run/*, @singularity-forge/* * Nix flake: Rust toolchain ready System ready for: nix develop && bun run build:native Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
299 lines
11 KiB
Markdown
299 lines
11 KiB
Markdown
# 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/<issue>-<slug>` | Feature work. Targets `next`. | `next` | No, developer creates |
|
|
| `fix/<issue>-<slug>` | 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 sf-run@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 sf-run@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.<sha>` | `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?
|