diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index b4979d6..d3839f5 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -9,12 +9,12 @@ jobs: dependabot: runs-on: ubuntu-latest timeout-minutes: 5 - if: ${{ github.actor == 'dependabot[bot]' }} + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'pixelated-au/streamline' steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.3.0 + uses: dependabot/fetch-metadata@v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 32a8b97..21a031c 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -15,16 +15,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.5 + uses: aglipanci/laravel-pint-action@v2 with: preset: laravel - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix styling diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c2c14b1..0f39704 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,3 +1,4 @@ +#file: noinspection SpellCheckingInspection name: run-tests on: @@ -33,7 +34,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 39de30d..c6c3e24 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: main @@ -25,7 +25,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: branch: main commit_message: Update CHANGELOG diff --git a/README.md b/README.md index 815b0f9..397cd7f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ Please review [our security policy](../../security/policy) on how to report secu - [Pixelated](https://github.com/pixelated-au) - [All Contributors](../../contributors) +## TODO +- [ ] Detect if the GitHub token has expired. If so, output an error to the user + ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/config/streamline.php b/config/streamline.php index 432c7f6..1cd516b 100644 --- a/config/streamline.php +++ b/config/streamline.php @@ -2,9 +2,8 @@ /** @noinspection PhpUnusedParameterInspection */ -use Pixelated\Streamline\Events\CommandClassCallback; -use Pixelated\Streamline\Interfaces\UpdateBuilderInterface; use Pixelated\Streamline\Pipes\BackupCurrentInstallation; +use Pixelated\Streamline\Pipes\CheckComposer; use Pixelated\Streamline\Pipes\CheckLaravelBasePathWritable; use Pixelated\Streamline\Pipes\DownloadRelease; use Pixelated\Streamline\Pipes\GetNextAvailableReleaseVersion; @@ -12,7 +11,6 @@ use Pixelated\Streamline\Pipes\RunUpdate; use Pixelated\Streamline\Pipes\UnpackRelease; use Pixelated\Streamline\Pipes\VerifyVersion; -use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; return [ @@ -315,6 +313,7 @@ 'pipeline-update' => [ CheckLaravelBasePathWritable::class, + CheckComposer::class, GetNextAvailableReleaseVersion::class, VerifyVersion::class, MakeTempDir::class, @@ -329,24 +328,12 @@ | Pipeline Cleanup |-------------------------------------------------------------------------- | - | Anything in this function will be executed after all pipeline steps have - | completed. Even if an error occurs during the pipeline execution. + | Anything in this class will be executed after all pipeline steps have + | completed, even if an error occurs during the pipeline execution. | */ - 'pipeline-finish' => static function(UpdateBuilderInterface $builder) { - $process = resolve(\Symfony\Component\Process\Process::class, [ - 'command' => [(new PhpExecutableFinder)->find(), 'artisan', 'streamline:finish-update'], - 'cwd' => $builder->getBasePath(), - ]); - $process->run(); - - if ($process->isSuccessful()) { - CommandClassCallback::dispatch('info', $process->getOutput()); - } else { - CommandClassCallback::dispatch('error', $process->getErrorOutput()); - } - }, + 'pipeline-finish-class' => \Pixelated\Streamline\Services\FinishUpdate::class, /* |-------------------------------------------------------------------------- diff --git a/src/Actions/InstantiateStreamlineUpdater.php b/src/Actions/InstantiateStreamlineUpdater.php index 818e7bf..59b27b4 100644 --- a/src/Actions/InstantiateStreamlineUpdater.php +++ b/src/Actions/InstantiateStreamlineUpdater.php @@ -5,6 +5,7 @@ namespace Pixelated\Streamline\Actions; use Closure; +use Illuminate\Container\Attributes\Config as ConfigAttribute; use Illuminate\Support\Facades\Config; use Pixelated\Streamline\Factories; use ReflectionClass; @@ -13,23 +14,17 @@ class InstantiateStreamlineUpdater { - /** @var class-string */ - private readonly string $runnerClass; - public function __construct( private readonly Factories\ProcessFactory $process, - // TODO restore this after upgrading to Laravel 11 - // #[ConfigAttribute('streamline.runner_class')] - // private readonly string $runnerClass, - ) { - // TODO restore this after upgrading to Laravel 11 - $this->runnerClass = Config::get('streamline.runner_class'); - } + #[ConfigAttribute('streamline.runner_class')] + /** @var class-string */ + private readonly string $runnerClass, + ) {} /** * @param Closure(string, string): void $callback */ - public function execute(string $versionToInstall, Closure $callback): void + public function execute(string $versionToInstall, string $composerPath, Closure $callback): void { $classFilePath = $this->getClassFilePath(); @@ -46,6 +41,7 @@ public function execute(string $versionToInstall, Closure $callback): void 'PUBLIC_DIR_NAME' => public_path(), 'FRONT_END_BUILD_DIR' => config('streamline.laravel_build_dir_name'), 'INSTALLING_VERSION' => $versionToInstall, + 'COMPOSER_PATH' => $composerPath, 'PROTECTED_PATHS' => $protectedPaths, 'DIR_PERMISSION' => (int) config('streamline.directory_permissions'), 'FILE_PERMISSION' => (int) config('streamline.file_permissions'), diff --git a/src/Commands/UpdateCommand.php b/src/Commands/UpdateCommand.php index e79877e..c31794b 100644 --- a/src/Commands/UpdateCommand.php +++ b/src/Commands/UpdateCommand.php @@ -3,11 +3,12 @@ namespace Pixelated\Streamline\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; use Pixelated\Streamline\Commands\Traits\GitHubApi; use Pixelated\Streamline\Commands\Traits\OutputSubProcessCalls; +use Pixelated\Streamline\Interfaces\UpdateBuilderInterface; use Pixelated\Streamline\Pipeline\Pipeline; -use Pixelated\Streamline\Updater\UpdateBuilder; use Throwable; class UpdateCommand extends Command @@ -17,6 +18,7 @@ class UpdateCommand extends Command protected $signature = 'streamline:run-update {--install-version= : Specify version to install} + {--composer= : Set the path to composer.phar if not installed globally} {--force : Force update. Use for overriding the current version.}'; protected $description = 'CLI update'; @@ -28,10 +30,11 @@ public function handle(): int { $this->listenForSubProcessEvents(); - $builder = (new UpdateBuilder) + $builder = App::make(UpdateBuilderInterface::class) ->setBasePath(base_path()) ->setRequestedVersion($this->option('install-version')) ->setCurrentlyInstalledVersion(Config::get('streamline.installed_version')) + ->setComposerPath($this->option('composer')) ->forceUpdate($this->option('force')); return (new Pipeline($builder)) @@ -41,7 +44,7 @@ public function handle(): int return self::FAILURE; }) - ->finally(config('streamline.pipeline-finish')) + ->finally(App::make(config('streamline.pipeline-finish-class'))) ->then(fn() => self::SUCCESS); } } diff --git a/src/Global/StreamlineUpdater.php b/src/Global/StreamlineUpdater.php index c090ecc..57065e8 100644 --- a/src/Global/StreamlineUpdater.php +++ b/src/Global/StreamlineUpdater.php @@ -16,6 +16,8 @@ class StreamlineUpdater public string $installingVersion; + public string $composerPath; + public string $oldReleaseArchivePath; public array $protectedPaths; @@ -46,6 +48,7 @@ public function __construct() $this->publicDirName = $this->env('PUBLIC_DIR_NAME'); $this->frontEndBuildDir = $this->env('FRONT_END_BUILD_DIR'); $this->installingVersion = $this->env('INSTALLING_VERSION'); + $this->composerPath = $this->env('COMPOSER_PATH'); $this->protectedPaths = $this->jsonEnv('PROTECTED_PATHS'); $this->dirPermission = (int) $this->env('DIR_PERMISSION'); $this->filePermission = (int) $this->env('FILE_PERMISSION'); @@ -109,6 +112,7 @@ public function run(): void publicDirName: $this->publicDirName, frontendBuildDir: $this->frontEndBuildDir, installingVersion: $this->installingVersion, + composerPath: $this->composerPath, protectedPaths: $this->protectedPaths, dirPermission: $this->dirPermission, filePermission: $this->filePermission, diff --git a/src/Interfaces/UpdateBuilderInterface.php b/src/Interfaces/UpdateBuilderInterface.php index e204663..9a1148f 100644 --- a/src/Interfaces/UpdateBuilderInterface.php +++ b/src/Interfaces/UpdateBuilderInterface.php @@ -8,6 +8,10 @@ public function setBasePath(string $basePath): UpdateBuilderInterface; public function getBasePath(): string; + public function setComposerPath(string $path): UpdateBuilderInterface; + + public function getComposerPath(): string; + public function setCurrentlyInstalledVersion(string $version): UpdateBuilderInterface; public function getCurrentlyInstalledVersion(): ?string; diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php index d1b88f8..30ae490 100644 --- a/src/Pipeline/Pipeline.php +++ b/src/Pipeline/Pipeline.php @@ -15,7 +15,8 @@ class Pipeline private Closure $destination; - private ?Closure $finallyHandler = null; + /** @var string|callable */ + private $finallyHandler; public function __construct(protected UpdateBuilderInterface $builder) {} @@ -40,9 +41,10 @@ public function catch(Closure $exceptionHandler): static } /** + * @param class-string | callable $finallyHandler * @return $this */ - public function finally(Closure $finallyHandler): static + public function finally(string|callable $finallyHandler): static { $this->finallyHandler = $finallyHandler; diff --git a/src/Pipes/CheckComposer.php b/src/Pipes/CheckComposer.php new file mode 100644 index 0000000..e6b5936 --- /dev/null +++ b/src/Pipes/CheckComposer.php @@ -0,0 +1,36 @@ +checkComposerPath( + $builder->getComposerPath() + ); + + $builder->setComposerPath($composerPath); + + return $builder; + } + + protected function checkComposerPath(string $composerPath): string + { + if (Process::run("$composerPath -V")->failed()) { + throw new RuntimeException( + message: 'Error: Cannot find composer. It doesn\'t appear to be installed globally. ' . + 'Please specify the path to composer using the --composer option. ' . + 'See https://getcomposer.org for more information.' + ); + } + + // return the composer full path by using 'which'. Trim off the newline character which is added by Process::run + return trim(Process::run('which composer')->output()); + } +} diff --git a/src/Pipes/RunUpdate.php b/src/Pipes/RunUpdate.php index 2f901ec..c1156be 100644 --- a/src/Pipes/RunUpdate.php +++ b/src/Pipes/RunUpdate.php @@ -12,11 +12,12 @@ { public function __construct(private InstantiateStreamlineUpdater $runUpdate) {} - public function __invoke($builder): UpdateBuilderInterface + public function __invoke(UpdateBuilderInterface $builder): UpdateBuilderInterface { $this->runUpdate->execute( - $builder->getRequestedVersion() ?? $builder->getNextAvailableRepositoryVersion(), - function(string $type, string $output) { + versionToInstall: $builder->getRequestedVersion() ?? $builder->getNextAvailableRepositoryVersion(), + composerPath: $builder->getComposerPath(), + callback: function(string $type, string $output) { if ($type === 'err') { CommandClassCallback::dispatch('error', $output); diff --git a/src/Services/FinishUpdate.php b/src/Services/FinishUpdate.php new file mode 100644 index 0000000..aae62b7 --- /dev/null +++ b/src/Services/FinishUpdate.php @@ -0,0 +1,31 @@ + [$this->phpExecutableFinder->find(), 'artisan', 'streamline:finish-update'], + 'cwd' => $this->builder->getBasePath(), + ]); + $process->run(); + + if ($process->isSuccessful()) { + CommandClassCallback::dispatch('info', $process->getOutput()); + } else { + CommandClassCallback::dispatch('error', $process->getErrorOutput()); + } + } +} diff --git a/src/StreamlineServiceProvider.php b/src/StreamlineServiceProvider.php index c21a616..2269256 100644 --- a/src/StreamlineServiceProvider.php +++ b/src/StreamlineServiceProvider.php @@ -14,8 +14,10 @@ use Pixelated\Streamline\Commands\InitInstalledVersionCommand; use Pixelated\Streamline\Commands\ListCommand; use Pixelated\Streamline\Commands\UpdateCommand; +use Pixelated\Streamline\Interfaces\UpdateBuilderInterface; use Pixelated\Streamline\Macros\ConfigCommaToArrayMacro; use Pixelated\Streamline\Testing\Mocks\UpdateRunnerFake; +use Pixelated\Streamline\Updater\UpdateBuilder; use Spatie\LaravelPackageTools\Commands\InstallCommand; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -41,6 +43,8 @@ public function configurePackage(Package $package): void public function registeringPackage(): void { + $this->app->singleton(UpdateBuilderInterface::class, UpdateBuilder::class); + $this->app->bind( CreateArchive::class, fn(Application $app) => new CreateArchive( @@ -50,7 +54,7 @@ public function registeringPackage(): void ) ); - Config::get('logging.channels.streamline', Config::get('streamline.logging')); + Config::set('logging.channels.streamline', Config::get('streamline.logging')); if ($this->app->environment('local')) { // @codeCoverageIgnoreStart diff --git a/src/Updater/RunCompleteGitHubVersionRelease.php b/src/Updater/RunCompleteGitHubVersionRelease.php index ae794e5..90cfbbd 100644 --- a/src/Updater/RunCompleteGitHubVersionRelease.php +++ b/src/Updater/RunCompleteGitHubVersionRelease.php @@ -22,6 +22,7 @@ public function __construct( private readonly string $publicDirName, private readonly string $frontendBuildDir, private readonly string $installingVersion, + private readonly string $composerPath, private readonly array $protectedPaths, private readonly int $dirPermission, private readonly int $filePermission, @@ -40,6 +41,7 @@ public function run(): void $this->copyFrontEndAssetsFromOldToNewRelease(); $this->preserveProtectedPaths(); $this->moveNewReleaseIntoDeployment(); + $this->runComposerUpdate(); $this->removeOldDeployment(); $this->setEnvVersionNumber(); $this->optimiseNewRelease(); @@ -94,6 +96,13 @@ protected function moveNewReleaseIntoDeployment(): void rename($this->tempDirName, $this->laravelBasePath); } + protected function runComposerUpdate(): void + { + $this->output("Running composer update with path: $this->composerPath"); + + $this->runCommand("$this->composerPath install"); + } + protected function removeOldDeployment(): void { $this->output("Deleting of $this->laravelTempBackupDir as it's no longer needed"); diff --git a/src/Updater/UpdateBuilder.php b/src/Updater/UpdateBuilder.php index 0cab847..b2cadc1 100644 --- a/src/Updater/UpdateBuilder.php +++ b/src/Updater/UpdateBuilder.php @@ -10,6 +10,8 @@ class UpdateBuilder implements UpdateBuilderInterface private string $currentlyInstalledVersion; + private ?string $composerPath = null; + private ?string $requestedVersion = null; private ?string $versionToInstall = null; @@ -49,6 +51,21 @@ public function getCurrentlyInstalledVersion(): ?string return $this->currentlyInstalledVersion; } + public function setComposerPath(?string $path): UpdateBuilderInterface + { + if (!$path) { + $path = 'composer'; + } + $this->composerPath = $path; + + return $this; + } + + public function getComposerPath(): string + { + return $this->composerPath; + } + public function setRequestedVersion(?string $version): UpdateBuilderInterface { $this->requestedVersion = $version; diff --git a/tests/Feature/Commands/UpdateCommandTest.php b/tests/Feature/Commands/UpdateCommandTest.php index 214b57d..e739b8b 100644 --- a/tests/Feature/Commands/UpdateCommandTest.php +++ b/tests/Feature/Commands/UpdateCommandTest.php @@ -7,10 +7,11 @@ use Mockery\MockInterface; use Pixelated\Streamline\Events\CommandClassCallback; use Pixelated\Streamline\Factories\ProcessFactory; +use Pixelated\Streamline\Tests\Feature\Traits\CheckComposerPath; use Pixelated\Streamline\Tests\Feature\Traits\HttpMock; use Pixelated\Streamline\Tests\Feature\Traits\UpdateCommandCommon; -pest()->uses(UpdateCommandCommon::class, HttpMock::class); +pest()->uses(UpdateCommandCommon::class, HttpMock::class, CheckComposerPath::class); beforeEach(function() { $this->app->bind( @@ -57,6 +58,7 @@ function(MockInterface $mock) use ($process) { ->mockZipArchive(); $this->mockGetWebArchive(); + $this->mockComposerPath('composer'); Config::set('streamline.laravel_build_dir_name', '/path/to/build'); @@ -105,8 +107,9 @@ function() { File::shouldReceive('deleteDirectory'); Config::set('streamline.installed_version', 'v2.8.1'); $this->mockGetWebArchive(); + $this->mockComposerPath('/dev/null'); - $this->artisan('streamline:run-update --install-version=v2.9.0') + $this->artisan('streamline:run-update --install-version=v2.9.0 --composer=/dev/null') ->expectsOutputToContain('Changing deployment to version: v2.9.0') ->assertExitCode(0); }); @@ -114,9 +117,11 @@ function() { it('should run an update but notifies there are no new versions', function() { $this->mockCache(['v2.0.0', 'v1.0.0'], 'v2.0.0'); $this->mockGetWebArchive(); + $this->mockComposerPath('/dev/null'); + Config::set('streamline.installed_version', 'v2.0.0'); - $this->artisan('streamline:run-update') + $this->artisan('streamline:run-update --composer=/dev/null') ->expectsOutputToContain('You are currently using the latest version (v2.0.0)') ->assertExitCode(0); }); @@ -125,8 +130,9 @@ function() { $this->mockCache([]); $this->mockGetWebArchive() ->mockHttpReleases(Http::response([])); + $this->mockComposerPath('/dev/null'); - $this->artisan('streamline:run-update') + $this->artisan('streamline:run-update --composer=/dev/null') ->expectsOutputToContain('The next available version could not be determined.') ->assertExitCode(1); }); @@ -135,8 +141,9 @@ function() { $this->mockCache(['v2.0.0', 'v1.0.0'], 'v2.0.0'); Config::set('streamline.installed_version', 'v1.0.0'); Http::fake(['github.com/*' => Http::failedConnection()]); + $this->mockComposerPath('/dev/null'); - $this->artisan('streamline:run-update') + $this->artisan('streamline:run-update --composer=/dev/null') ->expectsOutputToContain('Failed to connect to GitHub API') ->assertExitCode(1); }); @@ -147,10 +154,11 @@ function() { $this->mockCache(['v2.0.0', 'v1.0.0']) ->mockGetAvailableVersions(); $this->mockHttpReleases(); + $this->mockComposerPath('/dev/null'); Config::set('streamline.installed_version', 'v2.0.0'); - $this->artisan('streamline:run-update --install-version=v1.0.0') + $this->artisan('streamline:run-update --install-version=v1.0.0 --composer=/dev/null') ->expectsOutputToContain('Version v1.0.0 is not greater than the current version (v2.0.0)') ->assertExitCode(1); } @@ -159,8 +167,9 @@ function() { it('should run an update requesting a pre-release version and fail', function(string $version) { $this->mockCache(availableVersions: [$version, 'v1.0.0']); $this->mockHttpReleases(); + $this->mockComposerPath('/dev/null'); - $this->artisan("streamline:run-update --install-version=$version") + $this->artisan("streamline:run-update --install-version=$version --composer=/dev/null") ->expectsOutputToContain("Version $version is a pre-release version, use --force to install it.") ->assertExitCode(1); })->with(['v1.0.1-alpha', 'v1.0.1-beta', 'v1.0.1a', 'v1.0.1b']); @@ -172,6 +181,7 @@ function() { ->mockCache(availableVersions: [$version, 'v1.0.0']) ->mockGetAvailableVersions() ->mockZipArchive(); + $this->mockComposerPath('/dev/null'); File::shouldReceive('deleteDirectory'); @@ -193,24 +203,19 @@ function() { return $mock; } ); - // Process::fake([ - // '* artisan streamline:finish-update' => Process::result('test output'), - // ]); - // $this->withoutMockingConsoleOutput(); - // $this->artisan("streamline:run-update --install-version=$version --force"); - $this->artisan("streamline:run-update --install-version=$version --force") + $this->artisan("streamline:run-update --install-version=$version --composer=/dev/null --force") ->expectsOutputToContain("Version: $version will be installed") ->expectsOutputToContain("Downloading archive for version $version") ->assertExitCode(0); - // Process::assertRan(PHP_BINARY . ' artisan streamline:finish-update'); })->with(['v1.0.1-alpha', 'v1.0.1-beta', 'v1.0.1a', 'v1.0.1b']); it('should run an update requesting an invalid version and return an error', function() { $this->mockCache(); $this->mockHttpReleases(); + $this->mockComposerPath('/dev/null'); - $this->artisan('streamline:run-update --install-version=hello') + $this->artisan('streamline:run-update --install-version=hello --composer=/dev/null') ->expectsOutputToContain('Version hello is not a valid version!') ->assertExitCode(1); }); @@ -218,8 +223,9 @@ function() { it('should run a "forced" update requesting an invalid version and return an error', function() { $this->mockCache(); $this->mockHttpReleases(); + $this->mockComposerPath('/dev/null'); - $this->artisan('streamline:run-update --install-version=hello --force') + $this->artisan('streamline:run-update --install-version=hello --composer=/dev/null --force') ->expectsOutputToContain('Version hello is not a valid version!') ->assertExitCode(1); }); @@ -229,11 +235,13 @@ function() { ->mockCache(['v2.0.0', 'v1.0.0']) ->mockGetAvailableVersions() ->mockZipArchive(); + $this->mockComposerPath('/dev/null'); + File::shouldReceive('deleteDirectory'); Config::set('streamline.installed_version', 'v2.0.0'); $this->mockGetWebArchive(); - $this->artisan('streamline:run-update --force --install-version=v1.0.0') + $this->artisan('streamline:run-update --force --install-version=v1.0.0 --composer=/dev/null') ->expectsOutputToContain('Version v1.0.0 is not greater than the current version (v2.0.0) (Forced update)') ->assertExitCode(0); }); @@ -243,12 +251,38 @@ function() { ->mockCache(['v2.0.0', 'v1.0.0'], 'v2.0.0') ->mockGetAvailableVersions() ->mockZipArchive(); + $this->mockComposerPath('/dev/null'); File::shouldReceive('deleteDirectory'); Config::set('streamline.installed_version', 'v2.0.0'); $this->mockGetWebArchive(); - $this->artisan('streamline:run-update --force') + $this->artisan('streamline:run-update --composer=/dev/null --force') ->expectsOutputToContain('You are currently using the latest version (v2.0.0) (Forced update)') ->assertExitCode(0); }); + +it('can run with a globally installed composer', function() { + $this->mockFile() + ->mockCache(['v2.0.0', 'v1.0.0']) + ->mockGetAvailableVersions() + ->mockZipArchive(); + $this->mockComposerPath('composer'); + + File::shouldReceive('deleteDirectory'); + Config::set('streamline.installed_version', 'v2.0.0'); + + $this->mockGetWebArchive(); + $this->artisan('streamline:run-update') + ->assertExitCode(0); +}); + +it('throws a runtime exception when the composer path is incorrect', function() { + Process::fake([ + '/dev/null -V' => Process::result(exitCode: 1), + ]); + + $this->artisan('streamline:run-update --composer=/dev/null') + ->expectsOutputToContain('Error: Cannot find composer') + ->assertExitCode(1); +}); diff --git a/tests/Feature/FinishUpdateCommandTest.php b/tests/Feature/FinishUpdateCommandTest.php index 597ddad..8f56e3f 100644 --- a/tests/Feature/FinishUpdateCommandTest.php +++ b/tests/Feature/FinishUpdateCommandTest.php @@ -4,6 +4,9 @@ use Pixelated\Streamline\Actions\UncachedEnvironment; use Pixelated\Streamline\Enums\CacheKeysEnum; use Pixelated\Streamline\Events\InstalledVersionSet; +use Pixelated\Streamline\Tests\Feature\Traits\CheckComposerPath; + +pest()->uses(CheckComposerPath::class); it('sets the installed version and dispatches an event', function() { Event::fake(); diff --git a/tests/Feature/InstallPackageTest.php b/tests/Feature/InstallPackageTest.php deleted file mode 100644 index 1eb11a1..0000000 --- a/tests/Feature/InstallPackageTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertTrue(File::missing(config_path('streamline.php'))); - $this->assertTrue(File::missing(app_path('.streamline'))); - - $this->withoutMockingConsoleOutput() - ->artisan('streamline:install'); // Spatie Package Tools function - - $this->assertTrue(File::exists(config_path('streamline.php'))); -}) - ->after(function() { - File::delete(config_path('streamline.php')); - }); diff --git a/tests/Feature/Traits/CheckComposerPath.php b/tests/Feature/Traits/CheckComposerPath.php new file mode 100644 index 0000000..364e145 --- /dev/null +++ b/tests/Feature/Traits/CheckComposerPath.php @@ -0,0 +1,18 @@ + Process::result('Composer version 2.0.0'), + 'which composer' => Process::result($composerPath), + ]); + + return $this; + } +} diff --git a/tests/Unit/Actions/InstantiateStreamlineUpdaterTest.php b/tests/Unit/Actions/InstantiateStreamlineUpdaterTest.php index 230d207..0187af9 100644 --- a/tests/Unit/Actions/InstantiateStreamlineUpdaterTest.php +++ b/tests/Unit/Actions/InstantiateStreamlineUpdaterTest.php @@ -15,12 +15,12 @@ $this->expectExceptionMessage("Error instantiating updater class '$filename': Class \"$filename\" does not exist"); // TODO remove this after upgrading to Laravel 11 - Config::set('streamline.runner_class', $filename); + // Config::set('streamline.runner_class', $filename); // TODO restore this after upgrading to Laravel 11 - // (new InstantiateStreamlineUpdater($process, $nonExistentClassName)) - (new InstantiateStreamlineUpdater($process)) - ->execute('1.0.0', fn() => null); + (new InstantiateStreamlineUpdater($process, $filename)) +// (new InstantiateStreamlineUpdater($process)) + ->execute('1.0.0', '', fn() => null); }); it('should return the file path when a valid class is passed in', function() { @@ -33,8 +33,7 @@ $process = Mockery::mock(ProcessFactory::class); /** @noinspection PhpUndefinedClassInspection */ - Config::set('streamline.runner_class', ValidTestClass::class); - $updater = new InstantiateStreamlineUpdater($process); + $updater = new InstantiateStreamlineUpdater($process, ValidTestClass::class); $pathInvokable = Closure::bind(fn() => $this->getClassFilePath(), $updater, $updater); $this->assertSame(Storage::path($filename), $pathInvokable()); @@ -44,8 +43,7 @@ $process = Mockery::mock(ProcessFactory::class); /** @noinspection PhpUndefinedClassInspection */ - Config::set('streamline.runner_class', ValidTestClass::class); - $updater = new InstantiateStreamlineUpdater($process); + $updater = new InstantiateStreamlineUpdater($process, ValidTestClass::class); $arrayValue = Closure::bind(fn() => $this->parseArray(['one', 'two']), $updater, $updater); $this->assertSame('["one","two"]', $arrayValue()); @@ -61,7 +59,6 @@ $runnerClass = 'TestRunnerClass'; $classPath = "/path/to/$runnerClass.php"; - Config::set('streamline.runner_class', $runnerClass); Config::set('streamline.laravel_build_dir_name', 'build_dir_value'); Config::set('streamline.work_temp_dir', 'temp_dir_value'); Config::set('streamline.backup_dir', 'backup_dir_value'); @@ -78,6 +75,7 @@ 'PUBLIC_DIR_NAME' => public_path(), 'FRONT_END_BUILD_DIR' => config('streamline.laravel_build_dir_name'), 'INSTALLING_VERSION' => $versionToInstall, + 'COMPOSER_PATH' => '/dev/null', 'PROTECTED_PATHS' => '["path1","path2"]', 'DIR_PERMISSION' => 0755, 'FILE_PERMISSION' => 0644, @@ -88,9 +86,9 @@ $process = mockProcess($expectedEnv, $callback); $processFactory = mockProcessFactory($classPath, $runnerClass, $process); - $updater = mockUpdaterClass($processFactory, $classPath); + $updater = mockUpdaterClass($processFactory, $classPath, $runnerClass); - $updater->execute($versionToInstall, $callback); + $updater->execute($versionToInstall, '/dev/null', $callback); }); function mockProcessFactory(string $classPath, string $runnerClass, $process): ProcessFactory @@ -131,9 +129,10 @@ function mockProcess(array $expectedEnv, Closure $callback): Process function mockUpdaterClass( ProcessFactory $processFactory, - string $classPath + string $classPath, + string $runnerClass ): InstantiateStreamlineUpdater|LegacyMockInterface { - $updater = Mockery::mock(InstantiateStreamlineUpdater::class, [$processFactory]) + $updater = Mockery::mock(InstantiateStreamlineUpdater::class, [$processFactory, $runnerClass]) ->makePartial() ->shouldAllowMockingProtectedMethods(); diff --git a/tests/Unit/PipelineTest.php b/tests/Unit/PipelineTest.php index 71f9dd1..b96c92c 100644 --- a/tests/Unit/PipelineTest.php +++ b/tests/Unit/PipelineTest.php @@ -6,9 +6,11 @@ it('can process function pipes', closure: function() { $this->expectOutputString('Test pipe'); $result = (new Pipeline(new UpdateBuilder)) - ->through([function() { - echo 'Test pipe'; - }]) + ->through([ + function() { + echo 'Test pipe'; + }, + ]) ->then(fn() => true); $this->assertTrue($result); @@ -17,9 +19,11 @@ it('can handle pipe exceptions properly', closure: function() { $this->expectOutputString('Caught exception: Test exception'); $result = (new Pipeline(new UpdateBuilder)) - ->through([function() { - throw new RuntimeException('Test exception'); - }]) + ->through([ + function() { + throw new RuntimeException('Test exception'); + }, + ]) ->catch(function(Throwable $e) { echo 'Caught exception: ' . $e->getMessage(); @@ -35,9 +39,11 @@ $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Test throwing exception'); (new Pipeline(new UpdateBuilder)) - ->through([function() { - throw new RuntimeException('Test throwing exception'); - }]) + ->through([ + function() { + throw new RuntimeException('Test throwing exception'); + }, + ]) ->then(function() { return true; }); @@ -71,7 +77,18 @@ it('will throw an exception but still run the finally function', closure: function() { $this->expectOutputString('Caught exception: Test finally exception'); - $finallyRun = false; + $x = new class(false) + { + public function __construct(public bool $finallyWasCalled) {} + + public function __invoke(): void + { + // This function should be called. First confirm that $finallyRun is false. Then set $finallyRun to true + // so that it can be confirmed that the finally function was called + $this->finallyWasCalled = true; + } + }; + $this->assertFalse($x->finallyWasCalled); $result = (new Pipeline(new UpdateBuilder)) ->through([fn() => throw new RuntimeException('Test finally exception')]) @@ -80,17 +97,12 @@ return false; }) - ->finally(function() use (&$finallyRun) { - // This function should be called. First confirm that $finallyRun is false. Then set $finallyRun to true - // so that it can be confirmed that the finally function was called - $this->assertFalse($finallyRun); - $finallyRun = true; - }) + ->finally($x) ->then(function() { $this->assertTrue(false, 'This should not be reached, therefore it should fail if called'); return true; }); - $this->asserttrue($finallyRun); + $this->assertTrue($x->finallyWasCalled); $this->assertFalse($result); }); diff --git a/tests/Unit/Pipes/CheckComposerTest.php b/tests/Unit/Pipes/CheckComposerTest.php new file mode 100644 index 0000000..42368d9 --- /dev/null +++ b/tests/Unit/Pipes/CheckComposerTest.php @@ -0,0 +1,20 @@ +uses(CheckComposerPath::class); + +it('confirm the composer path is valid', function() { + $builder = Mockery::mock(UpdateBuilderInterface::class); + $builder->shouldReceive('getComposerPath')->andReturn('composer'); + $builder->shouldReceive('setComposerPath')->with('/usr/bin/composer'); + + $this->mockComposerPath('/usr/bin/composer'); + + $checkComposer = new CheckComposer; + $result = $checkComposer($builder); + + expect($result)->toBe($builder); +}); diff --git a/tests/Unit/Pipes/RunUpdateTest.php b/tests/Unit/Pipes/RunUpdateTest.php index 0d3a661..e34af20 100644 --- a/tests/Unit/Pipes/RunUpdateTest.php +++ b/tests/Unit/Pipes/RunUpdateTest.php @@ -9,12 +9,13 @@ $builder = $this->mock(UpdateBuilderInterface::class); $builder->expects('getRequestedVersion')->andReturnNull(); $builder->expects('getNextAvailableRepositoryVersion')->andReturn('1.0.0'); + $builder->expects('getComposerPath')->andReturn('test/path'); $this->mock(InstantiateStreamlineUpdater::class) ->shouldReceive('execute') ->once() - ->withArgs(['1.0.0', Mockery::type('Closure')]) - ->andReturnUsing(function($version, $callback) { + ->withArgs(['1.0.0', Mockery::type('string'), Mockery::type('Closure')]) + ->andReturnUsing(function($version, $composerPath, $callback) { $callback('out', 'Update successful'); }); @@ -32,12 +33,13 @@ $builder = $this->mock(UpdateBuilderInterface::class); $builder->expects('getRequestedVersion')->andReturnNull(); $builder->expects('getNextAvailableRepositoryVersion')->andReturn('1.0.0'); + $builder->expects('getComposerPath')->andReturn('test/path'); $this->mock(InstantiateStreamlineUpdater::class) ->shouldReceive('execute') ->once() - ->withArgs(['1.0.0', Mockery::type('Closure')]) - ->andReturnUsing(function($version, $callback) { + ->withArgs(['1.0.0', Mockery::type('string'), Mockery::type('Closure')]) + ->andReturnUsing(function($version, $composerPath, $callback) { $callback('err', 'Update failed'); }); @@ -59,12 +61,13 @@ $builder = $this->mock(UpdateBuilderInterface::class); $builder->expects('getRequestedVersion')->andReturnNull(); $builder->expects('getNextAvailableRepositoryVersion')->andReturn('1.0.0'); + $builder->expects('getComposerPath')->andReturn('test/path'); $this->mock(InstantiateStreamlineUpdater::class) ->shouldReceive('execute') ->once() - ->withArgs(['1.0.0', Mockery::type('Closure')]) - ->andReturnUsing(function($version, $callback) { + ->withArgs(['1.0.0', 'test/path', Mockery::type('Closure')]) + ->andReturnUsing(function($version, $composerPath, $callback) { $callback('err', 'Critical error occurred'); }); diff --git a/tests/Unit/StreamlineConfigFileTest.php b/tests/Unit/StreamlineConfigFileTest.php index 1deb98b..48c0b57 100644 --- a/tests/Unit/StreamlineConfigFileTest.php +++ b/tests/Unit/StreamlineConfigFileTest.php @@ -1,7 +1,8 @@ setBasePath(base_path()); - $callback = Config::get('streamline.pipeline-finish'); + $builder = App::make(UpdateBuilderInterface::class) + ->setBasePath(base_path()); + $callback = App::make(Config::get('streamline.pipeline-finish-class')); $callback($builder); Event::assertDispatchedTimes(CommandClassCallback::class); @@ -67,8 +69,9 @@ function() { ); Event::fake(CommandClassCallback::class); - $builder = (new UpdateBuilder)->setBasePath(base_path()); - $callback = Config::get('streamline.pipeline-finish'); + $builder = App::make(UpdateBuilderInterface::class) + ->setBasePath(base_path()); + $callback = App::make(Config::get('streamline.pipeline-finish-class')); $callback($builder); Event::assertDispatchedTimes(CommandClassCallback::class); diff --git a/tests/Updater/Feature/RunUpdateTest.php b/tests/Updater/Feature/RunUpdateTest.php index ef23aa8..e218377 100644 --- a/tests/Updater/Feature/RunUpdateTest.php +++ b/tests/Updater/Feature/RunUpdateTest.php @@ -49,6 +49,7 @@ function() { publicDirName: $disk->path('laravel/public'), frontendBuildDir: 'build', installingVersion: '1.0.0', + composerPath: '/dev/null', protectedPaths: ['.env', 'storage/logs/streamline.log', 'missing-file.txt'], dirPermission: 0755, filePermission: 0644, @@ -72,6 +73,8 @@ function() { 'Protected paths preserved successfully.', 'Moving ' . $disk->path('laravel') . ' to ' . $disk->path('laravel_old'), 'Moving ' . $tempDisk->path('unpacked') . ' to ' . $disk->path('laravel'), + 'Running composer update with path: /dev/null', + 'Executing: /dev/null install', 'Deleting of ' . $disk->path('laravel_old') . " as it's no longer needed", 'Setting version number in .env file to: 1.0.0', 'Version number updated successfully in .env file', diff --git a/tests/Updater/Unit/RunUpdateTest.php b/tests/Updater/Unit/RunUpdateTest.php index 19cff67..25cb282 100644 --- a/tests/Updater/Unit/RunUpdateTest.php +++ b/tests/Updater/Unit/RunUpdateTest.php @@ -580,6 +580,7 @@ function runUpdateClassFactory(array $options = []): RunCompleteGitHubVersionRel 'publicDirName' => 'public', 'frontendBuildDir' => 'build', 'installingVersion' => '1.0.0', + 'composerPath' => '/dev/null', 'protectedPaths' => ['.env'], 'dirPermission' => 0755, 'filePermission' => 0644, @@ -597,6 +598,7 @@ function runUpdateClassFactory(array $options = []): RunCompleteGitHubVersionRel publicDirName: $options['publicDirName'], frontendBuildDir: $options['frontendBuildDir'], installingVersion: $options['installingVersion'], + composerPath: $options['composerPath'], protectedPaths: $options['protectedPaths'], dirPermission: $options['dirPermission'], filePermission: $options['filePermission'], diff --git a/tests/Updater/Unit/StreamlineUpdaterTest.php b/tests/Updater/Unit/StreamlineUpdaterTest.php index 1f701d3..63b61cf 100644 --- a/tests/Updater/Unit/StreamlineUpdaterTest.php +++ b/tests/Updater/Unit/StreamlineUpdaterTest.php @@ -44,7 +44,8 @@ $project->addChild($composerFile); setEnv([ - 'LARAVEL_BASE_PATH' => $project->url(), 'IS_TESTING' => StreamlineUpdater::TESTING_ON_AND_SKIP_REQUIRE_AUTOLOAD, + 'LARAVEL_BASE_PATH' => $project->url(), + 'IS_TESTING' => StreamlineUpdater::TESTING_ON_AND_SKIP_REQUIRE_AUTOLOAD, ]); $updater = new StreamlineUpdater; @@ -68,7 +69,8 @@ $this->expectExceptionMessage('The file ' . $root->url() . '/composer.json file contains invalid JSON'); setEnv([ - 'LARAVEL_BASE_PATH' => $root->url(), 'IS_TESTING' => StreamlineUpdater::TESTING_ON_AND_SKIP_REQUIRE_AUTOLOAD, + 'LARAVEL_BASE_PATH' => $root->url(), + 'IS_TESTING' => StreamlineUpdater::TESTING_ON_AND_SKIP_REQUIRE_AUTOLOAD, ]); (new StreamlineUpdater)->autoloadFile(); }); @@ -94,6 +96,7 @@ * PUBLIC_DIR_NAME?: string, * FRONT_END_BUILD_DIR?: string, * INSTALLING_VERSION?: string, + * COMPOSER_PATH?: string, * PROTECTED_PATHS?: array, * DIR_PERMISSION?: string, * FILE_PERMISSION?: string, @@ -122,6 +125,7 @@ function getEnvVars(): array 'PUBLIC_DIR_NAME' => 'public', 'FRONT_END_BUILD_DIR' => 'build', 'INSTALLING_VERSION' => '1.0.0', + 'COMPOSER_PATH' => '/dev/null', 'PROTECTED_PATHS' => '["protected.txt"]', 'DIR_PERMISSION' => '0755', 'FILE_PERMISSION' => '0644',