File size: 5,015 Bytes
e317359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34681d6
 
e317359
34681d6
e317359
 
 
 
 
 
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
#!/usr/bin/env python3
"""Deploy the tested GitHub source snapshot as the organization's Docker Space."""
from __future__ import annotations

import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile

ROOT = Path(__file__).resolve().parents[1]


def stage_cloud(destination: Path, revision: str, *, include_seed=True):
    fixed = ["pyproject.toml", "LICENSE.txt", "space/app.py", "space/requirements.txt", "space/constraints.txt",
             "space/vendor/ts_bench/collect_weekly_data.py", "space/vendor/ts_bench/export_model_inputs.py",
             "bootstrap/manifest.json"]
    if include_seed:
        fixed.append("bootstrap/seed.tar.gz")
    files = [ROOT / name for name in fixed]
    for name in ("cloud", "scripts", "src", "configs", "space/src", "space/user_models"):
        files.extend(p for p in (ROOT / name).rglob("*") if p.is_file()
                     and p.suffix in {".py", ".json", ".yaml", ".yml", ".txt", ".md"})
    secrets = [v.encode() for k,v in os.environ.items() if v and len(v)>=12
               and any(word in k for word in ("TOKEN", "API_KEY", "PASSWORD", "SECRET"))]
    for source in sorted(set(files)):
        rel = source.relative_to(ROOT)
        if any(part.startswith(".env") for part in rel.parts):
            raise ValueError("Environment files must never enter the Space")
        content = source.read_bytes()
        if any(secret in content for secret in secrets):
            raise ValueError(f"Configured credential detected in deployment file {rel}")
        path = destination / rel
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(content)
    shutil.copy2(ROOT / "cloud/Dockerfile", destination / "Dockerfile")
    shutil.copy2(ROOT / "cloud/README.md", destination / "README.md")
    (destination / "deployment.json").write_text(json.dumps({
        "source_repository":"https://github.com/Thinkcat-Lab/LiveHouse-TS", "source_branch":"main",
        "source_revision":revision,
    }, indent=2) + "\n")


def publish_bootstrap(api, state_repo):
    if not api.dataset_info(state_repo).private:
        raise ValueError("Bootstrap storage must use the private operator Dataset")
    expected = json.loads((ROOT / "bootstrap/manifest.json").read_text())["sha256"]
    archive = ROOT / "bootstrap/seed.tar.gz"
    if hashlib.sha256(archive.read_bytes()).hexdigest() != expected:
        raise ValueError("Local bootstrap checksum mismatch")
    remote_path = f"bootstrap/{expected}.tar.gz"
    if not api.file_exists(state_repo, remote_path, repo_type="dataset"):
        api.upload_file(path_or_fileobj=archive, path_in_repo=remote_path,
                        repo_id=state_repo, repo_type="dataset",
                        commit_message="Store immutable bootstrap for hosted deployment")


def main():
    from dotenv import load_dotenv
    from huggingface_hub import HfApi
    load_dotenv(ROOT / ".env")
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo", default=os.getenv("HF_SPACE_REPO", "ThinkcatLab/LiveHouse-TS"))
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--output-dir", type=Path, help="Keep a validated build context; requires --dry-run")
    args = parser.parse_args()
    if args.output_dir and not args.dry_run:
        parser.error("--output-dir requires --dry-run")
    revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
    with tempfile.TemporaryDirectory(prefix="livehouse-cloud-stage-") as temp:
        stage = Path(temp)
        stage_cloud(stage, revision, include_seed=args.dry_run)
        print(f"Validated cloud deployment from {revision}: {sum(p.is_file() for p in stage.rglob('*'))} files")
        if args.dry_run:
            if args.output_dir:
                if args.output_dir.exists():
                    raise SystemExit("Build context destination already exists")
                shutil.copytree(stage, args.output_dir)
            return
        if not os.getenv("HF_TOKEN"):
            raise SystemExit("HF_TOKEN required")
        api = HfApi(token=os.environ["HF_TOKEN"])
        publish_bootstrap(api, os.getenv("HF_STATE_REPO", "ThinkcatLab/LiveHouse-TS-state"))
        # A changed README SDK converts the transferred Space to Docker while
        # preserving its Git history. No hardware purchase is performed.
        api.repo_info(args.repo, repo_type="space")
        # A transferred Space may contain legacy top-level packages that shadow
        # src/. Replace the deployment tree while retaining all Git history.
        commit = api.upload_folder(folder_path=stage, repo_id=args.repo, repo_type="space",
                                  delete_patterns=["*"],
                                  commit_message=f"Deploy GitHub main {revision[:12]}")
        print(f"Deployed {commit.oid}: https://huggingface.co/spaces/{args.repo}")


if __name__ == "__main__":
    main()