"""Write release/RELEASE_MANIFEST.json: one coherent record of what RC1 is made of (no secrets). python scripts/release_manifest.py [HUB_URL] Registers together: release version, agent/hub versions, fiscal engine + decision versions, engine module versions, ACTIVE dataset versions (kind, provider, sha256, record_count) read from the running Hub, alembic head (from the migration files) and the applied revision. """ import json import os import re import subprocess import sys import urllib.request from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) def get(url, token): req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}) with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) def main(): token = sys.argv[1] hub = sys.argv[2] if len(sys.argv) > 2 else "http://localhost:8000" from central.app.classification import DECISION_VERSION, ENGINE_VERSION from central.app.fiscal_decision import DECISION_ENGINE_VERSION from central.app.icms_context import ICMS_CONTEXT_ENGINE_VERSION from central.app.product_defaults import PRODUCT_DEFAULTS_ENGINE_VERSION version = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip() # Real revision ids (e.g. "0020_not_null_timestamps"), not file-name prefixes: # from 0015 on the id is longer than the 4-digit prefix. from alembic.config import Config from alembic.script import ScriptDirectory alembic_cfg = Config(str(ROOT / "central" / "alembic.ini")) alembic_cfg.set_main_option("script_location", str(ROOT / "central" / "alembic")) script = ScriptDirectory.from_config(alembic_cfg) head, revision_count = script.get_current_head(), len(list(script.walk_revisions())) container = os.getenv("HUB_CONTAINER", "projeto-fiscal-hub-1") try: current = subprocess.run(["docker", "exec", container, "sh", "-c", "cd /app && alembic -c central/alembic.ini current"], capture_output=True, text=True, timeout=60).stdout applied = re.findall(r"^(\d{4}\S*)", current, re.M)[-1:] or ["unknown"] except Exception: # noqa: BLE001 applied = ["unknown"] datasets = sorted(({"kind": d["kind"], "sha256": d["sha256"][:16], "record_count": d["record_count"]} for d in get(f"{hub}/v1/datasets/status", token) if d["status"] == "ACTIVE"), key=lambda x: x["kind"]) manifest = {"artifact": "RELEASE_MANIFEST", "GTI_FISCAL_VERSION": version, "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "hub": get(f"{hub}/version", token), "agent_version": version, "fiscal_engine": {"ENGINE_VERSION": ENGINE_VERSION, "DECISION_VERSION": DECISION_VERSION, "DECISION_ENGINE_VERSION": DECISION_ENGINE_VERSION, "PRODUCT_DEFAULTS_ENGINE_VERSION": PRODUCT_DEFAULTS_ENGINE_VERSION, "ICMS_CONTEXT_ENGINE_VERSION": ICMS_CONTEXT_ENGINE_VERSION}, "migrations": {"alembic_head_in_repo": head, "applied_revision": applied[0], "applied_revision_source": f"container {container}", "count": revision_count}, "datasets_active": datasets, "note": "A release bump does not change fiscal semantics; engine/decision versions are versioned independently."} (ROOT / "release" / "RELEASE_MANIFEST.json").write_text(json.dumps(manifest, indent=1), encoding="utf-8") print(json.dumps({"version": version, "engine": ENGINE_VERSION, "migrations": manifest["migrations"], "datasets": len(datasets)})) if __name__ == "__main__": main()