ARG RUST_IMAGE=rust:1.98-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e ARG RUNNER_IMAGE=docker.gitea.com/runner-images:ubuntu-latest@sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614 FROM ${RUST_IMAGE} AS rust-toolchain RUN rustup component add rustfmt \ && rustc --version \ && cargo --version \ && rustfmt --version # 显式的一次性迁移入口:只导入下载缓存,不继承旧 CI 镜像层。 FROM rust-toolchain AS download-cache-seed RUN --mount=type=bind,from=download-seed,target=/seed \ --mount=type=cache,id=genarrative-ci-cargo-cache-v1,target=/downloads/cargo-cache,sharing=locked \ --mount=type=cache,id=genarrative-ci-cargo-index-v1,target=/downloads/cargo-index,sharing=locked \ --mount=type=cache,id=genarrative-ci-npm-v1,target=/downloads/npm,sharing=locked \ cp -a /seed/cargo-cache/. /downloads/cargo-cache/ \ && cp -a /seed/cargo-index/. /downloads/cargo-index/ \ && cp -a /seed/npm/. /downloads/npm/ FROM rust-toolchain AS rust-dependency-cache ENV CARGO_HTTP_MULTIPLEXING=false \ CARGO_NET_RETRY=10 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 --mount=type=cache,id=genarrative-ci-cargo-cache-v1,target=/usr/local/cargo/registry/cache,sharing=locked \ --mount=type=cache,id=genarrative-ci-cargo-index-v1,target=/usr/local/cargo/registry/index,sharing=locked \ find /tmp/genarrative-cargo-cache -name Cargo.toml -exec dirname {} \; \ | while IFS= read -r crate_dir; do \ mkdir -p "${crate_dir}/src"; \ : > "${crate_dir}/src/lib.rs"; \ : > "${crate_dir}/src/main.rs"; \ : > "${crate_dir}/src/active.rs"; \ done \ && cargo_fetch_with_retry() { \ manifest_path="$1"; \ for attempt in 1 2 3 4 5; do \ if cargo fetch --locked \ --target x86_64-unknown-linux-gnu \ --manifest-path "${manifest_path}"; then \ return 0; \ fi; \ if [ "${attempt}" -eq 5 ]; then \ return 1; \ fi; \ sleep "$((attempt * 5))"; \ done; \ } \ && cargo_fetch_with_retry /tmp/genarrative-cargo-cache/server-rs/Cargo.toml \ && cargo_fetch_with_retry /tmp/genarrative-cargo-cache/desktop-shell/Cargo.toml \ && cargo_fetch_with_retry /tmp/genarrative-cargo-cache/apps/ai-game-creator-shell/src-tauri/Cargo.toml \ && CARGO_NET_OFFLINE=true cargo fetch --locked \ --target x86_64-unknown-linux-gnu \ --manifest-path /tmp/genarrative-cargo-cache/server-rs/Cargo.toml \ && CARGO_NET_OFFLINE=true cargo fetch --locked \ --target x86_64-unknown-linux-gnu \ --manifest-path /tmp/genarrative-cargo-cache/desktop-shell/Cargo.toml \ && CARGO_NET_OFFLINE=true cargo fetch --locked \ --target x86_64-unknown-linux-gnu \ --manifest-path /tmp/genarrative-cargo-cache/apps/ai-game-creator-shell/src-tauri/Cargo.toml \ && mkdir -p /opt/ci-downloads/registry/cache \ && cp -a /usr/local/cargo/registry/index /opt/ci-downloads/registry/ \ && for source in /usr/local/cargo/registry/src/*/*; do \ [ -d "${source}" ] || continue; \ registry="$(basename "$(dirname "${source}")")"; \ package="$(basename "${source}")"; \ mkdir -p "/opt/ci-downloads/registry/cache/${registry}"; \ cp "/usr/local/cargo/registry/cache/${registry}/${package}.crate" \ "/opt/ci-downloads/registry/cache/${registry}/" || exit 1; \ done \ && rm -rf /tmp/genarrative-cargo-cache 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=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.2" RUN test "$(dpkg --print-architecture)" = "amd64" \ && find /etc/apt/sources.list.d -maxdepth 1 -type f \ ! -name ubuntu.sources -delete \ && apt-get update \ && apt-get install -y --no-install-recommends \ bubblewrap \ build-essential \ ca-certificates \ clang \ cmake \ curl \ ffmpeg \ file \ libayatana-appindicator3-dev \ libclang-dev \ libcurl4-openssl-dev \ librsvg2-dev \ libssl-dev \ libwebkit2gtk-4.1-dev \ libxdo-dev \ lld \ patchelf \ pkg-config \ ripgrep \ wget \ xz-utils \ && google_key="$(mktemp)" \ && curl --retry 3 --retry-all-errors --retry-delay 2 \ --proto '=https' --tlsv1.2 -fsSL \ https://dl.google.com/linux/linux_signing_key.pub \ -o "${google_key}" \ && test "$(gpg --batch --show-keys --with-colons "${google_key}" \ | awk -F: '$1 == "fpr" { print $10; exit }')" \ = "${GOOGLE_LINUX_SIGNING_KEY_FINGERPRINT}" \ && install -d -m 0755 /etc/apt/keyrings \ && install -m 0644 "${google_key}" /etc/apt/keyrings/google-chrome.asc \ && printf '%s\n' \ 'deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.asc] https://dl.google.com/linux/chrome/deb/ stable main' \ > /etc/apt/sources.list.d/google-chrome.list \ && rm -f "${google_key}" \ && apt-get update \ && apt-get install -y --no-install-recommends \ "google-chrome-stable=${GOOGLE_CHROME_VERSION}" \ && rm -rf /var/lib/apt/lists/* RUN node_archive="node-v${NODE_VERSION}-linux-x64.tar.xz" \ && curl --retry 3 --retry-all-errors --retry-delay 2 \ --proto '=https' --tlsv1.2 -fsSLO \ "https://nodejs.org/dist/v${NODE_VERSION}/${node_archive}" \ && printf '%s %s\n' "${NODE_LINUX_X64_SHA256}" "${node_archive}" \ | sha256sum --check --strict \ && install -d -m 0755 /usr/local/lib/genarrative-node \ && tar -xJf "${node_archive}" \ --strip-components=1 \ --directory /usr/local/lib/genarrative-node \ && rm -f "${node_archive}" \ && 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 COPY --from=rust-dependency-cache /usr/local/cargo /usr/local/cargo COPY --from=rust-dependency-cache /opt/ci-downloads/registry /usr/local/cargo/registry COPY --from=rust-dependency-cache /usr/local/rustup /usr/local/rustup ARG NPM_LOCK_SHA256 ARG SERVER_RUST_LOCK_SHA256 ARG DESKTOP_RUST_LOCK_SHA256 ARG AGC_RUST_LOCK_SHA256 COPY package.json package-lock.json /usr/local/share/genarrative-ci/npm/ COPY apps/admin-web/package.json /usr/local/share/genarrative-ci/npm/apps/admin-web/package.json COPY apps/ai-game-creator-shell/package.json /usr/local/share/genarrative-ci/npm/apps/ai-game-creator-shell/package.json COPY apps/desktop-shell/package.json /usr/local/share/genarrative-ci/npm/apps/desktop-shell/package.json COPY apps/mobile-shell/package.json /usr/local/share/genarrative-ci/npm/apps/mobile-shell/package.json COPY apps/preview-deployer-web/package.json /usr/local/share/genarrative-ci/npm/apps/preview-deployer-web/package.json COPY packages/image-canvas-core/package.json /usr/local/share/genarrative-ci/npm/packages/image-canvas-core/package.json COPY packages/image-canvas-react/package.json /usr/local/share/genarrative-ci/npm/packages/image-canvas-react/package.json COPY packages/shared/package.json /usr/local/share/genarrative-ci/npm/packages/shared/package.json COPY tools/spine-json-export-validator/package.json /usr/local/share/genarrative-ci/npm/tools/spine-json-export-validator/package.json COPY server-rs/Cargo.lock /usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock COPY apps/desktop-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/locks/desktop-shell.Cargo.lock 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 COPY scripts/export-ci-npm-download-cache.mjs /usr/local/share/genarrative-ci/export-npm-cache.mjs # npm registry 偶发 ECONNRESET,镜像预热也需要整命令级有界重试; # 失败重试复用同一 npm cache,不会重复下载已完成的包。 RUN --mount=type=cache,id=genarrative-ci-npm-v1,target=/var/cache/genarrative-ci-npm,sharing=locked \ test -n "${NPM_LOCK_SHA256}" \ && test -n "${SERVER_RUST_LOCK_SHA256}" \ && test -n "${DESKTOP_RUST_LOCK_SHA256}" \ && test -n "${AGC_RUST_LOCK_SHA256}" \ && printf '%s %s\n' \ "${NPM_LOCK_SHA256}" \ /usr/local/share/genarrative-ci/npm/package-lock.json \ | sha256sum --check --strict \ && printf '%s %s\n' \ "${SERVER_RUST_LOCK_SHA256}" \ /usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock \ | sha256sum --check --strict \ && printf '%s %s\n' \ "${DESKTOP_RUST_LOCK_SHA256}" \ /usr/local/share/genarrative-ci/locks/desktop-shell.Cargo.lock \ | sha256sum --check --strict \ && printf '%s %s\n' \ "${AGC_RUST_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_with_retry() { \ for attempt in 1 2 3 4 5; do \ if npm ci \ --cache /var/cache/genarrative-ci-npm \ --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 \ /usr/local/share/genarrative-ci/npm/packages/*/node_modules \ /usr/local/share/genarrative-ci/npm/tools/*/node_modules \ && rm -rf /root/.npm/_cacache \ && mkdir -p /root/.npm/_cacache \ && node /usr/local/share/genarrative-ci/export-npm-cache.mjs \ /usr/local/lib/genarrative-node/lib/node_modules/npm \ /usr/local/share/genarrative-ci/npm/package-lock.json \ /var/cache/genarrative-ci-npm/_cacache /root/.npm/_cacache \ && 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; # 而 command.exec 只信任 /usr/local/bin 等系统目录,Node 必须在那里可见。 # 因此固定工具链写成绝对路径 wrapper,并用同一组链接覆盖 base 镜像残留的 toolcache bin, # 避免 shell 是否登录改变解析结果,同时保证 command.exec 能找到受信任 node。 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}" \ && ln -sfn /usr/local/lib/genarrative-node/bin/node /usr/local/bin/node \ && 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}"; \ done ENV CARGO_HOME=/usr/local/cargo \ GENARRATIVE_GITEA_CI_AGC_RUST_LOCK_SHA256=${AGC_RUST_LOCK_SHA256} \ GENARRATIVE_GITEA_CI_DESKTOP_RUST_LOCK_SHA256=${DESKTOP_RUST_LOCK_SHA256} \ GENARRATIVE_GITEA_CI_NPM_LOCK_SHA256=${NPM_LOCK_SHA256} \ GENARRATIVE_GITEA_CI_NPM_VERSION=${NPM_VERSION} \ GENARRATIVE_GITEA_CI_SERVER_RUST_LOCK_SHA256=${SERVER_RUST_LOCK_SHA256} \ NPM_CONFIG_AUDIT=false \ NPM_CONFIG_FETCH_RETRIES=10 \ NPM_CONFIG_FETCH_RETRY_FACTOR=2 \ NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=60000 \ NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=2000 \ NPM_CONFIG_FUND=false \ NPM_CONFIG_PREFER_OFFLINE=true \ RUSTUP_AUTO_INSTALL=0 \ RUSTUP_HOME=/usr/local/rustup \ PATH=/usr/local/lib/genarrative-node/bin:/usr/local/bin:/usr/local/cargo/bin:/opt/acttoolcache/node/24.18.0/x64/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin ARG IMAGE_REVISION=uncommitted LABEL org.opencontainers.image.vendor="GenarrativeAI" \ 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" \ com.genarrative.ci.dockerfile="deploy/container/gitea-ci-job.Dockerfile" \ com.genarrative.ci.definition-sha256="${IMAGE_REVISION}" RUN node --version \ && test "$(npm --version)" = "${NPM_VERSION}" \ && rustup show active-toolchain \ && rustc --version \ && cargo --version \ && rustfmt --version \ && google-chrome --version \ && bwrap --version \ && test -x /usr/local/bin/genarrative-gitea-checkout \ && bash -n /usr/local/bin/genarrative-gitea-checkout \ && test -d /root/.npm/_cacache \ && test -d /usr/local/cargo/registry/cache \ && rg --version \ && ffmpeg -version | head -n 1