fix(core,installer): dev vendor 설치본의 코어 업데이트 중단 수정 — stale 패키지 매니페스트 3계층

Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)

3계층으로 막는다.

 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
 자체에 도달할 수 없다).

업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.

실측 중 드러난 결함 2건을 함께 고쳤다.

 - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
 - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).

인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).

코어 7.0.10 → 7.0.11.
This commit is contained in:
HeuJung
2026-09-08 09:46:57 +09:00
parent 21177c8e17
commit ffab451b4a
42 changed files with 2660 additions and 82 deletions
@@ -39,6 +39,8 @@ class CoreUpdateCommandSpawnFailureTest extends TestCase
private ?string $originalEnvVersion = null;
private ?string $originalUpdateFlag = null;
protected function setUp(): void
{
parent::setUp();
@@ -46,6 +48,12 @@ class CoreUpdateCommandSpawnFailureTest extends TestCase
$this->failingStepPath = base_path('upgrades/Upgrade_0_0_1_test_spawn_failure_fail.php');
$this->silentStepPath = base_path('upgrades/Upgrade_0_0_1_test_spawn_failure_silent.php');
// 업데이트 트리 플래그도 원값을 보관하고 비운다 — env APP_VERSION 우선 판독이 이 플래그로
// 게이트되므로, 다른 테스트가 남긴 플래그가 있으면 부모/자식 시나리오 구분이 무너진다.
$flag = $_ENV['G7_UPDATE_IN_PROGRESS'] ?? $_SERVER['G7_UPDATE_IN_PROGRESS'] ?? getenv('G7_UPDATE_IN_PROGRESS');
$this->originalUpdateFlag = ($flag === false || $flag === null) ? null : (string) $flag;
$this->setUpdateFlag(false);
// stale 가드는 spawn 자식이 받는 env APP_VERSION 을 먼저 읽는다 (config 캐시가 있으면
// config('app.version') 은 캐시에 박힌 구버전이라 신뢰할 수 없다 — 7.0.9→7.0.10 실사례).
// 각 테스트가 부모/자식 시나리오에 맞춰 env 를 직접 세우도록 원값을 보관하고 비운다.
@@ -63,9 +71,36 @@ class CoreUpdateCommandSpawnFailureTest extends TestCase
$this->setEnvVersion($this->originalEnvVersion);
if ($this->originalUpdateFlag === null) {
$this->setUpdateFlag(false);
} else {
$_ENV['G7_UPDATE_IN_PROGRESS'] = $this->originalUpdateFlag;
$_SERVER['G7_UPDATE_IN_PROGRESS'] = $this->originalUpdateFlag;
putenv('G7_UPDATE_IN_PROGRESS='.$this->originalUpdateFlag);
}
parent::tearDown();
}
/**
* 업데이트 트리 플래그를 세 채널($_ENV/$_SERVER/putenv)에 세우거나 지웁니다.
*
* @param bool $enabled true 면 플래그 설정, false 면 세 채널 모두 제거
*/
private function setUpdateFlag(bool $enabled): void
{
if (! $enabled) {
unset($_ENV['G7_UPDATE_IN_PROGRESS'], $_SERVER['G7_UPDATE_IN_PROGRESS']);
putenv('G7_UPDATE_IN_PROGRESS');
return;
}
$_ENV['G7_UPDATE_IN_PROGRESS'] = '1';
$_SERVER['G7_UPDATE_IN_PROGRESS'] = '1';
putenv('G7_UPDATE_IN_PROGRESS=1');
}
/**
* 프로세스 env 의 APP_VERSION 을 세우거나(문자열) 비운다(null).
*
@@ -436,6 +471,10 @@ PHP);
public function run_upgrade_steps_spawn_자식은_config_캐시가_stale_해도_env_버전으로_가드를_통과한다(): void
{
// 자식이 받는 env 는 toVersion, 캐시로 부팅한 config 는 fromVersion.
// 실제 spawn 은 `APP_VERSION` 과 `G7_UPDATE_IN_PROGRESS=1` 을 **함께** 넘긴다
// (CoreUpdateCommand::spawnUpgradeStepsProcess 의 $env). env 우선 판독은 그 플래그가
// 그리는 업데이트 트리 안에서만 켜지므로, 자식 시뮬레이션은 둘 다 세워야 실제와 같다.
$this->setUpdateFlag(true);
$this->setEnvVersion('7.0.10');
config(['app.version' => '7.0.9']);
config(['app.update.spawn_failure_mode' => 'abort']);