[Raspberry Piメモ] Node.jsでCIっぽいことをする(GitHubにPushしたらRaspberry PiがPullして実行)

Raspberry Pi上でTypescriptで書いたプログラムを動かしたいが、開発はPCでやりたい場合は、GitHubからのWebHookをトリガとしてJenkinsからビルド・実行する方法がメジャー(?)だと思う。ただ、JenkinsをRaspberry Pi上で動かすのは敷居が高く感じるし、まだ試行錯誤している段階で、WebHookを受け取るためにポートを解放するのも怖く感じる。

Jenkinsを導入していなくても、ちょっとしたことでCIっぽいことができるので、以下に方法をまとめる。複雑なことをやろうとしたらJenkinsの方が良いかもしれないが、取り敢えず動かしたいだけならそこそこ使えそうだった。

ここではNestJSのプログラムで試しているが特にプラットフォームやライブラリは問わない。

# 概要
Raspberry Piは非力なので、TypescriptのトランスパイルまでをPCで行う。手間を省くため、[1]ソースの編集があるたびに[2]変更を検出して自動的にトランスパイルを行う。(Eclipseであればエラーをすぐ見つけてくれるので、変更の度にビルドする必要はないが、たまにビルドしないと分からない変なエラーがあるので用意しておく)

PC上での編集が終わりRaspberry Pi上で動作確認する段階になったら、[3][4]ソースとトランスパイル結果をGitHubにpushする。[5]PCからのPushが完了したら、TortoiseGitの`Post-push hook`によりRaspberry Piへpush完了を通知する。

[7]Raspberry Pi上の「通知受付」が[6]push完了の通知を受け取ったら`git pull`し、ソースとトランスパイル結果を更新する。[8]Raspberry Pi上のnodemonがトランスパイル結果の変更を検出したら、nodeを再起動し更新されたプログラムを開始する。

なお、Raspberry Pi上の「通知受付」はJenkinsでも実現できるものの、ここでは簡易的にHTTPリクエストを受け付けたら`git pull`するだけにする。簡単ではあるが、ローカルでWebHookに相当することができる。
> ローカルならDDoS的なことは気にしなくてよいが、Pullの間隔を制限するなど工夫した方がベターだとは思う。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/52dcb26c-fcc3-7ce9-8319-9d71ce3b4808.png" width="75%">

#準備: Node.jsのインストール
まずはNode.jsをインストールすることから始める。Raspberry Pi3 B+では「[Node.js and Raspberry Pi](https://www.w3schools.com/nodejs/nodejs_raspberrypi.asp)」のコマンドを実行するだけでインストールできる。

```text:v12系の最新版をインストールする場合
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
```

しかし、ずいぶん前に購入したRaspberry Pi B+はCPUがARMv6のため、以下のようなメッセージが出てインストールできなかった。

```text:ARMv6で起きるエラー
You appear to be running on ARMv6 hardware.
Unfortunately this is not currently supported by the NodeSource Linux distributions.
Please use the 'linux-armv6l' binary tarballs available directly from nodejs.org for Node.js 4 and later.
```

その場合は、対象を直接wgetして解凍、インストールしたい場所にコピーすることになる。ARMv6向けのバイナリは現時点のLTSである10.16.0と、サポートが無い11.xしかない。

```sh:ARMv6へのインストール
wget https://nodejs.org/dist/latest-v10.x/node-v10.16.0-linux-armv6l.tar.gz
tar -zxvf node-v10.16.0-linux-armv6l.tar.gz
cd node-v10.16.0-linux-armv6l.tar.gz
sudo cp -R * /usr/local/
```

# 設定
人が行う操作[1][3]以外が行えるよう、設定・ちょっとしたプログラムの作成を行う。

##package.jsonの設定
[2]PC上の変更検出・自動トランスパイルのために、`build:watch`コマンドを作る。このコマンドでは`-w`(watch)オプションを付けて`tsc`を実行することにより、`tsconfig.build.json`で指定したソースディレクトリ内の変更を検出し、自動的にトランスパイルを開始する。


また、[8]Raspberry Pi上の変更検出・node再起動のために、`start:raspi`コマンドを作る。このコマンドでは、nodemonによりトランスパイル結果`dist`の中身が変更されたらnodeを再起動する。

```json:package.json
{
  "scripts": {
    "build:watch": "tsc -w -p tsconfig.build.json",
    "start:raspi": "nodemon --config nodemon-raspi.json",
  },
}
```

###tsconfig.build.jsonの変更
`tsconfig.build.json`(NestJSが作成したファイル)で`dist`を変更の検出対象から除外することで、`build:watch`でトランスパイル→トランスパイル結果`dist`が変わる→変更を検出→トランスパイル…にならないようにする。(実際は無限ループは起きないが、エラーになる)。これには`tsconfig.build.json`(ベースとなるNestJSが生成したファイルの場合)に対して、`exclude`にトランスパイル結果`dist`を追加すればよい。

```json:tsconfig.build.json変更(distを除外)
{
  "extends": "./tsconfig.json",
  "exclude": ["node_modules", "test", "**/*spec.ts", "dist" ]
}
```

###nodemon-raspi.jsonの作成
トランスパイル結果`dist`内の`.js`ファイルが変更されたらnodeを再起動するよう、nodemonの設定ファイルを以下のように作成する。

```json:nodemon-raspi.json
{
  "watch": ["dist"],
  "ext": "js",
  "exec": "node dist/main.js"
}
```

## 通知受付サーバの作成
Raspbery Pi上に、[5]PCからPush完了の通知を受け付け、[6]`git pull`するための小規模なサーバを設ける。ここでは、Listenしているポートで何らかのリクエストを受信したら`git pull`するだけの簡易的な機能にとどめている。

```javascript:trigger/index.js
const http = require( 'http' );
const spawn = require('child_process').spawn;

// git pullする。spawn( 'git pull' )にするとエラーになるので注意。
function executePull() {
	const prc = spawn( 'git', [ 'pull' ] );
	
	return new Promise( ( resolve, reject ) => {
		prc.once( 'exit', code => { 
			resolve( code );
		} );
		prc.once( 'error', err => { 
			reject( err )
		} );
	} );
}

// 通知受付サーバを作る
const app = http.createServer( ( req, res ) => {
	let message = '';
	
	executePull().then( ( code ) => {
		// 成功した場合
		res.writeHead( 200, { 'Content-Type': 'text/plain' } );
		message = `Pull succeeded. (code:${code})`;
	} ).catch( ( err ) => {
		// 失敗した場合
		res.writeHead( 405, { 'Content-Type': 'text/plain' } );
		message = `Pull failed. (code:${err})`;
	} ).finally( () => {
		console.log( message );
		// 応答を返す
		res.write( message );
		res.end();
	} );
} );

// 通知受付サーバを起動する。ポートは適当。
app.listen( 4000 );
```

# Post-push Hookの追加
TortoiseGitの設定画面の「Hook Scripts」で、[5]Push完了をRaspberry Piに通知するためのスクリプトを追加する。フックはTortoiseGit全体に適用されてしまう(らしい)ので、「Run when working tree path is under:」で適用対象を絞るとよい。また、通知を行うコマンドはHTTPリクエストさえRaspberry Piに届けばよいので、`curl <RaspberyPiのIP:簡易トリガのPort>`のように設定する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/f7ba91ab-d092-2452-8685-79f8e078be58.png)

# 起動
予め上記設定をしたプログラムをGitHubにpush済みの状態から、pushをトリガに変更が自動的に反映できるようにする。

## ソースの入手/更新とプログラムの起動
Raspberry Pi上に何もない状態から開始する場合、`git clone`でGitHubにpush済みのプログラムを入手する。既にclone済みであれば、`git pull`により更新する。

```sh
# コードをGitHubから持ってくる。初回はclone、2回目以降はpullで良い。
git clone <repository> <clone先>
cd <clone先>

# パッケージをインストールする(これは実行するところでしかできない)
npm install

# package.json で追加した Raspberry Pi用起動コマンド。
# distの変更を検出したらnodeを再起動する。
npm run start:raspi

# Raspberry Piにトランスパイルからやらせたければ、NestJSで元々存在する以下のコマンドで起動する。
# Rapsberry Pi B+ではNestJSのテンプレ(Hello Worldレベル)でさえ3～4分かかるので、かなり遅い。
# npm run start:dev
```

## 通知受付サーバの起動
通知受付サーバは以下のようなコマンドで起動する。(「[Linuxコマンド(Bash)でバックグラウンド実行する方法のまとめメモ](https://qiita.com/inosy22/items/341cfc589494b8211844)」参照)

```sh:通知サーバのディレクトリでのコマンド例
# フォアグラウンドで実行…画面上にpullできたか表示されるので安心
node index.js

# バックグラウンドで実行
node index.js &

# SSH切断後もバックグラウンドで実行
nohup node index.js &
```

# まとめ
動作しているところの動画は用意していないが、以上のやり方でPCで行った変更をGitHub経由で反映し、動作確認することができるようになった。リソースが潤沢なPCであればJenkinsという汎用的な手段が使えると思うが、上記の設定だけでも似たようなことが軽量に実行できるので、お手軽に使いたい場合に役に立つかと思う。