Homebrew Taps 自動更新で比べる CircleCI Orbs と GitHub Actions

自分は Go で CLI ツールを作って配ることが多く、それの配布には GitHub Actions と Homebrew の [Taps repository](https://docs.brew.sh/Taps) を利用している。このリリース作業は結構面倒になので、極力自動化したい。

リリースの流派は「どこでやるか」「どのようにやるか」などの観点で、いろいろな考え方がある。

- WHERE
    - 手元でコマンド一発でぜんぶやる派
    - `git push --tags` から CI でやる派
- HOW
    - 全部入りツールでｴｲｯてやる（e.g. [GoReleaser](https://goreleaser.com/)）
    - 「一つのことをうまくやる」ツールを組み合わせる（e.g. [goxz](https://github.com/Songmu/goxz) + [ghr](https://github.com/tcnksm/ghr) + [maltmil](https://github.com/Songmu/maltmill)）

自分は「CI でやる」「ツールの組み合わせでやる」派。そして、このリリースフローを実現・再利用するため、 CircleCI Orbs と GitHub Actions それぞれに公開している。本記事では、これらの利用・実装コードから CircleCI と GitHub Actions を比べてみたい。


## 利用者目線


### CircleCI Orbs
[izumin5210/github-releases](https://circleci.com/orbs/registry/orb/izumin5210/github-release) および [izumin5210/homebrew](https://circleci.com/orbs/registry/orb/izumin5210/homebrew) で実現している。
Go で CLI ツールを書いたときは [timakin/go-module](https://circleci.com/orbs/registry/orb/timakin/go-module) や [izumin5210/go-crossbuild](https://circleci.com/orbs/registry/orb/izumin5210/go-crossbuild) を組み合わせて良い感じにやってる。

```yaml
version: 2.1

orbs:
  github-release: izumin5210/github-release@0.1.1
  homebrew: izumin5210/homebrew@0.1.3

aliases:
  filter-release: &filter-release
    filters:
      branches:
        ignore: /.*/
      tags:
        only: /^v\d+\.\d+\.\d+$/

executors:
  default:
    docker:
      - image: circleci/golang:1.13

workflows:
  build:
    jobs:

      # build something to release...

      - github-release/create:
          <<: *filter-release
          executor: default
          context: tool-releasing
          requires:
            - build

      - homebrew/update:
          <<: *filter-release
          executor: default
          context: tool-releasing
          requires:
            - github-release/create
```

- :thinking: CircleCI の良いところでも悪いところでもある、かなりしっかりした記述
  - YAML の [Anchor と Alias](https://circleci.com/docs/2.0/writing-yaml/#anchors-and-aliases)
 を活用すればちょっと見やすくなる
- :+1: CircleCI の [**Contexts**](https://circleci.com/docs/2.0/contexts/) による、credentials の中央管理
    - Organization に Context（この例では `tool-releasing`）を作り、必要な環境変数をセットしておく
      - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14845/6917a913-c46a-b88a-50cf-1b9a22e0f14d.png)
    - 同一 Organization であれば Context を使い回せるので、トークン取得の手間などが減る
        - 要するに、2つ目以降のリポジトリであれば YAML のコピペだけで（だいたい）動く


### GitHub Actions

[izumin5210/homebrew-tap](https://github.com/marketplace/actions/homebrew-tap) と [softprops/action-gh-release](https://github.com/marketplace/actions/gh-release) で実現する。
Go で CLI ツールを書いたときは [izumin5210/action-go-crossbuild](https://github.com/izumin5210/action-go-crossbuild) を組み合わせて良い感じにやってる。

```yaml
name: Release

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:

    # build something to release...

    - name: Create GitHub Release
      uses: softprops/action-gh-release@v1
      with:
        files: './dist/*'
      if: startsWith(github.ref, 'refs/tags/')
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    - name: Update Homebrew Formula
      uses: izumin5210/action-homebrew-tap@releases/v0
      with:
        tap: izumin5210/homebrew
        token: ${{ secrets.GITHUB_TOKEN }}
        tap-token: ${{ secrets.TAP_GITHUB_TOKEN }}
      if: startsWith(github.ref, 'refs/tags/')
```

- :+1: （CircleCI と比べて）スッキリとした記述
    - すなおに上から読み下すことができる
- :+1: （自リポジトリであれば）勝手に `secrets.GITHUB_TOKEN` が生えてくる
    - GitHub Releases へのアップロードだけなら YAML のコピペだけで OK
- :thinking: 別リポジトリを読み書きしたい場合は、別で AccessToken の発行が必要


## 実装者目線
### CircleCI Orbs

先程の2つは https://github.com/izumin5210/orbs-for-tools でコードを公開している。ShellScript がある程度書けないと基本しんどい（CI の宿命か？）。

- :thinking: 全部 YAML で書く
    - 複雑なものを書くと、どんどん長くなっていく（そしてインデントがよくわからなくなってくる）
- :+1: [Pack](https://circleci.com/docs/2.0/creating-orbs/#packing-a-config) という機能を使うと YAML の分割は可能
    - だが、ファイルをまたぐと Anchor & Alias が効かなくなってしまう…
- :thinking: 処理実体はだいたい ShellScript を YAML にベタ書きすることになる
    - かんたんなものなら良いが、 GitHub API とか叩こうものなら[大変なことになる](https://github.com/izumin5210/orbs-for-tools/blob/914595e8ee56b8c65cfb83d1f614752cfd19925c/src/homebrew/%40orb.yml#L90-L115)
    - 凝ったことやりたければ、別で Go などで実装したツールを叩くだけにするとか？
        - github-releases と homeberw はそれぞれ [ghr](https://github.com/tcnksm/ghr) と [maltmil](https://github.com/Songmu/maltmill) に強く依存している


### GitHub Actions

実装は [izumin5210/action-homebrew-tap](https://github.com/izumin5210/action-homebrew-tap) にある。 Orb と同じく [maltmil](https://github.com/Songmu/maltmill) をうまく使う形。

- :+1: TypeScript で書く
    - （ShellScript でもかけるが、Docker コンテナで動かすことになる。pull のオーバーヘッドが意外とでかい…）
    - TypeScript には型があるので事前に色々チェックできる
        - push して動かしてはじめて変数名の typo に気づく… なんてことが起きない！ステキ！
    - 型の恩恵が大きすぎて、 ShellScript にくらべ多少記述が長くなるとか気にならない
- :+1: npm にあるパッケージが利用できる
    - GitHub API を叩きたければ [@actions/github](https://github.com/actions/toolkit/tree/%40actions/github%401.1.0/packages/github) が使える！など
    - [actions/toolkit](https://github.com/actions/toolkit) に便利パッケージが置いてある
    - もちろん普通に npm に置いてある諸々が利用できる
- :thinking: ファイル操作や外部コマンド実行がちょっとしんどい（ShellScript にくらべると）
    - maltmil の安定版を GitHub Releases から落としてきて、展開して、パス通して…
        - ShellScript だと 2〜3行なのに… みたいな気持ちになること多し


## 所感
- 利用者目線では…
    - 正直、大した差異は無い
    - 結局、他に何やるかによって向いている CI が変わりそう
    - e.g.
        - 大きい Matrix を組みたい場合は GitHub Actions のほうが簡潔に書ける
        - 複雑なワークフローを組む場合は CircleCI のほうが依存グラフを記述しやすい
    - CircleCI の Contexts はかなり強力なので、組織とかでは有効かも？
- 実装者目線では…
    - TypeScript はもたらされる複雑製よりも型・静的解析の恩恵がかなり強いという印象
    - 一方で、ポータビリティを考えると結局ワンバイナリで動くツールをつくって CI 上で使うのが最善かも
        - 「CI が落ちてもリリースできる」状態にしたい
        - となると、CircleCI のほうが Orb 作るのはラクかも？
            - なんなら Orb すらいらなくて、`curl ... | tar xf - && sudo cp ...` をベタ書きすればいい

結局、こういう Custom step みたいなものは自分のために作ることが多いので、自分が利用者として CI に何を求めているかによってどちらを選択するのかが変わってくるはず。