File size: 6,678 Bytes
1642ffb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc151a4
79e11ca
1642ffb
023c4d6
b31b0fe
023c4d6
 
1642ffb
 
 
 
 
cd7e317
1642ffb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Allowlisted consistent HF packages. Never uploads; never packages arbitrary cwd files."""
from __future__ import annotations

import argparse
import hashlib
import json
import re
import shutil
import subprocess
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DIRS = ("backend", "frontend", "gpu_service", "docs", "deploy", "scripts", "tests")
FILES = ("README.md", "Dockerfile", ".dockerignore", ".gitignore", ".env.example", "pyproject.toml",
         "requirements.txt", "requirements-dev.txt", "requirements-gpu.txt", "constraints-cpu.txt", "model-lock.json")
EXCLUDED = {"node_modules", "dist", "__pycache__", ".pytest_cache", ".ruff_cache", ".venv", "output", "private-media"}
ALLOWED = {".py", ".md", ".txt", ".json", ".html", ".css", ".ts", ".tsx", ".js", ".sh", ".ttf", ".toml", ".Dockerfile"}
REQUIRED = ("backend/main.py", "backend/session.py", "backend/perception.py", "backend/portfolio.py",
            "backend/public_output.py", "backend/settings.py", "backend/schemas.py",
            "frontend/package.json", "frontend/package-lock.json", "frontend/index.html",
            "frontend/vite.config.ts", "frontend/src/App.tsx", "frontend/src/api.ts", "frontend/src/styles.css",
            "frontend/src/mediaCapture.ts", "frontend/src/transport.ts", "frontend/src/audio.ts", "frontend/src/latency.ts",
            "frontend/public/audio-worklet.js", "frontend/src/components/ObservatoryLogo.tsx",
            "frontend/src/components/InstrumentPicker.tsx", "frontend/src/instruments.ts",
            "frontend/src/components/PortfolioEditor.tsx", "frontend/src/portfolio.ts",
            "frontend/src/instrumentSearch.ts", "frontend/src/catalog/equities.json",
            "frontend/src/catalog/funds.json", "frontend/src/catalog/maritime.json",
            "frontend/src/assets/manrope/Manrope-Variable.ttf", "frontend/src/assets/manrope/OFL.txt",
            "frontend/public/licenses/Manrope-OFL.txt", "gpu_service/app.py", "gpu_service/native.py",
            "gpu_service/contracts.py", "gpu_service/smoke.py", "requirements.txt",
            "requirements-gpu.txt", "constraints-cpu.txt", "deploy/gpu.Dockerfile",
            "docs/model-interface.md", "docs/architecture.md", "docs/hugging-face-delivery.md",
            "docs/verification.md", "model-lock.json", "scripts/verify_gpu_install.py")
SECRET = re.compile(rb"\b(?:hf_[A-Za-z0-9]{16,}|sk-[A-Za-z0-9_-]{20,})\b")


def files():
    result = []
    for directory in DIRS:
        for path in (ROOT / directory).rglob("*"):
            relative = path.relative_to(ROOT)
            if set(relative.parts) & EXCLUDED or path.name.endswith(".tsbuildinfo"):
                continue
            if path.is_symlink():
                raise RuntimeError("Symlink is not allowed in release: " + str(relative))
            if path.is_dir():
                continue
            if path.name.startswith(".env"):
                raise RuntimeError("Runtime environment file cannot be released: " + str(relative))
            if path.suffix not in ALLOWED and path.name != "Dockerfile":
                raise RuntimeError("Unexpected file in release directory: " + str(relative))
            result.append(path)
    result.extend(ROOT / name for name in FILES)
    for relative in REQUIRED:
        if ROOT / relative not in result:
            raise RuntimeError("Required application file missing: " + relative)
    return sorted(set(result))


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--validate-only", action="store_true")
    parser.add_argument("--output", type=Path, default=ROOT / "release")
    args = parser.parse_args()
    selected = files()
    manifest = {}
    for source in selected:
        data = source.read_bytes()
        # Secret-shaped fixture rejection strings are written via concatenation
        # in tests, so the exact package never contains usable token-shaped text.
        if SECRET.search(data):
            raise RuntimeError("Credential-shaped value found; remove it before release: " + str(source.relative_to(ROOT)))
        manifest[str(source.relative_to(ROOT))] = hashlib.sha256(data).hexdigest()
    canonical = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()
    revision = hashlib.sha256(canonical).hexdigest()
    try:
        git_revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, stderr=subprocess.DEVNULL, text=True).strip()
    except subprocess.CalledProcessError:
        git_revision = None
    if args.validate_only:
        print(f"Package validation passed: {len(manifest)} allowlisted files; source SHA256 {revision}")
        return
    destination = args.output.resolve()
    # Keep all writes inside an explicit package output folder; refuse workspace root.
    if destination == ROOT or ROOT.is_relative_to(destination):
        raise RuntimeError("Choose a release output subdirectory, not the source root")
    for flavor in ("app", "gpu"):
        stage = destination / flavor
        if stage.exists():
            shutil.rmtree(stage)
        stage.mkdir(parents=True)
        for source in selected:
            target = stage / source.relative_to(ROOT)
            target.parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(source, target)
            if hashlib.sha256(target.read_bytes()).hexdigest() != manifest[str(source.relative_to(ROOT))]:
                raise RuntimeError("Source changed during packaging; rerun after edits finish")
        if flavor == "gpu":
            shutil.copyfile(stage / "deploy/gpu.Dockerfile", stage / "Dockerfile")
            (stage / "README.md").write_text("---\ntitle: OmnAI MiniCPM-o 4.5\nsdk: docker\napp_port: 8090\n---\n\nPersistent authenticated MiniCPM-o 4.5 audiovisual and portfolio research service. One model, one ongoing native duplex session. See docs/model-interface.md, docs/architecture.md and docs/hugging-face-delivery.md.\n\nSource package SHA256: " + revision + "\n")
        package_files = {str(p.relative_to(stage)): hashlib.sha256(p.read_bytes()).hexdigest()
                         for p in stage.rglob("*") if p.is_file()}
        record = {"source_sha256": revision, "git_revision": git_revision, "flavor": flavor,
                  "source_files": manifest, "package_files": package_files}
        (stage / "RELEASE_MANIFEST.json").write_text(json.dumps(record, indent=2) + "\n")
        for name, digest in package_files.items():
            assert hashlib.sha256((stage / name).read_bytes()).hexdigest() == digest
    print(f"Validated app and GPU packages: {destination}\nShared source SHA256: {revision}")


if __name__ == "__main__":
    main()