この記事でわかること:設定の全体像と結論
pnpm monorepo GitHub Actions CI/CD設定を正しく動かすには、pnpmのセットアップ・workspaceのビルド順序・Cloudflare側の認証情報という3つの要素を正確に組み合わせる必要がある。以下のYAMLは、packages/yarutokoなどを持つpnpm workspaceのmonorepoでCloudflare Pagesへの自動デプロイを実際に通過したワークフローをベースにしている。動作には次の4点が揃っている必要がある。
- pnpm 9.x:
package.jsonに"packageManager": "pnpm@9.x.x"フィールドの記載が必須 - Node.js 20.x:
actions/setup-nodeで指定 - Cloudflareアカウント:Dashboard上でPagesプロジェクトを作成済みであること
- GitHubシークレット:
CLOUDFLARE_API_TOKENとCLOUDFLARE_ACCOUNT_IDの両方を登録済みであること
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# pnpm セットアップ(setup-node より前に置く)
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
# shared パッケージを先にビルドしておく
- name: Build shared
run: pnpm --filter shared build
# デプロイ対象アプリをビルド
- name: Build yarutoko
run: pnpm --filter yarutoko build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=yarutoko
monorepo自動デプロイで詰まる場面は、次の6パターンに集約できる。自分のエラーログや症状と照らし合わせることで、参照すべきセクションを素早く特定できる。
- pnpm/action-setup が未設定:
actions/setup-nodeだけではpnpmコマンドが使えず、ワークフロー起動直後にエラーで終了する - packageManagerフィールドの未記載:
package.jsonに"packageManager"がないとpnpm/action-setupがバージョンを解決できない - CLOUDFLARE_ACCOUNT_IDの設定漏れ:
CLOUDFLARE_API_TOKENだけではwrangler pages deployが404エラーを返してデプロイが停止する - sharedパッケージのビルド順序ミス:sharedを先にビルドしないと、依存しているアプリ側でモジュール解決エラーが発生する
- –filterのパッケージ名ミス:
package.jsonの"name"フィールドと一致していない場合、フィルターが空振りしてビルドがスキップされる - –frozen-lockfileの未指定:CI環境で
pnpm installを素のまま実行するとlockfileの差分により依存解決が失敗するケースがある
pnpm monorepo GitHub Actions CI/CD設定の基本構成
Q. pnpmを使ったmonorepoをCI/CDに接続するとき、なぜnpmやyarnより設定が手間になるのですか?
A. pnpmはNode.jsに同梱されていないため、CI環境では毎実行ごとにインストールが必要です。加えて、workspace間の依存パッケージのビルド順を自分で制御しなければならない点がnpmとの最大の違いです。この2点を押さえないまま進めると、ローカルでは動くのにCIだけ失敗する状況が続きます。
pnpm workspaceは pnpm-workspace.yaml で複数パッケージを束ねる構成です。たとえば packages/shared(共通ロジック)・packages/yarutoko・packages/lifeevent-hub という3パッケージ構成では、sharedに依存するアプリをビルドする前に、shared自身のビルドを完了させる必要があります。GitHub Actionsはこのビルド順を自動解決しないため、pnpm --filter で実行順を明示的に記述するのが前提になります。
ワークフローYAMLの骨格は「on(トリガー)」「jobs(実行環境)」「steps(処理単位)」の3層で設計します。on には push と pull_request を並列定義し、mainへのマージ時とPRレビュー時の双方でCIが動く構成が基本です。jobsは最初は1つに集約し、ビルド時間が伸びてから並列化を検討するのが現実的な判断基準です。
pnpmセットアップワークフローで最初につまずくのが pnpm/action-setup の追加漏れです。actions/setup-node だけではpnpmコマンドは使えません。あわせて package.json の "packageManager" フィールドにバージョンを明記しておくと、CI環境とローカルのバージョン不一致を防げます。以下が推奨セットアップパターンです。
name: CI / Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# actions/setup-node だけでは pnpm は使えない
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm' # lockfile ベースでキャッシュ
# lockfile に差分があれば CI を失敗させる
- run: pnpm install --frozen-lockfile
# 共通パッケージを先にビルド(順序が重要)
- run: pnpm --filter shared build
# アプリ本体をビルド
- run: pnpm --filter yarutoko build
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # 両方必須
projectName: yarutoko
directory: packages/yarutoko/dist
YAMLの accountId 行で参照している CLOUDFLARE_ACCOUNT_ID をGitHub Secretsに登録していない場合、Wranglerは 404 Not Found を返してデプロイが停止します。CLOUDFLARE_API_TOKEN と CLOUDFLARE_ACCOUNT_ID の2つが揃って初めて動作します。アカウントIDはCloudflareダッシュボードの右サイドバー「Account ID」欄から取得できます。
GitHub ActionsでpnpmをCI/CD設定する詳細手順
Q: pnpm/action-setup のバージョン指定で、確実な方法を教えてください。
A: 最も再現性が高い方法は、package.json の packageManager フィールドに使用する pnpm のバージョンを明記することです。"packageManager": "pnpm@9.1.4" と記載しておけば、pnpm/action-setup@v4 はそのバージョンを自動的に参照します。Action 側の version パラメータを省略でき、ローカルと CI 環境で同一バージョンが保証されます。
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
# version を省略 → package.json の packageManager フィールドから自動解決
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm' # pnpm-lock.yaml のハッシュでキャッシュキーを自動生成
Q: monorepo のキャッシュ戦略で、ビルドを速くするにはどう設定すればいいですか?
A: actions/setup-node の cache: 'pnpm' を使うと、pnpm-lock.yaml のハッシュをキーに ~/.local/share/pnpm/store を自動でキャッシュします。monorepo では各パッケージの package.json が更新されれば pnpm-lock.yaml も変化するため、この仕組みで通常十分です。ロックファイルに変更がないジョブでは pnpm install が数秒で完了し、毎回ゼロからダウンロードする場合と比べてビルド時間を大幅に削減できます。
より細かく制御したい場合は、actions/cache を直接使うことでキャッシュキーを柔軟に設定できます。
- uses: actions/cache@v4
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
Q: workspace 全体に pnpm install を走らせるか、--filter で絞るか、どう使い分ければいいですか?
A: インストールは workspace 全体に対して行い、ビルドとデプロイだけを --filter で絞るのが基本方針です。--filter でインストール対象を絞ると、workspace 間の依存解決が不完全になるケースがあります。lifeevent-tools の構成では、以下の順序でステップを定義しました。
pnpm install(フィルターなし:workspace 全体の依存を解決)pnpm --filter lifeevent-hub build(共有パッケージを先行ビルド)pnpm --filter yarutoko build(アプリのビルド)wrangler pages deploy(Cloudflare Pages へのデプロイ)
ビルド順を誤ると、lifeevent-hub の型定義やエクスポートが見つからず yarutoko のビルドが即座に失敗します。この順序制御を YAML 上で --filter を使って明示的に記述する点が、pnpm monorepo GitHub Actions CI/CD 設定における最重要箇所です。
Cloudflare Pages自動デプロイの設定手順
Cloudflare Pages への自動デプロイを GitHub Actions から実行するには、CLOUDFLARE_ACCOUNT_ID と CLOUDFLARE_API_TOKEN の2つを GitHub Secrets に登録することが前提条件です。API トークンだけを設定した状態でデプロイを実行すると、404 エラーで失敗します。この挙動は Cloudflare 公式ドキュメント(Cloudflare Pages / CI/CD integration)にも明記されており、初回セットアップで最もつまずきやすいポイントです。
CLOUDFLARE_ACCOUNT_IDとAPIトークンの取得・GitHub Secrets登録
Q:CLOUDFLARE_ACCOUNT_ID はどこで確認できますか?
A:Cloudflare ダッシュボードにログイン後、右サイドバーの「Account ID」欄に 32 桁の文字列として表示されます。URL にも含まれており、dash.cloudflare.com/の形式でも確認できます。Q:API トークンの権限スコープはどこまで必要ですか?
A:Cloudflare Pages へのデプロイのみを目的とする場合は、「Cloudflare Pages: Edit」権限だけで十分です。過剰なスコープを付与しないことがセキュリティ上のベストプラクティスです。
GitHub Secrets への登録手順は次のとおりです。
- Cloudflare ダッシュボードの右サイドバーから Account ID をコピーする
- 「My Profile」→「API Tokens」→「Create Token」を開き、Cloudflare Pages: Edit テンプレートを選択してトークンを発行する
- GitHub リポジトリの「Settings」→「Secrets and variables」→「Actions」→「New repository secret」から以下の2つを登録する
CLOUDFLARE_ACCOUNT_ID:手順1でコピーした Account IDCLOUDFLARE_API_TOKEN:手順2で発行したトークン
cloudflare/pages-actionの正しい設定方法と注意点
公式の cloudflare/pages-action@v1 を使う場合、directory(ビルド成果物のパス)の指定が必須です。directory を省略または誤指定すると、空のディレクトリがデプロイされ、本番で真っ白なページが公開されます。gitHubToken を渡すと、プルリクエスト上にプレビュー URL が自動表示され、レビューフローが大幅に改善されます。
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: yarutoko # Cloudflare Pages のプロジェクト名
directory: packages/yarutoko/dist # ビルド成果物のパス(要確認)
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
wranglerVersion: '3' # wrangler v3 を明示指定
projectName は Cloudflare ダッシュボードの Pages プロジェクト名と完全一致させる必要があります。大文字・小文字も区別されるため、ダッシュボードからコピーして貼り付けるのが確実です。
monorepo複数パッケージの差分デプロイ設計
lifeevent-tools のような pnpm monorepo 構成では、変更のあったパッケージのみをビルド・デプロイする設計が必要です。全パッケージを毎回デプロイすると CI 時間とコストが無駄になります。pnpm --filter でビルド対象を絞り込み、GitHub Actions の paths フィルタで発火条件を制御する2段構えが実務上のベストプラクティスです。
on:
push:
branches: [main]
paths:
- 'packages/yarutoko/**'
- 'packages/lifeevent-hub/**' # 依存 shared パッケージも監視
jobs:
deploy-yarutoko:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# 依存する shared パッケージを先にビルド(順序厳守)
- run: pnpm --filter lifeevent-hub build
# アプリ本体をビルド
- run: pnpm --filter yarutoko build
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: yarutoko
directory: packages/yarutoko/dist
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
新しいパッケージを monorepo に追加した場合は、paths フィルタと --filter 指定の両方に追記が必要です。この2箇所の同期を忘れると、コードを変更したのにデプロイが走らない、または意図しないパッケージのデプロイが発火するという問題に発展します。変更時はセットで確認する運用ルールをチーム内で決めておくと安全です。
pnpm monorepo CI/CDで詰まるポイントと解決策
Q: 最初にハマったのはどこでしたか?
最初の壁は pnpm のセットアップそのものでした。actions/setup-node だけでワークフローを走らせると、即座に次のエラーが出ます。
sh: pnpm: not found
Error: Process completed with exit code 127.
原因は actions/setup-node が Node.js しか導入しないためです。pnpm を使うには pnpm/action-setup を別途追加し、さらに package.json に "packageManager": "pnpm@9.x.x" を記載する必要があります。この2点がセットでなければ、corepack が有効な環境では別のエラー(ERR_PNPM_NO_PKG_MANAGER_INSTALLED)も発生します。
# ワークフロー冒頭に必ず両方を記載する
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
Q: Cloudflare Pages へのデプロイで「404」が出た経験はありますか?
あります。CLOUDFLARE_API_TOKEN だけを GitHub Secrets に登録してデプロイを走らせると、次のエラーで止まります。
✘ [ERROR] A request to the Cloudflare API failed.
GET .../accounts/undefined/pages/projects/...
[code: 7003] Could not route to /accounts/undefined/pages/projects
wrangler は CLOUDFLARE_API_TOKEN と CLOUDFLARE_ACCOUNT_ID の両方を必要とします。Account ID はCloudflare ダッシュボードの右サイドバー「Account ID」欄から取得し、CLOUDFLARE_ACCOUNT_ID という名前でリポジトリの Secrets に登録します。公式の「Get started」ガイドにはトークンの取得手順しか目立って書かれておらず、Account ID は見落としやすい箇所です。
Q: monorepo 特有の詰まりはありましたか?
shared パッケージへの依存順序は一番気づきにくい問題でした。packages/shared に型定義やユーティリティを置いている構成では、ビルド前にそのパッケージが存在しない状態でアプリをビルドしようとして次のエラーが出ます。
Cannot find module '@lifeevent/shared'
Module not found: Error: Can't resolve '@lifeevent/shared/utils'
解決策は、ワークフロー内でビルドステップを依存関係の順に明示的に分割することです。pnpm --filter shared build を先に実行し、その後に pnpm --filter yarutoko build を走らせます。pnpm -r build のような再帰実行はトポロジカルソートを行いますが、Workspace の依存宣言(workspace:*)が正しく書かれていないと順序が保証されないため、CI では明示的な順序指定が安全です。
下表に6大詰まりポイントを原因・エラー・解決コマンドのセットでまとめます。
| # | 詰まりポイント | 代表エラー | 解決策 |
|---|---|---|---|
| ① | pnpm コマンドが見つからない | pnpm: not found (exit 127) | pnpm/action-setup を追加 |
| ② | packageManager フィールド未記載 | ERR_PNPM_NO_PKG_MANAGER_INSTALLED | package.json に "packageManager": "pnpm@9.x.x" を記載 |
| ③ | CLOUDFLARE_ACCOUNT_ID 未設定 | /accounts/undefined/pages/… [code: 7003] | Secrets に CLOUDFLARE_ACCOUNT_ID を追加 |
| ④ | shared パッケージ未ビルド | Cannot find module ‘@xxx/shared’ | pnpm --filter shared build を先行実行 |
| ⑤ | ロックファイル不整合 | ERR_PNPM_OUTDATED_LOCKFILE | pnpm install --frozen-lockfile で CI 実行し、ローカルでロックファイルを更新してプッシュ |
| ⑥ | paths フィルタと –filter の不一致 | (エラーなし・サイレント失敗) | パッケージ追加時に paths と --filter を同時更新 |
⑤のロックファイル不整合は「ローカルでは動くのに CI だけ落ちる」典型パターンです。pnpm install をローカルで実行した際に pnpm-lock.yaml の更新をコミットし忘れると、--frozen-lockfile オプションによって CI が意図通り失敗します。これは CI の正常動作であり、ロックファイルをプッシュすれば解消します。⑥はエラーメッセージが出ない分だけ発見が遅れます。act(GitHub Actions のローカル実行ツール)で事前検証する運用を入れると、プッシュ前に気づけます。
本番運用で役立つGitHub Actionsベストプラクティス
「CI/CDが動いた」から「本番で壊れない運用ができている」への距離は、意外と短い。pnpm monorepo GitHub Actions CI/CD 設定を通じて実際に詰まりを経験したエンジニアへのヒアリングでは、以下の3点が「後から追加したのではなく、最初から入れておけばよかった」設定として繰り返し挙がった。
① concurrencyで二重デプロイを防ぐ
Q: 本番稼働後に最初に当たったトラブルは何でしたか?
A: 同じブランチへ連続でプッシュしたとき、前のデプロイジョブが走ったまま次のジョブが始まって、Cloudflare Pages上の状態が壊れました。concurrencyを入れていなかったのが原因です。
concurrencyキーにcancel-in-progress: trueを指定すると、同じグループのジョブが実行中に新しいトリガーが入ったとき、古いジョブを自動キャンセルします。グループキーにgithub.refを含めることで、mainブランチとfeature/*ブランチが互いにキャンセルし合わない点も重要です。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
② セキュリティを考慮したシークレット管理パターン
Q: CLOUDFLARE_ACCOUNT_IDの管理はどうしていましたか?
A: 最初はリポジトリシークレット1か所にまとめていましたが、本番とプレビューで使うトークンを分けるためEnvironmentシークレットに移行しました。スコープが明確になってレビューもしやすくなりました。
GitHubのシークレット管理には2段階あります。全ワークフロー共通の値はRepository secrets、production/previewなどデプロイ先ごとに分けたい値はEnvironment secretsに登録します。CloudflareのAPIトークンは「Cloudflare Pages: Edit」権限だけを持つスコープ限定トークンを発行し、Account全体への書き込みは与えないのが最小権限の原則に沿った運用です。
- Repository secrets:Slack Webhook URLなど環境を問わない共通値
- Environment secrets:
CLOUDFLARE_API_TOKEN・CLOUDFLARE_ACCOUNT_IDなど環境依存の値 - permissions キー:ワークフロー側でも
contents: readなどスコープを明示する
permissions:
contents: read
deployments: write
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
③ デプロイ失敗時のSlack通知設定
Q: 失敗に気づくまでどのくらいかかりましたか?
A: 通知を入れていなかった頃は、数時間後に本番サイトを確認して初めて気づくことがありました。if: failure()でSlack通知を入れてからは即時で検知できています。
slackapi/slack-github-actionとif: failure()の組み合わせが最短の実装経路です。通知文にgithub.run_idを埋め込んでActionsのログURLを直リンクにしておくと、Slackから一クリックで原因調査に入れます。成功通知を省いて失敗時だけ飛ばすことで、通知疲れを防ぐバランスも取れます。
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v2
with:
payload: |
{
"text": "❌ Deploy failed: ${{ github.repository }} @ ${{ github.ref_name }}\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
この3点——concurrencyによる競合排除、Environmentシークレットによるスコープ分離、失敗時限定のSlack通知——は、monorepo 自動デプロイ構成の安定稼働に直結する設定です。ワークフローの動作確認が取れた直後に追加するのが、後から修正コストをかけない最適なタイミングといえます。
まとめ:pnpm monorepo CI/CD設定を成功させるポイント
この記事では、lifeevent-toolsのようなpnpm workspace構成のmonorepoをGitHub ActionsでCloudflare Pagesに自動デプロイする手順を、実際に詰まった経験をもとに整理した。設定の核心は3点に集約される。pnpm/action-setupの追加とpackageManagerフィールドの記載、CLOUDFLARE_ACCOUNT_IDを含むシークレットの両方登録、そしてsharedパッケージを先行ビルドするための--filterによるビルド順制御だ。
「なぜCLOUDFLARE_API_TOKENだけではデプロイが通らなかったのか」——wranglerのCLIは内部でAccount IDを参照してプロジェクトを特定するため、APIトークンが正しくてもCLOUDFLARE_ACCOUNT_IDが未設定だと404エラーで止まる。Cloudflareの公式ドキュメント(Cloudflare Workers / Pages — Environment Variables)にも両方の環境変数が必要と明記されており、CI設定時の確認漏れが最も多いポイントといえる。
デプロイ前チェックリスト
- ルートの
package.jsonに"packageManager": "pnpm@x.x.x"が記載されているか pnpm/action-setupがactions/setup-nodeより前のステップに配置されているか- GitHub EnvironmentsシークレットにCLOUDFLARE_API_TOKENとCLOUDFLARE_ACCOUNT_IDの両方が登録されているか
- 依存するsharedパッケージのビルドを
--filter shared buildで先行実行しているか - デプロイ対象のアプリを
--filterで限定しているか
次に試せる発展的な設定
ワークフローが安定稼働したら、以下を追加すると運用品質が上がる。特に大規模なmonorepoでは、差分検知によるCI時間の短縮効果が大きい。
- 差分デプロイの導入:
dorny/paths-filterで変更のあったパッケージのみをビルド対象にする。変更のないパッケージのCIをスキップでき、実行時間を削減できる。 - pnpm storeのキャッシュ設定:
actions/cacheで~/.pnpm-storeをキャッシュする。GitHub Actionsの公式事例では依存インストールステップを40〜60%短縮できると報告されている。 - Preview URLのPR自動コメント:
cloudflare/pages-actionのgitHubTokenオプションを設定すると、PRにデプロイ先のPreview URLが自動的にコメントされ、レビューとデプロイ確認を一元化できる。


コメント