🐞

Github ActionsでAWS CDKの自動Pull Request生成に挑戦した話

2022/01/07に公開

概要

  • 趣味の開発でよく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の選定理由は、(余程のヘビーユースをしなければ)無料で手早く利用でき、メンテナンスも容易なので、個人開発の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

実行結果

  • 以下のようになる

自動作成されたPull Request

変更差分

ビルド結果のコメント

  • (私の場合、Personal Access Tokenを利用したため、一人でやっているように見えてならないが。。。)
  • 一応、CIから実行し、上記の作業を自動で実行してくれる

コード解説

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-updatesglobalでインストールする
    - 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 "\""
  1. cat package.jsonpackage.jsonの内容を持ってくる
  2. grepaws-cdk関連のパッケージを抽出
  3. awk x split"@aws-cdk/assert": "2.4.0",のような文字列を:で分割し、"@aws-cdk/assert"の箇所のみを抽出する
  4. 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の作成

    - 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
  1. まず、冒頭で投稿用のコメントを一時保存するファイルを作成する
touch comment-${{ steps.date.outputs.date }}.md
  1. このあたりは単に装飾。
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
  1. ここで、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
  1. echoで出力した結果は改行コードを含んでしまうため、最後にsedで改行コードを置換(Github APIにJSONで送信する際に扱いやすいよう)
sed -i -z 's/\n/\\n/g' comment-${{ steps.date.outputs.date }}.md
  1. ファイルの作成後、一応コメントをプレビューするため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アクションは、outputspull-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