🍺

GitHub ActionsでHomebrewのtapを更新する

2021/12/29に公開

はじめに

こちらは Aizu Advent Calendar 2021 8日目の記事となります。


どうも、 @uzimaru0000 です。
以前、uzimaru0000/tv を開発した時にGitHubActions経由でHomebrewのtapを更新したので、そのことについてまとめます。

Homebrew Tapとは?

公式以外のリポジトリからフォーミュラを参照できる機能です。
これによりHomebrew経由で自分が作ったアプリケーションをインストールすることが出来るようになります。
詳しくは、公式docsをご覧ください。

モチベーション

もちろん、tapリポジトリは手動でも更新可能です。しかし、以下のような問題点があります。

  • 開発初期の高頻度のリリースに合わせて手動で更新するのは大変
  • 更新に必要な値(アプリのチェックサム等)を集めるのが大変

これらの問題が面倒になり結果的に更新、開発が止まるという事が良くありました。
こう言う面倒事は仕組みで解決したいのでCIで更新できるようにします。

更新の流れ

更新をするために以下のようなフローで行います。

  1. 開発ブランチで開発をする
  2. mainのブランチにMargeする
  3. 程よいタイミングでリリースする
  4. リリースtagがコミットされたらCIが走る
  5. tapリポジトリにPRが作られる
  6. Margeされて更新完了

GitHubActionsでは4と5の間をやります。

やり方

今回は別のリポジトリにPRを立てる方法として対象のリポジトリのCIを動かすと言う方法をとりました。
その際のtriggerには repository_dispatch を使いました。

repository_dispatch はGitHubAPI経由でActionsを発火する事ができる機能です。
詳しい使い方は、↓の記事が分かりやすいです

https://zenn.dev/mizchi/articles/3117b92a834531361fc8

図にするとこんな感じ

(汚い図ですみません🙇‍♂️)

tap側の設定

tapリポジトリ
https://github.com/uzimaru0000/homebrew-tap

アップデートスクリプト
#!/usr/bin/env node
const fs = require('fs');
const { exit } = require('process');

const formulaTemplatePath = (name) => `template/${name}.rb.tmp`;
const formulaPath = (name) => `Formula/${name}.rb`;

const main = ({ formula, description, url, sha256, version }) => {
  if (!fs.existsSync(formulaTemplatePath(formula))) {
    console.error(`Error: ${formulaTemplatePath(formula)} is not exists`);
    exit(1);
  }

  const template = fs.readFileSync(formulaTemplatePath(formula)).toString();
  const code = template
    .replace(/{{\s?description\s?}}/, `"${description}"`)
    .replace('{{ url }}', `"${url}"`)
    .replace('{{ sha256 }}', `"${sha256}"`)
    .replace('{{ version }}', `"${version}"`);

  fs.writeFileSync(formulaPath(formula), code);
};

const [, , formula, description, url, sha256, version] = process.argv;
main({ formula, description, url, sha256, version });
テンプレート
class Tv < Formula
  desc {{description}}
  homepage "https://github.com/uzimaru0000/tv"
  url {{ url }}
  sha256 {{ sha256 }}
  version {{ version }}

  def install
    bin.install "tv"
    zsh_completion.install  "completions/zsh/_tv"
    bash_completion.install "completions/bash/tv.bash"
    fish_completion.install "completions/fish/tv.fish"
  end
end
workflow.yaml
name: Update brew
on:
  repository_dispatch:
    types: [update-brew] # with client_payload.packages
  workflow_dispatch:
    inputs:
      formula:
        description: 'update target formula'
        required: true
        default: ''
      description:
        description: 'formula description'
        required: true
        default: ''
      url:
        description: 'Download URL'
        required: true
        default: ''
      sha256:
        description: 'checksum'
        required: true
        default: ''
      version:
        description: 'version'
        required: true
        default: ''
jobs:
  update-brew:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: setup Node
        uses: actions/setup-node@v1
      - name: Update formula
        run: |
          ./scripts/update.js\
            "${{ github.event.client_payload.formula }}"\
            "${{ github.event.client_payload.description }}"\
            "${{ github.event.client_payload.url }}"\
            "${{ github.event.client_payload.sha256 }}"\
            "${{ github.event.client_payload.version }}"
      - name: Create Pull Request
        id: cpr
        uses: peter-evans/create-pull-request@v3
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: 'Update packages'
          committer: GitHub <noreply@github.com>
          author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
          signoff: false
          branch: feature/update-package
          branch-suffix: timestamp
          delete-branch: true
          title: '${{ github.event.client_payload.formula }} update for ${{ github.event.client_payload.version }}'
          body: |
            @${{ github.actor }}
            
            ${{ github.event.client_payload.formula }} update for ${{ github.event.client_payload.version }}
            
      - name: Check Pull Request
        run: |
          echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
          echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

スクリプトのやってることは単純でテンプレートの決まった位置に適当な値を置換してるだけです。
API経由で渡ってきた値は以下のように参照できます。

./scripts/update.js\
  "${{ github.event.client_payload.formula }}"\
  "${{ github.event.client_payload.description }}"\
  "${{ github.event.client_payload.url }}"\
  "${{ github.event.client_payload.sha256 }}"\
  "${{ github.event.client_payload.version }}"

payloadに渡したjsonのキーで各値を参照出来ます。

アプリケーション側の設定

release.yaml

一部抜粋

  update-homebrew:
    needs: [create-release]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v1
        with:
          name: checksum-x86_64-apple-darwin
      - id: checksum
        run: |
          echo "::set-output name=sha256::$(cat checksum-x86_64-apple-darwin/x86_64-apple-darwin.sum)"
      - id: version
        run: |
          VERSION=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
          echo ::set-output name=version::$VERSION
      - uses: peter-evans/repository-dispatch@v1
        with:
          token: ${{ secrets.PERSONAL_TOKEN }}
          repository: uzimaru0000/homebrew-tap
          event-type: update-brew
          client-payload: '{ "formula": "tv", "description": "Format json into table view", "url": "https://github.com/uzimaru0000/tv/releases/download/${{ steps.version.outputs.version }}/tv-x86_64-apple-darwin.zip", "sha256": "${{ steps.checksum.outputs.sha256 }}", "version": "${{ steps.version.outputs.version }}" }'

注意点としては、別リポジトリにアクセスするので PERSONAL_TOKEN が必要になります。
client-payload には先程指定した値を設定します。

おわり

こんな感じで設定するとリリースタグを打ったタイミングで更新をしてくれるので開発だけしてリリースが出来てないと言う状況を無くせます!
自動化できる所は自動化して良い開発ライフを送りましょ〜

Discussion