🧰

Reusable Workflowsでポリレポでも保守しやすいトイル解消法

2024/02/09に公開

はじめに

モノレポがそれなりに人気?になっている昨今ですが、まだまだポリレポな開発をしている方も多いはず。弊社もマイクロサービスというほどではありませんが、10個程度のリポジトリで開発しています。

各リポジトリでは運用業務の負担軽減のために、GitHub Actionsを使った自動化を行なっています。そんな弊社で行ったGitHub Actionsのリファクタについて紹介します。

トイルは少ないほうがいい

運用する上で必要だったりチームで動く以上、避けられない作業というのはあります。
タスクやチケットのステータス管理、PRのステータス管理、Assignee管理…etc
そういった繰り返しの作業はトイルかもしれない…?

トイルとは何でしょうか。トイルとは、プロダクションサービスを動作させることに関係する作業で、手作業で繰り返し行われ、自動化することが可能であり、戦術的で長期的な価値を持たず、作業量がサービスの成長に比例するといった傾向を持つものです。
https://www.oreilly.com/library/view/sre-google/9784873117911/ch05.xhtml#:~:text=トイルとは、プロダクションサービス,傾向を持つものです。

エンジニアなら、めんどくさい作業は自動化させたいですよね。
自動化していきましょう。

どんなトイルがあるか

チケット管理を例に挙げます。
弊社ではNotionを社内のドキュメント・タスク・チケット管理ツールとして使用しています。
「チケットはお手紙」のスローガン?のもと、開発プロセスの中で、様々な役割の人がチケットを書き込み・確認をします。

弊社の開発プロセス。太線が開発フロー、破線がチケットへのアクセス

しかし問題があります。チケットとPRのステータス管理が二重で発生してしまうことです。
PMとエンジニアで普段触るところが異なります。そのため両方のステータスが同期している必要があります。ですが、毎回PRとチケット2つのステータスを変えるのは面倒ですし、人間なので変え忘れることもあります。

  • PM・朝会:チケットを確認して進捗管理や議論する
  • エンジニア:PR上で細かい仕様や設計についての議論する

特に弊社はPMがエンジニアに比べて少なく、PMは様々なプロジェクトを管理しており、可能な限り無駄な管理コストを下げたいという思いもあります。

GitHub Actionsで自動化しよう

そこで弊社ではGitHub Actionsを使って、PRのラベルが変更されたことをトリガーとして、Notionチケットのステータスを変更しています。これによってGitHubのステータス管理さえ徹底すれば、変え忘れは発生しません。

他にも様々な運用業務をGitHub Actionsによって効率化・自動化しています。

  • PRへのプロジェクトラベル付与
  • PRへのAssigneeの自動Assign
  • Staging環境へのリリース時に検証担当者へSlackで通知
  • リリースPR、リリースドラフトの自動作成
  • Hotfix時、開発ブランチにHotfixの内容を反映するPRの自動作成
  • git-secretsを使ったシークレットスキャン
  • ...etc

弊社にとってGitHub Actionsはなくてはならない存在となりつつあります。

問題:リポジトリが多い

冒頭でも紹介しましたが、弊社では10個程度のリポジトリで開発を行なっています。私がジョインした2022年9月の段階では、全く同じ内容のGitHub Actionsとスクリプトを各リポジトリに配置していました。

これでは変更する時、

  1. すべてのリポジトリで同じ内容の変更を加え、
  2. すべてのリポジトリでPRを作成し、
  3. すべてのリポジトリでApproveをもらい
  4. すべてのリポジトリでマージする

という作業が発生します。
めんどくさいですね。

一応1つのリポジトリの内容を他のリポジトリにばら撒いてPRまで作ってくれるGitHubActionsまきまきくん.shというスクリプトファイルはありました。しかし、毎回Approveを依頼してマージするのは面倒でした。

Reusable Workflowで一箇所にまとめたいなあ……………………。

でもPublicリポジトリしかできないよね………………………………………。

えっ!?できるの!?


https://youtu.be/EL0xkI9LtQo?si=qUQNRyadoUVV3dmI

(宣伝失礼しました。太鳳ちゃん可愛いですね。😌

Reusable workflowsで一箇所にまとめちゃう

2022年12月くらいにPrivateリポジトリのworkflowもcallできるようになりました。(すごい👏)

「workflowをcallするって何?」って方

usesのことです。
これによって他のリポジトリで定義されているGitHub Actionsのworkflowを再利用できるようになります。(便利😌)

    steps:
      - uses: actions/checkout@v4

この再利用できるworkflowのことをReusable workflows(再利用可能なworkflow)と呼びます。
Reusable workflowsは↓のようにするだけで定義できます。

on:
  workflow_call:
以前までは、Privateリポジトリで定義されているworkflowはcallできませんでした。
error parsing called workflow "xxxx": Workflows in 'xxxx' cannot access remote workflows in 'xxxx'. See https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#access-to-reusable-workflows for more information.

https://zenn.dev/jerome/articles/618af7cc934f2f#追記-private-repositoryのreusable-workflowの使用について

https://github.blog/changelog/2022-12-14-github-actions-sharing-actions-and-reusable-workflows-from-private-repositories-is-now-ga/

このアップデートを受けて、いろんなリポジトリに散らばっていた自動化workflowの定義を、一箇所のリポジトリ(productivity)に集約しました。これによって、GitHub Actionsの保守性がグッと高まりました。

構成図

  1. call-common.ymlを各開発リポジトリに定義し、productivityのcommon.ymlをcallする
  2. common.yml内で各workflowの実行条件を判定し、workflowを呼び出すか判定する
  3. それぞれのworkflowで具体的な処理を実行する

ここからは、スクリプト実行のパターンと工夫ポイントについて話していきます。

パターン1:簡単なスクリプトしか書かない時

シェルスクリプトだけで完結する時や、GitHubホステッドランナーに元から入っているパッケージ しか使わない場合は、簡単な設定で実行できます。

例として、PRの作成時に作成者をassigneeに追加するworkflowを紹介します。

add-assignee.yml
name: PR作成時にPRの作成者をassigneeにする(Reusable)

on:
  workflow_call:

jobs:
  add-assignee:
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ secrets.GH_TOKEN }}
      PR_NUMBER: ${{ github.event.pull_request.number }}
    steps:
      - uses: actions/checkout@v4
      - name: PR作成時にPRの作成者をassigneeにする
        run: |
          author_login=$(gh pr view $PR_NUMBER --json author | jq -r '.author.login')
          gh pr edit $PR_NUMBER --add-assignee $author_login

GitHub CLIとjqを使用しているのみで、簡潔に書けています。
※GitHub CLIを使うにはGH_TOKENが必要になるので、渡してあげましょう。

パターン2:複雑なスクリプトを実行したいとき

シェルスクリプトだけで書くのはしんどいとき、GitHubホステッドランナーに入っていないパッケージを使いたい時などは一工夫必要です。

弊社では、より複雑な処理を実行したい時にzxを活用しています。
https://google.github.io/zx/

「zx知らないよ!」って方のために簡単に説明を入れておきました。

zxとは

JavaScriptで簡単にシェルコマンドを実行できるツールです。

Bash is great, but when it comes to writing more complex scripts, many people prefer a more convenient programming language. JavaScript is a perfect choice, but the Node.js standard library requires additional hassle before using. The zx package provides useful wrappers around child_process, escapes arguments and gives sensible defaults.
https://google.github.io/zx/getting-started#overview

簡単に書き方を紹介します。

  1. .mjsを拡張子に持つファイルを作成する
  2. 最上部にshebangを記述しする
  3. $`command` のように実行したいコマンドをバッククオートで囲む
script.mjs
#!/usr/bin/env zx

const name = 'foo & bar'
await $`mkdir ${name}`

これだけで、簡単にコマンドが実行できます。また、${...}で囲まれたものは全て自動でエスケープされ、クオートで囲まれるため、インジェクションを気にせず書けることも魅力です。

作成したzxのスクリプトを実行するには下記のコマンドを実行します。

zx ./script.mjs

非常に魅力的なzxですが、Reusable Workflowで活用するには一工夫が必要です。
GitHub ActionsのReusing workflowsという章で下記の記述があります。

If you reuse a workflow from a different repository, any actions in the called workflow run as if they were part of the caller workflow. For example, if the called workflow uses actions/checkout, the action checks out the contents of the repository that hosts the caller workflow, not the called workflow.
https://docs.github.com/en/actions/using-workflows/reusing-workflows

別のリポジトリからワークフローを再利用する場合、呼び出されたワークフロー内のアクションは、呼び出されたワークフローの一部であるかのように実行されます。例えば、呼び出されたワークフローが actions/checkout を使用する場合、アクションは呼び出されたワークフローではなく、呼び出されたワークフローをホストするリポジトリのコンテンツをチェックアウトします。

つまり、workflow_callによってactions/checkoutを実行する場合、productivity内のスクリプト群をチェックアウトできません。スクリプトがランナー上に存在しないので、スクリプトを実行しようとするとエラーになります。

これを回避するために、actions/checkoutのCheckout multiple repos (private) を活用します。

    steps:
      # productivityにアクセス可能なtokenを発行
      - name: Generate github token
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.PRIVATE_KEY }}
          owner: ${{ github.repository_owner }}
          repositories: "productivity"

      # productivityをランナーにcheckout
      - name: Check out the target repository
        uses: actions/checkout@v4
        with:
          repository: socialdb/productivity
          ref: master
          token: ${{ steps.generate_token.outputs.token }}
          path: productivity

      # 依存パッケージをinstall
      - name: Install dependencies
        run: npm i --prefix ./productivity/.github/helper_scripts && npm i -g zx

これを使うことで、プライベートリポジトリのコードをランナーにチェックアウトできます。
token発行には↓を使用しました。
https://github.com/actions/create-github-app-token

工夫ポイント1:workflowが失敗した時のSlack通知

運用効率化のための(=プロダクトに直接関係しない)ツールといえど、社内の業務に関わるため、workflowがバグっているときや、一時的に失敗してしまった時はSlack等に通知して問題を把握したいですよね。
ただ、workflowがネストしているため、どこでjobの失敗を判定してSlack通知を飛ばすかという問題があります。

これを解決するためにworkflowのoutputsを活用しました。
https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_calloutputs

具体的には、下記のような処理でSlack通知を実現しました。

  1. jobの最後に、jobが失敗したらフラグを立てる処理を入れる
  2. jobのoutputsをworkflow_callのoutputsに渡す
  3. common.ymlの最後に、各jobの成功/失敗フラグを見てSlackに通知するjobを定義する

↓実装例です。

workflowA.yml
name: PR作成時にPRの作成者をassigneeにする(Reusable)

on:
  workflow_call:
    outputs:
      IS_FAILURE:
        description: jobがfailしたかどうかのフラグ
        value: ${{ jobs.add-assignee.outputs.IS_FAILURE }}

jobs:
  add-assignee:
    runs-on: ubuntu-latest
    outputs:
      IS_FAILURE: ${{ steps.is_failure.outputs.IS_FAILURE }}
    steps:

      ###################
      # メインの処理はここ #
      ###################

      - name: 失敗したらSlack通知用のフラグを立てる
        if: ${{ failure() }}
        id: is_failure
        run: |
          echo "IS_FAILURE=true" >> $GITHUB_OUTPUT
common.yml
  notify-slack-if-job-failed:
    runs-on: ubuntu-latest
    if: ${{ always() }} # needsのjobの成功/失敗に関係なく、needsのjobがすべて完了したあとに実行する
    needs: [workflowA] # 実行するjobを配列形式で全て記述する
    steps:
      - name: フラグ出力
        env:
          EVENT_CONTEXT: ${{ toJSON(needs.*.outputs.IS_FAILURE) }}
        run: |
          echo $EVENT_CONTEXT
      - name: jobが1つでも失敗したらSlackに通知
        if: ${{ contains(toJSON(needs.*.outputs.IS_FAILURE), true) }}
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_USERNAME: ${{ inputs.SLACK_USERNAME }}
          SLACK_WEBHOOK: ${{ inputs.SLACK_WEBHOOK }}
          SLACK_CHANNEL: ${{ inputs.SLACK_CHANNEL }}
          SLACK_COLOR: failure

これによって、フラグを立てる処理を各workflowに定義するだけで、Slack通知自体の処理はまとめることができました。

工夫ポイント2:common.ymlの呼び出し側で実行するjobを選択可能にする

これらは、元々メインプロダクト向けに作成したworkflowでした。しかし、社内の別プロダクトの開発チームからも使いたいという要望が上がるようになりました。特にシークレットのスキャンは、セキュリティに関わる部分であるため、すべてのプロダクトでチェックできるようにしたいのもあります。

しかし、メインプロダクト独自の運用に合わせたworkflowもあります。プロダクトごとに人数規模も違うため、プロジェクトの管理方法も異なります。すべてのプロダクトの運用をメインプロダクトの運用に統合させるのは現実的ではありません。そのため、common.ymlを呼び出す側で、実行するjobを選択できるようにしました。

以下、実装例です

  1. call-common.yml側でパイプ繋ぎによって実行したいworkflowを指定
    workflow: "add-assignee|scan-secret"
    
  2. common.yml側でパイプ繋ぎされた文字列を解体しGITHUB_OUTPUTに出力
    pickup-jobs:
      runs-on: ubuntu-latest
      outputs: 
        workflows: ${{ steps.workflows.outputs.value }}
      steps:
        - name: 実行するjobを選択
          if: ${{ inputs.workflow != 'all' }}
          id: workflows
          run: echo -n '${{ inputs.workflow }}'|jq -c -R 'split("|")|tojson'|xargs -0 -I {} echo "value={}"|tr -d "\n" >> $GITHUB_OUTPUT
    
  3. 各jobの実行条件にpickup-jobsの出力結果の判定を加える

今発生している問題:テストやデバッグの困難さ

CI/CD系に常に付きまとう問題ですが、テストやdebugが本当に面倒かつ困難です。
最近見かけた小技で多少マシにはなるかもしれませんが…。

https://twitter.com/zaru/status/1749616032724168805?s=46&t=ZSkjAZg5Uci8bxBfc0NaHA

ローカルでGitHub Actionsをテストできるライブラリもありますが、以前触った時「これできないのか〜」となった記憶があります。(何ができなかったのか忘れた)
https://github.com/nektos/act

終わりに

最近見かけた記事が良かったので。

めんどくさい作業にぶち当たった時、一気に改善してしまう人がいる。ガッと自動化したり仕組みそのものを変えたりしてしまうのだ。「めんどくさい」と心の中で思ったなら、その時スデに行動は終わっているのである。
https://konifar-zatsu.hatenadiary.jp/entry/2023/12/21/124953

ガッと自動化していくぞ、某兄貴のように…。

ソーシャルデータバンク テックブログ

Discussion