├── tests ├── server │ └── foobar │ │ ├── current │ │ └── .gitkeep │ │ ├── releases │ │ ├── .gitkeep │ │ ├── 10000000000000 │ │ │ └── .gitkeep │ │ └── 20000000000000 │ │ │ └── .gitkeep │ │ └── shared │ │ └── .gitkeep ├── meta │ ├── deployments.json │ ├── MyCustomTask.php │ └── coverage.txt ├── Tasks │ ├── CheckTest.php │ ├── RollbackTest.php │ ├── TestTest.php │ ├── SetupTest.php │ ├── CurrentReleaseTest.php │ ├── CleanupTest.php │ ├── TeardownTest.php │ ├── DeployTest.php │ ├── IgniteTest.php │ └── UpdateTest.php ├── ConsoleTest.php ├── TasksTest.php ├── ReleasesManagerTest.php ├── ServerTest.php ├── BashTest.php ├── TasksQueueTest.php ├── RocketeerTest.php └── _start.php ├── .gitignore ├── .gitmodules ├── rocketeer ├── CONTRIBUTING.md ├── .travis.yml ├── src ├── Rocketeer │ ├── Console.php │ ├── Facades │ │ ├── Console.php │ │ └── Rocketeer.php │ ├── Tasks │ │ ├── Test.php │ │ ├── Closure.php │ │ ├── Update.php │ │ ├── Ignite.php │ │ ├── Cleanup.php │ │ ├── CurrentRelease.php │ │ ├── Teardown.php │ │ ├── Rollback.php │ │ ├── Setup.php │ │ ├── Deploy.php │ │ └── Check.php │ ├── Commands │ │ ├── DeployCommand.php │ │ ├── DeployTestCommand.php │ │ ├── DeployFlushCommand.php │ │ ├── DeployUpdateCommand.php │ │ ├── DeployDeployCommand.php │ │ ├── BaseTaskCommand.php │ │ ├── DeployRollbackCommand.php │ │ └── BaseDeployCommand.php │ ├── Scm │ │ ├── ScmInterface.php │ │ ├── Git.php │ │ └── Svn.php │ ├── Traits │ │ ├── Scm.php │ │ └── Task.php │ ├── ReleasesManager.php │ ├── Server.php │ ├── RocketeerServiceProvider.php │ ├── Rocketeer.php │ ├── Bash.php │ └── TasksQueue.php └── config │ └── config.php ├── provides.json ├── composer.json ├── phpunit.xml ├── README.md ├── CHANGELOG.md └── composer.lock /tests/server/foobar/current/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/server/foobar/releases/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/server/foobar/shared/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/server/foobar/releases/10000000000000/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/server/foobar/releases/20000000000000/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | storage 3 | tests/coverage 4 | vendor -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs"] 2 | path = docs 3 | url = https://github.com/Anahkiasen/rocketeer.wiki.git 4 | -------------------------------------------------------------------------------- /rocketeer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | pretendTask('Check'); 8 | $task->execute(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/ConsoleTest.php: -------------------------------------------------------------------------------- 1 | assertContains('Rocketeer version 0', $console); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/Tasks/RollbackTest.php: -------------------------------------------------------------------------------- 1 | task('Rollback')->execute(); 8 | 9 | $this->assertEquals(10000000000000, $this->app['rocketeer.releases']->getCurrentRelease()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/Tasks/TestTest.php: -------------------------------------------------------------------------------- 1 | pretendTask('Test')->execute(); 7 | 8 | $this->assertEquals('cd '.$this->server.'/releases/20000000000000', $tests[0]); 9 | $this->assertContains('phpunit --stop-on-failure', $tests[1]); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/Tasks/SetupTest.php: -------------------------------------------------------------------------------- 1 | app['files']->deleteDirectory($this->server); 8 | $this->task('Setup')->execute(); 9 | 10 | $this->assertFileExists($this->server); 11 | $this->assertFileExists($this->server.'/current'); 12 | $this->assertFileExists($this->server.'/releases'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Tasks/CurrentReleaseTest.php: -------------------------------------------------------------------------------- 1 | task('CurrentRelease')->execute(); 8 | $this->assertContains('20000000000000', $current); 9 | 10 | $this->app['rocketeer.server']->setValue('current_release', 0); 11 | $current = $this->task('CurrentRelease')->execute(); 12 | $this->assertEquals('No release has yet been deployed', $current); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Tasks/CleanupTest.php: -------------------------------------------------------------------------------- 1 | task('Cleanup'); 8 | $output = $cleanup->execute(); 9 | 10 | $this->assertFileNotExists($this->server.'/releases/10000000000000'); 11 | $this->assertEquals('Removing 1 release from the server', $output); 12 | 13 | $output = $cleanup->execute(); 14 | $this->assertEquals('No releases to prune from the server', $output); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Rocketeer/Facades/Console.php: -------------------------------------------------------------------------------- 1 | command->info('Testing the application'); 27 | 28 | return $this->runTests(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Tasks/TeardownTest.php: -------------------------------------------------------------------------------- 1 | task('Teardown')->execute(); 8 | 9 | $this->assertFileNotExists($this->deploymentsFile); 10 | $this->assertFileNotExists($this->server); 11 | } 12 | 13 | public function testCanAbortTeardown() 14 | { 15 | $command = Mockery::mock('Command');; 16 | $command->shouldReceive('confirm')->andReturn(false); 17 | $command->shouldReceive('info')->andReturnUsing(function ($message) { return $message; }); 18 | 19 | $message = $this->task('Teardown', $command)->execute(); 20 | 21 | $this->assertEquals('Teardown aborted', $message); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/DeployCommand.php: -------------------------------------------------------------------------------- 1 | option('version')) { 27 | return $this->line('Rocketeer version '.Rocketeer::VERSION.''); 28 | } 29 | 30 | // Deploy 31 | return parent::fire(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/DeployTestCommand.php: -------------------------------------------------------------------------------- 1 | input->setOption('verbose', true); 31 | 32 | return $this->fireTasksQueue('Rocketeer\Tasks\Test'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/DeployFlushCommand.php: -------------------------------------------------------------------------------- 1 | laravel['rocketeer.server']->deleteRepository(); 31 | $this->info("Rocketeer's cache has been properly flushed"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Tasks/DeployTest.php: -------------------------------------------------------------------------------- 1 | app['config']->shouldReceive('get')->with('rocketeer::scm')->andReturn(array( 8 | 'repository' => 'https://github.com/Anahkiasen/rocketeer.git', 9 | 'username' => '', 10 | 'password' => '', 11 | )); 12 | 13 | $this->task('Deploy')->execute(); 14 | $release = $this->app['rocketeer.releases']->getCurrentRelease(); 15 | 16 | $releasePath = $this->server.'/releases/'.$release; 17 | $this->assertFileExists($this->server.'/shared/tests/meta/deployments.json'); 18 | $this->assertFileExists($releasePath); 19 | $this->assertFileExists($releasePath.'/.git'); 20 | $this->assertFileExists($releasePath.'/vendor'); 21 | 22 | $this->recreateVirtualServer(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anahkiasen/rocketeer", 3 | "description": "Rocketeer provides a fast and easy way to deploy your Laravel projects", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "deployment", 8 | "ssh" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Maxime Fabre", 13 | "email": "ehtnam6@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.3.0", 18 | "illuminate/support": "~4", 19 | "illuminate/config": "~4", 20 | "illuminate/console": "~4", 21 | "illuminate/container": "~4", 22 | "illuminate/filesystem": "~4" 23 | }, 24 | "require-dev": { 25 | "mockery/mockery": "dev-master", 26 | "nesbot/carbon": "dev-master", 27 | "patchwork/utf8": "dev-master" 28 | }, 29 | "minimum-stability": "dev", 30 | "bin": [ 31 | "rocketeer" 32 | ], 33 | "autoload": { 34 | "psr-0": { 35 | "Rocketeer": "src/" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Closure.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 27 | } 28 | 29 | /** 30 | * Get the Task's Closure 31 | * 32 | * @return Closure 33 | */ 34 | public function getClosure() 35 | { 36 | return $this->closure; 37 | } 38 | 39 | /** 40 | * Run the Task 41 | * 42 | * @return void 43 | */ 44 | public function execute() 45 | { 46 | $closure = $this->closure; 47 | 48 | return $closure($this); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Rocketeer/Scm/ScmInterface.php: -------------------------------------------------------------------------------- 1 | updateRepository(); 27 | 28 | // Recompile dependencies and stuff 29 | $this->runComposer(); 30 | 31 | // Set permissions 32 | $this->setApplicationPermissions(); 33 | 34 | // Run migrations 35 | if ($this->getOption('migrate')) { 36 | $this->runMigrations($this->getOption('seed')); 37 | } 38 | 39 | // Clear cache 40 | $this->runForCurrentRelease('php artisan cache:clear'); 41 | 42 | $this->command->info('Successfully updated application'); 43 | 44 | return $this->history; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Ignite.php: -------------------------------------------------------------------------------- 1 | app->bound('artisan')) { 27 | return $this->command->call('config:publish', array('package' => 'anahkiasen/rocketeer')); 28 | } 29 | 30 | // Else copy it at the root 31 | $config = __DIR__.'/../../config/config.php'; 32 | $root = $this->app['path.base'].'/rocketeer.php'; 33 | $this->app['files']->copy($config, $root); 34 | 35 | // Display info 36 | $folder = basename(dirname($root)).'/'.basename($root); 37 | $this->command->line('The Rocketeer configuration was created at '.$folder.''); 38 | 39 | return $this->history; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Cleanup.php: -------------------------------------------------------------------------------- 1 | releasesManager->getDeprecatedReleases(); 28 | foreach ($trash as $release) { 29 | $this->removeFolder($this->releasesManager->getPathToRelease($release)); 30 | } 31 | 32 | // If no releases to prune 33 | if (empty($trash)) { 34 | return $this->command->comment('No releases to prune from the server'); 35 | } 36 | 37 | // Create final message 38 | $trash = sizeof($trash); 39 | $message = sprintf('Removing %d %s from the server', $trash, Str::plural('release', $trash)); 40 | 41 | return $this->command->line($message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/CurrentRelease.php: -------------------------------------------------------------------------------- 1 | releasesManager->getCurrentRelease(); 28 | if (!$currentRelease) { 29 | return $this->command->error('No release has yet been deployed'); 30 | } 31 | 32 | // Create message 33 | $date = Carbon::createFromFormat('YmdHis', $currentRelease)->toDateTimeString(); 34 | $state = $this->runForCurrentRelease($this->scm->currentState()); 35 | $message = sprintf('The current release is %s (%s deployed at %s)', $currentRelease, $state, $date); 36 | 37 | return $this->command->line($message); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/DeployUpdateCommand.php: -------------------------------------------------------------------------------- 1 | fireTasksQueue('Rocketeer\Tasks\Update'); 33 | } 34 | 35 | /** 36 | * Get the console command options. 37 | * 38 | * @return array 39 | */ 40 | protected function getOptions() 41 | { 42 | return array_merge(parent::getOptions(), array( 43 | array('migrate', 'm', InputOption::VALUE_NONE, 'Run the migrations'), 44 | array('seed', 's', InputOption::VALUE_NONE, 'Seed the database after migrating the database'), 45 | )); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | src/Rocketeer 23 | 24 | src/Rocketeer/RocketeerServiceProvider.php 25 | src/Rocketeer/Commands 26 | src/Rocketeer/Facades 27 | 28 | 29 | 30 | 31 | 32 | 33 | tests 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Teardown.php: -------------------------------------------------------------------------------- 1 | command->confirm('This will remove all folders on the server, not just releases. Do you want to proceed ?'); 34 | if (!$confirm) { 35 | return $this->command->info('Teardown aborted'); 36 | } 37 | 38 | // Remove remote folders 39 | $this->removeFolder(); 40 | 41 | // Remove deployments file 42 | $this->server->deleteRepository(); 43 | 44 | $this->command->info('The application was successfully removed from the remote servers'); 45 | 46 | return $this->history; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/DeployDeployCommand.php: -------------------------------------------------------------------------------- 1 | fireTasksQueue(array( 33 | 'Rocketeer\Tasks\Deploy', 34 | 'Rocketeer\Tasks\Cleanup', 35 | )); 36 | } 37 | 38 | /** 39 | * Get the console command options. 40 | * 41 | * @return array 42 | */ 43 | protected function getOptions() 44 | { 45 | return array_merge(parent::getOptions(), array( 46 | array('tests', 't', InputOption::VALUE_NONE, 'Runs the tests on deploy'), 47 | array('migrate', 'm', InputOption::VALUE_NONE, 'Run the migrations'), 48 | array('seed', 's', InputOption::VALUE_NONE, 'Seed the database after migrating the database'), 49 | )); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Tasks/IgniteTest.php: -------------------------------------------------------------------------------- 1 | app['artisan'] = null; 8 | $this->app->offsetUnset('artisan'); 9 | 10 | $this->app['path.base'] = __DIR__.'/../..'; 11 | 12 | // Execute Task 13 | $task = $this->task('Ignite'); 14 | $task->execute(); 15 | 16 | $root = $this->app['path.base'].'/rocketeer.php'; 17 | $this->assertFileExists($root); 18 | 19 | $config = include $this->app['path.base'].'/src/config/config.php'; 20 | $contents = include $root; 21 | $this->assertEquals($config, $contents); 22 | } 23 | 24 | public function testCanIgniteConfigurationInLaravel() 25 | { 26 | $this->app['path.base'] = __DIR__.'/../..'; 27 | $root = $this->app['path.base'].'/rocketeer.php'; 28 | 29 | $command = $this->getCommand(); 30 | $command->shouldReceive('call')->with('config:publish', array('package' => 'anahkiasen/rocketeer'))->andReturnUsing(function () use ($root) { 31 | file_put_contents($root, 'foobar'); 32 | }); 33 | 34 | $task = $this->task('Ignite', $command); 35 | $task->execute(); 36 | 37 | $contents = file_get_contents($root); 38 | $this->assertEquals('foobar', $contents); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Tasks/UpdateTest.php: -------------------------------------------------------------------------------- 1 | pretendTask('Update', array( 8 | 'migrate' => true, 9 | 'seed' => true 10 | )); 11 | 12 | $update = $task->execute(); 13 | $composer = exec('which composer'); 14 | 15 | $matcher = array( 16 | array( 17 | "cd " .$this->server. "/releases/20000000000000", 18 | "git reset --hard", 19 | "git pull", 20 | ), 21 | $composer, 22 | array( 23 | "cd " .$this->server. "/releases/20000000000000", 24 | $composer. " install", 25 | ), 26 | array( 27 | "cd " .$this->server. "/releases/20000000000000", 28 | "chmod -R 755 " .$this->server. "/releases/20000000000000/tests", 29 | "chmod -R g+s " .$this->server. "/releases/20000000000000/tests", 30 | "chown -R www-data:www-data " .$this->server. "/releases/20000000000000/tests", 31 | ), 32 | array( 33 | "cd " .$this->server. "/releases/20000000000000", 34 | "php artisan migrate --seed", 35 | ), 36 | array( 37 | "cd " .$this->server. "/releases/20000000000000", 38 | "php artisan cache:clear", 39 | ), 40 | ); 41 | 42 | $this->assertEquals($matcher, $update); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Rocketeer/Traits/Scm.php: -------------------------------------------------------------------------------- 1 | app = $app; 24 | } 25 | 26 | //////////////////////////////////////////////////////////////////// 27 | //////////////////////////////// HELPERS /////////////////////////// 28 | //////////////////////////////////////////////////////////////////// 29 | 30 | /** 31 | * Returns a command with the SCM's binary 32 | * 33 | * @param string $command 34 | * 35 | * @return string 36 | */ 37 | public function getCommand($command) 38 | { 39 | return $this->binary. ' ' .$command; 40 | } 41 | 42 | /** 43 | * Execute one of the commands 44 | * 45 | * @param string $command 46 | * @param string $arguments,... 47 | * 48 | * @return mixed 49 | */ 50 | public function execute() 51 | { 52 | $arguments = func_get_args(); 53 | $command = array_shift($arguments); 54 | $command = call_user_func_array(array($this, $command), $arguments); 55 | 56 | return $this->app['rocketeer.bash']->run($command); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/TasksTest.php: -------------------------------------------------------------------------------- 1 | task->runForCurrentRelease('git init'); 8 | $this->task->updateRepository(); 9 | $output = $this->task->run('git status'); 10 | 11 | $this->assertContains('working directory clean', $output); 12 | } 13 | 14 | public function testCanDisplayOutputOfCommandsIfVerbose() 15 | { 16 | $task = $this->pretendTask('Check', array( 17 | 'verbose' => true, 18 | 'pretend' => false 19 | )); 20 | 21 | ob_start(); 22 | $task->run('ls'); 23 | $output = ob_get_clean(); 24 | 25 | $this->assertContains('tests', $output); 26 | } 27 | 28 | public function testCanPretendToRunTasks() 29 | { 30 | $task = $this->pretendTask(); 31 | 32 | $output = $task->run('ls'); 33 | $this->assertEquals('ls', $output); 34 | } 35 | 36 | public function testCanGetDescription() 37 | { 38 | $task = $this->task('Setup'); 39 | 40 | $this->assertNotNull($task->getDescription()); 41 | } 42 | 43 | public function testCanRunMigrations() 44 | { 45 | $task = $this->pretendTask(); 46 | 47 | $commands = $task->runMigrations(); 48 | $this->assertEquals('php artisan migrate', $commands[1]); 49 | 50 | $commands = $task->runMigrations(true); 51 | $this->assertEquals('php artisan migrate --seed', $commands[1]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/BaseTaskCommand.php: -------------------------------------------------------------------------------- 1 | task = $task; 38 | $this->task->command = $this; 39 | 40 | // Set name 41 | $this->name = $name ?: $task->getSlug(); 42 | $this->name = 'deploy:'.$this->name; 43 | 44 | // Set description 45 | $this->setDescription($task->getDescription()); 46 | } 47 | 48 | /** 49 | * Fire the custom Task 50 | * 51 | * @return string 52 | */ 53 | public function fire() 54 | { 55 | return $this->fireTasksQueue($this->task); 56 | } 57 | 58 | /** 59 | * Get the Task this command executes 60 | * 61 | * @return Task 62 | */ 63 | public function getTask() 64 | { 65 | return $this->task; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/DeployRollbackCommand.php: -------------------------------------------------------------------------------- 1 | fireTasksQueue('Rocketeer\Tasks\Rollback'); 34 | } 35 | 36 | /** 37 | * Get the console command arguments. 38 | * 39 | * @return array 40 | */ 41 | protected function getArguments() 42 | { 43 | return array( 44 | array('release', InputArgument::OPTIONAL, 'The release to rollback to'), 45 | ); 46 | } 47 | 48 | /** 49 | * Get the console command options. 50 | * 51 | * @return array 52 | */ 53 | protected function getOptions() 54 | { 55 | return array_merge(parent::getOptions(), array( 56 | array('list', 'L', InputOption::VALUE_NONE, 'Shows the available release to rollbacl to'), 57 | )); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/meta/coverage.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Code Coverage Report 4 | 2013-10-19 14:44:37 5 | 6 | Summary: 7 | Classes: 60.00% (12/20) 8 | Methods: 86.99% (107/123) 9 | Lines: 90.18% (588/652) 10 | 11 | \Rocketeer::Bash 12 | Methods: 100.00% (17/17) Lines: 93.52% (101/108) 13 | \Rocketeer::ReleasesManager 14 | Methods: 100.00% ( 9/ 9) Lines: 100.00% ( 22/ 22) 15 | \Rocketeer::Rocketeer 16 | Methods: 100.00% (22/22) Lines: 100.00% ( 97/ 97) 17 | \Rocketeer::Server 18 | Methods: 100.00% (10/10) Lines: 91.11% ( 41/ 45) 19 | \Rocketeer::TasksQueue 20 | Methods: 100.00% (17/17) Lines: 95.73% (112/117) 21 | \Rocketeer\Scm::Git 22 | Methods: 100.00% ( 6/ 6) Lines: 100.00% ( 8/ 8) 23 | \Rocketeer\Tasks::Check 24 | Methods: 100.00% ( 7/ 7) Lines: 66.67% ( 36/ 54) 25 | \Rocketeer\Tasks::Cleanup 26 | Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 9/ 9) 27 | \Rocketeer\Tasks::Closure 28 | Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 5/ 5) 29 | \Rocketeer\Tasks::CurrentRelease 30 | Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 7/ 7) 31 | \Rocketeer\Tasks::Deploy 32 | Methods: 100.00% ( 4/ 4) Lines: 63.89% ( 23/ 36) 33 | \Rocketeer\Tasks::Ignite 34 | Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 8/ 8) 35 | \Rocketeer\Tasks::Rollback 36 | Methods: 100.00% ( 2/ 2) Lines: 50.00% ( 10/ 20) 37 | \Rocketeer\Tasks::Setup 38 | Methods: 100.00% ( 2/ 2) Lines: 88.89% ( 24/ 27) 39 | \Rocketeer\Tasks::Teardown 40 | Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 7/ 7) 41 | \Rocketeer\Tasks::Test 42 | Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 2/ 2) 43 | \Rocketeer\Tasks::Update 44 | Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 9/ 9) 45 | \Rocketeer\Traits::Scm 46 | Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 7/ 7) 47 | \Rocketeer\Traits::Task 48 | Methods: 93.75% (15/16) Lines: 94.81% ( 73/ 77) 49 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Rollback.php: -------------------------------------------------------------------------------- 1 | getRollbackRelease(); 20 | 21 | // If no release specified, display the available ones 22 | if ($this->command->option('list')) { 23 | $releases = $this->releasesManager->getReleases(); 24 | $this->command->info('Here are the available releases :'); 25 | 26 | foreach ($releases as $key => $name) { 27 | $name = DateTime::createFromFormat('YmdHis', $name); 28 | $name = $name->format('Y-m-d H:i:s'); 29 | 30 | $this->command->comment(sprintf('[%d] %s', $key, $name)); 31 | } 32 | 33 | // Get actual release name from date 34 | $rollbackRelease = $this->command->ask('Which one do you want to go back to ? (0)', 0); 35 | $rollbackRelease = $releases[$rollbackRelease]; 36 | } 37 | 38 | // Rollback release 39 | $this->command->info('Rolling back to release '.$rollbackRelease); 40 | $this->updateSymlink($rollbackRelease); 41 | 42 | return $this->history; 43 | } 44 | 45 | //////////////////////////////////////////////////////////////////// 46 | /////////////////////////////// HELPERS //////////////////////////// 47 | //////////////////////////////////////////////////////////////////// 48 | 49 | /** 50 | * Get the release to rollback to 51 | * 52 | * @return integer 53 | */ 54 | protected function getRollbackRelease() 55 | { 56 | $release = $this->command->argument('release'); 57 | if (!$release) { 58 | $release = $this->releasesManager->getPreviousRelease(); 59 | } 60 | 61 | return $release; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/ReleasesManagerTest.php: -------------------------------------------------------------------------------- 1 | app['rocketeer.releases']->getCurrentRelease(); 9 | 10 | $this->assertEquals(20000000000000, $currentRelease); 11 | } 12 | 13 | public function testCanGetReleasesPath() 14 | { 15 | $releasePath = $this->app['rocketeer.releases']->getReleasesPath(); 16 | 17 | $this->assertEquals($this->server.'/releases', $releasePath); 18 | } 19 | 20 | public function testCanGetCurrentReleaseFolder() 21 | { 22 | $currentReleasePath = $this->app['rocketeer.releases']->getCurrentReleasePath(); 23 | 24 | $this->assertEquals($this->server.'/releases/20000000000000', $currentReleasePath); 25 | } 26 | 27 | public function testCanGetReleases() 28 | { 29 | $releases = $this->app['rocketeer.releases']->getReleases(); 30 | 31 | $this->assertEquals(array(1 => 10000000000000, 0 => 20000000000000), $releases); 32 | } 33 | 34 | public function testCanGetDeprecatedReleases() 35 | { 36 | $releases = $this->app['rocketeer.releases']->getDeprecatedReleases(); 37 | 38 | $this->assertEquals(array(10000000000000), $releases); 39 | } 40 | 41 | public function testCanGetPreviousRelease() 42 | { 43 | $currentRelease = $this->app['rocketeer.releases']->getPreviousRelease(); 44 | 45 | $this->assertEquals(10000000000000, $currentRelease); 46 | } 47 | 48 | public function testCanUpdateCurrentRelease() 49 | { 50 | $this->app['rocketeer.releases']->updateCurrentRelease(30000000000000); 51 | 52 | $this->assertEquals(30000000000000, $this->app['rocketeer.server']->getValue('current_release')); 53 | } 54 | 55 | public function testCanGetFolderInRelease() 56 | { 57 | $folder = $this->app['rocketeer.releases']->getCurrentReleasePath('{path.storage}'); 58 | 59 | $this->assertEquals($this->server.'/releases/20000000000000/app/storage', $folder); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/ServerTest.php: -------------------------------------------------------------------------------- 1 | app['path.storage'] = null; 13 | $this->app->offsetUnset('path.storage'); 14 | 15 | new Rocketeer\Server($this->app); 16 | 17 | $storage = __DIR__.'/../storage'; 18 | $exists = file_exists($storage); 19 | $this->app['files']->deleteDirectory($storage); 20 | $this->assertTrue($exists); 21 | } 22 | 23 | public function testCanGetValueFromDeploymentsFile() 24 | { 25 | $this->assertEquals('bar', $this->app['rocketeer.server']->getValue('foo')); 26 | } 27 | 28 | public function testCanSetValueInDeploymentsFile() 29 | { 30 | $this->app['rocketeer.server']->setValue('foo', 'baz'); 31 | 32 | $this->assertEquals('baz', $this->app['rocketeer.server']->getValue('foo')); 33 | } 34 | 35 | public function testCandeleteRepository() 36 | { 37 | $this->app['rocketeer.server']->deleteRepository(); 38 | 39 | $this->assertFalse($this->app['files']->exists(__DIR__.'/meta/deployments.json')); 40 | } 41 | 42 | public function testCanFallbackIfFileDoesntExist() 43 | { 44 | $this->app['rocketeer.server']->deleteRepository(); 45 | 46 | $this->assertEquals(null, $this->app['rocketeer.server']->getValue('foo')); 47 | } 48 | 49 | public function testCanGetLineEndings() 50 | { 51 | $this->app['rocketeer.server']->deleteRepository(); 52 | 53 | $this->assertEquals(PHP_EOL, $this->app['rocketeer.server']->getLineEndings()); 54 | } 55 | 56 | public function testCanGetSeparators() 57 | { 58 | $this->app['rocketeer.server']->deleteRepository(); 59 | 60 | $this->assertEquals(DIRECTORY_SEPARATOR, $this->app['rocketeer.server']->getSeparator()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Rocketeer/Scm/Git.php: -------------------------------------------------------------------------------- 1 | getCommand('--version'); 30 | } 31 | 32 | /** 33 | * Get the current state 34 | * 35 | * @return string 36 | */ 37 | public function currentState() 38 | { 39 | return $this->getCommand('rev-parse HEAD'); 40 | } 41 | 42 | /** 43 | * Get the current branch 44 | * 45 | * @return string 46 | */ 47 | public function currentBranch() 48 | { 49 | return $this->getCommand('rev-parse --abbrev-ref HEAD'); 50 | } 51 | 52 | //////////////////////////////////////////////////////////////////// 53 | /////////////////////////////// ACTIONS //////////////////////////// 54 | //////////////////////////////////////////////////////////////////// 55 | 56 | /** 57 | * Clone a repository 58 | * 59 | * @param string $destination 60 | * 61 | * @return string 62 | */ 63 | public function checkout($destination) 64 | { 65 | $branch = $this->app['rocketeer.rocketeer']->getRepositoryBranch(); 66 | $repository = $this->app['rocketeer.rocketeer']->getRepository(); 67 | 68 | return sprintf($this->getCommand('clone --depth 1 -b %s %s %s'), $branch, $repository, $destination); 69 | } 70 | 71 | /** 72 | * Resets the repository 73 | * 74 | * @return string 75 | */ 76 | public function reset() 77 | { 78 | return $this->getCommand('reset --hard'); 79 | } 80 | 81 | /** 82 | * Updates the repository 83 | * 84 | * @return string 85 | */ 86 | public function update() 87 | { 88 | return $this->getCommand('pull'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Setup.php: -------------------------------------------------------------------------------- 1 | executeTask('Check')) { 34 | return false; 35 | } 36 | 37 | // Remove existing installation 38 | if ($this->isSetup()) { 39 | $this->executeTask('Teardown'); 40 | } 41 | 42 | // Create base folder 43 | $this->createFolder(); 44 | $this->createStages(); 45 | 46 | // Set setup to true 47 | $this->server->setValue('is_setup', true); 48 | 49 | // Get server informations 50 | $this->command->comment('Getting some informations about the server'); 51 | $this->server->getSeparator(); 52 | $this->server->getLineEndings(); 53 | 54 | // Create confirmation message 55 | $application = $this->rocketeer->getApplicationName(); 56 | $homeFolder = $this->rocketeer->getHomeFolder(); 57 | $this->command->info(sprintf('Successfully setup "%s" at "%s"', $application, $homeFolder)); 58 | 59 | return $this->history; 60 | } 61 | 62 | //////////////////////////////////////////////////////////////////// 63 | /////////////////////////////// HELPERS //////////////////////////// 64 | //////////////////////////////////////////////////////////////////// 65 | 66 | /** 67 | * Create the Application's folders 68 | * 69 | * @return void 70 | */ 71 | protected function createStages() 72 | { 73 | // Get stages 74 | $stages = $this->rocketeer->getStages(); 75 | if (empty($stages)) { 76 | $stages = array(null); 77 | } 78 | 79 | // Create folders 80 | foreach ($stages as $stage) { 81 | $this->rocketeer->setStage($stage); 82 | $this->createFolder('releases', true); 83 | $this->createFolder('current', true); 84 | $this->createFolder('shared', true); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Deploy.php: -------------------------------------------------------------------------------- 1 | isSetup()) { 20 | $this->command->error('Server is not ready, running Setup task'); 21 | $this->executeTask('Setup'); 22 | } 23 | 24 | // Update current release 25 | $release = date('YmdHis'); 26 | $this->releasesManager->updateCurrentRelease($release); 27 | 28 | // Clone Git repository 29 | if (!$this->cloneRepository()) { 30 | return $this->cancel(); 31 | } 32 | 33 | // Run Composer 34 | if (!$this->runComposer()) { 35 | return $this->cancel(); 36 | } 37 | 38 | // Run tests 39 | if ($this->getOption('tests')) { 40 | if (!$this->runTests()) { 41 | $this->command->error('Tests failed'); 42 | return $this->cancel(); 43 | } 44 | } 45 | 46 | // Set permissions 47 | $this->setApplicationPermissions(); 48 | 49 | // Run migrations 50 | if ($this->getOption('migrate')) { 51 | $this->runMigrations($this->getOption('seed')); 52 | } 53 | 54 | // Synchronize shared folders and files 55 | $this->syncSharedFolders(); 56 | 57 | // Update symlink 58 | $this->updateSymlink(); 59 | 60 | $this->command->info('Successfully deployed release '.$release); 61 | 62 | return $this->history; 63 | } 64 | 65 | //////////////////////////////////////////////////////////////////// 66 | /////////////////////////////// HELPERS //////////////////////////// 67 | //////////////////////////////////////////////////////////////////// 68 | 69 | /** 70 | * Cancel deploy 71 | * 72 | * @return false 73 | */ 74 | protected function cancel() 75 | { 76 | $this->executeTask('Rollback'); 77 | 78 | return false; 79 | } 80 | 81 | /** 82 | * Sync the requested folders and files 83 | * 84 | * @return void 85 | */ 86 | protected function syncSharedFolders() 87 | { 88 | $shared = (array) $this->rocketeer->getOption('remote.shared'); 89 | foreach ($shared as $file) { 90 | $this->share($file); 91 | } 92 | } 93 | 94 | /** 95 | * Set permissions for the folders used by the application 96 | * 97 | * @return void 98 | */ 99 | protected function setApplicationPermissions() 100 | { 101 | $files = (array) $this->rocketeer->getOption('remote.permissions.files'); 102 | foreach ($files as $file) { 103 | $this->setPermissions($file); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Rocketeer/Scm/Svn.php: -------------------------------------------------------------------------------- 1 | getCommand('--version'); 30 | } 31 | 32 | /** 33 | * Get the current state 34 | * 35 | * @return string 36 | */ 37 | public function currentState() 38 | { 39 | return $this->getCommand('info -r "HEAD" | grep "Revision"'); 40 | } 41 | 42 | /** 43 | * Get the current branch 44 | * 45 | * @return string 46 | */ 47 | public function currentBranch() 48 | { 49 | return 'echo trunk'; 50 | } 51 | 52 | //////////////////////////////////////////////////////////////////// 53 | /////////////////////////////// ACTIONS //////////////////////////// 54 | //////////////////////////////////////////////////////////////////// 55 | 56 | /** 57 | * Clone a repository 58 | * 59 | * @param string $destination 60 | * 61 | * @return string 62 | */ 63 | public function checkout($destination) 64 | { 65 | $branch = $this->app['rocketeer.rocketeer']->getRepositoryBranch(); 66 | $repository = $this->app['rocketeer.rocketeer']->getRepository(); 67 | 68 | return sprintf( 69 | $this->getCommand('co %s %s %s'), 70 | $this->getCredentials(), 71 | rtrim($repository, '/') . '/' . ltrim($branch, '/'), 72 | $destination 73 | ); 74 | } 75 | 76 | /** 77 | * Resets the repository 78 | * 79 | * @return string 80 | */ 81 | public function reset() 82 | { 83 | $cmd = 'status -q | grep -v \'^[~XI ]\' | awk \'{print $2;}\' | xargs %s revert'; 84 | 85 | return $this->getCommand(sprintf($cmd, $this->binary)); 86 | } 87 | 88 | /** 89 | * Updates the repository 90 | * 91 | * @return string 92 | */ 93 | public function update() 94 | { 95 | return sprintf($this->getCommand('up %s'), $this->getCredentials()); 96 | } 97 | 98 | /** 99 | * Return credential options 100 | * 101 | * @return string 102 | */ 103 | protected function getCredentials() 104 | { 105 | $options = array('--non-interactive'); 106 | $credentials = $this->app['rocketeer.rocketeer']->getCredentials(); 107 | 108 | // Build command 109 | if ($user = array_get($credentials, 'username')) { 110 | $options[] = '--username=' . $user; 111 | } 112 | if ($pass = array_get($credentials, 'password')) { 113 | $options[] = '--password=' . $pass; 114 | } 115 | 116 | return implode(' ', $options); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/BashTest.php: -------------------------------------------------------------------------------- 1 | app['rocketeer.server']->setValue('paths.composer', 'foobar'); 7 | 8 | $this->assertEquals('foobar', $this->task->which('composer')); 9 | } 10 | 11 | public function testCanGetBinary() 12 | { 13 | $whichGrep = exec('which grep'); 14 | $grep = $this->task->which('grep'); 15 | 16 | $this->assertEquals($whichGrep, $grep); 17 | } 18 | 19 | public function testCanGetFallbackForBinary() 20 | { 21 | $whichGrep = exec('which grep'); 22 | $grep = $this->task->which('foobar', $whichGrep); 23 | 24 | $this->assertEquals($whichGrep, $grep); 25 | $this->assertFalse($this->task->which('fdsf')); 26 | } 27 | 28 | public function testCanGetArraysFromRawCommands() 29 | { 30 | $contents = $this->task->runRaw('ls', true); 31 | 32 | $this->assertCount(12, $contents); 33 | } 34 | 35 | public function testCanListContentsOfAFolder() 36 | { 37 | $contents = $this->task->listContents($this->server); 38 | 39 | $this->assertEquals(array('current', 'releases', 'shared'), $contents); 40 | } 41 | 42 | public function testCanCheckIfFileExists() 43 | { 44 | $this->assertTrue($this->task->fileExists($this->server)); 45 | $this->assertFalse($this->task->fileExists($this->server.'/nope')); 46 | } 47 | 48 | public function testCanCheckStatusOfACommand() 49 | { 50 | $this->task->remote = clone $this->getRemote()->shouldReceive('status')->andReturn(1)->mock(); 51 | ob_start(); 52 | $status = $this->task->checkStatus(null, 'error'); 53 | $output = ob_get_clean(); 54 | $this->assertEquals('error'.PHP_EOL, $output); 55 | $this->assertFalse($status); 56 | 57 | $this->task->remote = clone $this->getRemote()->shouldReceive('status')->andReturn(0)->mock(); 58 | $status = $this->task->checkStatus(null); 59 | $this->assertNull($status); 60 | } 61 | 62 | public function testCanForgetCredentialsIfInvalid() 63 | { 64 | $this->app['rocketeer.server']->setValue('credentials', array( 65 | 'repository' => 'https://Anahkiasen@bitbucket.org/Anahkiasen/registry.git', 66 | 'username' => 'Anahkiasen', 67 | 'password' => 'baz', 68 | )); 69 | 70 | // Create fake remote 71 | $remote = clone $this->getRemote(); 72 | $remote->shouldReceive('status')->andReturn(1); 73 | $task = $this->task(); 74 | $task->remote = $remote; 75 | 76 | $task->cloneRepository($this->server.'/test'); 77 | $this->assertNull($this->app['rocketeer.server']->getValue('credentials')); 78 | } 79 | 80 | public function testCancelsSymlinkForUnexistingFolders() 81 | { 82 | $task = $this->pretendTask(); 83 | $folder = '{path.storage}/logs'; 84 | $share = $task->share($folder); 85 | 86 | $this->assertFalse($share); 87 | } 88 | 89 | public function testCanSymlinkFolders() 90 | { 91 | // Create dummy file 92 | $folder = $this->server.'/releases/20000000000000/src'; 93 | mkdir($folder); 94 | file_put_contents($folder.'/foobar.txt', 'test'); 95 | 96 | $task = $this->pretendTask(); 97 | $folder = '{path.base}/foobar.txt'; 98 | $share = $task->share($folder); 99 | $matcher = sprintf('ln -s %s %s', $this->server.'/shared//src/foobar.txt', $this->server.'/releases/20000000000000//src/foobar.txt'); 100 | 101 | $this->assertEquals($matcher, $share); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Rocketeer/ReleasesManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 26 | } 27 | 28 | //////////////////////////////////////////////////////////////////// 29 | /////////////////////////////// RELEASES /////////////////////////// 30 | //////////////////////////////////////////////////////////////////// 31 | 32 | /** 33 | * Get all the releases on the server 34 | * 35 | * @return array 36 | */ 37 | public function getReleases() 38 | { 39 | // Get releases on server 40 | $releases = $this->app['rocketeer.bash']->listContents($this->getReleasesPath()); 41 | rsort($releases); 42 | 43 | return $releases; 44 | } 45 | 46 | /** 47 | * Get an array of deprecated releases 48 | * 49 | * @return array 50 | */ 51 | public function getDeprecatedReleases() 52 | { 53 | $releases = $this->getReleases(); 54 | $maxReleases = $this->app['config']->get('rocketeer::remote.keep_releases'); 55 | 56 | return array_slice($releases, $maxReleases); 57 | } 58 | 59 | //////////////////////////////////////////////////////////////////// 60 | ////////////////////////////// PATHS /////////////////////////////// 61 | //////////////////////////////////////////////////////////////////// 62 | 63 | /** 64 | * Get the path to the releases folder 65 | * 66 | * @return string 67 | */ 68 | public function getReleasesPath() 69 | { 70 | return $this->app['rocketeer.rocketeer']->getFolder('releases'); 71 | } 72 | 73 | /** 74 | * Get the path to a release 75 | * 76 | * @param integer $release 77 | * 78 | * @return string 79 | */ 80 | public function getPathToRelease($release) 81 | { 82 | return $this->app['rocketeer.rocketeer']->getFolder('releases/'.$release); 83 | } 84 | 85 | /** 86 | * Get the path to the current release 87 | * 88 | * @param string $folder A folder in the release 89 | * 90 | * @return string 91 | */ 92 | public function getCurrentReleasePath($folder = null) 93 | { 94 | if ($folder) { 95 | $folder = '/'.$folder; 96 | } 97 | 98 | return $this->getPathToRelease($this->getCurrentRelease().$folder); 99 | } 100 | 101 | //////////////////////////////////////////////////////////////////// 102 | /////////////////////////// CURRENT RELEASE //////////////////////// 103 | //////////////////////////////////////////////////////////////////// 104 | 105 | /** 106 | * Get the current release 107 | * 108 | * @return string 109 | */ 110 | public function getCurrentRelease() 111 | { 112 | return $this->app['rocketeer.server']->getValue('current_release'); 113 | } 114 | 115 | /** 116 | * Get the release before the current one 117 | * 118 | * @param string $release A release name 119 | * 120 | * @return string 121 | */ 122 | public function getPreviousRelease($release = null) 123 | { 124 | // Get all releases and the current one 125 | $releases = $this->getReleases(); 126 | $current = $release ?: $this->getCurrentRelease(); 127 | 128 | // Get the one before that, or default to current 129 | $key = array_search($current, $releases); 130 | $release = array_get($releases, $key + 1, $current); 131 | 132 | return $release; 133 | } 134 | 135 | /** 136 | * Update the current release 137 | * 138 | * @param string $release A release name 139 | * 140 | * @return void 141 | */ 142 | public function updateCurrentRelease($release) 143 | { 144 | $this->app['rocketeer.server']->setValue('current_release', $release); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rocketeer 2 | 3 | [![Build Status](https://travis-ci.org/Anahkiasen/rocketeer.png?branch=master)](https://travis-ci.org/Anahkiasen/rocketeer) 4 | [![Latest Stable Version](https://poser.pugx.org/anahkiasen/rocketeer/v/stable.png)](https://packagist.org/packages/anahkiasen/rocketeer) 5 | [![Total Downloads](https://poser.pugx.org/anahkiasen/rocketeer/downloads.png)](https://packagist.org/packages/anahkiasen/rocketeer) 6 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/Anahkiasen/rocketeer/badges/quality-score.png?s=20d9a4be6695b7677c427eab73151c1a9d803044)](https://scrutinizer-ci.com/g/Anahkiasen/rocketeer/) 7 | [![Code Coverage](https://scrutinizer-ci.com/g/Anahkiasen/rocketeer/badges/coverage.png?s=f6e022cbcf1a51f82b5d9e6fb30bd1643fc70e76)](https://scrutinizer-ci.com/g/Anahkiasen/rocketeer/) 8 | 9 | Rocketeer provides a fast and easy way to set-up and deploy your Laravel projects. **Rocketeer requires Laravel 4.1 as it uses the new _illuminate/remote_ component**. 10 | It can be used on Laravel 4.0 but requires a tiny-bit more setup, see the getting started guide for more informations. 11 | 12 | ## Using Rocketeer 13 | 14 | I recommend you checkout this [Getting Started](https://github.com/Anahkiasen/rocketeer/wiki/Getting-started) guide before anything. It will get you quickly set up to use Rocketeer. 15 | 16 | The available commands in Rocketeer are : 17 | 18 | ``` 19 | deploy 20 | deploy:check Check if the server is ready to receive the application 21 | deploy:cleanup Clean up old releases from the server 22 | deploy:current Display what the current release is 23 | deploy:deploy Deploy the website. 24 | deploy:rollback Rollback to the previous release, or to a specific one 25 | deploy:rollback {release} Rollback to a specific release 26 | deploy:setup Set up the remote server for deployment 27 | deploy:teardown Remove the remote applications and existing caches 28 | deploy:test Run the tests on the server and displays the output 29 | deploy:update Update the remote server without doing a new release 30 | ``` 31 | 32 | ## Tasks 33 | 34 | An important concept in Rocketeer is Tasks : most of the commands you see right above are using predefined Tasks underneath : **Rocketeer\Tasks\Setup**, **Rocketeer\Tasks\Deploy**, etc. 35 | Now, the core of Rocketeer is you can hook into any of those Tasks to perform additional actions, for this you'll use the `before` and `after` arrays of Rocketeer's config file. 36 | 37 | You can read more about Tasks and what you can do with them [in the wiki](https://github.com/Anahkiasen/rocketeer/wiki/Tasks). 38 | 39 | ## Why not Capistrano ? 40 | 41 | That's a question that's been asked to me, why not simply use Capistrano ? I've used Capistrano in the past, it does everything you want it to do, that's a given. 42 | 43 | But, it remains a Ruby package and one that's tightly coupled to Rails in some ways; Rocketeer makes it so that you don't have Ruby files hanging around your app. That way you configure it once and can use it wherever you want in the realm of Laravel, even outside of the deploy routine. 44 | It's also meant to be a lot easier to comprehend, for first-time users or novices, Capistrano is a lot to take at once – Rocketeer aims to be as simple as possible by providing smart defaults and speeding up the time between installing it and first hitting `deploy`. 45 | 46 | It's also more thought out for the PHP world – although you can configure Capistrano to run Composer and PHPUnit, that's not something it expects from the get go, while those tasks that are a part of every Laravel developer are integrated in Rocketeer's core deploy process. 47 | 48 | ## Table of contents 49 | 50 | - **[Getting Started](https://github.com/Anahkiasen/rocketeer/wiki/Getting-started)** 51 | - **[Tasks](https://github.com/Anahkiasen/rocketeer/wiki/Tasks)** 52 | - **[Architecture](https://github.com/Anahkiasen/rocketeer/wiki/Architecture)** -------------------------------------------------------------------------------- /src/Rocketeer/Tasks/Check.php: -------------------------------------------------------------------------------- 1 | checkScm()) { 37 | $errors[] = $this->command->error($this->scm->binary . ' could not be found on the server'); 38 | } 39 | 40 | // Check PHP 41 | if (!$this->checkPhpVersion()) { 42 | $errors[] = $this->command->error('The version of PHP on the server does not match Laravel\'s requirements'); 43 | } 44 | 45 | // Check MCrypt 46 | if (!$this->checkPhpExtension('mcrypt')) { 47 | $errors[] = $this->command->error(sprintf($extension, 'mcrypt')); 48 | } 49 | 50 | // Check Composer 51 | if (!$this->checkComposer()) { 52 | $errors[] = $this->command->error('Composer does not seem to be present on the server'); 53 | } 54 | 55 | // Check database 56 | $database = $this->app['config']->get('database.default'); 57 | if (!$this->checkDatabaseExtension($database)) { 58 | $errors[] = $this->command->error(sprintf($extension, $database)); 59 | } 60 | 61 | // Check Cache/Session driver 62 | $cache = $this->app['config']->get('cache.driver'); 63 | $session = $this->app['config']->get('session.driver'); 64 | if (!$this->checkCacheDriver($cache) or !$this->checkCacheDriver($session)) { 65 | $errors[] = $this->command->error(sprintf($extension, $cache)); 66 | } 67 | 68 | // Return false if any error 69 | if (!empty($errors)) { 70 | return false; 71 | } 72 | 73 | // Display confirmation message 74 | $this->command->info('Your server is ready to deploy'); 75 | 76 | return true; 77 | } 78 | 79 | //////////////////////////////////////////////////////////////////// 80 | /////////////////////////////// HELPERS //////////////////////////// 81 | //////////////////////////////////////////////////////////////////// 82 | 83 | /** 84 | * Check the presence of an SCM on the server 85 | * 86 | * @return boolean 87 | */ 88 | public function checkScm() 89 | { 90 | $this->command->comment('Checking presence of '.$this->scm->binary); 91 | $this->scm->execute('check'); 92 | 93 | return $this->remote->status() == 0; 94 | } 95 | 96 | /** 97 | * Check if Composer is on the server 98 | * 99 | * @return boolean 100 | */ 101 | public function checkComposer() 102 | { 103 | $this->command->comment('Checking presence of Composer'); 104 | 105 | return $this->getComposer(); 106 | } 107 | 108 | /** 109 | * Check if the server is ready to support PHP 110 | * 111 | * @return boolean 112 | */ 113 | public function checkPhpVersion() 114 | { 115 | $this->command->comment('Checking PHP version'); 116 | $version = $this->run('php -r "print PHP_VERSION;"'); 117 | 118 | return version_compare($version, '5.3.7', '>='); 119 | } 120 | 121 | /** 122 | * Check the presence of the correct database PHP extension 123 | * 124 | * @param string $database 125 | * 126 | * @return boolean 127 | */ 128 | public function checkDatabaseExtension($database) 129 | { 130 | switch ($database) { 131 | case 'sqlite': 132 | return $this->checkPhpExtension('pdo_sqlite'); 133 | 134 | case 'mysql': 135 | return $this->checkPhpExtension('mysql') and $this->checkPhpExtension('pdo_mysql'); 136 | 137 | default: 138 | return true; 139 | } 140 | } 141 | 142 | /** 143 | * Check the presence of the correct cache PHP extension 144 | * 145 | * @param string $cache 146 | * 147 | * @return boolean 148 | */ 149 | public function checkCacheDriver($cache) 150 | { 151 | switch ($cache) { 152 | case 'memcached': 153 | case 'apc': 154 | case 'redis': 155 | return $this->checkPhpExtension($cache); 156 | 157 | default: 158 | return true; 159 | } 160 | } 161 | 162 | /** 163 | * Check the presence of a PHP extension 164 | * 165 | * @param string $extension The extension 166 | * 167 | * @return boolean 168 | */ 169 | public function checkPhpExtension($extension) 170 | { 171 | $this->command->comment('Checking presence of '.$extension. ' extension'); 172 | 173 | if (!$this->extensions) { 174 | $this->extensions = $this->run('php -m', true, true); 175 | } 176 | 177 | return in_array($extension, $this->extensions); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | array('production'), 11 | 12 | // SCM repository 13 | ////////////////////////////////////////////////////////////////////// 14 | 15 | 'scm' => array( 16 | 17 | // The SCM used (supported: "git", "svn") 18 | 'scm' => 'git', 19 | 20 | // The SSH/HTTPS adress to your repository 21 | // Example: https://github.com/vendor/website.git 22 | 'repository' => '', 23 | 24 | // The repository credentials : you can leave those empty 25 | // if you're using SSH or if your repository is public 26 | // In other cases you can leave this empty too, and you will 27 | // be prompted for the credentials on deploy 28 | 'username' => '', 29 | 'password' => '', 30 | 31 | // The branch to deploy 32 | 'branch' => 'master', 33 | ), 34 | 35 | // Stages 36 | // 37 | // The multiples stages of your application 38 | // if you don't know what this does, then you don't need it 39 | ////////////////////////////////////////////////////////////////////// 40 | 41 | 'stages' => array( 42 | 43 | // Adding entries to this array will split the remote folder in stages 44 | // Like /var/www/yourapp/staging and /var/www/yourapp/production 45 | 'stages' => array(), 46 | 47 | // The default stage to execute tasks on when --stage is not provided 48 | 'default' => '', 49 | ), 50 | 51 | // Remote server 52 | ////////////////////////////////////////////////////////////////////// 53 | 54 | 'remote' => array( 55 | 56 | // Variables about the servers. Those can be guessed but in 57 | // case of problem it's best to input those manually 58 | 'variables' => array( 59 | 'directory_separator' => '/', 60 | 'line_endings' => "\n", 61 | ), 62 | 63 | // The root directory where your applications will be deployed 64 | 'root_directory' => '/home/www/', 65 | 66 | // The name of the application to deploy 67 | // This will create a folder of the same name in the root directory 68 | // configured above, so be careful about the characters used 69 | 'application_name' => 'application', 70 | 71 | // The number of releases to keep at all times 72 | 'keep_releases' => 4, 73 | 74 | // A list of folders/file to be shared between releases 75 | // Use this to list folders that need to keep their state, like 76 | // user uploaded data, file-based databases, etc. 77 | 'shared' => array( 78 | '{path.storage}/logs', 79 | '{path.storage}/sessions', 80 | ), 81 | 82 | 'permissions' => array( 83 | 84 | // The permissions to CHMOD folders to 85 | // Change to null to leave the folders untouched 86 | 'permissions' => 755, 87 | 88 | // The folders and files to set as web writable 89 | // You can pass paths in brackets, so {path.public} will return 90 | // the correct path to the public folder 91 | 'files' => array( 92 | 'app/database/production.sqlite', 93 | '{path.storage}', 94 | '{path.public}', 95 | ), 96 | 97 | // The web server user and group to CHOWN folders to 98 | // Leave empty to leave the above folders untouched 99 | 'webuser' => array( 100 | 'user' => 'www-data', 101 | 'group' => 'www-data', 102 | ), 103 | 104 | ), 105 | ), 106 | 107 | // Tasks 108 | // 109 | // Here you can define in the `before` and `after` array, Tasks to execute 110 | // before or after the core Rocketeer Tasks. You can either put a simple command, 111 | // a closure which receives a $task object, or the name of a class extending 112 | // the Rocketeer\Traits\Task class 113 | // 114 | // In the `custom` array you can list custom Tasks classes to be added 115 | // to Rocketeer. Those will then be available in Artisan 116 | // as `php artisan deploy:yourtask` 117 | ////////////////////////////////////////////////////////////////////// 118 | 119 | 'tasks' => array( 120 | 121 | // Tasks to execute before the core Rocketeer Tasks 122 | 'before' => array( 123 | 'setup' => array(), 124 | 'deploy' => array(), 125 | 'cleanup' => array(), 126 | ), 127 | 128 | // Tasks to execute after the core Rocketeer Tasks 129 | 'after' => array( 130 | 'setup' => array(), 131 | 'deploy' => array(), 132 | 'cleanup' => array(), 133 | ), 134 | 135 | // Custom Tasks to register with Rocketeer 136 | 'custom' => array(), 137 | ), 138 | 139 | // Contextual options 140 | // 141 | // In this section you can fine-tune the above configuration according 142 | // to the stage or connection currently in use. 143 | // Per example : 144 | // 'stages' => array( 145 | // 'staging' => array( 146 | // 'scm' => array('branch' => 'staging'), 147 | // ), 148 | // 'production' => array( 149 | // 'scm' => array('branch' => 'master'), 150 | // ), 151 | // ), 152 | 153 | 'on' => array( 154 | 155 | // Stages configurations 156 | 'stages' => array( 157 | ), 158 | 159 | // Connections configuration 160 | 'connections' => array( 161 | ), 162 | 163 | ), 164 | 165 | ); 166 | -------------------------------------------------------------------------------- /src/Rocketeer/Commands/BaseDeployCommand.php: -------------------------------------------------------------------------------- 1 | laravel['events'])) { 42 | return str_replace('deploy:', null, $this->name); 43 | } 44 | 45 | return $this->name; 46 | } 47 | 48 | //////////////////////////////////////////////////////////////////// 49 | ///////////////////////////// CORE METHODS ///////////////////////// 50 | //////////////////////////////////////////////////////////////////// 51 | 52 | /** 53 | * Fire a Tasks Queue 54 | * 55 | * @param string|array $tasks 56 | * 57 | * @return mixed 58 | */ 59 | protected function fireTasksQueue($tasks) 60 | { 61 | // Check for credentials 62 | $this->getServerCredentials(); 63 | $this->getRepositoryCredentials(); 64 | 65 | // Start timer 66 | $timerStart = microtime(true); 67 | 68 | // Convert tasks to array if necessary 69 | if (!is_array($tasks)) { 70 | $tasks = array($tasks); 71 | } 72 | 73 | // Run tasks and display timer 74 | $this->laravel['rocketeer.tasks']->run($tasks, $this); 75 | $this->line('Execution time: '.round(microtime(true) - $timerStart, 4). 's'); 76 | } 77 | 78 | /** 79 | * Get the Repository's credentials 80 | * 81 | * @return void 82 | */ 83 | protected function getRepositoryCredentials() 84 | { 85 | // Check for repository credentials 86 | $repositoryInfos = $this->laravel['rocketeer.rocketeer']->getCredentials(); 87 | $credentials = array('repository'); 88 | if (!array_get($repositoryInfos, 'repository') or $this->laravel['rocketeer.rocketeer']->needsCredentials()) { 89 | $credentials = array('repository', 'username', 'password'); 90 | } 91 | 92 | // Gather credentials 93 | foreach ($credentials as $credential) { 94 | ${$credential} = array_get($repositoryInfos, $credential); 95 | if (!${$credential}) { 96 | ${$credential} = $this->ask('No '.$credential. ' is set for the repository, please provide one :'); 97 | } 98 | } 99 | 100 | // Save them 101 | $credentials = compact($credentials); 102 | $this->laravel['rocketeer.server']->setValue('credentials', $credentials); 103 | foreach ($credentials as $key => $credential) { 104 | $this->laravel['config']->set('rocketeer::scm.'.$key, $credential); 105 | } 106 | } 107 | 108 | /** 109 | * Get the Server's credentials 110 | * 111 | * @return void 112 | */ 113 | protected function getServerCredentials() 114 | { 115 | if ($connections = $this->option('on')) { 116 | $this->laravel['rocketeer.rocketeer']->setConnections($connections); 117 | } 118 | 119 | // Check for configured connections 120 | $connections = $this->laravel['rocketeer.rocketeer']->getAvailableConnections(); 121 | $connectionName = $this->laravel['rocketeer.rocketeer']->getConnection(); 122 | 123 | if (is_null($connectionName)) { 124 | $connectionName = $this->ask('No connections have been set, please create one : (production)', 'production'); 125 | } 126 | 127 | // Check for server credentials 128 | $connection = array_get($connections, $connectionName, array()); 129 | $credentials = array('host' => true, 'username' => true, 'password' => false, 'keyphrase' => null, 'key' => false); 130 | 131 | // Gather credentials 132 | foreach ($credentials as $credential => $required) { 133 | ${$credential} = array_get($connection, $credential); 134 | if (!${$credential} and $required) { 135 | ${$credential} = $this->ask('No '.$credential. ' is set for [' .$connectionName. '], please provide one :'); 136 | } 137 | } 138 | 139 | // Get password or key 140 | if (!$password and !$key) { 141 | $type = $this->ask('No password or SSH key is set for [' .$connectionName. '], which would you use ? [key/password]'); 142 | if ($type == 'key') { 143 | $key = $this->ask('Please enter the full path to your key'); 144 | $keyphrase = $this->ask('If a keyphrase is required, provide it'); 145 | } else { 146 | $password = $this->ask('Please enter your password'); 147 | } 148 | } 149 | 150 | // Save them 151 | $credentials = compact(array_keys($credentials)); 152 | $this->laravel['rocketeer.server']->setValue('connections.'.$connectionName, $credentials); 153 | $this->laravel['config']->set('remote.connections.'.$connectionName, $credentials); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Rocketeer/Server.php: -------------------------------------------------------------------------------- 1 | app = $app; 35 | 36 | // Create personnal storage if necessary 37 | if (!$app->bound('path.storage') and !$storage) { 38 | $storage = __DIR__.DS.'..'.DS.'..'.DS.'storage'; 39 | @mkdir($storage); 40 | } 41 | 42 | // Get correct storage path 43 | $storage = $storage ?: $app['path.storage'].DS.'meta'; 44 | $this->repository = $storage.DS.'deployments.json'; 45 | } 46 | 47 | //////////////////////////////////////////////////////////////////// 48 | /////////////////////////// REMOTE VARIABLES /////////////////////// 49 | //////////////////////////////////////////////////////////////////// 50 | 51 | /** 52 | * Get the directory separators on the remove server 53 | * 54 | * @return string 55 | */ 56 | public function getSeparator() 57 | { 58 | // If manually set by the user, return it 59 | $user = $this->app['rocketeer.rocketeer']->getOption('remote.variables.directory_separator'); 60 | if ($user) { 61 | return $user; 62 | } 63 | 64 | $bash = $this->app['rocketeer.bash']; 65 | return $this->getValue('directory_separator', function ($server) use ($bash) { 66 | $separator = $bash->runRaw('php -r "echo DIRECTORY_SEPARATOR;"'); 67 | 68 | // Throw an Exception if we receive invalid output 69 | if (strlen($separator) > 1) { 70 | throw new Exception( 71 | 'An error occured while fetching the directory separators used on the server.'.PHP_EOL. 72 | 'Output received was : '.$separator 73 | ); 74 | } 75 | 76 | // Cache separator 77 | $server->setValue('directory_separator', $separator); 78 | 79 | return $separator; 80 | }); 81 | } 82 | 83 | /** 84 | * Get the remote line endings on the remove server 85 | * 86 | * @return string 87 | */ 88 | public function getLineEndings() 89 | { 90 | // If manually set by the user, return it 91 | $user = $this->app['rocketeer.rocketeer']->getOption('remote.variables.line_endings'); 92 | if ($user) { 93 | return $user; 94 | } 95 | 96 | $bash = $this->app['rocketeer.bash']; 97 | return $this->getValue('line_endings', function ($server) use ($bash) { 98 | $endings = $bash->runRaw('php -r "echo PHP_EOL;"'); 99 | $server->setValue('line_endings', $endings); 100 | 101 | return $endings; 102 | }); 103 | } 104 | 105 | //////////////////////////////////////////////////////////////////// 106 | /////////////////////////////// KEYSTORE /////////////////////////// 107 | //////////////////////////////////////////////////////////////////// 108 | 109 | /** 110 | * Get a value from the repository file 111 | * 112 | * @param string $key 113 | * @param \Closure|string $fallback 114 | * 115 | * @return mixed 116 | */ 117 | public function getValue($key, $fallback = null) 118 | { 119 | $value = array_get($this->getRepository(), $key, null); 120 | 121 | // Get fallback value 122 | if (is_null($value)) { 123 | return is_callable($fallback) ? $fallback($this) : $fallback; 124 | } 125 | 126 | return $value; 127 | } 128 | 129 | /** 130 | * Set a value from the repository file 131 | * 132 | * @param string $key 133 | * @param mixed $value 134 | * 135 | * @return array 136 | */ 137 | public function setValue($key, $value) 138 | { 139 | $repository = $this->getRepository(); 140 | array_set($repository, $key, $value); 141 | 142 | return $this->updateRepository($repository); 143 | } 144 | 145 | /** 146 | * Forget a value from the repository file 147 | * 148 | * @param string $key 149 | * 150 | * @return array 151 | */ 152 | public function forgetValue($key) 153 | { 154 | $repository = $this->getRepository(); 155 | array_forget($repository, $key); 156 | 157 | return $this->updateRepository($repository); 158 | } 159 | 160 | //////////////////////////////////////////////////////////////////// 161 | ////////////////////////// REPOSITORY FILE ///////////////////////// 162 | //////////////////////////////////////////////////////////////////// 163 | 164 | /** 165 | * Replace the contents of the deployments file 166 | * 167 | * @param array $data 168 | * 169 | * @return array 170 | */ 171 | public function updateRepository($data) 172 | { 173 | $this->app['files']->put($this->repository, json_encode($data)); 174 | 175 | return $data; 176 | } 177 | 178 | /** 179 | * Get the contents of the deployments file 180 | * 181 | * @return array 182 | */ 183 | public function getRepository() 184 | { 185 | // Cancel if the file doesn't exist 186 | if (!$this->app['files']->exists($this->repository)) { 187 | return array(); 188 | } 189 | 190 | // Get and parse file 191 | $repository = $this->app['files']->get($this->repository); 192 | $repository = json_decode($repository, true); 193 | 194 | return $repository; 195 | } 196 | 197 | /** 198 | * Deletes the deployments file 199 | * 200 | * @return boolean 201 | */ 202 | public function deleteRepository() 203 | { 204 | return $this->app['files']->delete($this->repository); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | ### 0.9.0 4 | 5 | - **Rocketeer now supports SVN** 6 | - **Rocketeer now has a [Campfire plugin](https://github.com/Anahkiasen/rocketeer-campfire)** 7 | - Add option to manually set remote variables when encountering problems 8 | - Add keyphrase support 9 | 10 | ### 0.8.0 11 | 12 | - **Rocketeer can now have specific configurations for stages and connections** 13 | - **Better handling of multiple connections** 14 | - **Added facade shortcuts `Rocketeer::execute(Task)` and `Rocketeer::on(connection[s], Task)` to execute commands on the remote servers** 15 | - Added the `--list` flag on the `rollback` command to show a list of available releases and pick one to rollback to 16 | - Added the `--on` flag to all commands to specify which connections the task should be executed on (ex. `production`, `staging,production`) 17 | - Added `deploy:flush` to clear Rocketeer's cache of credentials 18 | 19 | ### 0.7.0 20 | 21 | - **Rocketeer can now work outside of Laravel** 22 | - **Better handling of SSH keys** 23 | - Permissions are now entirely configurable 24 | - Rocketeer now prompts for confirmation before executing the Teardown task 25 | - Allow the use of patterns in shared folders 26 | - Share `sessions` folder by default 27 | - Rocketeer now prompts for binaries it can't find (composer, phpunit, etc) 28 | 29 | ### 0.6.5 30 | 31 | - **Make Rocketeer prompt for both server and SCM credentials if they're not stored** 32 | - **`artisan deploy` now deploys the project if the `--version` flat is not passed** 33 | - Make Rocketeer forget invalid credentials provided by prompt 34 | - Fix a bug where incorrect SCM urls would be generated 35 | 36 | ### 0.6.4 37 | 38 | - Make the output of commands in realtime when `--verbose` instead of when the command is done 39 | - Fix a bug where custom Task classes would be analyzed as string commands 40 | - Fix Rocketeeer not taking into account custom paths to **app/**, **storage/**, **public/** etc. 41 | - Reverse sluggification of application name 42 | 43 | ### 0.6.3 44 | 45 | - Application name is now always sluggified as a security 46 | - Fix a bug where the Check task would fail on pretend mode 47 | - Fix a bug where invalid directory separators would get cached and used 48 | 49 | ### 0.6.2 50 | 51 | - Make the Check task check for the remote presence of the configured SCM 52 | - Fix Rocketeer not being able to use a `composer.phar` on the server 53 | 54 | ### 0.6.1 55 | 56 | - Fix a bug where the configured user would not have the rights to set permissions 57 | 58 | ### 0.6.0 59 | 60 | - **Add multistage strategy** 61 | - **Add compatibility to Laravel 4.0** 62 | - Migrations are now under a `--migrate` flag 63 | - Split Git from the SCM implementation (**requires a config update**) 64 | - Releases are now named as `YmdHis` instead of `time()` 65 | - If the `scm.branch` option is empty, Rocketeer will now use the current Git branch 66 | - Fix a delay where the `current` symlink would get updated before the complete end of the deploy 67 | - Fix errors with Git and Composer not canceling deploy 68 | - Fix some compatibility problems with Windows 69 | - Fix a bug where string tasks would not be run in latest release folder 70 | - Fix Apache username and group using `www-data` by default 71 | 72 | ### 0.5.0 73 | 74 | - **Add a `deploy:update` task that updates the remote server without doing a new release** 75 | - **Add a `deploy:test` to run the tests on the server** 76 | - **Rocketeer can now prompt for Git credentials if you don't want to store them in the config** 77 | - The `deploy:check` command now checks PHP extensions for the cache/database/session drivers you set 78 | - Rocketeer now share logs by default between releases 79 | - Add ability to specify an array of Tasks in Rocketeer::before|after 80 | - Added a `$silent` flag to make a `Task::run` call silent no matter what 81 | - Rocketeer now displays how long the task took 82 | 83 | ### 0.4.0 84 | 85 | - **Add ability to share files and folders between releases** 86 | - **Add ability to create custom tasks integrated in the CLI** 87 | - **Add a `deploy:check` Task that checks if the server is ready to receive a Laravel app** 88 | - Add `Task::listContents` and `Task::fileExists` helpers 89 | - Add Task helper to run outstanding migrations 90 | - Add `Rocketeer::add` method on the facade to register custom Tasks 91 | - Fix `Task::runComposer` not taking into account a local `composer.phar` 92 | 93 | ### 0.3.2 94 | 95 | - Fixed wrong tag used in `deploy:cleanup` 96 | 97 | ### 0.3.1 98 | 99 | - Added `--pretend` flag on all commands to print out a list of the commands that would have been executed instead of running them 100 | 101 | ### 0.3.0 102 | 103 | - Added `Task::runInFolder` to run tasks in a specific folder 104 | - Added `Task::runForCurrentRelease` Task helper 105 | - Fixed a bug where `Task::run` would only return the last line of the command's output 106 | - Added `Task::runTests` methods to run the PHPUnit tests of the application 107 | - Integrated `Task::runTests` in the `Deploy` task under the `--tests` flag ; failing tests will cancel deploy and rollback 108 | 109 | ### 0.2.0 110 | 111 | - The core of Rocketeer's actions is now split into a system of Tasks for flexibility 112 | - Added a `Rocketeer` facade to easily add tasks via `before` and `after` (see Tasks docs) 113 | 114 | ### 0.1.1 115 | 116 | - Fixed a bug where the commands would try to connect to the remote hosts on construct 117 | - Fixed `ReleasesManager::getPreviousRelease` returning the wrong release 118 | 119 | ### 0.1.0 120 | 121 | - Add `deploy:teardown` to remove the application from remote servers 122 | - Add support for the connections defined in the remote config file 123 | - Add `deploy:rollback` and `deploy:current` commands 124 | - Add `deploy:cleanup` command 125 | - Add config file 126 | - Add `deploy:setup` and `deploy:deploy` commands 127 | -------------------------------------------------------------------------------- /tests/TasksQueueTest.php: -------------------------------------------------------------------------------- 1 | task('Deploy')); 10 | 11 | $this->assertEquals(array('ls'), $before); 12 | } 13 | 14 | public function testCanBuildTaskByName() 15 | { 16 | $task = $this->tasksQueue()->buildTask('Rocketeer\Tasks\Deploy'); 17 | 18 | $this->assertInstanceOf('Rocketeer\Traits\Task', $task); 19 | } 20 | 21 | public function testCanBuildCustomTaskByName() 22 | { 23 | $tasks = $this->tasksQueue()->buildQueue(array('Rocketeer\Tasks\Check')); 24 | 25 | $this->assertInstanceOf('Rocketeer\Tasks\Check', $tasks[0]); 26 | $this->assertInstanceOf('Tasks\MyCustomTask', $tasks[1]); 27 | } 28 | 29 | public function testCanAddCommandsToArtisan() 30 | { 31 | $command = $this->tasksQueue()->add('Rocketeer\Tasks\Deploy'); 32 | $this->assertInstanceOf('Rocketeer\Commands\BaseTaskCommand', $command); 33 | $this->assertInstanceOf('Rocketeer\Tasks\Deploy', $command->getTask()); 34 | } 35 | 36 | public function testCanGetTasksBeforeOrAfterAnotherTask() 37 | { 38 | $task = $this->task('Deploy'); 39 | $before = $this->tasksQueue()->getBefore($task); 40 | 41 | $this->assertEquals(array('before', 'foobar'), $before); 42 | } 43 | 44 | public function testCanAddTasksViaFacade() 45 | { 46 | $task = $this->task('Deploy'); 47 | $before = $this->tasksQueue()->getBefore($task); 48 | 49 | $this->tasksQueue()->before('deploy', 'composer install'); 50 | 51 | $newBefore = array_merge($before, array('composer install')); 52 | $this->assertEquals($newBefore, $this->tasksQueue()->getBefore($task)); 53 | } 54 | 55 | public function testCanAddMultipleTasksViaFacade() 56 | { 57 | $task = $this->task('Deploy'); 58 | $after = $this->tasksQueue()->getAfter($task); 59 | 60 | $this->tasksQueue()->after('Rocketeer\Tasks\Deploy', array( 61 | 'composer install', 62 | 'bower install' 63 | )); 64 | 65 | $newAfter = array_merge($after, array('composer install', 'bower install')); 66 | $this->assertEquals($newAfter, $this->tasksQueue()->getAfter($task)); 67 | } 68 | 69 | public function testCanAddSurroundTasksToNonExistingTasks() 70 | { 71 | $task = $this->task('Setup'); 72 | $this->tasksQueue()->after('setup', 'composer install'); 73 | 74 | $after = array('composer install'); 75 | $this->assertEquals($after, $this->tasksQueue()->getAfter($task)); 76 | } 77 | 78 | public function testCanAddSurroundTasksToMultipleTasks() 79 | { 80 | $this->tasksQueue()->after(array('cleanup', 'setup'), 'composer install'); 81 | 82 | $after = array('composer install'); 83 | $this->assertEquals($after, $this->tasksQueue()->getAfter($this->task('Setup'))); 84 | $this->assertEquals($after, $this->tasksQueue()->getAfter($this->task('Cleanup'))); 85 | } 86 | 87 | public function testCanGetBeforeOrAfterAnotherTaskBySlug() 88 | { 89 | $task = $this->task('Deploy'); 90 | $after = $this->tasksQueue()->getAfter($task); 91 | 92 | $this->assertEquals(array('after', 'foobar'), $after); 93 | } 94 | 95 | public function testCanBuildTaskFromString() 96 | { 97 | $string = 'echo "I love ducks"'; 98 | 99 | $string = $this->tasksQueue()->buildTaskFromClosure($string); 100 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $string); 101 | 102 | $closure = $string->getClosure(); 103 | $this->assertInstanceOf('Closure', $closure); 104 | 105 | $closureReflection = new ReflectionFunction ($closure); 106 | $this->assertEquals(array('stringTask' => 'echo "I love ducks"'), $closureReflection->getStaticVariables()); 107 | 108 | $this->assertEquals('I love ducks', $string->execute()); 109 | } 110 | 111 | public function testCanBuildTaskFromClosure() 112 | { 113 | $originalClosure = function ($task) { 114 | return $task->getCommand()->info('echo "I love ducks"'); 115 | }; 116 | 117 | $closure = $this->tasksQueue()->buildTaskFromClosure($originalClosure); 118 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $closure); 119 | $this->assertEquals($originalClosure, $closure->getClosure()); 120 | } 121 | 122 | public function testCanBuildQueue() 123 | { 124 | $queue = array( 125 | 'foobar', 126 | function ($task) { 127 | return 'lol'; 128 | }, 129 | 'Rocketeer\Tasks\Deploy' 130 | ); 131 | 132 | $queue = $this->tasksQueue()->buildQueue($queue); 133 | 134 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[0]); 135 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[1]); 136 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[2]); 137 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[3]); 138 | $this->assertInstanceOf('Rocketeer\Tasks\Deploy', $queue[4]); 139 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[5]); 140 | $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[6]); 141 | } 142 | 143 | public function testCanRunQueue() 144 | { 145 | $this->swapConfig(array( 146 | 'rocketeer::connections' => 'production', 147 | )); 148 | 149 | $this->expectOutputString('JOEY DOESNT SHARE FOOD'); 150 | $this->tasksQueue()->run(array( 151 | function ($task) { 152 | print 'JOEY DOESNT SHARE FOOD'; 153 | } 154 | ), $this->getCommand()); 155 | } 156 | 157 | public function testCanRunQueueOnDifferentConnectionsAndStages() 158 | { 159 | $this->swapConfig(array( 160 | 'rocketeer::connections' => array('staging', 'production'), 161 | 'rocketeer::stages.stages' => array('first', 'second'), 162 | )); 163 | 164 | $output = array(); 165 | $queue = array( 166 | function ($task) use (&$output) { 167 | $output[] = $task->rocketeer->getConnection(). ' - ' .$task->rocketeer->getStage(); 168 | } 169 | ); 170 | 171 | $queue = $this->tasksQueue()->buildQueue($queue); 172 | $this->tasksQueue()->run($queue, $this->getCommand()); 173 | 174 | $this->assertEquals(array( 175 | 'staging - first', 176 | 'staging - second', 177 | 'production - first', 178 | 'production - second', 179 | ), $output); 180 | } 181 | 182 | public function testCanRunQueueViaExecute() 183 | { 184 | $this->swapConfig(array( 185 | 'rocketeer::connections' => 'production', 186 | )); 187 | 188 | $output = $this->tasksQueue()->execute(array( 189 | 'ls -a', 190 | function ($task) { 191 | return 'JOEY DOESNT SHARE FOOD'; 192 | } 193 | )); 194 | 195 | $this->assertEquals(array( 196 | '.'.PHP_EOL.'..'.PHP_EOL.'.gitkeep', 197 | 'JOEY DOESNT SHARE FOOD', 198 | ), $output); 199 | } 200 | 201 | public function testCanRunOnMultipleConnectionsViaOn() 202 | { 203 | $this->swapConfig(array( 204 | 'rocketeer::stages.stages' => array('first', 'second'), 205 | )); 206 | 207 | $output = $this->tasksQueue()->on(array('staging', 'production'), function ($task) { 208 | return $task->rocketeer->getConnection(). ' - ' .$task->rocketeer->getStage(); 209 | }); 210 | 211 | $this->assertEquals(array( 212 | 'staging - first', 213 | 'staging - second', 214 | 'production - first', 215 | 'production - second', 216 | ), $output); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/RocketeerTest.php: -------------------------------------------------------------------------------- 1 | app['rocketeer.rocketeer']->getAvailableConnections(); 12 | $this->assertEquals(array('production', 'staging'), array_keys($connections)); 13 | 14 | $this->app['rocketeer.server']->setValue('connections.custom.username', 'foobar'); 15 | $connections = $this->app['rocketeer.rocketeer']->getAvailableConnections(); 16 | $this->assertEquals(array('custom'), array_keys($connections)); 17 | } 18 | 19 | public function testCanGetCurrentConnection() 20 | { 21 | $this->swapConfig(array('rocketeer::connections' => 'foobar')); 22 | $this->assertEquals('production', $this->app['rocketeer.rocketeer']->getConnection()); 23 | 24 | $this->swapConfig(array('rocketeer::connections' => 'production')); 25 | $this->assertEquals('production', $this->app['rocketeer.rocketeer']->getConnection()); 26 | 27 | $this->swapConfig(array('rocketeer::connections' => 'staging')); 28 | $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getConnection()); 29 | } 30 | 31 | public function testCanChangeConnection() 32 | { 33 | $this->assertEquals('production', $this->app['rocketeer.rocketeer']->getConnection()); 34 | 35 | $this->app['rocketeer.rocketeer']->setConnection('staging'); 36 | $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getConnection()); 37 | 38 | $this->app['rocketeer.rocketeer']->setConnections('staging,production'); 39 | $this->assertEquals(array('staging', 'production'), $this->app['rocketeer.rocketeer']->getConnections()); 40 | } 41 | 42 | public function testCanUseSshRepository() 43 | { 44 | $repository = 'git@github.com:Anahkiasen/rocketeer.git'; 45 | $this->expectRepositoryConfig($repository, '', ''); 46 | 47 | $this->assertEquals($repository, $this->app['rocketeer.rocketeer']->getRepository()); 48 | } 49 | 50 | public function testCanUseHttpsRepository() 51 | { 52 | $this->expectRepositoryConfig('https://github.com/Anahkiasen/rocketeer.git', 'foobar', 'bar'); 53 | 54 | $this->assertEquals('https://foobar:bar@github.com/Anahkiasen/rocketeer.git', $this->app['rocketeer.rocketeer']->getRepository()); 55 | } 56 | 57 | public function testCanUseHttpsRepositoryWithUsernameProvided() 58 | { 59 | $this->expectRepositoryConfig('https://foobar@github.com/Anahkiasen/rocketeer.git', 'foobar', 'bar'); 60 | 61 | $this->assertEquals('https://foobar:bar@github.com/Anahkiasen/rocketeer.git', $this->app['rocketeer.rocketeer']->getRepository()); 62 | } 63 | 64 | public function testCanUseHttpsRepositoryWithOnlyUsernameProvided() 65 | { 66 | $this->expectRepositoryConfig('https://foobar@github.com/Anahkiasen/rocketeer.git', 'foobar', ''); 67 | 68 | $this->assertEquals('https://foobar@github.com/Anahkiasen/rocketeer.git', $this->app['rocketeer.rocketeer']->getRepository()); 69 | } 70 | 71 | public function testCanCleanupProvidedRepositoryFromCredentials() 72 | { 73 | $this->expectRepositoryConfig('https://foobar@github.com/Anahkiasen/rocketeer.git', 'Anahkiasen', ''); 74 | 75 | $this->assertEquals('https://Anahkiasen@github.com/Anahkiasen/rocketeer.git', $this->app['rocketeer.rocketeer']->getRepository()); 76 | } 77 | 78 | public function testCanUseHttpsRepositoryWithoutCredentials() 79 | { 80 | $this->expectRepositoryConfig('https://github.com/Anahkiasen/rocketeer.git', '', ''); 81 | 82 | $this->assertEquals('https://github.com/Anahkiasen/rocketeer.git', $this->app['rocketeer.rocketeer']->getRepository()); 83 | } 84 | 85 | public function testCanCheckIfRepositoryNeedsCredentials() 86 | { 87 | $this->expectRepositoryConfig('https://github.com/Anahkiasen/rocketeer.git', '', ''); 88 | $this->assertTrue($this->app['rocketeer.rocketeer']->needsCredentials()); 89 | } 90 | 91 | public function testCangetRepositoryBranch() 92 | { 93 | $this->assertEquals('master', $this->app['rocketeer.rocketeer']->getRepositoryBranch()); 94 | } 95 | 96 | public function testCanGetApplicationName() 97 | { 98 | $this->assertEquals('foobar', $this->app['rocketeer.rocketeer']->getApplicationName()); 99 | } 100 | 101 | public function testCanGetHomeFolder() 102 | { 103 | $this->assertEquals($this->server.'', $this->app['rocketeer.rocketeer']->getHomeFolder()); 104 | } 105 | 106 | public function testCanGetFolderWithStage() 107 | { 108 | $this->app['rocketeer.rocketeer']->setStage('test'); 109 | 110 | $this->assertEquals($this->server.'/test/current', $this->app['rocketeer.rocketeer']->getFolder('current')); 111 | } 112 | 113 | public function testCanGetAnyFolder() 114 | { 115 | $this->assertEquals($this->server.'/current', $this->app['rocketeer.rocketeer']->getFolder('current')); 116 | } 117 | 118 | public function testCanReplacePatternsInFolders() 119 | { 120 | $folder = $this->app['rocketeer.rocketeer']->getFolder('{path.storage}'); 121 | 122 | $this->assertEquals($this->server.'/app/storage', $folder); 123 | } 124 | 125 | public function testCannotReplaceUnexistingPatternsInFolders() 126 | { 127 | $folder = $this->app['rocketeer.rocketeer']->getFolder('{path.foobar}'); 128 | 129 | $this->assertEquals($this->server.'/', $folder); 130 | } 131 | 132 | public function testCanUseRecursiveStageConfiguration() 133 | { 134 | $this->swapConfig(array( 135 | 'rocketeer::scm.branch' => 'master', 136 | 'rocketeer::on.stages.staging.scm.branch' => 'staging', 137 | )); 138 | 139 | $this->assertEquals('master', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); 140 | $this->app['rocketeer.rocketeer']->setStage('staging'); 141 | $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); 142 | } 143 | 144 | public function testCanUseRecursiveConnectionConfiguration() 145 | { 146 | $this->swapConfig(array( 147 | 'rocketeer::connections' => 'production', 148 | 'rocketeer::scm.branch' => 'master', 149 | 'rocketeer::on.connections.staging.scm.branch' => 'staging', 150 | )); 151 | $this->assertEquals('master', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); 152 | 153 | $this->swapConfig(array( 154 | 'rocketeer::connections' => 'staging', 155 | 'rocketeer::scm.branch' => 'master', 156 | 'rocketeer::on.connections.staging.scm.branch' => 'staging', 157 | )); 158 | $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); 159 | } 160 | 161 | //////////////////////////////////////////////////////////////////// 162 | //////////////////////////////// HELPERS /////////////////////////// 163 | //////////////////////////////////////////////////////////////////// 164 | 165 | /** 166 | * Make the config return specific SCM config 167 | * 168 | * @param string $repository 169 | * @param string $username 170 | * @param string $password 171 | * 172 | * @return void 173 | */ 174 | protected function expectRepositoryConfig($repository, $username, $password) 175 | { 176 | $this->app['config']->shouldReceive('get')->with('rocketeer::scm')->andReturn(array( 177 | 'repository' => $repository, 178 | 'username' => $username, 179 | 'password' => $password, 180 | )); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Rocketeer/RocketeerServiceProvider.php: -------------------------------------------------------------------------------- 1 | app = static::make($this->app); 47 | } 48 | 49 | /** 50 | * Get the services provided by the provider. 51 | * 52 | * @return array 53 | */ 54 | public function provides() 55 | { 56 | return array('rocketeer'); 57 | } 58 | 59 | //////////////////////////////////////////////////////////////////// 60 | /////////////////////////// CLASS BINDINGS ///////////////////////// 61 | //////////////////////////////////////////////////////////////////// 62 | 63 | /** 64 | * Make a Rocketeer container 65 | * 66 | * @return Container 67 | */ 68 | public static function make($app = null) 69 | { 70 | if (!$app) { 71 | $app = new Container; 72 | } 73 | 74 | $serviceProvider = new static($app); 75 | 76 | // Bind classes 77 | $app = $serviceProvider->bindCoreClasses($app); 78 | $app = $serviceProvider->bindClasses($app); 79 | $app = $serviceProvider->bindScm($app); 80 | $app = $serviceProvider->bindCommands($app); 81 | 82 | return $app; 83 | } 84 | 85 | /** 86 | * Bind the core classes 87 | * 88 | * @param Container $app 89 | * 90 | * @return Container 91 | */ 92 | public function bindCoreClasses(Container $app) 93 | { 94 | $app->bindIf('files', 'Illuminate\Filesystem\Filesystem'); 95 | 96 | $app->bindIf('request', function ($app) { 97 | return Request::createFromGlobals(); 98 | }, true); 99 | 100 | $app->bindIf('config', function ($app) { 101 | $fileloader = new FileLoader($app['files'], __DIR__.'/../config'); 102 | 103 | return new Repository($fileloader, 'config'); 104 | }, true); 105 | 106 | $app->bindIf('remote', function ($app) { 107 | return new RemoteManager($app); 108 | }, true); 109 | 110 | // Register factory and custom configurations 111 | $app = $this->registerConfig($app); 112 | 113 | return $app; 114 | } 115 | 116 | /** 117 | * Bind the Rocketeer classes to the Container 118 | * 119 | * @param Container $app 120 | * 121 | * @return Container 122 | */ 123 | public function bindClasses(Container $app) 124 | { 125 | $app->singleton('rocketeer.rocketeer', function ($app) { 126 | return new Rocketeer($app); 127 | }); 128 | 129 | $app->bind('rocketeer.releases', function ($app) { 130 | return new ReleasesManager($app); 131 | }); 132 | 133 | $app->bind('rocketeer.server', function ($app) { 134 | return new Server($app); 135 | }); 136 | 137 | $app->bind('rocketeer.bash', function ($app) { 138 | return new Bash($app); 139 | }); 140 | 141 | $app->singleton('rocketeer.tasks', function ($app) { 142 | return new TasksQueue($app); 143 | }); 144 | 145 | $app->singleton('rocketeer.console', function ($app) { 146 | return new Console('Rocketeer', Rocketeer::VERSION); 147 | }); 148 | 149 | $app['rocketeer.console']->setLaravel($app); 150 | 151 | return $app; 152 | } 153 | 154 | /** 155 | * Bind the SCM instance 156 | * 157 | * @param Container $app 158 | * 159 | * @return Container 160 | */ 161 | public function bindScm(Container $app) 162 | { 163 | // Currently only one 164 | $scm = $this->app['rocketeer.rocketeer']->getOption('scm.scm'); 165 | $scm = 'Rocketeer\Scm\\'.ucfirst($scm); 166 | 167 | $app->bind('rocketeer.scm', function ($app) use ($scm) { 168 | return new $scm($app); 169 | }); 170 | 171 | return $app; 172 | } 173 | 174 | /** 175 | * Bind the commands to the Container 176 | * 177 | * @param Container $app 178 | * 179 | * @return Container 180 | */ 181 | public function bindCommands(Container $app) 182 | { 183 | // Base commands 184 | $tasks = array( 185 | '' => '', 186 | 'check' => 'Check', 187 | 'cleanup' => 'Cleanup', 188 | 'current' => 'CurrentRelease', 189 | 'deploy' => 'Deploy', 190 | 'flush' => 'Flush', 191 | 'ignite' => 'Ignite', 192 | 'rollback' => 'Rollback', 193 | 'setup' => 'Setup', 194 | 'teardown' => 'Teardown', 195 | 'test' => 'Test', 196 | 'update' => 'Update', 197 | ); 198 | 199 | // Add User commands 200 | $userTasks = (array) $this->app['config']->get('rocketeer::tasks.custom'); 201 | $tasks = array_merge($tasks, $userTasks); 202 | 203 | // Bind the commands 204 | foreach ($tasks as $slug => $task) { 205 | 206 | // Check if we have an actual command to use 207 | $commandClass = 'Rocketeer\Commands\Deploy'.$task.'Command'; 208 | $fakeCommand = !class_exists($commandClass); 209 | 210 | // Build command slug 211 | if ($fakeCommand) { 212 | $taskInstance = $this->app['rocketeer.tasks']->buildTask($task); 213 | if (is_numeric($slug)) { 214 | $slug = $taskInstance->getSlug(); 215 | } 216 | } 217 | 218 | // Add command to array 219 | $command = trim('deploy.'.$slug, '.'); 220 | $this->commands[] = $command; 221 | 222 | // Look for an existing command 223 | if (!$fakeCommand) { 224 | $this->app->bind($command, function ($app) use ($commandClass) { 225 | return new $commandClass; 226 | }); 227 | 228 | // Else create a fake one 229 | } else { 230 | $this->app->bind($command, function ($app) use ($taskInstance, $slug) { 231 | return new Commands\BaseTaskCommand($taskInstance, $slug); 232 | }); 233 | } 234 | 235 | } 236 | 237 | // Add commands to Artisan 238 | foreach ($this->commands as $command) { 239 | $app['rocketeer.console']->add($app[$command]); 240 | if (isset($app['events'])) { 241 | $this->commands($command); 242 | } 243 | } 244 | 245 | return $app; 246 | } 247 | 248 | //////////////////////////////////////////////////////////////////// 249 | /////////////////////////////// HELPERS //////////////////////////// 250 | //////////////////////////////////////////////////////////////////// 251 | 252 | /** 253 | * Register factory and custom configurations 254 | * 255 | * @param Container $app 256 | * 257 | * @return Container 258 | */ 259 | protected function registerConfig(Container $app) 260 | { 261 | // Register paths 262 | if (!$app->bound('path.base')) { 263 | $app['path.base'] = realpath(__DIR__.'/../../../'); 264 | } 265 | 266 | // Register config file 267 | $app['config']->package('anahkiasen/rocketeer', __DIR__.'/../config'); 268 | 269 | // Register custom config 270 | $custom = $app['path.base'].'/rocketeer.php'; 271 | if (file_exists($custom)) { 272 | $app['config']->afterLoading('rocketeer', function ($me, $group, $items) use ($custom) { 273 | $custom = include $custom; 274 | return array_replace_recursive($items, $custom); 275 | }); 276 | } 277 | 278 | return $app; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/Rocketeer/Traits/Task.php: -------------------------------------------------------------------------------- 1 | description; 51 | } 52 | 53 | /** 54 | * Run the Task 55 | * 56 | * @return void 57 | */ 58 | abstract public function execute(); 59 | 60 | //////////////////////////////////////////////////////////////////// 61 | /////////////////////////////// HELPERS //////////////////////////// 62 | //////////////////////////////////////////////////////////////////// 63 | 64 | /** 65 | * Check if the remote server is setup 66 | * 67 | * @return boolean 68 | */ 69 | public function isSetup() 70 | { 71 | return $this->fileExists($this->rocketeer->getFolder('current')); 72 | } 73 | 74 | /** 75 | * Check if the Task uses stages 76 | * 77 | * @return boolean 78 | */ 79 | public function usesStages() 80 | { 81 | $stages = $this->rocketeer->getStages(); 82 | 83 | return $this->usesStages and !empty($stages); 84 | } 85 | 86 | /** 87 | * Run actions in the current release's folder 88 | * 89 | * @param string|array $tasks One or more tasks 90 | * 91 | * @return string 92 | */ 93 | public function runForCurrentRelease($tasks) 94 | { 95 | return $this->runInFolder($this->releasesManager->getCurrentReleasePath(), $tasks); 96 | } 97 | 98 | /** 99 | * Execute another Task by name 100 | * 101 | * @param string $task 102 | * 103 | * @return string The Task's output 104 | */ 105 | public function executeTask($task) 106 | { 107 | return $this->app['rocketeer.tasks']->buildTask($task)->execute(); 108 | } 109 | 110 | //////////////////////////////////////////////////////////////////// 111 | //////////////////////////////// TASKS ///////////////////////////// 112 | //////////////////////////////////////////////////////////////////// 113 | 114 | /** 115 | * Clone the repo into a release folder 116 | * 117 | * @param string $destination Where to clone to 118 | * 119 | * @return string 120 | */ 121 | public function cloneRepository($destination = null) 122 | { 123 | if (!$destination) { 124 | $destination = $this->releasesManager->getCurrentReleasePath(); 125 | } 126 | 127 | $this->command->info('Cloning repository in "' .$destination. '"'); 128 | $output = $this->scm->execute('checkout', $destination); 129 | 130 | $status = $this->checkStatus('Unable to clone the repository', $output); 131 | if ($status === false) { 132 | // Forget the SCM credentials if they're invalid 133 | $this->server->forgetValue('credentials'); 134 | } 135 | 136 | return $status; 137 | } 138 | 139 | /** 140 | * Update the current release 141 | * 142 | * @param boolean $reset Whether the repository should be reset first 143 | * 144 | * @return string 145 | */ 146 | public function updateRepository($reset = true) 147 | { 148 | $this->command->info('Pulling changes'); 149 | $tasks = array($this->scm->update()); 150 | 151 | // Reset if requested 152 | if ($reset) { 153 | array_unshift($tasks, $this->scm->reset()); 154 | } 155 | 156 | return $this->runForCurrentRelease($tasks); 157 | } 158 | 159 | /** 160 | * Update the current symlink 161 | * 162 | * @param integer $release A release to mark as current 163 | * 164 | * @return string 165 | */ 166 | public function updateSymlink($release = null) 167 | { 168 | // If the release is specified, update to make it the current one 169 | if ($release) { 170 | $this->releasesManager->updateCurrentRelease($release); 171 | } 172 | 173 | // Get path to current/ folder and latest release 174 | $currentReleasePath = $this->releasesManager->getCurrentReleasePath(); 175 | $currentFolder = $this->rocketeer->getFolder('current'); 176 | 177 | return $this->symlink($currentReleasePath, $currentFolder); 178 | } 179 | 180 | /** 181 | * Share a file or folder between releases 182 | * 183 | * @param string $file Path to the file in a release folder 184 | * 185 | * @return string 186 | */ 187 | public function share($file) 188 | { 189 | // Get path to current file and shared file 190 | $currentFile = $this->releasesManager->getCurrentReleasePath($file); 191 | $sharedFile = preg_replace('#releases/[0-9]+/#', 'shared/', $currentFile); 192 | 193 | // If no instance of the shared file exists, use current one 194 | if (!$this->fileExists($sharedFile)) { 195 | $this->move($currentFile, $sharedFile); 196 | } 197 | 198 | $this->command->comment('Sharing file '.$currentFile); 199 | 200 | return $this->symlink($sharedFile, $currentFile); 201 | } 202 | 203 | /** 204 | * Set a folder as web-writable 205 | * 206 | * @param string $folder 207 | * 208 | * @return string 209 | */ 210 | public function setPermissions($folder) 211 | { 212 | $commands = array(); 213 | 214 | // Get path to folder 215 | $folder = $this->releasesManager->getCurrentReleasePath($folder); 216 | $this->command->comment('Setting permissions for '.$folder); 217 | 218 | // Get permissions options 219 | $options = $this->rocketeer->getOption('remote.permissions'); 220 | $chmod = array_get($options, 'permissions'); 221 | $user = array_get($options, 'webuser.user'); 222 | $group = array_get($options, 'webuser.group'); 223 | 224 | // Add chmod 225 | if ($chmod) { 226 | $commands[] = sprintf('chmod -R %s %s', $chmod, $folder); 227 | $commands[] = sprintf('chmod -R g+s %s', $folder); 228 | } 229 | 230 | // And chown 231 | if ($user and $group) { 232 | $commands[] = sprintf('chown -R %s:%s %s', $user, $group, $folder); 233 | } 234 | 235 | // Cancel if setting of permissions is not configured 236 | if (empty($commands)) { 237 | return true; 238 | } 239 | 240 | return $this->runForCurrentRelease($commands); 241 | } 242 | 243 | //////////////////////////////////////////////////////////////////// 244 | //////////////////////// LARAVEL-SPECIFIC TASKS //////////////////// 245 | //////////////////////////////////////////////////////////////////// 246 | 247 | /** 248 | * Run Composer on the folder 249 | * 250 | * @return string 251 | */ 252 | public function runComposer() 253 | { 254 | $this->command->comment('Installing Composer dependencies'); 255 | $output = $this->runForCurrentRelease($this->getComposer(). ' install'); 256 | 257 | return $this->checkStatus('Composer could not install dependencies', $output); 258 | } 259 | 260 | /** 261 | * Get the path to Composer binary 262 | * 263 | * @return string 264 | */ 265 | public function getComposer() 266 | { 267 | $composer = $this->which('composer', $this->releasesManager->getCurrentReleasePath().'/composer.phar'); 268 | if (strpos($composer, 'composer.phar') !== false) { 269 | $composer = 'php '.$composer; 270 | } 271 | 272 | return $composer; 273 | } 274 | 275 | /** 276 | * Run any outstanding migrations 277 | * 278 | * @param boolean $seed Whether the database should also be seeded 279 | * 280 | * @return string 281 | */ 282 | public function runMigrations($seed = false) 283 | { 284 | $seed = $seed ? ' --seed' : null; 285 | $this->command->comment('Running outstanding migrations'); 286 | 287 | return $this->runForCurrentRelease('php artisan migrate'.$seed); 288 | } 289 | 290 | /** 291 | * Run the application's tests 292 | * 293 | * @param string $arguments Additional arguments to pass to PHPUnit 294 | * 295 | * @return boolean 296 | */ 297 | public function runTests($arguments = null) 298 | { 299 | // Look for PHPUnit 300 | $phpunit = $this->which('phpunit', $this->releasesManager->getCurrentReleasePath().'/vendor/bin/phpunit'); 301 | if (!$phpunit) { 302 | return true; 303 | } 304 | 305 | // Run PHPUnit 306 | $this->command->info('Running tests...'); 307 | $output = $this->runForCurrentRelease(array( 308 | $phpunit. ' --stop-on-failure '.$arguments, 309 | )); 310 | 311 | return $this->checkStatus('Tests failed', $output, 'Tests passed successfully'); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/Rocketeer/Rocketeer.php: -------------------------------------------------------------------------------- 1 | app = $app; 56 | } 57 | 58 | /** 59 | * Get an option from Rocketeer's config file 60 | * 61 | * @param string $option 62 | * 63 | * @return mixed 64 | */ 65 | public function getOption($option) 66 | { 67 | if ($contextual = $this->getContextualOption($option, 'stages')) { 68 | return $contextual; 69 | } 70 | 71 | if ($contextual = $this->getContextualOption($option, 'connections')) { 72 | return $contextual; 73 | } 74 | 75 | return $this->app['config']->get('rocketeer::'.$option); 76 | } 77 | 78 | /** 79 | * Get a contextual option 80 | * 81 | * @param string $option 82 | * @param string $type [stage,connection] 83 | * 84 | * @return mixed 85 | */ 86 | protected function getContextualOption($option, $type) 87 | { 88 | switch ($type) { 89 | case 'stages': 90 | $contextual = sprintf('rocketeer::on.stages.%s.%s', $this->stage, $option); 91 | break; 92 | 93 | case 'connections': 94 | $contextual = sprintf('rocketeer::on.connections.%s.%s', $this->getConnection(), $option); 95 | break; 96 | } 97 | 98 | return $this->app['config']->get($contextual); 99 | } 100 | 101 | //////////////////////////////////////////////////////////////////// 102 | //////////////////////////////// STAGES //////////////////////////// 103 | //////////////////////////////////////////////////////////////////// 104 | 105 | /** 106 | * Set the stage Tasks will execute on 107 | * 108 | * @param string $stage 109 | * 110 | * @return void 111 | */ 112 | public function setStage($stage) 113 | { 114 | $this->stage = $stage; 115 | } 116 | 117 | /** 118 | * Get the current stage 119 | * 120 | * @return string 121 | */ 122 | public function getStage() 123 | { 124 | return $this->stage; 125 | } 126 | 127 | /** 128 | * Get the various stages provided by the User 129 | * 130 | * @return array 131 | */ 132 | public function getStages() 133 | { 134 | return $this->getOption('stages.stages'); 135 | } 136 | 137 | //////////////////////////////////////////////////////////////////// 138 | ///////////////////////////// APPLICATION ////////////////////////// 139 | //////////////////////////////////////////////////////////////////// 140 | 141 | /** 142 | * Whether the repository used is using SSH or HTTPS 143 | * 144 | * @return boolean 145 | */ 146 | public function needsCredentials() 147 | { 148 | return Str::contains($this->getRepository(), 'https://'); 149 | } 150 | 151 | /** 152 | * Get the available connections 153 | * 154 | * @return array 155 | */ 156 | public function getAvailableConnections() 157 | { 158 | $connections = $this->app['rocketeer.server']->getValue('connections'); 159 | if (!$connections) { 160 | $connections = $this->app['config']->get('remote.connections'); 161 | } 162 | 163 | return $connections; 164 | } 165 | 166 | /** 167 | * Check if a connection has credentials related to it 168 | * 169 | * @param string $connection 170 | * 171 | * @return boolean 172 | */ 173 | public function isValidConnection($connection) 174 | { 175 | $available = (array) $this->getAvailableConnections(); 176 | 177 | return array_key_exists($connection, $available); 178 | } 179 | 180 | /** 181 | * Get the connection in use 182 | * 183 | * @return string 184 | */ 185 | public function getConnections() 186 | { 187 | // Get cached resolved connections 188 | if ($this->connections) { 189 | return $this->connections; 190 | } 191 | 192 | // Get all and defaults 193 | $connections = (array) $this->app['config']->get('rocketeer::connections'); 194 | $default = $this->app['config']->get('remote.default'); 195 | 196 | // Remove invalid connections 197 | $instance = $this; 198 | $connections = array_filter($connections, function ($value) use ($instance) { 199 | return $instance->isValidConnection($value); 200 | }); 201 | 202 | // Return default if no active connection(s) set 203 | if (empty($connections)) { 204 | return array($default); 205 | } 206 | 207 | // Set current connection as default 208 | $this->connections = $connections; 209 | 210 | return $connections; 211 | } 212 | 213 | /** 214 | * Get the active connection 215 | * 216 | * @return string 217 | */ 218 | public function getConnection() 219 | { 220 | // Get cached resolved connection 221 | if ($this->connection) { 222 | return $this->connection; 223 | } 224 | 225 | $connection = array_get($this->getConnections(), 0); 226 | $this->connection = $connection; 227 | 228 | return $this->connection; 229 | } 230 | 231 | /** 232 | * Set the active connections 233 | * 234 | * @param string|array $connections 235 | */ 236 | public function setConnections($connections) 237 | { 238 | if (!is_array($connections)) { 239 | $connections = explode(',', $connections); 240 | } 241 | 242 | $this->connections = $connections; 243 | } 244 | 245 | /** 246 | * Set the curent connection 247 | * 248 | * @param string $connection 249 | */ 250 | public function setConnection($connection) 251 | { 252 | if ($this->isValidConnection($connection)) { 253 | $this->connection = $connection; 254 | $this->app['config']->set('remote.default', $connection); 255 | } 256 | } 257 | 258 | /** 259 | * Flush active connection(s) 260 | * 261 | * @return void 262 | */ 263 | public function disconnect() 264 | { 265 | $this->connection = null; 266 | $this->connections = null; 267 | } 268 | 269 | /** 270 | * Get the name of the application to deploy 271 | * 272 | * @return string 273 | */ 274 | public function getApplicationName() 275 | { 276 | return $this->getOption('remote.application_name'); 277 | } 278 | 279 | //////////////////////////////////////////////////////////////////// 280 | /////////////////////////// GIT REPOSITORY ///////////////////////// 281 | //////////////////////////////////////////////////////////////////// 282 | 283 | /** 284 | * Get the credentials for the repository 285 | * 286 | * @return array 287 | */ 288 | public function getCredentials() 289 | { 290 | $credentials = $this->app['rocketeer.server']->getValue('credentials'); 291 | if (!$credentials) { 292 | $credentials = $this->getOption('scm'); 293 | } 294 | 295 | return $credentials; 296 | } 297 | 298 | /** 299 | * Get the URL to the Git repository 300 | * 301 | * @param string $username 302 | * @param string $password 303 | * 304 | * @return string 305 | */ 306 | public function getRepository() 307 | { 308 | // Get credentials 309 | $repository = $this->getCredentials(); 310 | $username = array_get($repository, 'username'); 311 | $password = array_get($repository, 'password'); 312 | $repository = array_get($repository, 'repository'); 313 | 314 | // Add credentials if possible 315 | if ($username or $password) { 316 | 317 | // Build credentials chain 318 | $credentials = $password ? $username.':'.$password : $username; 319 | $credentials .= '@'; 320 | 321 | // Add them in chain 322 | $repository = preg_replace('#https://(.+)@#', 'https://', $repository); 323 | $repository = str_replace('https://', 'https://'.$credentials, $repository); 324 | } 325 | 326 | return $repository; 327 | } 328 | 329 | /** 330 | * Get the Git branch 331 | * 332 | * @return string 333 | */ 334 | public function getRepositoryBranch() 335 | { 336 | exec($this->app['rocketeer.scm']->currentBranch(), $fallback); 337 | $fallback = trim($fallback[0]) ?: 'master'; 338 | $branch = $this->getOption('scm.branch') ?: $fallback; 339 | 340 | return $branch; 341 | } 342 | 343 | //////////////////////////////////////////////////////////////////// 344 | //////////////////////////////// PATHS ///////////////////////////// 345 | //////////////////////////////////////////////////////////////////// 346 | 347 | /** 348 | * Replace patterns in a folder path 349 | * 350 | * @param string $path 351 | * 352 | * @return string 353 | */ 354 | public function replacePatterns($path) 355 | { 356 | $app = $this->app; 357 | 358 | // Replace folder patterns 359 | return preg_replace_callback('/\{[a-z\.]+\}/', function ($match) use ($app) { 360 | $folder = substr($match[0], 1, -1); 361 | 362 | if ($app->bound($folder)) { 363 | return str_replace($app['path.base'].'/', null, $app->make($folder)); 364 | } 365 | 366 | return false; 367 | }, $path); 368 | } 369 | 370 | /** 371 | * Get the path to a folder, taking into account application name and stage 372 | * 373 | * @param string $folder 374 | * 375 | * @return string 376 | */ 377 | public function getFolder($folder = null) 378 | { 379 | $folder = $this->replacePatterns($folder); 380 | 381 | $base = $this->getHomeFolder().'/'; 382 | if ($folder and $this->stage) { 383 | $base .= $this->stage.'/'; 384 | } 385 | $folder = str_replace($base, null, $folder); 386 | 387 | return $base.$folder; 388 | } 389 | 390 | /** 391 | * Get the path to the root folder of the application 392 | * 393 | * @return string 394 | */ 395 | public function getHomeFolder() 396 | { 397 | $rootDirectory = $this->getOption('remote.root_directory'); 398 | $rootDirectory = Str::finish($rootDirectory, '/'); 399 | 400 | return $rootDirectory.$this->getApplicationName(); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /tests/_start.php: -------------------------------------------------------------------------------- 1 | server = __DIR__.'/server/foobar'; 49 | $this->deploymentsFile = __DIR__.'/meta/deployments.json'; 50 | 51 | $this->app = new Container; 52 | 53 | // Laravel classes --------------------------------------------- / 54 | 55 | $this->app->instance('path.base', '/src'); 56 | $this->app->instance('path', '/src/app'); 57 | $this->app->instance('path.public', '/src/public'); 58 | $this->app->instance('path.storage', '/src/app/storage'); 59 | 60 | $this->app['files'] = new Filesystem; 61 | $this->app['config'] = $this->getConfig(); 62 | $this->app['remote'] = $this->getRemote(); 63 | $this->app['artisan'] = $this->getArtisan(); 64 | 65 | // Rocketeer classes ------------------------------------------- / 66 | 67 | $serviceProvider = new RocketeerServiceProvider($this->app); 68 | $this->app = $serviceProvider->bindClasses($this->app); 69 | $this->app = $serviceProvider->bindScm($this->app); 70 | 71 | $this->app->bind('rocketeer.server', function ($app) { 72 | return new Rocketeer\Server($app, __DIR__.'/meta'); 73 | }); 74 | 75 | $command = $this->getCommand(); 76 | $this->app->singleton('rocketeer.tasks', function ($app) use ($command) { 77 | return new Rocketeer\TasksQueue($app, $command); 78 | }); 79 | 80 | // Bind dummy Task 81 | $this->task = $this->task('Cleanup'); 82 | $this->recreateVirtualServer(); 83 | } 84 | 85 | /** 86 | * Tears down the tests 87 | * 88 | * @return void 89 | */ 90 | public function tearDown() 91 | { 92 | Mockery::close(); 93 | } 94 | 95 | /** 96 | * Recreates the local file server 97 | * 98 | * @return void 99 | */ 100 | protected function recreateVirtualServer() 101 | { 102 | // Recreate deployments file 103 | $this->app['files']->put($this->deploymentsFile, json_encode(array( 104 | "foo" => "bar", 105 | "current_release" => 20000000000000, 106 | "directory_separator" => "/", 107 | "is_setup" => true, 108 | "webuser" => array("username" => "www-datda","group" => "www-datda"), 109 | "line_endings" => "\n", 110 | ))); 111 | 112 | // Recreate altered local server 113 | $this->app['files']->deleteDirectory(__DIR__.'/../storage'); 114 | $folders = array('current', 'shared', 'releases', 'releases/10000000000000', 'releases/20000000000000'); 115 | foreach ($folders as $folder) { 116 | $folder = $this->server.'/'.$folder; 117 | 118 | $this->app['files']->deleteDirectory($folder); 119 | $this->app['files']->delete($folder); 120 | $this->app['files']->makeDirectory($folder, 0777, true); 121 | file_put_contents($folder.'/.gitkeep', ''); 122 | } 123 | 124 | // Delete rocketeer binary 125 | $binary = __DIR__.'/../rocketeer.php'; 126 | $this->app['files']->delete($binary); 127 | } 128 | 129 | //////////////////////////////////////////////////////////////////// 130 | /////////////////////////////// HELPERS //////////////////////////// 131 | //////////////////////////////////////////////////////////////////// 132 | 133 | /** 134 | * Get a pretend Task to run bogus commands 135 | * 136 | * @return Task 137 | */ 138 | protected function pretendTask($task = 'Deploy', $options = array()) 139 | { 140 | // Default options 141 | $default = array('pretend' => true, 'verbose' => false); 142 | $options = array_merge($default, $options); 143 | 144 | // Create command 145 | $command = clone $this->getCommand(); 146 | foreach ($options as $name => $value) { 147 | $command->shouldReceive('option')->with($name)->andReturn($value); 148 | } 149 | 150 | // Bind it to Task 151 | $task = $this->task($task); 152 | $task->command = $command; 153 | 154 | return $task; 155 | } 156 | 157 | /** 158 | * Get Task instance 159 | * 160 | * @param string $task 161 | * 162 | * @return Task 163 | */ 164 | protected function task($task = null, $command = null) 165 | { 166 | if ($command) { 167 | $this->app->singleton('rocketeer.tasks', function ($app) use ($command) { 168 | return new Rocketeer\TasksQueue($app, $command); 169 | }); 170 | } 171 | 172 | if (!$task) { 173 | return $this->task; 174 | } 175 | 176 | return $this->tasksQueue()->buildTask('Rocketeer\Tasks\\'.$task); 177 | } 178 | 179 | /** 180 | * Get TasksQueue instance 181 | * 182 | * @return TasksQueue 183 | */ 184 | protected function tasksQueue() 185 | { 186 | return $this->app['rocketeer.tasks']; 187 | } 188 | 189 | //////////////////////////////////////////////////////////////////// 190 | ///////////////////////////// DEPENDENCIES ///////////////////////// 191 | //////////////////////////////////////////////////////////////////// 192 | 193 | /** 194 | * Mock the Command class 195 | * 196 | * @return Mockery 197 | */ 198 | protected function getCommand() 199 | { 200 | $message = function ($message) { 201 | return $message; 202 | }; 203 | 204 | $command = Mockery::mock('Command'); 205 | $command->shouldReceive('comment')->andReturnUsing($message); 206 | $command->shouldReceive('error')->andReturnUsing($message); 207 | $command->shouldReceive('line')->andReturnUsing($message); 208 | $command->shouldReceive('info')->andReturnUsing($message); 209 | $command->shouldReceive('argument'); 210 | $command->shouldReceive('ask'); 211 | $command->shouldReceive('confirm')->andReturn(true); 212 | $command->shouldReceive('secret'); 213 | $command->shouldReceive('option')->andReturn(null)->byDefault(); 214 | 215 | return $command; 216 | } 217 | 218 | /** 219 | * Mock the Config component 220 | * 221 | * @return Mockery 222 | */ 223 | protected function getConfig($options = array()) 224 | { 225 | $config = Mockery::mock('Illuminate\Config\Repository'); 226 | $config->shouldIgnoreMissing(); 227 | 228 | foreach ($options as $key => $value) { 229 | $config->shouldReceive('get')->with($key)->andReturn($value); 230 | } 231 | 232 | // Drivers 233 | $config->shouldReceive('get')->with('cache.driver')->andReturn('file'); 234 | $config->shouldReceive('get')->with('database.default')->andReturn('mysql'); 235 | $config->shouldReceive('get')->with('remote.default')->andReturn('production'); 236 | $config->shouldReceive('get')->with('remote.connections')->andReturn(array('production' => array(), 'staging' => array())); 237 | $config->shouldReceive('get')->with('session.driver')->andReturn('file'); 238 | 239 | // Rocketeer 240 | $config->shouldReceive('get')->with('rocketeer::connections')->andReturn(array('production', 'staging')); 241 | $config->shouldReceive('get')->with('rocketeer::remote.application_name')->andReturn('foobar'); 242 | $config->shouldReceive('get')->with('rocketeer::remote.keep_releases')->andReturn(1); 243 | $config->shouldReceive('get')->with('rocketeer::remote.permissions')->andReturn(array( 244 | 'permissions' => 755, 245 | 'webuser' => array('user' => 'www-data', 'group' => 'www-data') 246 | )); 247 | $config->shouldReceive('get')->with('rocketeer::remote.permissions.files')->andReturn(array('tests')); 248 | $config->shouldReceive('get')->with('rocketeer::remote.root_directory')->andReturn(__DIR__.'/server/'); 249 | $config->shouldReceive('get')->with('rocketeer::remote.shared')->andReturn(array('tests/meta')); 250 | $config->shouldReceive('get')->with('rocketeer::stages.default')->andReturn(null); 251 | $config->shouldReceive('get')->with('rocketeer::stages.stages')->andReturn(array()); 252 | 253 | // SCM 254 | $config->shouldReceive('get')->with('rocketeer::scm.branch')->andReturn('master'); 255 | $config->shouldReceive('get')->with('rocketeer::scm.repository')->andReturn('https://github.com/Anahkiasen/rocketeer.git'); 256 | $config->shouldReceive('get')->with('rocketeer::scm.scm')->andReturn('git'); 257 | 258 | // Tasks 259 | $config->shouldReceive('get')->with('rocketeer::tasks')->andReturn(array( 260 | 'before' => array( 261 | 'deploy' => array( 262 | 'before', 263 | 'foobar' 264 | ), 265 | ), 266 | 'after' => array( 267 | 'check' => array( 268 | 'Tasks\MyCustomTask', 269 | ), 270 | 'Rocketeer\Tasks\Deploy' => array( 271 | 'after', 272 | 'foobar' 273 | ), 274 | ), 275 | )); 276 | 277 | return $config; 278 | } 279 | 280 | /** 281 | * Swap the current config 282 | * 283 | * @param array $config 284 | * 285 | * @return void 286 | */ 287 | protected function swapConfig($config) 288 | { 289 | $this->app['rocketeer.rocketeer']->disconnect(); 290 | $this->app['config'] = $this->getConfig($config); 291 | } 292 | 293 | /** 294 | * Mock the Remote component 295 | * 296 | * @return Mockery 297 | */ 298 | protected function getRemote() 299 | { 300 | $run = function ($task, $callback) { 301 | if (is_array($task)) { 302 | $task = implode(' && ', $task); 303 | } 304 | $output = shell_exec($task); 305 | 306 | $callback($output); 307 | }; 308 | 309 | $remote = Mockery::mock('Illuminate\Remote\Connection'); 310 | $remote->shouldReceive('into')->andReturn(Mockery::self()); 311 | $remote->shouldReceive('status')->andReturn(0)->byDefault(); 312 | $remote->shouldReceive('run')->andReturnUsing($run)->byDefault(); 313 | $remote->shouldReceive('runRaw')->andReturnUsing($run)->byDefault(); 314 | $remote->shouldReceive('display')->andReturnUsing(function ($line) { 315 | print $line.PHP_EOL; 316 | }); 317 | 318 | return $remote; 319 | } 320 | 321 | /** 322 | * Mock Artisan 323 | * 324 | * @return Mockery 325 | */ 326 | protected function getArtisan() 327 | { 328 | $artisan = Mockery::mock('Artisan'); 329 | $artisan->shouldReceive('add')->andReturnUsing(function ($command) { 330 | return $command; 331 | }); 332 | 333 | return $artisan; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/Rocketeer/Bash.php: -------------------------------------------------------------------------------- 1 | app = $app; 49 | $this->command = $command; 50 | } 51 | 52 | /** 53 | * Get an instance from the Container 54 | * 55 | * @param string $key 56 | * 57 | * @return object 58 | */ 59 | public function __get($key) 60 | { 61 | $shortcuts = array( 62 | 'releasesManager' => 'rocketeer.releases', 63 | 'server' => 'rocketeer.server', 64 | 'rocketeer' => 'rocketeer.rocketeer', 65 | 'scm' => 'rocketeer.scm', 66 | ); 67 | 68 | // Replace shortcuts 69 | if (array_key_exists($key, $shortcuts)) { 70 | $key = $shortcuts[$key]; 71 | } 72 | 73 | return $this->app[$key]; 74 | } 75 | 76 | /** 77 | * Set an instance on the Container 78 | * 79 | * @param string $key 80 | * @param object $value 81 | */ 82 | public function __set($key, $value) 83 | { 84 | $this->app[$key] = $value; 85 | } 86 | 87 | //////////////////////////////////////////////////////////////////// 88 | ///////////////////////////// CORE METHODS ///////////////////////// 89 | //////////////////////////////////////////////////////////////////// 90 | 91 | /** 92 | * Run actions on the remote server and gather the ouput 93 | * 94 | * @param string|array $commands One or more commands 95 | * @param boolean $silent Whether the command should stay silent no matter what 96 | * @param boolean $array Whether the output should be returned as an array 97 | * 98 | * @return string|array 99 | */ 100 | public function run($commands, $silent = false, $array = false) 101 | { 102 | $output = null; 103 | $commands = $this->processCommands($commands); 104 | $verbose = $this->getOption('verbose') and !$silent; 105 | 106 | // Log the commands for pretend 107 | if ($this->getOption('pretend') and !$silent) { 108 | $this->command->line(implode(PHP_EOL, $commands)); 109 | $commands = (sizeof($commands) == 1) ? $commands[0] : $commands; 110 | $this->history[] = $commands; 111 | 112 | return $commands; 113 | } 114 | 115 | // Run commands 116 | $bash = $this; 117 | $this->remote->run($commands, function ($results) use (&$output, $verbose, $bash) { 118 | $output .= $results; 119 | 120 | if ($verbose) { 121 | $bash->remote->display(trim($results)); 122 | } 123 | }); 124 | 125 | // Explode output if necessary 126 | if ($array) { 127 | $output = explode($this->server->getLineEndings(), $output); 128 | } 129 | 130 | // Trim output 131 | $output = is_array($output) 132 | ? array_filter($output) 133 | : trim($output); 134 | 135 | // Append output 136 | $this->history[] = $output; 137 | 138 | return $output; 139 | } 140 | 141 | /** 142 | * Run a raw command, without any processing, and 143 | * get its output as a string or array 144 | * 145 | * @param string|array $commands 146 | * @param boolean $array Whether the output should be returned as an array 147 | * 148 | * @return string 149 | */ 150 | public function runRaw($commands, $array = false) 151 | { 152 | $output = null; 153 | 154 | // Run commands 155 | $this->remote->run($commands, function ($results) use (&$output) { 156 | $output .= $results; 157 | }); 158 | 159 | // Explode output if necessary 160 | if ($array) { 161 | $output = explode($this->server->getLineEndings(), $output); 162 | $output = array_filter($output); 163 | } 164 | 165 | return $output; 166 | } 167 | 168 | /** 169 | * Run commands in a folder 170 | * 171 | * @param string $folder 172 | * @param string|array $tasks 173 | * 174 | * @return string 175 | */ 176 | public function runInFolder($folder = null, $tasks = array()) 177 | { 178 | // Convert to array 179 | if (!is_array($tasks)) { 180 | $tasks = array($tasks); 181 | } 182 | 183 | // Prepend folder 184 | array_unshift($tasks, 'cd '.$this->rocketeer->getFolder($folder)); 185 | 186 | return $this->run($tasks); 187 | } 188 | 189 | /** 190 | * Get a binary 191 | * 192 | * @param string $binary The name of the binary 193 | * @param string $fallback A fallback location 194 | * 195 | * @return string 196 | */ 197 | public function which($binary, $fallback = null) 198 | { 199 | // Get custom path if any was set 200 | $custom = 'paths.'.$binary; 201 | if ($location = $this->server->getValue($custom)) { 202 | return $location; 203 | } 204 | 205 | // Else ask the server where the binary is 206 | $location = $this->run('which '.$binary, true); 207 | if ($location and $this->fileExists($location)) { 208 | return $location; 209 | } 210 | 211 | // Else use the fallback path 212 | if ($fallback) { 213 | $location = $this->run('which '.$fallback, true); 214 | if ($location and $this->fileExists($location)) { 215 | return $location; 216 | } 217 | } 218 | 219 | // Else prompt the User for the actual path 220 | $location = $this->command->ask($binary. ' could not be found, please enter the path to it'); 221 | if ($location) { 222 | $this->server->setValue($custom, $location); 223 | return $location; 224 | } 225 | 226 | return false; 227 | } 228 | 229 | /** 230 | * Check the status of the last run command, return an error if any 231 | * 232 | * @param string $error The message to display on error 233 | * @param string $output The command's output 234 | * @param string $success The message to display on success 235 | * 236 | * @return boolean|string 237 | */ 238 | public function checkStatus($error, $output = null, $success = null) 239 | { 240 | // If all went well 241 | if ($this->remote->status() == 0) { 242 | if ($success) { 243 | $this->command->comment($success); 244 | } 245 | 246 | return $output; 247 | } 248 | 249 | // Else 250 | $this->command->error($error); 251 | print $output.PHP_EOL; 252 | 253 | return false; 254 | } 255 | 256 | //////////////////////////////////////////////////////////////////// 257 | /////////////////////////////// FOLDERS //////////////////////////// 258 | //////////////////////////////////////////////////////////////////// 259 | 260 | /** 261 | * Symlinks two folders 262 | * 263 | * @param string $folder The folder in shared/ 264 | * @param string $symlink The folder that will symlink to it 265 | * 266 | * @return string 267 | */ 268 | public function symlink($folder, $symlink) 269 | { 270 | if (!$this->fileExists($folder)) { 271 | if (!$this->fileExists($symlink)) { 272 | return false; 273 | } 274 | 275 | $this->move($symlink, $folder); 276 | } 277 | 278 | // Remove existing symlink 279 | $this->removeFolder($symlink); 280 | 281 | return $this->run(sprintf('ln -s %s %s', $folder, $symlink)); 282 | } 283 | 284 | /** 285 | * Move a file 286 | * 287 | * @param string $origin 288 | * @param string $destination 289 | * 290 | * @return string 291 | */ 292 | public function move($origin, $destination) 293 | { 294 | $folder = dirname($destination); 295 | if (!$this->fileExists($folder)) { 296 | $this->createFolder($folder, true); 297 | } 298 | 299 | return $this->run(sprintf('mv %s %s', $origin, $destination)); 300 | } 301 | 302 | /** 303 | * Get the contents of a directory 304 | * 305 | * @param string $directory 306 | * 307 | * @return array 308 | */ 309 | public function listContents($directory) 310 | { 311 | return $this->run('ls '.$directory, false, true); 312 | } 313 | 314 | /** 315 | * Check if a file exists 316 | * 317 | * @param string $file Path to the file 318 | * 319 | * @return boolean 320 | */ 321 | public function fileExists($file) 322 | { 323 | $exists = $this->runRaw('if [ -e ' .$file. ' ]; then echo "true"; fi'); 324 | 325 | return trim($exists) == 'true'; 326 | } 327 | 328 | /** 329 | * Create a folder in the application's folder 330 | * 331 | * @param string $folder The folder to create 332 | * @param boolean $recursive 333 | * 334 | * @return string The task 335 | */ 336 | public function createFolder($folder = null, $recursive = false) 337 | { 338 | $recursive = $recursive ? '-p ' : null; 339 | 340 | return $this->run('mkdir '.$recursive.$this->rocketeer->getFolder($folder)); 341 | } 342 | 343 | /** 344 | * Remove a folder in the application's folder 345 | * 346 | * @param string $folder The folder to remove 347 | * 348 | * @return string The task 349 | */ 350 | public function removeFolder($folder = null) 351 | { 352 | return $this->run('rm -rf '.$this->rocketeer->getFolder($folder)); 353 | } 354 | 355 | //////////////////////////////////////////////////////////////////// 356 | /////////////////////////////// HELPERS //////////////////////////// 357 | //////////////////////////////////////////////////////////////////// 358 | 359 | /** 360 | * Get an option from the Command 361 | * 362 | * @param string $option 363 | * 364 | * @return string 365 | */ 366 | protected function getOption($option) 367 | { 368 | return $this->command ? $this->command->option($option) : null; 369 | } 370 | 371 | /** 372 | * Process an array of commands 373 | * 374 | * @param string|array $commands 375 | * 376 | * @return array 377 | */ 378 | protected function processCommands($commands) 379 | { 380 | $stage = $this->rocketeer->getStage(); 381 | $separator = $this->server->getSeparator(); 382 | 383 | // Cast commands to array 384 | if (!is_array($commands)) { 385 | $commands = array($commands); 386 | } 387 | 388 | // Process commands 389 | foreach ($commands as &$command) { 390 | 391 | // Replace directory separators 392 | if (DS !== $separator) { 393 | $command = str_replace(DS, $separator, $command); 394 | } 395 | 396 | // Add stage flag 397 | if (Str::startsWith($command, 'php artisan') and $stage) { 398 | $command .= ' --env='.$stage; 399 | } 400 | 401 | } 402 | 403 | return $commands; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/Rocketeer/TasksQueue.php: -------------------------------------------------------------------------------- 1 | app = $app; 58 | $this->tasks = $app['config']->get('rocketeer::tasks'); 59 | $this->command = $command; 60 | } 61 | 62 | //////////////////////////////////////////////////////////////////// 63 | ////////////////////////// PUBLIC INTERFACE //////////////////////// 64 | //////////////////////////////////////////////////////////////////// 65 | 66 | /** 67 | * Register a custom Task with Rocketeer 68 | * 69 | * @param Task|string $task 70 | * 71 | * @return Container 72 | */ 73 | public function add($task) 74 | { 75 | // Build Task if necessary 76 | if (is_string($task)) { 77 | $task = $this->buildTask($task); 78 | } 79 | 80 | return $this->app['artisan']->add(new BaseTaskCommand($task)); 81 | } 82 | 83 | /** 84 | * Execute a Task before another one 85 | * 86 | * @param string $task 87 | * @param string|Closure|Task $surroundingTask 88 | * 89 | * @return void 90 | */ 91 | public function before($task, $surroundingTask) 92 | { 93 | $this->addSurroundingTask($task, $surroundingTask, 'before'); 94 | } 95 | 96 | /** 97 | * Execute a Task after another one 98 | * 99 | * @param string $task 100 | * @param string|Closure|Task $surroundingTask 101 | * 102 | * @return void 103 | */ 104 | public function after($task, $surroundingTask) 105 | { 106 | $this->addSurroundingTask($task, $surroundingTask, 'after'); 107 | } 108 | 109 | /** 110 | * Get the tasks to execute before a Task 111 | * 112 | * @param Task $task 113 | * 114 | * @return array 115 | */ 116 | public function getBefore(Task $task) 117 | { 118 | return $this->getSurroundingTasks($task, 'before'); 119 | } 120 | 121 | /** 122 | * Get the tasks to execute after a Task 123 | * 124 | * @param Task $task 125 | * 126 | * @return array 127 | */ 128 | public function getAfter(Task $task) 129 | { 130 | return $this->getSurroundingTasks($task, 'after'); 131 | } 132 | 133 | /** 134 | * Execute Tasks on the default connection 135 | * 136 | * @param string|array|Closure $task 137 | * 138 | * @return array 139 | */ 140 | public function execute($queue) 141 | { 142 | $queue = (array) $queue; 143 | $queue = $this->buildQueue($queue); 144 | 145 | return $this->run($queue); 146 | } 147 | 148 | /** 149 | * Execute Tasks on various connections 150 | * 151 | * @param string|array $connections 152 | * @param string|array|Closure $queue 153 | * 154 | * @return array 155 | */ 156 | public function on($connections, $queue) 157 | { 158 | $this->app['rocketeer.rocketeer']->setConnections($connections); 159 | 160 | $queue = (array) $queue; 161 | $queue = $this->buildQueue($queue); 162 | 163 | return $this->run($queue); 164 | } 165 | 166 | //////////////////////////////////////////////////////////////////// 167 | //////////////////////////////// QUEUE ///////////////////////////// 168 | //////////////////////////////////////////////////////////////////// 169 | 170 | /** 171 | * Run the queue 172 | * 173 | * Here we will actually process the queue to take into account the 174 | * various ways to hook into the queue : Tasks, Closures and Commands 175 | * 176 | * @param array $tasks An array of tasks 177 | * @param Command $command The command executing the tasks 178 | * 179 | * @return array An array of output 180 | */ 181 | public function run(array $tasks, $command = null) 182 | { 183 | $this->command = $command; 184 | $queue = $this->buildQueue($tasks); 185 | 186 | // Get the connections to execute the tasks on 187 | $connections = (array) $this->app['rocketeer.rocketeer']->getConnections(); 188 | foreach ($connections as $connection) { 189 | $this->app['rocketeer.rocketeer']->setConnection($connection); 190 | 191 | // Check if we provided a stage 192 | $stage = $this->getStage(); 193 | $stages = $this->app['rocketeer.rocketeer']->getStages(); 194 | if ($stage and in_array($stage, $stages)) { 195 | $stages = array($stage); 196 | } 197 | 198 | // Run the Tasks on each stage 199 | if (!empty($stages)) { 200 | foreach ($stages as $stage) { 201 | $state = $this->runQueue($queue, $stage); 202 | } 203 | } else { 204 | $state = $this->runQueue($queue); 205 | } 206 | } 207 | 208 | return $this->output; 209 | } 210 | 211 | /** 212 | * Run the queue, taking into account the stage 213 | * 214 | * @param array $tasks 215 | * @param string $stage 216 | * 217 | * @return boolean 218 | */ 219 | protected function runQueue($tasks, $stage = null) 220 | { 221 | foreach ($tasks as $task) { 222 | $currentStage = $task->usesStages() ? $stage : null; 223 | $this->app['rocketeer.rocketeer']->setStage($currentStage); 224 | 225 | $state = $task->execute(); 226 | $this->output[] = $state; 227 | if ($state === false) { 228 | return false; 229 | } 230 | } 231 | 232 | return true; 233 | } 234 | 235 | /** 236 | * Build a queue from a list of tasks 237 | * 238 | * Here we will take the various Task names or actual Task instances 239 | * provided by the user, get the Tasks to execute before and after 240 | * each one, and flatten the whole thing into an actual queue 241 | * 242 | * @param array $tasks 243 | * 244 | * @return array 245 | */ 246 | public function buildQueue(array $tasks) 247 | { 248 | $queue = array(); 249 | foreach ($tasks as $task) { 250 | 251 | // If we provided a Closure or a string command, add straight to queue 252 | if ($task instanceof Closure or (is_string($task) and !class_exists($task))) { 253 | $queue[] = $task; 254 | continue; 255 | } 256 | 257 | // Else build class and add to queue 258 | if (!($task instanceof Task)) { 259 | $task = $this->buildTask($task); 260 | } 261 | 262 | $queue = array_merge($queue, $this->getBefore($task), array($task), $this->getAfter($task)); 263 | } 264 | 265 | // Build the tasks provided as Closures/strings 266 | foreach ($queue as &$task) { 267 | if (!($task instanceof Task)) { 268 | $task = $this->buildTaskFromClosure($task); 269 | } 270 | } 271 | 272 | return $queue; 273 | } 274 | 275 | /** 276 | * Build a Task from a Closure or a string command 277 | * 278 | * @param Closure|string $task 279 | * 280 | * @return Task 281 | */ 282 | public function buildTaskFromClosure($task) 283 | { 284 | // If the User provided a string to execute 285 | if (is_string($task) and !class_exists($task)) { 286 | $stringTask = $task; 287 | $closure = function ($task) use ($stringTask) { 288 | return $task->runForCurrentRelease($stringTask); 289 | }; 290 | 291 | // If the User provided a Closure 292 | } elseif ($task instanceof Closure) { 293 | $closure = $task; 294 | } 295 | 296 | // Build the ClosureTask 297 | if (isset($closure)) { 298 | $task = $this->buildTask('Rocketeer\Tasks\Closure'); 299 | $task->setClosure($closure); 300 | } 301 | 302 | if (!($task instanceof Task)) { 303 | $task = $this->buildTask($task); 304 | } 305 | 306 | return $task; 307 | } 308 | 309 | /** 310 | * Build a Task from its name 311 | * 312 | * @param string $task 313 | * 314 | * @return Task 315 | */ 316 | public function buildTask($task) 317 | { 318 | // Shortcut for calling Rocketeer Tasks 319 | if (class_exists('Rocketeer\Tasks\\'.$task)) { 320 | $task = 'Rocketeer\Tasks\\'.$task; 321 | } 322 | 323 | return new $task( 324 | $this->app, 325 | $this->command 326 | ); 327 | } 328 | 329 | //////////////////////////////////////////////////////////////////// 330 | ///////////////////////////// SURROUNDINGS ///////////////////////// 331 | //////////////////////////////////////////////////////////////////// 332 | 333 | /** 334 | * Add a Task to surround another Task 335 | * 336 | * @param string $task 337 | * @param mixed $surroundingTask 338 | * @param string $position before|after 339 | */ 340 | protected function addSurroundingTask($task, $surroundingTask, $position) 341 | { 342 | // Recursive call 343 | if (is_array($task)) { 344 | foreach ($task as $t) { 345 | $this->addSurroundingTask($t, $surroundingTask, $position); 346 | } 347 | 348 | return; 349 | } 350 | 351 | // Create array if it doesn't exist 352 | if (!array_key_exists($task, $this->tasks[$position])) { 353 | $this->tasks[$position][$task] = array(); 354 | } 355 | 356 | // Add Task to Tasks 357 | if (is_array($surroundingTask)) { 358 | $this->tasks[$position][$task] = array_merge($this->tasks[$position][$task], $surroundingTask); 359 | } else { 360 | $this->tasks[$position][$task][] = $surroundingTask; 361 | } 362 | } 363 | 364 | /** 365 | * Get the tasks surrounding another Task 366 | * 367 | * @param Task $task 368 | * @param string $position before|after 369 | * 370 | * @return array 371 | */ 372 | protected function getSurroundingTasks(Task $task, $position) 373 | { 374 | // First we look for the fully qualified class name 375 | $key = get_class($task); 376 | if (array_key_exists($key, $this->tasks[$position])) { 377 | $tasks = array_get($this->tasks, $position.'.'.$key); 378 | 379 | // Then for the class slug 380 | } else { 381 | $tasks = array_get($this->tasks, $position.'.'.$task->getSlug()); 382 | } 383 | 384 | return (array) $tasks; 385 | } 386 | 387 | //////////////////////////////////////////////////////////////////// 388 | //////////////////////////////// STAGES //////////////////////////// 389 | //////////////////////////////////////////////////////////////////// 390 | 391 | /** 392 | * Get the stage to execute Tasks in 393 | * If null, execute on all stages 394 | * 395 | * @return string 396 | */ 397 | protected function getStage() 398 | { 399 | $stage = $this->app['rocketeer.rocketeer']->getOption('stages.default'); 400 | if ($this->command) { 401 | $stage = $this->command->option('stage') ?: $stage; 402 | } 403 | 404 | // Return all stages if "all" 405 | if ($stage == 'all') { 406 | $stage = null; 407 | } 408 | 409 | return $stage; 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" 5 | ], 6 | "hash": "71efae810506470c9d4c7718dcf34cb4", 7 | "packages": [ 8 | { 9 | "name": "illuminate/config", 10 | "version": "dev-master", 11 | "target-dir": "Illuminate/Config", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/illuminate/config.git", 15 | "reference": "a48873b5e777ea09777291b02cb498032924844e" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/illuminate/config/zipball/a48873b5e777ea09777291b02cb498032924844e", 20 | "reference": "a48873b5e777ea09777291b02cb498032924844e", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "illuminate/filesystem": "4.1.x", 25 | "illuminate/support": "4.1.x", 26 | "php": ">=5.3.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "3.7.*" 30 | }, 31 | "type": "library", 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "4.1-dev" 35 | } 36 | }, 37 | "autoload": { 38 | "psr-0": { 39 | "Illuminate\\Config": "" 40 | } 41 | }, 42 | "notification-url": "https://packagist.org/downloads/", 43 | "license": [ 44 | "MIT" 45 | ], 46 | "authors": [ 47 | { 48 | "name": "Taylor Otwell", 49 | "email": "taylorotwell@gmail.com", 50 | "homepage": "https://github.com/taylorotwell", 51 | "role": "Developer" 52 | } 53 | ], 54 | "time": "2013-10-01 15:20:00" 55 | }, 56 | { 57 | "name": "illuminate/console", 58 | "version": "dev-master", 59 | "target-dir": "Illuminate/Console", 60 | "source": { 61 | "type": "git", 62 | "url": "https://github.com/illuminate/console.git", 63 | "reference": "0b4badcd1aecdb623d3e435d1c6d8797e772e233" 64 | }, 65 | "dist": { 66 | "type": "zip", 67 | "url": "https://api.github.com/repos/illuminate/console/zipball/0b4badcd1aecdb623d3e435d1c6d8797e772e233", 68 | "reference": "0b4badcd1aecdb623d3e435d1c6d8797e772e233", 69 | "shasum": "" 70 | }, 71 | "require": { 72 | "symfony/console": "2.4.*" 73 | }, 74 | "require-dev": { 75 | "phpunit/phpunit": "3.7.*" 76 | }, 77 | "type": "library", 78 | "extra": { 79 | "branch-alias": { 80 | "dev-master": "4.1-dev" 81 | } 82 | }, 83 | "autoload": { 84 | "psr-0": { 85 | "Illuminate\\Console": "" 86 | } 87 | }, 88 | "notification-url": "https://packagist.org/downloads/", 89 | "license": [ 90 | "MIT" 91 | ], 92 | "authors": [ 93 | { 94 | "name": "Taylor Otwell", 95 | "email": "taylorotwell@gmail.com", 96 | "homepage": "https://github.com/taylorotwell", 97 | "role": "Developer" 98 | } 99 | ], 100 | "time": "2013-10-08 21:46:57" 101 | }, 102 | { 103 | "name": "illuminate/container", 104 | "version": "dev-master", 105 | "target-dir": "Illuminate/Container", 106 | "source": { 107 | "type": "git", 108 | "url": "https://github.com/illuminate/container.git", 109 | "reference": "1444907a30937bf4b7a910489160ba4576f3afa9" 110 | }, 111 | "dist": { 112 | "type": "zip", 113 | "url": "https://api.github.com/repos/illuminate/container/zipball/1444907a30937bf4b7a910489160ba4576f3afa9", 114 | "reference": "1444907a30937bf4b7a910489160ba4576f3afa9", 115 | "shasum": "" 116 | }, 117 | "require": { 118 | "php": ">=5.3.0" 119 | }, 120 | "require-dev": { 121 | "phpunit/phpunit": "3.7.*" 122 | }, 123 | "type": "library", 124 | "extra": { 125 | "branch-alias": { 126 | "dev-master": "4.1-dev" 127 | } 128 | }, 129 | "autoload": { 130 | "psr-0": { 131 | "Illuminate\\Container": "" 132 | } 133 | }, 134 | "notification-url": "https://packagist.org/downloads/", 135 | "license": [ 136 | "MIT" 137 | ], 138 | "authors": [ 139 | { 140 | "name": "Taylor Otwell", 141 | "email": "taylorotwell@gmail.com", 142 | "homepage": "https://github.com/taylorotwell", 143 | "role": "Developer" 144 | } 145 | ], 146 | "time": "2013-10-02 21:50:28" 147 | }, 148 | { 149 | "name": "illuminate/filesystem", 150 | "version": "dev-master", 151 | "target-dir": "Illuminate/Filesystem", 152 | "source": { 153 | "type": "git", 154 | "url": "https://github.com/illuminate/filesystem.git", 155 | "reference": "98c6e3710853f4e0aee7ea14b09194083815bf1b" 156 | }, 157 | "dist": { 158 | "type": "zip", 159 | "url": "https://api.github.com/repos/illuminate/filesystem/zipball/98c6e3710853f4e0aee7ea14b09194083815bf1b", 160 | "reference": "98c6e3710853f4e0aee7ea14b09194083815bf1b", 161 | "shasum": "" 162 | }, 163 | "require": { 164 | "illuminate/support": "4.1.*", 165 | "php": ">=5.3.0", 166 | "symfony/finder": "2.4.*" 167 | }, 168 | "require-dev": { 169 | "phpunit/phpunit": "3.7.*" 170 | }, 171 | "type": "library", 172 | "extra": { 173 | "branch-alias": { 174 | "dev-master": "4.1-dev" 175 | } 176 | }, 177 | "autoload": { 178 | "psr-0": { 179 | "Illuminate\\Filesystem": "" 180 | } 181 | }, 182 | "notification-url": "https://packagist.org/downloads/", 183 | "license": [ 184 | "MIT" 185 | ], 186 | "authors": [ 187 | { 188 | "name": "Taylor Otwell", 189 | "email": "taylorotwell@gmail.com", 190 | "homepage": "https://github.com/taylorotwell", 191 | "role": "Developer" 192 | } 193 | ], 194 | "time": "2013-10-08 21:46:57" 195 | }, 196 | { 197 | "name": "illuminate/support", 198 | "version": "dev-master", 199 | "target-dir": "Illuminate/Support", 200 | "source": { 201 | "type": "git", 202 | "url": "https://github.com/illuminate/support.git", 203 | "reference": "33df9bdf3d9b38353097d2e6cfdfc4ebfe9900c1" 204 | }, 205 | "dist": { 206 | "type": "zip", 207 | "url": "https://api.github.com/repos/illuminate/support/zipball/33df9bdf3d9b38353097d2e6cfdfc4ebfe9900c1", 208 | "reference": "33df9bdf3d9b38353097d2e6cfdfc4ebfe9900c1", 209 | "shasum": "" 210 | }, 211 | "require": { 212 | "php": ">=5.3.0" 213 | }, 214 | "require-dev": { 215 | "mockery/mockery": "0.7.2", 216 | "patchwork/utf8": "1.1.*", 217 | "phpunit/phpunit": "3.7.*" 218 | }, 219 | "type": "library", 220 | "extra": { 221 | "branch-alias": { 222 | "dev-master": "4.1-dev" 223 | } 224 | }, 225 | "autoload": { 226 | "psr-0": { 227 | "Illuminate\\Support": "" 228 | }, 229 | "files": [ 230 | "Illuminate/Support/helpers.php" 231 | ] 232 | }, 233 | "notification-url": "https://packagist.org/downloads/", 234 | "license": [ 235 | "MIT" 236 | ], 237 | "authors": [ 238 | { 239 | "name": "Taylor Otwell", 240 | "email": "taylorotwell@gmail.com", 241 | "homepage": "https://github.com/taylorotwell", 242 | "role": "Developer" 243 | } 244 | ], 245 | "time": "2013-10-03 21:32:36" 246 | }, 247 | { 248 | "name": "symfony/console", 249 | "version": "dev-master", 250 | "target-dir": "Symfony/Component/Console", 251 | "source": { 252 | "type": "git", 253 | "url": "https://github.com/symfony/Console.git", 254 | "reference": "608960cd7f7e906e64d6586b9152e308f7ea0ff2" 255 | }, 256 | "dist": { 257 | "type": "zip", 258 | "url": "https://api.github.com/repos/symfony/Console/zipball/608960cd7f7e906e64d6586b9152e308f7ea0ff2", 259 | "reference": "608960cd7f7e906e64d6586b9152e308f7ea0ff2", 260 | "shasum": "" 261 | }, 262 | "require": { 263 | "php": ">=5.3.3" 264 | }, 265 | "require-dev": { 266 | "symfony/event-dispatcher": "~2.1" 267 | }, 268 | "suggest": { 269 | "symfony/event-dispatcher": "" 270 | }, 271 | "type": "library", 272 | "extra": { 273 | "branch-alias": { 274 | "dev-master": "2.4-dev" 275 | } 276 | }, 277 | "autoload": { 278 | "psr-0": { 279 | "Symfony\\Component\\Console\\": "" 280 | } 281 | }, 282 | "notification-url": "https://packagist.org/downloads/", 283 | "license": [ 284 | "MIT" 285 | ], 286 | "authors": [ 287 | { 288 | "name": "Fabien Potencier", 289 | "email": "fabien@symfony.com" 290 | }, 291 | { 292 | "name": "Symfony Community", 293 | "homepage": "http://symfony.com/contributors" 294 | } 295 | ], 296 | "description": "Symfony Console Component", 297 | "homepage": "http://symfony.com", 298 | "time": "2013-10-16 16:16:10" 299 | }, 300 | { 301 | "name": "symfony/finder", 302 | "version": "dev-master", 303 | "target-dir": "Symfony/Component/Finder", 304 | "source": { 305 | "type": "git", 306 | "url": "https://github.com/symfony/Finder.git", 307 | "reference": "e2ce3164ab58b4d54612e630571f158035ee8603" 308 | }, 309 | "dist": { 310 | "type": "zip", 311 | "url": "https://api.github.com/repos/symfony/Finder/zipball/e2ce3164ab58b4d54612e630571f158035ee8603", 312 | "reference": "e2ce3164ab58b4d54612e630571f158035ee8603", 313 | "shasum": "" 314 | }, 315 | "require": { 316 | "php": ">=5.3.3" 317 | }, 318 | "type": "library", 319 | "extra": { 320 | "branch-alias": { 321 | "dev-master": "2.4-dev" 322 | } 323 | }, 324 | "autoload": { 325 | "psr-0": { 326 | "Symfony\\Component\\Finder\\": "" 327 | } 328 | }, 329 | "notification-url": "https://packagist.org/downloads/", 330 | "license": [ 331 | "MIT" 332 | ], 333 | "authors": [ 334 | { 335 | "name": "Fabien Potencier", 336 | "email": "fabien@symfony.com" 337 | }, 338 | { 339 | "name": "Symfony Community", 340 | "homepage": "http://symfony.com/contributors" 341 | } 342 | ], 343 | "description": "Symfony Finder Component", 344 | "homepage": "http://symfony.com", 345 | "time": "2013-09-19 09:47:34" 346 | } 347 | ], 348 | "packages-dev": [ 349 | { 350 | "name": "mockery/mockery", 351 | "version": "dev-master", 352 | "source": { 353 | "type": "git", 354 | "url": "https://github.com/padraic/mockery.git", 355 | "reference": "09ab879a09def2a658d6e8030f88432cc479f5a8" 356 | }, 357 | "dist": { 358 | "type": "zip", 359 | "url": "https://api.github.com/repos/padraic/mockery/zipball/09ab879a09def2a658d6e8030f88432cc479f5a8", 360 | "reference": "09ab879a09def2a658d6e8030f88432cc479f5a8", 361 | "shasum": "" 362 | }, 363 | "require": { 364 | "lib-pcre": ">=7.0", 365 | "php": ">=5.3.2" 366 | }, 367 | "require-dev": { 368 | "hamcrest/hamcrest": "1.1.0" 369 | }, 370 | "type": "library", 371 | "autoload": { 372 | "psr-0": { 373 | "Mockery": "library/" 374 | } 375 | }, 376 | "notification-url": "https://packagist.org/downloads/", 377 | "license": [ 378 | "BSD-3-Clause" 379 | ], 380 | "authors": [ 381 | { 382 | "name": "Pádraic Brady", 383 | "email": "padraic.brady@gmail.com", 384 | "homepage": "http://blog.astrumfutura.com" 385 | } 386 | ], 387 | "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", 388 | "homepage": "http://github.com/padraic/mockery", 389 | "keywords": [ 390 | "BDD", 391 | "TDD", 392 | "library", 393 | "mock", 394 | "mock objects", 395 | "mockery", 396 | "stub", 397 | "test", 398 | "test double", 399 | "testing" 400 | ], 401 | "time": "2013-10-18 15:18:30" 402 | }, 403 | { 404 | "name": "nesbot/carbon", 405 | "version": "dev-master", 406 | "source": { 407 | "type": "git", 408 | "url": "https://github.com/briannesbitt/Carbon.git", 409 | "reference": "06f0b8a99a90c5392ceccb09b75b74ff6c08ec07" 410 | }, 411 | "dist": { 412 | "type": "zip", 413 | "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/06f0b8a99a90c5392ceccb09b75b74ff6c08ec07", 414 | "reference": "06f0b8a99a90c5392ceccb09b75b74ff6c08ec07", 415 | "shasum": "" 416 | }, 417 | "require": { 418 | "php": ">=5.3.0" 419 | }, 420 | "type": "library", 421 | "autoload": { 422 | "psr-0": { 423 | "Carbon": "src" 424 | } 425 | }, 426 | "notification-url": "https://packagist.org/downloads/", 427 | "license": [ 428 | "MIT" 429 | ], 430 | "authors": [ 431 | { 432 | "name": "Brian Nesbitt", 433 | "email": "brian@nesbot.com", 434 | "homepage": "http://nesbot.com" 435 | } 436 | ], 437 | "description": "A simple API extension for DateTime.", 438 | "homepage": "https://github.com/briannesbitt/Carbon", 439 | "keywords": [ 440 | "date", 441 | "datetime", 442 | "time" 443 | ], 444 | "time": "2013-09-09 02:39:19" 445 | }, 446 | { 447 | "name": "patchwork/utf8", 448 | "version": "dev-master", 449 | "source": { 450 | "type": "git", 451 | "url": "https://github.com/nicolas-grekas/Patchwork-UTF8.git", 452 | "reference": "efd5478a233f6940d3296ab27c2a02ba47831968" 453 | }, 454 | "dist": { 455 | "type": "zip", 456 | "url": "https://api.github.com/repos/nicolas-grekas/Patchwork-UTF8/zipball/efd5478a233f6940d3296ab27c2a02ba47831968", 457 | "reference": "efd5478a233f6940d3296ab27c2a02ba47831968", 458 | "shasum": "" 459 | }, 460 | "require": { 461 | "lib-pcre": "*", 462 | "php": ">=5.3.0" 463 | }, 464 | "suggest": { 465 | "ext-mbstring": "Use Mbstring for best performance", 466 | "lib-iconv": "Use iconv for best performance", 467 | "lib-icu": "Use Intl for best performance" 468 | }, 469 | "type": "library", 470 | "autoload": { 471 | "psr-0": { 472 | "Patchwork": "class/", 473 | "Normalizer": "class/" 474 | } 475 | }, 476 | "notification-url": "https://packagist.org/downloads/", 477 | "license": [ 478 | "(Apache-2.0 or GPL-2.0)" 479 | ], 480 | "authors": [ 481 | { 482 | "name": "Nicolas Grekas", 483 | "email": "p@tchwork.com", 484 | "role": "Developer" 485 | } 486 | ], 487 | "description": "Extensive, portable and performant handling of UTF-8 and grapheme clusters for PHP", 488 | "homepage": "https://github.com/nicolas-grekas/Patchwork-UTF8", 489 | "keywords": [ 490 | "i18n", 491 | "unicode", 492 | "utf-8", 493 | "utf8" 494 | ], 495 | "time": "2013-10-15 08:18:30" 496 | } 497 | ], 498 | "aliases": [ 499 | 500 | ], 501 | "minimum-stability": "dev", 502 | "stability-flags": { 503 | "mockery/mockery": 20, 504 | "nesbot/carbon": 20, 505 | "patchwork/utf8": 20 506 | }, 507 | "platform": { 508 | "php": ">=5.3.0" 509 | }, 510 | "platform-dev": [ 511 | 512 | ] 513 | } 514 | --------------------------------------------------------------------------------