Files
Investidor-V2/tools/portal-owner-administration-release.py
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

134 lines
7.7 KiB
Python

"""Apply only the explicitly authorized fixed administration delta; never install schema."""
from pathlib import Path
from datetime import datetime, timezone
import hashlib
import json
import os
import re
import shutil
import sys
import zipfile
from portal_diagnostics_deployment_guard import assert_diagnostics_off
R = Path(__file__).resolve().parent.parent
assert str(R).lower().replace("\\", "/") == "c:/laragon/www/portal-investidor"
assert_diagnostics_off(R)
O = R / "outputs/investmoz-internal-designation-20261011"
P = R / "storage/app/private/investmoz-v2/portal-audit"
load = lambda p: json.loads(p.read_text(encoding="utf-8-sig"))
sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
m = load(O / "manifesto.json")
assert sha(O / "manifesto.json") == "d42336c3aafcc6919bbec3263860ce2a850c4bdb831bf64003967a7de68b8c95"
archive = O / m["package"]
assert sha(archive) == "c31537ce1d17a1670784076e10e3e0d2ec89007be919ad9e6712119651af8cee"
expected = {x["path"]: x["after_sha256"] for x in m["files"]} | {x["path"]: x["sha256"] for x in m["assets"]}
assert len(m["files"]) == 16 and len(m["assets"]) == 159 and len(expected) == 175
assert all(x.startswith(("app/", "config/", "routes/", "resources/", "public/build/")) for x in expected)
assert "public/build/manifest.json" in expected
action = sys.argv[1]
record = O / "disponibilizacao-codigo.json"
def absolute(relative):
p = R / relative
assert not Path(relative).is_absolute() and ":" not in relative and ".." not in Path(relative).parts
assert p.resolve().is_relative_to(R.resolve())
return p
def write_atomic(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".owner-admin-deployment-tmp")
assert not temporary.exists()
temporary.write_bytes(data)
os.replace(temporary, path)
def env_flag(data, enabled):
# Touch one known flag only; all keys/cookies/connection/other flags remain byte-identical.
pattern = rb"(?m)^PORTAL_PROFILE_ADMINISTRATION_ENABLED=[^\r\n]*"
matches = list(re.finditer(pattern, data))
assert len(matches) <= 1
value = b"PORTAL_PROFILE_ADMINISTRATION_ENABLED=" + (b"true" if enabled else b"false")
if matches:
return re.sub(pattern, lambda _: value, data)
newline = b"\r\n" if b"\r\n" in data else b"\n"
return data + (b"" if data.endswith(b"\n") else newline) + value + newline
if action == "--deploy":
assert not record.exists(), "Preserve the prior release receipt; do not repeat blindly."
pre = load(O / "disponibilizacao-preflight.json")
assert pre["result"] == "ready_for_authorized_code_and_administration_only"
assert pre["revision"] == "2876cdfa900a"
assert (datetime.now(timezone.utc) - datetime.fromisoformat(pre["at"])).total_seconds() < 300
assert pre["backup_permanent_source_data_unchanged"] and pre["source_hashes_match"]
assert datetime.now(timezone.utc) < datetime.fromisoformat(pre["backup_valid_until"])
original = {}
for f in m["files"]:
p = absolute(f["path"])
actual = sha(p) if p.is_file() else None
assert actual == f["before_sha256"], "Destination changed: " + f["path"]
original[f["path"]] = actual
stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
stage = P / ("owner-administration-release-" + stamp)
assert stage.parent.resolve() == P.resolve() and not stage.exists()
stage.mkdir()
restore = stage / "before"
for relative in [*original, "public/build/manifest.json", ".env"]:
source = absolute(relative)
if source.is_file():
dest = restore / relative
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, dest)
old_env = (R / ".env").read_bytes()
with zipfile.ZipFile(archive) as z:
assert set(z.namelist()) == set(expected) and len(z.namelist()) == len(expected)
for relative, digest in expected.items():
dest = stage / "candidate" / relative
assert dest.resolve().is_relative_to((stage / "candidate").resolve())
payload = z.read(relative)
assert hashlib.sha256(payload).hexdigest() == digest
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(payload)
if relative.startswith("public/build/") and relative != "public/build/manifest.json":
active_asset = absolute(relative)
assert not active_asset.is_file() or sha(active_asset) == digest, "Immutable asset collision"
assert_diagnostics_off(R, payload={p:(stage / 'candidate' / p).read_bytes() for p in expected})
receipt = {
"at": datetime.now(timezone.utc).isoformat(), "revision": "2876cdfa900a",
"manifest_sha256": sha(O / "manifesto.json"), "package_sha256": sha(archive),
"destination": "https://portal-investidor.test", "private_reversal_directory": str(restore),
"before_source_hashes": original, "before_environment_sha256": hashlib.sha256(old_env).hexdigest(),
"before_build_manifest_sha256": sha(restore / "public/build/manifest.json"),
"new_sources": [p for p, h in original.items() if h is None],
"authorization": "Explicit administration-only authorization for fixed revision in this turn",
"new_tables": 0, "new_columns": 0, "accounts_organizations_representations_grants_created": 0,
"migrations_applied": [], "services_changed": False, "COUNTRY02_preserved": True,
"workflow_enabled": False, "result": "applying_with_administration_disabled",
}
record.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
write_atomic(R / ".env", env_flag(old_env, False))
order = sorted(expected, key=lambda p: (2 if p == "public/build/manifest.json" else 0 if p.startswith("public/build/") else 1, p))
for relative in order:
write_atomic(absolute(relative), (stage / "candidate" / relative).read_bytes())
assert all(sha(absolute(p)) == h for p, h in expected.items())
assert_diagnostics_off(R)
assert (R / ".env").read_bytes() == env_flag(old_env, False)
receipt.update(result="code_and_bundle_deployed_administration_OFF", completed_at=datetime.now(timezone.utc).isoformat(),
source_files_verified=16, compiled_files_verified=159, old_hashed_resources_retained=True)
record.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({k: receipt[k] for k in ["result","revision","source_files_verified","compiled_files_verified","migrations_applied","services_changed","COUNTRY02_preserved","workflow_enabled"]}))
elif action == "--enable":
receipt = load(record)
assert receipt["result"] == "code_and_bundle_deployed_administration_OFF"
assert all(sha(absolute(p)) == h for p, h in expected.items())
restore = Path(receipt["private_reversal_directory"])
assert restore.resolve().is_relative_to(P.resolve())
before = (restore / ".env").read_bytes()
assert (R / ".env").read_bytes() == env_flag(before, False)
write_atomic(R / ".env", env_flag(before, True))
receipt.update(result="administration_ON_workflow_OFF_country_preserved", enabled_at=datetime.now(timezone.utc).isoformat(),
enabled_environment_sha256=sha(R / ".env"), configuration_keys_changed=["PORTAL_PROFILE_ADMINISTRATION_ENABLED"],
authenticated_owner_interface_verification="pending owner session/MFA; not inferred from deployment")
record.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"result":receipt["result"],"revision":receipt["revision"],"configuration_keys_changed":receipt["configuration_keys_changed"],"migrations_applied":[],"new_tables":0,"new_columns":0}))
else:
raise RuntimeError("Supported actions: --deploy, --enable")