TurborepoでCIを爆速にしよう
手元でビルドした結果をCIがそのまま再利用、初回から1.2秒!キャッシュとCIの設定をまとめました
はじめに
どうも、Caruです!
自分のモノレポはだいたいTurborepo + pnpmで組んでいます(テンプレートから作った古いものはBunです)。手元だとキャッシュが効いてかなり速いんですが、CIはまっさらなマシンから始まるので、何もしないと毎回全部のタスクが走ります。
この記事では、自分のリポジトリで実際にやっているturbo.jsonの設定と、GitHub ActionsでCIを速くする工夫をまとめます。turbo 2.11時点の話です。
Turborepoが速いのはキャッシュのおかげ
turboはタスクを走らせる前に入力のハッシュを計算します。材料はパッケージ内のgit管理下のファイルに加えて、envやglobalEnvに書いた環境変数、globalDependencies、依存先タスクのハッシュなどです。同じハッシュのキャッシュがあればタスクは実行せず、outputsに書いたファイルを復元してログを再生します。

Bunでバイナリを作るCLIとドキュメントサイトのモノレポ(CLIリポジトリと呼びます)で測ってみます。turbo run check-types(5タスク)は、--forceでキャッシュを無視すると5.294秒、2回目は152msでした。
Tasks: 5 successful, 5 total
Cached: 5 cached, 5 total
Time: 152ms >>> FULL TURBOただしCIでは.turbo/cacheが毎回空の状態から始まります。この速さをCIでも出すには、やることが3つあります。
- キャッシュが正しく効く設定にする
- CIの実行をまたいでキャッシュを持ち越す
- 変わっていないところはそもそも動かさない
順番にいきます。
まずはキャッシュを正しく効かせる
outputsには生成物を全部書く
公式ドキュメントに「キャッシュミスのときは通るのに、ヒットすると落ちるなら、outputsの設定が間違っている可能性が高い」とあります。自分もまさにこれを踏みました。
outputsを省略するか空にすると、キャッシュされるのはログだけでファイルは復元されません。Next.jsのbuildなら[".next/**", "!.next/cache/**", "!.next/dev/**"]が定番です。.next/cacheは本番ビルドに要らず、入れてもアーティファクトが太るだけです。
自分がハマったのはcheck-typesでした。このスクリプトはCSS Modulesの型(.css-types/)とルートの型(.next/types/やnext-env.d.ts、TanStack Routerならsrc/routeTree.gen.ts)を生成してからtscを回します。生成したファイルは他のタスクやエディタも読みますし、CIではoxlintの型情報つきlintも読みます。ところがキャッシュに当たると生成ステップが走らず、outputsに入れていない限りファイルがありません。そこで"outputs": [".css-types/**", ".next/types/**", "next-env.d.ts"]のように、生成物をcheck-typesのoutputsに書いています。
パッケージの外のファイルはinputsで足す
inputsのデフォルトはパッケージ内のgit管理下ファイル全部です。外のファイルを含めたいときは、../ではなく$TURBO_ROOT$から始まるグロブで、リポジトリルートからの相対パスを書きます。inputsを書くとデフォルトは丸ごと置き換わるので、残したいなら$TURBO_DEFAULT$を一緒に書きます。
CLIリポジトリのドキュメントサイトは、リポジトリ直下のdocs/からページを作ります。そのためsite#buildのinputsは["$TURBO_DEFAULT$", "$TURBO_ROOT$/docs/**"]です。これがないと、docs/を直してもsite#buildがキャッシュに当たり、古いページのままの成果物が復元されます。
FastAPI + Next.jsのチーム開発のリポジトリでは、webのAPIクライアントを、コミット済みのapps/api/openapi.jsonからorvalで生成しています。generateタスクのinputsはorval.config.ts、package.json、$TURBO_ROOT$/apps/api/openapi.jsonの3つだけ。コミット済みのJSONから生成するので、webのビルドにPythonは要りません。openapi.jsonが古いままになっていないかは、CIで別途再生成して、差分があれば落とすようにしています。
環境変数はstrictのまま、ハッシュに入れるかどうかを選ぶ
envModeはデフォルトで"strict"です。envやglobalEnvに書いた変数だけがタスクに渡り、値はハッシュにも入ります。passThroughEnv / globalPassThroughEnvに書いた変数はタスクに渡るけれど、ハッシュには入りません。自分はdev(そもそもcache: falseです)に渡す変数と、デプロイ用のシークレットにこちらを使っています。
"loose"にすると全部の環境変数が通り、書き忘れても動きます。ただし、書いていない変数はハッシュに入りません。ドキュメントの例だと、プレビュー用のMY_API_URLでビルドしたあと本番用にビルドすると、キャッシュに当たってプレビューを向いたままの成果物が出てきます。CLIリポジトリも最初は"envMode": "loose"で、「fix turbo caching」というコミットでこの指定を外し、strictにしました。
もうひとつ、turboは.envをタスクに読み込みません。.envを変えたときにキャッシュを外したいなら、"inputs": ["$TURBO_DEFAULT$", ".env*"]のようにinputsへ足します(Bunテンプレートのリポジトリはこうしています)。ツールのバージョンはmiseで固定しているため、globalDependencies: ["mise.toml"]も入れています。これでNodeやpnpmを上げると、全タスクのハッシュが変わります。
キャッシュしないタスクもある
devはcache: falseとpersistent: true、デプロイやDBのコマンドもcache: falseです。CLIリポジトリのサーバーはBunで単一バイナリをリポジトリ直下のdist/に出すため、成果物がパッケージの外にあります。コンパイルは1秒程度なので、キャッシュする代わりにserver#buildをcache: falseにしました。"<package>#<task>"の書き方でパッケージごとに上書きできます。
効かないときは--dryと--summarize
turbo run build --dry(--dry=jsonも可)で、何が走るかとそのハッシュを実行せずに確認できます。turbo run build --summarizeは.turbo/runs/<run-id>.jsonに入力・ハッシュ・所要時間・キャッシュしたファイルを書き出します。2回分を比べれば、ハッシュが変わった理由がわかります。
CIでキャッシュを持ち越す
GitHub Actionsならactions/cacheで.turbo/cacheを保存するだけです。CLIリポジトリのワークフローから関係する部分だけ抜き出すとこうなります。
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: jdx/mise-action@v5
- run: pnpm install --frozen-lockfile
- uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: turbo-${{ runner.os }}-
- run: pnpm check:ci
- env:
TURBO_CACHE_MAX_SIZE: 1GB
run: pnpm turbo run check-types test buildkeyにコミットSHAが入っているため毎回新しいエントリが保存され、restore-keysの前方一致で直近のものが復元されます。Turborepoの公式GitHub Actionsガイドと同じパターンです(fetch-depth: 0は後述の--affected用)。
毎回.turbo/cacheを丸ごと保存する以上、放っておくと育ち続けます。TURBO_CACHE_MAX_SIZE(turbo.jsonならcacheMaxSize)を付けておくと、turbo runの開始時に上限を超えた分を古いものから消してくれます。GitHub Actions側のキャッシュはリポジトリあたりデフォルト10GBで、7日間アクセスがないエントリは消えます。PRのrunはベースブランチのキャッシュも使えます。
効果はどれくらいか
CLIリポジトリのmainへのpushで、turboのステップだけ測りました(全タスク実行、--affectedなし)。
| 変更内容 | タスク数 | キャッシュ | 時間 |
|---|---|---|---|
| actions/cache導入直後(キャッシュ空) | 11 | 0 | 1m13.9s |
| docs/とREADME、CHANGELOGだけ | 12 | 6 | 32.8s |
| サーバーのコード | 12 | 2 | 44s〜1m17s |
復元されたキャッシュは数MB〜25MBくらいでした。小さい個人リポジトリなので元から短いですが、ドキュメントだけの変更なら半分以下になります。逆にサーバーのコードを触ると多くのタスクに波及して、あまり縮みません。ここは次の--affectedの出番です。
Remote Cacheで手元とCIのキャッシュを共有する
actions/cacheはGitHub Actionsの中でしか共有できません。Remote Cacheなら、手元のマシン、CI、チームメンバー、ビルドサーバーで1つのキャッシュを共有できます。
VercelのRemote Cacheは、Vercelでホスティングしていなくても全プランで無料です(fair useの範囲で、Hobbyはアップロード月100GB、アーティファクトのリクエストは毎分100回まで)。アップロードしたアーティファクトは7日で消えます。手元ではリポジトリルートでturbo loginしてからturbo linkするだけです。.turbo/cacheを消して再実行し、アーティファクトがダウンロードされてログが再生されれば動いています。
CIにはOIDCで渡すのがおすすめ
CIで必要なのはTURBO_TEAM(チームのslug)と、Remote Cacheにアクセスするためのトークンです。CIにトークンを持たせる方法は2つあって、おすすめはOIDCです。ジョブにpermissions: id-token: writeを付けてvercel/setup-turborepo-remote-cache-actionを置くと、GitHubのOIDCトークンをTurborepo用の短命トークンに交換して、後続のステップにセットしてくれます。このトークンでできるのはRemote Cacheへのアクセスだけで、メンバー個人ではなくチームに紐づきます。アクションのv1.1.0はジョブの終わりにトークンを失効させるところまでやってくれて(revoke入力、デフォルトtrue)、ログの最後にRevoked the Turborepo access token.と出ます。
Bunテンプレートのリポジトリは、この記事を書いている途中でPATからOIDCに切り替えました。手順と結果をそのまま書いておきます。
Vercel側では先にOIDCポリシーを作ります。Hobbyプランでも使えました。場所はAll Projects → Settings → Build and Deployment → 「OIDC Policies」で、「VCR Policies」と「Turborepo CLI Policies」の行が並んでいるセクションです。ドキュメントでは「OIDC Policies for CLI Access」と呼ばれていて画面の見出しと違うので、自分はその名前で探して最初は見つけられませんでした。アクションのREADMEにある直リンクから、追加フォームを直接開くこともできます。

「Add Turborepo CLI Policy」のフォームで入れるのは、Policy Name、Provider(GitHub Actions)、Issuer URL(https://token.actions.githubusercontent.comが最初から入っています)、Workload identityのGitHubアカウントです。Repository・Workflow(Any workflow)・Branch(Any branch)とAudienceは任意です。自分はRepository・Workflow・Branchを空にして、GitHubアカウント配下の全リポジトリを対象にしました。Audienceも空です。フォームの説明に「ワークフローがgetIDToken()にカスタム値を渡すときだけ設定する」とあり、このアクションはaudienceを渡さないので、GitHubのデフォルトであるhttps://github.com/<owner>が使われます(アクションのソースで確認しました)。

アカウント全体のポリシーには注意点があります。自分が持っているどのリポジトリのワークフローでもトークンを取れます。他の人がwrite権限を持つリポジトリも含めてです。自分のところには、ほかに3人のコラボレーターがいるチームのリポジトリがあって、そこのワークフローからも共有キャッシュに書けることになります。これが気になるなら、リポジトリ単位でポリシーを作るほうが安全です。それから、複数のポリシーが同じトークンに一致すると、policy入力を渡さない限り交換がエラーになります。ポリシーは1つにしておくのが無難です。
ワークフロー側の変更はこれだけです。CIとDeployの2つのワークフローに同じ変更を入れました。
check:
runs-on: ubuntu-latest
- env:
- TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
- TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+ permissions:
+ contents: read
+ id-token: write
steps:
- uses: actions/checkout@v5
+ - uses: vercel/[email protected]
+ with:
+ team: ${{ vars.TURBO_TEAM }}
+リポジトリ変数のTURBO_TEAMはもともとあったのでそのままです。TURBO_TOKENのシークレットは使わなくなったので消せます。
PRのCIではトークン交換が通って、• Remote caching enabledと出ました。check-typesは4/4 cachedで806ms >>> FULL TURBO、buildは3/3 cachedで1.252s >>> FULL TURBOです。このPRのコードは、10/07にmainで最後に走ったCIと同じものです。そのCIはPATで走ってアーティファクトをアップロードしていたので、3日前のアーティファクトがOIDCのトークンでそのまま再利用されたことになります。キャッシュはチームのもので、認証方式は関係ありません。
| ステップ | 10/07(PAT・コード変更あり) | 10/10(OIDC・同じコード) |
|---|---|---|
| Install dependencies | 12s | 14s |
| Type check | 25s | 2s |
| Lint & Format check | 16s | 11s |
| Build | 74s | 1s |
| ステップの合計 | 132s(約2分12秒) | 35s |
速くなった理由はOIDCそのものではありません。同じコードでキャッシュに当たっただけで、10/07はコード変更があってほとんどミスしていました。Installが変わらないのは、Remote Cacheに入るのがturbo runのタスクの成果物とログだけで、依存のインストールはパッケージマネージャーのキャッシュの担当だからです。
マージ後のDeployワークフロー(ジョブにenvironment: productionが付いています)も、同じアカウント全体のポリシーでトークン交換が通りました。bun turbo -F server buildは708ms >>> FULL TURBOで、10/07には同じタスクが13.3sでミスしています。deployタスク自体はcache: falseで1m31s。最後にトークンもちゃんと失効されていました。
この機能は2026-07-30にVercelのchangelogで発表されたもので、VercelもPATからOIDCへの移行を勧めています。
OIDCが使えないCIなら、Personal Access Tokenをそのまま渡します。Bunテンプレートのリポジトリも切り替える前はこの方式で、2日前のCIが作ったキャッシュに、次の実行が当たっていました。
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}自分はdotenvxで渡している
Next.jsのポートフォリオサイトでは、TURBO_TOKENとTURBO_TEAMをdotenvxで暗号化した.env / .env.productionに入れてコミットしています。ルートのscriptsがdotenvx run -- turbo run buildなので、turboには環境変数として届きます。CIに登録するシークレットは復号鍵の1つだけです。
"build": "dotenvx run -- turbo run build",# The build fetches the posts with GITHUB_TOKEN, and turbo reads its remote cache
# credentials, both from the encrypted .env.
- env:
DOTENV_PRIVATE_KEY: ${{ secrets.DOTENV_PRIVATE_KEY }}
run: pnpm buildこのリポジトリでCIが初めて動いたPRで、pnpm buildがいきなりRemote Cacheに当たりました。ログには• Remote caching enabled、cache hit, replaying logs、Cached: 1 cached, 1 total、Time: 1.172s >>> FULL TURBOと出ています。CIはまだ一度もビルドしていないので、当たったのは手元でpnpm buildしたときにアップロードされたアーティファクトです。自分のマシンでのビルドが、そのままCIで使い回されたわけですね。続く3回(マージでのmainへのpush、Dockerfileと.dockerignoreだけ変えたPRと、そのマージのpush)もweb#buildのハッシュが同じで、全部0.6〜1.2秒のFULL TURBOでした。キャッシュミスだと20〜26秒かかるビルドです。
同じ暗号化ファイルをCoolifyのDockerビルドでも使っていて、鍵はビルド引数で渡しています(pnpm build:prodがdotenvx run -f .env.production -- turbo run buildです)。ただしハッシュが一致するのはinputsとenvの値が揃ったときだけなので、envに書いた変数の値が本番と開発で違えば、本番ビルドは開発用ビルドのキャッシュには当たりません。Dockerビルドでどれだけ当たっているかは、まだ測っていません。
落とし穴もひとつ。同じCIのpnpm check-typesのステップは• Remote caching disabledと出ます。このscriptはdotenvx runを挟まずにturbo run check-typesを呼んでいて、TURBO_TOKENが環境にないからです。トークンが渡らないステップはエラーにならず、黙ってローカルキャッシュだけになります。check-typesは2秒ほどなので放置していますが、重いタスクで起きていたら気づきにくいところでした。
自前でホストする選択肢もある
Remote CacheのAPIはOpenAPIの仕様が公開されていて、これを実装したHTTPサーバーなら何でも使えます(turboのどのバージョンもv8のエンドポイントと互換です)。向き先の指定は環境変数TURBO_API(サーバーのURL)、TURBO_TOKEN、TURBO_TEAMの3つで、手元でもCIでも同じです。turbo.jsonのremoteCache.apiUrl(とteamSlug / teamId)でも指定できますし、turbo login --api=<url> --manualでトークンを手入力する方法もあります。teamIdはteam_で始まるときだけ使われます。
選択肢は主にこの3つです。1つ目のducktorsのものは、このあと実際に立てて試しています。
- ducktors/turborepo-remote-cache: TypeScript製でいちばん使われているもの(約1.5kスター、2026-10-07にv2.14.3)。
docker run --env-file=.env -p 3000:3000 ducktors/turborepo-remote-cacheのようにDockerイメージで動かします。保存先はSTORAGE_PROVIDER/STORAGE_PATHで、ローカルファイルシステム、S3、DigitalOcean Spaces、GCS、Azure Blob、MinIOから選べます。認証はAUTH_MODEで、デフォルトのstaticはサーバー側のTURBO_TOKEN(カンマ区切りで複数可)とクライアントのTURBO_TOKENを突き合わせます。ほかにjwt(JWKS)とnoneもあります - AdiRishi/turborepo-remote-cache-cloudflare: Cloudflare Workers + R2(またはKV)で動くもの(2026-01-19にv4.0.0)。cloneして
pnpm install、pnpm wrangler r2 bucket create turborepo-cache、pnpm run deploy、echo "<token>" | pnpm wrangler secret put TURBO_TOKENで立ちます。毎日3時(UTC)のcronでBUCKET_OBJECT_EXPIRATION_HOURS(デフォルト720時間 = 30日)より古いオブジェクトを消してくれます。READMEによるとTURBO_TEAMはteam_始まり、TURBO_APIは末尾スラッシュなしで指定します - brunojppb/turbo-cache-server: Rust製(2026-10-05に4.0.19)で、S3互換ストレージ(AWS S3、Cloudflare R2、RustFSで動作確認済み)に保存します。面白いのはGitHub Actionとしても使えるところで、ランナー上でキャッシュサーバーをバックグラウンド起動して
TURBO_API: http://127.0.0.1:8585を向かせ、裏はバケット、という構成が組めます。Dockerイメージghcr.io/brunojppb/turbo-cache-serverもあります。デフォルトは認証なし(プライベートネットワークやランナー内を想定)で、サーバー側にTURBO_TOKENを設定するとBearer認証になります
turboのドキュメントにはTapico/tapico-turborepo-remote-cacheも載っていますが、最後のリリースが2021年で、メンテナンスされていなさそうです。
試したのはducktors/turborepo-remote-cacheです。自分のサーバーのCoolifyで、Dockerイメージducktors/turborepo-remote-cache:2.14.3からアプリケーションを作り、ポート3000を公開して自分のドメインのサブドメインを当てました。デプロイは30秒ほどで終わります。環境変数はNODE_ENV=production、PORT=3000、TURBO_TOKEN(openssl rand -hex 32で作ったランダムな値)、STORAGE_PROVIDER=local、STORAGE_PATH=turbo-cacheの5つ。保存先はコンテナ内のローカルストレージで、ボリュームには載せていません。試すだけならこれで十分ですが、再デプロイで消えます。本番で使うならボリュームをマウントするか、S3互換のストレージにします。GET /v8/artifacts/statusを叩くと{"status":"enabled","version":"2.14.3"}が返ってきました(トークンなしでも答えてくれます)。
クライアント側は環境変数だけで、turbo.jsonや設定ファイルは触っていません。
TURBO_API=https://<your-cache-domain> TURBO_TOKEN=<token> TURBO_TEAM=<any name> turbo run check-types build --cache=remote:w手元のマシンから、CLIリポジトリのturbo run check-types build(8タスク)を測りました。--cacheオプションがあると試しやすくて、remote:wはRemote Cacheへの書き込みだけ、remote:rは読み込みだけ、local:はローカルキャッシュなしです(書かなかったソースは無効になります)。
| 実行 | キャッシュ | 時間 |
|---|---|---|
--cache=remote:w(アップロードだけ) |
0/8 | 40.1s |
--cache=remote:r(リモートから読むだけ) |
7/8 | 855ms |
--cache=local:(キャッシュなし) |
0/8 | 41.7s |
読み込みで1つミスしているのは、cache: falseにしているserver#buildです。測ったのは手元のマシンからだけで、CIからはまだです。用が済んだので、Coolifyのアプリとプロジェクトは消しました。
自前ホストは、サーバーとストレージの費用、掃除、認証を自分で持つ代わりに、fair useの制限がなく、保持期間も決められて、アーティファクトを自分のストレージに置けます。Coolifyならアプリ1つと環境変数5つで立ちました。すでにCloudflareを使っている自分なら、次に試すのはWorkers + R2のものかなと思っています。
共有する前に気をつけること
ログもアーティファクトとしてキャッシュされるため、シークレットを出力しないようにします。outputsやenvが間違ったまま共有キャッシュを有効にすると、壊れたアーティファクトが1つあるだけで全員に配られます。共有は、キャッシュが正しく効くようになってからにしましょう。
もうひとつ、turboのハッシュにはデフォルトでOSやCPUアーキテクチャが入っていません。Macのdarwin-arm64でビルドしたアーティファクトでも、入力が同じならLinux x64のCIでそのまま復元されます。型チェック、lint、テスト、Next.jsやViteのJSバンドルのようにプラットフォームに依存しない結果ならそれで問題なく、むしろそれを共有するのがRemote Cacheの目的です。危ないのはネイティブバイナリが成果物になるタスクで、bun build --compileの単一バイナリ、Prismaのエンジン、sharpなどが該当します。自分のCLIリポジトリではserver#build(Bunの単一バイナリで、matrixで4プラットフォーム向けにリリースしています)をcache: falseにしているので、これは起きません。
ドキュメントの「Handling platforms」にある対処は、turbo runの前にプラットフォームとアーキテクチャをファイルに書き出し、gitignoreしたうえでinputsに足す方法です。
// scripts/create-turbo-cache-key.js
const { writeFileSync } = require("fs");
const { platform, arch } = process;
writeFileSync("turbo-cache-key.json", JSON.stringify({ platform, arch }));"server#build": {
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/turbo-cache-key.json"]
}タスクのinputsに入れれば、そのタスクだけがプラットフォームごとのキャッシュになります。globalDependenciesに入れると全タスクがプラットフォームごとになって、OSが違う手元とCIでは共有が効かなくなります。Node.jsのバージョンは、package.jsonのenginesを変えるとハッシュが変わります。自分のglobalDependencies: ["mise.toml"]でもカバーできています。
"remoteCache": { "signature": true }とTURBO_REMOTE_CACHE_SIGNATURE_KEYを設定すると、アップロード時にHMAC-SHA256で署名し、ダウンロードしたアーティファクトの署名がないか不正ならミス扱いにします。ドキュメントにある通り、これはセキュリティ機能ではなく整合性チェックです。アップロードやダウンロードの欠け、キャッシュサーバー側の問題に対する保険と思ってください。鍵はfutureFlags.longerSignatureKey: trueで32バイト以上を強制できます(短い鍵はHMACを弱めるので、将来のメジャーバージョンでは拒否されます)。
actions/cacheとどちらを使うか

| actions/cache | VercelのRemote Cache | 自前ホスト | |
|---|---|---|---|
| 必要なもの | なし | トークンかOIDC(OIDCならSecret不要) | サーバーとストレージ、トークン |
| 転送 | 毎回キャッシュディレクトリを丸ごと復元・保存 | 各タスクに必要なアーティファクトだけ取得 | 同左 |
| 共有範囲 | GitHub Actionsの中だけ | 手元・他のCI・ビルドサーバーとも | 同左 |
| 上限 | 10GB、7日間アクセスなしで削除 | 無料だがfair use制限、7日で失効 | ストレージ次第、保持期間は自分で決める |
| 運用 | なし | なし | 掃除と認証は自分で |
自分は、Vercelの設定がないリポジトリはactions/cache、ポートフォリオとBunテンプレートのリポジトリはVercelのRemote Cacheにしています。トークンやポリシーの管理が増えないぶんactions/cacheのほうが気楽で、手元とCIで同じビルドを何度もするならRemote Cacheが効く、というのが個人的な感覚です。
--affectedで変わったところだけ動かす
turbo run build --affectedのように--affectedを付けると、変更の影響を受けるパッケージのタスクだけ走ります。比較対象はデフォルトでmain...HEADです(TURBO_SCM_BASE / TURBO_SCM_HEADで変更可)。GitHub Actions上では、pull_requestならGITHUB_BASE_REFから、pushならイベントのpayload(GITHUB_EVENT_PATH)から自動で拾ってくれます。
必要なのはgitの履歴です。actions/checkoutはデフォルトで深さ1のshallow cloneなので、そのままだと比較できず、全部変更扱いになります(全タスク実行にフォールバック)。自分はfetch-depth: 0にしています。
futureFlagsを2つ有効にする
"futureFlags": {
// `--affected` selects a task only when a changed file is among its inputs, so a change outside
// the packages (docs/, the workflows) runs only the tasks that read it.
"affectedUsingTaskInputs": true,
// A pull request checks out detached, with its base branch only as origin/<branch>.
"githubActionsRemoteBaseRefFallback": true,
},affectedUsingTaskInputsがないと判定はパッケージ単位で、パッケージ内のどのファイルを変えてもそのパッケージの全タスクが選ばれます。有効にするとタスク単位になり、変更ファイルがそのタスクのinputsに含まれるときだけ選ばれます。さっきのsite#buildの$TURBO_ROOT$/docs/**のように、パッケージの外の変更も拾えます。なお、ルートのpackage.json、turbo.json(c)、ロックファイル、globalDependenciesのファイルを変えると、このフラグがあっても全タスクが選ばれます。

githubActionsRemoteBaseRefFallbackは、PRのベースブランチがローカルのrefにないときにorigin/<branch>へフォールバックする設定です。actions/checkoutはdetachedでチェックアウトするため、これがないとベースが見つからないことがあります。
PRとdevelopだけ--affectedにする
CLIリポジトリでは、PRとdevelopへのpushは--affected、リリースタグを打つmainへのpushは全部走らせています。シェルの${AFFECTED:+--affected}で、AFFECTEDが空でないときだけフラグを付けています。
- env:
AFFECTED: ${{ (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') && 'true' || '' }}
TURBO_CACHE_MAX_SIZE: 1GB
run: pnpm turbo run check-types test build ${AFFECTED:+--affected}走らないタスクもあるわけで、後続でバイナリを使うステップはファイルがあるときだけ動くようにしています。
デプロイや重いテストにも効く
cache: falseのタスクでもinputsはaffectedの判定に使われます。この仕組みで、デプロイとDockerを使うテストを関係あるときだけ走らせています。
// The deploy script builds the site for Cloudflare itself. CI runs it when the site is affected.
"site#deploy": {
"dependsOn": ["^build"],
"cache": false,
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/docs/**", "$TURBO_ROOT$/.github/workflows/site.yml"],
"passThroughEnv": ["DOTENV_PRIVATE_KEY_PRODUCTION"],
},
// Real containers, so never cached. Affected by the drivers, the tests, and the engine versions.
"server#test:docker": {
"cache": false,
"inputs": ["src/live/**", "src/**/*.docker.test.ts", "package.json", "$TURBO_ROOT$/.github/workflows/docker.yml"],
},run: pnpm turbo run deploy --filter=site ${AFFECTED:+--affected}サイトはdevelopへのpushのうち、サイト本体・docs/・依存パッケージ・サイト自身のワークフローファイルのどれかが変わったときだけデプロイされます。ワークフローファイルをinputsに入れておくと、ワークフローを直したときもちゃんと動いてくれます。Dockerのドライバテストも、ドライバのコード・テスト・ワークフローが変わったときだけ本物のDBコンテナを立てます(別途、夜間に全部走るスケジュールも組んでいます)。--affectedと--filterを両方付けると、どちらにも当てはまるパッケージだけが対象になります。
もう一歩進めるなら、turbo query affected --packages web(--exit-codeも使えます)で影響がなければジョブごとスキップして、依存のインストールすら省く手もあります。自分はまだやっていません。次の課題です。
よく使う小ワザ
ルートのpackage.jsonのscriptsはturbo run <task>を呼ぶだけにしています。turbo buildと省略せずにturbo runと書いておくと、将来turboに同名のサブコマンドが増えてもぶつかりません(ドキュメントの例はturbo deploy)。
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"check-types": "turbo run check-types",
"test": "turbo run test",
"check": "oxlint --fix && oxfmt --write",
"check:ci": "oxlint && oxfmt --check"
}oxlintとoxfmtはパッケージごとにturboで回さず、ルートから1回だけ走らせています。十分速いので。TurborepoのOxcガイドも//#lintのようなルートタスクを勧めています。ポートフォリオのCIでは型情報つきlintがcheck-typesの生成物を読むので、check-typesをcheck:ciより先に走らせています。
他にはこのあたりです。
--filter/-Fで1パッケージだけ動かす(turbo run dev -F web)withで、長く動くタスクを一緒に起動する。"infra#dev": { "cache": false, "persistent": true, "with": ["web#watch:content"] }で、dev serverと並べてコンテンツのインデックス監視を立ち上げています- Cloudflareのインフラにalchemyを使っている一部のリポジトリでは、alchemyに専用のTUIがあるので
dev/deployはturboを通さず直接呼んでいます。turboはbuild・check・testだけ。全部turboに通す必要はないです "agentGuidance": false。turboがAIコーディングエージェントから実行されたことを検出すると、ルートのAGENTS.mdにturboパッケージ同梱のドキュメントへ案内するブロックを書き込みます。自分はAGENTS.mdを自分で管理しているので切っています(切っても既存のブロックは消えません)turbo.jsoncならコメントが書けます。自明でない設定には「なぜそうしたか」をコメントで残していますnode_modules/turbo/docsに、インストールしたバージョンと一致するドキュメントが入っています。オプションの確認はオフラインでも済みます
参考
まとめ
TurborepoでCIを速くする手順は、キャッシュを正しく効かせる(outputs・inputs・env)、actions/cacheかRemote Cacheで持ち越す、--affectedで変わったところだけ動かす、の3段です。1つ目を飛ばすと2つ目以降で壊れたキャッシュが広まるだけなので、まずは--dryや--summarizeでハッシュが意図通りかを確かめるところからがおすすめです。
参考になったよという方は、Xのフォローやシェアをもらえると嬉しいです!







