☰
Positron CI 镜像批量重建实战:用 update-ci-images Skill 驱动 7 个多架构镜像的一次性升级
2026/10/13 19:57:11 网站建设 项目流程
  • 开发工具
  • 代码编辑器
  • 数据科学

【免费下载链接】positron

Positron, a next-generation data science IDE

项目地址:https://gitcode.com/gh_mirrors/po/positron
点击查看免费下载

Positron 的 CI 测试环境依赖一组预构建的 Docker 镜像(ubuntu24_04、rocky_8、rocky_9、debian、openSUSE15_6、SLES15_6 与 postgres),当 Node.js 需要升级或镜像需要随依赖打上统一的新 tag 时,逐个手工触发构建、盯日志、修错误代价高昂。update-ci-images是一个面向长期编排的 Agent Skill,它把"新建分支 → 改 Node 版本 → 以受限并发 dispatch 构建工作流 → 监控 run → 自动修复失败并重试 → 全部变绿后留下 PR 供人工审查"这条完整链路固化成了可重复执行的流程。读完本文,你将掌握这套 7 镜像批量重建的输入参数、阶段化执行模型、失败分类与重试预算、以及仓库中每个脚本的真实行为。

背景:7 个 CI 镜像与两条构建工作流

镜像构建的实际执行者是两个workflow_dispatch工作流,Skill 只是它们的"指挥者"。构建矩阵共 7 个构建任务:

工作流变体数量
ci-images-build-os.ymlos∈ {ubuntu24_04, rocky_8, rocky_9, debian, openSUSE15_6, SLES15_6}6
ci-images-build-postgres.ymlpostgres1

每个 run 同时产出 amd64 与 arm64 的 manifest list

每次 dispatch 只触发一个 run,但该 run 内部是两个架构的矩阵构建加一个合并任务:

  • build矩阵在原生 runner上分别构建linux/amd64与linux/arm64(ci-images-build-os.yml 中fail-fast: false,一个架构失败不会取消另一个),各自推送单架构 tag(如positron-ubuntu24-amd64:<tag>、positron-ubuntu24-arm64:<tag>);
  • 随后的merge任务用docker buildx imagetools create将两个单架构 tag 合并为多架构 manifest list,即ghcr.io/<owner>/positron-ubuntu24:<tag>;
  • 单架构 tag 会保留在 GHCR 上,作为过渡期存量消费方的兼容输出。

postgres 镜像在 Dockerfile.postgres 中只是postgres:latest加初始化脚本(COPY init-scripts/与COPY sql/),没有任何 Node,因此它不受NODE_VERSION变更影响。

镜像里到底装了什么

从 Dockerfile.ubuntu24_04 可以看到这类 OS 测试镜像的内容:系统依赖与 VNC/窗口管理器(Fluxbox + x11vnc 用于本地观察测试)、AWS CLI v2、Node.js 与全局 npm 依赖、Quarto、Miniforge Conda、rig 管理的多个 R 版本、TinyTeX(LaTeX)、uv 管理的 Python 3.10.12 venv 与 e2e 测试内容依赖、pyenv、隐藏 Python/R、以及 Positron license server。镜像还暴露8080、5900、9323端口,与 docker/images/README.md 中"本地开发复刻 CI 环境"的用途一致。

输入参数:一次升级只需要三个信息

Skill 从用户的调用中解析输入(缺失时才询问):

  • tag(必填)——所有镜像统一使用的 Docker tag,例如2025.06.0;
  • node version(必填)——完整 Node 版本号,例如22.22.1,且必须同时存在于https://nodejs.org/dist/v<version>/的 x64 与 arm64 路径下(预检会用 HEAD 请求验证);
  • concurrency N(可选,默认 4)——同时最多有多少个构建任务在飞行中。

五个执行阶段

Phase 0 — Preflight:动手前先确认基线

在改动任何东西之前,依次确认以下前置条件全部通过:

gh auth status # 已认证,且有 workflow + repo scope git -C "$REPO" status --porcelain # 必须为空(工作树干净) git -C "$REPO" rev-parse --abbrev-ref HEAD # 汇报当前基础分支(任意分支均可) gh repo view --json nameWithOwner -q .nameWithOwner # 期望 posit-dev/positron
  • 用 HEAD 请求验证 Node 版本在双架构都存在:curl -sfI https://nodejs.org/dist/v<node>/node-v<node>-linux-x64.tar.xz与...-linux-arm64.tar.xz,任一返回 404 就停止并告知用户版本无效;
  • 工作树必须干净,否则停止并询问如何处理;
  • 基础分支不必是main:update-images/<tag>分支从当前所在位置 fork 即可,这对从功能分支上测试很有用。只需在 PR body 中注明 fork 自哪个基础分支。

Phase 1 — 分支、改版本、开 PR

git -C "$REPO" switch -c update-images/<tag> # 非字母数字字符会被清洗为 '-' bash <dir>/scripts/bump-node-version.sh <node> # 编辑全部 12 个 compose 文件并打印证明 git -C "$REPO" add -A git -C "$REPO" commit -m "Bump Node to <node> and rebuild all images at <tag>" git -C "$REPO" push -u origin HEAD gh pr create --base main --head update-images/<tag> \ --title "Rebuild all CI images at <tag> (Node <node>)" \ --body "<see template below>"

PR body 模板(构建逐个变绿时勾选):

Rebuilds every image at tag `<tag>` with Node `<node>`. Driven by the `update-ci-images` skill. Builds are dispatched on this branch via workflow_dispatch with max <N> jobs in flight. ### Builds (each is a multi-arch amd64+arm64 manifest) - [ ] ubuntu24_04 - [ ] rocky_8 - [ ] rocky_9 - [ ] debian - [ ] openSUSE15_6 - [ ] SLES15_6 - [ ] postgres

更新 PR body/checklist 时务必用 REST API 而不是gh pr edit:gh pr edit --body可能触发 Projects-classic GraphQL 弃用错误并静默地不更新 body,因此统一用不受影响的 REST PATCH:

gh api -X PATCH repos/<owner>/<repo>/pulls/<n> -f body="$NEW_BODY"

每次勾选时都基于最新内容重建完整 body 再 PATCH。

bump-node-version.sh 到底改了哪些文件

scripts/bump-node-version.sh 维护一个 12 个文件的显式列表(6 个 OS × amd64/arm64 各一),用sed -i.bak只替换NODE_VERSION:的值并保留缩进。它的防御性设计值得一提:任一预期文件缺失、或文件中没有NODE_VERSION:行,脚本都会报错退出,从而杜绝"静默部分更新"。postgres 没有 Node,刻意排除在列表之外。执行后打印:

Updated NODE_VERSION to <node> in 12 files:

当前仓库中这些文件的实际值,例如 ubuntu24_04 的 amd64 compose 里NODE_VERSION: 24.18.0,对应的 Dockerfile 在 Dockerfile.ubuntu24_04 中用curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz安装 Node 并用node -v && npm -v验证。

PPM_SNAPSHOT 是独立的旋钮,普通 Node 重建不动它

R 的 dev-deps 安装还有一个独立的PPM_SNAPSHOTbuild arg(由 scripts/bump-ppm-snapshot.sh 负责修改),它在正常的 Node 重建中不会被触碰——除非遇到 Phase 4 描述的 PPM 发布窗口竞争。bump-ppm-snapshot.sh 同样以显式文件列表 + 缺失即报错的方式工作,并且把日期用引号包裹(sed写成"${NEW}"),避免 YAML 把日期解析成时间戳(buildx bake 会拒绝time.Time);debian 被有意排除(冻结在2026-03-01,见 debian compose,原因是 terra/GDAL 问题)。

Phase 2 — 建队列与状态

在 scratchpad 里创建一个状态文件以支撑长时间运行。每个任务的状态沿queued→running(记录 run id)→done/failed迁移。每个任务维护两个相互独立的计数器(原因见 Phase 4):

  • fix_attempts——做了真正的代码修复并重新 dispatch 的次数;
  • transient_retries——因基础设施抖动(无代码改动)重新 dispatch 的次数。

7 个任务按 checklist 行命名。每个任务调用dispatch-job.sh <branch> <tag> [os]:6 个 OS 任务传os,postgres 任务省略;每次 dispatch 只产生一个 run(双架构构建 + manifest 合并)。

Phase 3 — Dispatch + 监控循环(长时间运行部分)

维护in_flight(run id → 任务)与queued,循环直到queued与in_flight都为空:

  1. 补位:当len(in_flight) < N且queued非空时,弹出一个任务并执行:

    id=$(bash <dir>/scripts/dispatch-job.sh <branch> <tag> [os])

    记录id、标记任务running、递增其attempts、更新状态文件。

  2. 等待:把 waiter 放到后台启动(不阻塞会话),传入所有在飞 run id(每 60s 轮询一次)。macOS 上包一层caffeinate -i,防止主机空闲休眠导致轮询停滞:

    WAIT="bash <dir>/scripts/wait-for-runs.sh 60 <id1> <id2> ..." if [ "$(uname)" = "Darwin" ]; then caffeinate -i $WAIT; else $WAIT; fi

    (注意caffeinate -i只防空闲休眠,不防电池状态下的合盖休眠;Linux 上可换systemd-inhibit --what=idle。)只要有任一在飞 run 完成,脚本就以<id> <conclusion>格式打印并退出;harness 会携带该输出重新调用 Agent。

  3. 处理完成事件:对每行输出——success→ 标记done、移出in_flight、用gh api -X PATCH勾选 PR checklist 与 TodoWrite 项,回到步骤 1 补位;其他结论(failure/cancelled/timed_out/startup_failure)→ 进入 Phase 4。

  4. 当queued与in_flight都为空 → 进入 Phase 5。

不要用前台的sleep轮询;用步骤 2 的后台 waiter,harness 会在其退出时重新调用你。等待期间也可以使用 Monitor 工具。

脚本实现细节

scripts/dispatch-job.sh 用run-name(displayTitle)来匹配刚触发的 run:有os参数时工作流是ci-images-build-os.yml、标题<tag>-<os>;无os时是ci-images-build-postgres.yml、标题postgres-<tag>。它先查同分支同标题已有 run 的最大databaseId,再 dispatch,然后最多 3 次尝试、每次 20 次 × 3s 轮询去定位databaseId更大的新 run——这样即使 dispatch 命令报错但实际已落地,也不会重复触发。run id 输出到 stdout,其余日志走 stderr,便于调用方干净地捕获 id。

scripts/wait-for-runs.sh 阻塞直到至少一个给定 run 达到status=completed,然后逐行打印已完成 run 的<id> <conclusion>(进行中的不打印),success | failure | cancelled | timed_out | ...,一旦有完成即退出 0。后续用仍在飞的 id 重新调用即可继续等待。

Phase 4 — 诊断、修复、重试失败的构建

对失败的 run id:

gh run view <id> --log-failed # 失败步骤 + 错误

每个 run 有两个build矩阵腿(amd64、arm64)加一个merge任务。--log-failed会指出哪条腿(哪个架构)失败——据此选择要编辑的文件(按架构区分的docker/images/<os>/docker-compose.<arch>.yml或共享的docker/images/<os>/Dockerfile.*)。merge任务失败说明某条腿没推上它的架构 tag(真正的失败在腿,修腿),或者是一次瞬时的 GHCR/imagetools抖动(重新 dispatch)。重新 dispatch 会重建两个架构并重新合并,这是预期行为。

按失败特征(失败步骤 + 错误)分类,而不是只看任务名:

  • 瞬时失败(runner 丢失、网络抖动、GHCR 5xx、"no space left"、setup-qemu抖动、CTAN/mirror 拉取失败、甚至一次脆弱的gh调用):无代码改动,重新 dispatch 同一任务(无需 push),放回in_flight,递增transient_retries(上限5——抖动不是任务的错);
  • 真实构建错误(包/仓库找不到、某架构 Node 404、Dockerfile/compose 问题、基础镜像或上游包变更):编辑对应文件(docker/images/<os>/Dockerfile.*、docker/images/<os>/docker-compose.<arch>.yml或docker/images/postgres/Dockerfile.postgres),保持改动最小且匹配周围风格,然后:
    git -C "$REPO" add -A git -C "$REPO" commit -m "Fix <os>/<arch> build: <what>" git -C "$REPO" push

    在(已更新的)分支上重新 dispatch,放回in_flight,递增fix_attempts;

  • 某架构 Node 版本 404 通常意味着输入错误——停止并上报,不要浪费重试。

计数按根因、不按任务:一个任务可能连续命中多个不同的失败(先真实错误、再独立的基础设施抖动)。要把相应计数器重置/限定到当前失败特征,避免一个问题的预算被另一个问题消耗。

停止条件:当某个任务满足以下任一条件时暂停循环并升级——同一失败特征下fix_attempts达到3(真实修复不生效)、或transient_retries达到5(持续抖动)、或失败不明确/修复会改变镜像内容而应由所有者决策(例如固定包版本)。暂停时报告:任务、失败步骤、已尝试内容、gh run view <id> --log-failed摘录。其他在飞/排队任务继续,只有卡住的任务阻塞最终成功。不要无限循环。

推送到分支的修复只影响推送之后dispatch 的构建;已在飞的 run 沿用旧代码,让它们自然结束并独立评判即可。

仓库内已证实的三类失败模式

1. R 源码包需要比发行版更新的系统库(例如terra1.9 需要 GDAL ≥ 3.7,而 Debian bookworm 只有 3.6.2,报gdal_multidimensional.cpp ... no matching function for ... AsClassicDataset)。修复:把 dev-deps 安装固定到破坏性包版本之前的某个 PPM 快照日期,作用于该 OS 的R_REPO/RSPM:https://packagemanager.posit.co/cran/<DATE>(源码)与https://packagemanager.posit.co/cran/__linux__/<distro>/<DATE>(二进制)。破坏日期可从https://cran.r-project.org/src/contrib/Archive/<pkg>/查得。(rocky_8走不同路线,从源码构建更新的 GEOS/GDAL/libgit2,所以这只影响 Debian 系;rocky_9两者都不需要:EL9 + EPEL9 已自带 GEOS 3.13 / GDAL 3.10 / PROJ 9.6 / libgit2 1.7 和支持 C++20 的 GCC,那里应优先用固定日期的 PPM 快照而不是重新引入源码构建。)

2. PPMlatest发布窗口竞争(报! Failed to download <pkg> from https://packagemanager.posit.co/cran/.../latest/.../<pkg>_<ver>.tar.gz,发生在pak::local_install_dev_deps期间,前面很多包已正常下载)。根因:滚动latest通道上,pak 从PACKAGES元数据解析出版本,但对应的二进制/源码文件可能已在发布中途轮出latest→ 间歇性 404。它看起来像瞬时问题但会反复出现。修复:把构建期dev-deps 安装固定到有日期快照(源码https://packagemanager.posit.co/cran/<DATE>、二进制.../cran/__linux__/<distro>/<DATE>),保证内部一致;各镜像运行期的.Rprofile/ENV RSPM保持latest。日期就是PPM_SNAPSHOTbuild arg,按 OS 设置在docker/images/<os>/docker-compose.*.yml中,由各Dockerfile.*消费(参见 Dockerfile.ubuntu24_04 中options(repos=c(RSPM='.../${PPM_SNAPSHOT}'), ...)的用法)。四个滚动 OS 用scripts/bump-ppm-snapshot.sh <DATE>提升;debian 有意排除(因上述 terra/GDAL 原因冻结在2026-03-01,不要随其他 OS 一起动)。改变镜像内容(包版本)属于所有者决策——重新固定前要与用户确认日期。

3. TinyTeX /tlmgr间歇性失败(could not get texlive.tlpdb,或从随机镜像拉取后安装失败)。根因:tlmgr option repository https://mirror.ctan.org/...是轮询式分配,每次调用可能落在过期/不可达的镜像上。修复:固定到单个完整镜像(https://ctan.math.illinois.edu/systems/texlive/tlnet),并把网络依赖的tlmgr update/install包进一个小重试循环。6 个 OS 的 Dockerfile 中都存在该模式(Dockerfile.ubuntu24_04 即是固定 CTAN 镜像 +retry函数包裹tlmgr步骤的实例)。

Phase 5 — 收尾

全部 7 个任务done(success)后:

  • 用gh api -X PATCH(见 Phase 1 备注)勾选全部 PR checklist 框;用gh pr comment <n>添加总结评论,列出每个推送的 manifest:tag,例如ghcr.io/posit-dev/positron-ubuntu24:<tag>…positron-postgres:<tag>(单架构-amd64/-arm64tag 也作为构建产物推上去了);
  • PR 保持打开供人工审查——不要合并;
  • 向用户汇报:PR URL、tag、Node 版本,以及 7 个多架构镜像全部构建并推送成功的确认;注明任何需要修复的内容(附上对应 commit 链接)。

如果因卡住的任务提前停止,则精确报告哪些已绿、哪些被阻塞、诊断结果,并把分支/PR 留在原地供用户接管。

一次完整重建的端到端视图

把整个流程串起来看:预检确认认证与干净工作树 → 在update-images/<tag>分支上由 bump-node-version.sh 批量更新 12 个 compose 文件的NODE_VERSION并开 PR → 用 dispatch-job.sh 以默认并发 4 逐批 dispatch 7 个构建 → 由 wait-for-runs.sh 后台轮询、按成功/失败分流 → 失败按特征归类,瞬时抖动最多重试 5 次,真实错误修复后最多 3 次 → 全绿后勾选 checklist、留总结评论,PR 交给人工审查合并。每个 OS 的构建最终产出ghcr.io/<owner>/positron-<os-short>:<tag>多架构 manifest,同时保留-amd64/-arm64单架构 tag 供存量流程过渡。

这套编排的关键设计都沉淀在 SKILL.md 与其 scripts 目录中:双计数器分离"代码修复"与"基础设施抖动"的重试预算、run-name + databaseId 双保险防重复 dispatch、后台 waiter 避免长轮询阻塞会话,以及三类已证实的失败模式及其修复方案——它们共同保证了在数小时的批量重建中,Agent 能以受控并发稳定地把全部 7 个镜像推向同一个新 tag。

  • 开发工具
  • 代码编辑器
  • 数据科学

【免费下载链接】positron

Positron, a next-generation data science IDE

项目地址:https://gitcode.com/gh_mirrors/po/positron
点击查看免费下载

相关推荐

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

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

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

立即咨询