├── .gitignore ├── README.md ├── app ├── dependencies.php ├── middleware.php ├── routes.php ├── settings.php ├── src │ ├── Classes │ │ ├── Auth.php │ │ ├── Message.php │ │ ├── Pagination.php │ │ ├── Result.php │ │ └── Utils.php │ ├── Controllers │ │ ├── AuthController.php │ │ ├── GitController.php │ │ ├── HomeController.php │ │ ├── ProjectsController.php │ │ └── SettingController.php │ └── Services │ │ ├── AuthService.php │ │ ├── BitBucketService.php │ │ ├── DbUpdateService.php │ │ ├── GitHubService.php │ │ ├── GitLabService.php │ │ ├── GitService.php │ │ └── RepoService.php └── templates │ ├── base.html.twig │ ├── flash.twig │ ├── full.layout.html.twig │ ├── login.twig │ ├── paging.twig │ ├── profile_form.twig │ ├── project_form.twig │ ├── projects.twig │ ├── setting_form.twig │ └── simple.layout.html.twig ├── composer.json ├── img ├── deployment-log.png ├── edit-profile.png ├── login-form.png ├── project-form.png ├── project-list.png ├── setting-form.png └── settings.png ├── public ├── .htaccess ├── assets │ ├── css │ │ ├── material.min.css │ │ └── style.css │ └── js │ │ └── material.min.js └── index.php ├── sample-post-hook.php └── test ├── .gitignore ├── pre_post_hooks.php └── test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | log/* 3 | cache/* 4 | composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-auto-deploy 2 | 3 | 4 | ## Deploy your GitHub, GitLab or Bitbucket projects automatically on Git push events or web hooks 5 | Installation 6 | -------- 7 | 8 | 9 | 10 | * With Composer `composer create-project --no-interaction scriptburn/git-auto-deploy my-app` 11 | * Or simply download and unzip from github 12 | * Suggest you to create a subdomain like **deploy.example.com** 13 | * open app\settings.php and update **db, git_path, composer_path** according to your server 14 |  15 | 16 | * Make sure to add webserver user's public ssh keys in **GitHub, BitBucket, GitLab** 17 | * Login to your ssh as the same user as your webserver process is running and issue these commands And accept 'Yes' after you have added your public ssh keys to **GitHub, BitBucket, GitLab** 18 | `$ ssh git@github.com` 19 | `$ ssh git@bitbucket.com` 20 | `$ ssh git@gitlab.com` 21 | 22 | 23 | 24 | * $ pbcopy < ~/.ssh/id_rsa.pub 25 | * Put **http://< your-domain >/webhook** as WebHook url in appropriate repository 26 | * Default login and password is **admin** and **admin** 27 | * Now Create a new project and appropriate details and you are ready to go 28 | * After you push the changes to your remote repo deployment process in your server will start and if you entered your email in project form you will get detailed deployment log or you can check last deployment log any time by visting project list page 29 | 30 | Pre/Post Hook script 31 | -------------------- 32 | You can check `sample-post-hook.php` for sample code used in a post hook script 33 | 34 | ### Login Page 35 |  36 | 37 | ### Projects list page: 38 | * Search By project name,status and repository type (GitHub ,BitBucket, GitLab) 39 |  40 | 41 | 42 | 43 | ### Projects Form: 44 | * Options to add custom pre and post deployment script. 45 | * Option to send detailed Deployment status report to provided email 46 | * Mark a project active or inactive 47 | * Run composer update after successfull deployment 48 |  49 | 50 | ### Deployment log 51 | * View Detailed log of last failed or sucessfull deployment 52 |  53 | 54 | ### Update Profile 55 | * Change your email and password 56 |  57 | 58 | ### Email setting form 59 | * You can choose how deployment email will be sent , either by native php command or by any external smtp server 60 |  -------------------------------------------------------------------------------- /app/dependencies.php: -------------------------------------------------------------------------------- 1 | getContainer(); 4 | 5 | // ----------------------------------------------------------------------------- 6 | // Service providers 7 | // ----------------------------------------------------------------------------- 8 | 9 | // Util function 10 | $container['utils'] = function ($c) 11 | { 12 | return new App\Classes\Utils($c['router'], $c['logger'], $c['request'], $c['setting']); 13 | }; 14 | 15 | // Twig 16 | $container['view'] = function ($c) 17 | { 18 | $settings = $c->get('settings'); 19 | $view = new Slim\Views\Twig($settings['view']['template_path'], $settings['view']['twig']); 20 | 21 | // Add extensions 22 | $view->addExtension(new Slim\Views\TwigExtension($c->get('router'), $c->get('request')->getUri())); 23 | $view->addExtension(new Twig_Extension_Debug()); 24 | $view->addExtension(new Knlv\Slim\Views\TwigMessages( 25 | $c['flash'] 26 | )); 27 | 28 | $function = new Twig_SimpleFunction('var_dump', function ($v) 29 | { 30 | echo ("
"); 31 | var_dump($v); 32 | echo (""); 33 | }); 34 | $view->getEnvironment()->addFunction($function); 35 | 36 | $function = new Twig_SimpleFunction('urlFor', function ($v, $d = [], $debug = false) use ($c) 37 | { 38 | 39 | return $c['utils']->urlFor($v, $d, $debug); 40 | }); 41 | $view->getEnvironment()->addFunction($function); 42 | 43 | return $view; 44 | }; 45 | 46 | // Flash messages 47 | $container['flash'] = function ($c) 48 | { 49 | return new Slim\Flash\Messages; 50 | }; 51 | 52 | // ----------------------------------------------------------------------------- 53 | // Service factories 54 | // ----------------------------------------------------------------------------- 55 | 56 | // monolog 57 | $container['logger'] = function ($c) 58 | { 59 | $settings = $c->get('settings'); 60 | $logger = new Monolog\Logger($settings['logger']['name']); 61 | $logger->pushProcessor(new Monolog\Processor\UidProcessor()); 62 | $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['logger']['path'], Monolog\Logger::DEBUG)); 63 | return $logger; 64 | }; 65 | 66 | // ----------------------------------------------------------------------------- 67 | // Action factories 68 | // ----------------------------------------------------------------------------- 69 | 70 | $container['HomeController'] = function ($c) 71 | { 72 | return new App\Controllers\HomeController($c->get('view'), $c->get('logger')); 73 | }; 74 | $container['AuthController'] = function ($c) 75 | { 76 | return new App\Controllers\AuthController($c['view'], $c['auth'], $c['session'], $c['flash'], $c['router']); 77 | }; 78 | $container['SettingController'] = function ($c) 79 | { 80 | return new App\Controllers\SettingController($c['view'], $c['setting'], $c['flash'], $c['utils'], $c['db']); 81 | }; 82 | $container['ProjectsController'] = function ($c) 83 | { 84 | return new App\Controllers\ProjectsController($c['view'], $c['db'], $c['flash'], $c['router'], $c['paging'], $c['session'], $c['utils'], $c['auth']); 85 | }; 86 | $container['GitController'] = function ($c) 87 | { 88 | 89 | return new App\Controllers\GitController($c['db'], $c['utils'], $c['logger'], $c['git']); 90 | }; 91 | $container['auth'] = function ($c) 92 | { 93 | return new App\Services\AuthService($c['db'], $c['session']); 94 | }; 95 | $container['paging'] = function ($c) 96 | { 97 | return new App\Classes\Pagination($c['view'], $c['request'], $c['utils'], 5); 98 | }; 99 | $container['git'] = function ($c) 100 | { 101 | return new App\Services\RepoService($c['db'], $c['logger'], $c['setting'], $c['auth'], $c['utils'], $c->settings['binpaths'], [$c['github'], $c['bitbucket'], $c['gitlab']]); 102 | }; 103 | $container['github'] = function ($c) 104 | { 105 | return new \App\Services\GitHubService(); 106 | }; 107 | $container['bitbucket'] = function ($c) 108 | { 109 | return new \App\Services\BitBucketService(); 110 | }; 111 | $container['gitlab'] = function ($c) 112 | { 113 | return new \App\Services\GitLabService(); 114 | }; 115 | 116 | //PDO 117 | 118 | $container['db'] = function ($c) 119 | { 120 | try 121 | { 122 | 123 | $dsn = "mysql:host={$c->settings['db']['host']};dbname={$c->settings['db']['name']};charset=utf8"; 124 | $pdo = new \Slim\PDO\Database($dsn, $c->settings['db']['user'], $c->settings['db']['pass'], array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET sql_mode="TRADITIONAL"')); 125 | 126 | return $pdo; 127 | 128 | } 129 | catch (\Exception $e) 130 | { 131 | die("Unable to connect to db:" . $e->getMessage()); 132 | } 133 | }; 134 | 135 | // Register globally to app 136 | $container['session'] = function ($c) 137 | { 138 | return new \SlimSession\Helper; 139 | }; 140 | $container['flash'] = function () 141 | { 142 | return new \App\Classes\Message(); 143 | }; 144 | $container['setting'] = function ($c) 145 | { 146 | return new \Scriptburn\Setting\Setting($c['db']); 147 | }; 148 | -------------------------------------------------------------------------------- /app/middleware.php: -------------------------------------------------------------------------------- 1 | add(function ( $request, $response, $next) 4 | { 5 | $dbUpdateCheck = new App\Services\DbUpdateService($this->setting, $this->db, ['type' => 'composer', 'path' => __DIR__ . "/../composer.json"]); 6 | $dbUpdateCheck->maybeUpdate('scriptburn/git-auto-deploy'); 7 | 8 | return $next($request, $response); 9 | }); 10 | 11 | $app->add(new \Slim\Middleware\Session([ 12 | 'name' => 'scb_session', 13 | 'autorefresh' => true, 14 | 'lifetime' => '1 hour', 15 | ])); 16 | $app->add(function ( $request, $response, $next) 17 | { 18 | $uri = $request->getUri(); 19 | $path = $uri->getPath(); 20 | if ($path != '/' && substr($path, -1) == '/') 21 | { 22 | // permanently redirect paths with a trailing slash 23 | // to their non-trailing counterpart 24 | $uri = $uri->withPath(substr($path, 0, -1)); 25 | 26 | if ($request->getMethod() == 'GET') 27 | { 28 | return $response->withRedirect((string) $uri, 301); 29 | } 30 | else 31 | { 32 | return $next($request->withUri($uri), $response); 33 | } 34 | } 35 | $this->view->getEnvironment()->addGlobal('auth_user', $this->auth->user()); 36 | $this->view->getEnvironment()->addGlobal('loggedin', $this->auth->loggedin()); 37 | 38 | return $next($request, $response); 39 | }); 40 | 41 | $app->add(new RKA\Middleware\IpAddress(false, [])); 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/routes.php: -------------------------------------------------------------------------------- 1 | auth->loggedin()) 10 | { 11 | $this->flash->addMessage('error', 'Please lgin'); 12 | return $response->withRedirect($this->router->pathFor('login')); 13 | } 14 | return $next($request, $response); 15 | 16 | }; 17 | $is_guest = function ($request, $response, $next) 18 | { 19 | if ($this->auth->loggedin()) 20 | { 21 | return $response->withRedirect($this->router->pathFor('home')); 22 | } 23 | return $next($request, $response); 24 | }; 25 | 26 | $app->map(['GET', 'POST'], '/login', 'AuthController:login') 27 | ->setName('login')->add($is_guest); 28 | 29 | $app->get('/logout', 'AuthController:logout') 30 | ->setName('logout'); 31 | $app->get('/', 'ProjectsController:listAll') 32 | ->setName('projects')->add($is_loggedin); 33 | 34 | $app->get('/projects', 'ProjectsController:listAll') 35 | ->setName('project_list')->add($is_loggedin); 36 | 37 | $app->map(['GET', 'POST'], '/project[/{id}]', 'ProjectsController:form') 38 | ->setName('project_form')->add($is_loggedin); 39 | $app->map(['POST'], '/project_delete', 'ProjectsController:delete') 40 | ->setName('project_delete')->add($is_loggedin); 41 | 42 | $app->map(['GET', 'POST'], '/projects/search', 'ProjectsController:search') 43 | ->setName('project_search')->add($is_loggedin); 44 | 45 | $app->map(['GET', 'POST'], '/webhook', 'GitController:webhook') 46 | ->setName('webhook'); 47 | 48 | $app->map(['GET', 'POST'],'/profile', 'AuthController:profile') 49 | ->setName('profile')->add($is_loggedin); 50 | $app->map(['GET', 'POST'],'/settings', 'SettingController:settings') 51 | ->setName('settings')->add($is_loggedin); -------------------------------------------------------------------------------- /app/settings.php: -------------------------------------------------------------------------------- 1 | [ 4 | // Slim Settings 5 | 'determineRouteBeforeAppMiddleware' => false, 6 | 'displayErrorDetails' => true, 7 | 8 | // View settings 9 | 'view' => [ 10 | 'template_path' => __DIR__ . '/templates', 11 | 'twig' => [ 12 | 'cache' => __DIR__ . '/../cache/twig', 13 | 'debug' => true, 14 | 'auto_reload' => true, 15 | ], 16 | ], 17 | 18 | // monolog settings 19 | 'logger' => [ 20 | 'name' => 'app', 21 | 'path' => __DIR__ . '/../log/app.log', 22 | ], 23 | 24 | 'db' => [ 25 | 'name' => 'my_deploy', 26 | 'host' => 'localhost', 27 | 'user' => 'root', 28 | 'pass' => 'root', 29 | ], 30 | 'binpaths' => [ 31 | 'git_path' => '/usr/bin/git', 32 | 'composer_path' => '/usr/local/bin/composer.phar', 33 | ], 34 | 35 | 36 | ], 37 | ]; 38 | -------------------------------------------------------------------------------- /app/src/Classes/Auth.php: -------------------------------------------------------------------------------- 1 | container = $container; 13 | } 14 | public function authenticate($username, $password) 15 | { 16 | try 17 | { 18 | $user = $this->container->db->select()->from("users")->where("username", "=", $username)->execute()->fetch(); 19 | 20 | if ($user) 21 | { 22 | $validator = new PasswordValidator(); 23 | if (!$validator->isValid($password, $user['password'])) 24 | { 25 | throw new \Exception("Invalid username or password"); 26 | 27 | } 28 | return new \App\Classes\Result(false,"",$user); 29 | 30 | } 31 | else 32 | { 33 | throw new \Exception("Invalid username or password"); 34 | } 35 | } 36 | catch (\Exception $e) 37 | { 38 | return new \App\Classes\Result(true, $e->getMessage()); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/Classes/Message.php: -------------------------------------------------------------------------------- 1 | storage[$this->storageKey][$key])) 10 | { 11 | $this->storage[$this->storageKey][$key] = []; 12 | } 13 | 14 | // Push onto the array 15 | $this->storage[$this->storageKey][$key] = $message; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/Classes/Pagination.php: -------------------------------------------------------------------------------- 1 | request = $request; 11 | $this->view = $view; 12 | $this->utils = $utils; 13 | 14 | $this->per_page = $per_page > 0 ? $per_page : 5; 15 | } 16 | public function page($count,$current_page = 0, $per_page = 0 ) 17 | { 18 | $page = $current_page && $current_page > 0 ? $current_page : (($this->request->getParam('page', 0) > 0) ? $this->request->getParam('page') : 1); 19 | $limit = $per_page ? $per_page : $this->per_page; 20 | $skip = ($page - 1) * $limit; 21 | 22 | $params['pagination'] = [ 23 | 'needed' => $count > $limit, 24 | 'count' => $count, 25 | 'page' => $page, 26 | 'lastpage' => (ceil($count / $limit) == 0 ? 1 : ceil($count / $limit)), 27 | 'limit' => $limit, 28 | 'skip' => $skip, 29 | 'url'=>$this->utils->currentUrl() 30 | ]; 31 | 32 | $params['paging']=$this->view->fetch( 'paging.twig',$params); 33 | return $params; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/Classes/Result.php: -------------------------------------------------------------------------------- 1 | error = $error; 11 | $this->message = $message; 12 | $this->data = $data; 13 | } 14 | public static function create($error, $message="", $data = null) 15 | { 16 | return new self($error, $message , $data ); 17 | } 18 | public function isValid() 19 | { 20 | return !$this->error; 21 | } 22 | public function getMessages() 23 | { 24 | return $this->message; 25 | } 26 | public function getData($key=null) 27 | { 28 | if(is_null($key)) 29 | { 30 | return $this->data; 31 | } 32 | elseif(is_array($this->data)) 33 | { 34 | if(isset($this->data[$key])) 35 | { 36 | return $this->data[$key]; 37 | } 38 | else 39 | { 40 | return null; 41 | } 42 | } 43 | else 44 | { 45 | return $this->data; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/Classes/Utils.php: -------------------------------------------------------------------------------- 1 | router = $router; 11 | $this->logger = $logger; 12 | $this->request = $request; 13 | $this->setting = $setting; 14 | 15 | } 16 | 17 | public function p_l($msg, $dump = false) 18 | { 19 | $bt = debug_backtrace(); 20 | 21 | $caller1 = $bt[0]; 22 | $caller2 = @$bt[1]; 23 | 24 | $caller1['file'] = str_replace(__DIR__, "", $caller1['file']); 25 | $str = microtime(true) . "-" . $caller1['file'] . "@" . @$caller2['function'] . "():$caller1[line]" . "-->"; 26 | if ($dump !== 1) 27 | { 28 | error_log($str); 29 | } 30 | 31 | if ($dump === true) 32 | { 33 | ob_start(); 34 | var_dump($msg); 35 | $rr = ob_get_clean(); 36 | } 37 | elseif ($dump) 38 | { 39 | $this->logger->info($msg); 40 | } 41 | else 42 | { 43 | $rr = print_r($msg, 1); 44 | $this->logger->info($rr); 45 | } 46 | 47 | } 48 | 49 | public function urlFor($v, $d = [], $debug = false) 50 | { 51 | $v = trim($v); 52 | 53 | if (strtolower(substr($v, 0, 1)) == '/' || strtolower(substr($v, 0, 7)) == 'http://' || strtolower(substr($v, 0, 8)) == 'https://') 54 | { 55 | return $this->makeUrl($v, $d); 56 | } 57 | else 58 | { 59 | return $this->router->pathFor($v, $d); 60 | } 61 | } 62 | public function makeUrl($url, $args = []) 63 | { 64 | $query = []; 65 | 66 | $parsed_url = parse_url($url); 67 | if (isset($parsed_url['query']) && $parsed_url['query']) 68 | { 69 | parse_str($parsed_url['query'], $query); 70 | } 71 | $query = array_merge($query, $args); 72 | $url = []; 73 | 74 | if (isset($parsed_url['scheme'])) 75 | { 76 | $url[] = $parsed_url['scheme'] . ":/"; 77 | } 78 | if (isset($parsed_url['host'])) 79 | { 80 | $url[] = $parsed_url['host']; 81 | } 82 | if (isset($parsed_url['path'])) 83 | { 84 | $url[] = rtrim(ltrim($parsed_url['path'], "/"), ""); 85 | } 86 | $url = (empty($parsed_url['scheme']) && empty($parsed_url['host']) ? "/" : "") . implode("/", $url); 87 | if (count($query)) 88 | { 89 | $url .= '?' . http_build_query($query); 90 | } 91 | if (isset($parsed_url['fragment'])) 92 | { 93 | $url .= $parsed_url['fragment']; 94 | } 95 | return $url; 96 | } 97 | public function baseUrl() 98 | { 99 | return $this->request->getUri()->getBasePath(); 100 | } 101 | public function currentUrl($withQueryString = true) 102 | { 103 | $uri = ($this->baseUrl() ? rtrim($this->baseUrl(), "/") . "/" : '') . $this->request->getUri()->getPath(); 104 | 105 | if ($withQueryString) 106 | { 107 | 108 | if (isset($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING']) 109 | { 110 | $uri .= '?' . $_SERVER['QUERY_STRING']; 111 | } 112 | } 113 | return $uri; 114 | } 115 | 116 | public function mail($to, $message, $options = []) 117 | { 118 | $default_options = ['html' => false]; 119 | $options = array_merge($default_options, is_array($options)?$options:[] ); 120 | $mailSetting = isset($options['mail']) && is_array($options['mail']) ? $options['mail'] : $this->setting->get(['send_method', 'smtp_host', 'smtp_user', 'smtp_password', 'smtp_port', 'smtp_enc']); 121 | if (@in_array(['mail', 'smtp'], @$mailSetting['send_method'])) 122 | { 123 | throw new Exception("Invalid mail send method {$mailSetting['send_method']}"); 124 | 125 | } 126 | 127 | if ($mailSetting['send_method'] == 'mail') 128 | { 129 | 130 | $headers = (@$message['from_email'] ? 'From: ' . $message['from_email'] . "\r\n" : '') . 131 | 'Reply-To: ' . $to . "\r\n" . 132 | 'X-Mailer: Git Auto Deploy'; 133 | 134 | if (!mail($to, $message['subject'], $message['body'], $headers)) 135 | { 136 | throw new \Exception("Message could not be sent."); 137 | } 138 | else 139 | { 140 | return true; 141 | } 142 | } 143 | elseif ($mailSetting['send_method'] == 'smtp') 144 | { 145 | if(empty($mailSetting['smtp_host']) || empty($mailSetting['smtp_user']) || empty($mailSetting['smtp_password'])) 146 | { 147 | throw new \Exception("Invalid smtp details"); 148 | } 149 | $mailSetting['smtp_port']=(int)$mailSetting['smtp_port']?$mailSetting['smtp_port']:587; 150 | $mail = new \PHPMailer; 151 | // $mail->SMTPDebug = 3; 152 | $mail->isSMTP(); // Set mailer to use SMTP 153 | $mail->Host = $mailSetting['smtp_host']; 154 | $mail->SMTPAuth = true; // Enable SMTP authentication 155 | $mail->Username = $mailSetting['smtp_user']; 156 | $mail->Password = $mailSetting['smtp_password']; 157 | $mail->SMTPSecure = $mailSetting['smtp_enc']; 158 | $mail->Port = $mailSetting['smtp_port']; 159 | if (@$message['from_email']) 160 | { 161 | $mail->setFrom(@$message['from_email'], @$message['from_name']); 162 | } 163 | $mail->addReplyTo($to); 164 | 165 | $mail->addAddress($to); // Add a recipient 166 | 167 | 168 | $mail->Subject = @$message['subject']; 169 | if (!$options['html']) 170 | { 171 | $mail->ContentType = 'text/plain'; 172 | } 173 | 174 | $mail->Body = @$message['body']; 175 | 176 | $mail->AltBody = @$message['body']; 177 | $mail->isHTML($options['html']); 178 | 179 | if (!$mail->send()) 180 | { 181 | throw new \Exception('Mailer Error: ' . $mail->ErrorInfo); 182 | } 183 | else 184 | { 185 | return true; 186 | } 187 | } 188 | 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /app/src/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | view = $view; 14 | $this->auth = $auth; 15 | $this->session = $session; 16 | $this->flash = $flash; 17 | $this->router = $router; 18 | 19 | } 20 | 21 | public function login(Request $request, Response $response, $args) 22 | { 23 | 24 | if ($request->isPost()) 25 | { 26 | $params = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 27 | 28 | $result = $this->auth->authenticate(@$params['username'], @$params['password']); 29 | 30 | if ($result->isValid()) 31 | { 32 | $this->session->set('admin_loggedin', $result->getData('id')); 33 | return $response->withRedirect($this->router->pathFor('projects')); 34 | } 35 | else 36 | { 37 | $messages = $result->getMessages(); 38 | $this->flash->addMessage('error', $messages); 39 | $this->flash->addMessage('form', ['username' => @$params['username'], 'password' => @$params['password']]); 40 | return $response->withRedirect($this->router->pathFor('login')); 41 | } 42 | } 43 | else 44 | { 45 | $this->view->render($response, 'login.twig'); 46 | } 47 | 48 | } 49 | public function logout(Request $request, Response $response, $args) 50 | { 51 | $this->session->delete('admin_loggedin'); 52 | return $response->withRedirect($this->router->pathFor('login')); 53 | 54 | } 55 | public function profile(Request $request, Response $response, $args) 56 | { 57 | 58 | if ($request->isPost()) 59 | { 60 | $params = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 61 | try 62 | { 63 | $this->auth->setEmail($params['email'],$this->auth->user('id')); 64 | if (!empty($params['password1']) || !empty($params['password2'])) 65 | { 66 | $this->auth->changePassword(@$params['current_password'], @$params['password1'], @$params['password2']); 67 | } 68 | $this->flash->addMessage('success', 'Profile updated'); 69 | return $response->withRedirect($this->router->pathFor('profile')); 70 | 71 | } 72 | catch (\Exception $e) 73 | { 74 | $this->flash->addMessage('error', $e->getMessage()); 75 | $this->flash->addMessage('form', ['password' => @$params['password']]); 76 | return $response->withRedirect($this->router->pathFor('profile')); 77 | } 78 | } 79 | else 80 | { 81 | $params['form']['email']=$this->auth->user('email'); 82 | 83 | $this->view->render($response, 'profile_form.twig',$params); 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | view = $view; 17 | $this->logger = $logger; 18 | } 19 | 20 | public function home(Request $request, Response $response, $args) 21 | { 22 | $this->logger->info("Home page action dispatched"); 23 | 24 | $this->view->render($response, 'home.twig'); 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/Controllers/ProjectsController.php: -------------------------------------------------------------------------------- 1 | view = $view; 14 | $this->db = $db; 15 | $this->flash = $flash; 16 | $this->router = $router; 17 | $this->paging = $paging; 18 | $this->session = $session; 19 | $this->utils = $utils; 20 | $this->auth = $auth; 21 | 22 | $this->project_types = ['gh' => 'GitHub', 'bb' => 'BitBucket','gl'=>'GitLab']; 23 | $this->project_status = [0 => 'Disabled', 1 => 'Active']; 24 | 25 | } 26 | 27 | function listAll(Request $request, Response $response, $args) 28 | { 29 | $args = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 30 | $params['query'] = $request->getQueryParams(); 31 | 32 | $dbQuery = $this->db->select()->from("projects"); 33 | if (isset($params['query']['search_text']) && $params['query']['search_text']) 34 | { 35 | $dbQuery = $dbQuery->where('name', "=", $params['query']['search_text']); 36 | 37 | } 38 | if (isset($params['query']['project_type']) && $params['query']['project_type']) 39 | { 40 | $dbQuery = $dbQuery->where('type', "=", $params['query']['project_type']); 41 | 42 | } 43 | 44 | if (isset($params['query']['project_status']) && $params['query']['project_status'] != "") 45 | { 46 | $dbQuery = $dbQuery->where('status', "=", $params['query']['project_status']); 47 | 48 | } 49 | if (!$this->auth->is_admin('role')) 50 | { 51 | $dbQuery = $dbQuery->where('uid', "=", $this->auth->user('id')); 52 | } 53 | 54 | $countQuery = clone $dbQuery; 55 | $dataQuery = clone $dbQuery; 56 | $project_count = $countQuery->count("id")->execute()->fetch(); 57 | 58 | $pagination = $this->paging->page($project_count['COUNT( id )']); 59 | 60 | $projecs = $dataQuery->orderby('id', 'desc')->limit($pagination['pagination']['limit'], $pagination['pagination']['skip'])->execute(); 61 | 62 | $params['projects'] = $projecs; 63 | $params['pager'] = $pagination; 64 | $params['project_types'] = $this->project_types; 65 | $params['project_status'] = $this->project_status; 66 | 67 | return $this->view->render($response, 'projects.twig', $params); 68 | 69 | } 70 | public function form(Request $request, Response $response, $args) 71 | { 72 | $params = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 73 | $project = []; 74 | if (isset($params['id'])) 75 | { 76 | $project = $this->db->select()->from("projects")->where('id', '=', $params['id'])->execute()->fetch(); 77 | 78 | } 79 | $validate = function ($params) 80 | { 81 | if (empty(trim($params['type']))) 82 | { 83 | throw new \Exception('Please strim(elect repository type'); 84 | } 85 | elseif (empty(trim($params['name']))) 86 | { 87 | throw new \Exception('Please enter repository name'); 88 | 89 | } 90 | elseif (empty(trim($params['path']))) 91 | { 92 | throw new \Exception('Please enter local repository path'); 93 | 94 | } 95 | 96 | }; 97 | if ($request->isPost()) 98 | { 99 | try 100 | { 101 | 102 | if (isset($params['id']) && $params['id'] && !$project) 103 | { 104 | throw new \Exception('Invalid project id'); 105 | } 106 | $validate($params); 107 | $project_exists = $this->db 108 | ->select() 109 | ->from("projects") 110 | ->where('name', '=', trim($params['name'])) 111 | ->where('type', '=', trim($params['type'])) 112 | ->where('branch', '=', trim($params['branch'])); 113 | $path_exists = $this->db 114 | ->select() 115 | ->from("projects") 116 | ->where('path', '=', trim($params['path'])); 117 | 118 | if ($project) 119 | { 120 | $project_exists = $project_exists->whereNotIn('id', array($params['id'])); 121 | $path_exists = $path_exists->whereNotIn('id', array($params['id'])); 122 | 123 | } 124 | 125 | $project_exists = $project_exists->execute()->fetch(); 126 | $path_exists = $path_exists->execute()->fetch(); 127 | 128 | if ($path_exists) 129 | { 130 | throw new \Exception('Local path is already in use'); 131 | } 132 | elseif ($project_exists) 133 | { 134 | throw new \Exception('Duplicate project'); 135 | } 136 | 137 | $fields = [ 138 | 139 | 'name' => trim($params['name']), 140 | 'type' => trim($params['type']), 141 | 'branch' => trim($params['branch']), 142 | 'path' => trim($params['path']), 143 | 'owner' => trim($params['owner']), 144 | 'status' => (int) ($params['status']), 145 | 'secret' => ($params['secret']), 146 | 'pre_hook' => trim($params['pre_hook']), 147 | 'post_hook' => trim($params['post_hook']), 148 | 'email_result' => ($params['email_result']), 149 | 'composer_update' => (int) @($params['composer_update']), 150 | ]; 151 | 152 | if ($project) 153 | { 154 | $this->db->update($fields) 155 | ->table('projects') 156 | ->set($fields) 157 | ->where('id', '=', $params['id']) 158 | ->execute(false); 159 | } 160 | else 161 | { 162 | $this->db->insert(array_merge(['uid'], array_keys($fields))) 163 | ->into('projects') 164 | ->values(array_merge([$this->auth->user('id')], array_values($fields))) 165 | ->execute(false); 166 | 167 | $params['id'] = $this->db->lastInsertId(); 168 | } 169 | 170 | $this->flash->addMessage('success', sprintf('Project %1$s successfully', $project ? 'updated' : 'created')); 171 | $this->flash->addMessage('form', $request->getParsedBody()); 172 | 173 | return $response->withRedirect($this->router->pathFor('project_form', ['id' => $params['id']])); 174 | 175 | } 176 | catch (\Exception $e) 177 | { 178 | $this->flash->addMessage('error', $e->getMessage()); 179 | $this->flash->addMessage('form', $request->getParsedBody()); 180 | return $response->withRedirect($this->utils->urlFor('project_form', isset($params['id']) && $params['id'] ? ['id' => $params['id']] : [])); 181 | } 182 | 183 | } 184 | else 185 | { 186 | if (isset(($params['id'])) && $params['id'] && !$project) 187 | { 188 | $this->flash->addMessage('error', 'Invalid project id'); 189 | return $response->withRedirect($this->router->pathFor('projects')); 190 | } 191 | 192 | $project_types = ['gh' => 'GitHub', 'bb' => 'BitBucket']; 193 | $params = [ 194 | 'form' => $project, 195 | 'project_types' => $this->project_types, 196 | 'project_status' => $this->project_status, 197 | 198 | 'title' => $project ? 'Edit project: ' . $project['name'] : 'Create project', 199 | ]; 200 | $this->view->render($response, 'project_form.twig', $params); 201 | } 202 | 203 | } 204 | 205 | public function search(Request $request, Response $response, $args) 206 | { 207 | $params = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 208 | $project = []; 209 | $url = ($this->utils->urlFor('project_list')); 210 | 211 | if (isset($params['reset'])) 212 | { 213 | return $response->withRedirect($url); 214 | 215 | } 216 | elseif (!empty($params['search_text']) || (isset($params['project_status']) && $params['project_status'] != "") || (isset($params['project_type']) && $params['project_type'] != "")) 217 | { 218 | $url = $this->utils->makeUrl($this->utils->UrlFor('project_list'), ['search_text' => $params['search_text'], 'project_status' => $params['project_status'], 'project_type' => $params['project_type']]); 219 | } 220 | 221 | return $response->withRedirect($url); 222 | 223 | } 224 | public function delete(Request $request, Response $response, $args) 225 | { 226 | $params = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 227 | $project = $this->db->delete() 228 | ->from('projects') 229 | ->where('id', "=", $params['id']); 230 | 231 | if (!$this->auth->is_admin('role')) 232 | { 233 | $project = $project->where('uid', '=', $this->auth->user('id')); 234 | } 235 | 236 | $project->execute(false); 237 | $this->flash->addMessage('success', 'Project deleted'); 238 | 239 | return $response->withRedirect($this->utils->urlFor('project_list')); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /app/src/Controllers/SettingController.php: -------------------------------------------------------------------------------- 1 | view = $view; 14 | $this->setting = $setting; 15 | $this->flash = $flash; 16 | $this->utils = $utils; 17 | $this->db = $db; 18 | 19 | $this->send_methods = ['mail' => 'PHP Mail', 'smtp' => 'SMTP']; 20 | $this->smtp_enc_methods = ['' => 'No Encryption', 'tls' => 'TLS', 'ssl' => 'SSL']; 21 | 22 | } 23 | 24 | public function settings(Request $request, Response $response, $args) 25 | { 26 | if ($request->isPost()) 27 | { 28 | 29 | 30 | $params = array_merge($request->getQueryParams(), is_array($request->getParsedBody()) ? $request->getParsedBody() : [], $args); 31 | 32 | try 33 | { 34 | $this->setting->set([ 35 | 'send_method' => @$params['send_method'], 36 | 'smtp_host' => @$params['smtp_host'], 37 | 'smtp_user' => @$params['smtp_user'], 38 | 'smtp_password' => @$params['smtp_password'], 39 | 'smtp_port' => @$params['smtp_port'], 40 | 'smtp_enc' => @$params['smtp_enc'], 41 | 'notify_deploy'=>@$params['notify_deploy'], 42 | ]); 43 | 44 | $this->flash->addMessage('success', 'Settings saved'); 45 | return $response->withRedirect($this->utils->urlFor('settings')); 46 | } 47 | catch (\Exception $e) 48 | { 49 | $this->flash->addMessage('error', $e->getMessage()); 50 | $this->flash->addMessage('form', $params); 51 | return $response->withRedirect($this->utils->urlFor('settings')); 52 | } 53 | } 54 | else 55 | { 56 | $params['form'] = $this->setting->get(['send_method', 'smtp_host', 'smtp_user', 'smtp_password', 'smtp_port', 'smtp_enc','notify_deploy']); 57 | $params['send_methods'] = $this->send_methods; 58 | $params['smtp_enc_methods'] = $this->smtp_enc_methods; 59 | 60 | $this->view->render($response, 'setting_form.twig', $params); 61 | } 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/Services/AuthService.php: -------------------------------------------------------------------------------- 1 | db = $db; 13 | $this->session = $session; 14 | 15 | } 16 | public function authenticate($username, $password) 17 | { 18 | try 19 | { 20 | $user = $this->db->select()->from("users")->where("username", "=", $username)->execute()->fetch(); 21 | 22 | if ($user) 23 | { 24 | $validator = new PasswordValidator(); 25 | $result = $validator->isValid($password, $user['password']); 26 | 27 | if (!$result->isValid()) 28 | { 29 | throw new \Exception("Invalid username or password"); 30 | 31 | } 32 | return new \App\Classes\Result(false, "", $user); 33 | 34 | } 35 | else 36 | { 37 | throw new \Exception("Invalid username or password"); 38 | } 39 | } 40 | catch (\Exception $e) 41 | { 42 | return new \App\Classes\Result(true, $e->getMessage()); 43 | } 44 | } 45 | 46 | public function user($key = null) 47 | { 48 | if (!($user_id = $this->loggedin())) 49 | { 50 | return null; 51 | } 52 | if (!$this->user) 53 | { 54 | $this->user = $this->db->select()->from("users")->where("id", "=", $user_id)->execute()->fetch(); 55 | } 56 | return !is_null($key) ? (isset($this->user[$key]) ? $this->user[$key] : null) : $this->user; 57 | } 58 | public function loggedin() 59 | { 60 | return $this->session->exists('admin_loggedin') && $this->session->get('admin_loggedin') ? $this->session->get('admin_loggedin') : false; 61 | } 62 | public function changePassword($oldPassword, $newpassword1, $newpassword2) 63 | { 64 | $validator = new PasswordValidator(); 65 | 66 | if ($newpassword1 !== $newpassword2) 67 | { 68 | throw new \Exception("Password and confirm password does not match"); 69 | 70 | } 71 | elseif (!$validator->isValid($oldPassword, $this->user('password'))) 72 | { 73 | throw new \Exception("Invalid old password"); 74 | } 75 | else 76 | { 77 | $this->setPassword($newpassword1); 78 | } 79 | 80 | } 81 | public function setPassword($password) 82 | { 83 | 84 | $validator = new PasswordValidator(); 85 | $fields = ['password' => $validator->rehash($password)]; 86 | $this->db->update($fields) 87 | ->table('users') 88 | ->set($fields) 89 | ->where('id', '=', $this->user('id')) 90 | ->execute(false); 91 | 92 | } 93 | public function setEmail($email, $uid) 94 | { 95 | 96 | $fields = ['email' => $email]; 97 | $user = $this->db 98 | ->select() 99 | ->from('users') 100 | ->where('email', '=', $email) 101 | ->where('id', '<>', $uid) 102 | ->limit(1, 0) 103 | ->execute() 104 | ->fetch(); 105 | if ($user) 106 | { 107 | throw new \Exception("Email address already in use"); 108 | } 109 | $this->db->update($fields) 110 | ->table('users') 111 | ->set($fields) 112 | ->where('id', '=', $uid) 113 | ->execute(false); 114 | 115 | } 116 | public function is_admin($uid = 0) 117 | { 118 | if (!$uid) 119 | { 120 | return $this->user('role') === 'admin'; 121 | } 122 | else 123 | { 124 | $user = $this->db 125 | ->select() 126 | ->from('users') 127 | ->where('id', '=', $uid) 128 | ->execute() 129 | ->fetch(); 130 | return $user && $user['role'] == 'admin'; 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /app/src/Services/BitBucketService.php: -------------------------------------------------------------------------------- 1 | hookData = $data; 14 | 15 | return true; 16 | } 17 | return false; 18 | } 19 | 20 | public function parseRemoteRepoName() 21 | { 22 | if (empty($this->hookData['post']['repository']['full_name'])) 23 | { 24 | throw new \Exception('Repositry name not found'); 25 | } 26 | $this->hookData['remote']['repo_name'] = $this->hookData['post']['repository']['full_name']; 27 | } 28 | public function parseRemoteBranchName() 29 | { 30 | $index = 'old'; 31 | if (empty($this->hookData['post']['push']['changes'][0]['old']['name'])) 32 | { 33 | $index = 'new'; 34 | } 35 | if (empty($this->hookData['post']['push']['changes'][0][$index]['name'])) 36 | { 37 | throw new \Exception("Unknown remote branch"); 38 | } 39 | elseif (@($this->hookData['post']['push']['changes'][0][$index]['type']) != 'branch') 40 | { 41 | throw new \Exception("Expecting remote branch"); 42 | 43 | } 44 | 45 | $this->hookData['remote']['branch_name'] = $this->hookData['post']['push']['changes'][0][$index]['name']; 46 | 47 | } 48 | public function parseRemoteUrl() 49 | { 50 | if (empty($this->project['name'])) 51 | { 52 | throw new \Exception("Unknown repo name to track"); 53 | } 54 | $name = explode("/", $this->project['name']); 55 | if (empty($name[0])) 56 | { 57 | throw new \Exception("Invalid repo name format"); 58 | } 59 | $this->hookData['remote']['repo_url'] = "git@bitbucket.org:/{$this->project['name']}.git"; 60 | } 61 | public function repoType() 62 | { 63 | return "bb"; 64 | } 65 | public function validate() 66 | { 67 | $curent_ip = $this->request->getAttribute('ip_address'); 68 | $valid_ip = false; 69 | foreach ($this->valid_bb_ips as $ip) 70 | { 71 | if ($this->ip_in_range($curent_ip, $ip)) 72 | { 73 | $valid_ip = true; 74 | break; 75 | } 76 | } 77 | if (!$valid_ip) 78 | { 79 | //throw new \Exception("Webhook originated from unknown bitbucket ip:$curent_ip, Valie ip range:" . implode(",", $this->valid_bb_ips)); 80 | } 81 | 82 | } 83 | 84 | public function test() 85 | { 86 | /* 87 | rm -rf /Volumes/MacData/htdocs/test/git-auto-deploy-test-bb; 88 | git clone git@bitbucket.org:rajneeshojha/test.git /Volumes/MacData/htdocs/test/git-auto-deploy-test-bb 89 | 90 | */ 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/Services/DbUpdateService.php: -------------------------------------------------------------------------------- 1 | pdo->query($tbl); 57 | 58 | } 59 | }; 60 | if (!$this->tableExists('users')) 61 | { 62 | $create_table($user_table); 63 | 64 | $validator = new PasswordValidator(); 65 | $this->pdo->insert(['username', 'password', 'role']) 66 | ->into('users') 67 | ->values(['admin', $validator->rehash('admin'), 'admin']) 68 | ->execute(false); 69 | } 70 | if (!$this->tableExists('projects')) 71 | { 72 | $create_table($projects_table); 73 | } 74 | 75 | p_l("in update_routine_1"); 76 | 77 | return true; 78 | } 79 | public function update_routine_2() 80 | { 81 | p_l("in update_routine_2"); 82 | $updates[] = "ALTER TABLE `users` CHANGE `email` `email` VARCHAR(255) NULL DEFAULT NULL"; 83 | $updates[] = "ALTER TABLE `users` CHANGE `role` `role` VARCHAR(255) NULL DEFAULT NULL"; 84 | $updates[] = "ALTER TABLE `projects` CHANGE `secret` `secret` VARCHAR(255) NULL DEFAULT NULL"; 85 | 86 | $updates[] = "ALTER TABLE `projects` CHANGE `pre_hook` `pre_hook` VARCHAR(255) NULL DEFAULT NULL"; 87 | $updates[] = "ALTER TABLE `projects` CHANGE `post_hook` `post_hook` VARCHAR(255) NULL DEFAULT NULL"; 88 | $updates[] = "ALTER TABLE `projects` CHANGE `email_result` `email_result` VARCHAR(255) NULL DEFAULT NULL"; 89 | 90 | $updates[] = "ALTER TABLE `projects` CHANGE `uid` `uid` bigint(10) NULL DEFAULT NULL"; 91 | $updates[] = "ALTER TABLE `projects` CHANGE `last_hook_status` `last_hook_status` int(1) NULL DEFAULT NULL"; 92 | 93 | $updates[] = "ALTER TABLE `projects` CHANGE `last_hook_time` `last_hook_time` datetime NULL DEFAULT NULL"; 94 | 95 | $updates[] = "ALTER TABLE `projects` CHANGE `last_hook_duration` `last_hook_duration` int(5) NULL DEFAULT NULL"; 96 | 97 | $updates[] = "ALTER TABLE `projects` CHANGE `last_hook_log` `last_hook_log` text NULL DEFAULT NULL"; 98 | $updates[] = "ALTER TABLE `projects` CHANGE `composer_update` `composer_update` int(1) NULL DEFAULT NULL"; 99 | 100 | $this->execute($updates); 101 | return true; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /app/src/Services/GitHubService.php: -------------------------------------------------------------------------------- 1 | hookData = $data; 11 | 12 | return true; 13 | } 14 | return false; 15 | } 16 | public function parseRemoteRepoName() 17 | { 18 | if (empty($this->hookData['post']['repository']['full_name'])) 19 | { 20 | throw new \Exception('Repositry name not found'); 21 | } 22 | $this->hookData['remote']['repo_name'] = $this->hookData['post']['repository']['full_name']; 23 | } 24 | public function parseRemoteBranchName() 25 | { 26 | if (empty($this->hookData['post']['ref'])) 27 | { 28 | throw new \Exception("Unknown remote branch"); 29 | } 30 | $ref = explode("/", $this->hookData['post']['ref']); 31 | $this->hookData['remote']['branch_name'] = $ref[2]; 32 | } 33 | public function parseRemoteUrl() 34 | { 35 | if (empty($this->project['name'])) 36 | { 37 | throw new \Exception("Unknown repo name to track"); 38 | } 39 | 40 | $this->hookData['remote']['repo_url'] = "git@github.com:/{$this->project['name']}.git"; 41 | 42 | } 43 | public function repoType() 44 | { 45 | 46 | return "gh"; 47 | } 48 | public function validate() 49 | { 50 | 51 | if (isset($this->hookData['headers']['HTTP_X_HUB_SIGNATURE'])) 52 | { 53 | if (!$this->project['secret']) 54 | { 55 | throw new \Exception('Secret has been set in repositry but secret was not entred in project'); 56 | 57 | } 58 | elseif (!extension_loaded('hash')) 59 | { 60 | throw new \Exception('Has Extension missing'); 61 | } 62 | list($algo, $hash) = explode('=', $this->hookData['headers']['HTTP_X_HUB_SIGNATURE'][0], 2) + array('', ''); 63 | if (!in_array($algo, hash_algos(), true)) 64 | { 65 | throw new \Exception("Hash algorithm '$algo' is not supported."); 66 | } 67 | elseif ($hash !== hash_hmac($algo, $this->hookData['body'], $this->project['secret'])) 68 | { 69 | throw new \Exception('Hook secret does not match.'); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/Services/GitLabService.php: -------------------------------------------------------------------------------- 1 | hookData = $data; 12 | 13 | return true; 14 | } 15 | return false; 16 | } 17 | public function parseRemoteRepoName() 18 | { 19 | if (empty($this->hookData['post']['project']['path_with_namespace'])) 20 | { 21 | throw new \Exception('Repositry name not found'); 22 | } 23 | $this->hookData['remote']['repo_name'] = $this->hookData['post']['project']['path_with_namespace']; 24 | } 25 | public function parseRemoteBranchName() 26 | { 27 | if (empty($this->hookData['post']['ref'])) 28 | { 29 | throw new \Exception("Unknown remote branch"); 30 | } 31 | $ref = explode("/", $this->hookData['post']['ref']); 32 | $this->hookData['remote']['branch_name'] = $ref[2]; 33 | } 34 | public function parseRemoteUrl() 35 | { 36 | if (empty($this->project['name'])) 37 | { 38 | throw new \Exception("Unknown repo name to track"); 39 | } 40 | 41 | $this->hookData['remote']['repo_url'] = "git@gitlab.com:/{$this->project['name']}.git"; 42 | 43 | } 44 | public function repoType() 45 | { 46 | 47 | return "gl"; 48 | } 49 | public function validate() 50 | { 51 | 52 | if (isset($this->hookData['headers']['HTTP_X_GITLAB_TOKEN'][0])) 53 | { 54 | if (!$this->project['secret']) 55 | { 56 | throw new \Exception('Secret has been set in repositry but secret was not entred in project'); 57 | 58 | } 59 | elseif ($this->project['secret'] != $this->hookData['headers']['HTTP_X_GITLAB_TOKEN'][0]) 60 | { 61 | throw new \Exception('Hook secret does not match.'); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/Services/GitService.php: -------------------------------------------------------------------------------- 1 | git = $git; 27 | $this->setting = $setting; 28 | $this->auth = $auth; 29 | $this->db = $db; 30 | $this->utils = $utils; 31 | 32 | $this->logger = $logger; 33 | 34 | $this->request = $request; 35 | $this->binpaths = $binpaths; 36 | 37 | $this->parsePayLoad(); 38 | $this->parseRemoteRepoName(); 39 | $this->parseRemoteBranchName(); 40 | 41 | $this->project = $this->git->findProject($this->getHookData('remote', 'repo_name'), $this->repoType()); 42 | 43 | $index = array_search($this->getHookData('remote', 'branch_name'), array_column($this->project, 'branch')); 44 | 45 | if ($index === false) 46 | { 47 | throw new \Exception("Unknown branch '" . $this->getHookData('remote', 'branch_name') . "'"); 48 | } 49 | else 50 | { 51 | $this->project = $this->project[$index]; 52 | } 53 | 54 | $this->project['path'] = rtrim($this->project['path'], "/"); 55 | 56 | if (!$this->project['status']) 57 | { 58 | throw new \Exception("Project '{$this->project['name']}' status is inactive"); 59 | } 60 | elseif (empty($this->project['path'])) 61 | { 62 | throw new \Exception("Local path not set for repo '{$this->project['name']}' "); 63 | 64 | } 65 | 66 | $this->parseRemoteUrl(); 67 | $this->startedon = microtime(true); 68 | $fields = ['last_hook_time' => date("Y-m-d H:i:s", time())]; 69 | $this->db->update($fields) 70 | ->table('projects') 71 | ->set($fields) 72 | ->where('id', '=', $this->project['id']) 73 | ->execute(false); 74 | 75 | } 76 | public function getHookData($index = "", $key = "") 77 | { 78 | if ($index && $key) 79 | { 80 | return isset($this->hookData[$index][$key]) ? $this->hookData[$index][$key] : null; 81 | } 82 | elseif ($index) 83 | { 84 | return isset($this->hookData[$index]) ? $this->hookData[$index] : null; 85 | } 86 | else 87 | { 88 | return $this->hookData; 89 | } 90 | } 91 | private function parsePayLoad() 92 | { 93 | if ($this->hookData['headers']['CONTENT_TYPE'][0] == 'application/x-www-form-urlencoded') 94 | { 95 | $this->hookData['post'] = (array) @json_decode($this->hookData['post']['payload'], true); 96 | 97 | } 98 | 99 | } 100 | public function sync() 101 | { 102 | 103 | $completed = false; 104 | try 105 | { 106 | if ($this->project['pre_hook']) 107 | { 108 | $data['hook_data'] = $this->hookData; 109 | $data['project'] = $this->project; 110 | $data['hook'] = 'pre'; 111 | $data['status'] = 0; 112 | $this->log("Runing pre hook command:{$this->project['pre_hook']} "); 113 | 114 | $out = $this->exec(['cmd' => $this->project['pre_hook'], 'input' => json_encode($data)]); 115 | if (!$out[0]) 116 | { 117 | $this->log("Failed to run pre hook script"); 118 | } 119 | 120 | } 121 | $repoPath = $this->project['path'] . "/.git/"; 122 | if (!is_dir($repoPath) || !is_file($repoPath . 'HEAD')) 123 | { 124 | 125 | $this->log("Absent repository for '{$this->project['name']}', cloning"); 126 | if (file_exists($this->project['path'])) 127 | { 128 | $di = new \RecursiveDirectoryIterator($this->project['path'], \FilesystemIterator::SKIP_DOTS); 129 | $ri = new \RecursiveIteratorIterator($di, \RecursiveIteratorIterator::CHILD_FIRST); 130 | foreach ($ri as $file) 131 | { 132 | $file->isDir() ? rmdir($file) : unlink($file); 133 | } 134 | } 135 | $this->exec( 136 | $this->prepareCmd("git", "clone " . $this->getHookData('remote', 'repo_url') . " {$this->project['path']}"), "Unable to clone repo" 137 | ); 138 | } 139 | // Else fetch changes 140 | else 141 | { 142 | 143 | $this->log("Fetching repository '{$this->project['name']}'"); 144 | 145 | $this->exec(['cmd' => 146 | $this->prepareCmd("git", "fetch"), 'cwd' => $repoPath], "Unable to fetch repo" 147 | ); 148 | 149 | if (!empty($out[1]) && stripos($out[1], "have diverged") !== false) 150 | { 151 | $this->exec(['cmd' => 152 | $this->prepareCmd("git", "reset --hard HEAD", ""), 'cwd' => $this->project['path']], "Unable to reset head" 153 | ); 154 | $this->exec(['cmd' => 155 | $this->prepareCmd("git", "clean -f -d", ""), 'cwd' => $this->project['path']], "Unable to git clean" 156 | ); 157 | $this->exec(['cmd' => 158 | $this->prepareCmd("git", "pull", ""), 'cwd' => $this->project['path']], "Unable to pull from repo" 159 | ); 160 | } 161 | 162 | } 163 | $out = $this->exec($this->prepareCmd("git", "checkout -f {$this->project['branch']}", " cd $repoPath && GIT_WORK_TREE={$this->project['path']}"), "Unable to checkput repo"); 164 | 165 | if (!empty($out[1]) && stripos($out[1], "Your branch is behind") !== false) 166 | { 167 | $this->exec(['cmd' => 168 | $this->prepareCmd("git", "pull", ""), 'cwd' => $this->project['path']], "Unable to pull from repo" 169 | ); 170 | } 171 | 172 | $out = ($this->exec(['cmd' => $this->prepareCmd("git", "status", ""), 'cwd' => $this->project['path']], "Unable to check repo status")); 173 | 174 | if (empty($out[1]) || stripos($out[1], "On branch {$this->project['branch']}") === false) 175 | { 176 | throw new \Exception("Webhook failed"); 177 | } 178 | 179 | $completed = true; 180 | 181 | } 182 | catch (ProcessFailedException $e) 183 | { 184 | $this->log($e->getMessage()); 185 | throw new \Exception("Fetch from repo failed"); 186 | } 187 | finally 188 | { 189 | if ($this->project['post_hook']) 190 | { 191 | $data['hook_data'] = $this->hookData; 192 | $data['project'] = $this->project; 193 | $data['hook'] = 'post'; 194 | $data['status'] = $completed ? 1 : 0; 195 | $this->log("Runing post hook command:{$this->project['post_hook']} "); 196 | $out = $this->exec(['cmd' => $this->project['post_hook'], 'input' => json_encode($data)]); 197 | if (!$out[0]) 198 | { 199 | $this->log("Failed to run post hook script"); 200 | } 201 | } 202 | 203 | if ($completed && $this->project['composer_update']) 204 | { 205 | 206 | $_ENV['HOME'] = realpath(__DIR__ . "/../../../"); 207 | $_ENV['HOME'] = rtrim($_ENV['HOME'], "/") . "/cache"; 208 | $this->log("Set composer HOME to " . $_ENV['HOME']); 209 | 210 | if (!file_exists($_ENV['HOME'])) 211 | { 212 | if (!mkdir($_ENV['HOME'])) 213 | { 214 | throw new \Exception("Unable to create cache folder '{$_ENV['HOME']}'"); 215 | } 216 | else 217 | { 218 | $this->log("Cache folder created "); 219 | } 220 | } 221 | $this->exec(['cmd' => 222 | $this->prepareCmd("composer", "update", ""), 'cwd' => $this->project['path'], 'env' => $_ENV], "Unable to run composer update" 223 | ); 224 | } 225 | if ($this->project['email_result']) 226 | { 227 | 228 | $this->log("Sending result to email:{$this->project['email_result']} "); 229 | try 230 | { 231 | 232 | $message = [ 233 | 'subject' => 'Git Auto deploy for ' . $this->project['name'] . '@' . $this->project['type'] . " was " . ($completed ? 'successfull' : 'failed'), 234 | 'body' => "
--" . implode("
--", $this->log) . "
",
235 | 'from_email' => $this->project['email_result'],
236 | 'from_name' => '',
237 | ];
238 |
239 | $this->utils->mail($this->project['email_result'], $message, ['html' => true]);
240 | $this->log("Email sent ");
241 |
242 | }
243 | catch (\Exception $e)
244 | {
245 | $this->log("Message could not be sent.");
246 | $this->log('Mailer Error: ' . $e->getMessage());
247 |
248 | }
249 |
250 | }
251 | if (true == false && !empty($this->project['owner']) && count($owner = explode(":", $this->project['owner']) > 1))
252 | {
253 | $this->log("Setting owner of '{$this->project['path']}' to {$this->project['owner']} ");
254 | $out = $this->exec("chown -R {$this->project['path']} {$this->project['owner']} ");
255 | if (!$out[0])
256 | {
257 | $this->log("Failed to set folder owner");
258 | }
259 |
260 | }
261 |
262 | $fields = ['last_hook_status' => $completed ? 1 : 0, 'last_hook_duration' => microtime(true) - $this->startedon, 'last_hook_log' => implode("\n", $this->log)];
263 | $this->db->update($fields)
264 | ->table('projects')
265 | ->set($fields)
266 | ->where('id', '=', $this->project['id'])
267 | ->execute(false);
268 |
269 | }
270 |
271 | }
272 |
273 | private function exec($processes, $errorMessage = "")
274 | {
275 | $output = [];
276 | $processes = is_array($processes) && isset($processes[0]) ? $processes : [$processes];
277 |
278 | try
279 | {
280 | foreach ($processes as $key => $command)
281 | {
282 |
283 | $cmd = is_array($command) ? @$command['cmd'] : $command;
284 | $options = is_array($command) ? $command : [];
285 | unset($options['cmd']);
286 | if (!$cmd)
287 | {
288 | throw new \Exception("invalid cmd");
289 | }
290 | $this->log("Running $cmd");
291 |
292 | $process_options = array_merge(['cwd' => null, 'env' => null, 'input' => null, 'options' => []], $options);
293 | $process = new Process($cmd, @$process_options['cwd'], @$process_options['env'], @$process_options['input'], @$process_options['options']);
294 |
295 | $process->mustRun();
296 |
297 | //$process->wait();
298 |
299 | $this->log("Output:\n" . $process->getOutput());
300 | $output[$cmd] = $process->getOutput();
301 | }
302 | if (count($output) > 1)
303 | {
304 | return [1, ($output)];
305 |
306 | }
307 | else
308 | {
309 | $output = array_values($output);
310 | return [1, implode("\n", $output)];
311 |
312 | }
313 | }
314 | catch (ProcessFailedException $e)
315 | {
316 | $this->log($e->getMessage());
317 | if ($errorMessage)
318 | {
319 | throw new \Exception($errorMessage);
320 | }
321 | else
322 | {
323 |
324 | if (count($output) > 1)
325 | {
326 | return [0, ($output)];
327 |
328 | }
329 | else
330 | {
331 | return [0, $e->getMessage()];
332 |
333 | }
334 | }
335 | }
336 | }
337 |
338 | public function prepareCmd($bin, $cmd, $prepend = "")
339 | {
340 | if (empty($this->binpaths[$bin . "_path"]))
341 | {
342 | throw new Exception("Unable to find binary '$bin' location");
343 | }
344 | return "$prepend " . $this->binpaths[$bin . "_path"] . " $cmd";
345 | }
346 |
347 | public function ip_in_range($ip, $range)
348 | {
349 | if (is_array($range))
350 | {
351 | return in_array($range, $ip);
352 | }
353 | elseif (strpos($range, '/') == false)
354 | {
355 | $range .= '/32';
356 | }
357 | // $range is in IP/CIDR format eg 127.0.0.1/24
358 | list($range, $netmask) = explode('/', $range, 2);
359 | $range_decimal = ip2long($range);
360 | $ip_decimal = ip2long($ip);
361 | $wildcard_decimal = pow(2, (32 - $netmask)) - 1;
362 | $netmask_decimal = ~$wildcard_decimal;
363 | return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
364 | }
365 |
366 | public function log($msg)
367 | {
368 | $this->log[] = $msg;
369 | $this->logger->info(is_array($msg) || is_object($msg) ? print_r($msg, 1) : $msg);
370 | }
371 |
372 | // testing
373 | // from main repo git checkout master; vi readme.html ; git add . ; git commit -m "xxxxxx"; git push origin HEAD
374 | }
375 |
--------------------------------------------------------------------------------
/app/src/Services/RepoService.php:
--------------------------------------------------------------------------------
1 | db = $db;
11 |
12 | $this->logger = $logger;
13 | $this->setting = $setting;
14 | $this->auth = $auth;
15 | $this->utils = $utils;
16 |
17 | $this->binpaths = $binpaths;
18 | $this->repoServices = $repoServices;
19 |
20 | }
21 | public function findProject($name, $type = "")
22 | {
23 | $dbObj = $this->db->select()
24 | ->from('projects')
25 | ->where('name', "=", $name);
26 | if ($type)
27 | {
28 | $dbObj = $dbObj->where('type', "=", $type);
29 | }
30 |
31 | $dbObj = $dbObj->execute();
32 | while ($row = $dbObj->fetch())
33 | {
34 | $repo[] = $row;
35 | }
36 | if (!$repo)
37 | {
38 | throw new \Exception("Repo with name '$name' not found ");
39 | }
40 | return $repo;
41 |
42 | }
43 |
44 | public function identifyandValidateRepo($data, $request)
45 | {
46 |
47 |
48 | foreach ($this->repoServices as $repoService)
49 | {
50 |
51 | if ($repoService->identify($data))
52 | {
53 |
54 | $repoService->init($this, $this->setting, $this->auth, $this->logger, $this->db, $this->utils, $request, $this->binpaths);
55 | $repoService->validate();
56 | $repoService->sync();
57 | return $repoService;
58 | }
59 | }
60 |
61 | return false;
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/app/templates/base.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% block layout %}{% endblock %}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/templates/flash.twig:
--------------------------------------------------------------------------------
1 | {% set error_message = flash('error') %}
2 | {% set success_message = flash('success') %}
3 | {% if error_message |length %}
4 | 118 | # 119 | | 120 |121 | Name 122 | | 123 |124 | Branch 125 | | 126 | 127 | 128 |129 | Status 130 | | 131 | 132 | 133 ||
---|---|---|---|---|
Sorry no projects found. 138 | Create project 139 | | ||||
{{project.id}} | 145 |
146 | {{ project.name }} @ {{ attribute(project_types, project.type) }}
147 |
148 | {% if project.last_hook_time!='0000-00-00 00:00:00' %}
149 |
150 | Last deployment on {{project.last_hook_time |date("d M Y h:i:s a")}} was {{project.last_hook_status?'successfull':'failed'}} and took {{project.last_hook_duration|round}} seconds
151 |
153 |
154 | {%endif%}
155 |
156 |
157 |
158 | |
159 | 160 | {{ project.branch }} 161 | | 162 | 163 |164 | {{ attribute(project_status, project.status) }} 165 | | 166 |
167 |
168 |
169 |
170 | Action
171 |
172 |
173 |
177 |
178 |
179 |
180 |
181 | |
182 |
{{ pager.paging|raw }} |