diff --git a/deploy/container/README.md b/deploy/container/README.md index f888c1add..e5901466d 100644 --- a/deploy/container/README.md +++ b/deploy/container/README.md @@ -68,18 +68,18 @@ Jenkins 分支预览构建固定从宿主 `/data/jenkins/preview-secrets/.env.lo ### Gitea CI 预构建 Job 镜像 -`.gitea/workflows/project-ci.yml` 的四个 job 统一使用 `deploy/container/gitea-ci-job.Dockerfile` 构建的 `genarrative-ci` 环境。镜像固定 Ubuntu job base digest `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614` 和 Rust stage digest `sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e`;Node `22.23.1` 发行包在解压前执行 SHA-256 校验,Google Linux 主签名指纹固定为 `EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796`,Chrome 固定为 `150.0.7871.181-1`。镜像预装 Rust `1.98.1`、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并设置 `RUSTUP_AUTO_INSTALL=0`;仓库工具链变更时必须先重建镜像,不允许 job 现场下载补齐。 +`.gitea/workflows/project-ci.yml` 的四个 job 统一使用 `deploy/container/gitea-ci-job.Dockerfile` 构建的 `genarrative-ci` 环境。镜像固定 Ubuntu job base digest `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614` 和 Rust stage digest `sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e`;Node `22.23.1` 发行包在解压前执行 SHA-256 校验,Google Linux 主签名指纹固定为 `EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796`,Chrome 固定为 `153.0.8010.52-1`。镜像预装 Rust `1.98.1`、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并设置 `RUSTUP_AUTO_INSTALL=0`;仓库工具链变更时必须先重建镜像,不允许 job 现场下载补齐。 构建、校验和装入 runner 内层 Docker: ```bash bash scripts/gitea-ci-job-image.sh build bash scripts/gitea-ci-job-image.sh verify -bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260920.1.tar.zst +bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260920.2.tar.zst bash scripts/gitea-ci-job-image.sh load-runner ``` -默认构建 tag 为 `genarrative/gitea-project-ci:20260920.1`。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根 workspace 的唯一 npm lock 与全部 workspace manifest,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock;不会把业务源码、素材或本地私密文件发送给 Docker daemon。新镜像显式安装并精确校验 `npm 10.9.7`,不依赖 Node 发行包隐含的 npm 版本;除固定工具链外,还按一份 npm workspace lock 与三份 Cargo lock 预热下载缓存。npm 只执行一次忽略 lifecycle scripts 的 workspace `npm ci`,三个 `cargo fetch --locked` 最多执行 5 次整命令级有界重试,再分别以断网 `cargo fetch --locked` 验证缓存闭合,镜像不包含 `node_modules` 或 Cargo `target`。`build` 完成后会自动运行环境校验,`load-runner` 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。workspace lock 或 manifest 变化落地后必须按下述顺序重建并装载镜像;过渡期旧固定镜像缺少 `GENARRATIVE_GITEA_CI_NPM_VERSION` 时,校验只输出 `npm_version=partial` 和 Actions warning,继续由当前 job 的根 `npm ci` 验证唯一 lock,不能据此宣称 npm 版本或新依赖缓存已经闭合。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。 +默认构建 tag 为 `genarrative/gitea-project-ci:20260920.2`。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根 workspace 的唯一 npm lock 与全部 workspace manifest,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock,外加 AI 游戏创作壳本地路径依赖的三个编辑器 bridge crate 源树;不会把业务源码、素材或本地私密文件发送给 Docker daemon。新镜像显式安装并精确校验 `npm 10.9.7`,不依赖 Node 发行包隐含的 npm 版本;除固定工具链外,还按一份 npm workspace lock 与三份 Cargo lock 预热下载缓存。npm 只执行一次忽略 lifecycle scripts 的 workspace `npm ci`(最多 5 次整命令级有界重试,处理 registry ECONNRESET),三个 `cargo fetch --locked` 最多执行 5 次整命令级有界重试,再分别以断网 `cargo fetch --locked` 验证缓存闭合,镜像不包含 `node_modules` 或 Cargo `target`。`build` 完成后会自动运行环境校验,`load-runner` 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。workspace lock 或 manifest 变化落地后必须按下述顺序重建并装载镜像;过渡期旧固定镜像缺少 `GENARRATIVE_GITEA_CI_NPM_VERSION` 时,校验只输出 `npm_version=partial` 和 Actions warning,继续由当前 job 的根 `npm ci` 验证唯一 lock,不能据此宣称 npm 版本或新依赖缓存已经闭合。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。 runner 配置保留原 `ubuntu-latest` 映射,`genarrative-ci` 继续映射到经 `build / verify / load-runner` 验证并写入配置的完整 Image ID。内层 Docker 数据必须持久化,`force_pull` 保持 `false`;该精确 Image ID 在内层不存在时 job 应直接失败,不回退到浮动 tag 或现场拉取。四个 job 使用镜像内 `genarrative-gitea-checkout` 直接从当前 Gitea 拉取事件 commit,带 5 次有界重试,不再运行时下载 GitHub checkout action;随后以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,同时校验工具链、一份 npm workspace 缓存锁、三份 Cargo 缓存锁、bwrap 和 Chrome headless。锁不匹配时校验会输出 `partial` 和醒目的 Actions warning,提示在可信分支落地后刷新镜像。各 job 仍各自运行一次干净的根 `npm ci`,以唯一 workspace lock 校验全部 App/package/tool 依赖并隔离 PR 依赖;统一通过 `scripts/ci-npm-ci-with-retry.sh` 最多执行 3 次整命令级有界重试,并使用镜像内 npm cache 和 `prefer-offline`。锁文件新增依赖时允许经受控网络补齐,本阶段不启用共享 Actions cache。 diff --git a/deploy/container/gitea-ci-job.Dockerfile b/deploy/container/gitea-ci-job.Dockerfile index 4ffd55805..733d3b3be 100644 --- a/deploy/container/gitea-ci-job.Dockerfile +++ b/deploy/container/gitea-ci-job.Dockerfile @@ -16,6 +16,9 @@ ENV CARGO_HTTP_MULTIPLEXING=false \ COPY server-rs /tmp/genarrative-cargo-cache/server-rs COPY apps/desktop-shell/src-tauri /tmp/genarrative-cargo-cache/desktop-shell COPY apps/ai-game-creator-shell/src-tauri /tmp/genarrative-cargo-cache/apps/ai-game-creator-shell/src-tauri +COPY plugins/agc-cocos-editor/native/cocos-editor-bridge /tmp/genarrative-cargo-cache/plugins/agc-cocos-editor/native/cocos-editor-bridge +COPY plugins/agc-unity-editor/native/unity-editor-bridge /tmp/genarrative-cargo-cache/plugins/agc-unity-editor/native/unity-editor-bridge +COPY plugins/agc-godot-editor/native/godot-editor-bridge /tmp/genarrative-cargo-cache/plugins/agc-godot-editor/native/godot-editor-bridge RUN find /tmp/genarrative-cargo-cache -name Cargo.toml -exec dirname {} \; \ | while IFS= read -r crate_dir; do \ @@ -57,12 +60,12 @@ FROM ${RUNNER_IMAGE} ARG NODE_VERSION=22.23.1 ARG NODE_LINUX_X64_SHA256=9749e988f437343b7fa832c69ded82a312e41a03116d766797ac14f6f9eee578 ARG NPM_VERSION=10.9.7 -ARG GOOGLE_CHROME_VERSION=150.0.7871.181-1 +ARG GOOGLE_CHROME_VERSION=153.0.8010.52-1 ARG GOOGLE_LINUX_SIGNING_KEY_FINGERPRINT=EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796 LABEL org.opencontainers.image.title="Genarrative Gitea CI job image" \ org.opencontainers.image.description="Ubuntu 24.04 CI image with fixed toolchains and prewarmed npm/Cargo caches" \ - org.opencontainers.image.version="2026.09.20.1" + org.opencontainers.image.version="2026.09.20.2" RUN test "$(dpkg --print-architecture)" = "amd64" \ && find /etc/apt/sources.list.d -maxdepth 1 -type f \ @@ -120,12 +123,8 @@ RUN node_archive="node-v${NODE_VERSION}-linux-x64.tar.xz" \ --strip-components=1 \ --directory /usr/local/lib/genarrative-node \ && rm -f "${node_archive}" \ - && ln -sfn /usr/local/lib/genarrative-node/bin/node /usr/local/bin/node \ - && ln -sfn /usr/local/lib/genarrative-node/bin/npm /usr/local/bin/npm \ && ln -sfn /usr/local/lib/genarrative-node/bin/npx /usr/local/bin/npx \ - && ln -sfn /usr/local/lib/genarrative-node/bin/corepack /usr/local/bin/corepack \ - && npm install --global "npm@${NPM_VERSION}" --no-audit --no-fund \ - && test "$(npm --version)" = "${NPM_VERSION}" + && ln -sfn /usr/local/lib/genarrative-node/bin/corepack /usr/local/bin/corepack COPY --from=rust-dependency-cache /usr/local/cargo /usr/local/cargo COPY --from=rust-dependency-cache /usr/local/rustup /usr/local/rustup @@ -150,6 +149,8 @@ COPY apps/desktop-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/loc COPY apps/ai-game-creator-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/locks/ai-game-creator-shell.Cargo.lock COPY deploy/container/gitea-ci-checkout.sh /usr/local/bin/genarrative-gitea-checkout +# npm registry 偶发 ECONNRESET,镜像预热也需要整命令级有界重试; +# 失败重试复用同一 npm cache,不会重复下载已完成的包。 RUN test -n "${NPM_LOCK_SHA256}" \ && test -n "${SERVER_RUST_LOCK_SHA256}" \ && test -n "${DESKTOP_RUST_LOCK_SHA256}" \ @@ -171,12 +172,23 @@ RUN test -n "${NPM_LOCK_SHA256}" \ /usr/local/share/genarrative-ci/locks/ai-game-creator-shell.Cargo.lock \ | sha256sum --check --strict \ && chmod 0755 /usr/local/bin/genarrative-gitea-checkout \ - && npm ci \ - --ignore-scripts \ - --no-audit \ - --no-fund \ - --prefer-offline \ - --prefix /usr/local/share/genarrative-ci/npm \ + && npm_ci_with_retry() { \ + for attempt in 1 2 3 4 5; do \ + if npm ci \ + --ignore-scripts \ + --no-audit \ + --no-fund \ + --prefer-offline \ + --prefix /usr/local/share/genarrative-ci/npm; then \ + return 0; \ + fi; \ + if [ "${attempt}" -eq 5 ]; then \ + return 1; \ + fi; \ + sleep "$((attempt * 5))"; \ + done; \ + } \ + && npm_ci_with_retry \ && rm -rf \ /usr/local/share/genarrative-ci/npm/node_modules \ /usr/local/share/genarrative-ci/npm/apps/*/node_modules \ @@ -184,6 +196,28 @@ RUN test -n "${NPM_LOCK_SHA256}" \ /usr/local/share/genarrative-ci/npm/tools/*/node_modules \ && npm cache verify +# 依赖预热会在 workspace 内解析出 Node 发行包自带的 npm(例如 10.9.8), +# 并通过 PATH 里的 node_modules/.bin 影子化固定版本,因此全局固定版本在预热之后安装。 +# 基础 runner 镜像把 /opt/acttoolcache/node/24.18.0/x64/bin 放在 PATH 最前, +# 会影子化固定 Node / npm。这里把固定工具链写成绝对路径 wrapper, +# 并用同一组 wrapper 覆盖 base 镜像残留的 toolcache bin,避免 shell 是否登录改变解析结果。 +RUN npm install --global --prefix /usr/local/lib/genarrative-node \ + --no-audit --no-fund "npm@${NPM_VERSION}" \ + && test "$(/usr/local/lib/genarrative-node/bin/node /usr/local/lib/genarrative-node/lib/node_modules/npm/bin/npm-cli.js --version)" = "${NPM_VERSION}" \ + && rm -f /usr/local/bin/npm \ + && printf '%s\n' '#!/bin/sh' \ + 'exec /usr/local/lib/genarrative-node/bin/node /usr/local/lib/genarrative-node/lib/node_modules/npm/bin/npm-cli.js "$@"' \ + > /usr/local/bin/npm \ + && chmod 0755 /usr/local/bin/npm \ + && rm -rf /opt/acttoolcache/node/24.18.0/x64/bin \ + && mkdir -p /opt/acttoolcache/node/24.18.0/x64/bin \ + && ln -sfn /usr/local/lib/genarrative-node/bin/node /opt/acttoolcache/node/24.18.0/x64/bin/node \ + && ln -sfn /usr/local/bin/npm /opt/acttoolcache/node/24.18.0/x64/bin/npm \ + && ln -sfn /usr/local/lib/genarrative-node/bin/npx /opt/acttoolcache/node/24.18.0/x64/bin/npx \ + && test "$(readlink -f "$(command -v node)")" = "/usr/local/lib/genarrative-node/bin/node" \ + && test "$(node --version)" = "v${NODE_VERSION}" \ + && test "$(npm --version)" = "${NPM_VERSION}" + RUN install -m 0755 /usr/local/cargo/bin/rustup /usr/local/bin/rustup \ && for command_name in cargo cargo-fmt rustc rustdoc rustfmt; do \ ln -sfn rustup "/usr/local/bin/${command_name}"; \ @@ -208,7 +242,7 @@ ENV CARGO_HOME=/usr/local/cargo \ ARG IMAGE_REVISION=uncommitted LABEL org.opencontainers.image.vendor="GenarrativeAI" \ - org.opencontainers.image.version="2026.09.20.1" \ + org.opencontainers.image.version="2026.09.20.2" \ org.opencontainers.image.source="https://git.genarrative.world/GenarrativeAI/Genarrative" \ org.opencontainers.image.revision="${IMAGE_REVISION}" \ org.opencontainers.image.base.name="docker.gitea.com/runner-images:ubuntu-latest@sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614" \ diff --git a/deploy/container/gitea-ci-job.Dockerfile.dockerignore b/deploy/container/gitea-ci-job.Dockerfile.dockerignore index 8b373d682..43bc93a4c 100644 --- a/deploy/container/gitea-ci-job.Dockerfile.dockerignore +++ b/deploy/container/gitea-ci-job.Dockerfile.dockerignore @@ -38,3 +38,14 @@ !tools/ !tools/spine-json-export-validator/ !tools/spine-json-export-validator/package.json +!plugins/ +!plugins/agc-cocos-editor/ +!plugins/agc-cocos-editor/native/ +!plugins/agc-cocos-editor/native/cocos-editor-bridge/ +!plugins/agc-unity-editor/ +!plugins/agc-unity-editor/native/ +!plugins/agc-unity-editor/native/unity-editor-bridge/ +!plugins/agc-godot-editor/ +!plugins/agc-godot-editor/native/ +!plugins/agc-godot-editor/native/godot-editor-bridge/ +!plugins/agc-*-editor/native/*-editor-bridge/** diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index f8af79922..cd0a026f1 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -303,14 +303,14 @@ PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME 当前 `genarrative-station` 使用 Gitea `1.26.4` 和基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置保持 `docker_host: "-"`、`valid_volumes: []`、`bind_workdir: false` 和 `force_pull: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。 -CI job 镜像由 `deploy/container/gitea-ci-job.Dockerfile` 定义:Ubuntu job base 固定为 `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614`,Rust stage 固定为 `sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e`,Node `22.23.1` 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 `150.0.7871.181-1`。构建脚本以 NUL 分隔白名单 tar 流发送 Dockerfile、checkout 脚本、根 lock、全部 workspace manifests,以及 server-rs、桌面壳和 AI 游戏创作壳 Cargo manifests/lock。镜像按唯一根 npm workspace 锁、server-rs 锁、桌面壳锁和 AI 游戏创作壳 Cargo 锁预热四份下载缓存,不包含 `node_modules` 或 Cargo `target`;三个 `cargo fetch --locked` 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍分别以断网 `cargo fetch --locked` 关闭验证。CI 镜像定义或根 lock/workspace manifests 变化后必须重建并发布新的固定 Image ID;不得继续沿用旧镜像 digest。runner 标签保留 `ubuntu-latest`,并将 `genarrative-ci` 映射到当前已验证的完整 Image ID。内层 Docker 数据必须持久化;`force_pull: false` 表示只使用已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。 +CI job 镜像由 `deploy/container/gitea-ci-job.Dockerfile` 定义:Ubuntu job base 固定为 `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614`,Rust stage 固定为 `sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e`,Node `22.23.1` 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 `153.0.8010.52-1`。构建脚本以 NUL 分隔白名单 tar 流发送 Dockerfile、checkout 脚本、根 lock、全部 workspace manifests,以及 server-rs、桌面壳和 AI 游戏创作壳 Cargo manifests/lock,外加 AI 游戏创作壳本地路径依赖的三个编辑器 bridge crate 源树;镜像预热阶段的根 npm `ci` 与三个 `cargo fetch --locked` 都执行最多 5 次整命令级有界重试,分别处理 registry ECONNRESET 与 registry index 握手失败。镜像按唯一根 npm workspace 锁、server-rs 锁、桌面壳锁和 AI 游戏创作壳 Cargo 锁预热四份下载缓存,不包含 `node_modules` 或 Cargo `target`;三个 `cargo fetch --locked` 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍分别以断网 `cargo fetch --locked` 关闭验证。CI 镜像定义或根 lock/workspace manifests 变化后必须重建并发布新的固定 Image ID;不得继续沿用旧镜像 digest。runner 标签保留 `ubuntu-latest`,并将 `genarrative-ci` 映射到当前已验证的完整 Image ID。内层 Docker 数据必须持久化;`force_pull: false` 表示只使用已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。 镜像更新命令: ```bash bash scripts/gitea-ci-job-image.sh build bash scripts/gitea-ci-job-image.sh verify -bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260920.1.tar.zst +bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260920.2.tar.zst bash scripts/gitea-ci-job-image.sh load-runner ``` diff --git a/scripts/gitea-ci-job-image.sh b/scripts/gitea-ci-job-image.sh index 494af67e5..e9e06c8db 100644 --- a/scripts/gitea-ci-job-image.sh +++ b/scripts/gitea-ci-job-image.sh @@ -4,7 +4,7 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" dockerfile_context_path="deploy/container/gitea-ci-job.Dockerfile" -image_tag="${GENARRATIVE_GITEA_CI_IMAGE_TAG:-genarrative/gitea-project-ci:20260920.1}" +image_tag="${GENARRATIVE_GITEA_CI_IMAGE_TAG:-genarrative/gitea-project-ci:20260920.2}" runner_container="${GENARRATIVE_GITEA_RUNNER_CONTAINER:-gitea-runner}" write_build_context_file_list() { @@ -29,7 +29,10 @@ write_build_context_file_list() { server-rs/Cargo.lock \ apps/desktop-shell/src-tauri/Cargo.toml \ apps/desktop-shell/src-tauri/Cargo.lock - find server-rs/crates -name Cargo.toml -print0 | sort -z + find server-rs/crates plugins/agc-*-editor/native/*-editor-bridge \ + \( -name Cargo.toml -o -path 'plugins/agc-*-editor/native/*-editor-bridge/*' \) \ + -type f -print0 \ + | sort -z } usage() { @@ -87,7 +90,9 @@ case "${command_name}" in server-rs/Cargo.lock \ apps/desktop-shell/src-tauri/Cargo.toml \ apps/desktop-shell/src-tauri/Cargo.lock - find server-rs/crates -name Cargo.toml -print0 \ + find server-rs/crates plugins/agc-*-editor/native/*-editor-bridge \ + \( -name Cargo.toml -o -path 'plugins/agc-*-editor/native/*-editor-bridge/*' \) \ + -type f -print0 \ | sort -z \ | xargs -0 -r sha256sum } \ diff --git a/scripts/project-ci-workflow.test.ts b/scripts/project-ci-workflow.test.ts index 2f8b4a5ef..516fb6720 100644 --- a/scripts/project-ci-workflow.test.ts +++ b/scripts/project-ci-workflow.test.ts @@ -182,8 +182,16 @@ describe('project CI workflow', () => { /^ARG RUNNER_IMAGE=[^\s]+@sha256:[a-f0-9]{64}$/m, ); expect(imageDockerfile).toContain('ARG NPM_VERSION=10.9.7'); + // base runner 镜像把 /opt/acttoolcache 的 Node 放在 PATH 最前;固定 Node/npm + // 必须写成绝对路径 wrapper 并覆盖 toolcache bin,否则登录与否会解析到不同工具链。 expect(imageDockerfile).toContain( - 'npm install --global "npm@${NPM_VERSION}" --no-audit --no-fund', + 'rm -rf /opt/acttoolcache/node/24.18.0/x64/bin', + ); + expect(imageDockerfile).toContain( + 'exec /usr/local/lib/genarrative-node/bin/node /usr/local/lib/genarrative-node/lib/node_modules/npm/bin/npm-cli.js "$@"', + ); + expect(imageDockerfile).toContain( + 'npm install --global --prefix /usr/local/lib/genarrative-node', ); expect(imageDockerfile).toContain( 'GENARRATIVE_GITEA_CI_NPM_VERSION=${NPM_VERSION}', @@ -263,6 +271,22 @@ describe('project CI workflow', () => { expect(imageDockerignore).toContain(`!${path}`); } + // AGC 通过本地 path 依赖引用三个编辑器 bridge crate。镜像预热会对 + // AGC manifest 执行 cargo fetch --locked,构建上下文与 dockerignore + // 必须同时放行这些 crate,否则镜像在 cargo fetch 阶段必然失败。 + for (const bridgeDir of [ + 'plugins/agc-cocos-editor/native/cocos-editor-bridge', + 'plugins/agc-unity-editor/native/unity-editor-bridge', + 'plugins/agc-godot-editor/native/godot-editor-bridge', + ]) { + expect(imageBuildScript.split(bridgeDir)).toHaveLength(1); + expect(imageDockerignore).toContain(`!${bridgeDir}/`); + expect(imageDockerignore).toContain('**'); + expect(imageDockerfile).toContain( + `COPY ${bridgeDir} /tmp/genarrative-cargo-cache/${bridgeDir}`, + ); + } + expect(imageBuildScript).toContain( '--build-arg "AGC_RUST_LOCK_SHA256=${agc_rust_lock_sha256}"', );