summaryrefslogtreecommitdiffhomepage
path: root/vhosts/blog/content/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd.ndoc
blob: d65fffb52bd7c389d2a4454950805ef192a66a9a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
---
[article]
uuid = "a4c326a6-5ffe-450c-abf2-45833c5efb6a"
title = "【GitLab】 GitLab CI/CD 上での bash/sh は pipefail が有効になっている"
description = "GitLab CI/CD で bash/sh スクリプトを動かすと、pipefail オプションが有効になった状態で実行される。"
tags = [
  "ci-cd",
  "gitlab",
]

[[article.revisions]]
date = "2022-11-17"
remark = "デジタルサーカス株式会社の社内記事として公開"
isInternal = true

[[article.revisions]]
date = "2024-04-21"
remark = "ブログ記事として一般公開"
---
<article>
  <note>
    この記事は、2022-11-17 に<a href="https://www.dgcircus.com/">デジタルサーカス株式会社</a> の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。
  </note>
  <p>
    ハマったのでメモ。
  </p>
  <section id="background">
    <h>前提</h>
    <section id="gitlab-ci-cd">
      <h>GitLab CI/CD について</h>
      <p>
        GitLab CI/CD では、Docker executor を用いて任意の Docker image 上でスクリプトを走らせることができる。
      </p>
      <p>
        例:
      </p>
      <codeblock language="yaml" filename=".gitlab-ci.yml">
        <![CDATA[
        hello-world:
          stage: test
          image: alpine:latest
          script:
            - 'echo "Hello, World!"'
          rules:
            - if: '$CI_MERGE_REQUEST_IID'
          when: always
        ]]>
      </codeblock>
      <p>
        ここで、<code>script</code> に指定したコマンドが失敗する (exit status が 0 以外になる) と、即座に実行が停止され、ジョブは失敗する。
      </p>
      <p>
        では、次のようなケースだとどうなるか。
      </p>
      <codeblock language="yaml" filename=".gitlab-ci.yml">
        <![CDATA[
        hello-world:
          stage: test
          image: alpine:latest
          script:
            - 'exit 1 | exit 0'
          rules:
            - if: '$CI_MERGE_REQUEST_IID'
          when: always
        ]]>
      </codeblock>
      <p>
        失敗するコマンドをパイプに接続した。通常 Bash では、パイプの最後のコマンドの exit code が全体の exit code になる。
      </p>
    </section>
    <section id="pipefail-option">
      <h><code>pipefail</code> オプションについて</h>
      <p>
        前述したようなケースにおいて、途中で失敗したときに全体を失敗させるには、<code>pipefail</code> オプションを有効にする。
      </p>
      <codeblock language="bash">
        <![CDATA[
        # On にする
        set -o pipefail
        # Off にする
        set +o pipefail
        ]]>
      </codeblock>
      <p>
        こうすると、パイプ全体が失敗するようになる。
        この設定は、デフォルトだと off になっている。
      </p>
    </section>
  </section>
  <section id="problem">
    <h>発生した問題</h>
    <p>
      次のような GitLab CI/CD ジョブが失敗してしまった。
    </p>
    <codeblock language="yaml" filename=".gitlab-ci.yml">
      <![CDATA[
      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
      ]]>
    </codeblock>
    <p>
      <code>grep</code> コマンドは、パターンにマッチする行が一行もなかったとき、exit code 1 を返す。よって、<code>pipefail</code> が on になっていると、このジョブは失敗する。
      現在の <code>pipefail</code> がどうなっているか確かめるため <code>set +o</code> で全オプションを出力させたところ、<code>pipefail</code> が on になっていた。
    </p>
    <p>
      しかし、先述したように Bash における <code>pipefail</code> のデフォルト値は off のはずだ。
      実際に、ローカルで <code>alpine:latest</code> を動かしてみたところ、
    </p>
    <codeblock>
      <![CDATA[
      $ 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
      ]]>
    </codeblock>
    <p>
      確かに <code>pipefail</code> は無効になっている。
    </p>
    <p>
      なぜスクリプト内で <code>set -o pipefail</code> しているわけでもないのに <code>pipefail</code> が on になっているのか。
    </p>
  </section>
  <section id="where-pipefail-is-enabled">
    <h>どこで <code>pipefail</code> が on になるか</h>
    <p>
      <code>.gitlab-ci.yml</code> で明示的には書いていないので、GitLab Runner (GitLab CI/CD のスクリプトを実行するプログラム) が勝手に追加しているに違いない。
      そう仮説を立てて <a href="https://gitlab.com/gitlab-org/gitlab-runner">GitLab Runner のリポジトリ</a> を調査したところ、<a href="https://gitlab.com/gitlab-org/gitlab-runner/-/blob/c75da0796a0e3048991dccfdf2784e3d931beda4/shells/bash.go#L276">ソースコード中の以下の箇所</a> で <code>set -o pipefail</code> していることが判明した (コメントは筆者による)。
    </p>
    <codeblock language="go">
      <![CDATA[
      // pipefail オプションが存在しない環境にも対応するため、
      // 先に set -o でオプション一覧を表示させたあと、set -o pipefail している
      buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
      ]]>
    </codeblock>
  </section>
  <section id="how-to-solve">
    <h>どのように解決するか</h>
    <p>
      通常の Bash スクリプトを書く場合と同様に、<code>pipefail</code> が on になっていては困る場所だけ off にしてやればよい。
    </p>
    <codeblock language="yaml" diff="true" filename=".gitlab-ci.yml">
      <![CDATA[
       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
      ]]>
    </codeblock>
  </section>
  <section id="remarks">
    <h>備考</h>
    <p>
      なお、上述した実装ファイルは <code>shells/bash.go</code> だが、<code>alpine:latest</code> の例でもそうであったように、シェルが <code>sh</code> である場合にも適用される。
    </p>
  </section>
</article>