ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Gas Town 子模块提交插件 submodule-commit 深度解析:自动提交子模块变更并同步父仓库指针

Gas Town 子模块提交插件 submodule-commit 深度解析:自动提交子模块变更并同步父仓库指针 Gas Town 子模块提交插件 submodule-commit 深度解析自动提交子模块变更并同步父仓库指针【免费下载链接】gastownGas Town - multi-agent workspace manager项目地址: https://gitcode.com/GitHub_Trending/ga/gastown导读Gas Town 是一个多 Agent 工作区管理器其 Deacon 巡逻周期patrol cycle会定期执行各类自动化插件。submodule-commit插件专门解决一个现实痛点——Agentpolecat只被授权在父仓库工作区操作、没有子模块仓库的提交权限导致子模块内积累的变更无人处理、父仓库的子模块指针长期过期。本文基于 plugins/submodule-commit/plugin.md 与其配套脚本 plugins/submodule-commit/run.sh完整讲解该插件的声明元数据、按 rig 的 opt-in 机制、两步执行流程以及结果上报方式并结合internal/plugin源码揭示其底层运行原理让读者既能直接部署使用也能理解如何扩展自己的 Gas Town 插件。插件解决的问题子模块变更的权限真空在 Gas Town 的工作模型中polecat执行 Agent只负责在父仓库的 worktree 中工作并且没有对子模块仓库的提交授权。这意味着子模块内部会持续累积改动依赖更新、构建产物、配置变更等但没有任何 Agent 有义务去提交它们父仓库中记录的子模块指针gitlink会与实际子模块 HEAD 脱节导致git submodule status永远显示过期时间一长子模块与父仓库之间的状态差异会累积成难以梳理的技术债。submodule-commit插件正是填补这一空白的定时任务它扫描启用该插件的 rig 仓库自动把子模块内累积的变更提交到已知分支并在条件满足时更新父仓库的子模块指针。该插件属于opt-in显式启用模式——rig 必须在自己的plugin.mdfrontmatter 中显式开启插件不会对未启用的 rig 做任何操作。从文档记录看当前已启用该插件的 rig 是lilypad_chat含 3 个 Bitbucket 子模块这个信息可以作为部署时的参照。插件清单文件声明元数据与执行约束每个 Gas Town 插件都由一个plugin.md文件定义文件以 TOML frontmatter 声明元数据正文为执行指令。submodule-commit的完整 frontmatter 如下 name submodule-commit description Auto-commit accumulated changes inside git submodules and update parent pointer version 1 [gate] type cooldown duration 2h [tracking] labels [plugin:submodule-commit, category:git-hygiene] digest true [execution] timeout 15m notify_on_failure true severity low 各字段的语义可以在 internal/plugin/types.go 的Plugin、Gate、Tracking、Execution结构体中找到对应的 Go 定义字段值含义namesubmodule-commit插件唯一标识用于调度、查询和结果记录descriptionAuto-commit ...人类可读的插件说明version1插件 schema 版本号为未来演进预留[gate] typecooldown门控类型为冷却期cooldown距离上次运行足够久才再次执行[gate] duration2h冷却时长为 2 小时即每 2 小时最多触发一次[tracking] labels[plugin:submodule-commit, category:git-hygiene]执行痕迹wisp/bead上附加的标签便于检索与统计[tracking] digesttrue运行结果会进入每日摘要daily digest[execution] timeout15m单次执行的最大耗时超过即视为失败[execution] notify_on_failuretrue失败时触发通知/升级[execution] severitylow失败升级的严重级别为 low关于 gate 的更多类型types.go中定义了完整的门控体系cooldown冷却、cron定时、condition条件命令返回 0 才执行、event事件触发如 startup、manual手动触发永不自动执行。submodule-commit使用cooldown2h的组合既保证了变更能被及时清理又避免频繁扫描造成无谓开销。按 rig 的 opt-in 配置插件默认不启用rig 需要通过自己的插件 frontmatter 显式开启并可按需调整三个行为参数[plugin.submodule-commit] enabled true commit_branch main # 在每个子模块中提交的目标分支 push_enabled false # 是否推送子模块提交false 仅本地提交 allowlist [] # 空 处理所有子模块[path/to/sub] 仅处理列出的子模块三个参数的运行逻辑如下enabled插件扫描脚本通过gt rig show rig --json读取.plugins[submodule-commit].enabled只有值为true时该 rig 才会进入处理队列commit_branch子模块提交所在的分支默认main。脚本实际以子模块当前检出分支为准见下文已检出分支检测该参数用于指定预期分支语义push_enabled控制子模块提交后是否git push到远端。默认false表示只在本地提交、保留远端干净allowlist子模块路径白名单空数组表示处理.gitmodules中声明的全部子模块非空时只处理精确匹配路径的子模块。这段配置与插件发现机制直接相关。在 internal/plugin/scanner.go 中可以看到Gas Town 的插件分布在两个层级城镇级town-level~/gt/plugins/全局生效与 rig 级rig-levelrig/plugins/项目专属同名的 rig 级插件会覆盖城镇级插件。submodule-commit作为城镇级插件的定义与每个 rig 自己的启用配置是分离的——定义一处启用在各自 rig 中声明这正是该插件 opt-in 模式的实现基础。执行流程 Step 1发现启用插件的 rig插件的第一个阶段是枚举所有 rig找出「存在.gitmodules且启用了submodule-commit」的 rig。核心逻辑如下完整脚本见 plugins/submodule-commit/run.shRIG_JSON$(gt rig list --json 2/dev/null || true) if [ -z $RIG_JSON ]; then echo SKIP: could not get rig list exit 0 fi ENABLED_RIGS() while IFS read -r REPO_PATH; do [ -z $REPO_PATH ] continue [ ! -f $REPO_PATH/.gitmodules ] continue # 必须含子模块声明 RIG_NAME$(basename $REPO_PATH) PLUGIN_CONFIG$(gt rig show $RIG_NAME --json 2/dev/null | jq -r .plugins[submodule-commit].enabled // false 2/dev/null || echo false) if [ $PLUGIN_CONFIG true ]; then ENABLED_RIGS($REPO_PATH) fi done (echo $RIG_JSON | jq -r .[] | select(.repo_path ! null) | .repo_path // empty 2/dev/null) if [ ${#ENABLED_RIGS[]} -eq 0 ]; then echo SKIP: no opt-in rigs with submodules found exit 0 fi这段脚本的几个关键设计点优雅降级gt rig list --json失败或返回空时直接SKIP退出不产生错误单个 rig 的 JSON 解析失败也回退为false宁可漏过也不误伤双重过滤先检查$REPO_PATH/.gitmodules文件是否存在没有子模块的 rig 直接跳过再检查插件是否启用计数汇总ENABLED_RIGS数组累加匹配项为空时输出提示并退出 0避免后续无谓循环。执行流程 Step 2逐个处理 rig 的子模块第二阶段是核心处理循环对每个启用 rig 执行「提交子模块变更 → 按需推送 → 更新父仓库指针」三步操作。读取插件配置与子模块清单RIG_CONFIG$(gt rig show $RIG_NAME --json 2/dev/null | jq -r .plugins[submodule-commit] // {} 2/dev/null || echo {}) COMMIT_BRANCH$(echo $RIG_CONFIG | jq -r .commit_branch // main) PUSH_ENABLED$(echo $RIG_CONFIG | jq -r .push_enabled // false) ALLOWLIST$(echo $RIG_CONFIG | jq -r .allowlist // [] | .[] 2/dev/null || true) SUBMODULE_PATHS$(git -C $REPO_PATH config --file .gitmodules --get-regexp submodule\..*\.path 2/dev/null | awk {print $2} || true)配置读取全部带默认值兜底// main、// false、// []即使 rig 只写了enabled true也能正常运行。子模块路径直接通过git config --file .gitmodules --get-regexp submodule\..*\.path从.gitmodules解析不依赖git submodule命令的子命令输出格式兼容性更好。逐子模块提交变更对每个子模块路径依次执行# 白名单过滤 if [ -n $ALLOWLIST ]; then MATCHfalse while IFS read -r ALLOWED; do [ $SUB_PATH $ALLOWED ] MATCHtrue break done $ALLOWLIST $MATCH || continue fi # 未初始化检查 FULL_SUB$REPO_PATH/$SUB_PATH if [ ! -d $FULL_SUB/.git ] [ ! -f $FULL_SUB/.git ]; then echo SKIP: $SUB_PATH — not initialized continue fi # 脏状态检查status --porcelain 首行非空即有变更 SUB_DIRTY$(git -C $FULL_SUB status --porcelain 2/dev/null | head -1 || true) if [ -z $SUB_DIRTY ]; then echo $SUB_PATH: clean continue fi # detached HEAD 检查 SUB_BRANCH$(git -C $FULL_SUB branch --show-current 2/dev/null || true) if [ -z $SUB_BRANCH ]; then echo SKIP: $SUB_PATH — detached HEAD, skipping continue fi安全护栏非常明确一个子模块只有在满足全部条件时才会被提交通过白名单若配置了.git目录或文件存在子模块已initgit status --porcelain有输出确实存在变更当前 HEAD 不是 detached 状态处于已知分支提交不会丢失上下文。提交与推送逻辑git -C $FULL_SUB add -A 2/dev/null || true STAGED$(git -C $FULL_SUB diff --cached --name-only 2/dev/null | wc -l | tr -d ) if [ $STAGED -gt 0 ]; then git -C $FULL_SUB commit -m chore: accumulated changes [skip ci] Auto-committed by submodule-commit plugin ($STAGED file(s)). \ --authorGas Town gastownlocal 2/dev/null \ echo Committed $STAGED file(s) \ TOTAL_COMMITTED$((TOTAL_COMMITTED 1)) || \ { echo WARN: commit failed; continue; } # Push尽力而为失败仅告警不中断 if [ $PUSH_ENABLED true ]; then git -C $FULL_SUB push origin $SUB_BRANCH 2/dev/null \ TOTAL_PUSHED$((TOTAL_PUSHED 1)) || \ echo WARN: push failed (local commit preserved) fi PARENT_CHANGEDtrue fi提交信息的几个细节值得注意[skip ci]标记避免 CI 流水线被机器人提交触发--authorGas Town gastownlocal统一署名便于从历史中识别自动化提交推送采用尽力而为策略失败只打 WARN 且本地提交保留绝不因推送失败回滚已完成的提交。这正是文档中 local commit is priority 原则的体现——子模块提交一旦产生就应当被保留。更新父仓库子模块指针子模块有变更后父仓库的 gitlink 也过时了。只有满足全部条件时插件才更新父仓库指针if $PARENT_CHANGED; then PARENT_BRANCH$(git -C $REPO_PATH branch --show-current 2/dev/null || true) if [ $PARENT_BRANCH main ]; then # 只在 main 分支上更新 PARENT_DIRTY$(git -C $REPO_PATH status --porcelain 2/dev/null | grep -v ^?? | head -1 || true) if [ -z $PARENT_DIRTY ]; then # 父仓库无其他未提交变更 git -C $REPO_PATH add -A -- *.gitmodules $(git -C $REPO_PATH status --short 2/dev/null | awk {print $2}) 2/dev/null || true PARENT_STAGED$(git -C $REPO_PATH diff --cached --name-only 2/dev/null | head -1 || true) if [ -n $PARENT_STAGED ]; then git -C $REPO_PATH commit -m chore: update submodule pointers [skip ci] Auto-committed by submodule-commit plugin. \ --authorGas Town gastownlocal 2/dev/null \ TOTAL_PARENT_UPDATED$((TOTAL_PARENT_UPDATED 1)) || true git -C $REPO_PATH push origin main 2/dev/null || echo WARN: parent push failed (local commit preserved) fi else echo SKIP: parent repo dirty, not updating submodule pointer fi else echo SKIP: parent repo on $PARENT_BRANCH (not main), not updating pointer fi fi父仓库指针更新的三重保护仅在main分支$PARENT_BRANCH ! main时直接跳过避免在功能分支上产生指针变更污染 PR父仓库必须干净status --porcelain | grep -v ^??排除了未跟踪文件后仍为空才允许提交——防止把 Agent 正在进行的工作混入自动化提交只暂存指针相关变更add -A -- *.gitmodules ...精确圈定子模块指针文件避免git add -A误伤其他文件。这两层保护性跳过SKIP是插件安全设计的精髓宁可本次不更新也不要在脏状态下制造混乱提交。结果记录gt plugin record-run 上报处理完成后脚本汇总三类计数并上报运行结果SUMMARYsubmodule-commit: $TOTAL_COMMITTED submodule(s) committed, $TOTAL_PUSHED pushed, $TOTAL_PARENT_UPDATED parent pointer(s) updated echo echo Submodule Commit Summary echo $SUMMARY RESULTsuccess [ -n $ERRORS ] RESULTwarning gt plugin record-run --plugin submodule-commit --result $RESULT \ --title $SUMMARY --description $SUMMARY /dev/null 21 || truerecord-run的底层实现在 internal/plugin/recording.go 中Recorder.RecordRun会创建一个ephemeral临时bead并自动附加type:plugin-run、plugin:submodule-commit、result:success|failure|skipped等标签然后立即关闭该收据receipt。这些运行记录的价值在于冷却门控的数据源[gate] type cooldown通过查询最近 2 小时内的运行记录GetRunsSince/CountRunsSince见recording.go判断是否允许再次执行审计与检索每次运行都有可查询的 bead带时间戳、标题和结果标签可通过gt plugin list等命令追溯历史失败升级[execution] notify_on_failure true与severity low结合运行失败时触发低优先级升级通知。run.sh 与 plugin.md 的关系脚本优先执行本插件目录下同时存在 plugin.md指令文档与 run.sh可执行脚本。两者内容高度一致——run.sh是plugin.md中 bash 片段的完整化、日志化实现增加了log()函数、set -euo pipefail、每步输出[submodule-commit]前缀日志等。这种双文件结构在 Gas Town 插件体系中有明确约定internal/plugin/scanner.go的loadPlugin会检测plugin.md同目录下是否存在run.sh存在则置HasRunScript true而types.go的FormatMailBody在HasRunScript为真时会指示 dog worker直接执行脚本而不是解读 markdown 指令Execute the following plugin script ...cd plugin_dir bash run.sh... Do NOT interpret the plugin.md instructions. Do NOT write your own implementation.这意味着对于逻辑确定、需要严格一致执行的插件提供run.sh可以避免 Agent 每次对自然语言指令做出不同解释保证行为可复现。这也是为什么本文建议部署时以run.sh为权威执行入口、plugin.md为说明文档的原因。部署与使用小结将submodule-commit插件投入使用的完整路径安装插件确保插件目录存在于城镇级~/gt/plugins/submodule-commit/含plugin.md与run.sh或通过gt plugin sync从仓库同步见 internal/cmd/plugin.go 的gt plugin sync --source ./plugins --clean用法rig 中启用在目标 rig 的插件配置中写入[plugin.submodule-commit]并设enabled true按需调整commit_branch、push_enabled、allowlist等待巡逻周期触发Deacon 巡逻周期发现插件后由 cooldown gate2h决定触发时机dog worker 执行run.sh查看结果运行摘要通过gt plugin record-run写入可在每日 digestdigest true或gt plugin list中查看。执行中常见的预期行为包括SKIP: not initialized子模块未git submodule update --init、SKIP: detached HEAD子模块处于游离头、SKIP: parent repo dirty父仓库有其他未提交变更——这些都是插件的安全设计而非故障重新满足条件后会在下个周期自动处理。若涉及推送push_enabled true需确保运行环境具备对应远端的认证凭据。参考实现与源码索引关注点文件插件定义与执行入口plugins/submodule-commit/plugin.md完整可运行脚本plugins/submodule-commit/run.sh插件数据结构与执行类型internal/plugin/types.go插件发现与 frontmatter 解析internal/plugin/scanner.go运行结果记录bead 收据internal/plugin/recording.go插件同步与漂移检测internal/plugin/sync.gogt plugin list/sync/record-run命令internal/cmd/plugin.go【免费下载链接】gastownGas Town - multi-agent workspace manager项目地址: https://gitcode.com/GitHub_Trending/ga/gastown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表