先月GitHubActionsが正式にリリースされてから社内でも徐々に使い始めた。本記事ではGitHubActionsを業務で取り入れる上で苦労した点について書こうと思う。 GitHubActionsについては、ある程度知っている前提で書かれている。
はじめてのGitHubActionsMarketplace公開という記事をGitHub Actions Advent Calendar 2019に投稿したので、より開発に近いものに興味がある人はそちらも合わせて読んでいただけるとありがたいです。
fork先からfork元へのPRではPR上でActionが発火しない
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-events-for-forked-repositories
苦労したというか開発フローすら変更せざるを得なくなった。
なぜforkして開発するフローになったかは覚えていない。2014年ぐらいからそんな手法で開発している。メリットはたくさんあるが必要性はない。write権限ある人だってadmin権限の人だってforkして開発していた。
現在GCPを使ってる関係でCI/CDにはGoogleCloudBuildを使っているのだが、PRのtestに関してはマネージドではなくjenkinsで行っていた。PRをhookに動くGitHub Apps - Google Cloud Buildというアプリが去年か一昨年ぐらいに出てすぐ飛びついたわけだが、これもfork先からのPRには反応しなかった。fork先でも別途GitHub Appsをインストールする必要があった。
なので今回のGitHubActionsにはかなり期待していたがやっぱ無理で残念。諦めてfork開発自体をやめることにした。
正確にはpull_requestイベントは発火しないけどpushイベントはfork先のactionとして発火している。fork元のPRのchecksには表示されない。オープンソースならまだしも社内のrepositoryにはsecretsも盛々だしfork先で動かそうと思ったらforkした人が個々に設定しないと行けない。現実的ではない。
なぜなのか?
今ソースを見つけられないがこれを調べていたときに「年内にはどうにかしたい」みたいな書き込みがフォーラムにあった気がする。ただ現段階では実現していない。
なぜなのかを個人的に考えてみたが、secretsをrepositoryに登録してActionsで使用できる関係上、read権限があればforkできるfork者が、fork元にPR投げるだけでfork元のActionsを動かせるのならば、**fork元のsecretsも見放題になってしまう。**PersonalAccessTokenをsecretsに入れれば?なんて言っている記事もあるぐらいだからそれはまずい。なのでこの問題はそんな簡単な話ではないのだと思う。個人見解なので責任は持ちませんが。
どうしたか?
簡単に言えばfork開発をやめた。
forkによって得られていたメリットがなくなるので以下の整理をした。
ブランチ名の命名規則の厳格化
fork先はforkした人の持ち物だったのブランチ名は自由だったが、全員が同じリポジトリで開発する以上ブランチ名が競合したり、階層がぶつかったり(feature/hogeとfeature/hoge/fugaが両方あるとfetchしたときに.git/ディレクトリ配下が腐る)を防ぐ必要がある。ぶつからない工夫はただ自分の名前をブランチ名につけたってだけで、古いブランチかわかりやすくするためにソートしやすくfeature/{version}/{userId}/{キーワード}てな感じにした。
ゴミブランチの削除の徹底
今までreleaseブランチしかfork元にはなかったのでfeatureブランチが一気に作られるようになるわけで、マージ後の削除の徹底、マージする必要がなくなったときの削除の徹底をするようにした。これこそGitHubActionsで解決できるかも知れない。
プライベートリポジトリの扱い
個人的につくってるパブリックリポジトリに関しては発生してこなかった問題。
NodeJSやGolangなどのプロジェクトで、社内で作ったプライベートリポジトリを外部パッケージとして使っていることが多い。しかも1つのプロジェクトにいくつも依存リポジトリがある。
今まではGoogleCloudBuild上でビルドする分にはGoogleCloudKMSを使ってGitHubのデプロイキーを一つ一つ暗号化してリポジトリにおいていた。 https://cloud.google.com/cloud-build/docs/securing-builds/use-encrypted-secrets-credentials?hl=ja
今回の場合はリポジトリにSecretとして置いておけるのでgit管理下に何も置く必要がない分まだ綺麗なのだが、gcloudコマンドで済ませていた作業を今回は手作業で登録していったので大変だった。Secretに登録するGitHubAPIはおそらくあるのだろうが公式がCLI作ってるわけでもないのでこの辺の自動化はまだ課題が残っている。
プライベートアクションの作り方
「private github actions」 で調べると 好きなとこに置いていいけど.github/actions/配下にスクリプトを置いとくといいよ、みたいな記事がちらほら出てくる。求めているのは社内の複数プロジェクトで使う用のrepositoryが独立したアクションであった。
golangのプロジェクトにtypescript置きたくないし、nodejsのプロジェクトにもそのプロジェクトのtsconfigに合わせたりめんどくさい。
privateだろうがcheckoutしてきてJS動かせばいいだけ
深く考える必要もなく別リポジトリにあるコードだってcheckoutしてきて動かせばいいだけだった。何気なく使っていたactions/checkoutは、別リポジトリのcloneにも対応していたしprivateリポジトリ用にtokenを渡すことも出来た。
- uses: actions/checkout@v1
  with:
    repository: iyu/actions-milestone
    ref: master
    fetch-depth: 1
    path: actions-test/actions-milestone
    # token: "${{ secrets.DEPLOY_KEY }}" privateの場合はここにデプロイキー
- run: npm ci
  working-directory: ./actions-milestone
- run: npm run build
  working-directory: ./actions-milestone
- uses: ./actions-milestone
  with:
    repo-token: "${{ secrets.GITHUB_TOKEN }}"
    configuration-path: .github/milestone.yml
上記ははじめてのGitHubActionsMarketplace公開で作ったiyu/actions-milestoneをiyu/actions-testという別プロジェクトから実行したときの例。working-directoryがなかなかに気持ち悪い。
始めはよくわからなすぎてpwdやら$GITHUUB_WORKSPACEやら調べてた。

- 
pwd,$GITHUB_WORKSPACEともに/home/runner/work/{repository名}/{repository名}(あたりまえだが)
- action/checkoutでpathを指定しないと/home/runner/work/{repository名}/{repository名}にcheckoutされる(ふむ)
- action/checkoutで別リポジトリを指定すると/home/runner/work/{repository名}/{別repository名}にcheckoutされる(!)
- working-directoryでは相対パスで ../actions-milestoneとかはできない。あくまで相対パスは$GITHUB_WORKSPACEの配下
- どうやって指定してしたらいいんだ?
絶対パスで/home/runner/work/{repository名}/{別repository名}って指定すればいいのかもだが気持ち悪い。そもそもルールも変わりそう。
しょうがなくWORKSPACE配下にcheckoutするようにした。
Billingの無料枠がOrgs全体なのかrepo単位なのか
うちのプロジェクトだけで会社全体の無料枠使い尽くすわけにもいかず、ビルド時間はかなり気を使った。
そもそもリポジトリ単位なのか組織単位なのかよくわからない。user/repoで動くやつはuser単位で使用量計算されているわけだしorg/repoだってorg単位な気がするが。そうなるとやたらと無料枠小さいなって思うし。
https://help.github.com/ja/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions
結局読んでもいまいちわからない。おそらくorgs管理者が見れるページにはしっかり書いてあると思うが。repoのところに使用量ないしorg単位か。
上記の理由もあってビルド時間は出来るだけ減らさないといけない
今担当しているnodejsのプロジェクトではnpm installが1分超え、npm testが10分近くかかっている。なかなかに時間かかる。
npm install の高速化
対策としてcache機能が用意されている。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows
- uses: actions/cache@v1
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-
- run: npm install
keyはキャッシュファイルのキー(説明になっていないが)。このkeyで保存される。
restore-keysはkeyでヒットしなかったときに使われる。複数設定できるが上から優先的に使われる。最初はどういうときに使うんだろう?と思った。100個のmoduleをinstallしたpackage-lock.jsonとそこに1個moduleが追加されたpackage-lock.jsonは当たり前だがハッシュ値は変わる。完全一致のキャッシュは使えないわけだが、100個のmoduleをinstallしたあとの~/.npmを使えたら1個分のmoduleを取ってくるだけになるのでnpm installは早くなる。説明が下手くそだ・・・。
restore-keysを理解したい!
下記の感じの書き方をちらほら見つけたのだが、さきほどの下手くそな説明があっているのであればこの書き方は間違っている気がする。go.sumが書き換わっていてもhitすることがあるのでそうなると古いキャッシュがヒットしたことになる。modが足りてない状態のキャッシュがヒットしているのにgo mod downloadがhitしてないときしか実行しないのは間違っているのではないだろうか?正解はわからないが。
- uses: actions/cache@v1
  id: cache
  with:
    path: ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
    restore-keys: |
      ${{ runner.os }}-go-
- if: steps.cache.outputs.cache-hit != 'true'
  run: go mod download
cache-hitでスキップしたいのであればrestore-keysは指定しないのが正しいのかなと。
(2019/12/13 追記) restore-keysでヒットしたcacheはcache-hitは'true'にならないのでは?とコメント頂きました。確かに不完全なcacheでcache-hitが'true'になるのはおかしいですね。ちゃんと確認してませんでした。今も確認してないです。
actions/cacheの気をつけるところ
fork間で共有される
fork諸々のところで書いたがsecretsはfork先からは覗けない。だがこのcacheに関してはfork先とも共有される。privateだったらあまり関係ないかも知れないが公開したくないものをcacheするとやばいことになるので注意。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows#about-caching-workflow-dependencies
Warning: We recommend that you don't store any sensitive information in the cache of public repositories. For example, sensitive information can include access tokens or login credentials stored in a file in the cache path. Also, command line interface (CLI) programs like docker login can save access credentials in a configuration file. Anyone with read access can create a pull request on a repository and access the contents of the cache. Forks of a repository can also create pull requests on the base branch and access caches on the base branch.
sizeを超えると消される
今実はこの問題を抱えている。とあるリポジトリでキャッシュが何回やってもヒットしない(検証したわけではないが)。uploadは出来ているのでトータルサイズが超えて消されてるんじゃないかと思っている。たしかに~/.npmはgzで固めた後でも500MB近くある。1分近くかかるからcacheしたいのにサイズ大きいからcacheできないとかだったら悲しい。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
npm ci に変更 (2019/12/09 追記)
コメントで指摘頂き修正しました。packageの更新をしたい時以外は基本的にnpm installではなくnpm ciを使うようにしたほうがよいです。確認したところnpm iのところとnpm ciのところがめっちゃ混ざってた。
GitHubActions上1m0sかかっていた部分が40sになった(検証回数1回)。30%以上の時間短縮に!
npm test の高速化
testにはavaを使って並列実行している。jenkinsで動かしてたときもコンテナで動かしていたがCPU渡してガンガン並列処理してたがactionsで動かす場合は・・・あまりわかってない。circleciとかでも解決策があるらしいが。課題が残る。
あとはjunit形式のレポートを吐き出したりcoverageを出したりしていたが、とにかく早く終わらせたい、コケるかどうかだけ分かればいい、という状態だったので全部やめた。まぁそれでも10分かかっているのだが。
TestとLintは別JOBにしたいけど
testもlintも事前にnpm installが必要でそれがすぐ終わるのであればjob2個に分けてそれぞれ冗長的にinstallすればいいのだが、時間もかかる上にprivate repositoryのためにデプロキーの配布もあったりしてものすごく冗長になる。
ワークフローのジョブ間でデータを受け渡す方法も考えたがnode_modulesが大きすぎて圧縮と解凍だけで時間が潰れる。ボリューム使用量問題も出てきてしまう。
結局現状だとlint->testの順で直列実行している。circleciとかだと同じworkspaceで途中分岐できるらしいので羨ましい。CloudBuildもそれが出来る。ぜひ出来るようになってほしいが・・・
さいごに
最後の方がすごく雑な感じになったのは集中力が切れてきたからだ。
なんか雑なブログみたいになってしまってqiitaに上げる記事なのか悩ましいが許して欲しい。
まだ業務で使い始めてから1ヶ月経っていないので今後も問題はたくさん出てくるかもしれないが、それ以上にGitHubActionsを使っていけば業務改善がいろいろできそうで未来は明るい。
