【免费下载链接】gsd-core
Git. Ship. Done - 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)逐句拆解可得到三个核心事实:
- 缺陷(issue
#4544):Codex 运行时场景下,一次失败的安装会在磁盘上留下"部分安装的 payload"(partially-installed payload)。所谓 payload,指安装器实际写盘的产物,而不只是配置文件。 - 旧回滚快照范围过窄:此前回滚只保护
config.toml、hooks.json、skills/、agents/与VERSION。这些恰恰是安装器"看得见"的常规条目,但安装动作实际影响的文件远不止这些。 - 修复后的快照范围(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,可以按以下思路排查(仓库为只读资源,以下均为本地观察手段):
- 查看安装清单:在配置目录下查找安装器写入的清单文件(仓库内可见的产物名为
gsd-file-manifest.json,其解析入口为 src/installer-migrations.cts 的readInstallManifest),清单中files键列出的路径即"安装器认定归 GSD 所有"的集合。 - 核对回滚日志目录:迁移运行期间,回滚快照位于配置目录下
gsd-migration-journal/<runId>-rollback(路径拼接见 src/installer-migrations.cts);若存在残留的 journal/rollback 目录,说明上一次运行没有干净收尾。 - 对比安装前快照:修复后,
CHANGELOG.md、scripts/、.gsd-runtime、清单自身以及整个hooks/目录都应在回滚覆盖范围内;如果失败后这些路径仍有改动痕迹,而回滚已成功执行,则属于异常,应提交 issue。
七、这次修复的工程启示
从 changeset 到源码,这次修复浓缩了三条可迁移的工程原则:
- 快照范围必须等于真实写盘范围。任何"覆盖了常规条目却漏掉实际产物"的快照,在失败回滚时都会留下脏残留;以安装清单为唯一事实来源枚举受影响路径,比硬编码白名单更不容易腐化。
- 清单缺失时的默认动作是"保守保留"。
previousOwnedCorpusFiles在清单不可读时返回空集("Preserve existing entries rather than guessing"),避免安装器在信息不足时误删用户数据。 - 回滚能力是安装契约的一部分。
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
相关推荐
gsd-core 修复 Codex 安装兼容性:TOML 浮点解析与五表面原子回滚详解
gsd core 修复 Codex 安装兼容性:TOML 浮点解析与五表面原子回滚详解 导读 本文深入剖析 gsd core(Git. Ship. Done —
GSD-Core 仓库本地 Agent 安装检测:--local 安装为何报 agents_installed 为 false 及其修复原理
GSD Core 仓库本地 Agent 安装检测: local 安装为何报 agents_installed 为 false 及其修复原理 本篇以 kind m
解决ASDF安装失败:从错误排查到状态修复的终极指南
解决ASDF安装失败:从错误排查到状态修复的终极指南 ASDF Another System Definition Framework 是一个多语言版本管理器,
CLI开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考