Files
Gnuboard7/tests/scenarios/installer-runtime-env-finalize.yaml
T
2026-05-15 19:24:41 +09:00

172 lines
12 KiB
YAML

# audit:allow test-scenario-coverage reason: 본 매니페스트는 인스톨러 .env 분리 흐름의 시나리오 매트릭스 SSoT 로 기록. 핵심 회귀 가드는 test_files 의 통과 테스트 + Manual 시나리오 검증으로 커버.
feature: 인스톨러 .env 분리 흐름 (storage/installer/runtime.php → finalize-env.php)
description: |
설치 진행 중 동적 설정(DB 자격증명, APP_KEY) 을 .env 가 아닌
storage/installer/runtime.php 에 기록하여 php artisan serve 의 .env mtime
워처가 SSE/폴링 진행 중 워커를 재시작하지 않도록 한다. 설치 완료 UI 노출
직후 브라우저가 finalize-env.php 를 fire-and-forget 으로 호출하면 그 시점
에 .env 1회 머지 + INSTALLER_COMPLETED 추가가 일어난다.
배경:
- php artisan serve 의 ServeCommand 가 .env mtime 을 폴링 → 변경 시 워커 재시작.
- 기존 인스톨러는 Step 5 SSE 진행 중 .env 를 3회 변경 (env_update / key:generate / complete_flag)
하여 사용자에게 "Environment modified. Restarting server..." 와 함께 SSE 끊김이 보였다.
- 동적 설정을 .env 와 물리적으로 분리하고, .env 갱신은 완료 UI 노출 후로 미루는 설계.
axes:
installation_mode: [sse, polling]
runtime_state: [absent, present_with_db_only, present_with_db_and_key, present_corrupted]
env_state: [example_only, partially_filled, fully_finalized]
finalize_call: [success, browser_aborted, retry_after_failure]
rollback_trigger: [none, mid_install, post_finalize]
# 이슈 #371 — finalize 가드 자가 차단 회귀 axis
g7_installed_lock_state: [absent, present_before_finalize]
finalize_guard_signal: [lock_only, env_completed_true_only, lock_and_env_completed_true, neither]
orphaned_recovery_path: [none, upgrade_step_auto_finalize, upgrade_step_warn_manual_review]
# 이슈 #371 — 웹 서버 사용자/그룹 정합 axis (Upgrade Step 의 권한 조정)
cli_vs_phpfpm_user: [same_user, different_user_phpfpm_group_member, different_user_no_group_link]
env_preexisting_mode: [absent, 0600_strict, 0640_group_readable, 0664_group_writable]
bootstrap_cache_owner_group: [phpfpm_user, root_with_phpfpm_group, missing]
# 이슈 #371 — 부모 프로세스 stale ENV 가 spawn 자식에 전파되어
# Access denied 가 발생하던 회귀 차단 axis (한시적 보정 — beta.8 이후 트리거 안 됨)
parent_process_env_state: [matches_runtime, stale_from_env_example_root, stale_partial]
exclusions:
- { runtime_state: absent, env_state: example_only, finalize_call: success, reason: "runtime 부재 시 finalize 는 멱등적 no-op" }
- { runtime_state: present_corrupted, finalize_call: success, reason: "손상된 runtime 은 머지 미수행, runtime 보존" }
- { rollback_trigger: post_finalize, runtime_state: present_with_db_and_key, reason: "finalize 후 rollback 시점에는 runtime.php 가 이미 삭제된 상태" }
- { g7_installed_lock_state: present_before_finalize, finalize_guard_signal: lock_only, finalize_call: success, reason: "이슈 #371 회귀 가드 — 락 단독 시 finalize 통과해야 함 (자가 차단 금지)" }
- { orphaned_recovery_path: upgrade_step_warn_manual_review, env_state: fully_finalized, runtime_state: absent, reason: "정상 finalize 완료 환경은 warn 경로 자체가 트리거되지 않음" }
effects:
- install_step5_sse_does_not_modify_env_file
- install_step5_polling_does_not_modify_env_file
- env_update_task_writes_runtime_php_with_db_credentials
- key_generate_task_writes_app_key_to_runtime_php_via_pure_php
- key_generate_task_does_not_invoke_artisan_subprocess
- complete_flag_task_does_not_modify_env_file
- complete_flag_task_creates_g7_installed_marker_file
- retry_run_preserves_existing_app_key_in_runtime_php
- boot_with_runtime_php_injects_db_credentials_into_config
- boot_with_runtime_php_injects_app_key_into_config
- boot_without_runtime_php_is_silent_noop
- boot_with_corrupted_runtime_php_silently_skips
- encrypter_lazy_resolve_picks_up_injected_app_key
- finalize_endpoint_returns_202_immediately_via_flush
- finalize_endpoint_uses_fastcgi_finish_request_when_available
- finalize_endpoint_falls_back_to_ob_end_flush_on_cli_server
- finalize_writes_env_via_single_file_put_contents_with_lock_ex_for_one_mtime_change
- finalize_replaces_app_key_line_in_env_when_present
- finalize_appends_app_key_line_when_absent_from_template
- finalize_appends_installer_completed_true_with_status_comment
- finalize_does_not_duplicate_installer_completed_on_retry
- finalize_skips_app_key_when_runtime_value_lacks_base64_prefix
- finalize_replaces_db_write_credentials_with_runtime_values
- finalize_replaces_db_read_credentials_with_runtime_values_or_syncs_with_write
- finalize_replaces_db_prefix_with_runtime_value
- finalize_appends_db_lines_when_absent_from_template
- finalize_preserves_db_lines_when_runtime_has_no_db_block
- finalize_escapes_db_password_special_characters_via_escapeEnvValue
- finalize_completes_env_even_when_state_json_is_missing
- finalize_deletes_runtime_php_after_successful_merge
- finalize_deletes_state_json_after_successful_merge_when_DELETE_INSTALLER_AFTER_COMPLETE_set
- finalize_preserves_runtime_php_on_exception_for_retry
- finalize_no_op_when_runtime_php_absent_idempotent
- complete_flag_task_does_not_delete_state_json_so_finalize_can_read_state_config
- existing_db_action_drop_tables_resets_db_cleanup_db_migrate_db_seed_markers_unconditionally
- existing_db_action_drop_tables_preserves_non_db_task_markers_like_composer_check_env_update
- existing_db_action_skip_preserves_all_completed_task_markers
- retry_after_skip_failed_migration_with_drop_tables_consent_actually_drops_tables_before_remigration
- sse_to_polling_handoff_with_drop_tables_consent_drops_partially_created_tables_then_remigrates_from_scratch
- sse_completed_with_drop_tables_drops_existing_tables_then_migrates_normally
- polling_only_with_drop_tables_drops_existing_tables_then_migrates_normally
- sse_worker_exits_immediately_on_client_disconnect_via_connection_status_check
- sse_worker_does_not_continue_in_background_after_polling_worker_takeover_to_avoid_db_race
- install_worker_php_sets_ignore_user_abort_false_explicitly
- install_worker_php_enables_ob_implicit_flush_for_fast_disconnect_detection
- acquire_worker_lock_succeeds_when_no_active_worker_or_heartbeat_stale
- acquire_worker_lock_rejects_new_worker_when_active_heartbeat_within_stale_window
- acquire_worker_lock_takes_over_when_previous_heartbeat_exceeded_stale_seconds
- sse_emitter_refreshes_worker_heartbeat_on_each_emit_call
- polling_emitter_refreshes_worker_heartbeat_on_each_emit_call
- emitter_exits_immediately_when_another_worker_took_over_lock
- install_worker_releases_lock_on_normal_termination_via_register_shutdown_function
- install_process_polling_releases_lock_on_normal_termination
- install_worker_rejected_response_uses_aborted_event_with_busy_reason
- polling_rejected_logs_to_installation_log_and_exits_silently
- rollback_complete_flag_removes_runtime_php_when_present
- rollback_complete_flag_removes_installer_completed_from_env_when_present
- rollback_complete_flag_removes_g7_installed_marker_when_present
- artisan_serve_workers_restart_at_most_once_after_completion_ui_shown
- non_serve_environments_observe_zero_extra_restart
# 이슈 #371 — finalize 가드 자가 차단 회귀 차단 + 잔존 환경 자동 복구
- finalize_guard_passes_when_only_g7_installed_lock_is_present
- finalize_guard_passes_when_env_lacks_installer_completed_flag
- finalize_guard_blocks_when_env_has_installer_completed_true_idempotent
- finalize_guard_blocks_when_env_has_installer_completed_true_with_quoted_value
- finalize_guard_accepts_alternative_truthy_values_1_and_yes
- upgrade_step_auto_finalizes_orphaned_runtime_when_env_not_marked_completed
- upgrade_step_skips_when_runtime_php_is_absent_idempotent_noop
- upgrade_step_warns_when_env_marked_completed_but_runtime_php_still_exists_no_auto_delete
- upgrade_step_falls_back_to_env_example_when_env_file_is_absent
- upgrade_step_preserves_runtime_php_when_env_write_fails_safe_fallback
- upgrade_step_logs_skip_when_runtime_php_returns_non_array_format_mismatch
- upgrade_step_deletes_state_json_after_merge_when_DELETE_INSTALLER_AFTER_COMPLETE_set
- upgrade_step_preserves_state_json_when_DELETE_INSTALLER_AFTER_COMPLETE_unset
# 이슈 #371 — 웹 서버 권한 자동 조정
- upgrade_step_preserves_existing_env_mode_uid_gid_when_env_existed_before_merge
- upgrade_step_preserves_strict_0600_env_mode_when_owner_intent_present
- upgrade_step_uses_0640_default_permission_for_newly_created_env_not_0600
- upgrade_step_detects_web_server_group_from_bootstrap_cache_for_new_env
- upgrade_step_falls_back_to_storage_logs_when_bootstrap_cache_absent
- upgrade_step_skips_chown_when_lacking_root_privilege_silent_with_log_hint
- upgrade_step_skips_uid_when_bootstrap_cache_owner_is_root_to_avoid_root_owned_env
- upgrade_step_inherits_group_only_when_bootstrap_cache_owner_is_root_but_group_is_not
# 이슈 #371 — 부모 프로세스 process ENV 도 머지된 자격증명으로 갱신하여 spawn 자식 전파 보장
- upgrade_step_refreshes_parent_process_env_with_merged_db_write_credentials
- upgrade_step_refreshes_parent_process_env_with_merged_db_read_credentials
- upgrade_step_skips_env_refresh_when_runtime_credential_keys_are_missing
- upgrade_step_does_not_overwrite_app_key_env_when_runtime_value_lacks_base64_prefix
test_files:
- tests/Unit/Providers/InstallerRuntimeServiceProviderTest.php
- tests/Unit/Installer/InstallerRuntimeHelperTest.php
- tests/Unit/Installer/InstallerGuardTest.php
- tests/Feature/Console/Core/FinalizeOrphanedInstallerRuntimeTest.php
- tests/Feature/Upgrade/Beta7FinalizeOrphanedInstallerRuntimeTest.php
manual_verification:
- scenario: A_artisan_serve
description: |
php artisan serve 환경에서 Step 1~5 진행 중 "Environment modified" 메시지
0회 + Step 6 완료 화면 노출 직후 1회 재시작 + 재시작 후 .env 에 DB 자격증명
+ APP_KEY + INSTALLER_COMPLETED=true 모두 존재 + storage/installer/ 비어있음
- scenario: B_php_S_or_apache
description: |
워처 없는 환경에서 동일 흐름 실행 시 동작 차이 없음 (재시작 미발생, finalize
이후 .env 가 동일하게 머지됨)
- scenario: C_polling_mode
description: |
installer.js 가 폴링 모드로 강제 진입한 경우 state-management.php → completed
→ finalize 트리거 → .env 머지 동일하게 동작
- scenario: D_tab_close
description: |
Step 6 완료 직후 탭을 닫는 경우 runtime.php 가 보존되어 다음 부팅에서도
ServiceProvider 가 config 주입 → 앱 정상 동작 (관리자 재호출 또는 admin
알림 경로로 재시도 가능)
- scenario: E_rollback
description: |
Step 5 진행 중 의도된 실패 (DB 권한 오류 등) 발생 시 rollbackCompleteFlag
가 runtime.php / g7_installed / .env 의 INSTALLER_COMPLETED 모두 정리하여
인스톨러 재진입 가능
rules_layer_coverage:
- rule: service-provider-db-safety
coverage: InstallerRuntimeServiceProvider 는 runtime.php 부재 시 즉시 return, DB 미접근, Schema::hasTable 무사용
- rule: service-direct-data-access
coverage: Provider 는 Config::set 만 호출 (Eloquent/DB facade 미사용)
- rule: i18n-throw-hardcoded-korean
coverage: rollback_runtime_removed lang key 를 ko/en 양쪽 등록 (public/install/lang)