Github Actionsで簡単にPuppeteerを使えるaction-puppeteer-scriptを作りました

maku693です。

Github ActionsでPuppeteerを簡単に使えるCustom Actionを作りました。

github.com

最近Github Actions上でブラウザを動かしたくなったのですが、いちいち実行環境を整えるのも面倒なので、サクッとできないものかと調べたところ、意外とPuppeteerをそのまま使えるactionというのは存在しないようだったので、自分で作りました。

使い方はREADMEに書いてありますが、ここでも軽く紹介します。

以下のjobでは、ページのタイトルを取ってきて、それを後続のstepで利用しています。

- id: get-title
  uses: maku693/action-puppeteer-script@v0
  with:
    script: |
      const page = await browser.newPage();
      await page.goto("https://maku693.hatenablog.jp");
      const title = await page.$eval('title', el => el.textContent);
      return title;
- run: echo '${{ steps.get-title.outputs.result }}' #  `"The Third Law"` が出力される

ちゃんと試していないのですが、基本的にほぼ全てのPuppeteerのAPIが使えると思うので、スクリーンショットを撮るとか、ファイルをダウンロード・アップロードするとか、ブラウザが必要な色々な場面の自動化に活用できると思います。


ここから先はこのActionを作るにあたっておもしろかったポイントを紹介しようと思います。

JavaScript actionでnpmモジュールを使いたい場合はそれらをリポジトリに含める必要がある

Github ActionsのJavaScript actionはNode.jsで実行されるのですが、実行前にpackage.jsonを見て依存をインストールしてくれるような仕組みは存在しないので、node_modulesを直接コミットするか、webpackなどのツールで依存ライブラリをバンドルしてリポジトリにコミットしておく必要があります。

今回はドキュメントでもおすすめされていた @vercel/nccを使ってみました。

これはwebpackをラップしたツールで、webpackそのものが主にブラウザ向けにさまざまなアセット等を含めてバンドルする仕組みを提供するのに対し、nccはよりミニマルに、あるnpmモジュールを1ファイルにバンドルすることに特化しているようです。

使い方は簡単でハマりどころもなく、ドキュメント通りにコマンドを呼び出すだけでバンドルを実行してくれて便利でした。

Chromiumを実行時にダウンロードするようにした

PuppeteerはDevTools Protocolに準拠したブラウザを操作するライブラリなので、当然ながらブラウザがどこかにインストールされていないと利用できません。

普通のNode.jsプロジェクトでPuppeteerを使う際は、npm install puppeteerすると自動でChromiumがダウンロードされる*1ので意識する必要はないのですが、今回はGithub Actionsで実行したい都合上、自分でどうにかしてrunnerにChromiumをインストールしなければなりません。

Actionの利用者側でChromiumをインストールしてもらい、それを利用することもできなくはないのですが、かっこわるいし不便すぎると思ったので、 このAction単体で完結するようにできないか検討しました。

最初はJavaScript actionにするのをあきらめて、Docker container actionにしてコンテナの中にChromiumをインストールすることも考えたのですが、Dockerコンテナ内でChromiumを実行するのは少しトリッキーな上、Node.jsアプリケーションのDockerイメージは肥大化しがちで管理や利用が大変そうなため、JavaScript actionのまま開発を進めることにしました。

何か活用できるものはないかとPuppeteerのソースコードを眺めたところ、npm install puppeteerする際に自動でChromiumがダウンロードされる 仕組みで利用されている BrowserFetcher というクラスが公開されていたので、これを利用してactionの実行時にChromiumをダウンロードすることに決めました*2

BrowserFetcher を使い始めたところまではよかったのですが、ダウンロードする際にブラウザのリビジョンを指定する必要がありました。npm install puppeteer する際に実行されるスクリプトでは、決めうちでリビジョンを指定してダウンロードしていたのですが、このリビジョンはPuppeteerのAPIとしては公開されていません。したがって自分でリビジョンを決める必要がありました。

最初はChromiumの配布サイトに最新のリビジョンを示すファイルが置いてあるので、これを参照して最新のリビジョンを取るようにしていたのですが、このファイルと実際のバイナリの更新タイミングに差があるのか、ときどきダウンロードできないことがありました。

確実に存在するリビジョンを指定したいのですが、もうここまで来たらどんな雑な方法でも良いかと思い、node_modules内に存在するPuppeteerのソースコードから、自動ダウンロードに使われるリビジョン番号を抜き出し、それをソースコードとして出力するスクリプトを書いて、ビルドステップの一環として実行するようにしました。こうして無理やり抜き出したリビジョンをもとにChromiumをダウンロードするようにしました。

これでついにこのactionの実行時にChromiumをダウンロードして、Puppeteerを動かせるようになりました。

都度ダウンロードする方式だとそれに時間がかからないか気になるところですが、自分のリポジトリで試した限りでは20秒弱でダウンロードが終わっていたので、そこまで気にする必要はなさそうです。


というわけで、完成するまでには書いたこと以外にも紆余曲折あったのですが、思いついてから2日くらいで完成できました。久々に短期間で何かを完成させる体験ができて楽しめました。

*1:Puppeteerはpuppeteerとpuppeteer-coreという二つのパッケージで提供されており、puppeteerの方をインストールするとこの処理が走ります。puppeteer-coreはこの辺りのおもてなしがないバージョンで、今回のactionではcoreの方を使っています。

*2:実は使おうと思ってすぐ使えたわけではなく、この辺りの仕組みがここ最近バグっており、修正が進んでいるようだったので待っていたところ、この記事を書く十数時間前に修正版がリリースされ、無事actionを完成させられたという出来事がありました