deployphp /
deployer
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | /* (c) Anton Medvedev <[email protected]> |
||
| 6 | * |
||
| 7 | * For the full copyright and license information, please view the LICENSE |
||
| 8 | * file that was distributed with this source code. |
||
| 9 | */ |
||
| 10 | |||
| 11 | namespace Deployer\Command; |
||
| 12 | |||
| 13 | use Symfony\Component\Console\Command\Command; |
||
| 14 | use Symfony\Component\Console\Input\InputInterface; |
||
| 15 | use Symfony\Component\Console\Input\InputOption; |
||
| 16 | use Symfony\Component\Console\Output\OutputInterface; |
||
| 17 | use Symfony\Component\Console\Style\SymfonyStyle; |
||
| 18 | use Symfony\Component\Process\Exception\RuntimeException; |
||
| 19 | use Symfony\Component\Process\PhpProcess; |
||
| 20 | use Symfony\Component\Process\Process; |
||
| 21 | |||
| 22 | class InitCommand extends Command |
||
| 23 | { |
||
| 24 | use CommandCommon; |
||
| 25 | |||
| 26 | protected function configure() |
||
| 27 | { |
||
| 28 | $this |
||
| 29 | ->setName('init') |
||
| 30 | ->setDescription('Initialize deployer in your project') |
||
| 31 | ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Recipe path'); |
||
| 32 | } |
||
| 33 | |||
| 34 | protected function execute(InputInterface $input, OutputInterface $output): int |
||
| 35 | { |
||
| 36 | if (getenv('COLORTERM') === 'truecolor') { |
||
| 37 | $output->write( |
||
| 38 | <<<EOF |
||
| 39 | ╭───────────────────────────────────────╮ |
||
| 40 | │ │ |
||
| 41 | │ │ |
||
| 42 | │ \e[38;2;94;231;223m_\e[39m\e[38;2;95;231;226m_\e[39m\e[38;2;96;230;228m_\e[39m\e[38;2;96;229;230m_\e[39m \e[38;2;97;226;230m_\e[39m │ |
||
| 43 | │ \e[38;2;98;223;229m|\e[39m \e[38;2;98;220;229m\\\e[39m \e[38;2;99;216;228m_\e[39m\e[38;2;100;213;228m_\e[39m\e[38;2;101;210;228m_\e[39m \e[38;2;101;208;227m_\e[39m\e[38;2;102;205;227m_\e[39m\e[38;2;103;202;227m_\e[39m\e[38;2;104;199;226m|\e[39m \e[38;2;104;196;226m|\e[39m\e[38;2;105;194;225m_\e[39m\e[38;2;106;191;225m_\e[39m\e[38;2;106;188;225m_\e[39m \e[38;2;107;186;224m_\e[39m \e[38;2;108;183;224m_\e[39m \e[38;2;109;181;224m_\e[39m\e[38;2;109;178;223m_\e[39m\e[38;2;110;176;223m_\e[39m \e[38;2;111;174;222m_\e[39m\e[38;2;111;171;222m_\e[39m\e[38;2;112;169;222m_\e[39m │ |
||
| 44 | │ \e[38;2;113;167;221m|\e[39m \e[38;2;113;165;221m|\e[39m \e[38;2;114;163;221m|\e[39m \e[38;2;115;160;220m-\e[39m\e[38;2;115;158;220m_\e[39m\e[38;2;116;156;219m|\e[39m \e[38;2;117;155;219m.\e[39m \e[38;2;117;153;219m|\e[39m \e[38;2;118;151;218m|\e[39m \e[38;2;119;149;218m.\e[39m \e[38;2;119;147;218m|\e[39m \e[38;2;120;145;217m|\e[39m \e[38;2;121;144;217m|\e[39m \e[38;2;121;142;216m-\e[39m\e[38;2;122;140;216m_\e[39m\e[38;2;123;139;216m|\e[39m \e[38;2;123;137;215m_\e[39m\e[38;2;124;136;215m|\e[39m │ |
||
| 45 | │ \e[38;2;124;134;215m|\e[39m\e[38;2;125;133;214m_\e[39m\e[38;2;126;132;214m_\e[39m\e[38;2;126;130;214m_\e[39m\e[38;2;127;129;213m_\e[39m\e[38;2;127;128;213m/\e[39m\e[38;2;130;128;212m|\e[39m\e[38;2;132;129;212m_\e[39m\e[38;2;134;129;212m_\e[39m\e[38;2;137;130;211m_\e[39m\e[38;2;139;131;211m|\e[39m \e[38;2;141;131;211m_\e[39m\e[38;2;143;132;210m|\e[39m\e[38;2;145;132;210m_\e[39m\e[38;2;147;133;209m|\e[39m\e[38;2;149;133;209m_\e[39m\e[38;2;151;134;209m_\e[39m\e[38;2;153;135;208m_\e[39m\e[38;2;155;135;208m|\e[39m\e[38;2;157;136;208m_\e[39m \e[38;2;159;136;207m|\e[39m\e[38;2;161;137;207m_\e[39m\e[38;2;162;137;206m_\e[39m\e[38;2;164;138;206m_\e[39m\e[38;2;166;139;206m|\e[39m\e[38;2;167;139;205m_\e[39m\e[38;2;169;140;205m|\e[39m │ |
||
| 46 | │ \e[38;2;170;140;205m|\e[39m\e[38;2;172;141;204m_\e[39m\e[38;2;173;141;204m|\e[39m \e[38;2;175;142;203m|\e[39m\e[38;2;176;142;203m_\e[39m\e[38;2;177;143;203m_\e[39m\e[38;2;179;143;202m_\e[39m\e[38;2;180;144;202m|\e[39m │ |
||
| 47 | │ │ |
||
| 48 | │ │ |
||
| 49 | ╰───────────────────────────────────────╯ |
||
| 50 | |||
| 51 | EOF, |
||
| 52 | ); |
||
| 53 | } else { |
||
| 54 | $output->write( |
||
| 55 | <<<EOF |
||
| 56 | ╭───────────────────────────────────────╮ |
||
| 57 | │ │ |
||
| 58 | │ │ |
||
| 59 | │ ____ _ │ |
||
| 60 | │ | \ ___ ___| |___ _ _ ___ ___ │ |
||
| 61 | │ | | | -_| . | | . | | | -_| _| │ |
||
| 62 | │ |____/|___| _|_|___|_ |___|_| │ |
||
| 63 | │ |_| |___| │ |
||
| 64 | │ │ |
||
| 65 | │ │ |
||
| 66 | ╰───────────────────────────────────────╯ |
||
| 67 | |||
| 68 | EOF, |
||
| 69 | ); |
||
| 70 | } |
||
| 71 | |||
| 72 | $io = new SymfonyStyle($input, $output); |
||
| 73 | $recipePath = $input->getOption('path'); |
||
| 74 | |||
| 75 | $language = $io->choice('Select recipe language', ['php', 'yaml'], 'php'); |
||
| 76 | if (empty($recipePath)) { |
||
| 77 | $recipePath = "deploy.$language"; |
||
| 78 | } |
||
| 79 | |||
| 80 | // Avoid accidentally override of existing file. |
||
| 81 | if (file_exists($recipePath)) { |
||
| 82 | $io->warning("$recipePath already exists"); |
||
| 83 | if (!$io->confirm("Do you want to override the existing file?", false)) { |
||
| 84 | $io->block('👍🏻'); |
||
| 85 | exit(1); |
||
|
0 ignored issues
–
show
|
|||
| 86 | } |
||
| 87 | } |
||
| 88 | |||
| 89 | // Template |
||
| 90 | $template = $io->choice('Select project template', $this->recipes(), 'common'); |
||
| 91 | |||
| 92 | // Repo |
||
| 93 | $default = ''; |
||
| 94 | try { |
||
| 95 | $process = Process::fromShellCommandline('git remote get-url origin'); |
||
| 96 | $default = $process->mustRun()->getOutput(); |
||
| 97 | $default = trim($default); |
||
| 98 | } catch (RuntimeException $e) { |
||
|
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||
| 99 | } |
||
| 100 | $repository = $io->ask('Repository', $default); |
||
| 101 | |||
| 102 | // Guess host |
||
| 103 | if (preg_match('/github.com:(?<org>[A-Za-z0-9_.\-]+)\//', $repository, $m)) { |
||
| 104 | $org = $m['org']; |
||
| 105 | $tempHostFile = tempnam(sys_get_temp_dir(), 'temp-host-file'); |
||
| 106 | $php = new PhpProcess( |
||
| 107 | <<<EOF |
||
| 108 | <?php |
||
| 109 | \$ch = curl_init('https://api.github.com/orgs/$org'); |
||
| 110 | curl_setopt(\$ch, CURLOPT_USERAGENT, 'Deployer'); |
||
| 111 | curl_setopt(\$ch, CURLOPT_CUSTOMREQUEST, 'GET'); |
||
| 112 | curl_setopt(\$ch, CURLOPT_RETURNTRANSFER, true); |
||
| 113 | curl_setopt(\$ch, CURLOPT_FOLLOWLOCATION, true); |
||
| 114 | curl_setopt(\$ch, CURLOPT_MAXREDIRS, 10); |
||
| 115 | curl_setopt(\$ch, CURLOPT_CONNECTTIMEOUT, 5); |
||
| 116 | curl_setopt(\$ch, CURLOPT_TIMEOUT, 5); |
||
| 117 | \$result = curl_exec(\$ch); |
||
| 118 | curl_close(\$ch); |
||
| 119 | \$json = json_decode(\$result); |
||
| 120 | \$host = parse_url(\$json->blog, PHP_URL_HOST); |
||
| 121 | file_put_contents('$tempHostFile', \$host); |
||
| 122 | EOF, |
||
| 123 | ); |
||
| 124 | $php->start(); |
||
| 125 | } |
||
| 126 | |||
| 127 | // Project |
||
| 128 | $default = ''; |
||
| 129 | try { |
||
| 130 | $process = Process::fromShellCommandline('basename "$PWD"'); |
||
| 131 | $default = $process->mustRun()->getOutput(); |
||
| 132 | $default = trim($default); |
||
| 133 | } catch (RuntimeException $e) { |
||
|
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||
| 134 | } |
||
| 135 | $project = $io->ask('Project name', $default); |
||
| 136 | |||
| 137 | // Hosts |
||
| 138 | $host = null; |
||
| 139 | if (isset($tempHostFile)) { |
||
| 140 | $host = file_get_contents($tempHostFile); |
||
| 141 | } |
||
| 142 | $hostsString = $io->ask('Hosts (comma separated)', $host); |
||
| 143 | if ($hostsString !== null) { |
||
| 144 | $hosts = explode(',', $hostsString); |
||
| 145 | } else { |
||
| 146 | $hosts = []; |
||
| 147 | } |
||
| 148 | |||
| 149 | file_put_contents($recipePath, $this->$language($template, $project, $repository, $hosts)); |
||
| 150 | |||
| 151 | $this->telemetry(); |
||
| 152 | $output->writeln(sprintf( |
||
| 153 | '<info>Successfully created</info> <comment>%s</comment>', |
||
| 154 | $recipePath, |
||
| 155 | )); |
||
| 156 | return 0; |
||
| 157 | } |
||
| 158 | |||
| 159 | private function php(string $template, string $project, string $repository, array $hosts): string |
||
| 160 | { |
||
| 161 | $h = ""; |
||
| 162 | foreach ($hosts as $host) { |
||
| 163 | $h .= "host('{$host}')\n" . |
||
| 164 | " ->set('remote_user', 'deployer')\n" . |
||
| 165 | " ->set('deploy_path', '~/{$project}');\n"; |
||
| 166 | } |
||
| 167 | |||
| 168 | return <<<PHP |
||
| 169 | <?php |
||
| 170 | namespace Deployer; |
||
| 171 | |||
| 172 | require 'recipe/$template.php'; |
||
| 173 | |||
| 174 | // Config |
||
| 175 | |||
| 176 | set('repository', '{$repository}'); |
||
| 177 | |||
| 178 | add('shared_files', []); |
||
| 179 | add('shared_dirs', []); |
||
| 180 | add('writable_dirs', []); |
||
| 181 | |||
| 182 | // Hosts |
||
| 183 | |||
| 184 | {$h} |
||
| 185 | // Hooks |
||
| 186 | |||
| 187 | after('deploy:failed', 'deploy:unlock'); |
||
| 188 | |||
| 189 | PHP; |
||
| 190 | } |
||
| 191 | |||
| 192 | private function yaml(string $template, string $project, string $repository, array $hosts): string |
||
| 193 | { |
||
| 194 | $h = ""; |
||
| 195 | foreach ($hosts as $host) { |
||
| 196 | $h .= " $host:\n" . |
||
| 197 | " remote_user: deployer\n" . |
||
| 198 | " deploy_path: '~/{$project}'\n"; |
||
| 199 | } |
||
| 200 | |||
| 201 | $additionalConfigs = $this->getAdditionalConfigs($template); |
||
| 202 | |||
| 203 | return <<<YAML |
||
| 204 | import: |
||
| 205 | - recipe/$template.php |
||
| 206 | |||
| 207 | config: |
||
| 208 | repository: '$repository' |
||
| 209 | $additionalConfigs |
||
| 210 | hosts: |
||
| 211 | $h |
||
| 212 | tasks: |
||
| 213 | build: |
||
| 214 | - run: uptime |
||
| 215 | |||
| 216 | after: |
||
| 217 | deploy:failed: deploy:unlock |
||
| 218 | |||
| 219 | YAML; |
||
| 220 | } |
||
| 221 | |||
| 222 | private function getAdditionalConfigs(string $template): string |
||
| 223 | { |
||
| 224 | if ($template !== 'common') { |
||
| 225 | return ''; |
||
| 226 | } |
||
| 227 | |||
| 228 | return <<<YAML |
||
| 229 | shared_files: |
||
| 230 | - .env |
||
| 231 | shared_dirs: |
||
| 232 | - uploads |
||
| 233 | writable_dirs: |
||
| 234 | - uploads |
||
| 235 | |||
| 236 | YAML; |
||
| 237 | } |
||
| 238 | |||
| 239 | private function recipes(): array |
||
| 240 | { |
||
| 241 | $recipes = []; |
||
| 242 | $dir = new \DirectoryIterator(__DIR__ . '/../../recipe'); |
||
| 243 | foreach ($dir as $fileinfo) { |
||
| 244 | if ($fileinfo->isDot()) { |
||
| 245 | continue; |
||
| 246 | } |
||
| 247 | if ($fileinfo->isDir()) { |
||
| 248 | continue; |
||
| 249 | } |
||
| 250 | |||
| 251 | $recipe = pathinfo($fileinfo->getFilename(), PATHINFO_FILENAME); |
||
| 252 | |||
| 253 | if ($recipe === 'README') { |
||
| 254 | continue; |
||
| 255 | } |
||
| 256 | |||
| 257 | $recipes[] = $recipe; |
||
| 258 | } |
||
| 259 | |||
| 260 | sort($recipes); |
||
| 261 | return $recipes; |
||
| 262 | } |
||
| 263 | } |
||
| 264 |
For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example: