diff --git a/.gitignore b/.gitignore index 7a9fd6fc7..f6113e78e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ # Composer /vendor -/vendor-dev -/vendor-def auth.json composer.lock diff --git a/packages/devlink/README.md b/packages/devlink/README.md index 016e5a4b1..1da8132c3 100644 --- a/packages/devlink/README.md +++ b/packages/devlink/README.md @@ -7,79 +7,60 @@ It is used to link the packages from the `moox` monorepo into a project. It runs ## Installation ```bash -cp composer.json.example composer.json cp .env.example .env composer require moox/devlink php artisan vendor:publish --tag="devlink-config" ``` -## Usage - -```bash -php artisan moox:devlink -``` - ## Screenshot ![Moox Devlink](./devlink.jpg) -## Preparation +## How It Works -Before you can use this package, you need to prepare your project's `.gitignore` file. +1. Prepare your project's `.gitignore` file: ```bash + # Ignore all files in packages/ (including symlinks) packages/* # Allow tracking of real directories inside packages/ !packages/**/ # Ensure empty directories can be committed !packages/*/.gitkeep -# Ignore all files in packages-linked/ (for Windows) -packages-linked/* -``` - -## Configuration - -The configuration is done in the `config/devlink.php` file. - -```php - - 'packages_path' => 'packages', - - 'base_paths' => [ - base_path('../moox/packages'), - ], - - 'packages' => [ - 'moox/tag', - ], +# for windows +/packageslocal/* ``` -## Command - -The devlink command will create a `packages` directory in the root of the project and symlink the packages from the configured base paths. +2. Configure your paths and packages in the `config/devlink.php` file and the `.env` file, if needed (Windows users for example). -```bash +3. When running `devlink:link`: - php artisan moox:devlink + - Creates backup of original composer.json → composer.json.original + - Creates symlinks for all configured packages + - Updates composer.json with development configuration + - Creates composer.json-deploy for production use + - Asks to run `composer install` + - Asks to run `php artisan optimize:clear` + - Asks to run `php artisan queue:restart` -``` +4. When running `devlink:deploy`: -It will also update the `composer.json` file to include the packages in the `require` section and the `repositories` section. + - Removes all symlinks + - Deletes the packages folder, if empty + - Restores production-ready composer.json from composer.json-deploy -Finally, it will run `composer update`. +5. CI Safety Net - `deploy.sh`: + - If composer.json-deploy exists in the repository: + - The script will restore it as composer.json + - Commit and push the change in GH action + - This ensures no development configuration reaches production -### Changing branches +## Changing branches If you need to change the branches for ANY of the involved repositories, you just need to run the command again, it will automatically update the symlinks for the current branch. -```bash - - php artisan moox:devlink - -``` - > ⚠️ **Important** > If you forget to run the command, when CHANGING BRANCHES ON ANY OF THE REPOS, you will surely run into a 500 error, that drives you nuts. @@ -101,6 +82,17 @@ On Windows there are most probably some issues with the symlinks. If you run int Devlink will then link the packages into the `packages-linked` folder. +## Roadmap + +- [ ] Test on Mac +- [ ] Test on Windows +- [ ] Test Deployment on Mac +- [ ] Test Deployment on Windows +- [ ] Implement automatic Deployment +- [ ] Implement all 3 types of packages +- [ ] If package is a symlink itself, ...? +- [ ] If package is in multiple base paths...? + ## Security Vulnerabilities Please review [our security policy](https://github.com/mooxphp/moox/security/policy) on how to report security vulnerabilities. diff --git a/packages/devlink/config/devlink.php b/packages/devlink/config/devlink.php index 4b369cae8..e165d1759 100644 --- a/packages/devlink/config/devlink.php +++ b/packages/devlink/config/devlink.php @@ -1,23 +1,38 @@ env('DEVLINK_PACKAGES_PATH', 'packages'), + + // The base paths to the packages, project or branch-wise 'base_paths' => [ base_path('../moox/packages'), ], + // The internal packages need to be copied on deploy, project or branch-wise, + 'copy_packages' => [ + // 'builder-pro', + ], + + // The packages that need to be removed on deploy,project or branch-wise + 'skip_packages' => [ + 'devlink', + ], + + // The packages that are installed from Packagist on deploy, project or branch-wise 'packages' => [ - 'audit', + // 'audit', // 'backup-server-ui', - 'builder', - // 'builder-pro', + // 'builder', // 'category', // 'core', // 'connect', // 'data', // 'devops', // 'expiry', - 'flags', - 'jobs', + // 'flags', + // 'jobs', // 'login-link', // 'localization', // 'media', @@ -28,7 +43,7 @@ // 'press', // 'security', // 'sync', - // 'tag', + 'tag', // 'trainings', // 'user', // 'user-device', diff --git a/packages/devlink/deploy.sh b/packages/devlink/deploy.sh new file mode 100644 index 000000000..16a9ca5f9 --- /dev/null +++ b/packages/devlink/deploy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [ -f "composer.json-deploy" ]; then + # Remove all symlinks from /packages + find packages -type l -delete + # Remove the packages folder if empty + if [ -z "$(ls -A packages)" ]; then + rm packages + fi + cp composer.json-deploy composer.json + composer install +fi diff --git a/packages/devlink/src/Commands/DeployPackages.php b/packages/devlink/src/Commands/DeployPackages.php new file mode 100644 index 000000000..36780022a --- /dev/null +++ b/packages/devlink/src/Commands/DeployPackages.php @@ -0,0 +1,185 @@ +basePaths = config('devlink.base_paths', []); + $this->packages = config('devlink.packages', []); + + if (empty($this->basePaths)) { + $this->warn('No base paths configured in config/devlink.php'); + } + + if (empty($this->packages)) { + $this->warn('No packages configured in config/devlink.php'); + } + + $this->composerJsonPath = base_path('composer.json'); + $this->packagesPath = config('devlink.packages_path', base_path('packages')); + } + + public function handle(): void + { + $this->art(); + $this->hello(); + $this->checks(); + $this->removeAllSymlinks(); + $this->removePackagesDirectoryIfEmpty(); + $this->restoreComposerJson(); + $this->runComposerUpdate(); + $this->optimizeClear(); + $this->queueRestart(); + $this->goodbye(); + } + + public function art(): void + { + $this->info(' + + ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ + ▓▓▒░░▒▓▓▒▒░░░░░░▒▒▓▓▓▒░░░░░░░▒▓▓ ▓▓▓▓▒░░░░░░░▒▓▓▓▓ ▓▓▓▓▓▒░░░░░░░▒▒▓▓▓▓▓▒▒▒▒▓▓ ▓▓▓▒▒▒▒▓▓ + ▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓░░░░░▒▓▓ ▓▓▒░░░░░▓▓ + ▓▒░░░░░░▒▓▓▓▓▒░░░░░░░▒▓▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓▓▓▒░░░░░░░▓▓▓▓░░░░░░▒▓▓▓▓▓░░░░░░▒▓▓░░░░░▒▓▓▓▓▓░░░░░▒▓▓ + ▓▒░░░░▓▓▓▓ ▓▓░░░░░▓▓▓ ▓▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▓░░░░░▓░░░░░░▓▓▓▓ ▓▓▓▒░░░░▓▓▓▒░░░░░▓▓▓░░░░░▓▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▓░░▒░░░░░▓▓▓ ▓▓░░░░▒▓▓▓▓░░░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓ ▓▓▓░░░░░▒▓▓ ▓▓▒░░░░▓ ▓▓▓░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▒░░░░░▒░░▒▓▓ ▓▓░░░░▒▓▓▓▒░░░░░▒░░░░░▒▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▒░░░░░▒▒░░░░░▒▓▓▓ ▓▓▓░░░░░▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓░░░░░░▒▒▓▓▒░░░░░░▒▓▓▓▓░░░░░░░▒▒▓▓▒░░░░░░▓▓▓░░░░░▒▓▓▓▓▓▒░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▒░░░░░▓▓▓ ▓▓▒░░░░░▒▓ + ▓▓░░░▒▓▓ ▓▓▒░░░▒▓▓ ▓▓░░░░▓▓ ▓▓▓▓▒░░░░░░▒▒▓▓▓▓ ▓▓▓▓▓▒▒░░░░░▒▒▓▓▓▓▓░░░░▒▓▓ ▓▓▓░░░░▒▓ + ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ + + '); + } + + private function hello(): void + { + $this->info('Hello, I will prepare your project for deployment.'); + } + + private function checks(): void + { + $this->line("\nConfiguration:"); + $this->line('Base paths:'); + if (empty($this->basePaths)) { + $this->warn('- No base paths configured'); + } else { + foreach ($this->basePaths as $path) { + $resolvedPath = $this->resolvePath($path); + $this->line("- $path"); + $this->line(" → $resolvedPath".(is_dir($resolvedPath) ? ' (exists)' : ' (not found)')); + } + } + + $this->line("\nConfigured packages:"); + if (empty($this->packages)) { + $this->warn('No packages configured in config/devlink.php'); + } else { + foreach ($this->packages as $package) { + $this->line("- $package"); + } + } + + $this->line(''); + } + + private function removeAllSymlinks(): void + { + if (is_dir($this->packagesPath)) { + foreach (scandir($this->packagesPath) as $item) { + if ($item !== '.' && $item !== '..' && is_link("$this->packagesPath/$item")) { + unlink("$this->packagesPath/$item"); + } + } + } + } + + private function removePackagesDirectoryIfEmpty(): void + { + if (is_dir($this->packagesPath) && count(scandir($this->packagesPath)) === 2) { + rmdir($this->packagesPath); + } + } + + private function restoreComposerJson(): void + { + $deployFile = $this->composerJsonPath.'-deploy'; + if (! file_exists($deployFile)) { + $this->error('composer.json-deploy not found!'); + + return; + } + + unlink($this->composerJsonPath); + rename($deployFile, $this->composerJsonPath); + $this->info('Restored composer.json from composer.json-deploy'); + } + + private function runComposerUpdate(): void + { + if ($this->confirm('Run composer update now?', true)) { + $output = []; + $returnVar = 0; + exec('composer update 2>&1', $output, $returnVar); + + if ($returnVar !== 0) { + $this->error('Composer update failed: '.implode("\n", $output)); + + return; + } + + $this->info('Composer update completed successfully'); + } else { + $this->info("Please run 'composer update' manually"); + } + } + + private function optimizeClear(): void + { + if ($this->confirm('Run artisan optimize:clear now?', true)) { + $this->info('Clearing cache...'); + $this->call('optimize:clear'); + $this->info('Cache cleared successfully'); + } else { + $this->info("Please run 'artisan optimize:clear' manually"); + } + } + + private function queueRestart(): void + { + if ($this->confirm('Run queue:restart now?', false)) { + $this->info('Restarting queue...'); + $this->call('queue:restart'); + } + } + + private function goodbye(): void + { + $this->info('Have a nice dev!'); + } + + private function resolvePath(string $path): string + { + return str_starts_with($path, '~/') ? str_replace('~', getenv('HOME'), $path) : rtrim(realpath($path) ?: $path, '/'); + } +} diff --git a/packages/devlink/src/Commands/LinkPackages.php b/packages/devlink/src/Commands/LinkPackages.php index 29a08592e..0e62bb051 100644 --- a/packages/devlink/src/Commands/LinkPackages.php +++ b/packages/devlink/src/Commands/LinkPackages.php @@ -215,6 +215,7 @@ private function updateComposerJson(): void return; } + // Read original composer.json $composerContent = file_get_contents($this->composerJsonPath); $composerJson = json_decode($composerContent, true); @@ -224,44 +225,50 @@ private function updateComposerJson(): void return; } + // Backup original before modifications + file_put_contents($this->composerJsonPath.'-original', $composerContent); + + // Update development composer.json (with all packages and repositories) $repositories = $composerJson['repositories'] ?? []; $require = $composerJson['require'] ?? []; - $updated = false; - $addedRepos = []; - $addedRequires = []; + $allPackages = array_merge($this->packages, config('devlink.internal_packages', [])); - foreach ($this->packages as $package) { + foreach ($allPackages as $package) { $packagePath = "packages/{$package}"; $repoEntry = ['type' => 'path', 'url' => $packagePath, 'options' => ['symlink' => true]]; $packageName = "moox/{$package}"; if (! collect($repositories)->contains(fn ($repo) => $repo['url'] === $packagePath)) { $repositories[] = $repoEntry; - $updated = true; - $addedRepos[] = $package; } - if (! isset($require[$packageName])) { $require[$packageName] = '*'; - $updated = true; - $addedRequires[] = $packageName; } } - if ($updated) { - $composerJson['repositories'] = $repositories; - $composerJson['require'] = $require; - file_put_contents($this->composerJsonPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - - if ($addedRepos) { - $this->info('Added repository entries for: '.implode(', ', $addedRepos)); - } - if ($addedRequires) { - $this->info('Added requirements for: '.implode(', ', $addedRequires)); - } - } else { - $this->info('No changes needed in composer.json'); + $composerJson['repositories'] = $repositories; + $composerJson['require'] = $require; + file_put_contents( + $this->composerJsonPath, + json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n" + ); + + // Create composer.json-deploy (only public packages, no repositories) + $deployJson = $composerJson; + unset($deployJson['repositories']); + $deployJson['minimum-stability'] = 'stable'; + + // Remove internal packages from require + foreach (config('devlink.internal_packages', []) as $package) { + unset($deployJson['require']["moox/{$package}"]); } + + file_put_contents( + $this->composerJsonPath.'-deploy', + json_encode($deployJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n" + ); + + $this->info('Updated composer.json and created composer.json-deploy'); } private function runComposerUpdate(): void