"""발행된 사이트 **전부**를 Azure Blob(`$web`)에 다시 올린다. python scripts/republish_all.py (backend/ 에서 실행) python scripts/republish_all.py --dry-run (올리지 않고 목록만 본다) ★ 왜 필요한가 — 렌더러(solution/site)를 고쳐 배포하면 번들 파일명이 바뀐다 (`assets/index-DvNTmLhy.css` → `assets/index-<새해시>.css`). 그런데 평소 업로드 경로 (services/azure_static.publish)는 **방금 발행한 사이트 하나**만 올린다. 그래서 나머지 사이트의 HTML 은 Blob 에 옛 해시를 가리킨 채로 남는다. 옛 자산 블롭은 지워지지 않으니 화면이 깨지진 않지만, **디자인 수정이 그 사이트들에 영영 도달하지 않는다.** 프리렌더는 기동할 때 out/ 을 전부 다시 굽는다(watch-payloads.mjs) — 그 결과를 Blob 으로 밀어 넣는 짝이 없었다. 이 스크립트가 그 짝이다. ★ 순서: 프리렌더가 out/ 을 다 구운 **뒤에** 돌린다. 굽는 중에 돌리면 반쯤 구워진 HTML 이 올라간다. ★ 공용 자산(assets/·fonts/·robots.txt·sitemap.xml)은 한 번만 올린다. azure_static.publish 를 사이트마다 부르면 수백 KB 번들을 사이트 수만큼 다시 올린다. """ import argparse, os, sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ.setdefault("APP_ENV", "local") from azure.storage.blob import BlobServiceClient # noqa: E402 from services import azure_static # noqa: E402 def site_slugs(root) -> list[str]: base = root / azure_static.SITE_ROOT_DIR if not base.is_dir(): raise SystemExit(f"산출물 디렉터리가 없습니다: {base}") # index.html 이 있는 디렉토리만 발행본으로 본다(작업 중 잔재를 올리지 않는다). return sorted(d.name for d in base.iterdir() if d.is_dir() and (d / "index.html").is_file()) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true", help="업로드 없이 대상만 출력") args = parser.parse_args() root = azure_static.output_dir() slugs = site_slugs(root) print(f"[republish] 산출물: {root} · 사이트 {len(slugs)}개") if args.dry_run: for slug in slugs: print(f" - {slug}") return if not azure_static.is_configured(): raise SystemExit("AZURE_STORAGE_CONNECTION_STRING 이 비어 있습니다 — 올릴 곳이 없습니다.") container_name = os.environ.get("AZURE_STORAGE_CONTAINER", azure_static.DEFAULT_CONTAINER).strip() container_name = container_name or azure_static.DEFAULT_CONTAINER prefix = os.environ.get("AZURE_STORAGE_PREFIX", azure_static.DEFAULT_PREFIX).strip().strip("/") service = BlobServiceClient.from_connection_string(os.environ["AZURE_STORAGE_CONNECTION_STRING"].strip()) container = service.get_container_client(container_name) # ── 공용 자산 한 번 ── shared = azure_static._upload_shared(container, root, prefix) print(f"[republish] 공용 {len(shared)}개 (컨테이너 {container_name} · 접두사 {prefix or '(없음)'})") # ── 사이트별 ── # 한 사이트가 실패해도 나머지는 계속 올린다. 여기서 멈추면 절반만 새 번들을 가리키는 # 어중간한 상태로 남는다 — 어디까지 됐는지 로그로 남기고 끝까지 간다. failed: list[tuple[str, str]] = [] for i, slug in enumerate(slugs, 1): try: files = azure_static._upload_tree(container, root, f"{azure_static.SITE_ROOT_DIR}/{slug}", prefix) site_prefix = "/".join(p for p in (prefix, azure_static.SITE_ROOT_DIR, slug) if p) removed = azure_static._remove_stale_site_files(container, site_prefix, files) print(f" [{i}/{len(slugs)}] {slug} — 파일 {len(files)}개, 정리 {removed}개") except Exception as ex: # noqa: BLE001 — 사이트별 실패 격리가 목적이다 failed.append((slug, f"{type(ex).__name__}: {ex}")) print(f" [{i}/{len(slugs)}] {slug} — 실패: {type(ex).__name__}: {ex}") if failed: print(f"\n[republish] 실패 {len(failed)}개:") for slug, reason in failed: print(f" - {slug}: {reason}") raise SystemExit(1) print(f"\n[republish] 완료 — {len(slugs)}개 사이트") if __name__ == "__main__": main()