mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(release): add release-manager feature flag (default off) * feat(release): change classification + semver-bump derivation * feat(release): readiness audit (changelog/version-ref/docs/migration/gate) * feat(release): release-manager engine proposes a gated release * feat(release): fail-closed release executor (bump, gate, publish) * feat(release): CEO approve/reject release-proposal surface * docs(release): document the gated release manager * feat(memory): add org-memory feature flags (default off) * feat(memory): add playbooks table + status enum + migration * feat(memory): playbook service with auditor curation transitions * feat(memory): playbooks RAG index plugin * feat(memory): index a playbook into RAG on approval * feat(memory): distill a high-signal lesson at task completion * feat(memory): keep private journal reflections out of the shared RAG corpus * feat(memory): draft_playbook verb + auditor curation verbs * fix(ci): resolve mypy tests/ errors blocking the gate (UUID casts, annotations) * feat(memory): auto-inject similar lessons/playbooks into the briefing * feat(memory): auditor playbook review queue (api + panel) * docs(memory): document the org-memory loop + playbook verbs * fix(provisioning): idempotent pitch provisioning (reuse product/project by slug on re-approval) * fix(memory): add chunks_playbooks to the chunk schema + isolate release route tests - Migration 030's CHUNK_TABLES was missing chunks_playbooks, breaking the IndexType<->migration parity guard once the PLAYBOOKS index landed. The upgrade is ALTER ... IF EXISTS so adding it is safe on any DB shape. - The release-route fixture's approve/reject paths call db.commit() (real behavior), so a held proposal outlived the per-test rollback and leaked into engine tests that read the global list_open_release_proposals(). Tear down source=release_manager rows after each test. - Make the gather_snapshot real-repo smoke version-agnostic (semver match) so it stops pinning the literal repo version. * chore(release): 0.13.0 * ++ --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
"""The readiness audit: assess() turns a repo snapshot into a gap-flagged report.
|
|
|
|
assess() is pure over a ``ReleaseRepoSnapshot`` so every "no stone unturned"
|
|
check (changelog/version-ref/docs-drift/migration/gate completeness) is tested
|
|
from synthetic data. The git-backed gather_snapshot() is smoke-tested against
|
|
the real repo at the bottom.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from roboco.services.release_readiness import (
|
|
CommitInfo,
|
|
ReleaseReadinessReport,
|
|
ReleaseRepoSnapshot,
|
|
assess,
|
|
gather_snapshot,
|
|
)
|
|
|
|
_TODAY = "2026-06-25"
|
|
_DECLARED = 25
|
|
_DRIFTED = 26
|
|
|
|
|
|
def _snap(**overrides: Any) -> ReleaseRepoSnapshot:
|
|
base = ReleaseRepoSnapshot(
|
|
current_version="0.12.0",
|
|
last_tag="v0.12.0",
|
|
commits=[CommitInfo(sha="a1", subject="feat: add a thing", pr_number=1)],
|
|
tracked_files_with_version=["pyproject.toml"],
|
|
canonical_bump_files=["pyproject.toml"],
|
|
changelog_text="## [Unreleased]\n### Added\n- add a thing (#1)\n",
|
|
new_migrations=[],
|
|
migration_head_count=1,
|
|
master_ci_conclusion="success",
|
|
declared_agent_count=_DECLARED,
|
|
actual_agent_count=_DECLARED,
|
|
verb_tables_stale=False,
|
|
)
|
|
return replace(base, **overrides)
|
|
|
|
|
|
def _categories(report: ReleaseReadinessReport) -> set[str]:
|
|
return {gap.category for gap in report.gaps}
|
|
|
|
|
|
def test_assess_proposes_next_version_and_bump() -> None:
|
|
report = assess(_snap(), today=_TODAY)
|
|
assert report.bump_kind == "minor"
|
|
assert report.proposed_version == "0.13.0"
|
|
assert report.gate_state == "green"
|
|
|
|
|
|
def test_clean_snapshot_has_no_gaps() -> None:
|
|
assert assess(_snap(), today=_TODAY).gaps == []
|
|
|
|
|
|
def test_undocumented_commit_is_a_changelog_gap() -> None:
|
|
snap = _snap(
|
|
commits=[CommitInfo(sha="a1", subject="feat: undocumented", pr_number=99)],
|
|
changelog_text="## [Unreleased]\n",
|
|
)
|
|
assert "changelog" in _categories(assess(snap, today=_TODAY))
|
|
|
|
|
|
def test_chore_commit_does_not_need_a_changelog_line() -> None:
|
|
snap = _snap(
|
|
commits=[CommitInfo(sha="a1", subject="chore: tidy imports", pr_number=7)],
|
|
changelog_text="## [Unreleased]\n",
|
|
)
|
|
assert "changelog" not in _categories(assess(snap, today=_TODAY))
|
|
|
|
|
|
def test_missed_version_ref_is_a_gap() -> None:
|
|
snap = _snap(
|
|
tracked_files_with_version=["pyproject.toml", "panel/pnpm-lock.yaml"],
|
|
canonical_bump_files=["pyproject.toml"],
|
|
)
|
|
report = assess(snap, today=_TODAY)
|
|
version_gaps = [g for g in report.gaps if g.category == "version_ref"]
|
|
assert any("pnpm-lock.yaml" in g.detail for g in version_gaps)
|
|
|
|
|
|
def test_bump_plan_is_the_canonical_set() -> None:
|
|
snap = _snap(canonical_bump_files=["pyproject.toml", "roboco/__init__.py"])
|
|
report = assess(snap, today=_TODAY)
|
|
assert report.version_bump_plan == ["pyproject.toml", "roboco/__init__.py"]
|
|
|
|
|
|
def test_stale_agent_count_is_docs_drift_gap() -> None:
|
|
snap = _snap(declared_agent_count=_DECLARED, actual_agent_count=_DRIFTED)
|
|
assert "docs_drift" in _categories(assess(snap, today=_TODAY))
|
|
|
|
|
|
def test_stale_verb_tables_is_docs_drift_gap() -> None:
|
|
assert "docs_drift" in _categories(
|
|
assess(_snap(verb_tables_stale=True), today=_TODAY)
|
|
)
|
|
|
|
|
|
def test_new_migration_listed_in_notes() -> None:
|
|
snap = _snap(new_migrations=["alembic/versions/050_playbooks.py"])
|
|
report = assess(snap, today=_TODAY)
|
|
assert any("050_playbooks" in note for note in report.migration_notes)
|
|
|
|
|
|
def test_multiple_alembic_heads_is_a_migration_gap() -> None:
|
|
head_count = 2
|
|
assert "migration" in _categories(
|
|
assess(_snap(migration_head_count=head_count), today=_TODAY)
|
|
)
|
|
|
|
|
|
def test_red_ci_is_a_gate_gap() -> None:
|
|
report = assess(_snap(master_ci_conclusion="failure"), today=_TODAY)
|
|
assert report.gate_state == "red"
|
|
assert "gate" in _categories(report)
|
|
|
|
|
|
def test_unknown_ci_is_a_gate_gap() -> None:
|
|
report = assess(_snap(master_ci_conclusion=None), today=_TODAY)
|
|
assert report.gate_state == "unknown"
|
|
assert "gate" in _categories(report)
|
|
|
|
|
|
def test_unclassifiable_commit_is_a_classification_gap() -> None:
|
|
snap = _snap(
|
|
commits=[CommitInfo(sha="a1", subject="random merge subject", pr_number=5)],
|
|
changelog_text="- random merge subject (#5)\n",
|
|
)
|
|
assert "classification" in _categories(assess(snap, today=_TODAY))
|
|
|
|
|
|
def test_drafted_changelog_is_keepachangelog_and_single_line() -> None:
|
|
snap = _snap(
|
|
commits=[
|
|
CommitInfo(sha="a1", subject="feat: add A", pr_number=1),
|
|
CommitInfo(sha="b2", subject="fix: fix B", pr_number=2),
|
|
],
|
|
changelog_text="- add A (#1)\n- fix B (#2)\n",
|
|
)
|
|
report = assess(snap, today=_TODAY)
|
|
assert "## [0.13.0] - 2026-06-25" in report.drafted_changelog
|
|
assert "### Added" in report.drafted_changelog
|
|
assert "### Fixed" in report.drafted_changelog
|
|
bullets = [
|
|
ln for ln in report.drafted_changelog.splitlines() if ln.startswith("- ")
|
|
]
|
|
assert len(bullets) == 2 # noqa: PLR2004 - exactly the two commits above
|
|
|
|
|
|
# --- gather_snapshot: real-repo smoke (this repo is a git checkout at 0.12.0) ---
|
|
|
|
|
|
def test_gather_snapshot_reads_the_real_repo() -> None:
|
|
root = Path(__file__).resolve().parents[3]
|
|
snap = gather_snapshot(root, master_ci_conclusion=None)
|
|
# The repo version moves with each release — assert it's a semver, not a literal.
|
|
assert re.fullmatch(r"\d+\.\d+\.\d+", snap.current_version)
|
|
assert snap.last_tag is not None
|
|
assert isinstance(snap.commits, list)
|
|
assert "pyproject.toml" in snap.canonical_bump_files
|
|
assert snap.changelog_text # CHANGELOG.md is non-empty
|
|
assert snap.migration_head_count >= 1
|
|
|
|
|
|
def test_gather_snapshot_then_assess_produces_a_report() -> None:
|
|
root = Path(__file__).resolve().parents[3]
|
|
report = assess(gather_snapshot(root, master_ci_conclusion="success"), today=_TODAY)
|
|
assert report.proposed_version
|
|
assert report.bump_kind in {"major", "minor", "patch"}
|
|
assert report.gate_state == "green"
|