fix(core,admin_basic): 정적 게시 재게시 누락 3건 수정 + 관리자 수동 복구 UI + sudo core:update 소유권 정합

- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다
- config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단
- custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장
- 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제)
- 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비
- sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트
- 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
This commit is contained in:
HeuJung committed 2026-09-06 15:22:52 +09:00
1 parent d263f74cae
commit de18159986
105 files changed
+4055 -208

No files matched your search

@@ -14,6 +14,7 @@ use App\Services\ExtensionBundleService;
use App\Services\ExtensionStaticCacheService;
use App\Services\LanguagePackService;
use App\Services\TemplateService;
use App\Support\CustomAssets;
use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Filesystem\Filesystem;
@@ -288,6 +289,132 @@ class ExtensionStaticCacheServiceTest extends TestCase
}
}
/**
* 게시된 custom 파일 집합 == `CustomAssets::publishableFiles()` 집합 (#651 F7 패리티).
*
* 변경 감지가 이 열거자로 서명을 만들므로, 게시가 다른 집합을 복사하면 "감지됐는데 게시본에
* 없다" 또는 "게시본에 있는데 바꿔도 감지되지 않는다" 가 된다. 두 소비자가 같은 열거자를
* 쓰는지를 산출물로 잠근다.
*
* @effects custom_signature_covers_publishable_tree
*/
public function test_published_custom_set_matches_publishable_files(): void
{
$template = $this->createActiveTemplate();
$identifier = (string) $template->identifier;
$dir = base_path("templates/{$identifier}/custom");
File::ensureDirectoryExists($dir.'/img/deep');
File::put($dir.'/custom.css', "body { background-image: url('./img/deep/m.svg'); }");
File::put($dir.'/img/deep/m.svg', '<svg/>');
File::put($dir.'/fonts.woff2', 'font');
File::put($dir.'/notes.txt', 'not an asset');
try {
CustomAssets::flushCache();
$service = $this->service();
$this->assertTrue($service->publishCurrent(), $this->publishDiagnostics($service));
$customRoot = $service->versionDir(self::VERSION)."/templates/{$identifier}/assets/custom";
$published = [];
foreach (File::allFiles($customRoot) as $file) {
$published[] = str_replace('\\', '/', $file->getRelativePathname());
}
sort($published, SORT_STRING);
$enumerated = array_column(CustomAssets::publishableFiles('templates', $identifier), 'relative');
$this->assertSame($enumerated, $published, '게시된 custom 집합이 공유 열거자 집합과 다르다 — 감지와 게시 범위가 어긋난다');
$this->assertSame(['custom.css', 'fonts.woff2', 'img/deep/m.svg'], $published);
} finally {
File::deleteDirectory($dir);
// 요청 스코프 메모가 지운 파일을 가리키지 않도록 비운다 (같은 프로세스의 다음 테스트가 게시한다)
CustomAssets::flushCache();
}
}
/**
* 상태 보고서는 미게시/게시 상태를 필드로 구분해 돌려준다 — CLI·API·화면이 같은 판정을 소비한다.
*
* @effects status_endpoint_returns_report_fields
*/
public function test_status_report_reflects_publish_state(): void
{
$service = $this->service();
$before = $service->statusReport();
$this->assertSame(self::VERSION, $before['version']);
$this->assertFalse($before['published']);
$this->assertSame(0, $before['files']);
$this->assertNull($before['published_at']);
$this->assertFalse($before['publishable'], 'testing 환경은 publishable 이 아니다');
$this->assertTrue($before['enabled']);
$this->assertNull($before['failure']);
$this->assertSame([], $before['retained_versions']);
$this->assertTrue($service->publishCurrent(), $this->publishDiagnostics($service));
$after = $service->statusReport();
$this->assertTrue($after['published']);
$this->assertGreaterThan(0, $after['files']);
$this->assertNotNull($after['published_at']);
$this->assertSame([self::VERSION], $after['retained_versions']);
$this->assertTrue($after['tree_writable']);
}
/**
* 수동 복구는 버전을 올리고 새 버전을 강제 게시한다 (`force` 없으면 같은 초 재게시가 skip 된다).
*
* @effects republish_bumps_version_and_publishes
*/
public function test_republish_bumps_version_and_force_publishes(): void
{
$this->app['env'] = 'production';
ExtensionStaticCacheService::fakeRootProcessForTesting(false);
$service = $this->service();
$result = $service->republish();
$this->assertTrue($result['republished'], json_encode($result));
$this->assertSame(self::VERSION, $result['previous_version']);
$this->assertGreaterThan(self::VERSION, $result['version']);
$this->assertTrue($result['version_changed']);
$this->assertTrue($result['published']);
$this->assertNull($result['reason']);
$this->assertFileExists($service->versionDir($result['version']).'/manifest.json');
// 같은 초의 연속 복구도 **내용을 다시 만든다** — manifest 시각이 갱신된다
$firstManifest = (string) file_get_contents($service->versionDir($result['version']).'/manifest.json');
sleep(1);
$second = $service->republish();
$this->assertTrue($second['republished']);
$this->assertNotSame($firstManifest, (string) file_get_contents($service->versionDir($second['version']).'/manifest.json'));
}
/**
* 게시가 쓰이지 않는 환경(비프로덕션 / kill-switch)에서는 bump 하지 않고 사유를 돌려준다.
*
* @effects republish_disabled_when_not_publishable
*/
public function test_republish_refuses_without_bump_when_not_publishable(): void
{
$service = $this->service();
$result = $service->republish();
$this->assertFalse($result['republished']);
$this->assertSame('not_production', $result['reason']);
$this->assertSame(self::VERSION, $result['version']);
$this->assertFalse($result['version_changed']);
config(['core.static_cache.enabled' => false]);
$this->app['env'] = 'production';
$result = $service->republish();
$this->assertFalse($result['republished']);
$this->assertSame('disabled', $result['reason']);
$this->assertSame(self::VERSION, ExtensionStaticCacheService::getExtensionCacheVersion());
}
/**
* 비활성 확장의 custom 은 게시하지 않는다.
*