GitHub のリリースタグ作成時に Circle CI でリリース用の Pull Request を自動で作成する
目次
はじめに
こんにちは、 @okarin です。みなさんはどんなフローでプロダクトをリリースしていますか? リリースタグを作成したときに Docker イメージを自動でビルドしているものの、 K8s の Deployment のマニフェストをローカルで更新して GitHub に push する、という方も多いのではないでしょうか?
今回は、リリースタグを作成したときに、 CircleCI でリリース用の Pull Request(以下、PR と表記)を自動で作成するようにしたので、記事にまとめました。
CircleCI 設定
今回作成した .circleci/config.yml ファイルは以下のようになります。詳細は GitHub をみていただければと思います。
この workflow で行っていることは以下の 4 つになります。
- ssh key の設定
- 必要なコマンドのインストール
- git の設定
- PR の作成(別スクリプトファイル)
version: 2.1
commands:
install_commands:
steps:
- run:
name: install commands
command: |
apk add curl jq yq git bash openssh make
set_up_git:
steps:
- checkout
- run:
name: setup git user
command: |
git config user.email $GITHUB_EMAIL
git config user.name $GITHUB_USER
release_pr:
steps:
- checkout
- run:
name: release_pr
command: |
service_name=$(echo ${CIRCLE_TAG} | cut -d/ -f2)
version=$(echo $CIRCLE_TAG | cut -d/ -f3)
make microservices/app/release_pr SERVICE_NAME=${service_name} VERSION=${version} GITHUB_TOKEN=${GITHUB_TOKEN}
jobs:
release_pr_job:
docker:
- image: alpine:3.19.0
shell: /bin/sh -leo pipefail
environment:
BASH_ENV: /etc/profile
steps:
- add_ssh_keys:
fingerprints:
- "SHA256:7p8XHnZsyzH7w0g/mBhp4dEeHNO6gPyTV5io5esfZPs"
- install_commands
- set_up_git
- release_pr
workflows:
version: 2
build:
jobs:
- release_pr_job:
filters:
tags:
only: /^microservices.*/
branches:
ignore: /.*/
ssh key の設定
まずは CircleCI から PR を作成するために ssh key の設定を行います。CircleCI で GitHub のリポジトリをセットアップしたときに自動で生成される ssh key では読み取り権限しかないので、新しく鍵を生成して CircleCI と GitHub に登録する必要があります。
まずは CircleCI のドキュメントを参考にしつつ、 ssh key を生成し、秘密鍵を登録します。今回は GitHub を利用するので、 Hostname は github.com にしておきます。
鍵を生成したら、GitHub のリポジトリを開いて、 Settings > Deploy keys > Add deploy key で生成した鍵の公開鍵を登録します。
このとき「Allow write access」にチェックを入れて、書き込み権限を付与する必要があります。
この手順もドキュメントがあるので参考にしてください。
これらの登録作業が完了したら、CircleCI、GitHub のどちらも以下のように fingerprint がみえるようになっています。
これをコピーして .circleci/config.yml の add_ssh_keys.fingerprints に設定します。
jobs:
release_pr_job:
docker:
- image: xxxx
steps:
- add_ssh_keys:
fingerprints:
- "SHA256:7p8XHnZsyzH7w0g/mBhp4dEeHNO6gPyTV5io5esfZPs"
必要なコマンドのインストール
今回は alpine のイメージを利用したので、いろいろと必要なコマンドをインストールしておきます。 jq と yq は後ほど利用します。
install_commands:
steps:
- run:
name: install commands
command: |
apk add curl jq yq git bash openssh make
git の設定
git command で PR を push するため、 config を設定しておきます。これらの環境変数は CircleCI 側で設定しておきます。
set_up_git:
steps:
- checkout
- run:
name: setup git user
command: |
git config user.email $GITHUB_EMAIL
git config user.name $GITHUB_USER
PR の作成
このステップで PR を作成します。とはいえ少し長いので別スクリプトで実行するようにしています。また、私たちはモノレポでマイクロサービスを管理しており、リリースタグは microservices/<service name>/<version> といったフォーマットを使用しています。そのため、今回もそのフォーマットを想定して、 service name や version を抽出してからスクリプトを実行しています。
release_pr:
steps:
- checkout
- run:
name: release_pr
command: |
service_name=$(echo ${CIRCLE_TAG} | cut -d/ -f2)
version=$(echo $CIRCLE_TAG | cut -d/ -f3)
make microservices/app/release_pr SERVICE_NAME=${service_name} VERSION=${version} GITHUB_TOKEN=${GITHUB_TOKEN}
PR 作成のスクリプト
まず Makefile は以下のようになっています。受け取った引数をそのままスクリプトファイルに渡しているだけになります。
.PHONY: microservices/app/release_pr
microservices/app/release_pr:
scripts/app/release_pr $(SERVICE_NAME) $(VERSION) $(GITHUB_TOKEN)
scripts/app/release_pr ファイルは以下のようになります。これも長いので少しずつ解説していきます。
#!/bin/bash
main() {
set -eo pipefail
if [[ $# -ne 3 ]]; then
exit 1
fi
local service_name=${1}
local version=${2}
local github_token=${3}
local branch_name="release/$(echo ${service_name})_$(echo ${version})"
local owner="ksrnnb"
local repo="ci_create_pr"
echo "[INFO] Running 'scripts/app/release_pr' for ${service_name}"
git switch -c $branch_name
deployment_files=$(find ./microservices/${service_name} -type d -name "Deployment" -exec find {} -type f -name "*.yaml" \;)
for file in $deployment_files; do
echo "[INFO] Updating file: $file"
# tags.datadoghq.com/version を更新
yq e ".metadata.labels[\"tags.datadoghq.com/version\"] = \"${version}\"" -i "$file"
yq e ".spec.template.metadata.labels[\"tags.datadoghq.com/version\"] = \"${version}\"" -i "$file"
# service のイメージのタグのみを更新
yq e ".spec.template.spec.containers[] |= (select(.image | contains(\"${service_name}\")) .image |= sub(\":.*$\"; \":${version}\"))" -i "$file"
done
git add .
git commit -m "release: ${service_name} ${version}"
git push origin $branch_name
echo "[INFO] Git push has completed"
# create PR
response=$(curl -s -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${github_token}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${owner}/${repo}/pulls \
-d "{ \
\"title\": \"release: ${service_name} ${version}\", \
\"body\": \"release ${service_name} ${version}\", \
\"head\": \"${branch_name}\", \
\"base\": \"main\" \
}")
# `// empty`` is used to convert null to empty string
# ref: https://jqlang.github.io/jq/manual/#:~:text=Example-,empty,-empty%20returns%20no
pr_url=$(echo $response | jq -r '.html_url // empty')
if [ -z "$pr_url" ]; then
echo "[Error] PR URL not found in the response."
exit 1
fi
echo "[INFO] release PR has been created"
echo $pr_url
}
main "$@"
ブランチの作成
まずは引数が 3 つ渡されていることを確認して、それらをもとにブランチ名を決めてブランチを作成しています。
#!/bin/bash
main() {
set -eo pipefail
if [[ $# -ne 3 ]]; then
exit 1
fi
local service_name=${1}
local version=${2}
local github_token=${3}
local branch_name="release/$(echo ${service_name})_$(echo ${version})"
local owner="ksrnnb"
local repo="ci_create_pr"
echo "[INFO] Running 'scripts/app/release_pr' for ${service_name}"
git switch -c $branch_name
Deployment ファイルの Docker image のバージョン更新
先述したとおり、私たちはモノレポでマイクロサービスを管理しているので、k8s の Deployment のファイルが複数存在します。microservices/<service name>/kubernetes/<environment>/Deployment/<service name>.yaml というファイルで管理しているので、この Deployment のファイルを探して、バージョンを更新しています。
yq は yaml ファイルを更新するのに便利なコマンドとなります。3 つ目の yq が少しややこしいですが、containers で定義された image のうち、 service name が含まれるもののみ更新するようにしています。yq コマンドの詳細はドキュメントをご参照ください。
deployment_files=$(find ./microservices/${service_name} -type d -name "Deployment" -exec find {} -type f -name "*.yaml" \;)
for file in $deployment_files; do
echo "[INFO] Updating file: $file"
# tags.datadoghq.com/version を更新
yq e ".metadata.labels[\"tags.datadoghq.com/version\"] = \"${version}\"" -i "$file"
yq e ".spec.template.metadata.labels[\"tags.datadoghq.com/version\"] = \"${version}\"" -i "$file"
# service のイメージのタグのみを更新
yq e ".spec.template.spec.containers[] |= (select(.image | contains(\"${service_name}\")) .image |= sub(\":.*$\"; \":${version}\"))" -i "$file"
done
PR の作成
Deployment のファイルを更新したら、差分をコミットして GitHub に Push して、REST API を叩いて PR を作成します。今回は REST API を利用しましたが、 gh コマンドを利用するなどもう少し工夫の余地はありそうです。
git add .
git commit -m "release: ${service_name} ${version}"
git push origin $branch_name
echo "[INFO] Git push has completed"
# create PR
response=$(curl -s -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${github_token}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${owner}/${repo}/pulls \
-d "{ \
\"title\": \"release: ${service_name} ${version}\", \
\"body\": \"release ${service_name} ${version}\", \
\"head\": \"${branch_name}\", \
\"base\": \"main\" \
}")
PR の URL 取得
PR を作成したら jq コマンドを利用して、 PR の URL を取得しています。今回は URL を取得して出力するだけにしていますが、必要に応じて Slack 通知させるなど応用ができると思います。
# `// empty` is used to convert null to empty string
# ref: https://jqlang.github.io/jq/manual/#:~:text=Example-,empty,-empty%20returns%20no
pr_url=$(echo $response | jq -r '.html_url // empty')
if [ -z "$pr_url" ]; then
echo "[Error] PR URL not found in the response."
exit 1
fi
echo "[INFO] release PR has been created"
echo $pr_url
まとめ
本記事では GitHub のリリースタグ作成時に CircleCI でリリース用 PR を自動で作成するための workflow をみてきました。モノレポでマイクロサービスを管理しているという前提があるので、少し複雑になっている箇所もありましたが、みなさんが応用する時はもう少しシンプルに実装できるかと思います。
またモノレポだと、リリースタグの Description に含まれる差分がマイクロサービスごとの差分にならないという悩みがあるので、それも解消できたら次の記事にしたいと思います。