デプロイ用のシェルスクリプトの最初で、テストを走らせるようにしました。テストが落ちたらデプロイを止めるつもりでした。
#!/bin/bash
set -e
cd webapp && npm test && cd ..
cd webapp && npm run build && cd ..
firebase deploy
set -e を付けているので、npm test が失敗すればそこで止まる、と思っていました。ところが、テストが落ちてもデプロイまで進んでしまう書き方になっていました。
再現
false(必ず失敗するコマンド)で試してみます。
set -e
cd d && false && cd ..
echo "A: 止まらずにここまで来た (pwd=$(basename $PWD))"
$ bash a.sh
A: 止まらずにここまで来た (pwd=d)
$ echo $?
0
止まりません。しかも cd .. が実行されていないので、カレントディレクトリが d のまま次の処理に進んでいます。
なぜ止まらないのか
Bash のマニュアルでは、set -e(errexit)について次のように決まっています。&& や || でつないだコマンドのうち、最後のコマンド以外が失敗しても、シェルは終了しない。
cd d && false && cd .. では、false は最後のコマンドではありません(最後は cd ..)。そのため、false が失敗すると、後ろの cd .. は実行されません。それでも set -e は働かず、次の行に進みます。
比較のため、失敗するコマンドが最後になる書き方も試します。
set -e
cd d && false
echo "C: 最後のコマンドが失敗"
$ bash c.sh
$ echo $?
1
こちらはちゃんと止まります。「&& の途中で失敗した」ときだけ止まらない、というのがポイントです。
直し方:サブシェルで囲む
cd をサブシェル ( ) の中で行うようにしました。
set -e
(cd d && false)
echo "B: ここは表示されないはず"
$ bash b.sh
$ echo $?
1
サブシェル全体は1つのコマンドとして扱われ、その終了コードは中で最後に実行したコマンド(ここでは false)の終了コードになります。これが失敗するので、set -e が働いて止まります。
さらに、cd の効果がサブシェルの中に閉じるので、cd .. で戻る必要もなくなります。
#!/bin/bash
set -e
# `cd webapp && npm test && cd ..` の形だと、途中のコマンドの失敗では
# set -e が働かず処理が続いてしまうため、サブシェルで実行する
(cd webapp && npm test)
(cd webapp && npm run build)
firebase deploy
ほかにも、次の書き方があります。
-
cd webapp && npm test || exit 1のように、明示的に終了させる - 1行にまとめず、
cd webapp→npm test→cd ..と行を分ける(set -eは各行に効く)
どちらでも止まりますが、カレントディレクトリが戻らない問題まで一度に防げるので、サブシェルが一番扱いやすいと感じました。
まとめ
-
set -eは、&&でつないだコマンドの途中の失敗では止まらない -
cd dir && cmd && cd ..は、cmdが失敗すると止まらず、しかもディレクトリが戻らない -
(cd dir && cmd)とサブシェルで囲めば、失敗で止まり、ディレクトリも戻す必要がない
「テストが落ちたらデプロイしない」ような安全のための仕組みこそ、わざと失敗させて止まるかを一度確かめておくのがおすすめです。
参考
個人で開発している割り勘アプリ「WaliPro」のデプロイスクリプトで見つけた話でした。