更新履歴
- : デジタルサーカス株式会社の社内記事として公開
- : ブログ記事として一般公開
ハマったのでメモ。
前提
GitLab CI/CD について
GitLab CI/CD では、Docker executor を用いて任意の Docker image 上でスクリプトを走らせることができる。
例:
hello-world:
stage: test
image: alpine:latest
script:
- 'echo "Hello, World!"'
rules:
- if: '$CI_MERGE_REQUEST_IID'
when: always
ここで、script
に指定したコマンドが失敗する (exit status が 0 以外になる) と、即座に実行が停止され、ジョブは失敗する。
では、次のようなケースだとどうなるか。
hello-world:
stage: test
image: alpine:latest
script:
- 'exit 1 | exit 0'
rules:
- if: '$CI_MERGE_REQUEST_IID'
when: always
失敗するコマンドをパイプに接続した。通常 Bash では、パイプの最後のコマンドの exit code が全体の exit code になる。
pipefail
オプションについて
前述したようなケースにおいて、途中で失敗したときに全体を失敗させるには、pipefail
オプションを有効にする。
# On にする
set -o pipefail
# Off にする
set +o pipefail
こうすると、パイプ全体が失敗するようになる。 この設定は、デフォルトだと off になっている。
発生した問題
次のような GitLab CI/CD ジョブが失敗してしまった。
hoge:
stage: test
image: alpine:latest
script:
- 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"'
rules:
- if: '$CI_MERGE_REQUEST_IID'
when: always
grep
コマンドは、パターンにマッチする行が一行もなかったとき、exit code 1 を返す。よって、pipefail
が on になっていると、このジョブは失敗する。 現在の pipefail
がどうなっているか確かめるため set +o
で全オプションを出力させたところ、pipefail
が on になっていた。
しかし、先述したように Bash における pipefail
のデフォルト値は off のはずだ。 実際に、ローカルで alpine:latest
を動かしてみたところ、
$ docker run --rm alpine:latest sh -c "set +o"
set +o errexit
set +o noglob
set +o ignoreeof
set +o monitor
set +o noexec
set +o xtrace
set +o verbose
set +o noclobber
set +o allexport
set +o notify
set +o nounset
set +o vi
set +o pipefail
確かに pipefail
は無効になっている。
なぜスクリプト内で set -o pipefail
しているわけでもないのに pipefail
が on になっているのか。
どこで pipefail
が on になるか
.gitlab-ci.yml
で明示的には書いていないので、GitLab Runner (GitLab CI/CD のスクリプトを実行するプログラム) が勝手に追加しているに違いない。 そう仮説を立てて GitLab Runner のリポジトリ を調査したところ、ソースコード中の以下の箇所 で set -o pipefail
していることが判明した (コメントは筆者による)。
// pipefail オプションが存在しない環境にも対応するため、
// 先に set -o でオプション一覧を表示させたあと、set -o pipefail している
buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
どのように解決するか
通常の Bash スクリプトを書く場合と同様に、pipefail
が on になっていては困る場所だけ off にしてやればよい。
hoge:
stage: test
image: alpine:latest
script:
+ - 'set +o pipefail'
- 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"'
+ - 'set -o pipefail' # この例の場合、ここで終わりなので戻さなくてもよい
rules:
- if: '$CI_MERGE_REQUEST_IID'
when: always
備考
なお、上述した実装ファイルは shells/bash.go
だが、alpine:latest
の例でもそうであったように、シェルが sh
である場合にも適用される。