From f90eaa5e37067e74682f7dcd24a76da61d15675d Mon Sep 17 00:00:00 2001 From: Shinya Oguri Date: Sun, 16 Aug 2026 02:51:29 +0900 Subject: [PATCH 1/2] =?UTF-8?q?ci(repo):=20deploy=20=E3=81=AE=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=88=97=E7=9B=B4=E5=B1=95=E9=96=8B=E3=81=A8=20permis?= =?UTF-8?q?sions=20=E3=82=92=E7=B7=A0=E3=82=81=E3=80=81Playwright=20?= =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=81=A8=20e2e=20?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E4=BC=9D=E6=92=AD=E3=82=92=E8=B6=B3?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #239 (3 点): 1. PR プレビューの pages deploy が github.head_ref を command 文字列へ直接 展開していた。wrangler-action は command を child_process.exec() に渡す = /bin/sh が解釈するため、`;` `$` バッククォートを含むブランチ名で注入が 成立しうる (同 repo の push 権限が要るので現実的な脅威は事故としての破損の ほう)。env 経由でシェル変数として渡し、参照はクォートする。同じ理由で github-script の command-output も process.env 参照に変えた (wrangler 出力に バッククォートや ${ が混ざるとスクリプトごと壊れて job が落ちる)。 2. top-level permissions が無く deploy.yml だけが取り残されていた (labeler / pr-title / freshness は宣言済み)。contents: read を明示する。 リポジトリ設定は既に default_workflow_permissions: read なので実効の変化は 無く、目的は明示性と将来の設定変更に対する防御。job level の 3 箇所は据え置き。 3. Playwright ブラウザのキャッシュが無く毎回 chromium を DL していた (リポジトリ全体に actions/cache がゼロ)。~/.cache/ms-playwright を npx playwright --version の実解決バージョンでキーして復元する。--with-deps が 入れる OS パッケージはキャッシュ対象外なので install 自体は毎回走らせる。 #198: playwright.config の webServer が port: を宣言しているのに、起動コマンドへ ポートを一切渡していなかった。E2E_EDITOR_PORT を指定しても実サーバは既定ポートに 上がり、reuseExistingServer 時は既定ポートの無関係プロセスを自分のサーバと誤認 しうる (port: 指定では TCP 応答しか見ない)。scripts/dev.mjs と同じ配線に揃え、 editor は env (vite.config 側の受け口は実装済みだった)、workers は wrangler が env を読まないため --port で渡す。ADR-0030 の自動割当は npm run dev 経路にしか 効かないため #196 では解消していなかった。CI は reuseExistingServer: false なので 影響はローカル開発のみ。 Closes #239 Closes #198 Assisted-by: Claude --- .github/workflows/deploy.yml | 44 +++++++++++++++++++++++++++++-- packages/e2e/playwright.config.ts | 19 ++++++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 93941de..0d38d13 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,6 +7,12 @@ on: pull_request: branches: [main] +# デフォルトは read-only。デプロイ等が必要な job は各 job の permissions で個別に昇格する +# (161 / 260 / 321 行目付近)。リポジトリ設定は既に default_workflow_permissions: read だが、 +# 将来の設定変更に対する防御として明示する。 +permissions: + contents: read + # ref ごとに 1 run。PR は新しい push で古い run をキャンセル (CI 節約)。 # push (= deploy) はキャンセルせずキューイングし、最新 run が最後に走るので # 「古い deploy が後から完了して新しい成果物を上書きする」競合を防ぐ。 @@ -119,6 +125,26 @@ jobs: - name: Provision E2E env (test keys + 生成 checkpoint 鍵) run: npm run setup -w @typedcode/e2e + # キーには Playwright のバージョンを含める。package-lock.json のハッシュだけだと + # Playwright と無関係な依存更新でもキャッシュが無効化されてしまうため、 + # `npx playwright --version` (インストール済み @playwright/test の CLI) が + # 報告する実バージョンを切り出してキーに使う (package.json の ^1.62.0 のような + # 範囲指定ではなく、実際に解決されたバージョンで判定するため)。 + - name: Get installed Playwright version + id: playwright-version + run: echo "version=$(npx playwright --version | sed 's/Version //')" >> "$GITHUB_OUTPUT" + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + restore-keys: | + playwright-browsers-${{ runner.os }}- + + # キャッシュヒット時も install は実行したままにする。ブラウザ本体が揃っていれば + # 即座に終わる一方、--with-deps が入れる OS 依存パッケージ (apt) はキャッシュの + # 対象外 (~/.cache/ms-playwright に含まれない) なので毎回必要。 - name: Install Playwright browser run: npx playwright install --with-deps chromium @@ -191,19 +217,33 @@ jobs: - name: Deploy to Cloudflare Pages (Preview) id: preview-deploy uses: cloudflare/wrangler-action@v4 + # wrangler-action は command: を child_process.exec() に渡す = /bin/sh が解釈する。 + # よって ${{ }} を直接展開すると値がそのままシェルコマンド文字列に埋め込まれる。 + # head_ref (ブランチ名) は `;` `$` バッククォート等を含み得るため、env: 経由で + # シェル変数として渡し、command: 側は変数参照のみにする。参照は必ずクォートする + # (未クォートだと語分割とグロブ展開が残り、対策が半端になる)。 + env: + PREVIEW_BRANCH: ${{ github.head_ref }} + CF_PROJECT_NAME: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} + COMMIT_SHA: ${{ github.sha }} with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # --commit-message を明示的に ASCII で渡す。CF Pages API は非 ASCII # コミットメッセージを 8000111 で拒否することがあるため (wrangler issue)、 # SHA を message として送って追跡性を保つ。 - command: pages deploy ./dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.head_ref }} --commit-hash=${{ github.sha }} --commit-message="${{ github.sha }}" + command: pages deploy ./dist --project-name="$CF_PROJECT_NAME" --branch="$PREVIEW_BRANCH" --commit-hash="$COMMIT_SHA" --commit-message="$COMMIT_SHA" - name: Comment PR with preview URL uses: actions/github-script@v9 + # wrangler の出力にバッククォートや `${` が混ざると、テンプレートリテラルへの + # 直接展開ではスクリプトごと壊れて job が落ちる。env: 経由で渡し、 + # process.env 参照にすることで出力内容に関わらず安全に扱う。 + env: + PREVIEW_DEPLOY_OUTPUT: ${{ steps.preview-deploy.outputs.command-output }} with: script: | - const output = `${{ steps.preview-deploy.outputs.command-output }}`; + const output = process.env.PREVIEW_DEPLOY_OUTPUT; const urlMatch = output.match(/https:\/\/[^\s]+\.pages\.dev/); const previewUrl = urlMatch ? urlMatch[0] : null; diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts index 04cb375..c16898d 100644 --- a/packages/e2e/playwright.config.ts +++ b/packages/e2e/playwright.config.ts @@ -5,9 +5,9 @@ import { defineConfig, devices } from '@playwright/test'; * round-trip を基本形にする (UI の見た目ではなく、暗号的に検証可能な成果物を * オラクルにする)。詳細は packages/e2e/README.md。 * - * 2 つのローカルサーバを起動する: - * - workers (wrangler dev :8787): /api/session/start・/api/verify-captcha・署名 CP - * - editor (vite dev :5173): テスト対象アプリ + * 2 つのローカルサーバを起動する (ポートは既定値。E2E_EDITOR_PORT / E2E_WORKERS_PORT で上書き可能): + * - workers (wrangler dev 既定 :8787): /api/session/start・/api/verify-captcha・署名 CP + * - editor (vite dev 既定 :5173): テスト対象アプリ * * editor/.env は既に Cloudflare の Turnstile テストキー (1x...AA = 常に pass) と * VITE_API_URL=http://localhost:8787 を指すため、追加設定なしでフルスタックが回る。 @@ -64,10 +64,17 @@ export default defineConfig({ }, ], + // `port:` は TCP 応答しか見ないため、既定ポートを別プロセスが握っていると + // reuseExistingServer 時に「自分のサーバ」と誤認しうる (ローカル開発時のみ影響、 + // CI は reuseExistingServer: false)。起動コマンド自体にもポートを渡し、 + // E2E_EDITOR_PORT / E2E_WORKERS_PORT を指定したときに実際にそのポートで + // 上がるようにする (指定しない限りは無指定と同じ既定ポートに上がる)。 + // 配線は scripts/dev.mjs (118-145 行目) と同じパターンを踏襲する。 webServer: [ { name: 'workers', - command: 'npm run dev -w @typedcode/workers', + // wrangler dev は env を読まないため、コマンド側に --port を渡す。 + command: `npm run dev -w @typedcode/workers -- --port ${WORKERS_PORT}`, cwd: '../..', port: WORKERS_PORT, reuseExistingServer: !isCI, @@ -78,6 +85,10 @@ export default defineConfig({ { name: 'editor', command: 'npm run dev -w @typedcode/editor', + // packages/editor/vite.config.ts が EDITOR_PORT (server.port + strictPort) と + // WORKERS_PORT (VITE_API_URL 追従) を既に読む実装になっている。受け口は + // 実装済みで、これまで渡していなかっただけなので env で渡す。 + env: { EDITOR_PORT: String(EDITOR_PORT), WORKERS_PORT: String(WORKERS_PORT) }, cwd: '../..', port: EDITOR_PORT, reuseExistingServer: !isCI, From fe81de5440728f60472676bcecae99e75905ba56 Mon Sep 17 00:00:00 2001 From: Shinya Oguri Date: Sun, 16 Aug 2026 03:00:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci(repo):=20wrangler-action=20=E3=81=AE=20c?= =?UTF-8?q?ommand=20=E3=81=AF=20${{=20}}=20=E7=9B=B4=E6=8E=A5=E5=B1=95?= =?UTF-8?q?=E9=96=8B=E3=81=AB=E6=88=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前コミットで #239 の指摘どおり env 経由に変えたが、CI の実測でこれが誤りと判明 した。wrangler-action v4 は command: をシェルに渡さずトークン分割して npx を 直接 exec するため、$CF_PROJECT_NAME が展開されず wrangler がプロジェクト名と して文字列 "$CF_PROJECT_NAME" を受け取り deploy-preview が失敗した。 同時にこれは #239 子項目① の前提そのものを崩す。シェルが介在しないので head_ref に含まれうる `;` `$` バッククォートは不活性で、git はブランチ名に空白を 許さないため引数分割も起きない。command: 側にコマンドインジェクションは無い。 一方 github-script の command-output は本物のテンプレートリテラル評価なので env 経由のまま維持する (wrangler 出力にバッククォートや ${ が混ざると スクリプトごと壊れて job が落ちる)。 次に読む人が同じ「修正」を当てて再びデプロイを壊さないよう、実測の根拠を command: の直前にコメントで残した。 Assisted-by: Claude --- .github/workflows/deploy.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0d38d13..90d3fcb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -217,22 +217,22 @@ jobs: - name: Deploy to Cloudflare Pages (Preview) id: preview-deploy uses: cloudflare/wrangler-action@v4 - # wrangler-action は command: を child_process.exec() に渡す = /bin/sh が解釈する。 - # よって ${{ }} を直接展開すると値がそのままシェルコマンド文字列に埋め込まれる。 - # head_ref (ブランチ名) は `;` `$` バッククォート等を含み得るため、env: 経由で - # シェル変数として渡し、command: 側は変数参照のみにする。参照は必ずクォートする - # (未クォートだと語分割とグロブ展開が残り、対策が半端になる)。 - env: - PREVIEW_BRANCH: ${{ github.head_ref }} - CF_PROJECT_NAME: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} - COMMIT_SHA: ${{ github.sha }} with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # --commit-message を明示的に ASCII で渡す。CF Pages API は非 ASCII # コミットメッセージを 8000111 で拒否することがあるため (wrangler issue)、 # SHA を message として送って追跡性を保つ。 - command: pages deploy ./dist --project-name="$CF_PROJECT_NAME" --branch="$PREVIEW_BRANCH" --commit-hash="$COMMIT_SHA" --commit-message="$COMMIT_SHA" + # + # ここは ${{ }} の直接展開のままでよい (#239 で「env 経由に変えるべき」と + # 指摘されたが、実測の結果その前提が誤りだった)。wrangler-action v4 は + # command: を **シェルに渡さず**トークン分割して npx を直接 exec する。 + # 実測: env で $CF_PROJECT_NAME を渡すと wrangler がプロジェクト名として + # 文字列 "$CF_PROJECT_NAME" を受け取り deploy が失敗する。 + # シェルが介在しないので head_ref の `;` `$` バッククォートは不活性であり、 + # git はブランチ名に空白を許さないため引数分割も起きない。 + # **env 経由に「直す」とデプロイが壊れる。** + command: pages deploy ./dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.head_ref }} --commit-hash=${{ github.sha }} --commit-message="${{ github.sha }}" - name: Comment PR with preview URL uses: actions/github-script@v9