├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── Commands │ ├── Concerns │ │ └── EnsureHasToken.php │ ├── CreateDaemonCommand.php │ ├── CreateSshKeyCommand.php │ ├── CreateWebhookCommand.php │ ├── DeployCommand.php │ ├── DeployLogCommand.php │ ├── DeployResetCommand.php │ ├── ForgeCommand.php │ ├── InfoCommand.php │ ├── InitCommand.php │ ├── LoginCommand.php │ ├── PullConfigCommand.php │ ├── PullEnvCommand.php │ ├── PullNginxCommand.php │ ├── PushConfigCommand.php │ ├── PushEnvCommand.php │ ├── PushNginxCommand.php │ ├── RebootCommand.php │ ├── RebootMySQLCommand.php │ ├── RebootNginxCommand.php │ ├── RebootPostgresCommand.php │ ├── RebootServerCommand.php │ └── SeeLogCommand.php ├── Providers │ └── AppServiceProvider.php ├── Support │ ├── Configuration.php │ ├── Defaults.php │ └── TokenNodeVisitor.php ├── Sync │ ├── BaseSync.php │ ├── DaemonSync.php │ ├── DeploymentScriptSync.php │ ├── WebhookSync.php │ └── WorkerSync.php └── helpers.php ├── bootstrap └── app.php ├── box.json ├── builds └── forge ├── composer.json ├── composer.lock ├── config ├── app.php ├── commands.php └── forge.php ├── docs ├── _index.md ├── basic-commands │ ├── _index.md │ ├── info.md │ └── init.md ├── configuration │ ├── _index.md │ ├── pull.md │ └── push.md ├── daemon │ ├── _index.md │ └── creating-a-new-daemon.md ├── deployments │ ├── _index.md │ ├── deploy-log.md │ ├── deploy-reset.md │ ├── deploy.md │ └── deployment-script.md ├── environment-files │ ├── _index.md │ ├── pull.md │ └── push.md ├── getting-started │ ├── _index.md │ ├── environments.md │ ├── installation.md │ ├── logging-in.md │ └── questions-issues.md ├── introduction.md ├── logs │ ├── _index.md │ └── reading-logs.md ├── nginx-configuration │ ├── _index.md │ ├── pull.md │ └── push.md ├── services │ ├── _index.md │ ├── mysql.md │ ├── nginx.md │ ├── postgres.md │ └── server.md └── webhooks │ ├── _index.md │ └── creating-a-new-webhook.md ├── forge ├── phpunit.xml.dist └── tests ├── CreatesApplication.php ├── Feature └── InspiringCommandTest.php └── TestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | /.github export-ignore 3 | .styleci.yml export-ignore 4 | .scrutinizer.yml export-ignore 5 | BACKERS.md export-ignore 6 | CONTRIBUTING.md export-ignore 7 | CHANGELOG.md export-ignore 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.vscode 4 | /.vagrant 5 | .phpunit.result.cache 6 | .env.forge 7 | forge.yml 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Beyond Code GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Forge CLI 2 | 3 | An opinionated Laravel Forge CLI tool. 4 | 5 | ## Documentation 6 | 7 | You can find the documentation on the [official project website](https://beyondco.de/docs/forge-cli/). 8 | 9 | ## Contributing 10 | 11 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 12 | 13 | ### Security 14 | 15 | If you discover any security related issues, please email `marcel@beyondco.de` instead of using the issue tracker. 16 | 17 | ## Credits 18 | 19 | - [Marcel Pociot](https://github.com/mpociot) 20 | - [All Contributors](../../contributors) 21 | 22 | ## License 23 | 24 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 25 | -------------------------------------------------------------------------------- /app/Commands/Concerns/EnsureHasToken.php: -------------------------------------------------------------------------------- 1 | hasToken()) { 15 | $this->error('You have not configured your Forge API token yet. Please call "forge login" first.'); 16 | return false; 17 | } 18 | 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Commands/CreateDaemonCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 19 | 20 | $serverId = $configuration->get($environment, 'server'); 21 | $siteId = $configuration->get($environment, 'id'); 22 | 23 | $site = $forge->site($serverId, $siteId); 24 | 25 | $command = $this->ask('Which command do you want to run on your server'); 26 | $user = $this->ask('Which user should run the command', 'forge'); 27 | $directory = $this->ask('Which directory should the command run in', "/home/forge/{$site->name}"); 28 | $processes = $this->ask('How many processes do you want to run', 1); 29 | $startsecs = $this->ask('Start seconds (The total number of seconds the program needs to stay running to consider the start successful. 30 | )', 1); 31 | 32 | $daemons = $configuration->get($environment, 'daemons', []); 33 | $daemons[] = [ 34 | 'command' => $command, 35 | 'user' => $user, 36 | 'directory' => $directory, 37 | 'processes' => $processes, 38 | 'startsecs' => $startsecs, 39 | ]; 40 | 41 | $configuration->set($environment, 'daemons', $daemons); 42 | 43 | $configuration->store(getcwd() . '/forge.yml'); 44 | 45 | $this->info('Successfully stored daemon in your forge.yml config file. You can push the configuration using "forge config:push".'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Commands/CreateSshKeyCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 18 | 19 | $serverId = $configuration->get($environment, 'server'); 20 | 21 | $name = $this->ask('What is the key name', 'macbook'); 22 | $path = $this->ask('Where is the public key file', ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE'] ?? __DIR__).'/.ssh/id_rsa.pub'); 23 | $username = $this->ask('What is the user name', 'forge'); 24 | 25 | $key = file_get_contents($path); 26 | 27 | $forge->createSSHKey($serverId, compact('name', 'key', 'username')); 28 | 29 | $this->info('The key has been created'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Commands/CreateWebhookCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 18 | 19 | $url = $this->ask('Which webhook URL do you want to add'); 20 | 21 | $webhooks = $configuration->get($environment, 'webhooks', []); 22 | 23 | $webhooks[] = $url; 24 | 25 | $configuration->set($environment, 'webhooks', $webhooks); 26 | 27 | $configuration->store(getcwd() . '/forge.yml'); 28 | 29 | $this->info('Successfully stored the webhook in your forge.yml config file. You can push the configuration using "forge config:push".'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Commands/DeployCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 20 | 21 | $serverId = $configuration->get($environment, 'server'); 22 | $siteId = $configuration->get($environment, 'id'); 23 | 24 | if ($this->option('update-script')) { 25 | $this->info('Updating deployment script...'); 26 | 27 | $script = implode("\n", $configuration->get($environment, 'deployment', [])); 28 | 29 | $forge->updateSiteDeploymentScript($serverId, $siteId, $script); 30 | } 31 | 32 | $this->info("Deploying site on {$environment}..."); 33 | 34 | $forge->deploySite($serverId, $siteId); 35 | 36 | if (!$this->option('no-wait')) { 37 | $forge->retry(CarbonInterval::minutes(10)->totalSeconds, function () use ($serverId, $siteId, $forge) { 38 | $site = $forge->site($serverId, $siteId); 39 | 40 | return is_null($site->deploymentStatus); 41 | }, 5); 42 | } 43 | 44 | $this->info('The site has been deployed'); 45 | 46 | if (!$this->option('no-wait')) { 47 | $this->call('deploy:log', [ 48 | 'environment' => $environment, 49 | ]); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Commands/DeployLogCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 18 | 19 | $serverId = $configuration->get($environment, 'server'); 20 | $siteId = $configuration->get($environment, 'id'); 21 | 22 | $this->info("Retrieving the latest deployment log on {$environment}..."); 23 | 24 | try { 25 | $log = $forge->siteDeploymentLog($serverId, $siteId); 26 | $this->info(''); 27 | $this->info('---------- BEGIN DEPLOYMENT LOG ----------'); 28 | $this->line($log); 29 | $this->info('----------- END DEPLOYMENT LOG -----------'); 30 | } catch (NotFoundException $exception) { 31 | $this->error("There is currently no deployment log available."); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Commands/DeployResetCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 17 | 18 | $serverId = $configuration->get($environment, 'server'); 19 | $siteId = $configuration->get($environment, 'id'); 20 | 21 | $forge->resetDeploymentState($serverId, $siteId); 22 | 23 | $this->info('The deployment state has been reset'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Commands/ForgeCommand.php: -------------------------------------------------------------------------------- 1 | ensureHasToken()) { 17 | return static::FAILURE; 18 | } 19 | 20 | if (! file_exists(getcwd() . '/forge.yml')) { 21 | $this->error('You have not yet linked this project to Forge. Run `forge init` first.'); 22 | 23 | return static::FAILURE; 24 | } 25 | 26 | return parent::execute($input, $output); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Commands/InfoCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 18 | 19 | $serverId = $configuration->get($environment, 'server'); 20 | $siteId = $configuration->get($environment, 'id'); 21 | 22 | $server = $forge->server($serverId); 23 | $site = $forge->site($serverId, $siteId); 24 | 25 | $data = [ 26 | ['Server', $server->name], 27 | ['IP', $server->ipAddress], 28 | ['Site', $site->name], 29 | ['Directory', $site->directory], 30 | ]; 31 | 32 | $this->table(['Key', 'Value'], $data); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Commands/InitCommand.php: -------------------------------------------------------------------------------- 1 | 'General PHP/Laravel Application.', 19 | 'html' => 'Static HTML site.', 20 | 'symfony' => 'Symfony Application.', 21 | 'symfony_dev' => 'Symfony (Dev) Application.', 22 | 'symfony_four' => 'Symfony >4.0 Application.', 23 | ]; 24 | 25 | /** @var Forge */ 26 | protected $forge; 27 | 28 | protected $signature = 'init {environment=production}'; 29 | 30 | protected $description = 'Initialize a new app ready to get deployed on Laravel Forge'; 31 | 32 | /** 33 | * @param Forge $forge 34 | * @param Configuration $configuration 35 | */ 36 | public function handle(Forge $forge, Configuration $configuration) 37 | { 38 | $this->ensureHasToken(); 39 | 40 | $this->forge = $forge; 41 | 42 | $servers = $forge->servers(); 43 | 44 | $selectedServer = $this->menu('Which server do you want to use?', collect($servers)->map(function (Server $server) { 45 | return "{$server->name} - [{$server->id}]"; 46 | })->toArray())->open(); 47 | 48 | exit_if(is_null($selectedServer)); 49 | 50 | $server = $servers[$selectedServer]; 51 | 52 | $linkSite = $this->confirm('Do you want to link this directory to an existing site?'); 53 | 54 | if ($linkSite) { 55 | $sites = $forge->sites($server->id); 56 | 57 | $selectedSite = $this->menu('Which site do you want to link this project to?', collect($sites)->map(function (Site $site) { 58 | return "{$site->name} - [{$site->id}]"; 59 | })->toArray())->open(); 60 | 61 | exit_if(is_null($selectedSite)); 62 | 63 | $site = $sites[$selectedSite]; 64 | } else { 65 | $site = $this->createSite($server); 66 | } 67 | 68 | $configuration->initialize($this->argument('environment'), $server, $site, getcwd()); 69 | 70 | $this->info('The project was successfully initialized.'); 71 | } 72 | 73 | protected function createSite(Server $server) 74 | { 75 | $domain = $this->ask('What is the domain of your project?', basename(getcwd())); 76 | 77 | $selectedProjectType = $this->menu('What is your project type?', static::PROJECT_TYPES)->open(); 78 | 79 | $directory = $this->ask('What is the public directory of your project?', '/public'); 80 | 81 | exit_if(is_null($selectedProjectType)); 82 | 83 | $this->info('Creating site on Forge'); 84 | 85 | return $this->forge->createSite($server->id, [ 86 | 'domain' => $domain, 87 | 'project_type' => $selectedProjectType, 88 | 'directory' => $directory, 89 | ]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/Commands/LoginCommand.php: -------------------------------------------------------------------------------- 1 | hasToken() && ! $this->option('force')) { 38 | $this->warn('You are already logged in'); 39 | return 0; 40 | } 41 | 42 | $email = $this->ask('What is your Laravel Forge email?'); 43 | $password = $this->secret('What is your password?'); 44 | 45 | $this->info('Logging in...'); 46 | 47 | $this->login($email, $password); 48 | } 49 | 50 | protected function login($email, $password) 51 | { 52 | $browser = new HttpBrowser(HttpClient::create()); 53 | 54 | $browser->request('GET', static::LOGIN_URL); 55 | 56 | $browser->submitForm('Sign In', [ 57 | 'email' => $email, 58 | 'password' => $password 59 | ]); 60 | 61 | $uri = $browser->getHistory()->current()->getUri(); 62 | 63 | if ($uri === static::TWO_FACTOR_AUTH_URL) { 64 | $token = $this->ask('What is your 2FA token?'); 65 | 66 | $browser->submitForm('Verify', [ 67 | 'token' => $token, 68 | ]); 69 | 70 | $uri = $browser->getHistory()->current()->getUri(); 71 | } 72 | 73 | if ($uri === static::LOGIN_URL || $uri === static::TWO_FACTOR_AUTH_URL) { 74 | $this->error('Invalid credentials.'); 75 | exit(); 76 | } 77 | 78 | $browser->request('POST', static::TOKEN_URL, [ 79 | 'name' => 'Forge-CLI', 80 | 'scopes' => [], 81 | ]); 82 | 83 | /** @var Response $response */ 84 | $response = $browser->getResponse(); 85 | 86 | if ($response->getStatusCode() !== 200) { 87 | $this->error('Unable to create API Token'); 88 | $this->error($response->getContent()); 89 | exit(); 90 | } 91 | 92 | $responseObject = json_decode($response->getContent()); 93 | $this->saveToken($responseObject->accessToken); 94 | 95 | $this->info('Retrieved and stored your Forge access token!'); 96 | $this->info('You\'re all set and ready to go.'); 97 | } 98 | 99 | protected function saveToken($token) 100 | { 101 | $configFile = implode(DIRECTORY_SEPARATOR, [ 102 | $_SERVER['HOME'] ?? $_SERVER['USERPROFILE'], 103 | '.forge', 104 | 'config.php', 105 | ]); 106 | 107 | if (! file_exists($configFile)) { 108 | @mkdir(dirname($configFile), 0777, true); 109 | $updatedConfigFile = $this->modifyConfigurationFile(base_path('config/forge.php'), $token); 110 | } else { 111 | $updatedConfigFile = $this->modifyConfigurationFile($configFile, $token); 112 | } 113 | 114 | file_put_contents($configFile, $updatedConfigFile); 115 | 116 | return; 117 | } 118 | 119 | protected function modifyConfigurationFile(string $configFile, string $token) 120 | { 121 | $lexer = new Emulative([ 122 | 'usedAttributes' => [ 123 | 'comments', 124 | 'startLine', 'endLine', 125 | 'startTokenPos', 'endTokenPos', 126 | ], 127 | ]); 128 | $parser = new Php7($lexer); 129 | 130 | $oldStmts = $parser->parse(file_get_contents($configFile)); 131 | $oldTokens = $lexer->getTokens(); 132 | 133 | $nodeTraverser = new NodeTraverser; 134 | $nodeTraverser->addVisitor(new CloningVisitor()); 135 | $newStmts = $nodeTraverser->traverse($oldStmts); 136 | 137 | $nodeTraverser = new NodeTraverser; 138 | $nodeTraverser->addVisitor(new TokenNodeVisitor($token)); 139 | 140 | $newStmts = $nodeTraverser->traverse($newStmts); 141 | 142 | $prettyPrinter = new Standard(); 143 | 144 | return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/Commands/PullConfigCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 24 | 25 | $server = $forge->server($configuration->get($environment, 'server')); 26 | $site = $forge->site($server->id, $configuration->get($environment, 'id')); 27 | 28 | $configuration->initialize($environment, $server, $site, getcwd()); 29 | 30 | $this->info('Successfully updated the Forge configuration file.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Commands/PullEnvCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 23 | 24 | $env = $forge->siteEnvironmentFile($configuration->get($environment, 'server'), $configuration->get($environment, 'id')); 25 | 26 | file_put_contents(".env.forge.{$environment}", $env); 27 | 28 | $this->info("Wrote environment file to .env.forge.{$environment}"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Commands/PullNginxCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 24 | $filename = "nginx-forge-{$environment}.conf"; 25 | 26 | $config = $forge->siteNginxFile($configuration->get($environment, 'server'), $configuration->get($environment, 'id')); 27 | 28 | file_put_contents($filename, $config); 29 | 30 | $this->info('Wrote nginx config file to '.$filename); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Commands/PushConfigCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 39 | 40 | $server = $forge->server($configuration->get($environment, 'server')); 41 | $site = $forge->site($server->id, $configuration->get($environment, 'id')); 42 | 43 | $this->synchronize($environment, $server, $site); 44 | 45 | $this->info('Done'); 46 | } 47 | 48 | protected function synchronize(string $environment, Server $server, Site $site) 49 | { 50 | foreach (static::SYNC_CLASSES as $syncClass) { 51 | $this->info('Synchronizing ' . $syncClass); 52 | 53 | /** @var BaseSync $synchronizer */ 54 | $syncer = app($syncClass); 55 | $syncer->sync($environment, $server, $site, $this->getOutput(), $this->option('force')); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Commands/PushEnvCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 21 | $envFile = ".env.forge.{$environment}"; 22 | 23 | if (!file_exists($envFile)) { 24 | $this->error("The {$envFile} file does not exist."); 25 | exit(); 26 | } 27 | 28 | 29 | $siteId = $configuration->get($environment, 'id'); 30 | 31 | try { 32 | $forge->updateSiteEnvironmentFile( 33 | $configuration->get($environment, 'server'), 34 | $configuration->get($environment, 'id'), 35 | file_get_contents($envFile) 36 | ); 37 | 38 | $this->info("Successfully updated the environment on Forge ({$environment})."); 39 | 40 | $shouldDeleteEnvFile = $this->confirm("Do you want to delete the local {$envFile} file?"); 41 | 42 | if ($shouldDeleteEnvFile) { 43 | unlink($envFile); 44 | } 45 | } catch (\Exception $e) { 46 | $this->error('Something went wrong: '); 47 | $this->error($e->getMessage()); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Commands/PushNginxCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 24 | $filename = "nginx-forge-{$environment}.conf"; 25 | 26 | if (!file_exists($filename)) { 27 | $this->error("The {$filename} file does not exist."); 28 | exit(); 29 | } 30 | 31 | 32 | $siteId = $configuration->get($environment, 'id'); 33 | 34 | try { 35 | $forge->updateSiteNginxFile( 36 | $configuration->get($environment, 'server'), 37 | $configuration->get($environment, 'id'), 38 | file_get_contents($filename) 39 | ); 40 | 41 | $this->info("Successfully updated the Nginx configuration on Forge ({$environment})."); 42 | } catch (\Exception $e) { 43 | $this->error('Something went wrong: '); 44 | $this->error($e->getMessage()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Commands/RebootCommand.php: -------------------------------------------------------------------------------- 1 | forge = $forge; 22 | $this->configuration = $configuration; 23 | 24 | if (! $this->option('confirm')) { 25 | $this->warn('Rebooting ' . $this->subject . ' requires confirmation'); 26 | $this->warn('Please use --confirm to confirm that you want to reboot ' . $this->subject); 27 | 28 | return 1; 29 | } 30 | 31 | $this->info('Rebooting ' . $this->subject); 32 | 33 | $this->reboot(); 34 | 35 | $this->info('Depending on the server, this may take some time and may cause temporary downtime'); 36 | } 37 | 38 | abstract public function reboot(); 39 | } 40 | -------------------------------------------------------------------------------- /app/Commands/RebootMySQLCommand.php: -------------------------------------------------------------------------------- 1 | configuration->get($this->argument('environment'), 'server'); 16 | 17 | $this->forge->rebootMysql($serverId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Commands/RebootNginxCommand.php: -------------------------------------------------------------------------------- 1 | configuration->get($this->argument('environment'), 'server'); 16 | 17 | $this->forge->rebootNginx($serverId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Commands/RebootPostgresCommand.php: -------------------------------------------------------------------------------- 1 | configuration->get($this->argument('environment'), 'server'); 16 | 17 | $this->forge->rebootPostgres($serverId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Commands/RebootServerCommand.php: -------------------------------------------------------------------------------- 1 | configuration->get($this->argument('environment'), 'server'); 16 | 17 | $this->forge->rebootServer($serverId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Commands/SeeLogCommand.php: -------------------------------------------------------------------------------- 1 | argument('environment'); 20 | 21 | $serverId = $configuration->get($environment, 'server'); 22 | $siteId = $configuration->get($environment, 'id'); 23 | 24 | $logs = $forge->get("servers/{$serverId}/logs?file=".$this->option('file')); 25 | 26 | $this->info('Log file: '.Arr::get($logs, 'path')); 27 | $this->info(Arr::get($logs, 'content')); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadConfigurationFile(); 28 | 29 | $this->app->singleton(Forge::class, function () { 30 | return new Forge(config('forge.token')); 31 | }); 32 | } 33 | 34 | protected function loadConfigurationFile() 35 | { 36 | $builtInConfig = config('forge'); 37 | 38 | $configFile = implode(DIRECTORY_SEPARATOR, [ 39 | $_SERVER['HOME'] ?? $_SERVER['USERPROFILE'] ?? __DIR__, 40 | '.forge', 41 | 'config.php', 42 | ]); 43 | 44 | if (file_exists($configFile)) { 45 | $globalConfig = require $configFile; 46 | config()->set('forge', array_merge($builtInConfig, $globalConfig)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Support/Configuration.php: -------------------------------------------------------------------------------- 1 | forge = $forge; 27 | 28 | try { 29 | $this->config = Yaml::parseFile(getcwd() . '/forge.yml'); 30 | } catch (\Exception $e) { 31 | $this->config = []; 32 | } 33 | } 34 | 35 | public function initialize(string $environment, Server $server, Site $site, string $path) 36 | { 37 | $configFile = $path . '/forge.yml'; 38 | 39 | $this->config[$environment] = $this->getConfigFormat($server, $site); 40 | 41 | $this->store($configFile); 42 | } 43 | 44 | public function store(string $configFile) 45 | { 46 | $flags = Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK; 47 | 48 | $configContent = Yaml::dump($this->config, 4, 2, $flags); 49 | 50 | file_put_contents($configFile, $configContent); 51 | } 52 | 53 | public function get(string $environment, string $key, $default = null) 54 | { 55 | return Arr::get($this->config, "{$environment}.{$key}", $default); 56 | } 57 | 58 | public function set(string $environment, string $key, $value) 59 | { 60 | Arr::set($this->config, "{$environment}.{$key}", $value); 61 | } 62 | 63 | protected function getConfigFormat(Server $server, Site $site) 64 | { 65 | $workers = $this->forge->workers($server->id, $site->id); 66 | 67 | return [ 68 | 'id' => $site->id, 69 | 'name' => $site->name, 70 | 'server' => $server->id, 71 | 'quick-deploy' => $site->quickDeploy, 72 | 'deployment' => $site->getDeploymentScript(), 73 | 'webhooks' => $this->getWebhooks($server, $site), 74 | 'daemons' => $this->getDaemons($server, $site), 75 | 'workers' => $this->getWorkers($server, $site), 76 | ]; 77 | } 78 | 79 | protected function getWebhooks(Server $server, Site $site) 80 | { 81 | return collect($this->forge->webhooks($server->id, $site->id))->map(function (Webhook $webhook) { 82 | return $webhook->url; 83 | })->values()->toArray(); 84 | } 85 | 86 | protected function getDaemons(Server $server, Site $site) 87 | { 88 | return collect($this->forge->daemons($server->id)) 89 | ->filter(function (Daemon $daemon) use ($site) { 90 | return Str::endsWith($daemon->command, " #{$site->id}"); 91 | }) 92 | ->map(function (Daemon $daemon) use ($site) { 93 | return [ 94 | 'command' => Str::beforeLast($daemon->command, " #{$site->id}"), 95 | 'user' => $daemon->user, 96 | 'directory' => $daemon->directory, 97 | 'processes' => $daemon->processes, 98 | 'startsecs' => $daemon->startsecs, 99 | ]; 100 | })->values()->toArray(); 101 | } 102 | 103 | protected function getWorkers(Server $server, Site $site) 104 | { 105 | $cli = collect($this->forge->phpVersions($server->id))->firstWhere('usedOnCli', true)->version; 106 | 107 | $defaults = Defaults::worker($cli); 108 | 109 | return collect($this->forge->workers($server->id, $site->id))->map(function ($worker) use ($defaults) { 110 | $data = [ 111 | 'queue' => $worker->queue, 112 | 'connection' => $worker->connection, 113 | 'php_version' => str_replace('.', '', head(explode(' ', $worker->command))), 114 | 'daemon' => (bool) $worker->daemon, 115 | 'processes' => $worker->processes, 116 | 'timeout' => $worker->timeout, 117 | 'sleep' => $worker->sleep, 118 | 'delay' => $worker->delay, 119 | 'tries' => $worker->tries, 120 | 'environment' => $worker->environment, 121 | 'force' => (bool) $worker->force, 122 | ]; 123 | 124 | $nonDefaults = collect($data)->filter(fn ($value, $key) => $value !== $defaults[$key])->keys()->toArray(); 125 | 126 | return Arr::only($data, ['queue', 'connection', ...$nonDefaults]); 127 | })->toArray(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/Support/Defaults.php: -------------------------------------------------------------------------------- 1 | 'default', // Note: defaults to blank if omitted 11 | 'connection' => 'redis', // Required by Forge API 12 | 'php_version' => $php, // Required by Forge API 13 | 'daemon' => false, // Required by Forge API 14 | 'processes' => 1, 15 | 'timeout' => 60, // Note: defaults to 0 (no timeout) if omitted 16 | 'sleep' => 10, // Required by Forge API 17 | 'delay' => 0, 18 | 'tries' => null, 19 | 'environment' => null, 20 | 'force' => false, 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Support/TokenNodeVisitor.php: -------------------------------------------------------------------------------- 1 | token = $token; 17 | } 18 | 19 | public function enterNode(Node $node) 20 | { 21 | if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'token') { 22 | $node->value->value = $this->token; 23 | 24 | return $node; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Sync/BaseSync.php: -------------------------------------------------------------------------------- 1 | forge = $forge; 22 | $this->config = $config; 23 | } 24 | 25 | abstract public function sync(string $environment, Server $server, Site $site, OutputStyle $output, bool $force = false): void; 26 | } 27 | -------------------------------------------------------------------------------- /app/Sync/DaemonSync.php: -------------------------------------------------------------------------------- 1 | config->get($environment, 'daemons', [])); 18 | $daemonsOnForge = collect($this->forge->daemons($server->id))->filter(function (Daemon $daemon) use ($site) { 19 | return Str::endsWith($daemon->command, " #{$site->id}"); 20 | }); 21 | 22 | // Delete Daemons on Forge but removed/modified locally 23 | $deleteDaemons = collect($daemonsOnForge) 24 | ->reject(function (Daemon $daemon) use ($daemons, $site) { 25 | return $daemons->contains(function ($daemonFromConfig) use ($daemon, $site) { 26 | return 27 | $daemonFromConfig['command'] === Str::beforeLast($daemon->command, " #{$site->id}") && 28 | $daemonFromConfig['user'] === $daemon->user && 29 | $daemonFromConfig['directory'] === $daemon->directory && 30 | $daemonFromConfig['processes'] === $daemon->processes && 31 | $daemonFromConfig['startsecs'] === $daemon->startsecs; 32 | }); 33 | }); 34 | 35 | $deleteDaemons->map(function (Daemon $daemon) use ($server, $site, $output) { 36 | $command = Str::beforeLast($daemon->command, " #{$site->id}"); 37 | 38 | $output->writeln("Deleting daemon: {$command}"); 39 | $daemon->delete(); 40 | }); 41 | 42 | // Create daemons not on Forge 43 | $daemons->diffUsing($daemonsOnForge->map(function (Daemon $daemon) use ($site) { 44 | return [ 45 | 'command' => Str::beforeLast($daemon->command, " #{$site->id}"), 46 | 'user' => $daemon->user, 47 | 'directory' => $daemon->directory, 48 | 'processes' => $daemon->processes, 49 | 'startsecs' => $daemon->startsecs, 50 | ]; 51 | }), function ($a, $b){ 52 | return count(array_diff($a, $b)) > 0; 53 | })->map(function ($daemonData) use ($server, $site, $output) { 54 | $output->writeln("Creating daemon: {$daemonData['command']}"); 55 | $daemonData['command'] .= " #{$site->id}"; 56 | $this->forge->createDaemon($server->id, $daemonData); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Sync/DeploymentScriptSync.php: -------------------------------------------------------------------------------- 1 | config->get($environment, 'deployment', '')) ? join("\n", $script) : $script; 17 | $deploymentScriptOnForge = $this->forge->siteDeploymentScript($server->id, $site->id); 18 | 19 | if (!$force && $deploymentScript !== $deploymentScriptOnForge) { 20 | $output->warning("Skipping the deployment script update, as the script on Forge is different than your local script.\nUse --force to overwrite it."); 21 | return; 22 | } 23 | 24 | $this->forge->updateSiteDeploymentScript($server->id, $site->id, $deploymentScript); 25 | 26 | if ($this->config->get($environment, 'quick-deploy')) { 27 | try { 28 | $site->enableQuickDeploy(); 29 | } catch (ValidationException $e) { 30 | if (! in_array('Hook already exists on this repository', $e->errors())) { 31 | throw $e; 32 | } 33 | } 34 | } else { 35 | $site->disableQuickDeploy(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Sync/WebhookSync.php: -------------------------------------------------------------------------------- 1 | config->get($environment, 'webhooks', [])); 16 | $webhooksOnForge = $this->forge->webhooks($server->id, $site->id); 17 | 18 | // Create webhooks not on Forge 19 | $webhooks->diff(collect($webhooksOnForge)->map(function (Webhook $webhook) { 20 | return $webhook->url; 21 | }))->map(function ($url) use ($server, $site, $output) { 22 | $output->writeln("Creating webhook: {$url}"); 23 | $this->forge->createWebhook($server->id, $site->id, [ 24 | 'url' => $url, 25 | ]); 26 | }); 27 | 28 | // Delete webhooks on Forge but removed locally 29 | $deleteWebhooks = collect($webhooksOnForge) 30 | ->reject(function (Webhook $webhook) use ($webhooks) { 31 | return $webhooks->contains($webhook->url); 32 | }); 33 | 34 | if (!$force && $deleteWebhooks->isNotEmpty()) { 35 | $output->warning("Skipping the deletion of {$deleteWebhooks->count()} Webhooks. \nUse --force to delete them."); 36 | return; 37 | } 38 | 39 | $deleteWebhooks->map(function (Webhook $webhook) use ($server, $site, $output) { 40 | $output->writeln("Deleting webhook: {$webhook->url}"); 41 | $this->forge->deleteWebhook($server->id, $site->id, $webhook->id); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Sync/WorkerSync.php: -------------------------------------------------------------------------------- 1 | config->get($environment, 'workers', [])); 16 | $forgeWorkers = collect($this->forge->workers($server->id, $site->id))->keyBy('id'); 17 | 18 | // Create workers that are defined locally but do not exist on Forge 19 | $workers->reject(function (array $worker) use (&$forgeWorkers, $server, $site) { 20 | if ($match = $forgeWorkers->first(fn (Worker $forge) => $this->equivalent($server, $forge, $worker))) { 21 | // Remove each found worker from the list of 'unmatched' workers on Forge 22 | $forgeWorkers->forget($match->id); 23 | 24 | return true; 25 | } 26 | })->map(function (array $worker) use ($server, $site, $output) { 27 | $data = $this->getWorkerPayload($server, $worker); 28 | 29 | $output->writeln("Creating {$data['queue']} queue worker on {$data['connection']} connection..."); 30 | 31 | $this->forge->createWorker($server->id, $site->id, $data); 32 | }); 33 | 34 | if ($forgeWorkers->isNotEmpty()) { 35 | if ($force) { 36 | $forgeWorkers->map(function (Worker $worker) use ($server, $site, $output) { 37 | $output->writeln("Deleting {$worker->queue} queue worker present on Forge but not listed locally..."); 38 | 39 | $this->forge->deleteWorker($server->id, $site->id, $worker->id); 40 | }); 41 | } else { 42 | $output->writeln("Found {$forgeWorkers->count()} queue workers present on Forge but not listed locally."); 43 | $output->writeln('Run the command again with the `--force` option to delete them.'); 44 | } 45 | } 46 | } 47 | 48 | protected function equivalent(Server $server, Worker $worker, array $config): bool 49 | { 50 | $cli = collect($this->forge->phpVersions($server->id))->firstWhere('usedOnCli', true)->version; 51 | 52 | $defaults = Defaults::worker($cli); 53 | 54 | $forgeWorker = [ 55 | 'queue' => $worker->queue, 56 | 'connection' => $worker->connection, 57 | 'timeout' => $worker->timeout, 58 | 'delay' => $worker->delay, 59 | 'sleep' => $worker->sleep, 60 | 'tries' => $worker->tries, 61 | 'environment' => $worker->environment, 62 | 'daemon' => (bool) $worker->daemon, 63 | 'force' => (bool) $worker->force, 64 | 'php_version' => str_replace('.', '', head(explode(' ', $worker->command))), 65 | 'processes' => $worker->processes, 66 | ]; 67 | 68 | foreach (array_merge($defaults, $config) as $key => $value) { 69 | if ($forgeWorker[$key] !== $value) { 70 | return false; 71 | } 72 | } 73 | 74 | return true; 75 | } 76 | 77 | protected function getWorkerPayload(Server $server, array $worker): array 78 | { 79 | $cli = collect($this->forge->phpVersions($server->id))->firstWhere('usedOnCli', true)->version; 80 | 81 | return array_merge(Defaults::worker($cli), $worker); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/helpers.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Console\Kernel::class, 31 | LaravelZero\Framework\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Debug\ExceptionHandler::class, 36 | Illuminate\Foundation\Exceptions\Handler::class 37 | ); 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Return The Application 42 | |-------------------------------------------------------------------------- 43 | | 44 | | This script returns the application instance. The instance is given to 45 | | the calling script so we can separate the building of the instances 46 | | from the actual running of the application and sending responses. 47 | | 48 | */ 49 | 50 | return $app; 51 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "directories": [ 4 | "app", 5 | "bootstrap", 6 | "config", 7 | "vendor" 8 | ], 9 | "exclude-dev-files": false, 10 | "files": [ 11 | "composer.json" 12 | ], 13 | "exclude-composer-files": false, 14 | "compression": "GZ", 15 | "compactors": [ 16 | "KevinGH\\Box\\Compactor\\Php", 17 | "KevinGH\\Box\\Compactor\\Json" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /builds/forge: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/forge-cli/b425f471321780b8f921ed114554e6c417f0f2c0/builds/forge -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beyondcode/forge-cli", 3 | "description": "Laravel Forge CLI", 4 | "keywords": ["forge", "laravel", "cli"], 5 | "homepage": "https://beyondco.de/docs/forge-cli", 6 | "type": "project", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Marcel Pociot", 11 | "email": "marcel@beyondco.de" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.3|^8.0" 16 | }, 17 | "require-dev": { 18 | "laravel-zero/framework": "^8.0", 19 | "laravel/forge-sdk": "^3.2", 20 | "mockery/mockery": "^1.4.2", 21 | "nunomaduro/laravel-console-menu": "^3.1", 22 | "phpunit/phpunit": "^9.3", 23 | "symfony/browser-kit": "^5.1", 24 | "symfony/http-client": "^5.1", 25 | "symfony/mime": "^5.1", 26 | "symfony/yaml": "^5.1" 27 | }, 28 | "autoload": { 29 | "files": [ 30 | "app/helpers.php" 31 | ], 32 | "psr-4": { 33 | "App\\": "app/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Tests\\": "tests/" 39 | } 40 | }, 41 | "config": { 42 | "preferred-install": "dist", 43 | "sort-packages": true, 44 | "optimize-autoloader": true 45 | }, 46 | "minimum-stability": "dev", 47 | "prefer-stable": true, 48 | "bin": ["builds/forge"] 49 | } 50 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 'Forge', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Version 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "version" your application is currently running 24 | | in. You may want to follow the "Semantic Versioning" - Given a version 25 | | number MAJOR.MINOR.PATCH when an update happens: https://semver.org. 26 | | 27 | */ 28 | 29 | 'version' => '1.1.0', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Environment 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This value determines the "environment" your application is currently 37 | | running in. This may determine how you prefer to configure various 38 | | services the application utilizes. This can be overridden using 39 | | the global command line "--env" option when calling commands. 40 | | 41 | */ 42 | 43 | 'env' => 'development', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Autoloaded Service Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | The service providers listed here will be automatically loaded on the 51 | | request to your application. Feel free to add your own services to 52 | | this array to grant expanded functionality to your applications. 53 | | 54 | */ 55 | 56 | 'providers' => [ 57 | App\Providers\AppServiceProvider::class, 58 | ], 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /config/commands.php: -------------------------------------------------------------------------------- 1 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Commands Paths 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "paths" that should be loaded by the console's 24 | | kernel. Foreach "path" present on the array provided below the kernel 25 | | will extract all "Illuminate\Console\Command" based class commands. 26 | | 27 | */ 28 | 29 | 'paths' => [app_path('Commands')], 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Added Commands 34 | |-------------------------------------------------------------------------- 35 | | 36 | | You may want to include a single command class without having to load an 37 | | entire folder. Here you can specify which commands should be added to 38 | | your list of commands. The console's kernel will try to load them. 39 | | 40 | */ 41 | 42 | 'add' => [ 43 | // .. 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Hidden Commands 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Your application commands will always be visible on the application list 52 | | of commands. But you can still make them "hidden" specifying an array 53 | | of commands below. All "hidden" commands can still be run/executed. 54 | | 55 | */ 56 | 57 | 'hidden' => [ 58 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 59 | Symfony\Component\Console\Command\HelpCommand::class, 60 | Illuminate\Console\Scheduling\ScheduleRunCommand::class, 61 | Illuminate\Console\Scheduling\ScheduleFinishCommand::class, 62 | Illuminate\Foundation\Console\VendorPublishCommand::class, 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Removed Commands 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Do you have a service provider that loads a list of commands that 71 | | you don't need? No problem. Laravel Zero allows you to specify 72 | | below a list of commands that you don't to see in your app. 73 | | 74 | */ 75 | 76 | 'remove' => [ 77 | // .. 78 | ], 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /config/forge.php: -------------------------------------------------------------------------------- 1 | '', 5 | ]; 6 | -------------------------------------------------------------------------------- /docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | packageName: Forge CLI 3 | githubUrl: https://github.com/beyondcode/forge-cli 4 | --- 5 | -------------------------------------------------------------------------------- /docs/basic-commands/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Commands 3 | order: 3 4 | --- 5 | -------------------------------------------------------------------------------- /docs/basic-commands/info.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Info 3 | order: 3 4 | --- 5 | 6 | # Info 7 | 8 | The `forge info` command will give you a quick overview of the currently linked site on Laravel Forge. 9 | 10 | ![](/img/info.png) 11 | -------------------------------------------------------------------------------- /docs/basic-commands/init.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Linking projects with sites 3 | order: 1 4 | --- 5 | 6 | # Init 7 | 8 | The `forge init` command allows you to link the current working directory with a site and server on Laravel Forge. 9 | 10 | When calling `forge init`, you can choose from an interactive list, which server you want to link the site with. 11 | ![](/img/init_servers.png) 12 | 13 | After selecting the server, you can either go and create a new site on Forge, or link the directory with an already existing site on the selected server: 14 | ![](/img/init_sites.png) 15 | 16 | Once the site is linked/created, Forge CLI will create a file called `forge.yml` in your current working directory. 17 | This file contains the current site (and server) configuration that you have. 18 | 19 | You can safely put this file into version control to later synchronize changes to Forge. 20 | -------------------------------------------------------------------------------- /docs/configuration/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | order: 6 4 | --- 5 | -------------------------------------------------------------------------------- /docs/configuration/pull.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pull configuration from Forge 3 | order: 2 4 | --- 5 | 6 | # Pull configuration from Forge 7 | 8 | Forge CLI works best, if you use your `forge.yml` as the one way to configure and modify your Laravel Forge sites. 9 | 10 | But there will be situations, where you, or someone else, have updated parts of your site configuration directly on Laravel Forge. 11 | 12 | In this case, your local `forge.yml` file might be outdated and no longer contain the current site configuration. 13 | 14 | To reload the configuration from Forge, you can use the `forge config:pull` command. This command will overwrite all local changes in your `forge.yml` file and replace it with the current settings from Forge. 15 | -------------------------------------------------------------------------------- /docs/configuration/push.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Synchronize configuration with Forge 3 | order: 1 4 | --- 5 | 6 | # Synchronize configuration with Forge 7 | 8 | When you made changes to your `forge.yml` file, like modifying the deployment script, adding a webhook manually, or modifying the quick-deploy setting, you need to synchronize these changes with Laravel Forge in order for them to 9 | take effect. 10 | 11 | You can do this by running `forge config:push`. 12 | 13 | This command will read your local `forge.yml` file and synchronize its settings with Laravel Forge. 14 | -------------------------------------------------------------------------------- /docs/daemon/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Daemons 3 | order: 10 4 | --- 5 | -------------------------------------------------------------------------------- /docs/daemon/creating-a-new-daemon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating a new Daemon 3 | order: 2 4 | --- 5 | 6 | # Creating a new Daemon 7 | 8 | You can create a new daemon on the linked Forge server using: `forge daemon`. 9 | 10 | This command will ask you for all the required information to configure your daemon. 11 | 12 | In order to apply the new configuration and install the daemon on your server, use the `forge config:push` command. 13 | -------------------------------------------------------------------------------- /docs/deployments/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployments 3 | order: 4 4 | --- 5 | -------------------------------------------------------------------------------- /docs/deployments/deploy-log.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployment Logs 3 | order: 3 4 | --- 5 | 6 | # Deployment Logs 7 | 8 | The `forge deploy:log` command will show you the latest deployment log for the linked site: 9 | 10 | ![](/img/deploy-log.png) 11 | -------------------------------------------------------------------------------- /docs/deployments/deploy-reset.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reset Deployment State 3 | order: 4 4 | --- 5 | 6 | # Reset Deployment State 7 | 8 | If, for whatever reason, your Forge site is stuck in a deployment state, you can reset this by calling the `forge deploy:reset` command. 9 | -------------------------------------------------------------------------------- /docs/deployments/deploy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Triggering a deployment 3 | order: 2 4 | --- 5 | 6 | # Triggering a deployment 7 | 8 | The `forge deploy` command will trigger a deployment on Laravel Forge for the currently linked site and the given environment. 9 | 10 | You can also provide the `--update-script` option, to automatically update the deployment script on Laravel Forge with the latest deployment script that you have 11 | configured in your forge.yml file. 12 | 13 | After the deployment is done, you will see the latest deployment log output: 14 | 15 | ![](/img/deploy.png) 16 | 17 | If you do not want to wait for the deployment to finish,you can provide the `--no-wait` option. This will only trigger the deployment on Forge, without waiting for the deployment result / deployment log. 18 | -------------------------------------------------------------------------------- /docs/deployments/deployment-script.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployment Script 3 | order: 2 4 | --- 5 | 6 | # Deployment Script 7 | 8 | After linking your project directory with Laravel Forge, your `forge.yml` file will contain the current deployment script that you have configured on Laravel Forge. 9 | 10 | You can modify this script, by manually editing the `forge.yml` file. 11 | 12 | ```yaml 13 | production: 14 | id: 1 15 | name: my-site 16 | server: 1 17 | quick-deploy: false 18 | deployment: 19 | - 'cd /home/forge/my-site' 20 | - 'git pull origin master' 21 | - '$FORGE_COMPOSER install --no-interaction --prefer-dist --optimize-autoloader' 22 | - '' 23 | - '( flock -w 10 9 || exit 1' 24 | - ' echo ''Restarting FPM...''; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock' 25 | - '' 26 | - 'if [ -f artisan ]; then' 27 | - ' $FORGE_PHP artisan migrate --force' 28 | - fi 29 | webhooks: 30 | daemons: 31 | ``` 32 | 33 | In order to apply the changed deployment script, you can either [push the configuration file to Forge](/docs/forge-cli/configuration/push) or [manually trigger a new deployment](/docs/forge-cli/deployments/deploy). 34 | -------------------------------------------------------------------------------- /docs/environment-files/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Environment Files 3 | order: 5 4 | --- 5 | -------------------------------------------------------------------------------- /docs/environment-files/pull.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pull environment files 3 | order: 2 4 | --- 5 | 6 | # Pull environment files 7 | 8 | The `forge env:pull` command allows you to pull the current environment file of your Laravel Forge site to your local filesystem. 9 | 10 | The naming convention is: 11 | 12 | `.env.forge.[ENVIRONMENT]` 13 | 14 | So by running `forge env:pull`, Forge CLI will write the current environment file to `.env.forge.production`. 15 | Running `forge env:pull staging` would create a file called `.env.forge.staging`. 16 | -------------------------------------------------------------------------------- /docs/environment-files/push.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Push environment files 3 | order: 3 4 | --- 5 | 6 | # Push environment files 7 | 8 | Once you have pulled down your environment file using `forge env:pull`, you can push the changes back to Laravel Forge by calling: 9 | 10 | `forge env:push` 11 | 12 | To ensure that you do not accidentally keep an old state of your environment file, Forge CLI asks you if you want to delete the env file after pushing. 13 | -------------------------------------------------------------------------------- /docs/getting-started/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | order: 2 4 | --- 5 | -------------------------------------------------------------------------------- /docs/getting-started/environments.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Environments 3 | order: 4 4 | --- 5 | 6 | # Environments 7 | 8 | Forge CLI allows you to link and manage multiple sites and servers to one working directory. 9 | 10 | This is especially useful, when you have multiple environments for your project (for example staging and production). 11 | 12 | All Forge CLI commands allow you to pass the environment that you want to target as an additional command line argument. 13 | The default environment is always "production". 14 | 15 | Examples: 16 | 17 | ``` 18 | # This will link Forge with a site for the "production" environment 19 | forge init 20 | 21 | # This will link Forge with a site for the "staging" environment 22 | forge init staging 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | order: 1 4 | --- 5 | 6 | # Installation 7 | 8 | Forge CLI can be installed using composer. 9 | The easiest way to install Forge CLI is by making it a global composer dependency: 10 | 11 | ```bash 12 | composer global require beyondcode/forge-cli 13 | ``` 14 | 15 | Now you're ready to go and can [login to Laravel Forge](/docs/forge-cli/getting-started/logging-in). 16 | -------------------------------------------------------------------------------- /docs/getting-started/logging-in.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logging In 3 | order: 2 4 | --- 5 | 6 | # Logging In 7 | 8 | Before you can link your existing Laravel Forge sites to your local directores - or create new sites - you will need to authenticate and login with your 9 | Laravel Forge credentials. 10 | 11 | You can do this via: 12 | 13 | ```shell script 14 | forge login 15 | ``` 16 | 17 | This script will ask you for your Laravel Forge login credentials to create an API token, that will be used for additional requests. 18 | 19 | The API token will be stored in your home directory: `~/forge/config.php`. 20 | 21 | Now you can go and either [create a new site, or link an existing site from Forge](/docs/forge-cli/basic-commands/init). 22 | -------------------------------------------------------------------------------- /docs/getting-started/questions-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Questions & Issues 3 | order: 3 4 | --- 5 | 6 | # Questions and issues 7 | 8 | Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving Forge CLI? Feel free to create an issue on [GitHub](https://github.com/beyondcode/forge-cli/issues), we'll try to address it as soon as possible. 9 | 10 | If you've found a bug regarding security please mail [marcel@beyondco.de](mailto:marcel@beyondco.de) instead of using the issue tracker. 11 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | order: 1 4 | --- 5 | 6 | # Introduction 7 | 8 | Forge CLI is the missing link between Laravel Forge and your local (and version controlled) PHP applications. 9 | 10 | It allows you to keep server and site configurations under version control, and makes it easy to apply changes to your Forge provisioned server and sites. 11 | 12 | ```shell script 13 | forge login 14 | 15 | forge init 16 | 17 | forge deploy 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/logs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logs 3 | order: 7 4 | --- 5 | -------------------------------------------------------------------------------- /docs/logs/reading-logs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reading logs 3 | order: 2 4 | --- 5 | 6 | # Reading logs 7 | 8 | Forge CLI allows you to read your servers log files without having to connect to your server via SSH. 9 | 10 | You can see the Nginx error logs, using: `forge logs`. 11 | 12 | There are multiple available log files that you can access: 13 | 14 | ### Nginx Access Logs 15 | To retrieve the Nginx access logs, pass the `--file=nginx_access` option to the logs command. 16 | 17 | ``` 18 | forge logs --file=nginx_access 19 | ``` 20 | 21 | ### Nginx Error Logs 22 | To retrieve the Nginx error logs, pass the `--file=nginx_error` option to the logs command. 23 | 24 | ``` 25 | forge logs --file=nginx_error 26 | ``` 27 | 28 | ### Database Logs 29 | To retrieve the database logs, pass the `--file=database` option to the logs command. 30 | 31 | ``` 32 | forge logs --file=database 33 | ``` 34 | 35 | ### PHP FPM Logs 36 | To retrieve the PHP FPM logs, pass the `--file=php7x` option to the logs command, where `php7x` is a valid version number. For example: 37 | 38 | ``` 39 | forge logs --file=php74 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/nginx-configuration/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nginx Configuration 3 | order: 11 4 | --- 5 | -------------------------------------------------------------------------------- /docs/nginx-configuration/pull.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pull Nginx config 3 | order: 2 4 | --- 5 | 6 | # Pull Nginx config 7 | 8 | You can pull down the site nginx configuration file using `forge nginx:pull`. 9 | 10 | This will write a file called `nginx-forge-[environment].conf`. 11 | -------------------------------------------------------------------------------- /docs/nginx-configuration/push.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Push Nginx config 3 | order: 1 4 | --- 5 | 6 | # Push Nginx config 7 | 8 | When you made changes to your `nginx-forge-[environment].conf` file, you can update the Nginx configuration by running `forge nginx:push`. 9 | -------------------------------------------------------------------------------- /docs/services/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Services 3 | order: 7 4 | --- 5 | -------------------------------------------------------------------------------- /docs/services/mysql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reboot MySQL 3 | order: 2 4 | --- 5 | 6 | # Reboot MySQL 7 | 8 | Use the command `forge reboot:mysql` to reboot the MySQL server on the linked Forge site for the given environment. 9 | 10 | **IMPORTANT:** Please remember that rebooting the server or services may cause temporary downtime! Running the command will only initiate the reboot process. It is up to you to perform whatever steps are necessary to confirm that the server or service has properly rebooted. 11 | 12 | Every `reboot` command requires confirmation, which you can provide by adding the `--confirm` option. 13 | -------------------------------------------------------------------------------- /docs/services/nginx.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reboot Nginx 3 | order: 3 4 | --- 5 | 6 | # Reboot Nginx 7 | 8 | Use the command `forge reboot:nginx` to reboot the Nginx server on the linked Forge site for the given environment. 9 | 10 | **IMPORTANT:** Please remember that rebooting the server or services may cause temporary downtime! Running the command will only initiate the reboot process. It is up to you to perform whatever steps are necessary to confirm that the server or service has properly rebooted. 11 | 12 | Every `reboot` command requires confirmation, which you can provide by adding the `--confirm` option. 13 | -------------------------------------------------------------------------------- /docs/services/postgres.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reboot Postgres 3 | order: 4 4 | --- 5 | 6 | # Reboot Postgres 7 | 8 | Use the command `forge reboot:nginx` to reboot the Postgres server on the linked Forge site for the given environment. 9 | 10 | **IMPORTANT:** Please remember that rebooting the server or services may cause temporary downtime! Running the command will only initiate the reboot process. It is up to you to perform whatever steps are necessary to confirm that the server or service has properly rebooted. 11 | 12 | Every `reboot` command requires confirmation, which you can provide by adding the `--confirm` option. 13 | -------------------------------------------------------------------------------- /docs/services/server.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reboot Server 3 | order: 5 4 | --- 5 | 6 | # Reboot Server 7 | 8 | Use the command `forge reboot:server` to reboot the entire server that is linked with Laravel Forge. 9 | 10 | **IMPORTANT:** Please remember that rebooting the server or services may cause temporary downtime! Running the command will only initiate the reboot process. It is up to you to perform whatever steps are necessary to confirm that the server or service has properly rebooted. 11 | 12 | Every `reboot` command requires confirmation, which you can provide by adding the `--confirm` option. 13 | -------------------------------------------------------------------------------- /docs/webhooks/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Webhooks 3 | order: 9 4 | --- 5 | -------------------------------------------------------------------------------- /docs/webhooks/creating-a-new-webhook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating a new Webhook 3 | order: 2 4 | --- 5 | 6 | # Creating a new Webhook 7 | 8 | You can create a new webhook on the linked Forge site/server using: `forge webhook`. 9 | 10 | This command will ask you for the URL of the webhook and store it in your `forge.yml` file. 11 | 12 | In order to apply the new configuration, use the `forge config:push` command. 13 | -------------------------------------------------------------------------------- /forge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | ./tests/Unit 17 | 18 | 19 | 20 | 21 | ./app 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/InspiringCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('info') 16 | ->expectsOutput('You have not yet linked this project to Forge.') 17 | ->assertExitCode(1); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |