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:
@@ -137,4 +137,86 @@ describe("GitBranch", () => {
|
||||
expect(result.name).toMatch(/^phase\/01-/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateMergeOrder", () => {
|
||||
it("rejects phase → main when milestone branch exists", () => {
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
gitBranch.createMilestoneBranch("v0.5", "baseline");
|
||||
gitBranch.createPhaseBranch(1, "auth");
|
||||
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const result = gitBranch.validateMergeOrder("phase/01-auth", "main");
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reason).toContain("milestone");
|
||||
});
|
||||
|
||||
it("allows phase → milestone branch", () => {
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
gitBranch.createMilestoneBranch("v0.5", "baseline");
|
||||
gitBranch.createPhaseBranch(1, "auth");
|
||||
execSync(`git checkout milestone/v0.5-baseline`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const result = gitBranch.validateMergeOrder("phase/01-auth", "milestone/v0.5-baseline");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("allows phase → main when no milestone branch exists", () => {
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
gitBranch.createPhaseBranch(1, "auth");
|
||||
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const result = gitBranch.validateMergeOrder("phase/01-auth", "main");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("allows hotfix → main", () => {
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
gitBranch.createMilestoneBranch("v0.5", "baseline");
|
||||
execSync(`git checkout -b hotfix/critical-fix main`, { cwd: repoDir, stdio: "pipe" });
|
||||
fs.writeFileSync(path.join(repoDir, "fix.txt"), "fix");
|
||||
execSync(`git add . && git commit -m "hotfix: critical fix"`, { cwd: repoDir, stdio: "pipe" });
|
||||
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const result = gitBranch.validateMergeOrder("hotfix/critical-fix", "main");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeMilestoneTag", () => {
|
||||
it("computes next minor for feature milestone", () => {
|
||||
execSync(`git tag -a v0.5.1 -m "v0.5.1"`, { cwd: repoDir, stdio: "pipe" });
|
||||
execSync(`git tag -a v0.5.2 -m "v0.5.2"`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
const tag = gitBranch.computeMilestoneTag("feature");
|
||||
expect(tag).toBe("v0.6.0");
|
||||
});
|
||||
|
||||
it("computes next major for schema-breaking milestone", () => {
|
||||
execSync(`git tag -a v0.5.1 -m "v0.5.1"`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
const tag = gitBranch.computeMilestoneTag("schema-breaking");
|
||||
expect(tag).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
it("starts from v0.1.0 when no tags exist", () => {
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
const tag = gitBranch.computeMilestoneTag("feature");
|
||||
expect(tag).toBe("v0.1.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeMilestoneBranch", () => {
|
||||
it("rejects merge when unmerged phase branches remain", () => {
|
||||
const gitBranch = new GitBranch(repoDir);
|
||||
gitBranch.createMilestoneBranch("v0.5", "baseline");
|
||||
gitBranch.createPhaseBranch(1, "auth");
|
||||
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const result = gitBranch.mergeMilestoneBranch("milestone/v0.5-baseline", "main", true);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain("unmerged");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user