├── .gitignore ├── config.json ├── env.js ├── bin ├── cli.js ├── deploy.bat ├── deploy.sh ├── cli.bat └── cli.sh ├── package.json ├── README.md └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 6060, 3 | "defaultBranch": "origin/master", 4 | 5 | "projects": { 6 | "sample": { 7 | "origin/master": { 8 | "path": "/var/www/sample", 9 | "users": ["abc"] 10 | } 11 | } 12 | }, 13 | 14 | "users": { 15 | "abc": "ba1f2511fc30423bdbb183fe33f3dd0f" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var os = require('os'); 3 | 4 | var posix = process.platform !== 'win32'; 5 | 6 | module.exports = { 7 | configPath: path.join(os.homedir(), '.hookagent', 'config.json'), 8 | execFileOptions: { 9 | env: {} 10 | }, 11 | posix: posix, 12 | ext: posix ? 'sh' : 'bat' 13 | }; 14 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | var path = require('path'); 3 | var currentPlatform = require('../env'); 4 | 5 | var child_process = require('child_process'); 6 | // console.log(process.argv[2]) 7 | child_process.execFile(path.join(__dirname, `cli.${currentPlatform.ext}`), [ 8 | process.argv[2] 9 | ], 10 | { 11 | env: process.env, 12 | cwd: __dirname 13 | }, function (error, stdout, stderr) { 14 | if (error) { 15 | console.error(error); 16 | } else { 17 | console.log(stdout); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hookagent", 3 | "version": "0.7.5", 4 | "description": "Git webhook agent for server auto-deployment.", 5 | "main": "server.js", 6 | "bin": { 7 | "hookagent": "bin/cli.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": "https://github.com/mytharcher/hookagent.git", 13 | "author": "mytharcher ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "express": "^4.12.3", 17 | "basic-auth": "^1.0.0" 18 | }, 19 | "peerDependencies": { 20 | "pm2": "*" 21 | }, 22 | "engines": { 23 | "node": ">=10" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bin/deploy.bat: -------------------------------------------------------------------------------- 1 | :: This is a deploy shell for windows systerm. 2 | 3 | @echo off 4 | set PROJECT=%1 5 | set remote=%2 6 | set branch=%3 7 | 8 | set git=%4 9 | 10 | @echo start deploying %remote%/%branch%; 11 | 12 | set found=0 13 | @echo on 14 | 15 | cd 16 | 17 | @echo git reset --hard HEAD 18 | @%git% reset --hard HEAD 19 | 20 | @echo git fetch %remote% 21 | @%git% fetch %remote% 22 | 23 | 24 | 25 | @for /f "tokens=1,2 delims= " %%i in ('%git% branch') do ( 26 | @if "%branch%" == "%%i" @set found=1 27 | @if "%branch%" == "%%j" @set found=1 28 | ) 29 | 30 | @if %found% equ 1 goto merge 31 | @if %found% equ 0 goto checkout 32 | 33 | :merge 34 | @echo git checkout -q %branch% 35 | @%git% checkout -q %branch% 36 | echo git merge %remote%/%branch% 37 | @%git% merge %remote%/%branch% 38 | @%git% submodule update --init --recursive 39 | exit 40 | 41 | :checkout 42 | @echo git checkout %remote%/%branch% -b %branch% 43 | @%git% checkout %remote%/%branch% -b %branch% 44 | @%git% submodule update --init --recursive 45 | exit 46 | 47 | :end -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | HOME_PATH=`echo ~` 4 | APP_NAME=hookagent 5 | 6 | CONFIG_PATH="$HOME_PATH/.$APP_NAME" 7 | LOG_PATH="$CONFIG_PATH/log" 8 | 9 | PROJECT=$1 10 | 11 | remote=$2 12 | branch=$3 13 | 14 | echo "start deploying $remote/$branch" 15 | 16 | if [[ ! -d $LOG_PATH ]]; then 17 | mkdir -p $LOG_PATH 18 | fi 19 | 20 | TEMP_FLAG=$LOG_PATH/.$PROJECT.$remote-$branch 21 | 22 | if [[ ! -f $TEMP_FLAG ]]; then 23 | touch $TEMP_FLAG 24 | else 25 | echo "still deploying..." 26 | exit 0 27 | fi 28 | 29 | 30 | found=0 31 | 32 | pwd 33 | 34 | echo "git reset --hard HEAD" 35 | git reset --hard HEAD 36 | echo "git fetch $remote" 37 | git fetch $remote 38 | 39 | for br in $(git branch | sed 's/^[\* ]*//') 40 | do 41 | if [[ $br == $branch ]]; then 42 | echo "found branch: $branch" 43 | found=1 44 | fi 45 | done 46 | 47 | if [[ $found == 1 ]]; then 48 | echo "git checkout -q $branch" 49 | git checkout -q $branch 50 | echo "git merge $remote/$branch" 51 | git merge $remote/$branch 52 | else 53 | echo "git checkout $remote/$branch -b $branch" 54 | git checkout $remote/$branch -b $branch 55 | fi 56 | 57 | git submodule update --init --recursive 58 | 59 | rm $TEMP_FLAG 60 | -------------------------------------------------------------------------------- /bin/cli.bat: -------------------------------------------------------------------------------- 1 | set APP_NAME=hookagent 2 | 3 | :: get current user 4 | set currentUser=%username% 5 | 6 | :: get project root path 7 | set LIB_PATH=..\ 8 | 9 | set SERVER_SCRIPT=%LIB_PATH%\server.js 10 | set CONFIG_SOURCE=%LIB_PATH%\config.json 11 | set CONFIG_TARGET=%UserProfile%\.%APP_NAME%\config.json 12 | set LOG_PATH=%UserProfile%\.%APP_NAME\log% 13 | 14 | :: check if exists config file. 15 | if not exist %CONFIG_TARGET% ( 16 | echo No config file found as %CONFIG_SOURCE%. Please run '%APP_NAME% config' first to generate default config. 17 | pause 18 | exit 19 | ) 20 | 21 | :: check if exists log path 22 | if not exist %LOG_PATH% md %LOG_PATH% 23 | 24 | :: get user op 25 | set op=%1 26 | 27 | :: config 28 | if "%op%" == "config" ( 29 | echo %APP_NAME% configurations %CONFIG_TARGET% 30 | notepad %CONFIG_TARGET% 31 | exit 32 | ) 33 | if "%op%" == "start" ( 34 | echo Starting deploy agent server... 35 | pm2 start %SERVER_SCRIPT% --name %APP_NAME% 36 | exit 37 | ) 38 | if "%op%" == "stop" ( 39 | echo Stopping deploy agent server... 40 | pm2 stop %APP_NAME% 41 | exit 42 | ) 43 | if "%op%" == "restart" ( 44 | echo Restarting deploy agent server... 45 | pm2 restart %APP_NAME% 46 | exit 47 | ) 48 | if "%op%" == "update" ( 49 | echo Updating %APP_NAME% 50 | npm update -g hookagent 51 | exit 52 | ) 53 | 54 | echo "Usage: %APP_NAME% " 55 | echo config: to generate configuration file or show content. 56 | echo start: to start the agent as service. 57 | echo stop: to stop the running service. 58 | echo update: to update the version. 59 | -------------------------------------------------------------------------------- /bin/cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | HOME_PATH=`echo ~` 4 | APP_NAME=hookagent 5 | 6 | LIB_PATH="$(dirname $0)/$(dirname $(readlink $0))/.." 7 | SERVER_SCRIPT="$LIB_PATH/server.js" 8 | CONFIG_SOURCE="$LIB_PATH/config.json" 9 | CONFIG_PATH="$HOME_PATH/.$APP_NAME" 10 | CONFIG_TARGET="$CONFIG_PATH/config.json" 11 | LOG_PATH="$CONFIG_PATH/log" 12 | 13 | CONFIGURED=0 14 | 15 | if [[ -f $CONFIG_TARGET && -d $LOG_PATH ]]; then 16 | CONFIGURED=1 17 | fi 18 | 19 | check_config() { 20 | if [[ $CONFIGURED = 0 ]]; then 21 | echo "No config found as $CONFIG_SOURCE. Please run '$APP_NAME config' first to generate default config." 22 | fi 23 | } 24 | 25 | case $1 in 26 | config ) 27 | if [[ $CONFIGURED = 1 ]]; then 28 | echo "$APP_NAME configurations ($CONFIG_TARGET):" 29 | vim $CONFIG_TARGET 30 | else 31 | if [[ ! -d $CONFIG_PATH ]]; then 32 | mkdir -p $CONFIG_PATH && echo "$APP_NAME config folder created at $CONFIG_PATH." 33 | fi 34 | if [[ ! -f $CONFIG_TARGET ]]; then 35 | cp $CONFIG_SOURCE $CONFIG_TARGET && chmod 600 $CONFIG_TARGET && echo "$APP_NAME default configurations generated to $CONFIG_TARGET. You can add your project config as sample in it." 36 | fi 37 | if [[ ! -d $LOG_PATH ]]; then 38 | mkdir -p $LOG_PATH && echo "$APP_NAME log folder created at $LOG_PATH." 39 | fi 40 | fi 41 | ;; 42 | 43 | start ) 44 | if [[ $CONFIGURED = 1 ]]; then 45 | echo "Starting deploy agent server..." 46 | pm2 start $SERVER_SCRIPT --name $APP_NAME 47 | else 48 | check_config 49 | fi 50 | ;; 51 | 52 | stop ) 53 | if [[ $CONFIGURED = 1 ]]; then 54 | echo "Stopping deploy agent server..." 55 | pm2 stop $APP_NAME 56 | fi 57 | ;; 58 | 59 | restart ) 60 | if [[ $CONFIGURED = 1 ]]; then 61 | echo "Restarting deploy agent server..." 62 | pm2 restart $APP_NAME 63 | else 64 | check_config 65 | fi 66 | ;; 67 | 68 | update ) 69 | npm update -g hookagent 70 | ;; 71 | 72 | *) 73 | echo "Usage: $APP_NAME " 74 | echo " config: to generate configuration file or show content." 75 | echo " start: to start the agent as service." 76 | echo " stop: to stop the running service." 77 | echo " update: to update the version." 78 | ;; 79 | 80 | esac 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Git webhook deploy agent 2 | ========== 3 | 4 | For sysadmins simply setup a post hook agent on server to deploy git projects like PaaS from your using third-party git service. 5 | 6 | Usage 7 | ---------- 8 | 9 | ### Requirement ### 10 | 11 | * node version >= 10 12 | * [PM2][] installed in global: `sudo npm install -g pm2` 13 | 14 | ### On your server ### 15 | 16 | $ npm install -g hookagent 17 | 18 | After installed you can use this commands to start the agent: 19 | 20 | $ hookagent config 21 | $ hookagent start 22 | 23 | ### Get ready your repository ### 24 | 25 | Additional things about git you should make sure: 26 | 27 | * Project repository alread cloned once from 3rd-party git service. 28 | 29 | * All repository files(folders) is own by the specific user(s) you configured in the config file (`~/.hookagent/config.json`, see below). 30 | 31 | * Within the user, the ssh-key has been generated (no password), and set as deploy key in 3rd-party git service. 32 | 33 | * The ssh-key has been used at least once, to make sure it has been add to the list of known hosts of the service. 34 | 35 | ### Post hook ### 36 | 37 | Set a git post hook in the admin panel of your repository like this: 38 | 39 | [POST]:http://user:password@deploy.yourserver.com:6060/project/id@remote/branch 40 | 41 | * `user:password` is reqired part in post URL. The agent will check the request with HTTP basic authentication to avoid mistake request. 42 | 43 | * `6060` as port is set in the config, you can change it as you wish. 44 | 45 | * `/project/:id` is the router, `@remote/branch` is optional default to `origin/master`. If branch (or with remote) is set in hook URL, the part after `@` should be exactly match the config (see below). 46 | 47 | ### Configuration ### 48 | 49 | Before start the agent first time, run `hookagent config` to generate a config file named `config.json` in `~/.hookagent` folder(`C:\Users\YourName` for windows). 50 | 51 | Here is a sample of configuration structure: 52 | 53 | ```javascript 54 | { 55 | // The HTTP listening port 56 | "port": 6060, 57 | // Default branch which will be updated when not set in post request 58 | "defaultBranch": "origin/main", 59 | 60 | // Projects map. ID: object 61 | "projects": { 62 | "sample": { 63 | // branch 64 | "master": { 65 | // Project path 66 | "path": "/var/www/sample", 67 | 68 | // Task to be run after git pull, such as build etc. 69 | // "shell": "./build.sh", 70 | 71 | // Users in list allow to trigger deploy 72 | "users": ["abc"] 73 | }, 74 | "dev": { 75 | "path": "/var/www/test", 76 | "users": ["abc"] 77 | } 78 | } 79 | }, 80 | 81 | // Users list for HTTP basic authentication. ID: password 82 | // Each user ID should match server user name. 83 | "users": { 84 | "abc": "ba1f2511fc30423bdbb183fe33f3dd0f" 85 | }, 86 | // add git path for windows server 87 | "gitPath": "C:\Program Files\Git\bin\git.exe" 88 | } 89 | ``` 90 | 91 | **Make sure** that when using different branches in one project, the `path` of branch shouldn't be same on one server. This is just for different usage (such as testing) base on branch mapping. 92 | 93 | Once the config file generated, run the `hookagent config` will open the content with `vim`. 94 | 95 | **When you use it on Windows server, you should use node as default open method to javascript.** 96 | 97 | Roadmap 98 | ----------- 99 | 100 | * Add logger. 101 | * Add Github/Bitbucket/Gitlab post data parsing to avoid none updated pulling. 102 | * Add admin panel using basic authentication. 103 | * Use SQLite or other database to manage config data. 104 | * Add RSA authentication instead of basic HTTP. 105 | 106 | MIT Licensed 107 | ---------- 108 | 109 | -EOF- 110 | 111 | [PM2]: https://github.com/Unitech/PM2 112 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var child_process = require('child_process'); 4 | 5 | var express = require('express'); 6 | var basicAuth = require('basic-auth'); 7 | 8 | var currentPlatform = require('./env'); 9 | 10 | function hook(req, res, next) { 11 | console.log('Deployment request received: ' + JSON.stringify(req.params)); 12 | 13 | var id = req.params[0]; 14 | if (!id) { 15 | console.log('[400] Bad request. Without project id.'); 16 | return res.status(400).end(); 17 | } 18 | 19 | // find project in config 20 | var project = config.projects[id]; 21 | if (!project) { 22 | console.log('[404] Not found. No project named as "' + id + '" found.'); 23 | return res.status(404).end(); 24 | } 25 | 26 | // find branch options in config 27 | var branch = req.params[1] || config.defaultBranch || 'orign/main'; 28 | var options = project[branch]; 29 | if (!options) { 30 | console.log('[404] No options of branch "' + branch + '" found. Please check config.'); 31 | return res.status(404).end(); 32 | } 33 | 34 | var branchParam = branch.split('/'); 35 | var remote = branchParam[0]; 36 | if (branchParam.length == 1) { 37 | remote = config.defaultRemote || 'origin'; 38 | } else { 39 | branch = branchParam[1]; 40 | } 41 | 42 | // check auth 43 | var auth = basicAuth(req); 44 | if (!auth || 45 | !auth.pass || 46 | options.users.indexOf(auth.name) < 0 || 47 | config.users[auth.name] != auth.pass) { 48 | console.log('[403] Forbidden.'); 49 | return res.status(403).end(); 50 | } 51 | 52 | console.log('Authentication passed.'); 53 | 54 | if (!fs.existsSync(options.path)) { 55 | console.log('[404] No path found for project: "' + id + '"'); 56 | return res.status(404).end(); 57 | } 58 | 59 | var execFileOptions = Object.assign({}, currentPlatform.execFileOptions, { cwd: options.path }); 60 | 61 | if (currentPlatform.posix) { 62 | var uid = parseInt(child_process.execSync('id -u ' + auth.name).toString().trim(), 10); 63 | var gid = parseInt(child_process.execSync('id -g ' + auth.name).toString().trim(), 10); 64 | var home = child_process.execSync('echo ~' + auth.name).toString().trim(); 65 | 66 | if (auth.name && (uid == null || !home)) { 67 | return res.status(404).send('[404] No user or home directory found'); 68 | } 69 | 70 | execFileOptions.uid = uid; 71 | execFileOptions.gid = gid; 72 | 73 | // when running hookagent under root 74 | if (process.env.USER === 'root') { 75 | // get the target user's env 76 | var envBuffer = child_process.execSync(`su - -c env ${auth.name}`); 77 | envBuffer.toString().split('\n').forEach(line => { 78 | var [key, ...value] = line.split('='); 79 | execFileOptions.env[key] = value.join(''); 80 | }); 81 | } else { 82 | Object.assign(execFileOptions.env, process.env); 83 | } 84 | } 85 | 86 | res.status(200).send('ok'); 87 | 88 | child_process.execFile(path.join(__dirname, 'bin', `deploy.${currentPlatform.ext}`), [ 89 | id, 90 | remote, 91 | branch, 92 | config.gitPath || '' 93 | ], execFileOptions, function (error, stdout, stderr) { 94 | console.log(stdout); 95 | 96 | if (error) { 97 | console.error(error); 98 | } else { 99 | console.log('Deployment done.'); 100 | } 101 | }); 102 | 103 | console.log('[200] Deployment started.'); 104 | } 105 | 106 | var config = require(currentPlatform.configPath); 107 | 108 | var agent = express(); 109 | 110 | agent.get('/', function (req, res, next) { 111 | // indicate process is running 112 | res.status(200).send('ok'); 113 | }); 114 | 115 | // [POST]:/project/project-name[@[remote/]branch-name] 116 | agent.post(/\/project\/([\w\.\-]+)(?:@([\w\/\.\-]+))?/i, hook); 117 | 118 | agent.listen(config.port, function() { 119 | console.log("Hook agent started at %s. Listening on %d", new Date(), config.port); 120 | }); 121 | --------------------------------------------------------------------------------