feat(P06): 3-tier versioning, branch hierarchy enforcement, ARCHITECTURE-PLAN synthesis

---ci---
phase: 6
milestone: v0.5
status: complete
decisions:
  - id: D-006
    decision: Research as intermediate work product
    rationale: Conclusions update .ci/ files; full research doc intentionally not preserved
    confidence: 0.90
  - id: D-007
    decision: Branch hierarchy enforcement: main > milestone > phase
    rationale: Prevents out-of-order merges and semantically wrong tags
    confidence: 0.92
  - id: D-008
    decision: 3-tier versioning: NFR/feature/schema-breaking
    rationale: Patch per phase (NFR/feature) or minor per phase (schema-breaking); milestone gets minor (feature) or major (schema-breaking)
    confidence: 0.95
requirements:
  covered: [VER-06, BRANCH-01, BRANCH-02, ARCH-01]
---/ci---

- Synthesize ARCHITECTURE-PLAN.md into .ci/ci/ARCHITECTURE.md (expanded 51→230 lines)
- Add D-006/D-007/D-008 to .ci/ci/PROJECT.md key decisions table
- Delete ARCHITECTURE-PLAN.md after synthesis
- Rewrite ship.md with 3-tier versioning model + branch hierarchy merge flows
- Rewrite branch-strategy.md with 3-tier versioning + branch hierarchy + version validation
- Add MilestoneType to config types
- Replace isNfrMilestone() with getMilestoneType() returning nfr|feature|schema-breaking
- Add validateMergeOrder(), mergeMilestoneBranch(), computeMilestoneTag() to GitBranch
- Add computeShipVersion(), validateVersionOrder(), resolveMergeTarget() to ship command
- Remove hardcoded v0.5. from error-recovery rollback
- Create .githooks/pre-push for semver ordering + branch hierarchy validation
- Add 15 new tests (370 total, all passing)
This commit is contained in:
Jon Chery
2026-05-29 17:18:10 +00:00
parent 3d069319b5
commit ab6af144b7
14 changed files with 696 additions and 93 deletions
+106 -47
View File
@@ -23,7 +23,7 @@ Canonical branch naming and lifecycle conventions for CI. Branches encode projec
**Lifecycle:**
1. Created at phase start by `GitBranch.createPhaseBranch()`
2. All task commits for the phase land on this branch
3. Merged to main (squash) on phase completion
3. Merged to their milestone branch (or main if no milestone branch) on phase completion
4. Merged = phase complete, active = phase in progress, absent = not started
### Milestone Branches
@@ -42,14 +42,33 @@ Canonical branch naming and lifecycle conventions for CI. Branches encode projec
**Lifecycle:**
1. Created at first phase of milestone by `GitBranch.createMilestoneBranch()`
2. Spans multiple phases within the same milestone
3. Merged to main on milestone completion
4. Merged = milestone complete, active = milestone in progress
3. All phase branches merge into this branch on completion
4. Merged to main on milestone completion
5. Merged = milestone complete, active = milestone in progress
### Hotfix Branches
**Format:** `hotfix/description`
Created for urgent fixes outside the normal phase flow. Merged directly to main.
Created for urgent fixes outside the normal phase flow. Merged directly to main (exception to hierarchy).
## Branch Hierarchy (Enforced)
```text
main ─── milestone/vX.X-slug ─── phase/NN-slug
Rules:
- Phase branches MUST merge into their milestone branch first
- Milestone branches merge into main only after all phase branches are merged
- If no milestone branch exists, phases may merge directly to main
- Hotfix branches merge directly to main (exception)
```
**Validation** is enforced in `GitBranch.mergePhaseBranch()` and `createShipCommand()`:
- Phase → main: rejected if milestone branch exists for this milestone
- Phase → milestone: allowed
- Milestone → main: allowed only after all phase branches are merged
- Hotfix → main: allowed
## Branch Status Inference
@@ -69,67 +88,97 @@ const branches = gitContext.getBranches();
## Merge Strategy
Default: **squash merge** into main.
Default: **squash merge**.
Phase branches squash-merge into their milestone branch. Milestone branches squash-merge into main. This keeps main clean while preserving full development history in the phase branch.
```typescript
gitBranch.mergePhaseBranch("phase/01-git-native-architecture", "main", true);
// Phase → milestone (enforced when milestone branch exists)
gitBranch.mergePhaseBranch("phase/01-git-native-architecture", "milestone/v0.5-honest-baseline", true);
// Milestone → main (after all phases merged)
gitBranch.mergeMilestoneBranch("milestone/v0.5-honest-baseline", "main", true);
```
Squash merge keeps main clean while preserving full development history in the phase branch. Phase branches can be deleted after merge if desired.
Phase branches can be deleted after merge if desired.
## Versioning and Releases
**Every merge to main creates a release. No exceptions.** Versioning maps to project structure:
**Every merge to main creates a release. No exceptions.** Versioning follows a 3-tier model based on milestone type:
| Version Part | When | Example |
|-------------|------|---------|
| **Major** (X.0.0) | Project-level refactor or schema change | `v1.0.0` |
| **Minor** (0.X.0) | Every milestone completion | `v0.3.0` |
| **Patch** (0.0.X) | Every phase completion | `v0.2.3` |
### 3-Tier Versioning Model
### Phase completion (patch release)
| Milestone Type | Condition | Phase release | Milestone release |
|---------------|-----------|---------------|-------------------|
| **NFR** | All phases: fix/chore/docs/perf/refactor/test | Patch (`vX.Y.Z`) | None |
| **Feature** | Any phase is `feat`, no schema break | Patch (`vX.Y.Z`) | Minor — `vX.(Y+1).0` |
| **Schema-breaking** | Refactor/schema break/new direction | Minor — `vX.(Y+N).0` per phase | Major — `v(X+1).0.0` |
**IMPORTANT:** Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the milestone is implicit from the patch sequence
Determine milestone type via `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`.
### Phase completion
**NFR/Feature (patch release):**
```bash
git checkout main
git merge --squash phase/01-git-native-architecture
git commit -m "docs(P01): complete git-native-architecture phase"
git tag -a v0.2.1 -m "v0.2.1: git-native-architecture"
git checkout milestone/v0.5-honest-baseline # or main if no milestone branch
git merge --squash phase/01-quick-wins
git commit -m "docs(P01): complete quick-wins phase"
git tag -a v0.5.1 -m "v0.5.1: quick-wins"
git push origin main --tags
# Create Gitea release for v0.2.1
# Create Gitea release for v0.5.1
```
Phase number within the milestone determines the patch version (1st phase = .1, 2nd phase = .2, etc.)
### Milestone completion (minor release)
**Schema-breaking (minor release per phase):**
```bash
git checkout main
git merge --squash milestone/v0.2-git-native
git commit -m "docs(milestone): complete git-native"
git tag -a v0.2.0 -m "v0.2.0: git-native"
git checkout milestone/v0.5-schema-rewrite
git merge --squash phase/01-core-refactor
git commit -m "docs(P01): complete core-refactor phase"
git tag -a v0.5.0 -m "v0.5.0: core-refactor"
git push origin main --tags
# Create Gitea release for v0.2.0 with full milestone summary
# Create Gitea release for v0.5.0
```
### Major release
Each schema-breaking phase bumps the minor. 1st phase = next available minor, 2nd = minor+1, etc.
When the project undergoes a schema-breaking change (e.g., switching from file-based to git-native architecture), bump the major version. Major releases follow the same merge → tag → release flow.
### Milestone completion
## NFR Milestone Versioning
**Feature (minor release):**
```bash
# All phases already merged into milestone branch
git checkout main
git merge --squash milestone/v0.5-honest-baseline
git commit -m "docs(milestone): complete honest-baseline"
git tag -a v0.6.0 -m "v0.6.0: honest-baseline" # NEXT minor, NOT v0.5.0
git push origin main --tags
# Create Gitea release for v0.6.0 with full milestone summary
```
NFR milestones and feature milestones follow different versioning rules:
**Schema-breaking (major release):**
```bash
# All phases already merged into milestone branch
git checkout main
git merge --squash milestone/v0.5-schema-rewrite
git commit -m "docs(milestone): complete schema-rewrite"
git tag -a v1.0.0 -m "v1.0.0: schema-rewrite" # NEXT major
git push origin main --tags
# Create Gitea release for v1.0.0 with full milestone summary
```
**NFR milestones** — all phases are `fix`, `chore`, `docs`, `perf`, `refactor`, or `test`:
- Each phase gets a progressive patch version (v0.1.1, v0.1.2, v0.1.3)
- No separate milestone tag — the milestone is implicit from the patch sequence
- Example: milestone v0.1 with phases P01 (chore), P02 (test), P03 (perf) → v0.1.1, v0.1.2, v0.1.3
**NFR milestones produce no milestone tag.** The last phase's patch version is the final release.
**Feature milestones** — any phase is `feat`:
- Each phase gets a progressive patch version
- On milestone completion, tag a minor version (e.g., v0.2.0)
- Example: milestone v0.2 with phases P01 (feat), P02 (feat), P03 (fix) → v0.2.1, v0.2.2, v0.2.3 + milestone tag v0.3.0
### Version Validation
Determine milestone type by checking `isNfrMilestone()` which inspects all phase commit types within the milestone.
Before creating any tag:
1. Tag must be strictly greater than all existing tags on the same major.minor line
2. Milestone completion tag must be next minor (feature) or next major (schema-breaking)
3. NEVER create a tag that is semantically below existing phase tags
## Multi-Project Branch Naming
@@ -155,7 +204,7 @@ const milestones = gitBranch.listMilestones();
## Branch Creation Rules
1. Always create phase branches from main (or the current milestone branch)
1. Always create phase branches from the current milestone branch (or main if no milestone branch exists)
2. Never create a branch for a completed phase — it should already be merged
3. Milestone branches span phases — don't create one per phase
4. Use `GitBranch.createPhaseBranch()` to ensure consistent naming
@@ -164,25 +213,35 @@ const milestones = gitBranch.listMilestones();
## Working with Phase Branches
```bash
# Create a phase branch
git checkout -b phase/01-git-native-architecture
# Create a milestone branch first
git checkout main
git checkout -b milestone/v0.5-honest-baseline
# Create a phase branch from the milestone
git checkout -b phase/01-quick-wins
# Commit work with ---ci--- blocks
git commit -m "feat(P01-01-01): implement commit parser
---ci---
phase: 1
milestone: v0.2
milestone: v0.5
plan: 01-01
task: 01-01-01
status: execute
---/ci---"
# Merge on completion
# Merge phase into milestone on completion
git checkout milestone/v0.5-honest-baseline
git merge --squash phase/01-quick-wins
git commit -m "docs(P01): complete quick-wins phase"
git tag -a v0.5.1 -m "v0.5.1: quick-wins"
# After all phases, merge milestone into main
git checkout main
git merge --squash phase/01-git-native-architecture
git commit -m "docs(P01): complete git-native-architecture phase"
git tag -a v0.2.1 -m "v0.2.1: git-native-architecture"
git merge --squash milestone/v0.5-honest-baseline
git commit -m "docs(milestone): complete honest-baseline"
git tag -a v0.6.0 -m "v0.6.0: honest-baseline"
git push origin main --tags
```
+61 -21
View File
@@ -1,19 +1,23 @@
---
description: Ship CI phase or milestone — test, tag, release. Every phase gets a patch release. Every milestone gets a minor (or major) release. Full autopilot.
description: Ship CI phase or milestone — test, tag, release. Every phase and milestone gets a release. Full autopilot.
---
# CI Ship
Ship a CI phase or milestone. Every ship creates a release — no exceptions.
**Versioning rule:**
- **Major** (X.0.0): Project-level refactor or schema changes
- **Minor** (0.X.0): Feature milestone completion only
- **Patch** (0.0.X): Every phase completion
**3-Tier Versioning Model:**
**NFR versioning:**
- NFR milestones (all phases are fix/chore/docs/perf/refactor/test): progressive patch versions only (v0.1.1, v0.1.2, v0.1.3). No minor milestone tag.
- Feature milestones (any feat phase): progressive patch versions per phase + minor milestone tag on completion (e.g., v0.2.0).
| Milestone Type | Condition | Phase release | Milestone release |
|---------------|-----------|---------------|-------------------|
| **NFR** | All phases: fix/chore/docs/perf/refactor/test | Patch (`vX.Y.Z`) | None |
| **Feature** | Any phase is `feat`, no schema break | Patch (`vX.Y.Z`) | Minor — `vX.(Y+1).0` |
| **Schema-breaking** | Refactor/schema break/new direction | Minor — `vX.(Y+N).0` per phase | Major — `v(X+1).0.0` |
**CRITICAL:** Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the milestone is implicit from the patch sequence
**Usage:** `ci-ship [phase_number|milestone]`
@@ -36,7 +40,7 @@ git log --max-count=10
git branch -a
```
Determine what is being shipped: a single phase (patch release) or an entire milestone (minor/major release).
Determine what is being shipped: a single phase or an entire milestone.
Read `.ci/ROADMAP.md` to determine:
- Current milestone version (e.g., `v0.2`)
@@ -57,22 +61,51 @@ If any fail: iterate autonomously until tests pass. Do NOT ask the user for guid
## Step 3: Compute Version
Determine the release version from what is being shipped. Check `isNfrMilestone()` for versioning behavior:
Determine milestone type by calling `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`:
| What's shipping | Milestone Type | Version bump | Tag format | Example |
| What's shipping | Milestone Type | Phase release | Milestone release | Example |
|----------------|---------------|-------------|------------|---------|
| Single phase | NFR | Patch | `vX.Y.Z` | `v0.1.3` (3rd NFR phase in milestone v0.1) |
| Single phase | Feature | Patch | `vX.Y.Z` | `v0.2.3` (3rd phase in feature milestone v0.2) |
| Milestone completion | NFR | Patch (last phase) | `vX.Y.Z` | `v0.1.3` (no minor tag) |
| Milestone completion | Feature | Minor | `vX.Y.0` | `v0.3.0` (feature milestone v0.3 complete) |
| Project refactor/schema change | Any | Major | `vX.0.0` | `v1.0.0` (breaking schema) |
| Single phase | NFR | Patch `vX.Y.Z` | N/A | v0.1.3 (3rd NFR phase) |
| Single phase | Feature | Patch `vX.Y.Z` | N/A | v0.2.3 (3rd feature phase) |
| Single phase | Schema-breaking | Minor `vX.(Y+N).0` | N/A | v0.4.0 (2nd schema-breaking phase) |
| Milestone completion | NFR | Patch (last phase) | None | v0.1.3 (no milestone tag) |
| Milestone completion | Feature | Last patch | Minor `vX.(Y+1).0` | v0.3.0 (NOT v0.2.0) |
| Milestone completion | Schema-breaking | Last minor | Major `v(X+1).0.0` | v1.0.0 |
Count completed phases in the current milestone to determine the patch number.
Phase number within the milestone determines the increment:
- NFR/Feature: 1st phase = .1, 2nd = .2, etc. (v0.5.1, v0.5.2)
- Schema-breaking: 1st phase = next minor, 2nd = minor+1, etc. (v0.3.0, v0.4.0)
**Before creating ANY tag, validate:**
1. The tag must be strictly greater than all existing tags on the same major.minor line
2. Milestone completion tag must be the next minor (feature) or next major (schema-breaking)
3. NEVER create a milestone tag that is semantically below existing phase tags (e.g., v0.5.0 when v0.5.1 already exists)
## Step 4: Merge Branch
### Phase ship (patch release)
### Branch hierarchy: main > milestone/vX.X-slug > phase/NN-slug
Phases MUST merge into their milestone branch (or to main if no milestone branch exists). Milestones merge into main only after all phases are complete.
### Phase ship
**If milestone branch exists:**
```bash
git checkout milestone/vX.Y-slug
git merge --squash phase/NN-slug
git commit -m "docs(P##): complete [phase-name] phase
---ci---
phase: [N]
milestone: [vX.Y]
status: complete
requirements:
covered: [REQ-01, REQ-02]
partial: []
---/ci---"
```
**If no milestone branch exists (single-phase milestone):**
```bash
git checkout main
git merge --squash phase/NN-slug
@@ -88,9 +121,10 @@ requirements:
---/ci---"
```
### Milestone ship (minor/major release)
### Milestone ship (after last phase)
```bash
# Verify all phase branches are merged into milestone branch
git checkout main
git merge --squash milestone/vX.Y-slug
git commit -m "docs(milestone): complete [milestone-name]
@@ -109,6 +143,12 @@ git tag -a vX.Y.Z -m "vX.Y.Z: [phase-name or milestone-name]"
git push origin main --tags
```
**Tag format by milestone type:**
- NFR/Feature phase: patch format (`v0.5.1`, `v0.5.2`)
- Schema-breaking phase: minor format (`v0.3.0`, `v0.4.0`)
- Feature milestone: next minor (`v0.6.0`, NOT `v0.5.0`)
- Schema-breaking milestone: next major (`v1.0.0`)
## Step 6: Create Release
**Every ship creates a Gitea release. No exceptions.**
@@ -145,7 +185,7 @@ Commit the file updates.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase [N]: [name]
Milestone: [vX.Y]
Milestone: [vX.Y] ([nfr|feature|schema-breaking])
Version: vX.Y.Z
Release: https://git.cloudinit.dev/continuous-intelligence/ci/releases/tag/vX.Y.Z
Status: complete
@@ -158,6 +198,6 @@ Requirements covered: [N]
Commits: [N]
[If milestone complete:]
All phases in milestone v0.2 complete. Milestone released.
All phases in milestone v0.2 complete. Milestone released as vX.Y.Z.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```