🐞
Github ActionsでAWS CDKの自動Pull Request生成に挑戦した話
概要
- 趣味の開発でよくAWS CDKを利用してAWSの各種サービスを構築することが多い
- AWS CDKはインフラやCloud Formationの知識が少なくとも、好きな開発言語を使ってAWS環境を構築できるため、非常に優秀なソリューションであると感じる
- しかし、AWS自体の進化速度が非常に速く、これに劣らずAWS CDKも頻繁にアップデートされる(週1くらいでどんどんバージョンが上がる)
- 従来は、「気づいたときに」以下のように適宜手動でアップグレード対応をしていた
$ ncu -u
$ npm ci
$ npm run build
- AWS CDKは好きな言語で開発できるが、いくつか試してみてTypeScriptが書きやすいため好んで使っている
- TypeScriptの場合、
package.jsonでCDKのバージョンを管理するが、これをアップグレードするために npm-check-updatesというパッケージを利用している - 上記のようにこれを利用すると、
package.jsonの内容を自動で更新してくれる - しかしながら、AWS CDKのリポジトリを複数管理するとなると、このアップグレードの面倒を見るのが手作業では厳しい
- このため、CIから自動でアップグレードをサジェストしてくれる機能が欲しくなった
- 最初に思い立った上記ツイートが
2022-01-06 AM5:20とかだが、一応その翌日には一通りできたのでまあ良しとしたいw - 全体像はそれほど複雑ではないが、どちらかというと「こういうときどう書けば・・・?」というハマりが割とあり勉強になったため、自身の備忘録と、何かのお役に立てばという気持ちでこちらの記事を書きました
開発したもの
- 前置きが長くなったが、今回は以下のGithub Actionsを開発した

- Github Actionsの選定理由は、(余程のヘビーユースをしなければ)無料で手早く利用でき、メンテナンスも容易なので、個人開発のCIツールとして扱いやすいため
- 尚、現在はまだ試験運用中のため、マーケットプレイスへの公開は控えている
- 安定稼働が見込めたら公開するかもしれない
仕様
- 下表に簡単に記述する
| 仕様 |
|---|
npm-check-updates (ncu)パッケージをインストール、実行し、CI環境上にチェックアウトした package.json を更新する |
ncu 実行時、更新対象パッケージを aws-cdk 関連のものに限定する (jestなど、package.json 内の他のパッケージは対象としない) |
| 該当リポジトリに対してPull Requestを作成する |
Pull Request作成後、 npm run build でパッケージのビルドをトライする |
| ビルドの実行結果に関わらず、作成したPull Requestのコミットに対して結果をコメントする |
利用方法
- 手持ちのGithub Actionsの
workflowから以下のように利用する
name: "Create PR to update AWS CDK"
on:
push:
branches:
- main
jobs:
create-aws-cdk-update-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Create AWS CDK Update PR
uses: otajisan/aws-cdk-update-pr-builder-ts@v0.0.4
with:
token: ${{ secrets.CR_PAT }} # Github Personal Access Token
assignees: otajisan
reviewers: otajisan
- 上記の例では、
mainブランチにpushされたタイミングで実行するものとしているが、cronで定義すれば定期実行も可能なはず(未検証)
実行結果
- 以下のようになる
自動作成されたPull Request

変更差分

ビルド結果のコメント

- (私の場合、Personal Access Tokenを利用したため、一人でやっているように見えてならないが。。。)
- 一応、CIから実行し、上記の作業を自動で実行してくれる
コード解説
- action.ymlの内容を解説する
composite action
- composite actionを採用した
-
stepという単位で、複数の処理を定義できる - shell実行もできれば、他のActionを呼び出すこともできるため、workflow作成時の手間を減らせる
runs:
using: "composite"
steps:
-
stepの中身を順次説明する
日付を変数に格納
- PRに記載する日付などを管理するために、
dateという変数に20220107といった日付を設定する
- name: Set current date
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d')"
shell: sh
- ここで設定した変数は、後続の
stepより${{ steps.<step名>.outputs.<変数名> }}のように参照できる
# outputs変数を参照する例
steps:
- run: echo random-number ${{ steps.foo.outputs.random-number }}
shell: bash
実行環境のセットアップ
- TypeScriptで作成したCDKパッケージをビルドするため、Nodeの環境を作成する
- パッケージアップグレードに利用する
npm-check-updatesはglobalでインストールする
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: ${{ inputs.node-version }}
- name: Install npm-check-updates
run: npm i -g npm-check-updates
shell: sh
パッケージのアップデート
-
ncuを利用してパッケージをアップグレードする - ncu後にcleanインストールし、最後に
git statusで、CIコンソールから差分を確認できるようにする(デバッグ用途にも)
- name: Run npm-check-updates for AWS CDK
run: |
ncu -u \
$(cat package.json | grep aws-cdk | awk '!/^#/{n=split($0, ary, ":"); print(ary[1])}' | tr -d "\"")
shell: sh
- name: Update packages.json
run: npm ci
shell: sh
- name: Check git status
run: git status
shell: sh
-
ncuのコマンド箇所はshell芸。ちょっと分解して説明する。まず、整形すると以下のようになる
cat package.json |\
grep aws-cdk |\
awk '!/^#/{n=split($0, ary, ":"); print(ary[1])}' |\
tr -d "\""
-
cat package.jsonでpackage.jsonの内容を持ってくる -
grepでaws-cdk関連のパッケージを抽出 -
awkxsplitで"@aws-cdk/assert": "2.4.0",のような文字列を:で分割し、"@aws-cdk/assert"の箇所のみを抽出する -
trで"をトリム
- 以上、実行すると以下のように
aws-cdk関連のパッケージのみを抽出できるため、これをncuに食わせてアップデート対象とする(お手元のpackage.jsonでもお試しいただけると思います)
cat package.json |\
grep aws-cdk |\
awk '!/^#/{n=split($0, ary, ":"); print(ary[1])}' |\
tr -d "\""
@aws-cdk/assert
@aws-cdk/aws-ec2
@aws-cdk/aws-eks
@aws-cdk/aws-iam
aws-cdk
@aws-cdk/core
Pull Requestの作成
- 前述の通り、
step内ではshellのみでなく、他のGithub Actionsも統合(再利用)できる - Pull RequestはこちらのActionが利用しやすそうだったため採用
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@v3
with:
token: ${{ inputs.token }}
commit-message: ${{ steps.date.outputs.date }} Upgrade AWS CDK
signoff: false
base: ${{ inputs.base-branch }}
branch: upgrade/aws-cdk-${{ steps.date.outputs.date }}
delete-branch: true
title: ${{ steps.date.outputs.date }} Upgrade AWS CDK
body: " - [Release Notes](https://github.com/aws/aws-cdk/releases)"
labels: |
upgrade-aws-cdk
assignees: ${{ inputs.assignees }}
reviewers: ${{ inputs.reviewers }}
draft: false
- 内容から直感的に理解できると思うが、コミットメッセージやラベル、PRのブランチ名などを適宜指定している
- Pull Requestを作成するまでならばこれで終了でも良い
コメントの作成
- ここは少しこだわったところ
- とはいえ、「面倒なのでshell芸でなんとかする」という作りとしているので綺麗なコードとは言えない。。。
- 何をやっているかを説明する
- name: Try to build package and output to tmp file.
run: |
touch comment-${{ steps.date.outputs.date }}.md
echo "# AWS CDK build info" >> comment-${{ steps.date.outputs.date }}.md
echo "*Build result*" >> comment-${{ steps.date.outputs.date }}.md
echo "- please confirm build result and fix if error." >> comment-${{ steps.date.outputs.date }}.md
echo "\`\`\`node" >> comment-${{ steps.date.outputs.date }}.md
npm run build >> comment-${{ steps.date.outputs.date }}.md || true
echo "\`\`\`" >> comment-${{ steps.date.outputs.date }}.md
sed -i -z 's/\n/\\n/g' comment-${{ steps.date.outputs.date }}.md
shell: sh
- name: Preview comment
run: cat comment-${{ steps.date.outputs.date }}.md
shell: sh
- まず、冒頭で投稿用のコメントを一時保存するファイルを作成する
touch comment-${{ steps.date.outputs.date }}.md
- このあたりは単に装飾。
echo "# AWS CDK build info" >> comment-${{ steps.date.outputs.date }}.md
echo "*Build result*" >> comment-${{ steps.date.outputs.date }}.md
echo "- please confirm build result and fix if error." >> comment-${{ steps.date.outputs.date }}.md
- ここで、
npm run buildを実行し、結果をコメントに追記している- ポイントは
|| trueの箇所で、npm run buildの結果の成否に関わらず、後続の処理を継続できるようにしている
- ポイントは
echo "\`\`\`node" >> comment-${{ steps.date.outputs.date }}.md
npm run build >> comment-${{ steps.date.outputs.date }}.md || true
echo "\`\`\`" >> comment-${{ steps.date.outputs.date }}.md
-
echoで出力した結果は改行コードを含んでしまうため、最後にsedで改行コードを置換(Github APIにJSONで送信する際に扱いやすいよう)
sed -i -z 's/\n/\\n/g' comment-${{ steps.date.outputs.date }}.md
- ファイルの作成後、一応コメントをプレビューするため
catで開くようにしている- 展開した結果は以下のようなワンライナーの文字列となる
# AWS CDK build info\n*Build result*\n- please confirm build result and fix if error.\n```node\n\n> eks-cdk@0.1.0 build\n> tsc\n\nlib/eks-cdk-stack.ts(12,32): error TS2345: Argument of type 'this' is not assignable to parameter of type 'Construct'.\n Type 'EksCdkStack' is not assignable to type 'Construct'.\n Property 'onValidate' is protected but type 'Construct' is not a class derived from 'Construct'.\nlib/eks-cdk-stack.ts(30,33): error TS2345: Argument of type 'this' is not assignable to parameter of type 'Construct'.\n```\n
コメントの投稿
- 最後に
curlでコメントをポストする
- name: Post build result comment to PR
env:
GITHUB_TOKEN: ${{ inputs.token }}
COMMIT_COMMENT_URL: https://api.github.com/repos/${{ github.repository }}/commits/${{ steps.create-pr.outputs.pull-request-head-sha }}/comments
COMMENT_FILE: comment-${{ steps.date.outputs.date }}.md
run: |
curl -s -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/vnd.github.v3+json" \
-H "Accept: application/vnd.github.v3+json" \
${COMMIT_COMMENT_URL} \
-d "{\"body\": \"$(cat ${COMMENT_FILE})\"}"
shell: sh
- name: Remove comment file
run: rm -f comment-${{ steps.date.outputs.date }}.md
shell: sh
- ここのポイントは以下
env:
COMMIT_COMMENT_URL: https://api.github.com/repos/${{ github.repository }}/commits/${{ steps.create-pr.outputs.pull-request-head-sha }}/comments
-
GithubのPull RequestのコメントはPulls APIを利用するようなのだが、結構細かくパラメータを設定する必要があり面倒
-
このため、Commits API#create-a-commit-commentを利用した
-
こちらはPR作成時のコミット番号のハッシュ値が分かれば利用できる
-
前述のpeter-evans/create-pull-requestアクションは、
outputsにpull-request-head-shaというコミット番号のハッシュ値を携えているため、これを利用すれば良い -
あとは作成したコメントを
curlのリクエストボディにcatで開いて投入すれば良い
curl -s -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/vnd.github.v3+json" \
-H "Accept: application/vnd.github.v3+json" \
${COMMIT_COMMENT_URL} \
-d "{\"body\": \"$(cat ${COMMENT_FILE})\"}"
- 成功するとステータスコード
201が返り、コメントを投稿できる
I/O
- I/O設計は正直だいぶ甘い
- ひとまず最低限需要がありそうなパラメータのみを入力として構えているが、実際には、例えばPR作成時のブランチ名なども変更可能としたほうが親切だろう
-
outputも作成したPull Request番号や、AWS CDKのバージョン情報などを添えたほうが良さそう

作ってみての所感
- そもそもGithub Actionsが、私がだいぶ前に使っていた時と比べて高機能となっており、キャッチアップの題材としては良かった
- そんなこんなで、調べものや他の勉強をしつつだったため丸2日近く潰してしまったが、当初の目的に則ったものを作れたので良かった
- shellを多用する実装はあまり良くなかった。適宜Docker x プライベートActionの構成などに閉じ込めればだいぶスッキリしそう
Discussion