Merge branch 'develop'
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
- 설치가 끝난 사이트에서 매 요청마다 반복되던 불필요한 데이터베이스 조회를 줄여 페이지 응답 속도를 개선했습니다. 알림·본인인증 설정을 준비하는 과정에서 이미 존재가 보장된 테이블을 매번 다시 확인하던 동작을 설치 완료 상태에서는 건너뛰도록 했습니다.
|
||||
- 화면을 그릴 때마다 서버가 매번 확장 기능의 연결 정보를 처음부터 다시 계산하던 부담을 줄여 페이지 응답 속도를 개선했습니다. 이 연결 정보는 확장을 설치·수정하거나 코어를 업데이트할 때만 바뀌므로 그 시점에 한 번 저장해 두고, 이후 요청에서는 저장된 값을 재사용하도록 했습니다. 확장이나 코어를 변경하면 저장 값이 자동으로 갱신됩니다.
|
||||
- 설치된 모듈·플러그인의 코드를 찾을 때마다 서버가 파일을 하나하나 탐색하던 부담을 줄여, 특히 공유 호스팅 등 파일 접근이 느린 환경에서 페이지 응답 속도를 개선했습니다. 확장 코드의 위치를 설치·수정 시점에 미리 정리해 두고 이후 요청에서는 그 목록을 바로 참조하도록 했습니다. 확장을 설치·수정·삭제하거나 코어를 업데이트하면 목록이 자동으로 갱신됩니다.
|
||||
- 코어 업데이트 중 데이터 변환 단계가 현재 환경에서 자동으로 끝까지 실행되지 못하고 중단될 때, 남은 단계를 이어서 실행하는 안내를 실행 권한에 맞게 개선했습니다. 특히 관리자 권한(sudo)으로 업데이트한 경우, 남은 단계를 관리자 권한 그대로 실행하면 그 과정에서 만들어지는 파일이 관리자 소유로 남아 이후 웹사이트가 그 파일에 쓰지 못하는 문제가 생길 수 있습니다. 이제 이런 경우 웹 서버 실행 계정(예: www-data)으로 실행하도록 정확한 명령을 함께 안내하며, 웹 서버 계정을 자동으로 확인하지 못하면 어떤 계정으로 실행해야 하는지 확인 방법을 안내합니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Extension\Traits\ClearsTemplateCaches;
|
||||
use App\Extension\Vendor\Exceptions\VendorInstallException;
|
||||
use App\Extension\Vendor\VendorMode;
|
||||
use App\Services\CoreUpdateService;
|
||||
use App\Support\ConfigCacheHelper;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -489,7 +490,7 @@ class CoreUpdateCommand extends Command
|
||||
// 재생성하지 않으면 업데이트 후 config:cache 가 비활성 상태로 남아 이후 모든
|
||||
// 요청이 config 파일을 재파싱한다(성능 손실). ConfigCacheHelper 는 설치 미완료
|
||||
// 상태를 가드하고 실패를 안전하게 흡수한다.
|
||||
\App\Support\ConfigCacheHelper::rebuild();
|
||||
ConfigCacheHelper::rebuild();
|
||||
|
||||
$log('정리 완료');
|
||||
|
||||
@@ -600,7 +601,7 @@ class CoreUpdateCommand extends Command
|
||||
$this->line(" 사유: {$e->reason}");
|
||||
$this->newLine();
|
||||
$this->info('나머지 스텝을 적용하려면 아래 명령을 실행하세요 (재다운로드 없이 스텝만 실행):');
|
||||
$this->line(" {$resumeCommand}");
|
||||
$this->surfaceResumeCommandWithPermissionGuidance($resumeCommand);
|
||||
$this->newLine();
|
||||
$this->warn('⚠ 위 명령을 실행하지 않으면 버전 표시는 최신이지만 일부 스텝이 미실행 상태로 남습니다.');
|
||||
$this->newLine();
|
||||
@@ -1179,6 +1180,125 @@ class CoreUpdateCommand extends Command
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 업그레이드 스텝 재실행 명령을 실행 사용자 권한에 맞는 안내와 함께 출력합니다.
|
||||
*
|
||||
* 배경: sudo(root) 로 `core:update` 를 실행하면 업그레이드 스텝 spawn 자식이 root 로
|
||||
* 돌아가고, 그 자식이 실패(proc_open 미지원 · 비정상 종료 · silent skip)하면 스텝이
|
||||
* 미실행 상태로 남아 운영자에게 재실행을 안내한다. 이때 운영자가 안내받은 명령을 그대로
|
||||
* root 로 재실행하면 스텝이 만드는 파일·캐시가 root 소유로 생성되어, 이후 웹서버
|
||||
* (php-fpm www-data 등) 요청이 그 경로에 쓰기 실패한다.
|
||||
*
|
||||
* 실행 환경을 4가지로 분기해 안내한다:
|
||||
* 1. non-root (일반 SSH 사용자 = 파일 소유자) 또는 posix 미지원(Windows 등)
|
||||
* → 명령만 그대로 안내 (권한 분기 불필요, 공유 호스팅 = 웹서버·PHP·실행 유저 동일 포함).
|
||||
* 2. root 실행 + 웹서버 계정 식별 가능 + 실행 사용자와 다름
|
||||
* → `sudo -u {webUser}` 로 재실행 안내 + root 그대로 실행 시 위험 경고.
|
||||
* 3. root 실행 + 웹서버 계정을 root 로 추정 (root 로 서비스하는 비표준/컨테이너 구성)
|
||||
* → 권한 문제 없으므로 명령만 그대로 안내.
|
||||
* 4. root 실행 + 웹서버 계정 식별 불가 (스냅샷/추정 실패)
|
||||
* → 명령 그대로 안내하되 "웹서버 계정으로 실행" 일반 경고 (계정명 미상).
|
||||
*
|
||||
* @param string $resumeCommand 재실행할 `core:execute-upgrade-steps` 명령
|
||||
*/
|
||||
private function surfaceResumeCommandWithPermissionGuidance(string $resumeCommand): void
|
||||
{
|
||||
[$mode, $webUser] = $this->classifyResumeExecutionContext();
|
||||
|
||||
$this->renderResumeGuidance($resumeCommand, $mode, $webUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 환경 모드에 따라 재실행 명령과 권한 안내를 콘솔에 출력합니다.
|
||||
*
|
||||
* `classifyResumeExecutionContext()` 판정 결과를 입력으로 받아 출력만 담당한다 —
|
||||
* posix/파일시스템 상태에 의존하지 않으므로 4가지 모드 전부 결정적으로 테스트 가능하다.
|
||||
*
|
||||
* @param string $resumeCommand 재실행할 `core:execute-upgrade-steps` 명령
|
||||
* @param string $mode `classifyResumeExecutionContext()` 가 반환한 실행 환경 모드
|
||||
* @param string|null $webUser 웹서버 계정명 (`root_web_known` 일 때만 유효)
|
||||
*/
|
||||
private function renderResumeGuidance(string $resumeCommand, string $mode, ?string $webUser): void
|
||||
{
|
||||
// 케이스 1·3: 권한 분기 불필요 — 명령만 그대로 안내.
|
||||
// - non_root: 일반 SSH 사용자 / 공유 호스팅(웹서버=PHP=실행 유저 동일) / posix 미지원
|
||||
// - root_web_symmetric: root 로 서비스하는 구성 → 재실행도 root 로 무해
|
||||
if ($mode === 'non_root' || $mode === 'root_web_symmetric') {
|
||||
$this->line(" {$resumeCommand}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 케이스 2: root 실행 + 웹서버 계정 식별 → 웹서버 권한 재실행 안내.
|
||||
if ($mode === 'root_web_known' && $webUser !== null) {
|
||||
$this->line(" sudo -u {$webUser} {$resumeCommand}");
|
||||
$this->newLine();
|
||||
$this->warn('⚠ 현재 sudo(root) 로 실행 중입니다. 위 명령을 root 로 그대로 실행하면 업그레이드');
|
||||
$this->warn(" 스텝이 생성하는 파일이 root 소유가 되어 이후 웹서버({$webUser}) 요청이 쓰기 실패할 수");
|
||||
$this->warn(" 있습니다. 반드시 웹서버 계정({$webUser})으로 재실행하세요.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 케이스 4: root 실행 + 웹서버 계정 식별 불가 — 계정명 미상 상태의 일반 경고.
|
||||
$this->line(" sudo -u <웹서버계정> {$resumeCommand}");
|
||||
$this->newLine();
|
||||
$this->warn('⚠ 현재 sudo(root) 로 실행 중입니다. 웹서버 계정을 자동으로 식별하지 못했습니다.');
|
||||
$this->warn(' 위 명령을 root 로 그대로 실행하면 업그레이드 스텝이 생성하는 파일이 root 소유가 되어');
|
||||
$this->warn(' 이후 웹서버(php-fpm) 요청이 쓰기 실패할 수 있습니다. 웹서버 계정(예: www-data,');
|
||||
$this->warn(' nginx, apache)을 확인해 그 계정으로 재실행하세요.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 재실행 안내의 실행 환경을 분류합니다.
|
||||
*
|
||||
* 반환하는 모드:
|
||||
* - `non_root` : posix 미지원 또는 현재 유효 사용자가 root 가 아님
|
||||
* (일반 SSH 사용자 = 파일 소유자, 공유 호스팅 대칭 구성 포함).
|
||||
* - `root_web_known` : root 실행 + 웹서버 계정 식별 성공 + 실행 사용자(root)와 다름.
|
||||
* - `root_web_symmetric` : root 실행 + 웹서버 계정이 root 로 추정됨 (root 서비스 구성).
|
||||
* - `root_web_unknown` : root 실행 + 웹서버 계정 추정 실패 (스냅샷/추정 불가).
|
||||
*
|
||||
* 웹서버 계정은 `FilePermissionHelper::inferWebServerOwnership()` 이 storage/bootstrap
|
||||
* 쓰기 영역 소유자로 추정한다.
|
||||
*
|
||||
* @return array{0: string, 1: string|null} [모드, 웹서버 계정명 또는 null]
|
||||
*/
|
||||
private function classifyResumeExecutionContext(): array
|
||||
{
|
||||
if (! function_exists('posix_geteuid') || ! function_exists('posix_getpwuid')) {
|
||||
return ['non_root', null];
|
||||
}
|
||||
|
||||
// root(sudo) 실행이 아니면 일반 SSH 사용자 = 파일 소유자 → 권한 분기 안내 불필요.
|
||||
// (공유 호스팅에서 웹서버·PHP·실행 유저가 같은 경우도 여기서 non_root 로 처리됨.)
|
||||
if (posix_geteuid() !== 0) {
|
||||
return ['non_root', null];
|
||||
}
|
||||
|
||||
[$owner] = FilePermissionHelper::inferWebServerOwnership();
|
||||
|
||||
// 추정 실패 (스냅샷 불가) — 계정명 미상 경고 경로.
|
||||
if ($owner === false) {
|
||||
return ['root_web_unknown', null];
|
||||
}
|
||||
|
||||
// 웹서버 계정이 root 로 추정됨 — root 로 서비스하는 구성이라 재실행도 root 로 무해.
|
||||
if ($owner === 0) {
|
||||
return ['root_web_symmetric', null];
|
||||
}
|
||||
|
||||
$entry = posix_getpwuid($owner);
|
||||
$name = $entry['name'] ?? null;
|
||||
|
||||
// uid 는 나왔지만 이름 해석 실패 — 미상 경로로 처리 (uid 노출은 오히려 혼란).
|
||||
if ($name === null) {
|
||||
return ['root_web_unknown', null];
|
||||
}
|
||||
|
||||
return ['root_web_known', $name];
|
||||
}
|
||||
|
||||
/**
|
||||
* `restoreOwnership()` 직후 누적된 권한 정상화 실패 경고를 콘솔/로그에 즉시 노출합니다.
|
||||
*
|
||||
|
||||
@@ -217,6 +217,21 @@ v접두사 자동 감지 (resolveGithubArchiveUrl):
|
||||
|
||||
> **단독 실행 안전성 (beta.6 이후)**: `core:execute-upgrade-steps` 는 HANDOFF 안내 또는 수동 복구 목적으로 운영자가 직접 호출되는 경로가 있다. 단독 실행 시 자식은 기본값으로 부모 Step 9 (`runMigrations` + `reloadCoreConfigAndResync`), Step 11 (`updateVersionInEnv` + `clearAllCaches`), Step 12 (번들 확장 일괄 업데이트) 를 자체적으로 수행해 단일 명령으로 업그레이드를 완결한다. 부모 `CoreUpdateCommand::spawnUpgradeStepsProcess()` 는 자식 명령 라인에 `--skip-migrations`, `--skip-resync`, `--skip-version-env`, `--skip-cache-clear`, `--skip-bundled-updates` 5개를 무조건 추가해 중복 회피한다 — 부모가 자식 종료 후 동일 단계를 직접 수행하기 때문이다.
|
||||
|
||||
#### 재실행 안내의 권한 분기 (핸드오프 catch)
|
||||
|
||||
spawn 자식이 실패(`proc_open` 미지원 · 비정상 종료 · silent skip)하고 `spawn_failure_mode=abort`(기본값) 이면, 파일·버전은 이미 `toVersion` 으로 반영되지만 업그레이드 스텝이 미실행 상태로 남아 운영자에게 `core:execute-upgrade-steps` 재실행을 안내한다. 이때 **sudo(root) 로 `core:update` 를 실행한 경우**, 안내받은 명령을 root 로 그대로 재실행하면 스텝이 만드는 파일·캐시가 root 소유로 생성되어 이후 웹서버(php-fpm www-data 등) 요청이 그 경로에 쓰기 실패한다.
|
||||
|
||||
`CoreUpdateCommand::surfaceResumeCommandWithPermissionGuidance()` 는 실행 환경을 4가지로 분류(`classifyResumeExecutionContext()`)하여 안내를 분기한다:
|
||||
|
||||
| 모드 | 조건 | 안내 |
|
||||
|------|------|------|
|
||||
| `non_root` | root 아님(일반 SSH 사용자 = 파일 소유자) / posix 미지원(Windows) / 공유 호스팅(웹서버·PHP·실행 유저 동일) | 명령만 그대로 출력 |
|
||||
| `root_web_known` | root 실행 + 웹서버 계정 식별 가능 + 실행 사용자와 다름 | `sudo -u {계정} {명령}` + 계정명 명시 경고 |
|
||||
| `root_web_symmetric` | root 실행 + 웹서버 계정이 root 로 추정 (root 서비스 구성) | 명령만 그대로 출력 |
|
||||
| `root_web_unknown` | root 실행 + 웹서버 계정 추정 실패 | `sudo -u <웹서버계정>` placeholder + 계정 확인 안내 |
|
||||
|
||||
웹서버 계정은 `FilePermissionHelper::inferWebServerOwnership()` 이 `storage/*`·`bootstrap/cache` 쓰기 영역 소유자로 추정한다.
|
||||
|
||||
### Step 11: 마무리
|
||||
|
||||
```text
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use App\Console\Commands\Core\CoreUpdateCommand;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* CoreUpdateCommand 의 업그레이드 스텝 재실행 권한 안내 (renderResumeGuidance) 계약 테스트.
|
||||
*
|
||||
* 배경: sudo(root) 로 `core:update` 를 실행하면 업그레이드 스텝 spawn 자식이 root 로
|
||||
* 돌아가고, 그 자식이 실패하면 스텝이 미실행 상태로 남아 운영자에게 재실행을 안내한다.
|
||||
* 이때 실행 사용자 권한에 따라 4가지 경우로 분기해 안내해야 한다:
|
||||
*
|
||||
* 1. non_root — 일반 SSH 사용자 / 공유 호스팅(웹서버=PHP=실행 유저) / posix 미지원
|
||||
* → 명령만 그대로. sudo -u 접두사·경고 없음.
|
||||
* 2. root_web_known — root 실행 + 웹서버 계정 식별 가능
|
||||
* → `sudo -u {webUser}` + 계정명 명시 경고.
|
||||
* 3. root_web_symmetric — root 실행 + 웹서버 계정이 root 로 추정 (root 서비스 구성)
|
||||
* → 명령만 그대로.
|
||||
* 4. root_web_unknown — root 실행 + 웹서버 계정 식별 불가
|
||||
* → `sudo -u <웹서버계정>` placeholder + 계정명 미상 경고.
|
||||
*
|
||||
* `classifyResumeExecutionContext()` 는 posix/파일시스템 상태에 의존하므로, 판정 결과를
|
||||
* 입력으로 받아 출력만 담당하는 `renderResumeGuidance()` 를 리플렉션으로 직접 검증한다.
|
||||
*/
|
||||
class CoreUpdateResumeGuidanceTest extends TestCase
|
||||
{
|
||||
private const RESUME = 'php artisan core:execute-upgrade-steps --from=7.0.0 --to=7.0.1 --force';
|
||||
|
||||
/**
|
||||
* 모드/유저를 입력해 renderResumeGuidance 를 실행하고 콘솔 출력 문자열을 반환한다.
|
||||
*/
|
||||
private function render(string $mode, ?string $webUser): string
|
||||
{
|
||||
$command = app(CoreUpdateCommand::class);
|
||||
|
||||
$input = new ArrayInput([]);
|
||||
$output = new BufferedOutput;
|
||||
$style = new OutputStyle($input, $output);
|
||||
|
||||
$reflection = new \ReflectionClass($command);
|
||||
$property = $reflection->getProperty('output');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($command, $style);
|
||||
|
||||
$method = new \ReflectionMethod(CoreUpdateCommand::class, 'renderResumeGuidance');
|
||||
$method->setAccessible(true);
|
||||
$method->invoke($command, self::RESUME, $mode, $webUser);
|
||||
|
||||
return $output->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스 1: non_root — 명령만 그대로. sudo -u 접두사·경고 없음.
|
||||
*/
|
||||
public function test_non_root_prints_command_verbatim_without_permission_warning(): void
|
||||
{
|
||||
$out = $this->render('non_root', null);
|
||||
|
||||
$this->assertStringContainsString(self::RESUME, $out);
|
||||
$this->assertStringNotContainsString('sudo -u', $out);
|
||||
$this->assertStringNotContainsString('root', $out);
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스 2: root_web_known — sudo -u {webUser} 접두사 + 계정명 명시 경고.
|
||||
*/
|
||||
public function test_root_web_known_prefixes_sudo_and_warns_with_account_name(): void
|
||||
{
|
||||
$out = $this->render('root_web_known', 'www-data');
|
||||
|
||||
$this->assertStringContainsString('sudo -u www-data '.self::RESUME, $out);
|
||||
// 계정명이 경고 문구에 노출되어야 한다
|
||||
$this->assertStringContainsString('www-data', $out);
|
||||
$this->assertStringContainsString('root', $out);
|
||||
// placeholder 는 나오면 안 된다 (계정 식별 성공 경로)
|
||||
$this->assertStringNotContainsString('<웹서버계정>', $out);
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스 3: root_web_symmetric — root 서비스 구성 → 명령만 그대로.
|
||||
*/
|
||||
public function test_root_web_symmetric_prints_command_verbatim(): void
|
||||
{
|
||||
$out = $this->render('root_web_symmetric', null);
|
||||
|
||||
$this->assertStringContainsString(self::RESUME, $out);
|
||||
$this->assertStringNotContainsString('sudo -u', $out);
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스 4: root_web_unknown — placeholder + 계정명 미상 경고.
|
||||
*/
|
||||
public function test_root_web_unknown_uses_placeholder_and_generic_warning(): void
|
||||
{
|
||||
$out = $this->render('root_web_unknown', null);
|
||||
|
||||
$this->assertStringContainsString('sudo -u <웹서버계정> '.self::RESUME, $out);
|
||||
$this->assertStringContainsString('식별하지 못했습니다', $out);
|
||||
// 대표 웹서버 계정 예시를 안내에 포함해 운영자가 직접 확인하도록 유도
|
||||
$this->assertStringContainsString('www-data', $out);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ description: |
|
||||
세분화. 범위 내 스텝 파일이 애초에 없어 0건인 경우(discovered=0)를 정상 통과로 재분류하여,
|
||||
스텝이 필요 없는 패치 릴리즈(예: 7.0.0→7.0.1) 를 실패로 오판하던 결함을 차단.
|
||||
|
||||
재실행 권한 안내 (§8, resume_exec_context 축): spawn 자식 실패로 스텝이 미실행 상태로
|
||||
남으면 운영자에게 `core:execute-upgrade-steps` 재실행을 안내한다. sudo(root) 로 실행 중이면
|
||||
안내받은 명령을 root 로 그대로 재실행할 경우 스텝 생성 파일이 root 소유가 되어 이후 웹서버
|
||||
요청이 쓰기 실패한다. 실행 사용자(root 여부) × 웹서버 계정 식별성으로 4분기 안내:
|
||||
non_root(명령만) / root_web_known(sudo -u {계정} + 경고) / root_web_symmetric(root 서비스
|
||||
구성, 명령만) / root_web_unknown(placeholder + 계정명 미상 경고). 공유 호스팅에서 웹서버·
|
||||
PHP·실행 유저가 동일하면 non_root 로 처리된다.
|
||||
|
||||
axes:
|
||||
spawn_branch: [proc_open_disabled, proc_open_resource_fail, child_abnormal_exit, child_silent_skip_no_signal, child_zero_steps_with_discovered, child_zero_steps_no_step_files]
|
||||
spawn_failure_mode: [abort, fallback]
|
||||
@@ -39,6 +47,7 @@ axes:
|
||||
upgrade_step_v_one_pattern: [app_service_call, app_manager_call, app_repository_call, local_only]
|
||||
audit_allow_inline: [present, absent] # 면제 주석 적용 여부
|
||||
symlink_target_state: [valid, broken, windows_no_privilege] # symlink 보존 분기 (§1 연동)
|
||||
resume_exec_context: [non_root, root_web_known, root_web_symmetric, root_web_unknown] # 핸드오프 재실행 권한 안내 분기 (실행 사용자 × 웹서버 계정 식별성)
|
||||
|
||||
exclusions:
|
||||
- { spawn_branch: proc_open_disabled, parent_memory_version: equal_to_to, reason: "proc_open 비활성 시점에 부모는 항상 fromVersion 메모리 — equal 케이스 시뮬 불가" }
|
||||
@@ -83,10 +92,16 @@ effects:
|
||||
- upgrade_step_vone_safety_warns_on_app_manager_call_in_upgrade_step
|
||||
- upgrade_step_vone_safety_warns_on_app_repository_call_in_upgrade_step
|
||||
- upgrade_step_vone_safety_skips_when_audit_allow_inline_present
|
||||
# §8 핸드오프 재실행 권한 안내 (sudo/root × 웹서버 계정 식별성 4분기)
|
||||
- renderResumeGuidance_non_root_prints_command_verbatim_without_permission_warning
|
||||
- renderResumeGuidance_root_web_known_prefixes_sudo_u_and_warns_with_account_name
|
||||
- renderResumeGuidance_root_web_symmetric_prints_command_verbatim
|
||||
- renderResumeGuidance_root_web_unknown_uses_placeholder_and_generic_warning
|
||||
|
||||
test_files:
|
||||
- tests/Feature/Console/CoreUpdateCommandSpawnFailureTest.php
|
||||
- tests/Feature/Console/CoreUpdateCommandHandoffTest.php
|
||||
- tests/Feature/Console/CoreUpdateResumeGuidanceTest.php
|
||||
- tests/Feature/Upgrades/MultiVersionUpgradePathTest.php
|
||||
- tests/Unit/Extension/Helpers/FilePermissionHelperSymlinkTest.php
|
||||
|
||||
|
||||
Reference in New Issue
Block a user