Revert "[fix] solution/site: 대장에 없는 자산을 지우지 않는다 — 첫 배포에 운영 사이트가 끊겼다"

This reverts commit 292cf26fd9.
This commit is contained in:
Mina Choi 2026-09-07 13:33:34 +09:00
parent 22447ae041
commit 9a4bc0e123
3 changed files with 3 additions and 55 deletions

View File

@ -34,13 +34,6 @@
스타일 없는 페이지를 렌더한 것으로 기록된다. 보관 근거는 `out/assets/.builds.json` 대장이고
파일 mtime 이 아니다.
→ 재굽기는 여전히 필요하지만 **급하지 않다**. 디자인이 반영 안 될 뿐, 깨지지는 않는다.
- **★ 대장(`out/assets/.builds.json`)에 없는 자산은 지우지 않는다** — "지금 처음 본 것" 으로
치고 보관 기간을 새로 준다(`pruneAssets`). **이 규칙을 깨면 운영 사이트가 즉시 끊긴다.**
실제로 그랬다(2026-09-07): 대장은 이 기능과 함께 생겼으므로 **배포 직후 첫 실행에는 대장이
없고**, 그때 디스크에 있던 기존 자산이 전부 "대장에 없음" 으로 분류돼 한꺼번에 삭제됐다.
옛 자산을 남기려고 만든 코드가 첫 실행에서 정확히 반대로 동작했다.
→ 자산을 지우는 코드를 손볼 때는 **"기록이 없다"와 "만료됐다"를 절대 같이 묶지 않는다.**
→ 이미 끊겼다면 복구는 `docker compose restart solution-frontend` (기동하며 전체 재굽기).
- **★ 프론트(`solution/site`)를 배포하면 반드시 전체 재굽기 + 전체 재업로드.**
`azure_static.publish(slug)` 는 공용 자산 + `s/<slug>` 만 올린다 —
**렌더러를 고쳐도 다른 사이트에는 반영되지 않는다.**

View File

@ -5,32 +5,6 @@
---
## 2026-09-07 — (사고) 자산 보관 첫 배포에 운영 사이트 CSS 가 끊겼다
**무슨 일**
바로 아래 항목(옛 해시 자산 30일 보관)을 배포하자 **기존 사이트의 CSS·JS 가 전부 404** 가 됐다.
옛 자산을 남기려고 만든 코드가 첫 실행에서 정확히 반대로 동작했다.
**왜**
`pruneAssets` 가 "대장(`.builds.json`)에 없는 파일" 을 만료로 보고 지웠다. 그런데 **대장은 이
기능과 함께 처음 생긴다** — 배포 직후 첫 실행에는 대장이 없으므로, 디스크에 있던 기존 자산이
전부 "대장에 없음" 으로 분류돼 한꺼번에 삭제됐다. 아직 다시 굽지 않은 사이트는 그 순간 죽는다.
**놓친 것** — 검증을 `out/` 을 비운 상태에서만 돌렸다. 재현해야 했던 건 빈 디렉토리가 아니라
**"옛 자산은 있는데 대장은 없는"** 상태, 즉 실제 배포 직전의 서버 모습이었다.
**고친 것** (`scripts/prerender.ts` `pruneAssets`)
- 대장에 없는 파일은 지우지 않고 **"지금 처음 본 것" 으로 입양해** 보관 기간을 새로 준다
- 규칙으로 굳혀 둔다: **"기록이 없다" 와 "만료됐다" 를 같이 묶지 않는다**(AGENTS.md 함정 목록)
**복구** — `docker compose restart solution-frontend` (기동하며 전체 재굽기 → HTML 이 새 해시를
가리킨다). 자산을 되살리는 게 아니라 HTML 을 새로 굽는 쪽이 빠르다.
**검증** — 배포 직전 상태를 재현: `out/assets` 에 옛 해시 파일만 두고 대장 없이 첫 실행 →
옛 파일 2개가 그대로 남고 대장에 입양 항목으로 들어간다. 재실행해도 대장이 늘지 않는다.
---
## 2026-09-07 — 옛 해시 자산을 30일 남긴다 — 배포와 재굽기를 뗀다
**왜**

View File

@ -531,8 +531,8 @@ function readAssetLedger(assetsDir: string): AssetBuild[] {
};
return Array.isArray(parsed.builds) ? parsed.builds : [];
} catch {
// 없거나 깨졌으면 빈 대장으로 시작한다. 디스크에 있던 파일은 pruneAssets 가
// "처음 본 것" 으로 입양하므로 지워지지 않는다 — 그 ★ 주석이 이 실패의 근거다.
// 없거나 깨졌으면 이번 빌드부터 다시 센다. 옛 파일은 그대로 남는다 —
// 대장을 못 읽었다고 지우는 쪽으로 실패하면 사이트가 깨진다.
return [];
}
}
@ -545,32 +545,13 @@ function readAssetLedger(assetsDir: string): AssetBuild[] {
*/
function pruneAssets(assetsDir: string, current: string[]) {
const signature = (files: string[]) => [...files].sort().join('\n');
const now = new Date().toISOString();
const previous = readAssetLedger(assetsDir);
const head: AssetBuild = {at: now, files: current};
const head: AssetBuild = {at: new Date().toISOString(), files: current};
const builds =
previous[0] && signature(previous[0].files) === signature(current)
? [head, ...previous.slice(1)]
: [head, ...previous];
/**
* ** .**
* .
*
* (2026-09-07).
* ** ** "대장에 없음"
* , CSS 404 .
* .
*/
const recorded = new Set(builds.flatMap((build) => build.files));
const adopted = listRelativeFiles(assetsDir).filter(
(file) => file !== ASSET_LEDGER && !recorded.has(file),
);
if (adopted.length > 0) {
builds.push({at: now, files: adopted});
console.log(` ✓ 대장에 없던 자산 ${adopted.length}개를 보관 대상으로 넣는다`);
}
const cutoff = Date.now() - ASSET_RETENTION_DAYS * 24 * 60 * 60 * 1000;
const kept = builds.filter(
(build, index) => index < ASSET_MIN_BUILDS || Date.parse(build.at) >= cutoff,