Chapter 07

env 是缓存的隐形杀手

Turbo 不看 process.env 的全部——它只把你显式声明的 env 算进 hash。忘写一个就可能"dev 能跑,生产挂"或"缓存错误命中"。这一章搞清 env/globalEnv/passThroughEnv 的分工。

Turbo 的 env 模型

默认(strict 模式):
  任务启动时,只能看到:
    - PATH / HOME 等系统变量
    - 显式声明在 env/globalEnv/passThroughEnv 里的变量

  其它 process.env 全部被过滤掉

意图:让任务可复现——两个人同一个 commit,即使本地 .env 不同,build 结果一致。

三种声明位置

{
  "globalEnv": ["NODE_ENV", "VERCEL_ENV", "CI"],
  "globalPassThroughEnv": ["GITHUB_TOKEN"],
  "tasks": {
    "build": {
      "env": ["NEXT_PUBLIC_*", "DATABASE_URL"],
      "passThroughEnv": ["AWS_*"]
    }
  }
}
globalEnv
仓库级——所有任务都会读到,且值变 → 所有任务 hash 变。适合 NODE_ENVCI 这种全局影响构建行为的变量。
tasks.xxx.env
任务级——只该任务读到,值变只影响该任务的 hash。适合任务专属的 API_URL、TOKEN 等。
passThroughEnv / globalPassThroughEnv
"透传但不进 hash"——任务能读这个变量,但它变了不触发重 build。典型:登录凭证、CI token。

通配符

{
  "tasks": {
    "build": {
      "env": [
        "NEXT_PUBLIC_*",        // 所有 NEXT_PUBLIC_ 开头
        "!NEXT_PUBLIC_DEBUG",    // 除了 DEBUG
        "VITE_*"
      ]
    }
  }
}

Next.js / Vite 这种带前缀约定的项目,通配非常省事——加新 NEXT_PUBLIC_X 不用改 turbo.json。

env 和 passThroughEnv 的选择

场景用 env用 passThroughEnv
NEXT_PUBLIC_API_URL(被编译进产物)
DATABASE_URL(build 时用到)
GITHUB_TOKEN(只给 CI 用,变了不该重 build)
AWS_ACCESS_KEY(部署时用,不影响产物)
CI_JOB_ID(每次都变,进 hash 就废了缓存)
判断原则
这个变量的值变化,是否影响产物内容?影响 → env;不影响 → passThroughEnv。

.env 文件集成

.env
DATABASE_URL=postgres://localhost/myapp
STRIPE_SECRET=sk_xxx

.env.local
DEBUG=true

Turbo 本身不加载 .env——靠你的构建工具(Next.js/Vite)自己加载。Turbo 只负责声明和 hash。

// turbo.json
{
  "globalDependencies": [".env*"],      // .env 文件变化触发 hash 变
  "tasks": {
    "build": {
      "env": ["DATABASE_URL", "STRIPE_SECRET"]
    }
  }
}

envMode 的三种值

{
  "envMode": "strict"
}
strict(Turbo 2.x 默认)
任务只能看到声明过的 env——未声明的读到就是 undefined。最安全,鼓励显式声明。
loose(旧版默认)
任务可以读所有 env,但只有声明过的进 hash——方便但不安全,容易缓存错误命中。
# 临时切回 loose 模式调试
pnpm turbo run build --env-mode=loose

strict 模式的坑

// apps/web/next.config.js
if (process.env.MY_CUSTOM_FLAG === "1") { ... }

// turbo.json 没声明 MY_CUSTOM_FLAG
// → strict 模式下 process.env.MY_CUSTOM_FLAG === undefined,if 进不去
// → 本地 export MY_CUSTOM_FLAG=1 也不行

解决:

{
  "tasks": {
    "build": {
      "env": ["MY_CUSTOM_FLAG"]
    }
  }
}

调试:看任务实际收到的 env

pnpm turbo run build --dry-run=json | jq '.tasks[] | {task: .taskId, env: .resolvedTaskDefinition.env, envValues: .environmentVariables}'
{
  "task": "@myorg/web#build",
  "env": ["NEXT_PUBLIC_*", "DATABASE_URL"],
  "envValues": {
    "configured": ["NEXT_PUBLIC_API_URL=https://api.example.com"],
    "inferred": ["NEXT_PUBLIC_STRIPE_KEY=pk_xxx"]   # 通配匹配到的
  }
}

Vercel 集成

Vercel dashboard → Project Settings → Environment Variables:
  DATABASE_URL=...     (production)
  DATABASE_URL=...     (preview)
  NEXT_PUBLIC_API_URL=...

Vercel 在 build 时把这些环境变量注入,Turbo 读它们算 hash
→ 不同环境 hash 不同
→ Preview 和 Production 各自缓存
Vercel 自动推断 env
Vercel 能从 Next.js 代码里扫出你用了哪些 env,在 build 时自动注入。配合 Turbo,你只需要在 turbo.json 显式列上这些——不用重复在 vercel.json 里写一遍。

本地不同开发者的 .env.local

{
  "globalPassThroughEnv": ["LOCAL_DEV_*"]
}

开发者各自的 LOCAL_DEV_PORT=3001LOCAL_DEV_DB=postgres://me/...——这些不该影响缓存(影响了就没人命中了),用 passThroughEnv。

env 分组:多阶段配置

{
  "tasks": {
    "build": {
      "env": ["NEXT_PUBLIC_*"]
    },
    "build:staging": {
      "env": ["NEXT_PUBLIC_*", "STAGING_*"]
    },
    "build:production": {
      "env": ["NEXT_PUBLIC_*", "PROD_*"]
    }
  }
}

不同环境跑不同任务,env 声明也不同——产物天然不串。

env 作为动态 hash

两次 CI build:
  第一次:DATABASE_URL=postgres://old,hash=abc
  第二次:DATABASE_URL=postgres://new,hash=def

Turbo 发现 hash 变了 → 重跑 → 产物里注入新 URL

这是对的——生产 DB 变了,产物应该重编。Turbo 的 env hash 机制保证了这点。

不要往代码里写死秘密

env 机制的价值前提
如果你在代码里硬编码了 API Key,Turbo 的 env hash 完全保护不了你——改 key 代码 diff 就会让 hash 变。

CI 实际例子

# .github/workflows/ci.yml
env:
  # 进 hash 的
  NEXT_PUBLIC_API_URL: https://api.prod.example.com
  DATABASE_URL: ${{ secrets.DATABASE_URL }}

  # 不进 hash 的(passThroughEnv)
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

steps:
  - run: pnpm turbo run build
// turbo.json 对应
{
  "globalPassThroughEnv": ["TURBO_TOKEN", "GITHUB_TOKEN", "AWS_*"],
  "tasks": {
    "build": {
      "env": ["NEXT_PUBLIC_*", "DATABASE_URL"]
    }
  }
}

检查 env 配置是否完整

# 改一个应该影响缓存的 env,看 hash 是否变
export DATABASE_URL=postgres://test1
pnpm turbo run build --dry-run=json | jq '.tasks[0].hash'
# → "abc123..."

export DATABASE_URL=postgres://test2
pnpm turbo run build --dry-run=json | jq '.tasks[0].hash'
# → "def456..." ← 不同,说明 env 进了 hash,配对了

本章小结