fix(core): root 프로세스의 terminating 정적 게시를 웹 자가 치유로 위임

sudo 코어 업데이트(7.0.9→7.0.10 실서버)에서 자식 프로세스(신코드, root)의
terminating 게시가 소유권 복원 이후 시점에 실행되며 캐시 락 샤드 디렉토리
(storage/framework/cache/data/xx)와 병합 번들(storage/app/ext-bundles)을
root 소유로 남겼고, 이후 웹 프로세스의 캐시 쓰기가 그 샤드에 해시되는 순간
Permission denied 로 죽어 전면 500 이 됐다 (laravel.log 무기록 — 로거 도달 전).

normalizeOwnership 은 게시 트리(build/ext)만 다뤘다 — 게시의 간접 write-set
(캐시 락·번들 빌드)이 사각이었다. 뒤쫓아 chown 하는 대신 root 프로세스에서는
terminating 자동 게시를 예약하지 않고 다음 웹 렌더의 자가 치유(웹 계정)에
위임한다. 명시적 ext-static:publish 는 게이트 밖(운영자 책임 — 규정 §6).

- 시나리오 매니페스트에 process_user(web/root_cli) 축 신설 — uid 조합 실측
 부재가 이 결함을 통과시킨 구조적 원인 (기존 마커 전수에 web 부여)
- red 소급 증빙: 게이트 부재 커밋본에서 신규 케이스 실패 확인 → 15/15 green
This commit is contained in:
HeuJung committed 2026-08-25 17:37:24 +09:00
1 parent 5ba7a83597
commit 0bb33e3baa
9 files changed
+98 -17

No files matched your search

@@ -92,7 +92,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
/**
* 활성 템플릿 0개(설치 직전)여도 예외 없이 빈 게시가 성립한다.
*
* @scenario publish_state=unpublished, environment=production, trigger=manual_command
* @scenario publish_state=unpublished, environment=production, trigger=manual_command, process_user=web
*/
public function test_publishes_empty_tree_when_no_active_templates(): void
{
@@ -239,7 +239,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
/**
* 멱등 — 게시 완료 상태에서 재호출은 skip (기존 게시물 유지), force 는 재게시.
*
* @scenario publish_state=published, environment=production, trigger=manual_command
* @scenario publish_state=published, environment=production, trigger=manual_command, process_user=web
*
* @effects publish_is_idempotent_until_forced
*/
@@ -264,7 +264,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
/**
* 쓰기 단계 실패 시 예외를 삼키고 false + tmp 잔존물 정리 + manifest 부재.
*
* @scenario publish_state=partial, environment=production, trigger=lifecycle
* @scenario publish_state=partial, environment=production, trigger=lifecycle, process_user=web
*
* @effects write_failure_cleans_tmp_and_falls_back
*/
@@ -326,7 +326,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
/**
* kill-switch(core.static_cache.enabled=false) — 게시 자체가 중단된다.
*
* @scenario publish_state=unpublished, environment=production, trigger=kill_switch
* @scenario publish_state=unpublished, environment=production, trigger=kill_switch, process_user=web
*
* @effects kill_switch_disables_publish_and_gate
*/
@@ -343,7 +343,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
/**
* terminating 트리거 — 비프로덕션(testing)에서는 예약되지 않는다.
*
* @scenario publish_state=unpublished, environment=dev, trigger=lifecycle
* @scenario publish_state=unpublished, environment=dev, trigger=lifecycle, process_user=web
*
* @effects terminating_publish_gated_to_production
*/
@@ -359,7 +359,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
/**
* terminating 트리거 — 프로덕션에서는 종료 시점의 현재 버전으로 게시된다.
*
* @scenario publish_state=unpublished, environment=production, trigger=lifecycle
* @scenario publish_state=unpublished, environment=production, trigger=lifecycle, process_user=web
*
* @effects terminating_publish_uses_final_version_after_burst
*/
@@ -378,6 +378,34 @@ class ExtensionStaticCacheServiceTest extends TestCase
$this->assertFalse($this->service()->isPublished(self::VERSION));
}
/**
* root 프로세스(sudo CLI)에서는 terminating 게시가 예약되지 않는다.
*
* root 로 게시하면 캐시 락 샤드 디렉토리(`storage/framework/cache/data/xx`)와
* 병합 번들(`storage/app/ext-bundles`)이 root 소유로 남고, 이후 웹 프로세스의
* 캐시 쓰기가 그 샤드에 해시되는 순간 Permission denied 로 죽는다
* (실사례: sudo 코어 업데이트 직후 전면 500). 게시는 다음 웹 렌더의
* 자가 치유(웹 계정)가 수행한다.
*
* @scenario publish_state=unpublished, environment=production, trigger=lifecycle, process_user=root_cli
*
* @effects root_cli_defers_publish_to_web_self_heal
*/
public function test_terminating_publish_not_scheduled_for_root_process(): void
{
app()['env'] = 'production';
ExtensionStaticCacheService::fakeRootProcessForTesting(true);
try {
ExtensionStaticCacheService::schedulePublishOnTerminate();
$this->app->terminate();
$this->assertDirectoryDoesNotExist($this->service()->baseDir());
} finally {
ExtensionStaticCacheService::fakeRootProcessForTesting(null);
}
}
/**
* terminating 게시 예약 플래그는 콜백 실행 시점에 리셋된다.
*
@@ -386,7 +414,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
* 않은 채 플래그만 true 라 그 워커에서 영구 미게시가 되고, `AssetUrl` 자가 치유도
* 같은 플래그를 쓰므로 복구 경로가 없다. FPM(요청=프로세스)에서는 무영향.
*
* @scenario publish_state=published, environment=production, trigger=lifecycle
* @scenario publish_state=published, environment=production, trigger=lifecycle, process_user=web
*
* @effects terminating_schedule_rearms_after_execution
*/
@@ -409,7 +437,7 @@ class ExtensionStaticCacheServiceTest extends TestCase
* 게시 디렉토리가 스스로 압축을 선언해야 종전 API 대비 전송량 회귀가 없다
* (실측: lang/ko.json 524,915B 비압축 전송). nginx 는 규정 문서의 gzip 스니펫이 담당한다.
*
* @scenario publish_state=published, environment=production, trigger=manual_command
* @scenario publish_state=published, environment=production, trigger=manual_command, process_user=web
*
* @effects published_htaccess_declares_compression
*/