こんにちは。開発2部でインターンをしている神保です。
今回、MagicPodのテスト実行から本番デプロイまでを自動化するGitHub Actionsのワークフローを作成しました。その際にMagicPodのWeb APIを同期的に利用するGitHub Actionsを自作したので、本記事で紹介させていただきます。
https://github.com/buysell-technologies/magic-pod-action
背景
MagicPod について
私の所属するプロジェクトでは、e2eテストにMagicPodを利用しています。
MagicPodはノーコードでテストケースの作成ができるe2eテストツールです。学習コストが低く、非エンジニアでも比較的簡単にメンテナンスが可能なため重宝しています。
また、テストケースの設定や実行が可能なWeb APIを提供しており、CI/CDパイプラインに容易に組み込むことが可能です。
https://magic-pod.com/api/v1.0/doc/
GitHub Actions での MagicPod Web API の利用
私のプロジェクトではmainブランチをリリースブランチとしており、PRがmainブランチにマージされると、以下のようなワークフローが走るようになっています。
- PRがmainブランチにマージされる
- 検証用(staging)環境にデプロイを行う
- 検証用環境に対してMagicPodのe2eテストを実行する
このワークフローの3番のステップで、上記のMagicPodのWeb APIを利用してテストの実行開始をトリガーしていました。
その後MagicPodのテストが無事パスすれば、手動で本番(production)環境へのデプロイを行います。これは、workflow_dispatch
でまた別のワークフローを実行していました。
ここまでが、一連のリリース作業のフローとなっていました。
現状のワークフローの課題
上記の通り、現状のワークフローでは、MagicPodの実行結果を待ってから手作業で本番にデプロイを行う必要があります。これは大変不便なので自動化したい部分です。
しかし、単純に自動化のためだけに上記のワークフローにproductionデプロイのjobを追加してしまうと次の問題が生じます。
MagicPod のテスト実行と production デプロイが同時に走る
MagicPodにはテスト一括実行というテストの開始を行うAPIはありますが、テストの開始から終了まで待ってくれるようなAPIはありません。 公式ドキュメントにもそういった記述がありました。
一括テスト実行が完了するまで待ってテスト結果をチェックすることができません(テスト結果は MagicPod の画面やテスト結果メールで確認する必要があります)。
参考: コマンドラインからのテスト一括実行 - MagicPod ヘルプセンター
したがって、GitHub Actionsのワークフローにproductionデプロイのjobを追加すると、MagicPodのテスト開始だけが完了した時点でproductionのデプロイが走ってしまいます。
このようなワークフローでは、MagicPodのテストに失敗しても既に本番環境へのデプロイが完了してしまっているというケースが発生する恐れがあるため、安全に運用できません。 テストに成功した場合はデプロイを続行し、失敗した場合は本番デプロイを行わずにワークフローを停止するという制御が必要です。
そこで、この課題を解決するために、MagicPodのテスト開始から終了までを監視するGitHub Actionsを作成し、上記のワークフローに組み込んで利用することとしました。
解決策
MagicPodのテストの実行開始から終了までを監視する方法についても、MagicPod公式ドキュメントに記載がありました。
magicpod-api-client を使えばこの問題を解消できます。
magicpod-api-clientというMagicPodのWeb APIをラップしたGo製のCLIが公開されています。
https://github.com/Magic-Pod/magicpod-api-client
このCLIには、batch-runという、テストの実行開始から完了までを待つコマンドがありました。今回はこちらのコマンドを参考にさせていただき、以下のGitHub Actionsを作成しました。
https://github.com/buysell-technologies/magic-pod-action
使い方
以下はリリースのワークフローの設定例です。
with
でMagicPodのAPI Tokenや、プロジェクト名、実行時間設定などを渡します。
job
に needs
を設定することで、前の job
の正常終了を待ってから逐次実行することが可能になります。
magic-pod-test
のjob
はテスト開始から終了までを監視しているので、MagicPodのテストがこけた場合は後続の deploy-prd
は実行されず終了してくれます。
参考: Workflow syntax for GitHub Actions - GitHub Docs
本筋とは逸れますが、stagingやproductionのデプロイのワークフローを workflow_dispatch
などでも利用する場合、別のymlファイルにワークフローが書かれているかと思います。これを再利用する場合は、呼び出し先のymlファイルに on: workflow_call
を設定することで呼び出しが可能になります。その場合は、呼び出し先でリポジトリのsecretsを参照するために secrets: inherit
の設定が必要になるので注意が必要です。
name: Release on: push: branches: - main jobs: deploy-stg: # stagingデプロイのワークフローをworkflow_callで利用する uses: ./.github/workflows/deploy-stg.yml # workflow_callではsecrets: inheritの設定が必要 secrets: inherit magic-pod-test: # needsで前のjobを待つ needs: deploy-stg uses: buysell-technologies/magic-pod-action@v0.1 with: API_TOKEN: ${{ secrets.MAGIC_POD_API_TOKEN }} # MagicPodのAPI Token ORGANIZATION: your-organization # 組織名 PROJECT: your-project # プロジェクト名 TEST_SETTING_NUMBER: "1" # テストケース番号 ESTIMATED_TIME: "300" # 期待する実行秒数。この時間が経過するまではテスト終了のチェックを行わない RETRY_INTERVAL: "10" # ESTIMATED_TIMEの経過後に、テスト実行状況のチェックを行う間隔 WAIT_LIMIT: "600" # 最大実行時間。この時間を過ぎると実行失敗として終了する deploy-prd: needs: magic-pod-test uses: ./.github/workflows/deploy-prd.yml # productionデプロイのワークフロー secrets: inherit
実装
GitHub Actions について
独自のGitHub Actionsを作成する方法には、以下の3種類があります。
参考: カスタムアクションについて - GitHub Docs
Docker Container
「Docker ContainerはJavaScript Actionよりも遅い」との記述がドキュメントにありました。今回はデプロイ → テスト → デプロイという、ただでさえ時間のかかるワークフローの一部に組み込むため、選択肢から外れます。
コンテナのビルドおよび取得のレイテンシにより、Docker コンテナのアクションは JavaScript アクションより遅くなります。
JavaScript Action
こちらを選択しました。
後述する通り、MagicPodのCLIをCI/CDで実行するだけならComposite Actionが一番手取り早く、ミニマムで実現できるのですが、独自カスタマイズが可能なことと、自身の勉強も兼ねてJavaScriptアクションで作成することとしました。
本当は後から Composite Action で実現できることに気づいてしまったことが主な理由です。
Composite Action
MagicPodのCLIをcurlして、unzipしてコマンドを叩くだけで実行ができます。Circle CIのものですが、ドキュメントに具体的な設定方法が書かれているので参考にしてください。
参考: CircleCIとの連携 - MagicPod ヘルプセンター
コード一部抜粋
https://github.com/buysell-technologies/magic-pod-action/blob/main/src/batchRun.ts
TypeScriptで実装し、@vercel/ncc ですべての依存関係をまとめてビルドしたものをdist/index.jsに出力しています。
まずはpostCrossBatchRun
でMagicPodのテスト一括実行を開始したのち、一定間隔でgetBatchRun
を呼び出し、ジョブの実行状況を取得します。成功していればそのまま終了し、失敗または一定時間が経過した場合は異常終了とします。@actions/core
のsetFailed
を呼ぶと、actionは失敗として処理され、後続のjobは停止されます。
/** * テスト一括実行を開始し、終了まで待つ。 */ export const executeBatchRun = async ({ apiToken, organization, project, testSettingNumber, estimatedTime = 300, waitLimit = estimatedTime + 120, retryInterval = 10, }: Params) => { // テスト一括実行の開始(apiを叩く) const batchRun = await postCrossBatchRun({ apiToken, organization, project, testSettingNumber, }); // 同時実行数の制限などで、テスト開始に失敗する場合がある if (!batchRun) { return { success: false, error: new Error("failed to start cross-batch-run"), }; } console.log("started:", batchRun.url); const startTime = new Date().getTime(); // 設定した時間までは待つ(timers/promisesのsetTimeoutを使ってます) await setTimeout(estimatedTime * 1000); while (true) { // テストの実行状況を取得 const batchRunUnderProgress = await getBatchRun({ organization, project, apiToken, batchRunNumber: batchRun.batch_run_number, }); // 成功の場合そのまま終了 if (batchRunUnderProgress.status === "succeeded") { console.log("successfully finished!"); return { success: true }; } // 成功以外で実行完了していた場合は異常終了とする if (batchRunUnderProgress.status !== "running") { return { success: false, error: new Error( `finished with status: ${batchRunUnderProgress.status}` ), }; } console.log("running..."); // 一定時間経過後にリトライする await setTimeout(retryInterval * 1000); // 最大待ち時間を超過した場合も異常終了する if (new Date().getTime() - startTime > waitLimit * 1000) { return { success: false, error: new Error("wait limit has come") }; } } };
まとめ
今回作成したGitHub Actionsをリリースのワークフローに組み込んだことで、本番デプロイ前のe2eテストの成功を保証しつつ、一連のリリース作業を完全に自動化できました! これによって安心・安全かつ高速に開発からリリースまでのサイクルを回していけるようになりました。また、GitHub Actionsを自作するという選択肢を得たことも個人的に学びとなりました。
一方で、リリース作業を自動化できたものの、ワークフローに要する時間は以前と変わらず長時間になってしまっています。この問題は、デプロイ時点ではなくPRの段階でMagicPodのテストを行うなどの方法についても検討し、今後改善していきたいと考えています。
最後に、BuySell Technologiesではエンジニアを募集しています。興味がある方はぜひご応募ください!