CI 实战GitHub Actions Vercel 沙箱跑 scriptc多平台构建与差分验证流水线搭建实录【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptcscriptc 是一个把 TypeScript 直接编译成原生可执行文件与 WebAssembly 的实验性编译器静态构建不依赖 Node.js 或任何 JavaScript 引擎CLI 以自托管原生二进制分发npm 依赖和any类型代码通过内嵌 quickjs-ng 的动态岛--dynamic兜底。这样一个编译器项目的 CI难点和普通 Web 项目完全不同——它要同时验证 TypeScript 前端、LLVM 后端、C 运行时、预编译运行时包和跨平台链接器行为还要保证编译产物与 Node 行为一致这条底线。本文基于仓库内真实流水线工程scripts/下的测试编排与沙箱驱动、tests/harness/下的差分语料梳理这套 CI 是如何用 GitHub Actions 矩阵加 Vercel 沙箱跑通的。双轨验证矩阵分片 全量差分scriptc 的测试体系把提交门槛和可选深度验证分得很清楚。根目录 package.json 里test:sandbox是唯一被 AGENTS.md 标注为 full gate全量验证门槛的入口本地开发则用SCRIPTC_TEST_WORKERS4 pnpm test与SCRIPTC_SAN1 pnpm test两个本地回退车道。门槛的核心不是跑没跑过而是和 Node 逐字节一致。语料测试corpus的契约写在 tests/harness/differential.test.ts 的头部注释里每个语料程序同时在 Node 和 scriptc 编译出的原生二进制下运行stdout 与 stderr 必须逐字节相等、退出码必须一致而且没有黄金文件——Node 本身就是期望输出所以测试永远不会漂移。这是整条 CI 的地基任何修改只要让某个语料程序的输出偏离 Node门槛直接失败不存在人工比对或快照过期的空间。同一套语料还有第二道闸门 tests/harness/llvm-differential.test.ts它用 release 与 dev 两种 LLVM 优化模式分别编译每个语料程序任何后端拒绝backend refusal都算失败SCRIPTC_SAN1时还会把生成的函数和运行时用 AddressSanitizer 插桩。两道差分测试共享同一个分片切片SCRIPTC_TEST_SHARD这样同一分片的编译缓存能同时服务两条车道。CI 架构分片矩阵与 job 内并行编排GitHub Actions 侧的核心是 scripts/ci-test-plan.mjs 的ciTestPlan它解析shardi/n格式和flavorplain/san把一个矩阵 job 拆成两块——case-sharded corpus负责八个大型语料套件differential、llvm-differential、npm、server、test262、vercel-e2e、three、effect用SCRIPTC_TEST_SHARD在文件内部按用例粒度分片file-sharded tests则把其余测试文件按--shard分片并附带每个分片独有的侧任务自托管序列化、fetch、npm-static、surface manifest 等昂贵套件按 owner 轮值到不同分片native-cache 正确性与 coverage sweep 只在plainflavor 执行因为 sanitizer 不改变前端覆盖率结果。执行器在 scripts/ci-test.mjscorpus 与侧任务用sideWorkers决定串行还是并行只有一个 worker 时串行否则并行避免大型编译互相抢 worker 导致超时最后用 AggregateError 汇总所有失败并逐项报时。这套编排把矩阵分片从按文件均分细化到了按用例均分 昂贵套件定点轮值是墙钟时间能压下来的关键。跨 job 产物传递则体现在发布流水线上RELEASING.md 记录了 release workflow 的职责切分——CI 先比较packages/cli/package.json版本与 npm 上的差异随后在 macOS、Linux、Windows 三类 runner 上分别构建平台包cli、llvm helper、runtime pack校验版本脊线后按依赖顺序发布发布成功的后续 job 再创建vversion标签和 GitHub Release并附带surface-manifest.json——这份机器可读的静态层能力清单稳定按 entry id 组织让两个版本之间的能力差异可以机械 diff同时作为发布产物的可审计附件。LLVM 22 钉版与无 clang契约编译型工具链项目最怕的就是环境漂移本地能过、CI 过不了或者一个月后同一份代码跑出不同结果。scriptc 的做法是双管齐下——钉版本 钉环境变量。版本钉在 Dockerfile.sandboxARG NODE_VERSION24.15.0、PNPM_VERSION11.1.3、ZIG_VERSION0.16.0并通过 apt.llvm.org 的llvm-toolchain-noble-22源安装clang-22、llvm-22-dev、libclang-rt-22-dev装完立刻断言test $(llvm-config-22 --version) 22.1.8——版本不符直接构建失败而不是带着错版本往下走。Node 二进制还做了 SHA-256 严格校验sha256sum --check --strict -。更值得注意的是无 clang契约managed 镜像默认的vercel/sandbox/universal故意浮动发行版与语言运行时所以 scripts/sandbox-bootstrap.sh 在沙箱里做两件事——第一从 apt.llvm.org 安装与自定义镜像完全相同的 LLVM 22 与 Node/pnpm 组合保证差分 oracle 不会因为 managed 镜像被刷新而改变第二sudo rm -f /usr/local/bin/clang并只通过clang-22符号链接暴露编译器。这把环境中恰好有个 clang从偶然变成可测试的前提运行时包构建用CC: zig、AR: zig见 scripts/sandbox-test.mjs 的 runtime pack 步骤而工具链契约测试 packages/compiler/src/backend/native-toolchain.test.ts 在SCRIPTC_TEST_STABLE_TOOLCHAIN被删除的严格路径下运行——它会就地替换 wrapper、头文件、SDK 选择器和原生输入验证缓存身份与失效行为不能依赖任何恰好存在的系统编译器。cache 分片反过来显式钉SCRIPTC_TEST_STABLE_TOOLCHAIN: 0走生产路径的严格契约。这套设计的结论是差分语料可以安全地 memoize 工具链指纹因为工具链是自证不变的。Vercel 沙箱一次性容器跑全量差分语料沙箱驱动是这套 CI 里工程含量最高的部分主入口 scripts/sandbox-test.mjs 完整实现了一次性容器跑全量语料的闭环preflight先验证 Vercel CLI 可用、认证与项目作用域可达vercel sandbox list并明确打印 auth source、scope source、镜像来源和沙箱规模避免跑完 45 分钟才发现凭证失效。打包上传用git ls-files --cached --others --exclude-standard打 worktree tarball经 scripts/worktree-files.mjs 过滤掉已删除的脏工作树路径否则 dirty-worktree gate 会在任何测试运行前失败上传后用tar -tzf校验 gzip 流完整性——只校验非空会放过半途断传的文件。一次性沙箱vercel sandbox create --non-persistent默认 8 vCPU、45 分钟超时名称带随机 noncescriptc-lane-shard-nonce结束时统一 stop 清理SIGINT/SIGTERM 也会先停沙箱再退出。远程引导解包后在/workspace执行pnpm install --frozen-lockfile、pnpm build:fresh再单独构建 Linux 的 LLVM helper 与 runtime packscriptc/llvm-linux-x64-gnu和scriptc/runtime-linux-x64-gnu并把 zig 软链到隔离的构建工具目录、sudo rm -f /usr/local/bin/zig——和 clang 一样的思路系统里的工具要么是我钉好的要么就不存在。双 lane 全量plain与sanASan 运行时引用计数审计两条 lane 各自分 8 片语料用例按SCRIPTC_TEST_SHARD切片同时用 worker 预算函数sandboxEffectWorkerAllocation在side 进程file 分片、cache 用例、native bootstrap与case 语料之间静态分配 Vitest worker保证大编译不饿死语料、side 进程不超预算。沙箱机制上最值得学习的一个细节是退出码vercel sandbox exec不传播远程进程的退出码scripts/sandbox-test.mjs 的解决办法是非ce 标记——远程命令结束时打印一个带随机数的__SCRIPTC_REMOTE_EXIT_nonce__code标记本地从输出流里捕获它来判定远程成败CLI 自身超时/断连时再用状态探测脚本轮询远程命令是否已记录完成并tail -n 160恢复日志。这保证了网络抖动导致 CLI 没确认不会被误判为测试失败也不会因为静默丢日志而漏掉真正的失败。认证层面 scripts/sandbox-config.mjs 做得同样严谨首选VERCEL_OIDC_TOKEN项目作用域 JWT从 claims 里提取 owner_id/project_id 作为 VCR 作用域回退VERCEL_TOKEN 显式VERCEL_TEAM_ID/VERCEL_PROJECT_IDOIDC 优先时主动删除VERCEL_TOKEN避免 Vercel wrapper 偏好存储 token 导致凭证来源不明确。镜像选择上默认 managed 镜像启动后现装工具链冷启动约 9 分钟也可用SCRIPTC_SANDBOX_IMAGE指向预构建的 VCR 镜像约 4 分钟或用SCRIPTC_SANDBOX_SNAPSHOT复用带钉版工具链的 prepared snapshot——三者互斥从 scripts/sandbox-config.mjs 的sandboxTestSourceConfig可以清楚看到这套取舍。落地的坑与收益坑一CLI 退出码不可信。远程 exec 不传播退出状态必须自己发明非ce 标记 状态探测 日志恢复三层兜底否则命令完成但 CLI 未确认和真的失败无法区分——这是沙箱编排里最容易踩、也最隐蔽的坑。坑二环境漂移会静默污染差分。managed 镜像刷新后distro 与语言运行时都会浮动。解法不是信任镜像而是在沙箱里重装仓库钉版的 Node/pnpm/LLVM 组合并把系统 clang、zig 这类工具删掉或钉死让差分 oracle 只依赖仓库自证不变的工具链。坑三脏工作树会在测试前炸掉 gate。tar 打包遇到未提交删除的文件会让解包/校验在跑测试前就失败。仓库用git ls-files流式过滤 lstat 探活把这类噪音挡在打包阶段。收益是双重的。时间维度上AGENTS.md 给出的门槛数字是自定义镜像约 4 分钟、冷启动 managed 回退约 9 分钟跑完整套差分 gate——语料分片 side 任务并行 工具链预热 缓存身份复用共同压出来的结果而 45 分钟沙箱超时、8 vCPU、4 workers 这些默认值本身就是全量语料在一次性容器里跑得完的经验标尺。信心维度上Node 作为 oracle 的逐字节差分、双 lane 的 ASan RC 审计、LLVM 22 钉版与无 clang契约、以及发布时随附的 machine-readable surface manifest——这套流水线把TypeScript 编译成原生且行为不漂移从口号变成了每个提交都必须通过的机械证明。对一个编译器来说没有比这更值钱的 CI 了。【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考