GitLab CIのtrigger機能を試す


GitLabでパイプラインの分割を調べると、`include`と一緒に`trigger`もよく出てくる。
どう使えるんだろう、と思って調べた時のメモ。
`trigger`は複数のプロジェクトのパイプラインを実行するMulti-project piplinesと親子関係を持たせたパイプラインを実行するParent-child pipelinesというものそれぞれで利用できるが、ここでは親子パイプラインに絞ってメモを残している。
全体的にかなり荒い点はご了承を。

## `trigger`概要
`trigger`を使うとパイプラインに親子関係を持たせることが出来る。
オフィシャルドキュメントは[このあたり](https://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html)。

親子関係と言ってもしっくり来ないが、ざっくり言うとパイプラインを独立・並行して複数本走らせることが出来る、と考えておけば良さそう。
自分の頭の中のイメージは以下のような感じ（汚いが。。。）。

![1675156338468.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/00173721-4842-7aac-ce39-a96de3391bbc.png)

A,B,C,Dはそれぞれパイプラインのstage相当と考えるとよい。
普通のパイプラインは`.gitlab-ci.yml`に書ききってしまうが、`include`を使うケースだと外に出すことが出来る（参考：[昔の記事](https://qiita.com/ipppppei/items/acbde06062bbc8ba0bdf)）。
`trigger`は更に中で独立したパイプラインを作って.gitlab-ci.ymlのパイプラインとは独立して動かす感じになる。

オフィシャルドキュメントの親子パイプラインの説明では以下のようなものもあった。（DeepLでの翻訳）

>マルチプロジェクトパイプラインと同様に、パイプラインは、同じプロジェクト内で、同時に実行される一連の子パイプラインをトリガーすることができます。
> - 子パイプラインは、ステージシーケンスに従って各ジョブを実行しますが、親パイプラインの無関係なジョブの終了を待つことなく、自由にステージを進めることができます。
> - 構成は、より小さな子パイプラインの構成に分割され、理解しやすくなっています。これにより、構成全体を理解するための認知的負荷が軽減されます。
> - インポートは子パイプラインレベルで行われるため、衝突の可能性が低くなります。
> - 各パイプラインは関連するステップのみを持ち、何が起こっているのか理解しやすくなります。
> 子パイプラインは、GitLabの他のCI/CD機能とうまく連動する。
> - `only: changes` は、特定のファイルが変更されたときだけパイプラインを起動します。これは、モノレポ構成などで有効です。
`.gitlab-ci.yml`にある親パイプラインと子パイプラインは通常のパイプラインとして動作するので、トリガーに関する独自の動作や順序を持たせることができる。
> これらはすべて`include:`機能で動作するので、子パイプラインの設定を構成することができます。

上記には単一リポジトリに複数の種類のコードを入れる（例えばFrontendとBackendを同じリポジトリに突っ込む）、いわゆるモノレポ構成だと使えるとの記述がある。\
これはBackendの修正でFrontend向けのCI/CDを動かしたくない、といったようなニーズがある場合、triggerでBackendとFrontendのパイプラインをそれぞれincludeし、`only: changes`でパスを指定してFrontend向け修正契機で動いたのかBackend向け修正契機で動いたのかを検知して特定のパイプラインだけ動かす、といったことが出来ることを指している。
この辺りの詳細は[モノレポでのCI/CDパイプラインの定義方法を考える(GitLab)](https://zenn.dev/panyoriokome/articles/12a2f2404ace23)というブログが非常に分かりやすいのでそちらを参考にするとよいと思う。（今回触るにあたり、非常に参考にさせていただいた）

## `trigger`の仕様
ここでの説明は[オフィシャルの説明](https://docs.gitlab.com/ee/ci/yaml/index.html#trigger)をざっくり意訳したものとなる。
triggerのジョブ内で使えるkeyword(同じ高さで書けるkeyword)には制限があり、以下の利用可能となっている。
- `allow_failure`
- `extends`
- `needs` (ただし`needs:project`は不可）
- `only`と`except`
- `rules`
- `stage`
- `trigger`
- `variables`
- `when` (`on_success`, `on_failure`, `always`のいずれかのみ).

またtriggerで実行できるパイプラインは`trigger:project`でGitLab内プロジェクトを指定するか、`trigger:include`で`include`を使って引っ張ってくる。

`trigger`で使える他のkeywordとしては`trigger:strategy`と`trigger:forward`がある。

#### `trigger:strategy`
通常、親のパイプラインは子の結果を待たずに終了するが、`trigger`の中で`strategy: depend`を指定すると子のパイプラインの結果を待って終了する。
後ろの方で実際に使った結果を載せているので、詳細はそちらを参照してほしい。

#### `trigger:forward`
環境変数の引き継ぎを指定する。
デフォルトでは以下のようになっている。
- GitLab内の環境変数：子に引き継がれない
- `variables`の環境変数：子に引き継がれる

fowardの中で`pipeline_variables: true`を指定すると、GitLab内で設定した環境変数を引き継ぐことが出来るようになる。また、`yaml_variables: false`を指定すると`variables`で定義した環境変数が引き継ぎを拒否できる。
参考までに、[オフィシャルの説明](https://docs.gitlab.com/ee/ci/yaml/index.html#triggerforward)のサンプルを翻訳したものを以下に示す。

```yaml:example
## MYVARはGitLabのCI/CDで設定する環境変数
variables: # 各ジョブのデフォルトの環境変数 
  VAR: value

# デフォルトの振る舞い
# - VARは子に渡る
# - MYVARは子に渡らない
child1:
  trigger:
    include: .child-pipeline.yml

# CI/CDの環境変数を転送する
# - VARは子に渡る
# - MYVARは子に渡る
child2:
  trigger:
    include: .child-pipeline.yml
    forward:
      pipeline_variables: true

# YAMLのvariablesを転送しない
# - VARは子に渡らない
# - MYVARは子に渡らない
child3:
  trigger:
    include: .child-pipeline.yml
    forward:
      yaml_variables: false
```


## `trigger`のお試し
検証用なので、Runner用のYAMLのみの以下の構成で検証する。
```
.
├── README.md
├── .gitlab-ci.yml
└── gitlab-ci
    └── trigger.yml
```
ファイルは以下の様に作成した。
```yaml:.gitlab-ci.yml
stages:
  - build

build-job:
  stage: build
  trigger:
    include: gitlab-ci/trigger.yml
```
include対象はこちら。`stage`には親には定義されていないものを使っている。
```yaml:gitlab-ci/trigger.yml
stages:
  - huga

hoge:
  stage: huga
  script:
    - date
```

実行結果はこちら。UpstreamにProject名が表示され、`Parent`というのが表示されていることが分かる。

![1675128101592.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/49939d6f-16a7-dee2-4515-63d6d7359814.png)

なお、.gitlab-ci.ymlを以下のように`trigger`を外すと、Lintでエラーが出る。
```yaml:.gitlab-ci.yml
stages:
  - build

include:
 - gitlab-ci/trigger.yml

build-job:
  stage: build
  script:
  - echo "dummy"
```
`stage: huga`の定義が`.gitlab-ci.yml`にないのでデフォルトのstageを使うよう促される。

![1675130295725.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/9db223eb-0a8d-0f19-ae30-89c989efff93.png)


余談だが、`trigger`を利用するstageでは`script`は利用できない。こちらも文法的にNGとなる。

## Triggerの並列実行
triggerで子のパイプラインを生成する際、親のstageは子の完了を待たないため、並列実行のような形になる。それを確認する。\
`include`で呼び出すパイプラインを以下のように用意する。

```gitlab-ci/trigger.yml
stages:
  - huga
hoge:
  stage: huga
  script:
    - date
```
```gitlab-ci/sleep30s.yml
stages:
  - huga
hoge:
  stage: huga
  script:
    - sleep 30
    - date
    - exit 1
```
sleepさせているのは、最初のstage側でsleep30s.ymlをincludeして、後続のstageが追い抜くことを確認するために入れている。
また、それらを呼び出すように`.gitlab-ci.yml`を以下のように修正する。
```yaml:.gitlab-ci.yml
stages:
  - build

build-job-1:
  stage: build
  trigger:
    include:
     - gitlab-ci/sleep30s.yml

build-job-2:
  stage: build
  trigger:
    include:
     - gitlab-ci/trigger.yml
```
待たないのであれば、build-job-1の子のパイプラインが30秒sleepしている間にbuild-job-2の実行が終わる。また、build-job-1自体も子のパイプラインを起動した時点で完了する。
これを確認するために、修正をpushしてパイプラインを実行した結果が以下となる。Passedにはなるが、子はまだ完了していない。

![1675139092032.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/59e7f717-7bb3-f44f-63e5-1783b30321f2.png)
このように実行中である。

![1675139431646.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/0dc9f5cb-fdc7-23de-9913-e376a2a48387.png)

また、build-job-1, build-job-2で作成されたパイプラインのジョブの`date`の結果を見比べると以下のようになった。
- build-job-1: `Tue Jan 31 04:46:31 UTC 2023`
- build-job-2: `Tue Jan 31 04:46:01 UTC 2023`

build-job-2が追い抜いていることが分かる。
このように、
- 子の完了を待たずに親は終了する
- 子が失敗しても、親側では分からない
- 子の完了を待たずに次のstageが実行される
  
といったことがこの結果から言える。
また、`include`するファイルにそれぞれ`huga`というstageを用意しているが、通常同一stage名を含む複数ファイルをincludeするとどれかで上書きされてしまう。しかし、triggerでそれぞれincludeした場合は上書きされることがない。
stage名を意識しなくてもincludeが使える点はtriggerのメリットとも言えそう。

なお、子の完了を待って親に状態を反映するには前述の`trigger:strategy`を使えば良い。.gitlab-ci.ymlを以下のように修正する。
```.gitlab-ci.yml
stages:
  - build

build-job-1:
  stage: build
  trigger:
    include:
     - gitlab-ci/sleep30s.yml
    strategy: depend

build-job-2:
  stage: build
  trigger:
    include:
     - gitlab-ci/trigger.yml
```
これをPushすると、以下のような結果が得られる。
![1675159676684.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/81fd1388-75e5-1114-9d25-b802be6f643a.png)

パイプラインの完了はsleep 30の処理が終わってからとなり、sleep30s.ymlの最後のexit 1で子のジョブが失敗するため、それを引き継いでエラーとなる。