BitbucketPipelinesでCI/CDに挑戦する　その4：ブランチやタグによって動作を切り分ける

## 概要
既にある程度の開発が進んでいるBitbucket上のプロジェクトにPipelinesを使って継続的あれこれする機能を追加しようという、解説というよりも私の挑戦の記録です。

[前回（その3）](https://qiita.com/talesleaves/items/7427fe30f63ceb6e855b)はS3にアップロードしたテストレポートをCloudFrontを通してブラウザで閲覧できるようにしました。
今回はブランチやタグごとに別の動作を設定して、masterにタグがpushされたらリリース、を実現したいと思います。

## 運用形式の決定
BitbucketPipelinesのトリガーは基本、push時または手動です。スケジュールで設定もできますが、ここでは割愛します。
どのブランチにpushされたときにどんな作業が必要になるかを決めることで、様々な作業を自動化できます。

大人数の開発だとfeatureブランチの開発にもプルリクエストを使用したり、デプロイ環境ごとにブランチを切ったりするかと思いますが、ここでは、下記のような運用を想定して設定していきます。

- featuer/*ブランチで開発作業を行う
- featuer/*にpushしてテストを通過したらmasterにプルリクエストを出す
- 担当者がプルリクエストの内容を確認してマージ
- masterブランチに*-SNAPSHOTのタグがpushされたらstagingデプロイ
- masterブランチに*-RELEASEのタグがpushされたらproductデプロイ

masterにマージされるときには必ずテストは完了（通過）しているものと考えれば、テストとその結果の処理は前回までで対応済みです。
JarやJavadocのデプロイについても、アップロード作業そのものはJUnitのレポートの公開の応用です。
これを踏まえて、bitbucket-pipelines.ymlを編集していきます。

## デプロイの設定の共通化
デプロイの手順はaws-s3-deployパイプラインを使用してアーティファクトをS3にアップロードし、CloudFrontでのURLをビルドステータスAPIに追加するというものでした。
これの流れそのものは、前述のstagingデプロイやproductデプロイでも使用できます。

環境に合わせてプロパティに設定する値を変更する必要がありますが、これを実現するためにリポジトリ設定内のDeploymentsで設定できる[デプロイメント変数](https://ja.confluence.atlassian.com/bitbucket/set-up-bitbucket-deployments-968683907.html#SetupBitbucketDeployments-Step1:Defineyourenvironments)と、[YAMLアンカー](https://support.atlassian.com/ja/bitbucket-cloud/docs/yaml-anchors/)を使用します。

### YAMLアンカー
YAMLアンカーはYAMLファイル内での記述の重複を避けるための機能です。ある場所で宣言したブロックを別の個所から参照できます。まず前回の最終的なbitbucket-pipelines.ymlを確認しましょう。

```yaml:bitbucket-pipelines.yml（前回まで）
image: eclipse-temurin:17

definitions:
  caches:
    gradlewrapper: ~/.gradle/wrapper

pipelines:
  custom:
    manualTest:
      - step:
          name: Test #ステップが増えるので名前を付けて区別
          caches:
            - gradle
            - gradlewrapper
          script:
            - ./gradlew test
          artifacts:
            - build/reports/tests/test/** #JUnitの出力先ディレクトリをアーティファクトに指定
      - step:
          name: Upload Reports #作業内容がわかる名前を
          deployment: test
          oidc: true
          script:
            - pipe: atlassian/aws-s3-deploy:1.1.0 #パイプの実行
              variables: #パイプに渡す変数
                AWS_OIDC_ROLE_ARN: $AWS_OIDC_ROLE_ARN #必須ではないため宣言が必要
                S3_BUCKET: $BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER #例えばbucket/build_10などに展開される
                LOCAL_PATH: build/reports/tests/test #アーティファクトの存在するディレクトリ
            - export FRONT_URL="https://${FROMT_DOMAIN}/build_${BITBUCKET_BUILD_NUMBER}/"
            - export BUILD_STATUS="{\"key\":\"TEST_REPORT\", \"state\":\"SUCCESSFUL\",  \"name\":\"Test report\", \"url\":\"${FRONT_URL}\"}"
            - curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
```

これに*-SNAPSHOTタグのpushで起動するstagingデプロイをべた書きで追加します。

```yaml:bitbucket-pipelines.yml
image: eclipse-temurin:17

definitions:
  caches:
    gradlewrapper: ~/.gradle/wrapper

pipelines:
  custom:
    manualTest:
      - step:
          name: Test #ステップが増えるので名前を付けて区別
          caches:
            - gradle
            - gradlewrapper
          script:
            - ./gradlew test
          artifacts:
            - build/reports/tests/test/** #JUnitの出力先ディレクトリをアーティファクトに指定
      - step:
          name: Upload Reports #作業内容がわかる名前を
          deployment: test
          oidc: true
          script:
            - pipe: atlassian/aws-s3-deploy:1.1.0 #パイプの実行
              variables: #パイプに渡す変数
                AWS_OIDC_ROLE_ARN: $AWS_OIDC_ROLE_ARN #必須ではないため宣言が必要
                S3_BUCKET: $BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER #例えばbucket/build_10などに展開される
                LOCAL_PATH: build/reports/tests/test #アーティファクトの存在するディレクトリ
            - export FRONT_URL="https://${FROMT_DOMAIN}/build_${BITBUCKET_BUILD_NUMBER}/"
            - export BUILD_STATUS="{\"key\":\"TEST_REPORT\", \"state\":\"SUCCESSFUL\",  \"name\":\"Test report\", \"url\":\"${FRONT_URL}\"}"
            - curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"

  #タグのpushで起動するパイプラインを追加
  tags:
    '*-SNAPSHOT':
      - step:
          name: Sataging Deploy 
          deployment: staging
          oidc: true
          script:
            - ./gradlew shadowJar javadoc #テストではなくFatJarの生成とJavaDocの生成
            - pipe: atlassian/aws-s3-deploy:1.1.0
              variables:
                AWS_OIDC_ROLE_ARN: $AWS_OIDC_ROLE_ARN
                S3_BUCKET: $BUCKET_NAME/build_$BITBUCKET_BUILD_NUMBER
                LOCAL_PATH: build #デフォルトならbuild直下にlibsとdocsが生成されてその下にFatJarやJavaDocが生成される
                EXTRA_ARGS: --exclude=* --include=docs/* --include=libs/* #アップするディレクトリをフィルタリング
            - export FRONT_URL="https://${FROMT_DOMAIN}/build_${BITBUCKET_BUILD_NUMBER}/"

            #JavaDoCのステータス生成
            - export DOC_STATUS="{\"key\":\"DOC\", \"state\":\"SUCCESSFUL\",  \"name\":\"JavaDoc\", \"url\":\"${FRONT_URL}/dos/javadoc\"}"
            - curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"

            #FatJarのステータス生成
            - export JAR_NAME=$(ls ${BITBUCKET_CLONE_DIR}/build/libs/*-all.jar | xargs basename)
            - export JAR_STATUS="{\"key\":\"JAR\", \"state\":\"SUCCESSFUL\",  \"name\":\"Executable Jar\", \"url\":\"${FRONT_URL}/libs/${JAR_NAME}\"}"
            - curl -H "Content-Type:application/json" -X POST --user "${DEPLOY_AUTH}" -d "${BUILD_STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
```

ご覧のように、一部の値が異なる以外は同じ記述の繰り返しとなってしまいます。
これを解消するためにYAMLアンカーによる参照を導入して、重複を解消します。
アンカーを使用するには、参照されるブロックに`&`で名前を付けて、参照する側には`*`と名前を記述します。また、参照される側は参照する側よりも前に記述されている必要があります。このことから、bitbucket-pipelines.ymlでは`definitions`の中に参照される側を記述するのが良い方法だと思います。

### アンカーを使って書き換える
YAMLアンカーを使って上記を書き換えたものが下記になります。要点にはコメントで番号を振って、後ほど解説します。

```yaml:bitbucket-pipelines.yml(YAMLアンカー使用)
image: eclipse-temurin:17

definitions:
  caches:
    gradlewrapper: ~/.gradle/wrapper
  pipes:
    s3: &S3Deploy #1:S3へのアップロードを参照先として切り出し
      pipe: atlassian/aws-s3-deploy:1.1.0
      variables: &S3Variables #2:パイプの変数を参照可能にする
        AWS_OIDC_ROLE_ARN: ${AWS_DEPLOY_ROLE_ARN}
        S3_BUCKET: ${BUCKET_NAME}/${S3_PATH} #3:デプロイメント変数で環境ごとに書き換え可能にする
        LOCAL_PATH: ${LOCAL_PATH} #3
  scripts: #4:シェルスクリプトをすべて切り出す
      - &S3Path export S3_PATH="${BITBUCKET_DEPLOYMENT_ENVIRONMENT}/build_${BITBUCKET_BUILD_NUMBER}" #3
      - &FrontURL export FRONT_URL="https://${FROMT_DOMAIN}/${S3_PATH}"
      - &JunitStatus export STATUS="{\"key\":\"JUNIT\", \"state\":\"SUCCESSFUL\",  \"name\":\"JUnit report\", \"description\":\"junit test report\", \"url\":\"${FRONT_URL}\"}"
      - &PostStatus curl -H "Content-Type:application/json" -X POST --user "${WRITE_AUTH}" -d "${STATUS}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/commit/${BITBUCKET_COMMIT}/statuses/build"
      - &JacocoStatus export STATUS="{\"key\":\"JACOCO\", \"state\":\"SUCCESSFUL\", \"name\":\"JaCoCo report\", \"url\":\"${FRONT_URL}/coverage/\"}"
      - &DocStatus export STATUS="{\"key\":\"DOC\", \"state\":\"SUCCESSFUL\", \"name\":\"Javadoc\", \"url\":\"${FRONT_URL}/docs/javadoc/\"}"
      - &JarName export JAR_NAME=$(ls ${BITBUCKET_CLONE_DIR}/build/libs/*-all.jar | xargs basename) #12:生成したJarの検出
      - &JarStatus export STATUS="{\"key\":\"JAR\", \"state\":\"SUCCESSFUL\", \"name\":\"Executable Jar\", \"url\":\"${FRONT_URL}/libs/${JAR_NAME}\"}"
  steps:
    - step: &Test #5:テストデプロイを切り出す
        name: Test
        deployment: test
        oidc: true
        caches:
          - gradle
          - gradlewrapper
        artifacts:
          - build/reports/tests/test/**
        script:
          - ./gradlew test
          - *S3Path
          - *S3Deploy
          - *FrontURL
          - *JunitStatus
          - *PostStatus
          - *JacocoStatus
          - *PostStatus

pipelines:
  custom:
    manualTest:
      - step: *Test #6:参照を使用する
    manualBuild:
      - step: &Build_and_deploy
          <<: *Test #7:「マップのマージ」を利用した上書き
          name: Build
          deployment: staging
          artifacts:
            - build/docs/javadoc/**
            - build/libs/**
          script:
            - ./gradlew shadowJar javadoc #12
            - *S3Path
            - <<: *S3Deploy #8:マージの癖のある仕様
              variables:
                <<: *S3Variables #8
                EXTRA_ARGS: --exclude=* --include=docs/* --include=libs/* #9:アップロードするディレクトリをフィルタリング
            - *FrontURL
            - *DocStatus
            - *PostStatus
            - *JarName
            - *JarStatus
            - *PostStatus

  branches: #10:ブランチを指定してpushで起動するパイプライン
    feature/*:
      - step: *Test
  tags: #11:タグのpushで起動するパイプライン
    '*-SNAPSHOT':
      - step:
          <<: *Build_and_deploy #7
          name: Snapshot deploy
          deployment: staging
    '*-RELEASE':
      - step:
          <<: *Build_and_deploy #7
          name: Product Release
          deployment: production
```

#### \#1:S3へのアップロードを参照先として切り出し
`definitions`に`pipes`という独自のプロパティを追加して、aws-s3-deployパイプを呼び出せるようにしています。アンカーで名前を付けた「値」が参照個所に呼び出されます。アンカー以降のブロックが参照先に挿入されると考えると理解しやすいです。この中で、環境変数を利用して、環境ごとに別のURLを生成できるように工夫しています。

#### \#2:パイプの変数を参照可能にする
パイプに使用する変数は変更される可能性が高く、環境ごとの差異が発生しやすいのでこちらも参照できるようにしておきます。パイプの使用全体がすでに参照されていますが、\#7でこれが有効になる場面があります。

#### \#3:デプロイメント変数で環境ごとに書き換え可能にする
BitbucketからPipelinesの環境変数として設定される変数はリポジトリ変数のほかに、デプロイメント変数が存在します。デプロイメント変数は`step`で宣言されている`deployment`の値に応じて設定される変数で、リポジトリ変数を上書きできます。
この例ではアップロード先のパスを決める`S3_PATH`とアップロードするディレクトリを指定する`LOCAL_PATH`を環境別に指定した環境変数を使用するようにしています。
`S3_PATH`ではデフォルトで使用できる[変数](https://support.atlassian.com/ja/bitbucket-cloud/docs/variables-and-secrets/#Default-variables)を組み合わせて使用しています。
このデフォルト変数はビルドの実行時にならないと値が存在しないため、シェルスクリプトでコンテナ内の環境変数として`<CloudFrontのドメイン>/<デプロイ環境名>/build_<ビルド番号>`を生成しています。
`LOCAL_PATH`では[デプロイ変数](https://support.atlassian.com/ja/bitbucket-cloud/docs/variables-and-secrets/#Deployment-variables)を利用してアップロードするファイルのパスの値を変更します。
test環境ではテスト結果のみのアップロードでしたので`build/tests/test`を指定していましたが、その他の環境でのデプロイは(デフォルト設定であれば)`build`ディレクトリ直下で複数のディレクトリに生成物が分かれてしまうため、`build`を指定して、aws-s3-deployパイプのフィルタ機能でアップロードするディレクトリ、ファイルを制御します。

#### \#4:シェルスクリプトをすべて切り出す
そこまでするのであればシェルスクリプトファイルを書いた方がよいのでは、という意見もあるかと思いますが、ここではYAML内にとどめます。
シェルコマンドひとつひとつを参照できるようにして、実際の`script`内から参照します。特にビルドステータスAPIにポストするコマンドは複数回使用されますので参照できるようにする価値が大きいです。

#### \#5:テストデプロイを切り出す
前述の通り、この例でのテストのデプロイとその他の環境でのデプロイの差はパスなどの値が大部分でした。
そのため、テストのデプロイ手順を参照できるようにすることで、その他の環境でのデプロイで利用することが出来ます。\#7で活用します。

#### \#6:参照を使用する
手動テストの内容を切り出したテストデプロイを参照します。ここでは`&Test`以降の各プロパティが**そのまま`step`内に展開**されます。

#### \#7:「マップのマージ」を利用した上書き
YAMLアンカーの使用法で最も重要なのが「マップのマージ」です。値を呼び出すときに`<<:`を**値のレベルのインデントに合わせて配置**します。このインデントがずれると値が正しく呼び出されない[^WrongReference]ため、YAMLパーサー[^YamlParser]で展開後の値を確認するとエラーの解明に非常に役立ちます。

[^YamlParser]:https://yaml-online-parser.appspot.com/ など
[^WrongReference]:値を参照したいキーと同じレベルに記述してしまい、`{値を参照したいキー: null, 参照内容のキー1: 値1, 参照内容のキー2: 値2,…}`と展開されてしまうとバリデーターでは検出できません。

この例の場合、`step`の値に`&Test`を呼び出しています。これに対して同じレベルでプロパティ（キー）と値を記述すると、キーが参照に存在しない場合は挿入、存在する場合は上書きされます。この辺りはJavascriptでのオブジェクトの動作と同様です。ここでは`name`など、環境ごとに異なる値を上書きしています。

#### \#8:マージの癖のある仕様
まず、`step`のシークエンスの要素として、aws-s3-deployパイプを参照します。
ここでは`variables`に`EXTRA_ARGS`という値を追加してパイプでアップロードするディレクトリをフィルタリングする必要があります。この時、マージを利用していても下記のようには記述できません。
```yaml
  - step:
    - <<: *S3Deploy
      variables:
        EXTRA_ARGS: --exclude=* --include=docs/* --include=libs/*
```
上記だと`variables`を`EXTRA_ARGS`のみを持ったマップで差し替えてしまいます。これを避けるには、`variables`の値を別途、参照してマージする必要があります。そのため、[#2](#2パイプの変数を参照可能にする)で別途に参照を設定しています。
また、この機能で**マージできるのは「マップ」であることに注意してください**。シークエンスはマージできないため、下記のような記述はできません。
```yaml
seq-a: &a-ref
  - a
  - b
seq-b:
  <<: *a-ref #エラー！
seq-c:
  - *a-ref
  - c #エラーではないがseq-cの値として出来上がるのは[[a,b],c]となって[a,b,c]ではない
```
この仕様のため、`script`は要素のひとつひとつで値（つまりコマンドそのもの）を参照できるようにして、シークエンスの各要素から値を参照する必要があります。

#### \#9:アップロードするディレクトリ
 aws-s3-deployパイプは、AWSのcliをラップしたものです。このため、S3のコマンドで使用できるオプションを`EXTRA_ARGS`にセットすることでディレクトリやファイルを制御できます。ここではいったん`build`内のすべてを除外して、`libs`と`docs`の2つのディレクトリ内のすべて、を含める指定を行っています。
より詳しくは、[オプションの詳細](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-s3-commands.html#using-s3-commands-managing-objects-param)を参照してください。

#### \#10:ブランチを指定してpushで起動するパイプライン
`branches`プロパティでブランチ名を指定して、特定のブランチにpushされたときに起動するパイプラインを記述できます。ここでは`feature/*`として、feature/で始まる名前の機能開発ブランチを指定しています。
[運用形式の決定](#運用形式の決定)で決めた通り、このブランチではテストのみ行います。`&Test`で指定したテスト結果のデプロイを参照しています。

#### \#11:タグのpushで起動するパイプライン
考え方は`branches`と同様で、指定した名前のタグがpushされたら起動するパイプラインです。
ここでは`*-SNAPSHOT`と`*-RELEASE`を指定しています。それぞれ、stagingとproductデプロイとして、生成物をアップロードします。
このタグの指定をシングルクォートで括っているのは、先頭のアスタリスクをYAMLアンカーの参照と区別するためだと思われます。
デプロイ環境と名前だけ上書きして、実行しているのは`&Build_and_deploy`と同じ内容です。
ここを見ると、アンカーのありがたみを感じると思います。

#### \#12:生成したJarの検出
これはシェルコマンドの領域になるため最後に回しましたが、gradlewで生成したJarの名前を実行時に特定して変数にエクスポートしています。
これはGradleのプロジェクト、タスクの設定により、**生成されるファイル名が変わることと、生成されるJarがひとつに限定されるという条件を利用して簡易的に実装**しています。
lsコマンドで得られたJarファイルのパスからbasenameでファイル名のみを取り出して`$(...)`で変数に渡す、という処理を行っています。

lsコマンドでは、Pipelinsのコンテナ内では`.`を使用したカレントディレクトリの指定ができないため、`${BITBUCKET_CLONE_DIR}`を使用してコンテナ内でのパスを特定しています。
また、ファイル名を`*-all.jar`として検索しているのはGradleの「[Shadowプラグイン](https://imperceptiblethoughts.com/shadow/)」を使用して`shadowJar`コマンドで実行可能Jarを生成しているためです。仮にSpringBootで`bootJar`コマンドを使用するのであれば、単に`*.jar`で問題ありません。ここはプロジェクトの設定次第ですので、適合する指定に読み替えてください。
取得したファイル名は、ビルドステータスAPIの`url`の指定に使用しています。

## 次の記事？
ここまでご閲覧いただきありがとうございました。
ひとまずこのシリーズの開始時に目標にしていた自動でテストを行い結果をアップロードする、リリース時に実行可能Jarを公開するという目標は果たせました。

しかしながら、改善点はいくつか残っています。

- シェルコマンドの実行結果が常に成功と決め込んでいる
- ビルドステータスAPIはリポジトリのメンバーにしか公開されないため、実行可能Jarが公開できたといってよいのか微妙→WikiやConfluenceを利用する？
- パイプラインの実行時間の短縮ができるのではないか

この辺りは気になっているところですが、今後時間をとって調査できればと考えています。
ひとまずは目標達成、次の記事は未定として、一旦終了とします。
ありがとうございました。