summaryrefslogtreecommitdiffhomepage
path: root/vhosts/blog/content/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd.dj
blob: 9872d284f418ad428aba1b0e0cd0f429b90c3737 (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
---
[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 = "ブログ記事として一般公開"
---
::: note
この記事は、2022-11-17 に [デジタルサーカス株式会社](https://www.dgcircus.com/) の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。
:::

ハマったのでメモ。

{#background}
# 前提

{#gitlab-ci-cd}
## GitLab CI/CD について

GitLab CI/CD では、Docker executor を用いて任意の Docker image 上でスクリプトを走らせることができる。

例:

```yaml
hello-world:
  stage: test
  image: alpine:latest
  script:
    - 'echo "Hello, World!"'
  rules:
    - if: '$CI_MERGE_REQUEST_IID'
  when: always
```

ここで、`script` に指定したコマンドが失敗する (exit status が 0 以外になる) と、即座に実行が停止され、ジョブは失敗する。

では、次のようなケースだとどうなるか。

```yaml
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-option}
## `pipefail` オプションについて

前述したようなケースにおいて、途中で失敗したときに全体を失敗させるには、`pipefail` オプションを有効にする。

```bash
# On にする
set -o pipefail
# Off にする
set +o pipefail
```

こうすると、パイプ全体が失敗するようになる。
この設定は、デフォルトだと off になっている。

{#problem}
# 発生した問題

次のような GitLab CI/CD ジョブが失敗してしまった。

```yaml
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 になっているのか。

{#where-pipefail-is-enabled}
# どこで `pipefail` が on になるか

`.gitlab-ci.yml` で明示的には書いていないので、GitLab Runner (GitLab CI/CD のスクリプトを実行するプログラム) が勝手に追加しているに違いない。
そう仮説を立てて [GitLab Runner のリポジトリ](https://gitlab.com/gitlab-org/gitlab-runner) を調査したところ、 [ソースコード中の以下の箇所](https://gitlab.com/gitlab-org/gitlab-runner/-/blob/c75da0796a0e3048991dccfdf2784e3d931beda4/shells/bash.go#L276) で `set -o pipefail` していることが判明した (コメントは筆者による)。

```go
// pipefail オプションが存在しない環境にも対応するため、
// 先に set -o でオプション一覧を表示させたあと、set -o pipefail している
buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
```

{#how-to-solve}
# どのように解決するか

通常の Bash スクリプトを書く場合と同様に、`pipefail` が on になっていては困る場所だけ off にしてやればよい。

```yaml
 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
```

{#remarks}
# 備考

なお、上述した実装ファイルは `shells/bash.go` だが、`alpine:latest` の例でもそうであったように、シェルが `sh` である場合にも適用される。