这篇能做出什么
按下面的步骤走一遍,你会得到一套这样的工作方式:
- 仓库的依赖安装、系统包、环境变量被写进一份可复现的环境定义,每次开新环境自动重建,不需要靠记忆敲命令。
- 任务跑在云端,进度留在任务本身。换一台电脑、换一个终端,回到同一个任务就能接着往下做,不用把需求重新描述一遍。
- 包管理器和构建工具的缓存指向持久目录,第二次之后开环境省掉重复下载依赖的时间。
举一个具体例子。一个仓库跑完整测试要先装几百 MB 依赖、再跑六分钟。没有这套做法时,每次换设备都要先等一次安装,人的注意力被打断两次:一次是等装依赖,一次是重新想"我上次做到哪了"。有了这套做法后,新设备上打开同一个任务,直接进入"跑测试 → 改代码 → 再跑测试"的循环。
需要提前说明:不同产品、不同版本在界面上的叫法不一样,有的叫 environment,有的叫 workspace 或 sandbox;字段名和入口位置以官方文档当前版本为准。下面讲的是这套机制的通用做法,落到具体产品时把名字对上即可。
环境固化可以理解成三个层次,由浅到深:
1. 脚本层:一个 setup.sh,别人和新环境都跑它。
2. 声明层:devcontainer.json 或 Dockerfile,把基础镜像、环境变量、安装命令写成文件。
3. 快照层:把装好依赖的容器状态存下来复用,新环境直接从这个状态启动。
三层不是必须全做,但做到第二层,跨设备续跑才有稳定的地基。
前置条件清单
- 一个 Git 托管仓库,有可读的主分支,且你对该仓库有写权限。
- 已开通并能使用 Codex 云端任务(具体入口与权限以官方文档当前版本为准)。
- 本地已经有一份能跑通的最小命令清单:装依赖一条、跑测试一条、跑 lint 一条。
- 仓库根目录可以新增文件(放
setup.sh、.devcontainer/、AGENTS.md)。 - 私有依赖源或私有仓库需要的 token 已准备好。token 只放进平台的 secrets 功能,不进仓库。
- 本地装好 git,能正常 push。云端环境要读到你的脚本,前提是脚本已经在远端分支上。
分步骤
第 0 步:先在本地验证"从零能跑通"
这一步不做,后面所有问题都会混在一起。清空依赖目录,严格按 README 跑一遍:
```bash
记下每一步的耗时,后面判断要不要缓存就靠它
rm -rf node_modules
time npm ci
time npm test
```
把每个命令的耗时抄到便签上。安装 3 分钟、测试 6 分钟,说明安装值得缓存,测试值得并行——优先级一目了然。
如果这步就有报错,先修本地,别指望云端会变好。云端环境不比本地更聪明,它只是更干净。
第 1 步:写一个幂等的 setup 脚本
在仓库里新建 scripts/setup.sh。三个硬性要求:幂等(重复跑不报错)、非交互(不能弹任何提问)、失败即退出。
```bash
#!/usr/bin/env bash
scripts/setup.sh —— 环境初始化,要求幂等、非交互、失败即退出
set -euo pipefail
1) 系统包:先判断存在再装,避免每次重装
if ! command -v jq >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
sudo apt-get install -y jq
fi
2) 缓存目录指向工作区内路径,重建容器也能复用(详见第 3 步)
export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-/workspaces/.cache/npm}"
export PIP_CACHE_DIR="${PIP_CACHE_DIR:-/workspaces/.cache/pip}"
mkdir -p "$NPM_CONFIG_CACHE" "$PIP_CACHE_DIR"
3) Node 依赖:必须用锁定文件做冻结安装
if [ -f package-lock.json ]; then
npm ci
elif [ -f pnpm-lock.yaml ]; then
corepack enable
pnpm install --frozen-lockfile
elif [ -f yarn.lock ]; then
corepack enable
yarn install --immutable
fi
4) Python 依赖
if [ -f requirements.txt ]; then
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
fi
echo "setup 完成"
```
几个容易写错的点:
npm install会就地解析版本,同样代码在不同环境可能装出不同依赖树。npm ci、pnpm install --frozen-lockfile这类冻结安装才能保证一致。前提是锁定文件已经提交到仓库。- 所有安装命令都要加非交互参数。
apt-get用-y且设DEBIAN_FRONTEND=noninteractive,pip不要省略--upgrade pip之外的交互。 - 把耗时长的步骤放在脚本后面,日志定位更快。
写完给脚本执行权限,并在本地跑两遍验证幂等:
```bash
chmod +x scripts/setup.sh
bash scripts/setup.sh
bash scripts/setup.sh # 第二遍也必须成功退出
```
第 2 步:把环境写进声明文件
脚本只能管"装什么",管不了"在哪装"。基础镜像、工作目录、环境变量这些要写进声明文件。
Codex 云端环境通常有一个配置入口,可以填 setup 脚本、环境变量和密钥。更方便的做法是把这些写进仓库里的 devcontainer.json 或 Dockerfile,这样环境定义跟着代码走,谁都能看见、能评审、能改。
```json
{
"name": "repo-task",
"image": "<基础镜像,按官方文档当前版本选择并固定>",
"postCreateCommand": "bash scripts/setup.sh",
"containerEnv": {
"NPM_CONFIG_CACHE": "/workspaces/.cache/npm",
"PIP_CACHE_DIR": "/workspaces/.cache/pip",
"TZ": "Asia/Shanghai",
"LANG": "C.UTF-8"
},
"remoteUser": "vscode"
}
```
镜像尽量固定到一个明确的标识,不要用 latest 这类会漂移的标签,否则今天能跑、下周挂掉,排查起来会很痛苦。
密钥处理:环境变量分两类。一类是普通配置(缓存路径、时区、locale),直接写在文件里;另一类是凭证(私有源的 token、API key),只写变量名,值通过平台的 secrets 功能注入:
```bash
在 setup 脚本里按需读取,值由平台注入,脚本里不出现明文
if [ -n "${PRIVATE_REGISTRY_TOKEN:-}" ]; then
npm config set //your.registry.example/:_authAI 词典:Token">Token "${PRIVATE_REGISTRY_TOKEN}"
fi
```
把 .env、.cache/、node_modules/ 加进 .gitignore,别让缓存和密钥混进提交。
固化到什么程度可以算验收通过?新建一个干净环境,只执行 setup 脚本,测试能跑绿。 这条标准比任何描述都可靠。
第 3 步:让缓存真正被复用
这是很多人以为做了、其实没生效的一步。
原理很简单:npm、pip、cargo、go、maven 的默认缓存目录都在 $HOME 下面。如果每次都是全新容器,$HOME 里什么都没有,缓存自然等于不存在。把缓存目录指到会被持久化的位置,复用才发生。
```bash
统一放到工作区下的 .cache,并在 .gitignore 里排除
export NPM_CONFIG_CACHE=/workspaces/.cache/npm
export PIP_CACHE_DIR=/workspaces/.cache/pip
export CARGO_HOME=/workspaces/.cache/cargo
export GOMODCACHE=/workspaces/.cache/go/pkg/mod
export MAVEN_OPTS="-Dmaven.repo.local=/workspaces/.cache/maven"
```
不同产品的持久化范围不同:有的会保留整个工作区,有的提供容器快照功能,有的两者都有。以官方文档当前版本为准,先确认哪块目录能留到下次,再把缓存变量指过去。指错地方不报错,只是悄悄重新下载,所以一定要用"第二次开环境是否还下载依赖"来验证。
缓存失效的三种正常情况,遇到不用慌:
- 锁定文件改了(依赖本来就变了)。
- 基础镜像换了。
- 系统包大版本升级。
这三种情况重新下载是应该的。真正要警惕的是"什么都没改却每次全量下载",那说明缓存路径指错了。
第 4 步:把任务搬进云端,跑第一次
在 Codex 里针对这个仓库开一个任务。任务描述要包含四件事:做什么、约束是什么、怎么验证、产物放哪。
一段可以直接改的模板:
```text
目标:把 src/parser 模块的测试补齐,直到 npm test 全绿。
约束:
- 不要修改公共 API 的函数签名
- 不要升级任何依赖的大版本
- 不改动 CI 配置文件
验证命令:
- 单文件:npm test -- src/parser/lexer.test.ts
- 全量:npm test
产物:一个可以提交的 PR,改动说明写在 PR 描述里。
如果 setup 脚本报错,先停下把日志贴出来,不要绕过。
```
第一次运行重点看两段日志:setup 阶段的日志(环境问题都在这)、测试阶段的日志(代码问题在这)。这两类问题的排查方向完全不同,先分清再动手。
第 5 步:把"进度"写进仓库,而不是记在脑子里
跨设备续跑的关键不在工具,在于任务上下文存在哪儿。如果上下文只存在于你上一次的对话里,换设备就断了。
做法是在仓库里放一份任务上下文文件,常见名字是 AGENTS.md 或 docs/task.md,让每个新环境开局都能读到同样的信息:
```markdown
任务上下文
目标
补齐 src/parser 模块测试,保持 CI 全绿。
环境
- 初始化:bash scripts/setup.sh
- 全量测试:npm test
- 单文件测试:npm test -- <path>
- 代码检查:npm run lint
约束
- 不改公共 API 签名
- 不升级依赖大版本
当前进度
- [x] lexer.test.ts 补齐
- [x] parser.test.ts 补齐
- [ ] ast.test.ts(下一步,先看 3 个 skip 的用例)
- [ ] 全量测试跑绿
验收
- npm test 全绿
- npm run lint 无新增告警
```
于是"接力"变成三个动作,每个环境、每台设备都一样:
1. 读进度:让 agent 先读 AGENTS.md 的"当前进度"和"下一步"。
2. 跑验收:执行验证命令,确认起点是绿的,再开始改。
3. 更新进度:这一轮结束,把勾选状态和"下一步"改掉,提交。
第 2 步特别容易被跳过。不确认起点状态就动手,最后分不清是新改动引入的问题还是本来就坏着。
回到同一个任务时,不要重新描述一遍需求。同一个任务里的历史、文件改动、日志都在,重新描述等于把上下文丢掉一半。换设备后要做的是打开那个任务,然后执行上面三步。
第 6 步:切阶段,别一口气跑完
长期任务不适合一次提示做完。按验收命令能跑通的粒度切阶段,每阶段结束就提交一次。好处有三个:中断了不用重来、每步都有可回退点、进度文件能持续更新。
阶段之间如果环境有了新变化(比如新加了一个系统依赖),记得回头改 setup.sh,别只在当前容器里手动装。手动装的那一份,下一个环境就没有了——这正是"环境没固化"的典型症状:当前环境能用,新环境不能用。
常见坑与排错
setup 一直卡住不动。 九成是脚本里有交互式提示在等输入。所有安装命令加非交互参数,apt-get 加 -y 并设置 DEBIAN_FRONTEND=noninteractive。
日志里提示找不到锁定文件。 用了 npm ci 但仓库里没有 package-lock.json。先在本地生成并提交锁定文件,再上云端。
依赖每次重新下载。 缓存变量指向了 $HOME 下的默认位置,而 $HOME 不持久。改成工作区内的路径,然后开第二个环境验证是否还下载。
出现了"本地能跑、云端不能跑"。 先清缓存重跑一次,把缓存因素排除掉。如果清了还不行,就是环境差异:系统包版本、locale、时区、字体都可能。把这些统一写进声明文件。
私有源拉不动。 检查三件事:token 是否通过 secrets 注入、registry 地址是否配在固化配置里(而不是只在当前 shell 里 npm config set)、证书是否需要在镜像里预装。
工作区被撑满。 node_modules、构建产物、缓存三样叠起来很占空间。缓存和构建产物尽量放到工作区外的临时目录,或者定期清理。判断标准是"删了能不能重建"——能重建的就别当宝贝留着。
换设备后进度对不上。 说明上一轮的进度没写回仓库。养成习惯:阶段完成的最后一个动作是更新 AGENTS.md 并提交,而不是关掉窗口。
测试偶发失败。 时区、locale、并行度、随机种子都可能。先把这些在固化配置里统一(TZ、LANG),再看是不是真 bug。不要靠"重跑一次就过了"糊过去。
改了 setup 脚本但环境没变。 确认改动已经 push 到远端分支。云端环境读的是远端仓库,本地未提交的改动它看不见。
下一步建议
- 把
setup.sh和devcontainer.json纳入代码评审流程,改它们和改业务代码一个待遇。环境漂移的成本,通常在出问题那天才被看见。 - 给环境加一个自检脚本,新环境起来先跑一遍:检查关键命令是否存在、依赖目录是否就位、测试基础套件是否通过。几十秒换一个明确的起点,划算。
- 把常用命令做成
make目标或脚本,减少记忆负担和手敲错误:
```makefile
setup:
bash scripts/setup.sh
test:
npm test
check: test
npm run lint
resume:
@cat AGENTS.md | sed -n '/当前进度/,/验收/p'
```
make resume 这一条尤其省事:换设备第一件事就是它,两秒钟把"上次做到哪"读回来。
- 观察 setup 的耗时趋势。如果稳定在几分钟且内容长期不变,考虑把不变的部分固化进自定义镜像,只把变动部分留给脚本。
- 多个任务并行时留意缓存的并发写入。几个任务同时往同一个缓存目录写,偶发损坏是可能的。如果遇到诡异失败,先换一个独立缓存目录复现,判断是不是这个原因。
- 每隔一段时间回看一次:是不是还有"只在当前环境手动装过、没写进脚本"的东西。有就立刻补回去,这就是环境固化这件事的全部日常。
