Files
Kill_LIFE/tools/ci/release_policy.py
L'électron rareandClaude Opus 4.6 7870c6cf58 feat: close plans 06/09/14 — CI firmware/HW gate, bulk edit, release process
Plan 06 CI/CD: .clang-format, lint_firmware.sh, branch_protection.sh,
  protected_environments.sh, firmware-lint + hardware-gate CI jobs
Plan 09 Bulk Edit: snapshot.sh, bulk_edit.py (dry-run/apply/verify), hw_diff.sh
Plan 14 Release: release_prep.sh, generate_changelog.py, release_policy.py

All 20 items closed across 3 plans.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-25 12:43:33 +01:00

171 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Release policy documentation and enforcement for Kill_LIFE.
Usage:
python3 tools/ci/release_policy.py --show # Print policy
python3 tools/ci/release_policy.py --check # Verify current state
python3 tools/ci/release_policy.py --check --strict # Exit non-zero on violations
This script documents and enforces:
- Backport policy
- Hotfix process
- Version naming conventions
- Release branch rules
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
POLICY = """
# Kill_LIFE Release & Versioning Policy
## Version Scheme
| Component | Format | Example |
|-----------|-------------------|----------|
| Firmware | SemVer vX.Y.Z | v1.2.3 |
| Hardware | HW-V{n}r{rev} | HW-V1r2 |
| Specs | Internal rev / tag | rev-5 |
## Branching Model
- `main` : latest stable, always green CI
- `release/vX.Y.Z` : release candidate, bugfix-only
- `hotfix/vX.Y.Z` : emergency fix from a tagged release
## Backport Policy
Backports are applied to the **most recent release branch only**.
1. Fix the issue on `main` first.
2. Cherry-pick to the active `release/vX.Y.Z` branch.
3. If the release is already tagged, create a `hotfix/vX.Y.(Z+1)` branch.
4. Backports to older releases require explicit maintainer approval.
Eligible for backport:
- Security fixes : ALWAYS
- Data-loss bugs : ALWAYS
- CI/build regressions : CASE-BY-CASE
- New features : NEVER
## Hotfix Process
1. Branch from the release tag:
git checkout -b hotfix/vX.Y.(Z+1) vX.Y.Z
2. Apply minimal fix (no feature work).
3. Run full test suite:
bash tools/test_python.sh --suite stable
4. Generate changelog:
python3 tools/ci/generate_changelog.py --version vX.Y.(Z+1)
5. Tag and push:
git tag -a vX.Y.(Z+1) -m "Hotfix release"
git push origin vX.Y.(Z+1)
6. Cherry-pick the fix back to `main`.
## Release Checklist
- [ ] All CI checks green
- [ ] Evidence pack generated
- [ ] CHANGELOG.md updated
- [ ] Tag created (signed if possible)
- [ ] Release notes include AC references and breaking changes
- [ ] Firmware binaries attached to GitHub release
"""
def show_policy():
print(POLICY.strip())
def check_policy(strict=False):
"""Check current repo state against policy rules."""
issues = []
warnings = []
# Check: are we on a valid branch?
p = subprocess.run(
["git", "branch", "--show-current"],
text=True, capture_output=True, cwd=str(ROOT)
)
branch = p.stdout.strip()
valid_prefixes = ("main", "release/", "hotfix/", "feat/", "fix/", "chore/", "ci/")
if branch and not any(branch.startswith(pfx) or branch == pfx.rstrip("/") for pfx in valid_prefixes):
warnings.append(f"Branch '{branch}' does not follow naming convention.")
# Check: do we have any tags?
p = subprocess.run(
["git", "tag", "-l", "v*"],
text=True, capture_output=True, cwd=str(ROOT)
)
tags = [t.strip() for t in p.stdout.strip().split("\n") if t.strip()]
if not tags:
warnings.append("No version tags found (v*). Consider tagging a release.")
else:
# Validate tag format
for tag in tags:
if not re.match(r"^v\d+\.\d+\.\d+$", tag):
warnings.append(f"Tag '{tag}' does not match SemVer vX.Y.Z format.")
# Check: is CHANGELOG.md present?
changelog = ROOT / "CHANGELOG.md"
if not changelog.exists():
warnings.append("CHANGELOG.md not found at repo root.")
# Check: dirty working tree on release branch
if branch and branch.startswith("release/"):
p = subprocess.run(
["git", "status", "--porcelain"],
text=True, capture_output=True, cwd=str(ROOT)
)
if p.stdout.strip():
issues.append(f"Dirty working tree on release branch '{branch}'.")
# Report
print("=== Release Policy Check ===\n")
print(f"Branch: {branch or '(detached)'}")
print(f"Tags: {len(tags)} version tag(s)")
print("")
if issues:
print("ISSUES:")
for i in issues:
print(f" [ERROR] {i}")
print("")
if warnings:
print("WARNINGS:")
for w in warnings:
print(f" [WARN] {w}")
print("")
if not issues and not warnings:
print("All checks passed.")
if strict and issues:
return 1
return 0
def main():
ap = argparse.ArgumentParser(description="Kill_LIFE release policy.")
ap.add_argument("--show", action="store_true", help="Print release policy.")
ap.add_argument("--check", action="store_true", help="Check repo against policy.")
ap.add_argument("--strict", action="store_true", help="Exit non-zero on violations.")
args = ap.parse_args()
if args.show or (not args.check):
show_policy()
return 0
if args.check:
return check_policy(strict=args.strict)
if __name__ == "__main__":
raise SystemExit(main())