Files
GTI_GERENCIADOR_FISCAL/scripts/release_manifest.py
T
gtiandClaude Opus 5.5 d33149f5ee docs: record migration head 0020 in the release manifest and docs
RELEASE_MANIFEST.json still said head 0011 (repo was at 0018, now 0020)
and RELEASE_CANDIDATE_V1.md said 0010. Updated both, plus
AI_PROJECT_STATE.md and PILOT_DEPLOYMENT.md, to head
0020_not_null_timestamps as applied in production.

scripts/release_manifest.py reported the 4-digit file prefix as the head
("0020" instead of "0020_not_null_timestamps") and always read the
applied revision from the dev container. It now asks Alembic for the real
head and revision count, and the Hub container is configurable
(HUB_CONTAINER); the source is recorded in the manifest.

tests/test_release_candidate.py now fails when the manifest head differs
from Alembic's head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 07:23:13 -03:00

62 lines
3.5 KiB
Python

"""Write release/RELEASE_MANIFEST.json: one coherent record of what RC1 is made of (no secrets).
python scripts/release_manifest.py <AGENT_TOKEN> [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()