目次
- はじめに
- 今回の環境と前提
- ローカルE2EをそのままCIへ持ち込まなかった理由
- GitHub ActionsにPlaywright E2E専用jobを追加する
- CIではDocker Composeではなくrunner上でLaravelを起動する
- MySQLはGitHub Actionsのservice containerを使う
- CI専用の環境設定をworkflowから渡す
- playwright.config.tsをローカルとCIで分岐する
- CI用ポートを83から8000へ変更した理由
webServerでLaravelの起動とready確認をPlaywrightへ任せる- setup projectをローカルとCIの両方で使う
- GitHub Actionsでの実行順
- workers・retry・timeoutをどう決めたか
- CIでは
github+listreporterを使う - Artifactは失敗時のPNGだけに絞る
- CIではtraceをOFFにした理由
actions/upload-artifactをfull commit SHAで固定する- 既存jobとは
needsなしで並列実行する - 初回CIで実測して確定したこと
- AIレビューで見つかった「コードではなくPR本文」の不整合
- 今回理解できたこと
- まとめ
- 公式一次情報
- 関連記事
はじめに
前回の【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
- Communicating with Docker service containers
- Workflow syntax -
needs - Workflow artifacts
- Store and share data with workflow artifacts
- Secure use reference
GitHub Actions公式repository
Linux
参照日: 2026-09-06
Playwright、GitHub Actions、GitHub-hosted runner、ActionのReleaseは更新されるため、実際に導入する際は最新版の公式一次情報も確認してください。