☰
gsd-core 安装器回滚快照范围修复:让 Codex 失败安装完整还原到安装前状态(PR 4760)
2026/9/28 2:42:28 网站建设 项目流程

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

导读

本文围绕 gsd-core 仓库中编号为#4760的 changeset 修复展开:当 Codex 运行时执行 gsd-core 安装/升级失败时,旧版安装器的回滚快照只覆盖config.toml、hooks.json、skills/、agents/与VERSION这几个条目,导致失败安装留下的部分 payload(如CHANGELOG.md、scripts/、.gsd-runtime、安装清单本身乃至整个hooks/目录)残留在目标机器上,环境无法恢复到真正安装前的状态。修复后的回滚快照改为以前一次安装清单(install manifest)记录的每一个文件为基准,并额外覆盖整个hooks/目录。读完本文,你将理解 gsd-core 安装器的清单驱动回滚模型、快照范围如何决定回滚保真度,以及如何通过源码与测试验证这次修复。

一、这次修复在修什么:changeset 原文拆解

关联文档.changeset/zesty-pumas-hum.md的完整内容是:

--- type: Fixed pr: 4760 --- **Codex rollback no longer leaves a partially-installed payload behind** — the installer's rollback snapshot now covers every file the previous install's manifest recorded (CHANGELOG.md, scripts/, .gsd-runtime, the manifest itself) plus the whole `hooks/` directory, instead of only config.toml, hooks.json, skills/, agents/ and VERSION, so a failed install reverts to the true pre-install state. (#4544)

逐句拆解可得到三个核心事实:

  1. 缺陷(issue#4544):Codex 运行时场景下,一次失败的安装会在磁盘上留下"部分安装的 payload"(partially-installed payload)。所谓 payload,指安装器实际写盘的产物,而不只是配置文件。
  2. 旧回滚快照范围过窄:此前回滚只保护config.toml、hooks.json、skills/、agents/与VERSION。这些恰恰是安装器"看得见"的常规条目,但安装动作实际影响的文件远不止这些。
  3. 修复后的快照范围(PR#4760):覆盖前一次安装清单(manifest)记录到的每一个文件——包括CHANGELOG.md、scripts/、.gsd-runtime、清单文件自身——再加上整个hooks/目录。这样失败安装能够回滚到"真正的安装前状态"(true pre-install state)。

二、回滚快照的范围为什么决定"是否真的回到安装前"

安装器的回滚(rollback)本质上是一个"撤销"动作:把安装过程中被写入、覆盖或移动的文件,恢复到动作发生之前的样子。要做到这一点,安装器必须先知道两件事:

  • 哪些路径被这次安装动过;
  • 这些路径原来的内容是什么(即快照)。

如果快照范围小于实际写盘范围,那么回滚之后必然有文件残留——这就是"部分安装的 payload 被留下"的根源。

旧版快照覆盖的是config.toml、hooks.json、skills/、agents/、VERSION。但从仓库的安装布局看,一次安装还会触碰:

  • CHANGELOG.md:安装/升级流程会更新版本说明文件;
  • scripts/:安装器自带的脚本集会被写入该目录;
  • .gsd-runtime:用于物化全局运行时表面(Runtime Surface)的目录,源码注释称之为"installation-owned input needed to re-materialize a global Runtime Surface",见 src/install-engine.cts 中provisionRuntimeSurfaceCorpus的说明;
  • 安装清单自身(the manifest itself):清单记录着本次安装的归属文件与哈希;
  • 整个hooks/目录:仓库中hooks/下实际包含gsd-prompt-guard.js、gsd-write-guard.js、gsd-workflow-guard.js等二十余个钩子脚本(参见 hooks/),它们是安装器复制到目标环境的核心执行代码。

这些路径一旦写盘失败,旧快照无法还原,就会成为脏残留。

三、修复后的快照范围:清单全量 + hooks/ 目录

修复后的回滚快照范围可以精确表述为两个集合的并集:

rollback_snapshot = every file recorded in the previous install's manifest + the whole hooks/ directory

其中"前一次安装清单记录的每一个文件"包括CHANGELOG.md、scripts/、.gsd-runtime与清单本身;hooks/目录则被整体纳入,不再只挑其中的hooks.json。

这一设计的关键在于**"以清单为唯一事实来源"**:安装器不再硬编码一份"回滚白名单",而是读取上一次安装留下的清单,凡是清单证明"归 GSD 所有"的路径,全部纳入回滚快照。清单中没有记录的路径(比如用户自己放进目录的其他内容)不会被误删,也不会被回滚波及——这正是源码注释里强调的"Preserve existing entries rather than guessing that they are stale"(见下文)。

四、源码级剖析:清单如何驱动回滚

4.1 读取前一次安装清单:previousOwnedCorpusFiles

在 src/install-engine.cts 中,previousOwnedCorpusFiles(configDir, prefix)是这次修复的核心函数之一:

function previousOwnedCorpusFiles(configDir: string, prefix: string): string[] { try { const files = installerMigrations.readInstallManifest(configDir).files; return Object.keys(files) .filter((entry) => entry.startsWith(prefix)) .map((entry) => entry.slice(prefix.length)) .filter((entry) => entry !== '' && !path.posix.isAbsolute(entry) && !entry.split('/').some((part) => part === '' || part === '.' || part === '..')); } catch { // An absent or unreadable prior manifest provides no ownership evidence. // Preserve existing entries rather than guessing that they are stale. return []; } }

要点:

  • 数据来源是readInstallManifest(configDir),即仓库 src/installer-migrations.cts 中负责解析安装清单(仓库内可见的产物名称为gsd-file-manifest.json,在 src/install-engine.cts 的注释中被提及)的接口;
  • 过滤条件保证只返回"清单证明属于 GSD、且位于受控前缀之下"的合法相对路径;
  • 若清单缺失或不可读,函数返回空数组——此时安装器宁可保留现状也不猜测删除,这是一种保守的失败模式(fail-safe)。

4.2 只删清单证明归属的路径:syncRuntimeSurfaceCorpus

src/install-engine.cts 的syncRuntimeSurfaceCorpus展示了"清单驱动的清理"在写盘前的实际形态:

// Remove only paths the previous manifest proves GSD owned and which the // executing package no longer ships. Unknown neighbouring files survive. for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) { const sourceEntry = path.join(source, ...relative.split('/')); let sourceIsFile = false; try { sourceIsFile = installFs().lstatSync(sourceEntry).isFile(); } catch { sourceIsFile = false; } if (sourceIsFile) continue; const target = path.join(destination, ...relative.split('/')); if (!installFs().existsSync(target)) continue; const targetStat = installFs().lstatSync(target); if (!targetStat.isFile()) { throw new Error(`Runtime Surface corpus ownership conflict at ${target}`); } installFs().rmSync(target, { force: true }); pruneEmptyCorpusParents(target, destination); }

这段逻辑与本次修复遵循同一原则:只有前一次清单能够证明"归 GSD 所有"的路径才允许被移除,用户放入的无关文件(unknown neighbouring files)一律保留;遇到归属冲突(目标不是普通文件)则直接抛错中止,而不是强行删除。

4.3 目录级深度快照:_snapshotDir/_restoreDir

回滚快照的底层实现位于 src/install-engine.cts,一对配套函数负责把目录树深拷贝进内存、再按需还原:

// Deep-snapshot a directory tree into a Map<relPath, Buffer>. function _snapshotDir(dir: string): Map<string, Buffer> { ... } // Restore a directory tree from a Map<relPath, Buffer> produced by _snapshotDir. function _restoreDir(dir: string, snapshot: Map<string, Buffer>): void { for (const [relPath, buf] of snapshot) { ... } }

_snapshotDir把目录树变成Map<相对路径, Buffer>,_restoreDir再按这个映射逐项写回。这种"先整体快照、失败后整体还原"的模式,正是"覆盖清单每一个文件 + 整个 hooks/ 目录"这类宽范围快照得以落地的机制。需要说明的是,源码中还提到用户内容(如skills/下的用户文件)在清理前也会被_snapshotDir预快照(见 src/install-engine.cts),说明快照机制同时兼顾"还原安装产物"与"保护用户数据"两条线。

4.4 迁移引擎中的回滚执行:rollbackAppliedMigrationResult

在迁移(installer migration)场景中,回滚由 src/installer-migrations.cts 的rollbackAppliedMigrationResult执行:

  • 每个将被修改的托管路径在应用前会被复制到回滚根目录rollbackRoot(路径形态为gsd-migration-journal/<runId>-rollback,见 src/installer-migrations.cts);
  • 复制时使用copyPreservingSymlink,其注释明确说明:GSD 安装的产物不会是符号链接,因此忠实快照(restore 时重建同样的链接)能在不读取链接目标内容的前提下保证回滚保真(src/installer-migrations.cts);
  • 应用失败时按逆序(rollback.reverse())逐条还原(src/installer-migrations.cts);若回滚本身也有失败条目,则抛出携带rollbackFailures明细的错误,而不是静默吞掉;
  • 成功后清理rollbackRoot与 journal 产物(cleanupMigrationRunArtifacts),回滚目录本身是临时产物,不长期驻留。

rollbackInstallerMigrations是安装结果对外暴露的回滚入口(在 src/install-engine.cts 中被导出),调用方拿到后可在安装失败时主动执行回滚。

五、测试佐证:回滚入口在异常路径上仍然可用

仓库测试 tests/install.test.cjs 为该机制提供了直接佐证。其中一条测试名为:

malformed settings.local.json still returns configuredEntrypoints and a working rollbackInstallerMigrations

它断言:即便settings.local.json无法解析(一次典型的安装失败前置条件),install()返回结果中仍然带有configuredEntrypoints与可调用的rollbackInstallerMigrations;并且调用rollbackInstallerMigrations()时不得抛出异常("must not throw when called on this early-return path")。注释还特别强调:

若该早期返回路径遗漏rollbackInstallerMigrations,后续的rollbackFinalizedInstallerMigrations将无条件读取它,从而静默丢失该运行时的回滚能力。

这意味着回滚能力被视作安装契约的一部分:只要安装入口被执行,无论中途是否异常,调用方都必须能拿到一个可用且可调用的回滚函数。这正是"失败安装不能留下部分 payload"这条修复目标的测试侧保障。

六、如何在实际环境中验证这次修复

如果你在部署或升级 gsd-core 后怀疑残留了失败安装的 payload,可以按以下思路排查(仓库为只读资源,以下均为本地观察手段):

  1. 查看安装清单:在配置目录下查找安装器写入的清单文件(仓库内可见的产物名为gsd-file-manifest.json,其解析入口为 src/installer-migrations.cts 的readInstallManifest),清单中files键列出的路径即"安装器认定归 GSD 所有"的集合。
  2. 核对回滚日志目录:迁移运行期间,回滚快照位于配置目录下gsd-migration-journal/<runId>-rollback(路径拼接见 src/installer-migrations.cts);若存在残留的 journal/rollback 目录,说明上一次运行没有干净收尾。
  3. 对比安装前快照:修复后,CHANGELOG.md、scripts/、.gsd-runtime、清单自身以及整个hooks/目录都应在回滚覆盖范围内;如果失败后这些路径仍有改动痕迹,而回滚已成功执行,则属于异常,应提交 issue。

七、这次修复的工程启示

从 changeset 到源码,这次修复浓缩了三条可迁移的工程原则:

  1. 快照范围必须等于真实写盘范围。任何"覆盖了常规条目却漏掉实际产物"的快照,在失败回滚时都会留下脏残留;以安装清单为唯一事实来源枚举受影响路径,比硬编码白名单更不容易腐化。
  2. 清单缺失时的默认动作是"保守保留"。previousOwnedCorpusFiles在清单不可读时返回空集("Preserve existing entries rather than guessing"),避免安装器在信息不足时误删用户数据。
  3. 回滚能力是安装契约的一部分。rollbackInstallerMigrations必须在任何早期返回路径上都保持可调用(tests/install.test.cjs),否则回滚能力的静默丢失比安装失败本身更危险。

本次修复同时更新了仓库内的迁移实现(相关迁移文件可见 src/installer-migrations/003-rename-get-shit-done-to-gsd-core.cts 等,它们在installer-migrations目录内统一演进),从"只还原配置"升级为"还原到真正的安装前状态"——这正是"Git. Ship. Done"所要求的可失败、可恢复、可重试的安装体验。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载
上一篇:Duix-Avatar 本地部署 4 步走:免费跑虚拟形象视频合成实操教程
下一篇:终极指南:10分钟掌握C语言自编译神器c4编译器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询