Node.jsでgitHubのWebhookを動かして快適な自動デプロイ生活

Node.js で gitHub の Webhook を動かして快適なデプロイ生活を送らせて頂いてるのですが、自分だけ快適というのもあれなので、感謝の気持ちを込めて github にパブリックドメインで公開しました。発展途上でもあるので自己責任とバグ報告とかよろしくということで。

GitHub:  [node-git-webhook](https://github.com/toshirot/node-git-webhook/blob/master/README.md)

conf.js

```js
module.exports={
    "example.net": {
        about: "github webhook for example.net",
        secret: "Your シークレット for example.net"
    }
}
```

git-hook.js

```node.js
'use strict';

/*
github リポジトリへのpushなどだけでサーバー側に自動デプロイする。
これは、例えば example.netというレポジトリのdevブランチへのpushをすると
サーバー側の /pathTo/example.net 以下の内容を git からpull して更新する
@see https://github.com/toshirot/node-git-webhook
*/


// import
const fs = require('fs');
const https = require('https');
const crypto = require('crypto');
const exec = require('child_process').exec;

// https server setting
const PORT = '1234';
const HOST='example.net';
const pemPath='/etc/letsencrypt/live/'+HOST;// path of letsencript pem 
const CERT = fs.readFileSync(pemPath+'/fullchain.pem');
const KEY = fs.readFileSync(pemPath+'/privkey.pem');

// config
const conf=require(__dirname+'/conf');
const SECRET = conf[HOST].secret;

// git settings
const BRANCHName='dev';// 'master'|'dev'
const targetBRANCH = 'refs/heads/'+BRANCHName;//'refs/heads/master'|'refs/heads/dev'
const targetRepositoryDir='/home/hoge/'+HOST; // HOST名/public_html などが多いかな？
const pullStr='sudo git pull origin '+BRANCHName;
const REPOSITORY_NAME = HOST;

// log file
const logFilePath = '/home/hoge/webhook/github-webhook-'+HOST+'.log';

// ----------------------------------------------------------------------------
// HTTPSサーバー
// 
const option={
    cert: CERT,
    key: KEY,
};
const server = new https.createServer(option, function (req, res){
    let payload='';
    req.on('data', function(chunk) {
        
        payload+=chunk.toString();

        // chk SECRET
        if(!chkSECRET(req, payload))return;
         
        // parse payload
        try{
            payload=JSON.parse(payload);
        } catch(e){
            console.log('parse err', e)
        }

        // chk repositoryName
        if(!chkRepositoryName(payload))return;

        // chk target BRANCH
        if(!chkBranchName(payload))return;
    
        // do pull
        exec(pullStr, { cwd: targetRepositoryDir }, (error, stdout, stderr) => {
            if (error) {
                writeLog(`exec error: ${error}`);
                return;
            }
            writeLog(`3 stdout: ${stdout}`);
            writeLog(`3 stderr: ${stderr}`);
        });

    });
    res.end();

}).listen(PORT);

// ----------------------------------------------------------------------------
// シークレットのチェック 
// @return true|false
// 
function chkSECRET(req, data){
    // console.log(444,'chkSECRET')
    let sig = ''
        + 'sha1='
        + crypto
        .createHmac('sha1', SECRET)
        .update(data)
        .digest('hex');

    if (req.headers['x-hub-signature'] !== sig){
        return false;//シークレットが違えばパス
    } else {
        return true;
    }
}

// ----------------------------------------------------------------------------
// リポジトリ名のチェック 
// @return true|false
// 
function chkRepositoryName(payload){
    let repositoryName = payload.repository.name;
    console.log(555,'chkRepositoryName', REPOSITORY_NAME===repositoryName, REPOSITORY_NAME, repositoryName)

    if(REPOSITORY_NAME!==repositoryName){
        return false;//リポジトリ名が違えばパス
    } else {
        return true;
    }
}

// ----------------------------------------------------------------------------
// ブランチ名のチェック 
// @return true|false
// 
function chkBranchName(payload){
    console.log(666, 'chkBranchName', payload.ref===targetBRANCH, payload.ref, targetBRANCH)
    if(payload.ref!==targetBRANCH){
        return false;//ブランチ名が違えばパス
    } else {
        return true;
    }
}

// ----------------------------------------------------------------------------
// Date関連
// 
function getTimesInt(str, date){
    /* e.g.
      getTimesInt('Y');//2017
  
      var H = getTimesInt('H');
      var Mi= getTimesInt('Mi');
      var HM= H+':'+Mi; //'8:30'
  
      var W = getTimesInt('W');
      var week = new Array('日', '月', '火', '水', '木', '金', '土');
      week[W]; 
    */
    var curr = (date)?new Date(date):new Date();
    var Y  = +curr.getFullYear()
    var M  = +curr.getMonth()+1
    var D  = +curr.getDate()
    var H  = +curr.getHours()
    var Mi = +curr.getMinutes()
    var S  = +curr.getSeconds()
    var Ms = +curr.getMilliseconds()
    var W  = +curr.getDay()
  
    switch(str){
      case 'Y': return Y;break;
      case 'M': return M;break;
      case 'D': return D;break;
      case 'H': return H;break;
      case 'Mi': return Mi;break;
      case 'S': return S;break;
      case 'Ms': return Ms;break;
      case 'W': return W;break;
      default: return curr;
    }
    str= date=curr=Y=M=D=H=Mi=S=Ms=W=null;
  }
  function zero(num){//e.g. 3->'03'
    return num<10?'0'+num:''+num;
  }
  function getYMDHMiS(date){
    var Y = getTimesInt('Y',date);
    var M= getTimesInt('M',date);
    var D= getTimesInt('D',date);
    var H = getTimesInt('H',date);
    var Mi= getTimesInt('Mi',date);
    var S= getTimesInt('S',date);
    return Y+'-'+zero(M)+'-'+zero(D)+' '+zero(H)+':'+zero(Mi)+' '+zero(S); //'2017-01-04 08:30 05'
  }


// ----------------------------------------------------------------------------
// logの出力
// 
const writeLog = function (data){
    ifNotExistMkNewFile(logFilePath);
    console.log(222,  getYMDHMiS() + ' ' +data);
    fs.appendFile(logFilePath,  getYMDHMiS() +' '+ data+'\n', function (err) {
        if (err) {
            console.log(getYMDHMiS() +'error:'+  err);
            writeLog(getYMDHMiS() +'error:'+  err+'\n');
            throw err;
        }
    });
}

// ----------------------------------------------------------------------------
// logFilePathファイルが存在しなければ作る for log
// 
function ifNotExistMkNewFile(file){
    if(!isExistFile(logFilePath)){
        fs.writeFile(logFilePath, '', function (err) {
            if (err) {
                throw err;
            }
        })
    }
}

// ----------------------------------------------------------------------------
// ファイルの存在チェック for log
// @return true|false
// 
function isExistFile(file) {
    try {
      fs.statSync(file);
      return true
    } catch(err) {
      if(err.code === 'ENOENT') return false
    }
}
```

## 概要
github リポジトリへのpushなどだけでサーバー側に自動デプロイする。

例えば example.netというレポジトリのdev-2fブランチへのpushをするとサーバー側の /pathTo/example.net 以下の内容を git からpull して更新する。

## 設置例
```
    pathTo/
      ├── git-hook.js
      └── conf.js
```
## 設定手順例

#### 対象リポジトリ名
example.net
#### 対象ブラインチ名
dev-2f

#### 対象サーバーのパス例
```
    pathTo/
        └── example.net
                ├── html     
                ├── server
                └── etc..
```
#### githubの設定

まず、githubリポジトリで Settings > Webhooks に入り、「Add Webhook」ボタンを押すとGithubのパスワード入力が求められるので入力すると、下図のような「Webhooks /Add webhook」ページが現れます。

なお、Webhookのドキュメントは下記にあります。
[GitHub Developer Webhooks](https://developer.github.com/webhooks/)

![githubの設定](https://chatfor.net/img/github-webhook-1.png)

#### 「Webhooks /Add webhook」ページの入力手順は下記の通り

1) github > settings > WebHook の Payload URL に

```
　  https://<HOST名>:<PORT名>
```

2) github > settings > WebHook の Content type

```
    application/json
```

3) github > settings > WebHook の Secret

```
    conf.js に書いたのと同じシークレット
```

4) github > settings > WebHook の SSL verification

```
    今時は、✔️Enable SSL verification  
```
※要SSL設定 例えばLet's Encriptなどで。

5) github > settings > WebHook の Which events would you like to trigger this webhook?

```
    このWebhookをトリガーしたいイベントはどれですか？
    ✔️Just the push event.
```

6) github > settings > WebHook の Active

```
    ✔️Active
```

#### 対象デプロイサーバー側の設定

1) ssh-keygenで秘密鍵、公開鍵を作る

```
    sudo ssh-keygen -t rsa -b 4096 -C "Your@e-mail"  -f /root/.ssh/id_rsa_github_example.net
```

これで 

```
    /root/.ssh/id_rsa_github_example.net 秘密鍵
    /root/.ssh/id_rsa_github_example.net.pub　公開鍵
```

が生成される　
    （※ ここでは、 https の ssl の鍵に root権限 が必要なので sudo で動かすことを前提にしています。 
       sudo 以外で動かすときはその部分をそれぞれの権限用に読み替えてください。）

```
    この公開鍵を github settings > Deploy keys へ追加する
```
    
2) プライベートリポジトリの場合は、.ssh/configに鍵のパスを登録しておく

```
    sudo vi /root/.ssh/config
```

```

    # example.net
    Host            example.net
    HostName        github.com
    Port            7890
    IdentityFile   /root/.ssh/id_rsa_github_example.net
    User            git
    IdentitiesOnly yes
```

3) その他、gitにパスワード を毎回聞かれる場合問題
[git パスワード を毎回聞かれる問題の解決方法](
https://qiita.com/rorensu2236/items/df7d4c2cf621eeddd468)

```
git remote set-url origin git@github.com:<アカウント名>/<リポジトリ名>.git
```

#### 足りないモジュールを読み込んでおく

```
npm i crypto --save
```

暗号関連のメジャーなモジュールです。シークレットのチェックに使っています。

#### 対象デプロイサーバー側での起動とデーモン

pm2やforeverなどでgit-hook.jsを起動します。

```
sudo pm2 start /pathTo/git-hook.js
```

#### 参考：pm2やnodeをrootで動かすには(Ubuntu で NVMを使うケース)

```
//////////////////////////////////////////////
nvmでnode.jsインストール

sudo apt install git-core
git clone git://github.com/creationix/nvm.git ~/nvm

. ~/nvm/nvm.sh
echo ". ~/nvm/nvm.sh" >> ~/.bashrc

//////////////////////////////////////////////
node.jsインストール 

ここではv10の最新を入れる

$ nvm install v10
//////////////////////////////////////////////
pm2 インストール 

npm install pm2 -g

sudo pm2 start hoge.js したあと自動起動するには
sudo pm2 startup ubuntu
sudo pm2 save

//////////////////////////////////////////////
$ which node
/home/hoge/nvm/versions/node/v10.15.1/bin/node
$ which pm2
/home/hoge/nvm/versions/node/v10.15.1/bin/pm2

wssをrootで実行するためにリンクを張っておく
sudo ln -s /home/hoge/nvm/versions/node/v10.15.1/bin/node /usr/bin/node
sudo ln -s /home/hoge/nvm/versions/node/v10.15.1/bin/pm2 /usr/bin/pm2
//////////////////////////////////////////////
g++ or c++ などが不足してることもあるので
sudo apt install build-essential
sudo apt install libssl-dev



```

## License
### Public domain


#### 参考サイト:
Node.jsサーバーでGitHubhook受け取ってpushだけで簡単にページ反映させる https://qiita.com/kokoyoshi/items/c1c6050e8ef09aa78132 #Qiita