Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

turbo.jsonにpassThroughEnvが無いとテストが黙って全skipのままCIがgreenになる

公開 読了時間 約6分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

turbo.jsonのtest taskにpassThroughEnvを指定していないと、turboはCIのDATABASE_URLをテストプロセスへ渡さず、DB接続ガードがdescribe.skipへ倒れるため、CIは例外もエラーも出さないままRLSテスト24件を一度も実行せずにgreenになる。

結論

turbo.jsonのtest taskにpassThroughEnvが無いと、CIのpostgresサービスがDATABASE_URLを提供していても、turboはそれをvitestの子プロセスへ渡さない。 テスト側はDB接続用URLが取れないときに例外を投げず、describe.skipで自分自身をスキップするガードになっているため、vitestはこれを失敗として扱わない。CIはexit code 0のままgreenになり、RLSテスト24件は数週間、一度も実行されないまま存在していた。

症状

RLSテスト24件はCIのpostgres serviceが既にある状態で追加されていたが、CI Test jobはずっとgreenだった。だがそのgreenは「pass」ではなく「全件skip」だった。原因はturbo.jsontesttaskに1点欠けていた設定である。

// 修正前の turbo.json(抜粋)
"test": {
  "dependsOn": ["^build"],
  "outputs": ["coverage/**"]
}

envpassThroughEnvも無い。turboはタスクのキャッシュ可能性を担保するため、明示的に宣言されていない環境変数を子プロセスへ通さない。CIのワークフロー側でジョブにDATABASE_URLを設定していても、turbo run testが起動するvitestプロセスからはその変数が見えない。

原因

見えなくなったDATABASE_URLを、テスト基盤側は「エラー」ではなく「未設定」として扱うよう作られていた。vitestのglobalSetupがこれを検知する。

// packages/db/__tests__/_setup/global-setup.ts(抜粋)
export async function setup(): Promise<void> {
  const url = process.env.DATABASE_URL;
  if (!url) {
    console.warn(
      "[rls-tests] DATABASE_URL が未設定のため RLS テストをスキップします。" +
        " docker compose up -d postgres を実行し DATABASE_URL を設定してください。",
    );
    process.env.RLS_TESTS_SKIP = "1";
    return;
  }
  // H1: prod / staging DB 誤接続防止ガード
  assertTestDatabase(url);
  // ...マイグレーション適用...
}

DROP SCHEMA public CASCADEを伴うテスト基盤なので、URLが取れないときにthrowせずRLS_TESTS_SKIPを立ててreturnするだけ、という設計自体はローカル未整備時の安全側フォールバックとして妥当である。この値を各テストファイルが読む。

// packages/db/__tests__/_setup/db.ts
export function getConnectionUrl(): string | null {
  if (process.env.RLS_TESTS_SKIP === "1") return null;
  return process.env.DATABASE_URL ?? null;
}
// packages/db/__tests__/rls/tenant-isolation.test.ts(抜粋)
const url = getConnectionUrl();
const describeOrSkip = url ? describe : describe.skip;

describeOrSkip("RLS tenant_isolation (school_id ベースの分離)", () => {
  // ...

urlnullならdescribeではなくdescribe.skipが使われ、このファイル内のテストは全てスキップ扱いになる。vitestはスキップされたテストを失敗にカウントしない。ローカルでpostgresを起動し忘れたときに親切にスキップしてくれる設計が、CIでは「turboが環境変数を子プロセスへ渡し忘れている」という別の原因を、同じ経路で覆い隠した。

原因は2つ重なっていた。1つはここまでのturbo.jsonpassThroughEnv不足。もう1つは、その前段で追加されていたKIMITERRACE_TEST_DB_OKという別の環境変数(assertTestDatabaseのH1ガードをCI上で通すためのフラグ)もCI側に設定されていなかったことで、こちらも同じ「未設定→安全側でスキップ」という経路を通って見えなくなっていた。

直し方

turbo.jsontesttaskにenvpassThroughEnvを追加し、DATABASE_URLがキャッシュのハッシュキーにも入り、かつ子プロセスへ実際に渡るようにした。

// 修正後の turbo.json(抜粋)
"test": {
  "dependsOn": ["^build"],
  "outputs": ["coverage/**"],
  "env": ["DATABASE_URL"],
  "passThroughEnv": ["DATABASE_URL"]
}

envだけでは不十分な点に注意がいる。envはそのタスクのキャッシュキーに環境変数の値を含める宣言で、passThroughEnvは子プロセスへその変数を実際に渡す宣言である。値が変わったらキャッシュを無効化したいが、かつ実行時にも見えている必要があるため、両方を書く必要があった。

この修正でCI Test jobが初めてRLSテスト24件を実走させた結果、テストコード側に埋まっていたdormant bugが連鎖して露見した。0000_initial_baseline.sqlに8個のenum(user_rolepublish_scopeほか)のCREATE TYPEが無く、CREATE TABLE "users"type "user_role" does not existで落ちるという、テストが一度も走っていなかったために誰も踏んでいなかった不具合である。

-- packages/db/drizzle/0000_initial_baseline.sql(冒頭、修正後)
CREATE TYPE "public"."user_role" AS ENUM('school_admin', 'teacher', 'student', 'guardian');--> statement-breakpoint
CREATE TYPE "public"."publish_scope" AS ENUM('school', 'class', 'homeroom', 'private');--> statement-breakpoint
-- ...8個ぶん
CREATE TABLE "schools" (
  "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,

さらにテストコード自身のschema不整合(許容されないpublish_scope値の指定)や、RLSポリシー側でcurrent_settingmissing_okモードが未設定時にNULLではなく空文字列''を返し''::uuid型変換エラーを起こす不具合も、実走して初めて顕在化して同じコミットで解消された。

まとめ

「未設定なら安全側でスキップする」というガードは、ローカルの初期設定忘れには効くが、CIで環境変数の受け渡しが壊れているケースには効かない。 どちらも同じ「未設定」という状態に見え、テストランナーは両方を同じ経路でスキップとして扱うためだ。turboのようにタスクごとに環境変数を明示宣言する仕組みを使っている場合、CIのジョブ定義に変数を書いただけでは足りず、env/passThroughEnvの宣言まで揃って初めてテストプロセスに届く。CIがgreenであることは「pass した」ことの証明にはならず、「1件も実行されていない」状態もまったく同じ色で表示される。skip件数そのものをCIの出力で確認する運用が無いと、この種の劣化は誰にも気づかれないまま定着する。

よくある質問

Q1なぜCIはgreenなのにテストが1件も実行されていなかったのですか?

turbo.jsonのtest taskにpassThroughEnvの指定が無く、turboがCIのDATABASE_URL環境変数をvitestプロセスへ渡していなかったためです。テスト側はDB接続用URLが未設定のとき例外を投げず、describe.skipで自分自身をスキップするガードになっており、vitestはスキップを失敗として扱いません。結果としてCI上はテストが1件も走らないままexit codeは0になり、緑のまま気づけない状態が続いていました。

Q2CIのpostgresサービス自体は動いていなかったのですか?

動いていました。原因はDBが無かったことではなく、turboがCIの環境変数をテストプロセスへ渡していなかったことです。turboはキャッシュの再現性を保つため、明示的にenv/passThroughEnvで宣言した環境変数しか子プロセスへ通しません。DATABASE_URLを宣言していなかったため、CI上にpostgresがあってもvitestからは未設定に見えていました。

Q3テストを実走させたら、他にも不具合が見つかったのですか?

はい。テストが初めて実走した結果、0000_initial_baseline.sqlに8個のenumのCREATE TYPEが無く、CREATE TABLE "users"がtype "user_role" does not existで落ちる別バグが見つかりました。テストコード自身の環境変数不備で隠れていた不具合が、通るようにした途端に連鎖して露見した形です。

確認した環境

  • turbo ^2.9.16 / vitest ^3.2.6(モノレポ)
  • 2026-05-29 に修正コミットで解消

この記事の根拠

  • JSONファイル 19〜24行目コミット 3c747af
  • SQLファイル 1〜8行目コミット 3c747af
  • TypeScriptファイル 54〜63行目コミット 941e5aa
  • TypeScriptファイル 1〜14行目コミット 22c93be
  • TypeScriptファイル 1〜9行目コミット da0605a

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。