2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-17

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

GitHub: node-git-webhook

conf.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

githubの設定

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

  1. github > settings > WebHook の Payload URL に
   https://<HOST名>:<PORT名>
  1. github > settings > WebHook の Content type
    application/json
  1. github > settings > WebHook の Secret
    conf.js に書いたのと同じシークレット
  1. github > settings > WebHook の SSL verification
    今時は、✔️Enable SSL verification  

※要SSL設定 例えばLet's Encriptなどで。

  1. github > settings > WebHook の Which events would you like to trigger this webhook?
    このWebhookをトリガーしたいイベントはどれですか?
    ✔️Just the push event.
  1. 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 へ追加する
  1. プライベートリポジトリの場合は、.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
  1. その他、gitにパスワード を毎回聞かれる場合問題
    git パスワード を毎回聞かれる問題の解決方法
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

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?