ab6af144b7
---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)
80 lines
3.4 KiB
Bash
Executable File
80 lines
3.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# CI pre-push hook: enforce versioning and branching rules
|
|
# Install: git config core.hooksPath .githooks
|
|
|
|
zero="0000000000000000000000000000000000000000"
|
|
|
|
while read local_ref local_oid remote_ref remote_oid; do
|
|
if [ "$local_oid" = "$zero" ]; then
|
|
continue
|
|
fi
|
|
|
|
# Check pushed tags
|
|
if echo "$local_ref" | grep -qE "^refs/tags/"; then
|
|
tag_name=$(echo "$local_ref" | sed 's|^refs/tags/||')
|
|
|
|
# Validate semver format
|
|
if echo "$tag_name" | grep -qE "^v[0-9]+\.[0-9]+\.[0-9]+$"; then
|
|
tag_major=$(echo "$tag_name" | sed 's/v\([0-9]*\)\.[0-9]*\.[0-9]*/\1/')
|
|
tag_minor=$(echo "$tag_name" | sed 's/v[0-9]*\.\([0-9]*\)\.[0-9]*/\1/')
|
|
tag_patch=$(echo "$tag_name" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
|
|
|
|
# Check for semver ordering violations
|
|
for existing_tag in $(git tag -l "v${tag_major}.${tag_minor}.*" 2>/dev/null); do
|
|
if [ "$existing_tag" = "$tag_name" ]; then
|
|
continue
|
|
fi
|
|
existing_patch=$(echo "$existing_tag" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
|
|
if [ "$existing_patch" -ge "$tag_patch" ] && [ "$tag_patch" -le "$existing_patch" ]; then
|
|
echo "ERROR: Tag $tag_name is not greater than existing tag $existing_tag"
|
|
echo " Milestone tags must be the NEXT version (e.g., v0.6.0 after v0.5.1-5, NOT v0.5.0)"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Check for milestone-tags-below-phase-tags
|
|
# If this is a .0 tag (milestone), verify no .N tags exist with higher patch
|
|
if [ "$tag_patch" = "0" ]; then
|
|
for existing_tag in $(git tag -l "v${tag_major}.${tag_minor}.*" 2>/dev/null); do
|
|
existing_patch=$(echo "$existing_tag" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
|
|
if [ "$existing_patch" -gt 0 ] && [ "$existing_patch" -gt "$tag_patch" ]; then
|
|
echo "ERROR: Milestone tag $tag_name is below existing phase tags (e.g., $existing_tag)"
|
|
echo " Feature milestone completion must be tagged as v${tag_major}.$(($tag_minor + 1)).0, not v${tag_major}.${tag_minor}.0"
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check branch merges: reject direct-to-main pushes if milestone branch exists
|
|
if echo "$local_ref" | grep -qE "^refs/heads/main$"; then
|
|
milestone_branches=$(git branch -r 2>/dev/null | grep 'milestone/v' | grep -v ':$' || true)
|
|
if [ -n "$milestone_branches" ]; then
|
|
# Allow if this is a merge commit from a milestone branch
|
|
merge_parents=$(git cat-file -p "$local_oid" 2>/dev/null | grep "^parent" | wc -l)
|
|
if [ "$merge_parents" -lt 2 ]; then
|
|
# Not a merge commit — check if there are active milestone branches
|
|
active_milestones=""
|
|
for mb in $milestone_branches; do
|
|
clean_name=$(echo "$mb" | sed 's|^[^/]*/||' | tr -d ' ')
|
|
merged=$(git branch -r --merged origin/main 2>/dev/null | grep "$clean_name" || true)
|
|
if [ -z "$merged" ]; then
|
|
active_milestones="$active_milestones $clean_name"
|
|
fi
|
|
done
|
|
if [ -n "$active_milestones" ]; then
|
|
echo "WARNING: Pushing directly to main while active milestone branches exist:"
|
|
for ms in $active_milestones; do
|
|
echo " - $ms"
|
|
done
|
|
echo " Phase branches should merge into the milestone branch first."
|
|
# Warning only — not blocking. The code-level enforcement in git-branch.ts
|
|
# is the hard gate; this hook is a safety net.
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
|
|
exit 0 |