GoReleaserとGithub Actionsを使ってHomebrewパッケージ配布

# はじめに

写真を良い感じに整理するCLIツールを作り、GoReleaserでクロスコンパイルした成果物を自動的に配布できるようになったのはいいが、アップデートしたときなどいちいちバイナリを置き換えるのはダルい。

そこで、Homebrewで配布できるようにすれば楽なのでは無いか、と気軽に始めたらどハマりしたので、正攻法をまとめておく。この手の記事はその辺に転がっているのだが、Github Actionsの仕様変更、GoReleaserの仕様変更に追いついてなくて動かなかったりする。

当該CLIツール↓

https://github.com/Soli0222/exiforge 

# Tap用リポジトリをつくる

`brew tap`したときに参照するリポジトリをつくる。
このとき、リポジトリ名は`homebrew-${APP_NAME}`とする。中身に何か入れる必要はなく、空っぽでよい。後々、GoReleaserが自動的に構成ファイルをPushしてくれる。

今回は`exiforge`という名前なので、`homebrew-exiforge`とした。

https://github.com/Soli0222/homebrew-exiforge

# Github Appsを設定する

前述したとおり、アプリのリポジトリで走るGithub Actionsから、Tap用のリポジトリに対して構成ファイルをPushできるようにする必要がある。従来はPATというトークンを使う手法があったが、今から作るならGithub Appsを使った方が便利だしセキュアだろう。

参考

https://zenn.dev/tmknom/articles/github-apps-token

## つくりかた

まずはGithub Appsをつくる。
Account Settings → Developer settings → New GitHub App で作成画面までたどり着ける。

入力すべき項目は以下の2つだ。名前はなんでもいいし、Homepage URLもなんでもいい。

- GitHub App Name
- Homepage URL

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/f398fb19-5bb7-44e8-a73c-68000f475542.png)

また、Appが使われたときにWebhookでイベントの詳細を送ってくれる機能がデフォルトで有効になっているが、いらないので切っておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/1ddd4fae-e2ff-4ef4-b7e1-7356988da577.png)

最後に権限の設定を行う。Repository permissionsを展開し、ContentsをRead and writeにする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/1ccaf8fd-5678-461c-bf8a-4aa7da8df77c.png)

ここまできたら、一番下のCreate GitHub Appをクリックすれば、Github Appsが作成される。

## Github Appsに対する設定

作ったAppをActionsで使えるようにする。
まずは、App IDを控える。これは、作成後表示された画面に表示されている。Aboutの中、Owned byの下にあるものだ。

また、private keyを作成する。このまま一番下まで行くと、Generate a private keyというボタンがあるので、クリックして作成する。そうすると鍵がダウンロードされる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/1ecef115-002d-438f-b3d4-8d6972d3dcdb.png)

次に、アカウントに対して、Github Appをインストールする。
左側のメニューからInstall Appを選択し、Installボタンをクリックする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/cf5e79e4-9946-46f2-9e40-2a9c9276eba6.png)

このとき、このAppが使えるリポジトリを自分が所有するすべてにするか、絞ることができる。ここはお好みで。
少なくとも、homebrew-${APP_NAME}リポジトリで使えれば問題ない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/daff77e0-5c7d-42e9-b592-5fa6c966e37d.png)

これでGithub Appsに対する設定は終わりだ。

## リポジトリに対する設定

アプリケーションのリポジトリに対する設定を行う。
リポジトリのSettingsを開き、Security → Secrets and variables → Actions を開く。

New repository secretから、`APP_ID`という名前で、先ほど控えたGithub AppのID、`PRIVATE_KEY`という名前で、ダウンロードした秘密鍵の内容を貼り付ける。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/8352a21a-5dd1-46d3-8a86-09910b7f1ab5.png)

これで、アプリケーションのリポジトリ内で走るGithub Actionsから、homebrew-${APP_NAME}リポジトリへのPushができるようになった。(正確にはもうワンステップをGithub Actions Workflow内にて踏む必要があるが。)

# Github Actions Workflowをつくる

以下のようなWorkflowを作った。
バージョンのタグがPushされたタイミングで実行されるようにし、トークンの取得、Goのセットアップ、GoReleaserの実行というワークフローだ。

```yaml:release.yaml
name: goreleaser

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Generate GitHub Apps token
        id: generate
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23.6'
      
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          # either 'goreleaser' (default) or 'goreleaser-pro'
          distribution: goreleaser
          # 'latest', 'nightly', or a semver
          version: '~> v2'
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          APP_TOKEN: ${{ steps.generate.outputs.token }}
```

重要なのは、Generate Github Apps tokenというjobだ。
ここで、先ほど設定したApp IDと秘密鍵を使って、トークンを取得する。このトークンを使うことで、やっとリポジトリへの操作ができるわけだ。

```yaml
      - name: Generate GitHub Apps token
        id: generate
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}
```

ここで発行されたトークンは`steps.generate.outputs.token`で参照できるので、これを`APP_TOKEN`としてGoReleaserに与える。homebrew-${APP_NAME}への構成情報ファイルはGoReleaserが生成し、Pushを行うので、こうしなければならない。

```yaml
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          # either 'goreleaser' (default) or 'goreleaser-pro'
          distribution: goreleaser
          # 'latest', 'nightly', or a semver
          version: '~> v2'
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          APP_TOKEN: ${{ steps.generate.outputs.token }}
```

# GoReleaserの設定

最後にGoReleaserの設定だ。以下のような設定をしている。

```yaml:.goreleaser.yaml
version: 2

before:
  hooks:
    - go mod tidy
    - go generate ./...

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    binary: exiforge

archives:
  - formats: [ 'tar.gz' ]
    name_template: >-
      {{ .ProjectName }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64
      {{- else if eq .Arch "386" }}i386
      {{- else }}{{ .Arch }}{{ end }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}
    format_overrides:
      - goos: windows
        formats: [ 'zip' ]
    files:
      - nothing*

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"

brews:
  - repository:
      owner: # Githubのユーザー名
      name: # homebrew-${APP_NAME}
      token: "{{ .Env.APP_TOKEN }}"

```

Homebrewでパッケージ配布するにあたって重要なのはこの部分である。
brew以下に、オーナー名、Tap用のリポジトリ名、そしてGithub Appのトークンを設定する。
`APP_TOKEN`は先ほどWorkflow内にて、設定した環境変数名だ。

```yaml
brews:
  - repository:
      owner: # Githubのユーザー名
      name: # homebrew-${APP_NAME}
      token: "{{ .Env.APP_TOKEN }}"
```

これですべての設定が終わりだ。
Pushしてタグ付けPushして、お祈りを捧げる。

# インストールしてみる

Workflowが無事完了したら、`brew install`をしてみる。

```bash
$ brew tap ${USER_NAME}/${APP_NAME}
$ brew install ${USER_NAME}/${APP_NAME}/${APP_NAME}
```

これでインストールできれば、リリースとパッケージ配布の自動化は完了だ。

パッケージを更新したときは、こんな感じで更新できる。まあ、普通のbrewの操作である。

```bash
$ brew update
$ brew upgrade
```