[4/5] Playwright と GitHub Actions で deploy 後 E2E を 1 時間で組んだ夜
Vercel Deployment Protection で守られた preview に対して、 deploy 後に Playwright Smoke Test を自動実行する仕組みを 1 時間で組みました。 Vercel API での Protection Bypass Secret 発行 + GitHub Secrets 登録 + Slack 失敗通知まで全部 CLI 完結の手順を書きます。
Part 3 の続きです。
本番(preview)で 38 件焼けてたエラーを夜中のうちに直しきったあと、 ぼくは「次は deploy 前 / deploy 直後に検出したい」と思いました。 ローカルでは型チェックも build も lint も通ってたのに、 deploy してから 500 連発に気づいたのは、 ローカルチェックの限界です。
そこで、 deploy 後の preview に対して Playwright で Smoke Test を自動実行する仕組みを、 1 時間で組みました。 Vercel の Deployment Protection で守られた preview を相手にする話なので、 Protection Bypass の発行手順も合わせて書きます。
ゴール: deploy ごとに「主要ルートが 200 で表示できる」を自動チェック
Smoke Test の役割はシンプルで、 「とりあえず主要ページが 500 にならないか」 だけを確認します。 機能テストではなく、 大きな崩壊を検出するためのもの。
ターゲットルートはぼくの場合こんな感じ:
const PUBLIC_ROUTES = [
{ path: "/", name: "トップ" },
{ path: "/faq", name: "FAQ 一覧" },
{ path: "/news", name: "お知らせ一覧" },
{ path: "/login", name: "ログイン" },
{ path: "/register", name: "会員登録" },
{ path: "/reset-password", name: "パスワードリセット" },
{ path: "/terms", name: "利用規約" },
{ path: "/privacy", name: "プライバシーポリシー" },
{ path: "/sell", name: "売る(撮影入口)" },
];公開ルートのみ、 認証必要なページ(/mypage/* 等)は MVP 範囲外にしました。 まずは「render エラーが出てないこと」だけ。
Step 1: Playwright 導入と smoke spec
npm install --save-dev @playwright/test
npx playwright install chromiumChromium だけ落とすので 90 MB ちょいで済みます。
tests/e2e/smoke.spec.ts はこんな感じ:
import { test, expect } from "@playwright/test";
const PUBLIC_ROUTES = [/* 上記 */];
test.describe("公開ルート Smoke Test", () => {
for (const route of PUBLIC_ROUTES) {
test(`${route.path} (${route.name}) が 200 で表示される`, async ({ page }) => {
const response = await page.goto(route.path);
expect(response?.status()).toBeLessThan(400);
const bodyText = await page.locator("body").textContent();
expect(bodyText?.trim().length ?? 0).toBeGreaterThan(0);
});
}
});
test("存在しないルートが 404 を返す", async ({ page }) => {
const response = await page.goto("/this-route-does-not-exist-9b3f");
expect(response?.status()).toBe(404);
});ステータスコード + body が空じゃないこと、 だけ確認。 これで「白画面 render エラー」も検出できます。
Step 2: playwright.config.ts で baseURL を環境変数化
ローカルでは http://localhost:3000、 CI では Vercel preview URL を相手にしたい。 だから baseURL を env で切り替え:
export default defineConfig({
testDir: "./tests/e2e",
use: {
baseURL: process.env.E2E_BASE_URL ?? "http://localhost:3000",
trace: "on-first-retry",
extraHTTPHeaders: process.env.VERCEL_PROTECTION_BYPASS
? {
"x-vercel-protection-bypass": process.env.VERCEL_PROTECTION_BYPASS,
"x-vercel-set-bypass-cookie": "true",
}
: undefined,
},
webServer: process.env.E2E_BASE_URL
? undefined // 外部 URL モードでは dev server 立てない
: { command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: true },
});extraHTTPHeaders の部分が今回の肝で、 Vercel Deployment Protection が掛かった preview に対して bypass token を header で渡す仕組みです。
Step 3: GitHub Actions の deployment_status トリガ
# .github/workflows/e2e.yml
name: E2E
on:
deployment_status:
concurrency:
group: e2e-${{ github.event.deployment.environment }}-${{ github.event.deployment.sha }}
cancel-in-progress: true
jobs:
e2e:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with: { ref: ${{ github.event.deployment.sha }} }
- uses: actions/setup-node@v4
with: { node-version: "24", cache: "npm" }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run e2e
env:
E2E_BASE_URL: ${{ github.event.deployment_status.target_url }}
VERCEL_PROTECTION_BYPASS: ${{ secrets.VERCEL_PROTECTION_BYPASS }}ポイントは on: deployment_status。 Vercel が GitHub に「deployment_status: success」を通知すると、 ここで E2E が走ります。 CI のジョブとは独立してて、 CI が失敗してもデプロイが終わってれば E2E は走る。 これが地味に便利。
E2E_BASE_URL は webhook の payload から target_url(= preview URL)を拾うだけ。 ぼくが何もしなくても、 deploy するたびに最新の preview に対して E2E が走ります。
Step 4: Vercel Deployment Protection Bypass Secret を CLI で発行
ぼくの環境では preview にも Vercel Deployment Protection(SSO 経由のみアクセス可)を掛けてあるので、 Playwright から普通にアクセスすると 全部 401。 これを bypass するために、 Vercel API で Bypass Secret を発行します。
ダッシュボードで Generate も可能ですが、 CLI 完結したかったので Vercel API を直叩きしました。
VERCEL_TOKEN=$(jq -r '.token' ~/Library/Application\ Support/com.vercel.cli/auth.json)
PROJECT_ID="prj_xxxx"
TEAM_ID="team_xxxx"
# 既存 secret 確認
curl -sH "Authorization: Bearer $VERCEL_TOKEN" \
"https://api.vercel.com/v9/projects/$PROJECT_ID?teamId=$TEAM_ID" \
| jq '.protectionBypass'protectionBypass は object で、 key 自体が secret 値になってる構造です。 ここちょっとトラップで、 jq '.protectionBypass | keys' を叩くと secret 値がコンソールに出てきます。 ローカルなら問題ないけど、 ログ共有時は要注意。
新規発行 + 既存 revoke を 1 リクエストで完了させる API はこれ:
EXISTING=$(curl -sH "Authorization: Bearer $VERCEL_TOKEN" \
"https://api.vercel.com/v9/projects/$PROJECT_ID?teamId=$TEAM_ID" \
| jq -r '.protectionBypass | keys | .[0]')
jq -n --arg sec "$EXISTING" '{revoke: {secret: $sec, regenerate: true}}' \
| curl -X PATCH \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
"https://api.vercel.com/v9/projects/$PROJECT_ID/protection-bypass?teamId=$TEAM_ID" \
-d @-revoke.regenerate: true がポイントで、 「これを revoke して、 ついでに新しいの発行して」 を 1 リクエストにまとめてます。 漏洩したときも同じコマンド一発でローテートできる。
Step 5: 新 secret を GitHub Secrets に登録(値を晒さず)
gh CLI で登録します。 ただ --body "<value>" で渡すと、 プロセス引数に値が乗って ps eww で一瞬見える可能性があるので、 stdin pipe で渡すのが安全:
curl -sH "Authorization: Bearer $VERCEL_TOKEN" \
"https://api.vercel.com/v9/projects/$PROJECT_ID?teamId=$TEAM_ID" \
| jq -r '.protectionBypass | keys | .[0]' \
| gh secret set VERCEL_PROTECTION_BYPASS --repo owner/repoこれで GitHub Actions の ${{ secrets.VERCEL_PROTECTION_BYPASS }} から参照できる状態に。 値はコンソールにもプロセス引数にも一切出てません。
Step 6: 失敗時に Slack へ通知
失敗を検出するだけだと結局見落とすので、 失敗時 Slack 通知も入れました。
- name: Notify Slack on failure
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ secrets.E2E_SLACK_WEBHOOK_URL }}
run: |
curl -X POST -H 'Content-type: application/json' --data "$(cat <<EOF
{
"text": "🚨 E2E Smoke 失敗",
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": "🚨 E2E Smoke 失敗" } },
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Environment:*\n${{ github.event.deployment.environment }}" },
{ "type": "mrkdwn", "text": "*Commit:*\n\`${{ github.event.deployment.sha }}\`" },
{ "type": "mrkdwn", "text": "*Deploy URL:*\n<${{ github.event.deployment_status.target_url }}|Open>" },
{ "type": "mrkdwn", "text": "*Workflow:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
]
}
]
}
EOF
)" "$SLACK_WEBHOOK_URL"ヘッダー + 4 フィールドの Block Kit メッセージ。 失敗時のみ送信、 成功はノイズになるので送らない。 Slack App はぼくの場合「Alerts」という名前で Sentry 通知と同じチャンネルに集約しました。
動作確認
ここまでセットして、 develop に空 commit を一発:
git commit --allow-empty -m "chore: trigger first E2E" && git pushVercel が preview を deploy → GitHub に deployment_status を投げる → E2E workflow が起動 → 全テスト通過。 約 1m30s で結果が出ました。
E2E workflow が走らないとき
- Vercel と GitHub の連携設定で「Deployment status」イベントが有効か?
if: github.event.deployment_status.state == 'success'で 'success' 以外を弾いてるか?- preview の URL で
target_urlが正しく取れてるか? (CI ログでE2E_BASE_URLを echo するとデバッグしやすい)
ぼくは最初の 1 回、 「success」ではなく「in_progress」のイベントも拾ってしまって target_url が空になる事故がありました。
教訓
- deploy 後の E2E は
deployment_statuswebhook がいちばん相性いい。 CI と独立して走るので、 CI が落ちてても deploy された preview に対して走る。 - Vercel Protection Bypass は CLI 完結できる。 ダッシュボード作業ゼロでローテート可能。
- secret は
--bodyではなく stdin pipeで渡す。 これだけで漏洩リスクが大幅に下がる。 - Slack 通知は失敗時のみ。 全 deploy 通知すると見なくなるので、 「焦るやつだけ送る」設計。
まとめ
ここまで全部含めて、 セットアップから動作確認まで 約 1 時間。 ローカルの型チェック + build が通っただけでは捕まえられない事故を、 deploy 直後に自動検出できる体制ができました。
これからは「develop に push → preview 更新 → E2E 自動実行 → 緑 or Slack 通知」のサイクルが回ります。 安心感が一段違います。
次の Part では、 一連の作業中に CI を一度落として、 そこから npm audit の high 脆弱性を全部潰した話を書きます。 package.json の overrides で Next.js の中の古い postcss を上書きする小技も。