テスト失敗してるのにActionsがSuccessって言ってくる話

Github Actions上で、docker-composeを使ってRobotframeworkでのテストを行うコンテナを起動させる、というWorkflowを作成していたところ、 テストが失敗しているのにGithub ActionsがSuccessで終了する、という現象に出会ったので、それを調べた時のメモです

docker-compose-on-github-actions-get-exit-code-from-container

先に結論

docker-compose upをそのまま使用していたのが今回の敗因。 docker-compose runか、 docker-compose up --exit-code-from [service]を使用していれば起きなかった

原因

どうやら、 docker-compose upをそのまま実行すると、composeは個別のサービスの exit codeは返さず、サービスの起動自体は正常終了させたで、っていう意味での exit code 0を返す、という妥当な内容だった。なるほど。Actionsは、このcomposeの exit code 0を受け取って、「正常終了」と判別していたのが原因らしい。

検証してみる: ソースコード

ただ exit 1するだけのコンテナを作成して動作確認します

1
2
3
4
5
6
7
8
9
$ cat Dockerfile

FROM alpine:latest

RUN echo '#!/bin/sh' >> /failure.sh \
&& echo 'exit 1' >> /failure.sh \
&& chmod +x /failure.sh

CMD ["sh","/failure.sh"]

composeもビルドにまつわる記述以外は無し

1
2
3
4
5
6
7
8
9
10
11
$ cat docker-compose.yml

version: "3"

services:

failure:
image: failure:latest
build:
context: .
dockerfile: ./Dockerfile

以上のソースコードは、こちらにも置いてます

1
2
// ビルドもごく普通
$ docker-compose build

検証してみる: docker run

まずは docker runで実験。コンテナ内部の終了コードをそのまま認識している

1
2
3
4
5
$ docker run --rm failure

// 異常終了
$ echo $?
1

検証してみる: docker-compose run

次に、 docker-compose runで実験。こちらも、コンテナ内部の終了コードをそのまま認識している

1
2
3
4
5
$ docker-compose run --rm failure

// 異常終了
$ echo $?
1

検証してみる: docker-compose up

次に、 docker-compose upで実験。こちらは、コンテナ内部の終了コードではなく、 docker-compose自体の終了コードを認識しています。また、 failure_failure_1 exited with code 1の表示から、 failureサービス自体は、正しく exit code 1を返した事が確認出来ます

1
2
3
4
5
6
$ docker-compose up failure
failure_failure_1 exited with code 1

// 正常終了
$ echo $?
0

考えた: docker-compose up

この検証ではコンテナが1つしかないため違和感があるが、これが複数のサービスになった場合(もとい、composeはそういうアレなので)、1つ以上のサービスが exit code 1を返したからといって、compose自体の終了コードは 1と扱わないようになっている模様。

とはいえ、今回のようにActions上で、アプリケーションとテストのコンテナを2つ起動し、テストが失敗した場合(=テストのコンテナが exit code 1を返した場合)に、Slackに通知するなり、CI/CDを止めるなり、何らかの処理を行わないといけないので、それを検知出来ないとなるととても困る。

解決策: docker-compose up –exit-code-from を使う

こういう時のために、特定のサービスの exit codeを取得し、それを返すオプション --exit-code-fromが、docker-composeに用意されてた。

1
2
3
4
5
6
// 抜粋
$ docker-compose --help
--abort-on-container-exit Stops all containers if any container was
stopped. Incompatible with -d.
--exit-code-from SERVICE Return the exit code of the selected service
container. Implies --abort-on-container-exit.
  • --exit-code-from SERVICEを指定すると、SERVICE に指定したサービスの終了コードを正しく実行側にかえしてくれるようになる、との事。そうそうこれ欲しかってん
  • そして、 --exit-code-fromオプションを使用すると、 暗黙で --abort-on-container-exitオプションが有効になります、との事。
  • --abort-on-container-exitは、 いずれかのコンテナが停止した場合、すべてのコンテナを停止するオプション、との事。

検証してみる: docker-compose up –exit-code-from

さっそく試してみる。例えばさっきのdocker-compose.ymlに、nginxを追加してみて、

1
2
3
4
5
6
7
8
 services:

+ nginx:
+ image: nginx:alpine

failure:
image: failure:latest
build:

試しに、nginxを3つ起動する( --exit-code-from付きで)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ docker-compose up --exit-code-from failure --scale nginx=3

WARNING: using --exit-code-from implies --abort-on-container-exit
Creating network "failure_default" with the default driver
Creating failure_nginx_1 ... done
Creating failure_nginx_2 ... done
Creating failure_nginx_3 ... done
Creating failure_failure_1 ... done
Attaching to failure_failure_1, failure_nginx_3, failure_nginx_2, failure_nginx_1
nginx_1 | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
nginx_1 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
nginx_1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
nginx_1 | 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
nginx_1 | 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
nginx_2 | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
nginx_2 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
failure_failure_1 exited with code 1
Aborting on container exit...
Stopping failure_nginx_2 ... done
Stopping failure_nginx_1 ... done
Stopping failure_nginx_3 ... done

期待通りの結果になり、nginxもすべて停止。

そして、 --exit-code-fromを使用した場合、肝心の exit codeはどうなっているのかというと

1
2
3
// 異常終了
$ echo $?
1

期待通り、異常終了を得ることが出来ました(おわり)

これって、例えば、複数のコンテナの exit codeを取得して、「両方 1なら」みたいな条件ってどうやるんだろう