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:
@@ -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.1–v0.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
|
||||
```
|
||||
|
||||
|
||||
@@ -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.1–v0.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.
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
Reference in New Issue
Block a user