diff --git a/deploy/container/README.md b/deploy/container/README.md index 2e57768a2..2f1c50964 100644 --- a/deploy/container/README.md +++ b/deploy/container/README.md @@ -58,6 +58,14 @@ Linux Docker Engine 若要从宿主机 CLI 连到容器内服务,直接用 `ht `api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.7.0` 依赖链继续兼容 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`。Web builder 显式安装并校验 npm `10.9.7`,再按唯一根 workspace lock 执行一次 `npm ci`,不依赖 Node 基础镜像隐含的 npm 版本。镜像构建阶段会同时复制 `public/`,用于满足 API 二进制里 `include_bytes!` 引用的内置素材;不要把 `public/generated-*` 放入镜像上下文。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。 +### Jenkins 预览 secrets 镜像边界 + +Jenkins 分支预览构建固定从宿主 `/data/jenkins/preview-secrets/.env.secrets.local` 读取 secrets。目录由 Jenkins 运行账号所有且权限为 `0700`,文件由同一账号所有且权限为 `0600`;构建入口对缺失、链接、非普通文件、owner 不匹配和过宽权限均失败关闭。不要把真实值写入本 README、仓库示例或 Jenkins 参数。 + +该文件不复制到源码 checkout 和 Docker build context,而是以 BuildKit `secret` mount 只提供给 `api-runtime` stage。构建会把它安装到 API 运行镜像的 `/srv/genarrative/.env.secrets.local`,owner 为 `genarrative`、权限为 `0400`。Web builder、`nginx-runtime`、SpacetimeDB 和其它运行镜像不得获得该 mount 或目标文件;构建日志和 artifact 也不得回显或保存文件内容。容器的显式运行环境变量优先于该内置文件,可按预览实例覆盖其中的值。 + +修改宿主固定文件后必须重新构建并替换 API 镜像;重启旧容器不会读取宿主新内容。这个镜像不是可公开分发的无密钥产物:镜像持有者可以提取 `/srv/genarrative/.env.secrets.local`。只允许在当前受信任内网 Docker 主机使用,禁止 push 或 `docker save`、artifact 导出到跨信任边界的 registry、主机或存储。 + ### 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:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9`;Node `22.23.1` 发行包在解压前执行 SHA-256 校验,Google Linux 主签名指纹固定为 `EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796`,Chrome 固定为 `150.0.7871.181-1`。镜像预装 Rust `1.96.0`、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并设置 `RUSTUP_AUTO_INSTALL=0`;仓库工具链变更时必须先重建镜像,不允许 job 现场下载补齐。 @@ -245,5 +253,5 @@ npm run container:logs -- otelcol - 不改生产 systemd 单元。 - 不改 Jenkins 发布主流程。 - 不要求真实 HTTPS 证书。 -- 不把真实 `.env`、`.env.local`、`.env.secrets.local` 或 `deploy/container/api-server.env` 放入 Docker build context。 +- 不把真实 `.env`、`.env.local`、`.env.secrets.local` 或 `deploy/container/api-server.env` 放入 Docker build context;Jenkins 预览只使用上述 BuildKit secret 边界将固定文件内置到 `api-runtime`。 - 不在容器镜像里内置 SpacetimeDB 数据或 token。 diff --git a/deploy/container/api-server.Dockerfile b/deploy/container/api-server.Dockerfile index 30aef27aa..d97732bce 100644 --- a/deploy/container/api-server.Dockerfile +++ b/deploy/container/api-server.Dockerfile @@ -1,3 +1,5 @@ +# syntax=docker/dockerfile:1.7 + FROM rust:1.93-bookworm AS rust-builder WORKDIR /workspace @@ -21,6 +23,15 @@ COPY --from=rust-builder /tmp/api-server /usr/local/bin/api-server RUN mkdir -p /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox /var/lib/genarrative/wallet-refund-outbox && \ chown -R genarrative:genarrative /srv/genarrative /var/lib/genarrative +ARG GENARRATIVE_PREVIEW_SECRETS_SHA256= +RUN --mount=type=secret,id=genarrative_preview_secrets,required=false \ + if [ -n "${GENARRATIVE_PREVIEW_SECRETS_SHA256}" ]; then \ + test -f /run/secrets/genarrative_preview_secrets; \ + test "$(sha256sum /run/secrets/genarrative_preview_secrets | cut -d ' ' -f 1)" = "${GENARRATIVE_PREVIEW_SECRETS_SHA256}"; \ + install -o genarrative -g genarrative -m 0400 \ + /run/secrets/genarrative_preview_secrets /srv/genarrative/.env.secrets.local; \ + fi + USER genarrative EXPOSE 8082 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 661d4af5a..cc8e1fa76 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14325,3 +14325,9 @@ - 原生边界:根 H5 与 Desktop manifest 继续禁止 Tauri JS guest,AGC workspace 可以声明;统一 lock 出现 AGC guest 是合法聚合结果。Expo 沿用默认 npm monorepo 支持。AGC Cubone bundle、TypeScript、Tauri CLI 与 Windows Codex sidecar 都必须兼容根提升位置,不得依赖子 App 固定 `node_modules` 层级。 - 锁与平台:删除 AGC 和 Spine 子 lock;统一根 lock 必须保留 optional、bundled 和跨平台二进制节点。Linux 干净安装不能替代 Windows AGC sidecar、Android Expo/EAS 或可用 macOS/iOS runner 的平台构建证据。 - 权威方案:`docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md`。 + +## 2026-08-22 Jenkins 预览只向 API 运行镜像内置固定 secrets + +- 决策:预览 secrets 权威源固定为 Jenkins 宿主 `/data/jenkins/preview-secrets/.env.secrets.local`;目录 / 文件由 Jenkins 运行账号所有且权限分别为 `0700` / `0600`,缺失、链接、非普通文件、owner 异常或权限过宽时构建失败关闭。 +- 构建边界:只通过 BuildKit secret mount 把文件提供给 `api-runtime` stage,并安装为 `/srv/genarrative/.env.secrets.local` (`genarrative:genarrative`, `0400`)。文件不进 Git、build context、日志或 artifact,不进入 Web / Nginx、SpacetimeDB 或其它镜像。容器显式运行 env 优先覆盖内置值。 +- 更新与分发:固定源文件更新后必须重建并替换镜像,只重启容器无效。镜像可读者必然可提取内置 secrets,因此只允许留在当前受信任内网 Docker 主机,禁止 push、`docker save` 或作为 artifact 导出到跨信任边界的 registry、主机或存储。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index e6e84ac03..28be2e4db 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4824,3 +4824,10 @@ - 原因:两个 Cargo workspace 拥有独立 target 和锁文件,AGC 又以 path dependency 复用若干 server-rs crate;更主要的是全量 / 分组 `cargo test` 继承增量编译,每组 crate / feature / profile hash 都可以留下新会话,Cargo 不会按仓库期望自动收缩这些历史目录。 - 处理:保留两个 workspace 的产品 / 发布边界;两边 `[profile.test]` 关闭 incremental 并固定 `debug=1`,AGC dev profile 与 server-rs 对齐调试信息级别。日常用 `npm run audit:rust-build-cache` 只读核对;需要回收时先停止 Cargo / rustc,再显式运行 `npm run clean:rust-incremental -- --apply`,只删两个固定增量目录。 - 验证:清理前后各跑一次只读审计并核对磁盘可用空间;分别运行 server-rs 与 AGC 定向 `cargo test`,确认 test profile 不再生成持久 `debug/incremental` 堆积。共享 `CARGO_TARGET_DIR` 必须另做并发启动基准,不得为节省磁盘直接改变生产产物路径。 + +## BuildKit secret 不等于镜像内 secrets 不可提取(2026-08-22) + +- 现象:构建时使用 BuildKit secret mount,日志和普通 build context 都没有出现明文,于是误以为最终镜像也能不可提取地保存 secrets,随后将镜像 push 或导出给不同信任域。 +- 原因:BuildKit secret mount 只避免秘密作为 `ARG` / `COPY` 进入构建上下文和中间指令;一旦 Dockerfile 把 mount 的内容安装到最终 rootfs,任何能读取、保存或运行该镜像的主体都可以提取它。 +- 处理:预览固定 secrets 只从 Jenkins 宿主受控路径读取,严格校验目录 `0700`、文件 `0600`、owner、普通文件与非链接边界;只将其安装到 `api-runtime:/srv/genarrative/.env.secrets.local` 并设为 `0400`,明确排除 Nginx、Web、artifact 和其它镜像。镜像禁止推送或导出到跨信任边界。 +- 更新与验证:源文件变更不会改动已存镜像,必须重建并替换容器;不能用重启代替。验收同时扫描 transcript/context/artifact 零泄漏,检查只有 API 最终 rootfs 存在目标文件,并验证容器显式运行 env 优先覆盖内置值。 diff --git a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md index 878172c07..a82aed397 100644 --- a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md +++ b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md @@ -47,7 +47,15 @@ SpacetimeDB 2.7 CLI 发布到受控 Compose 网络地址时固定使用 `--yes=r 预览 Compose override 将 SpacetimeDB 内存上限设为 `2g`。基础 loadtest Compose 的 `896m` 是压测采样口径,当前完整模块首次发布和实例化会超过该上限;预览环境若沿用该值,容器会被 cgroup OOM 杀死并使模块上传中断。该覆盖只作用于分支预览实例,不修改生产或压测基线。 -每个预览实例使用独立 SpacetimeDB 空库,不继承生产账号和短信凭据。Jenkins 在实例私有 `api-server.env` 中开启预览专用认证:未注册的中国大陆手机号首次使用 6 到 128 位密码时自动创建预览账号;短信入口使用 `mock` provider 与固定预览验证码 `123456`,不向真实手机发送短信。该设置不写入公共 env 示例、生产配置或镜像层;实例重建会重建独立数据库,原预览账号不保留。 +每个预览实例使用独立 SpacetimeDB 空库,不继承生产账号数据。Jenkins 在实例私有 `api-server.env` 中开启预览专用认证:未注册的中国大陆手机号首次使用 6 到 128 位密码时自动创建预览账号;短信入口强制使用 `mock` provider 与固定预览验证码 `123456`,不调用镜像内置的真实短信凭据。该预览认证覆盖不写入公共 env 示例或生产配置;实例重建会重建独立数据库,原预览账号不保留。 + +## 预览 secrets 内置 + +Jenkins 节点上的预览 secrets 权威来源固定为 `/data/jenkins/preview-secrets/.env.secrets.local`。该文件不进 Git、Docker build context、构建日志或 artifact;构建时只通过 BuildKit `secret` mount 临时提供给 `api-runtime` stage,并在该运行镜像中安装为 `/srv/genarrative/.env.secrets.local`,权限固定为 `0400`。`nginx-runtime`、Web 静态产物、SpacetimeDB 镜像及其它镜像不得包含该文件。 + +宿主固定目录应由 Jenkins 运行账号所有且权限为 `0700`,源文件权限为 `0600`;缺失、不是普通文件、owner 不匹配或权限过宽时,预览构建必须失败关闭。源文件变更后必须重新构建并替换预览镜像,只重启容器不会刷新已内置的内容。容器启动时显式注入的运行环境变量优先级高于镜像内的 `.env.secrets.local`,用于按实例覆盖非通用值。 + +这种方案只隐藏构建传输过程,不能让内置后的 secrets 对镜像持有者保密:能读取、保存或运行 `api-runtime` 镜像的人可以提取该文件。因此该镜像只能留在当前受信任内网 Docker 主机,禁止 push 到公共或跨信任边界的 registry,也禁止通过 `docker save`/构建 artifact 导出传播。需要跨边界分发时必须改用不含 secrets 的镜像与运行时密钥注入。 ## Jenkins 参数与产物 @@ -110,6 +118,7 @@ Jenkins 在构建完成、归档 artifact 和更新 REST 状态之间可能短 - Jenkins service account 只授予 `shared/Genarrative-Preview-Deployer` 的 `Job/Read`、`Job/Build` 和读取构建产物所需权限,不授 `Overall/Administer`、`Job/Configure` 或 `Job/Delete`。 - 后端固定 Jenkins origin、Job 路径和参数白名单;客户端不能传 URL、Job 名、Compose project、容器名、宿主端口或 Jenkins 凭据。 - Git 查询固定使用本机 Gitea SSH 地址和服务端只读凭据;客户端不能传 remote、SSH 参数或凭据。Git 缓存只写入预览控制服务的受控状态目录,搜索接口需要控制台会话且结果有数量上限。 +- 预览 secrets 只从固定宿主路径读取,构建前校验 owner、类型和权限;不允许分支、Jenkins 参数或控制面请求改写 secrets 路径、BuildKit secret ID 或镜像内目标路径。 - Jenkins POST 支持动态 Crumb;API Token 即使免 Crumb,也不能把 Token 放进 URL 或日志。 - API 默认只接受同源请求,写请求校验 Origin;内网本身不作为认证。 - 同一 deployment 的发布和卸载串行执行;重复请求必须幂等或明确返回冲突。 @@ -120,5 +129,6 @@ Jenkins 在构建完成、归档 artifact 和更新 REST 状态之间可能短 - 后端:输入校验、登录会话、Origin、Crumb、Jenkins `401/403/404/5xx`、queue 到 build 状态机、artifact schema、卸载所有权和幂等测试。 - 前端:登录、分支与可选 commit、自动刷新、排队/构建/成功/失败状态、内网链接、卸载确认和刷新恢复测试。 - Jenkins:两个分支依次发布后在不同端口并存;同一分支换 commit 优先复用端口;非分支 commit 被拒绝;卸载只删除目标实例并释放端口。 -- 预览认证:新手机号可以首次密码登录并重复使用同一密码;错误密码被拒绝;获取验证码后使用 `123456` 可完成登录;容器不包含生产短信凭据。 +- 预览认证:新手机号可以首次密码登录并重复使用同一密码;错误密码被拒绝;获取验证码后使用 `123456` 可完成登录;短信入口保持 `mock` 且不调用内置的真实短信凭据。 +- secrets:构建 transcript、context 和 artifact 零出现原文;只有 `api-runtime` 包含 `0400` 的目标文件,`nginx-runtime` 与其它镜像均不包含;修改固定源文件后旧镜像不变、重建新镜像后摘要更新;显式运行 env 可覆盖内置值。 - 通用:`npm run check:encoding`、相关 typecheck/build/test、Rust 定向测试和 `git diff --check`。 diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 0808dc5ed..d63b06067 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -14,6 +14,7 @@ pipeline { GIT_REMOTE_URL = 'ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git' GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh' GENARRATIVE_PREVIEW_STATE_ROOT = '/data/jenkins/preview-deployments' + GENARRATIVE_PREVIEW_SECRETS_FILE = '/data/jenkins/preview-secrets/.env.secrets.local' GENARRATIVE_PREVIEW_WEB_HOST = '192.168.35.82' } diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index 860417d07..d0be67c58 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -5,6 +5,10 @@ const jenkinsfile = readFileSync( 'utf8', ); const deployer = readFileSync('scripts/jenkins-preview-deployer.sh', 'utf8'); +const apiServerDockerfile = readFileSync( + 'deploy/container/api-server.Dockerfile', + 'utf8', +); const resultWriter = readFileSync( 'scripts/preview-deployment-status.mjs', 'utf8', @@ -125,6 +129,67 @@ assertIncludes( 'restart: on-failure', '预览外部生成 worker 必须在运行时身份初始化竞态后自动重启。', ); +assertIncludes( + deployer, + 'GENARRATIVE_PREVIEW_SECRETS_SHA256', + '预览构建必须把固定 secrets 文件摘要作为镜像缓存与完整性校验参数。', +); +assertIncludes( + jenkinsfile, + "GENARRATIVE_PREVIEW_SECRETS_FILE = '/data/jenkins/preview-secrets/.env.secrets.local'", + 'Jenkins 必须从受保护的固定宿主路径读取预览 secrets。', +); +assertIncludes( + deployer, + '[[ "${secrets_mode}" == "600" ]]', + '预览构建必须拒绝权限过宽的 secrets 文件。', +); +assertIncludes( + deployer, + '[[ "${secrets_owner}" == "${EUID}" ]]', + '预览构建必须校验 secrets 文件归 Jenkins 执行用户所有。', +); +assertIncludes( + deployer, + 'genarrative_preview_secrets', + '预览 API 与外部生成 worker 构建必须使用固定名称的 BuildKit secret。', +); +assertCount( + deployer, + 'target: genarrative_preview_secrets', + 2, + '预览 secrets 必须且只能提供给 API 和外部生成 worker 两个构建。', +); +assertIncludes( + apiServerDockerfile, + 'ARG GENARRATIVE_PREVIEW_SECRETS_SHA256=', + 'API 镜像必须允许普通构建不提供预览 secrets 摘要。', +); +assertIncludes( + apiServerDockerfile, + 'RUN --mount=type=secret,id=genarrative_preview_secrets,required=false', + 'API 镜像必须通过可选 BuildKit secret 接收预览 secrets 文件。', +); +assertIncludes( + apiServerDockerfile, + 'test -f /run/secrets/genarrative_preview_secrets;', + '提供预览 secrets 摘要时必须要求 BuildKit secret 存在。', +); +assertIncludes( + apiServerDockerfile, + 'sha256sum /run/secrets/genarrative_preview_secrets', + 'API 镜像必须在安装前校验预览 secrets 文件摘要。', +); +assertIncludes( + apiServerDockerfile, + 'install -o genarrative -g genarrative -m 0400', + '预览 secrets 文件必须只允许 API 运行用户读取。', +); +assertIncludes( + apiServerDockerfile, + '/run/secrets/genarrative_preview_secrets /srv/genarrative/.env.secrets.local;', + '预览 secrets 文件必须安装到 API 启动时读取的固定路径。', +); assertIncludes( deployer, 'GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true', @@ -256,3 +321,7 @@ function assertIncludes(content, expected, message) { function assertExcludes(content, expected, message) { if (content.includes(expected)) failures.push(message); } + +function assertCount(content, expected, count, message) { + if (content.split(expected).length - 1 !== count) failures.push(message); +} diff --git a/scripts/jenkins-preview-deployer.sh b/scripts/jenkins-preview-deployer.sh index 9e61cebfc..ec1e7dcb2 100644 --- a/scripts/jenkins-preview-deployer.sh +++ b/scripts/jenkins-preview-deployer.sh @@ -10,6 +10,7 @@ SOURCE_DIR="${SOURCE_DIR:-${WORKSPACE:-$(pwd)}/source}" RESULT_FILE="${RESULT_FILE:-${WORKSPACE:-$(pwd)}/preview-result.json}" DESCRIPTION_FILE="${DESCRIPTION_FILE:-${WORKSPACE:-$(pwd)}/.jenkins-preview-description}" STATE_ROOT="${GENARRATIVE_PREVIEW_STATE_ROOT:-/data/jenkins/preview-deployments}" +PREVIEW_SECRETS_FILE="${GENARRATIVE_PREVIEW_SECRETS_FILE:-/data/jenkins/preview-secrets/.env.secrets.local}" WEB_HOST="${GENARRATIVE_PREVIEW_WEB_HOST:-}" LOCK_FILE="${GENARRATIVE_PREVIEW_LOCK_FILE:-${STATE_ROOT}/.lock}" @@ -18,6 +19,7 @@ STATE_FILE="" PROJECT_NAME="" SCRIPT_ROOT="" SCRIPT_FAILED=1 +PREVIEW_SECRETS_SHA256="" fail() { echo "[preview-deployer] $*" >&2 @@ -244,6 +246,27 @@ allocate_port() { fail "端口范围 ${start}-${end} 已无可用端口。" } +validate_preview_secrets_file() { + local secrets_dir secrets_mode secrets_owner canonical_secrets canonical_source + [[ "${PREVIEW_SECRETS_FILE}" == /* ]] || fail "预览 secrets 文件必须使用绝对路径。" + [[ -f "${PREVIEW_SECRETS_FILE}" && ! -L "${PREVIEW_SECRETS_FILE}" && -r "${PREVIEW_SECRETS_FILE}" ]] || \ + fail "预览 secrets 文件必须是 Jenkins 可读的非符号链接普通文件: ${PREVIEW_SECRETS_FILE}" + secrets_dir="$(dirname "${PREVIEW_SECRETS_FILE}")" + [[ -d "${secrets_dir}" && ! -L "${secrets_dir}" ]] || \ + fail "预览 secrets 目录必须是非符号链接目录: ${secrets_dir}" + secrets_mode="$(stat -c '%a' "${PREVIEW_SECRETS_FILE}")" + [[ "${secrets_mode}" == "600" ]] || fail "预览 secrets 文件权限必须是 0600: ${PREVIEW_SECRETS_FILE}" + secrets_owner="$(stat -c '%u' "${PREVIEW_SECRETS_FILE}")" + [[ "${secrets_owner}" == "${EUID}" ]] || fail "预览 secrets 文件必须归当前 Jenkins 执行用户所有。" + canonical_secrets="$(realpath -e "${PREVIEW_SECRETS_FILE}")" + canonical_source="$(realpath -e "${SOURCE_DIR}")" + [[ "${canonical_secrets}" != "${canonical_source}"/* ]] || \ + fail "预览 secrets 文件不能位于目标分支源码上下文内。" + PREVIEW_SECRETS_SHA256="$(sha256sum "${PREVIEW_SECRETS_FILE}")" + PREVIEW_SECRETS_SHA256="${PREVIEW_SECRETS_SHA256%% *}" + [[ "${PREVIEW_SECRETS_SHA256}" =~ ^[0-9a-f]{64}$ ]] || fail "无法计算预览 secrets 文件摘要。" +} + remove_project_resources() { local ids=() local images=() @@ -273,6 +296,8 @@ compose() { COMPOSE_PROJECT_NAME="${PROJECT_NAME}" \ GENARRATIVE_PREVIEW_SOURCE_DIR="${SOURCE_DIR}" \ GENARRATIVE_PREVIEW_CONTROLLER_ROOT="${SCRIPT_ROOT}/.." \ + GENARRATIVE_PREVIEW_SECRETS_FILE="${PREVIEW_SECRETS_FILE}" \ + GENARRATIVE_PREVIEW_SECRETS_SHA256="${PREVIEW_SECRETS_SHA256}" \ GENARRATIVE_CONTAINER_API_ENV_FILE="${STATE_DIR}/api-server.env" \ GENARRATIVE_CONTAINER_HTTP_PORT="${WEB_PORT}" \ GENARRATIVE_CONTAINER_SPACETIME_PORT="${SPACETIME_PORT}" \ @@ -292,10 +317,20 @@ services: build: context: ${GENARRATIVE_PREVIEW_SOURCE_DIR} dockerfile: ${GENARRATIVE_PREVIEW_CONTROLLER_ROOT}/deploy/container/api-server.Dockerfile + args: + GENARRATIVE_PREVIEW_SECRETS_SHA256: ${GENARRATIVE_PREVIEW_SECRETS_SHA256} + secrets: + - source: preview_runtime_env + target: genarrative_preview_secrets external-generation-worker: build: context: ${GENARRATIVE_PREVIEW_SOURCE_DIR} dockerfile: ${GENARRATIVE_PREVIEW_CONTROLLER_ROOT}/deploy/container/api-server.Dockerfile + args: + GENARRATIVE_PREVIEW_SECRETS_SHA256: ${GENARRATIVE_PREVIEW_SECRETS_SHA256} + secrets: + - source: preview_runtime_env + target: genarrative_preview_secrets restart: on-failure nginx: build: @@ -306,6 +341,9 @@ services: ports: !reset [] otelcol: ports: !reset [] +secrets: + preview_runtime_env: + file: ${GENARRATIVE_PREVIEW_SECRETS_FILE} YAML chmod 0600 "${override_file}" } @@ -394,6 +432,7 @@ deploy() { [[ "${source_commit}" == "${requested_commit}" ]] || fail "源码 checkout 与 COMMIT_HASH 不一致。" fi + validate_preview_secrets_file saved_web="$(state_value webPort)" WEB_PORT="${saved_web:-8400}" SPACETIME_PORT=""