0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel 13 + WSLで始めるPlaywright【6. GitHub Actions編】

0
Last updated at Posted at 2026-09-07

目次


はじめに

前回の【5. ブラウザテスト実装編】では、Laravel 13 + WSL環境でPlaywrightのブラウザテストを実装しました。

ローカルでは、

setup          1
smoke          1
退会modal      3
review modal   4
pagination     2
----------------
合計          11

をChromiumで実行できる状態まで進めています。

今回は、このPlaywright E2EをGitHub Actions上でも自動実行できるようにします。

ただし、ローカルで動いている構成をそのままCIへコピーしたわけではありません。

ローカルでは、

WSL
├─ Node.js
├─ Playwright
└─ Chromium
      │
      │ http://localhost:83
      ▼
Docker Compose
└─ laravel.e2e
      │
      ▼
MySQL
└─ e2e_testing

という構成でした。

一方、GitHub Actionsでは最終的に、

GitHub-hosted runner
├─ PHP 8.4
├─ Composer
├─ Node.js
├─ Playwright / Chromium
├─ php artisan serve
└─ MySQL service container

というrunner-native Laravel + MySQL service containerの構成にしました。

この記事では、

  • なぜCIではDocker Composeを使わなかったのか
  • PlaywrightのwebServerをどう使ったか
  • ローカルとCIでbaseURLをどう分けたか
  • E2E DBをどう分離したか
  • retry / timeout / workersをどう決めたか
  • なぜCIのtraceをOFFにしたのか
  • 失敗時ArtifactをなぜPNGだけにしたのか
  • GitHub Actionsをfull commit SHAで固定した理由
  • 初回CIで実際に何を確認したか

を、実装時の試行錯誤と公式一次情報をもとに整理します。


今回の環境と前提

Issue #159実装時の主な構成です。

項目 構成
PHP 8.4
Laravel 13.26.1
Node.js 24.18.0(CI)
Playwright Test 1.62.1
Browser Chromium
CI runner ubuntu-24.04
CI Laravel URL http://127.0.0.1:8000
ローカルLaravel URL http://localhost:83
CI DB e2e_testing
Laravel connection e2e_mysql
workers 1
retries 0
test timeout 30秒
webServer timeout 60秒
job timeout 20分
CI trace OFF
video OFF
storageState 不使用

前提として、前回までに次の基盤は作成済みです。

Playwright Test
TypeScript
Chromium
E2E専用Laravel
e2e_mysql
e2e_testing
e2e:reset
E2eSeeder
setup project
ブラウザテスト

今回はこれらを作り直すのではなく、CIで同じテスト契約を再現することに集中します。


ローカルE2EをそのままCIへ持ち込まなかった理由

ローカルではDocker Composeのlaravel.e2eを使っています。

localhost:83
↓
Docker port publish
↓
laravel.e2e:80

そのため最初は、GitHub ActionsでもDocker Composeを使えばローカルとの差を減らせるのでは、と考えました。

しかし、CIにはすでに、

PHP
Composer
Node.js
MySQL service container

を使う既存の品質チェックjobがありました。

そこでPlaywright E2EだけDocker Composeを丸ごと起動するより、

Laravel
→ runner上で直接起動

MySQL
→ GitHub Actionsのservice container

Playwright
→ runner上から実行

とした方が、CIの構造を単純にできます。

GitHub Actions公式では、servicesキーワードでjobに必要なservice containerを追加できます。

また、jobをrunner machine上で直接実行する場合、service containerのportをhostへmappingすると、localhost:<port>または127.0.0.1:<port>から接続できます。

公式ドキュメント:

今回の最終構成は、

GitHub runner
├─ Laravel
│    └─ 127.0.0.1:8000
│
├─ Playwright
│    └─ Chromium
│
└─ MySQL service
     └─ 127.0.0.1:3306

です。


GitHub ActionsにPlaywright E2E専用jobを追加する

既存CIには、

python-quality-checks
quality-checks

がありました。

ここへ新しく、

e2e-tests
表示名: Playwright E2E

を追加します。

概念的には次の構成です。

jobs:
  python-quality-checks:
    # 既存

  quality-checks:
    # 既存

  e2e-tests:
    name: Playwright E2E
    runs-on: ubuntu-24.04
    timeout-minutes: 20

今回、既存2jobの中へPlaywrightを混ぜなかった理由は、責務を分けたかったからです。

quality-checks
→ PHP / Laravel側の品質確認

Playwright E2E
→ 実ブラウザによるE2E

ブラウザやOS依存packageのinstallもあるため、独立jobの方が失敗箇所も分かりやすくなります。


CIではDocker Composeではなくrunner上でLaravelを起動する

LaravelはGitHub-hosted runner上で直接起動します。

最終的なcommandは、

php artisan serve \
  --host=127.0.0.1 \
  --port=8000 \
  --no-reload

です。

ここで重要なのは、workflowのstepとしてバックグラウンド起動するのではなく、PlaywrightのwebServerに起動を任せたことです。

つまり、

Playwright開始
↓
webServerがLaravelを起動
↓
ready URLを確認
↓
setup project
↓
E2E DB reset
↓
Chromium tests
↓
Playwright終了時にserverも終了

という流れにしました。

Playwright公式のwebServerは、テスト前にlocal serverを起動し、指定URLが利用可能になるまで待つための機能です。

公式ドキュメント:


MySQLはGitHub Actionsのservice containerを使う

E2E DBは、本番DBやローカル開発DBを使いません。

CIでも専用の、

e2e_testing

を利用します。

GitHub ActionsではMySQLをservice containerとして起動します。

実装時の構成は概ね次の形です。

services:
  mysql:
    image: mysql:8.4.11

    env:
      MYSQL_DATABASE: e2e_testing
      MYSQL_USER: <CI専用の合成user>
      MYSQL_PASSWORD: <CI専用の合成password>
      MYSQL_ROOT_PASSWORD: <CI専用の合成root password>

    ports:
      - 3306:3306

    options: >-
      --health-cmd="mysqladmin ping --host=127.0.0.1 ..."
      --health-interval=10s
      --health-timeout=5s
      --health-retries=10

ここで使うcredentialはCI内だけで使う合成値です。

本番credential
→ 使用しない

実在user
→ 使用しない

ローカルE2E credential
→ 使用しない

CI用の合成credential
→ workflow内だけで使用

GitHub Actions公式でも、runner machine上のjobからservice containerへ接続する場合はport mappingが必要で、mapping後はlocalhostまたは127.0.0.1からアクセスできます。

公式ドキュメント:


CI専用の環境設定をworkflowから渡す

CIでは.envを作成しません。

必要な設定はworkflowのenv:から渡します。

主な値は次のとおりです。

APP_ENV=e2e
APP_DEBUG=false

DB_CONNECTION=e2e_mysql
DATABASE_URL=''

E2E_DB_HOST=127.0.0.1
E2E_DB_PORT=3306
E2E_DB_DATABASE=e2e_testing
E2E_DB_USERNAME=CI専用合成user
E2E_DB_PASSWORD=CI専用合成password

さらに通常のDB_*は、誤って通常mysql connectionを利用した場合に接続できない値へ固定しました。

正常経路
→ e2e_mysql
→ E2E_DB_*

誤った通常connection
→ DB_*
→ 接続失敗

これは前回までにローカルE2Eで採用したfail-closed方針をCIでも維持するためです。

また、

APP_DEBUG=false

としています。

失敗時のscreenshotをArtifactへ保存するため、Laravelの詳細なdebug画面をCI成果物へ残しにくくする目的もあります。

config cacheは使わない

このE2E構成では、

config:cache
optimize

を実行していません。

ローカルE2Eでも、環境を切り替える初期運用ではconfig cacheを使わない方針にしていたため、CIでも同じ契約を維持します。


playwright.config.tsをローカルとCIで分岐する

最終的なplaywright.config.tsでは、

const isCI = !!process.env.CI;

を使って、ローカルとCIを分岐します。

主要部分は次のようになりました。

import { defineConfig } from '@playwright/test';

const isCI = !!process.env.CI;

export default defineConfig({
    testDir: './e2e',
    workers: 1,
    retries: 0,
    timeout: 30_000,
    forbidOnly: isCI,
    reporter: isCI ? [['github'], ['list']] : undefined,

    use: {
        baseURL: isCI
            ? 'http://127.0.0.1:8000'
            : 'http://localhost:83',

        screenshot: 'only-on-failure',
        trace: isCI ? 'off' : 'retain-on-failure',
        video: 'off',
    },

    webServer: isCI
        ? {
              command:
                  'php artisan serve --host=127.0.0.1 --port=8000 --no-reload',
              url: 'http://127.0.0.1:8000/login',
              reuseExistingServer: false,
              timeout: 60_000,
          }
        : undefined,

    projects: [
        {
            name: 'setup',
            testMatch: /global\.setup\.ts/,
        },
        {
            name: 'chromium',
            dependencies: ['setup'],
            testIgnore: /global\.setup\.ts/,
            use: {
                browserName: 'chromium',
            },
        },
    ],
});

ローカル

baseURL
→ http://localhost:83

webServer
→ 使用しない

Laravel
→ Docker Composeのlaravel.e2e

CI

baseURL
→ http://127.0.0.1:8000

webServer
→ 有効

Laravel
→ runner上のphp artisan serve

同じPlaywrightテストを使いながら、Laravelの起動方法だけを環境に合わせて変える構成です。


CI用ポートを83から8000へ変更した理由

これは実装中に一度引っかかったポイントです。

最初は、

ローカル
localhost:83

CI
127.0.0.1:83

と、同じ83番portを使う設計でした。

しかしローカルとCIでは、83番portをbindしている主体が違います

ローカル

Docker
83:80

なので、ホスト側83番portのpublishはDocker側が担当します。

CI

php artisan serve --port=83

なので、GitHub-hosted runner上のLaravel process自身が83番portへbindします。

今回のCIでは83番portでLaravelを起動できなかったため、CI側は8000番へ変更しました。

一般的なLinuxでは、ip_unprivileged_port_startの既定値は1024で、それ未満のportへのbindにはrootまたはCAP_NET_BIND_SERVICEが必要です。

公式:

今回も低番portを避けることでCIが正常に動作したため、最終的に、

ローカル
http://localhost:83
→ Docker port publish

CI
http://127.0.0.1:8000
→ runner上のLaravel process

へ分離しました。

ここでの学びは、

同じ番号に揃えることより、誰がそのportへbindするのかを見る

ことでした。


webServerでLaravelの起動とready確認をPlaywrightへ任せる

CIではwebServerを使います。

webServer: isCI
    ? {
          command:
              'php artisan serve --host=127.0.0.1 --port=8000 --no-reload',

          url: 'http://127.0.0.1:8000/login',

          reuseExistingServer: false,
          timeout: 60_000,
      }
    : undefined,

Playwright公式では、webServer.urlに指定したURLがready状態になるまで待ってからテストを開始できます。

公式:

なぜ/loginなのか

ready probeには、

http://127.0.0.1:8000/login

を使います。

理由は、/loginがこのプロジェクトではDB非依存で表示できる画面だからです。

E2E DBをまだresetしていない段階でも、

Laravel serverが起動したか

だけを確認できます。

順番は、

Laravel起動
↓
/login ready
↓
setup project
↓
e2e:reset
↓
Chromium test

です。

ready probeとDB resetの責務を分けています。


setup projectをローカルとCIの両方で使う

前回作成したsetup projectはそのまま維持します。

ただし、e2e:resetの呼び出し方法だけローカルとCIで変わります。

ローカル

docker compose exec -T \
  laravel.e2e \
  php artisan e2e:reset

CI

php artisan e2e:reset

e2e/global.setup.tsでは、概ね次のように分岐しました。

const isCI = !!process.env.CI;

const command = isCI
    ? 'php'
    : 'docker';

const args = isCI
    ? ['artisan', 'e2e:reset']
    : [
          'compose',
          'exec',
          '-T',
          'laravel.e2e',
          'php',
          'artisan',
          'e2e:reset',
      ];

const result = spawnSync(command, args, {
    stdio: isCI ? 'inherit' : 'ignore',
});

CIだけstdio: 'inherit'

ローカルでは従来どおり出力を抑えます。

一方CIでは、

stdio: 'inherit'

にして、reset失敗時の診断情報をworkflow logへ出します。

ただし、e2e:reset側はcredentialそのものを表示しない設計を維持します。

失敗原因を追える
+
credentialを出さない

の両立を狙いました。


GitHub Actionsでの実行順

E2E jobの主なstepは次の順番です。

checkout
↓
PHP setup
↓
composer install
↓
Node.js setup
↓
npm ci
↓
npm run build
↓
npm run typecheck:e2e
↓
npx playwright install --with-deps chromium
↓
npx playwright test --project=chromium
↓
失敗時だけscreenshot Artifact

Playwright公式のCIドキュメントでも、基本的な流れとして、

npm ci
npx playwright install --with-deps
npx playwright test

が案内されています。

公式:

今回はChromiumだけを使うため、

npx playwright install --with-deps chromium

としました。

PlaywrightのBrowser設定と同様、最初からFirefox / WebKitを追加せず、現在必要なChromiumだけに限定しています。


workers・retry・timeoutをどう決めたか

今回の最終値は次のとおりです。

workers
→ 1

retries
→ 0

test timeout
→ 30秒

webServer timeout
→ 60秒

job timeout
→ 20分

workers: 1

Playwright公式はCI環境で安定性と再現性を優先する場合、workers: 1を推奨しています。

公式:

このプロジェクトではE2E DBを共有しているため、初期段階では並列化より再現性を優先します。

retries: 0

PlaywrightのsampleではCI retryを設定する例もあります。

ただし今回は、

失敗
→ まず失敗として観測する

ことを優先しました。

ローカルWSLではNode.js自体の断続クラッシュも経験していたため、

とりあえずretryすれば通る

という状態をCIの標準仕様にしたくありませんでした。

そのため初期値は、

retries: 0

です。

timeoutは種類ごとに分ける

30秒
→ 1テストの上限

60秒
→ Laravel server起動待ち

20分
→ job全体

と、同じ「timeout」でも責務を分けています。


CIではgithub + list reporterを使う

CIでは、

reporter: [
    ['github'],
    ['list'],
],

を使います。

Playwright公式のgithub reporterは、GitHub Actions上でfailure annotationを表示するためのreporterです。

公式:

list reporterも併用し、

GitHub Actions UI
→ annotation

workflow log
→ testごとの実行結果

の両方から確認できるようにしました。

HTML reportは初期CIではArtifact化していません。


Artifactは失敗時のPNGだけに絞る

PlaywrightのCI例ではHTML reportをArtifactへ保存する例もあります。

しかし今回は、Artifactへ何を保存するかを最小化しました。

GitHub Actions Artifactへ保存するのは、

失敗時のPNG screenshotだけ

です。

workflowでは概ね次の設定です。

- name: Upload E2E failure screenshots
  if: ${{ failure() }}

  uses: actions/upload-artifact@<full-commit-sha>

  with:
    name: e2e-failure-screenshots
    path: test-results/**/*.png
    if-no-files-found: ignore
    retention-days: 7
    include-hidden-files: false

GitHub公式ではworkflow artifactの代表例として、

test results
failures
screenshots

などが挙げられています。

公式:

なぜtest-results/全体ではないのか

保存対象を、

test-results/**/*.png

へ絞っています。

そのため、

trace.zip
video
error-context.md
storageState
auth state

などをまとめてuploadしません。


CIではtraceをOFFにした理由

PlaywrightのTrace Viewerは非常に便利です。

公式でも、Trace Viewerでは、

  • DOM snapshot
  • screenshot
  • network request
  • request / response情報
  • console log

などを確認できます。

公式:

だからこそ、今回のpublic repository向けCIでは初期Artifactとして採用しませんでした。

今回の方針は、

ローカル
trace: retain-on-failure

CI
trace: off

です。

Playwright公式の一般推奨とは少し違う

Playwright公式のBest Practicesでは、CI失敗時の調査にはTrace Viewerが強力だと説明されています。

公式:

それでも今回OFFにしたのは、

Playwright一般の推奨

と、

このpublic repositoryで何をArtifactとして残すか

を分けて考えたためです。

初期段階では、

GitHub reporter
+
workflow log
+
失敗PNG

だけにし、必要ならローカルでtraceを再取得します。

つまり、

traceが悪いのではなく、CI Artifactとして残す情報量を最初は小さくした

という判断です。


actions/upload-artifactをfull commit SHAで固定する

Artifact uploadには、

actions/upload-artifact

を使います。

今回のworkflowでは、tagだけではなくfull commit SHAで固定しました。

uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a

実装時に確認したReleaseは、

v7.0.1

です。

GitHub公式Releaseでも、v7.0.1がcommit 043fb46...へ対応していることを確認できます。

またGitHubのSecure use referenceでは、actionをimmutable releaseとして使うにはfull-length commit SHAへpinすることが推奨されています。

公式:

今回の考え方は、

v7.0.1
→ 人間がversionを理解するためのコメント

full commit SHA
→ workflowが実際に参照する固定値

です。


既存jobとはneedsなしで並列実行する

今回の3jobは、

python-quality-checks
quality-checks
Playwright E2E

です。

Playwright E2Eに、

needs:

を設定していません。

GitHub Actionsのneedsは、

このjobが成功してから次を実行する

という依存関係を作るための設定です。

公式:

今回は、

PHP quality checks
Python quality checks
Playwright E2E

の間に実行上の依存関係がないため、並列にしました。

             ┌─ python-quality-checks
workflow ────┼─ quality-checks
             └─ Playwright E2E

E2Eを既存quality-checksの後に待たせないことで、workflow全体の待ち時間を増やしにくくしています。


初回CIで実測して確定したこと

今回、timeoutやretentionは「なんとなく決めて終了」にはしませんでした。

初回GitHub Actionsで実際に動かし、正常系と失敗系の両方を確認しています。

正常系

実測した主な結果です。

Playwright E2E job
→ PASS
→ 約2分18秒

Playwright test本体
→ 11 passed
→ 約14.7秒

workflow全体
→ 約2分22秒

既存jobも成功しました。

quality-checks
→ PASS
→ 約1分17秒

python-quality-checks
→ PASS

これを見て、

job timeout 20分
workers 1
retries 0
test timeout 30秒
webServer timeout 60秒

を初期運用として維持しました。

失敗系も確認した

成功時だけでなく、一時的に失敗経路を発生させ、

GitHub reporter annotation
失敗screenshot生成
Artifact upload

も確認しました。

さらにArtifactの中身が、

PNGだけ

であることを確認し、

trace
video
storageState

が含まれていないことも確認しています。

Artifact retentionは、

7日

へ確定しました。

GitHub公式ではretention-daysでArtifactごとの保持期間を設定できます。

公式:


AIレビューで見つかった「コードではなくPR本文」の不整合

今回、コードがほぼ完成した後のレビューで面白い指摘がありました。

最終コードは、

CI
http://127.0.0.1:8000

になっていたのに、PR本文には実装途中の、

http://127.0.0.1:83

が残っていました。

コードは正しい。

CIもPASSしている。

でも、説明文が古い

さらにPR本文には、

初回CIで確認する項目

が未チェックのまま残り、実測した、

11 passed
2分18秒
Artifact 7日
失敗経路probe

もまだ反映されていませんでした。

そこで最後に、

PR本文
↓
現行実装へ合わせて8000へ修正

初回CI実測
↓
PR本文へ記録

を行いました。

このときはソースコード変更もCI再実行も不要でした。

今回の学びは、

コード、CI結果、PR本文、後続ドキュメントまで一致して初めて作業が完了する

という点です。

実際、この修正をしなければ、後続のPlaywright運用ドキュメントへ古い83番portを引き継ぐ可能性がありました。


今回理解できたこと

GitHub ActionsへPlaywrightを追加して理解できたことを整理します。

GitHub Actions service container
→ DBなど、jobに必要なserviceを用意できる

Playwright webServer
→ テスト対象Laravelの起動・ready待ちを管理できる

baseURL
→ ローカル / CIの接続先を切り替える

process.env.CI
→ CI専用設定の分岐に使う

setup project
→ DB reset成功後だけChromium testへ進む

workers: 1
→ 初期CIでは安定性・再現性を優先

retries: 0
→ flakyをretryで隠さない

github reporter
→ Actions上へfailure annotationを出す

Artifact
→ 失敗調査に必要な情報だけを残す

full commit SHA
→ Actionをimmutableな参照へ固定する

needs
→ job間に実行依存がある場合だけ指定する

特に大きかったのは、

ローカルとCIを完全に同じにする

ことが目的ではないと分かったことです。

今回も、

ローカル
Docker Compose + localhost:83

CI
runner-native Laravel + 127.0.0.1:8000

と実行方法は違います。

一方で、

APP_ENV=e2e
e2e_mysql
e2e_testing
e2e:reset
E2eSeeder
setup project
Chromium
workers: 1

というテスト契約は同じです。

環境の実装を無理に同一化するのではなく、守るべき契約を同じにすることが重要でした。


まとめ

今回は、ローカルで動いていたPlaywright E2EをGitHub Actionsへ統合しました。

最終構成は、

GitHub Actions
└─ Playwright E2E
   ├─ ubuntu-24.04
   ├─ PHP 8.4
   ├─ Node.js
   ├─ MySQL service
   │  └─ e2e_testing
   ├─ php artisan serve
   │  └─ 127.0.0.1:8000
   ├─ setup project
   │  └─ e2e:reset
   └─ Chromium
      └─ 11 tests

となりました。

さらに、

workers 1
retries 0
CI trace OFF
github + list reporter
失敗PNGだけArtifact
retention 7日
Actionはfull commit SHA pin
既存jobとは並列実行

まで整備しています。

初回CIでは、

Playwright E2E
→ PASS

11 tests
→ PASS

を確認できました。

Playwright公式のCI例をそのままコピーするのではなく、

このLaravelプロジェクトのDB分離
public repositoryでのArtifact方針
既存CI構成
ローカルE2Eとの違い

を考えながら調整したことで、CI上でも安全にブラウザテストを実行できる構成になりました。

これで、

ローカルで実装
↓
Playwrightで確認
↓
Pull Request
↓
GitHub Actionsで自動E2E

まで一通りつながりました。


公式一次情報

この記事は、実装時のコード・CI実測に加えて、次の公式一次情報を確認しながら整理しています。

Playwright

GitHub Actions

GitHub Actions公式repository

Linux

参照日: 2026-09-06
Playwright、GitHub Actions、GitHub-hosted runner、ActionのReleaseは更新されるため、実際に導入する際は最新版の公式一次情報も確認してください。


関連記事

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?