chore: unify production pipelines on linux

This commit is contained in:
2026-06-01 06:05:19 +00:00
parent 42a9f3cce1
commit 6cd554473b
7 changed files with 171 additions and 476 deletions

View File

@@ -322,6 +322,7 @@
## 2026-05-18 Windows Jenkins PowerShell 统一改为显式 powershell.exe 启动
- 后续更新:该决策仅适用于历史 Windows Jenkins 节点;当前 `Genarrative-Stdb-Module-Build` 已改为 Linux agent实际执行路径不再依赖该口径。
- 背景:`Genarrative-Stdb-Module-Build` 在 Windows Jenkins 本地环境里调用裸 `powershell` step 时触发 `CreateProcess error=5, 拒绝访问`,而 `powershell.exe` 本体与 workspace ACL 都正常。
- 决策Windows Jenkins 上凡是需要执行 PowerShell 逻辑的流水线,优先通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`,不要再依赖 Jenkins `powershell` step 的隐式启动器。
- 追加决策:`Genarrative-Stdb-Module-Build` 的 Checkout 逻辑应复用 Jenkins GitSCM 已完成的工作区状态。`COMMIT_HASH` 为空或已与当前 `HEAD` 一致时,不再额外执行 `git clean` / `git checkout`;只有需要切到指定且不同的 commit 时才补 fetch、校验和切换避免在 Windows workspace 里二次清理触发权限拒绝。
@@ -366,6 +367,7 @@
## 2026-05-19 生产 provision 改为 Windows 下载包后由目标机本地安装
- 后续更新:该口径已被 `2026-06-01 生产 Jenkins 流水线统一改为 Linux 优先并先查 localhost` 取代;当前 `Genarrative-Server-Provision` 不再走 Windows 下载阶段,而是在 Linux build 节点直接准备 `provision-tools/`
- 背景:当前 `development` provision 目标实际就是 Linux agent `genarrative-build-01`,之前把 `Prepare Provision Tools` 放在 `linux && genarrative-build` 会让目标机自己连 GitHub 和 `install.spacetimedb.com`违背“Windows 本机先下载再传到目标机”的运维要求。
- 决策:`Genarrative-Server-Provision` 拆成 Windows 下载阶段和 Linux 目标机安装阶段。Windows 节点的 `Download Provision Tool Archives` 只下载 `spacetime-x86_64-unknown-linux-gnu.tar.gz``otelcol-contrib_0.151.0_linux_amd64.tar.gz`,通过 `stash/unstash` 传到目标 Linux 节点;目标机执行 `scripts/prepare-server-provision-tools.sh` 时设置 `PROVISION_REQUIRE_LOCAL_DOWNLOADS=true`,只消费已下载件生成 `provision-tools/`,缺包直接失败,不回退外网下载。
- 追加决策Server-Provision 的 Windows helper 不再对 Jenkins `writeFile` 刚写出的 `.ps1` 做原地 UTF-8 BOM 重写,而是由显式 `powershell.exe` 按 UTF-8 读入脚本文本,并用 `ScriptBlock::Create(...)` 在内存中执行;这样既保留中文脚本内容,又避免同一个 workspace 脚本被立即重写时触发 `拒绝访问`
@@ -1013,12 +1015,21 @@
## 2026-05-19 server provision 下载件固定由 Windows 节点断点续传
- 后续更新:该口径已被 `2026-06-01 生产 Jenkins 流水线统一改为 Linux 优先并先查 localhost` 取代;当前不再维护 Windows 下载阶段和 `.download` 断点续传 helper。
- 背景:`SpacetimeDB``otelcol-contrib` release 资产在 Linux 目标机直接下载很慢;改到 Windows Jenkins 节点下载后GitHub 大文件仍可能出现 `curl: (18)` 响应体截断。
- 决策:`Genarrative-Server-Provision``Download Provision Tool Archives` 阶段继续只在 Windows 节点下载,再通过 `stash/unstash` 交给目标 Linux agent下载前查 GitHub release asset `digest`,本地最终文件 SHA256 命中即跳过,`.download` 临时文件用于 `curl -C -` 断点续传,完整返回但 digest 不匹配才清理重下。
- 影响范围:`jenkins/Jenkinsfile.production-server-provision`、目标机 `scripts/prepare-server-provision-tools.sh` 的本地下载件消费路径、生产 provision 运维排障。
- 验证方式Windows 下载日志应出现 digest 查询、已存在校验跳过或 `curl 断点续传`Linux 目标机阶段只使用 `provision-tool-downloads/` 中的 tarball不访问 GitHub 下载地址。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-06-01 生产 Jenkins 流水线统一改为 Linux 优先并先查 localhost
- 背景:生产流水线长期混用 Windows、Linux 和公网 Git 入口,导致构建 / 发布 / provision 的 checkout 口径分叉;同时 `Genarrative-Server-Provision` 还残留过 Windows 下载 helper和当前 Linux 构建 / 发布部署路径不一致。
- 决策:生产 Jenkins 流水线统一把执行节点收口到 Linux label`Pipeline script from SCM` 仍保留公网域名,但所有生产流水线首次 `GitSCM checkout` 先尝试 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`,失败后再回退到 `https://git.genarrative.world/GenarrativeAI/Genarrative.git``Genarrative-Stdb-Module-Build``Genarrative-Server-Provision``Genarrative-Notify-Email` 也都切到 Linux 节点。`Genarrative-Server-Provision` 的工具准备不再依赖 Windows helper而是在 Linux build 节点直接生成 `provision-tools/` 后交给后续 Linux 发布阶段。
- 影响范围:`jenkins/Jenkinsfile.production-*``scripts/jenkins-checkout-source.sh``scripts/prepare-server-provision-tools.sh`、生产运维文档。
- 验证方式:扫描 Jenkinsfile 时应看到 `linux && genarrative-*` 节点和 localhost-first checkout 口径;`Genarrative-Server-Provision` 日志不再出现 Windows 相关 helper 输出,工具准备阶段应直接生成 `provision-tools/`
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 个人任务与埋点首版边界冻结
- 背景“我的”Tab、任务、奖励、钱包和埋点涉及用户、运营、分析多条链路需要避免范围泛化。

View File

@@ -307,6 +307,7 @@
## Windows provision 下载截断要断点续传而不是回退目标机下载
- 当前状态已废弃。2026-06-01 起生产 Jenkins 流水线统一切到 Linux agent`Genarrative-Server-Provision` 不再维护 Windows 下载阶段。
- 现象:`Genarrative-Server-Provision``Download Provision Tool Archives` 阶段出现 `curl: (18) end of response ... bytes missing`,常见于 `otelcol-contrib_0.151.0_linux_amd64.tar.gz` 等 GitHub release 大文件。
- 原因:这是 Windows Jenkins 节点到 GitHub 的响应体被截断;若每轮都删除 `.download` 临时文件,就会丢掉已下载部分,下一次又从头开始。
- 处理Windows 下载函数保留 `${Output}.download``curl` 失败时下一轮使用 `-C -` 断点续传;最终只以 GitHub release asset 的 SHA256 `digest` 作为放行条件,完整返回但 digest 不匹配才删除临时文件重新下载。不要把 SpacetimeDB 或 `otelcol-contrib` 下载挪回 Linux 目标机。
@@ -1162,12 +1163,12 @@
- 验证:运行 `git ls-files --stage scripts/prepare-server-provision-tools.sh`,确认 mode 为 `100755`;重新跑 `Genarrative-Server-Provision` 时应进入工具下载/打包日志,而不是停在 `Permission denied`
- 关联:`jenkins/Jenkinsfile.production-server-provision``scripts/prepare-server-provision-tools.sh``scripts/jenkins-checkout-source.sh``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## Server-Provision 下载阶段不要放回 genarrative-build-01
## Server-Provision 工具准备只在 Linux build 节点做一次
- 现象:`Genarrative-Server-Provision` 日志里 `Prepare Provision Tools` 显示 `Running on genarrative-build-01 in /root/...`,随后在该节点上下载 GitHub release `install.spacetimedb.com` 失败
- 原因:`genarrative-build-01` 在当前 provision 流程里是 Linux 目标发布机/目标 agent不是用户本地 Windows 下载环境;把下载阶段放在 `linux && genarrative-build` 等于让目标机自己外连
- 处理:下载必须发生在 Jenkins `windows` 节点 `Download Provision Tool Archives` 阶段,先下载 SpacetimeDB Linux release tarball 和 `otelcol-contrib` Linux amd64 包,再 `stash/unstash` 到目标 Linux 节点。目标机执行 `scripts/prepare-server-provision-tools.sh` 时设置 `PROVISION_REQUIRE_LOCAL_DOWNLOADS=true`,缺少下载件直接失败,不回退联网下载
- 验证Jenkins 日志应先出现 `Running on ... windows``[prepare-provision-downloads] 下载 ...`,目标节点只出现 `[prepare-provision-tools] 使用已下载的 ...`;如果目标节点出现 `下载 otelcol-contrib:``下载 SpacetimeDB 官方安装器脚本:`,说明又回退到错误路径
- 现象:`Genarrative-Server-Provision` 在后续目标发布节点重复执行 `scripts/prepare-server-provision-tools.sh`,或日志里出现目标节点继续访问 GitHub release / `install.spacetimedb.com`
- 原因:当前流水线已经改成 Linux build 节点一次性准备 `provision-tools/` 并 stash 给目标发布阶段;如果目标发布阶段又重新准备工具包,就会重复下载并把目标节点暴露到外网依赖
- 处理:只允许 `Prepare Provision Tools` 阶段在 `linux && genarrative-build` 节点生成 `provision-tools/`;后续 `Provision Server` 阶段`unstash 'server-provision-tools'` 并安装其中的 `spacetime``otelcol-contrib`,不要再运行 `prepare-server-provision-tools.sh`
- 验证Jenkins 日志应先在 Linux build 节点出现 `[prepare-provision-tools] 工具包已准备`,后续目标发布节点只出现安装 / systemd / Nginx provision 日志;目标节点不应出现 `下载 otelcol-contrib:``下载 SpacetimeDB 官方安装器脚本:`
- 关联:`jenkins/Jenkinsfile.production-server-provision``scripts/prepare-server-provision-tools.sh``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 个人任务 scope 不得扩成 work/site/module
@@ -1486,6 +1487,7 @@
## Windows Jenkins `powershell` step 在 Stdb module 构建里曾触发 CreateProcess error=5
- 当前状态:已废弃。`Genarrative-Stdb-Module-Build` 已切到 Linux agent不再执行 Windows PowerShell 流程。
- 现象:`Genarrative-Stdb-Module-Build` 在 Windows Jenkins 节点上报 `java.io.IOException: Cannot run program "powershell" (in directory "C:\\Users\\DSK\\.jenkins-local\\workspace\\Genarrative-Stdb-Module-Build"): CreateProcess error=5, 拒绝访问。`;日志里能看到 `durable-task` 已写出 `powershellWrapper.ps1`,但在真正启动裸 `powershell` 子进程时失败。
- 原因Jenkins durable-task 的 `powershell` step 依赖一个隐式命令解析/启动路径,在这台 Windows 本地 Jenkins 环境里会被拒绝。`powershell.exe` 本体和 workspace ACL 都是正常的,问题出在 Jenkins step 的启动方式,而不是 PowerShell 脚本内容。修复后若日志能打印 `[jenkins-powershell] exe:`,但随后仅报 `拒绝访问` / `script returned exit code 5`,通常已经不是 PowerShell 启动失败,而是 Checkout 脚本内部命令在 Windows workspace 里触发权限拒绝。若 `.jenkins-*.ps1` 里中文 `throw '[stdb-build] ...'``MissingArrayIndexExpression`,则是 Windows PowerShell 5.1 用 `-File` 解析无 BOM UTF-8 脚本时按本地 ANSI 误解码。
- 处理:把 `jenkins/Jenkinsfile.production-stdb-module-build``Checkout``Build Stdb Module` 两处 `powershell` step 收口成 `runWindowsPowerShell(...)` helper先用 `writeFile` 写出临时 `.ps1`,再用显式 `powershell.exe` 把脚本重写成 UTF-8 with BOM最后通过 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...` 执行。这个 helper 写在 Groovy GString 里时PowerShell 的 `$path` / `$text` / `$true` 必须写成 `\$path` / `\$text` / `\$true`,否则 Jenkinsfile 会在 Groovy 编译阶段报 `unexpected token: true`。Checkout 阶段优先复用 Jenkins GitSCM 已完成的工作区结果;`COMMIT_HASH` 为空或已经等于当前 `HEAD` 时不再重复 `git fetch` / `git checkout` / `git clean`,只有确实要切到另一个指定 commit 时才补 fetch、归属校验和 checkout。

View File

@@ -234,14 +234,14 @@ Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分
`Genarrative-Web-Build` 的主站构建失败若出现 Rollup 报错 `"xxx" is not exported by "src/services/publicWorkCode.ts"`,优先按前端公开作品号工具缺失处理,而不是排查 Jenkins 节点环境。修复时要让 `publicWorkCode.ts``build<Play>PublicWorkCode``isSame<Play>PublicWorkCode` 成对导出,并补 `src/services/publicWorkCode.test.ts` 覆盖对应玩法前缀;随后用 `npm run build:production-release -- --component web --name <临时名>` 复现 Jenkins web 构建路径。
生产 Jenkins 的 `Pipeline script from SCM`Windows controller 读取 JenkinsfileSCM URL 继续使用 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`运行在 `linux && genarrative-build` 构建机上的 `Genarrative-Full-Build-And-Deploy` 源码解析阶段和 `Genarrative-Web-Build` checkout 阶段,优先使用 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`,失败后回退到 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`;两层 checkout 都必须保留单分支 refspec、`shallow=true``depth=1``noTags=true``honorRefspec=true`,后续二次源码确认继续走 `scripts/jenkins-checkout-source.sh`
生产 Jenkins 的 `Pipeline script from SCM` Jenkins controller 读取 JenkinsfileSCM URL 继续使用 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`现在所有生产流水线 job 的首次 checkout 都先走 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`,失败后回退到 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`;两层 checkout 都必须保留单分支 refspec、`shallow=true``depth=1``noTags=true``honorRefspec=true`,后续二次源码确认继续走 `scripts/jenkins-checkout-source.sh`
`Genarrative-Stdb-Module-Publish``Pipeline script from SCM` 阶段如果一开始就报 `No such DSL method 'pipeline'`,优先检查 `jenkins/Jenkinsfile.production-stdb-module-publish` 是否带 UTF-8 BOM。Jenkins Declarative Pipeline 的首个 token 必须是纯 `pipeline`;仓库中的 Jenkinsfile 应保存为 UTF-8 without BOM只有临时写给 Windows PowerShell 5.1 `-File` 执行的 `.ps1` 才需要按对应 helper 转成带 BOM。验证时可检查文件前三字节不再是 `EF BB BF`,并运行 `validateDeclarativePipeline` 或重放该流水线。
`Genarrative-Stdb-Module-Build` 或 SpacetimeDB module 构建失败若出现 Rust `E0425 cannot find function migrate_*`,优先排查 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs` 等同文件内默认种子迁移 helper 是否在分支合并时只保留了调用、漏掉了函数定义。修复时不要直接删除迁移调用;应恢复只纠偏历史默认种子且不覆盖后台手动配置的 helper并用 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 复现 Jenkins module 编译路径。
`Genarrative-Stdb-Module-Build` 或 SpacetimeDB module 构建失败若出现 Rust `E0425 cannot find function migrate_*`,优先排查 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs` 等同文件内默认种子迁移 helper 是否在分支合并时只保留了调用、漏掉了函数定义。`Genarrative-Stdb-Module-Build` 现在运行在 `linux && genarrative-build` 节点上Checkout 与 Build 都走 bash + cargo + sccache不再依赖 Windows PowerShell 或 Git Bash。修复时不要直接删除迁移调用;应恢复只纠偏历史默认种子且不覆盖后台手动配置的 helper并用 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 复现 Jenkins module 编译路径。
Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该流水线需要执行 PowerShell 逻辑时,统一通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`,不要直接使用 Jenkins `powershell` step本地 Jenkins durable-task 曾在 `Genarrative-Stdb-Module-Build` workspace 中启动裸 `powershell` 时触发 `CreateProcess error=5, 拒绝访问`。临时 `.ps1` 由 Jenkins `writeFile` 写出后要先转成 UTF-8 with BOM 再交给 Windows PowerShell 5.1 `-File` 解析,避免中文错误消息在无 BOM UTF-8 下被当成本地 ANSI 误解码。Checkout 阶段要优先复用 Jenkins GitSCM 已经完成的结果:`COMMIT_HASH` 为空或与当前 `HEAD` 一致时,不要再额外 `git clean` / `git checkout`,只在确实需要切到别的指定 commit 时才补 fetch、校验和切换。排查时先看对应 build log、`@tmp/durable-*` 下的 `powershellWrapper.ps1`,以及日志中的 `[jenkins-powershell] user/exe`
`Genarrative-Server-Provision` 现在也运行在 `linux && genarrative-build` / `linux && genarrative-release-deploy` 节点上,`Prepare Provision Tools` 会在 Linux build 节点直接准备 SpacetimeDB 与 `otelcol-contrib` 交付件,再 stash 给后续发布阶段;旧 Windows 下载 helper 已退役。`Genarrative-Stdb-Module-Build``Genarrative-Server-Provision` `Genarrative-Notify-Email` 都不再需要单独的 Windows 节点口径
生产环境变量模板:`deploy/env/api-server.env.example`。真实密钥只放服务器,不提交 Git不写入文档示例。
@@ -252,9 +252,8 @@ Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该
- `api-server` 生产模板默认 `GENARRATIVE_API_LISTEN_BACKLOG=1024``GENARRATIVE_API_WORKER_THREADS=4`;本地未设置 worker threads 时继续使用 Tokio 默认值。
- `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压;`GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320``GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64``GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16` 分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回 `429 Too Many Requests``Retry-After: 1``/healthz` 不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连 `api-server` 的极高 RPS 压测若出现 `connection refused`,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。
- `genarrative-api.service` 设置 `LimitNOFILE=65535``TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax``cat /proc/$(pidof api-server)/limits` 核对。
- Server provision 不在目标机联网下载 SpacetimeDB 或 `otelcol-contrib``Genarrative-Server-Provision` 先在 Windows Jenkins 节点执行 `Download Provision Tool Archives`,把 `spacetime-x86_64-unknown-linux-gnu.tar.gz``otelcol-contrib_0.151.0_linux_amd64.tar.gz` 先下载到工作区,再通过 `stash/unstash` 带到 `genarrative-build-01`Windows 下载前会先查 GitHub release asset 的 `digest` 字段做 SHA256 校验,已有本地文件且 digest 一致就直接复用,不再重复下载。目标 Linux 节点上的 `scripts/prepare-server-provision-tools.sh` 只消费这些本地下载件生成 `provision-tools/`,再交给 `scripts/jenkins-server-provision.sh` 安装 `/stdb/spacetime``/stdb/bin/current/*``/usr/local/bin/otelcol-contrib`。Windows 侧的 `runWindowsPowerShell(...)` 现在会先 `writeFile` 生成 UTF-8 `.ps1`,再直接把脚本文本读入内存并通过 `ScriptBlock::Create(...)` 执行,避免对同一个 workspace 脚本做原地 BOM 重写。排查时除了看下载日志,还要看 `[jenkins-powershell] workspace:``[jenkins-powershell] script:``[jenkins-powershell] loaded bytes:`。注意 `scripts/jenkins-checkout-source.sh` 会执行 `git reset --hard` / `git clean`,因此被直接执行的新增脚本必须以 Git `100755` 模式提交,或在二次 checkout 之后再补 `chmod +x`
- Windows 下载阶段如果出现 `curl: (18)` 或响应体截断,流水线会保留同名 `.download` 临时文件并用 `curl -C -` 断点续传;只有完整返回但 SHA256 digest 仍不匹配时才删除临时文件后重新下载。目标 Linux 节点仍只接收 `stash/unstash` 带过去的本地下载件,不回退外网下载
- Windows 下载阶段如果走代理,在 `Genarrative-Server-Provision` 参数 `PROVISION_DOWNLOAD_PROXY` 填写 Windows Jenkins 节点可访问的 HTTP 代理,例如 `http://127.0.0.1:7890`;不要填写目标 release 机器视角的 `127.0.0.1`,除非代理确实运行在该 Windows 节点本机。Linux 目标机阶段会强制要求使用本地下载件,缺少文件直接失败,不再回退到外网下载。
- Server provision 不再通过 Windows helper 下载。`Genarrative-Server-Provision``Prepare Provision Tools` 在 Linux build 节点直接准备 `spacetime-x86_64-unknown-linux-gnu.tar.gz``otelcol-contrib_0.151.0_linux_amd64.tar.gz`,再 stash `provision-tools/` 给后续发布阶段;如果 build 节点需要代理,在 `PROVISION_DOWNLOAD_PROXY` 配置 Linux 侧可访问的 HTTP 代理。后续 Linux 目标节点只消费 `provision-tools/`,不再回退到外网下载
- `Genarrative-Stdb-Module-Build``Genarrative-Web-Build``Genarrative-Api-Build``Genarrative-*Deploy``Genarrative-Database-Import/Export``Genarrative-Full-Build-And-Deploy``Genarrative-Notify-Email` 的生产流水线现都以 Linux agent 为主,统一走 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git` 优先、`https://git.genarrative.world/GenarrativeAI/Genarrative.git` 备用的 checkout 口径
- `otelcol-contrib.service` 作为可选系统服务加入 provision默认监听 `127.0.0.1:4317/4318` 并使用 `deploy/otelcol/genarrative-debug.yaml`。api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制,服务 unit 见 `deploy/systemd/otelcol-contrib.service`
- Nginx `/api/``/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`upstream keepalive 为 64`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s``burst=4096``limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`。通用 `/api` location 设置 `client_max_body_size 64m` 是反代兜底,防止拼图入口页 / 新增关卡本地参考图 Data URL 或旧兼容请求在到达 `api-server` 前被默认 1 MiB 上限拦截;拼图本地参考图前后端统一限制 6MB历史图片仍提交 `referenceImageAssetObjectId(s)`。若线上出现 `413 Request Entity Too Large` 且 access log 中 `request_time=0.000``upstream_status=-`,说明请求在 Nginx 层被拦截,先用 `nginx -T | grep client_max_body_size` 检查 release 模板是否已渲染并 reload同时检查前端是否超出 6MB 或错误提交了未压缩大图。`limit_conn_status 429``limit_req_status 429` 必须在 HTTP 与 HTTPS server 中同时生效;若线上压测看到 `limiting connections by zone "genarrative_api_conn"` 却返回 503优先检查 `nginx -T` 里 HTTPS server 是否缺少这些状态码,以及 `/api/runtime/puzzle/gallery` 是否误落到通用 `location ~ ^/api``limit_conn=64`。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time``upstream_connect_time``upstream_header_time``upstream_response_time``upstream_status``request_id`
- 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`
@@ -272,7 +271,7 @@ npm run container:k6
npm run container:down
```
容器方案默认暴露 `http://127.0.0.1:18080``api-server` 在容器内监听 `0.0.0.0:8082`Nginx 通过 `api-server:8082` upstream 反代 `/api/``/admin/api/`。SpacetimeDB 也纳入 compose容器内由 `spacetimedb:3101` 提供服务,宿主机通过 `http://127.0.0.1:13101` 进行模块发布Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0`。生产 provision 侧则通过 Windows Jenkins 下载件在目标 Linux 节点生成 `provision-tools/otelcol-contrib`,再安装本机 `otelcol-contrib.service`真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env`,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md`
容器方案默认暴露 `http://127.0.0.1:18080``api-server` 在容器内监听 `0.0.0.0:8082`Nginx 通过 `api-server:8082` upstream 反代 `/api/``/admin/api/`。SpacetimeDB 也纳入 compose容器内由 `spacetimedb:3101` 提供服务,宿主机通过 `http://127.0.0.1:13101` 进行模块发布Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0`。生产 provision 侧现在由 Linux build 节点直接准备 `provision-tools/otelcol-contrib`,再交给后续 Linux 发布阶段安装本机 `otelcol-contrib.service`真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env`,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md`
`npm run container:config` 默认只做 quiet 校验,避免把本地 env 中的 token 展开到终端;确需排查完整 compose 时再传 `-- --print`
OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs但本地日志与 Nginx 文件日志仍保留:

View File

@@ -10,7 +10,8 @@ pipeline {
}
environment {
GIT_REMOTE_URL = 'https://git.genarrative.world/GenarrativeAI/Genarrative.git'
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
GIT_REMOTE_FALLBACK_URL = 'https://git.genarrative.world/GenarrativeAI/Genarrative.git'
CARGO_HOME = '/home/dsk/.cache/genarrative-jenkins/api-server/cargo-home'
CARGO_TARGET_DIR = '/home/dsk/.cache/genarrative-jenkins/api-server/cargo-target/prod-release'
CARGO_INCREMENTAL = '0'
@@ -33,6 +34,8 @@ pipeline {
stages {
stage('Checkout') {
steps {
script {
def checkoutFromRemote = { String remoteUrl ->
checkout([
$class: 'GitSCM',
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
@@ -41,15 +44,26 @@ pipeline {
[$class: 'CleanBeforeCheckout'],
[$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true],
],
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}", refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]],
userRemoteConfigs: [[url: remoteUrl, refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]],
])
}
try {
checkoutFromRemote(env.GIT_REMOTE_URL)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL
} catch (error) {
echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${env.GIT_REMOTE_FALLBACK_URL}"
checkoutFromRemote(env.GIT_REMOTE_FALLBACK_URL)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_FALLBACK_URL
}
}
sh '''
bash -lc '
set -euo pipefail
chmod +x scripts/jenkins-checkout-source.sh
SOURCE_BRANCH="${SOURCE_BRANCH:-master}" \
COMMIT_HASH="${COMMIT_HASH:-}" \
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
GIT_REMOTE_URL="${EFFECTIVE_GIT_REMOTE_URL:-${GIT_REMOTE_URL}}" \
GIT_REMOTE_FALLBACK_URL="${GIT_REMOTE_FALLBACK_URL:-}" \
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
scripts/jenkins-checkout-source.sh
'

View File

@@ -1,5 +1,7 @@
pipeline {
agent none
agent {
label 'linux && genarrative-build'
}
options {
disableConcurrentBuilds()

View File

@@ -1,27 +1,3 @@
def runWindowsPowerShell(String scriptName, String scriptBody) {
def scriptPath = ".jenkins-${scriptName}.ps1"
writeFile file: scriptPath, text: scriptBody, encoding: 'UTF-8'
bat label: "PowerShell ${scriptName}", script: """
@echo off
setlocal
set "GENARRATIVE_POWERSHELL=%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
if not exist "%GENARRATIVE_POWERSHELL%" (
echo [jenkins-powershell] powershell.exe not found: %GENARRATIVE_POWERSHELL%
exit /b 1
)
echo [jenkins-powershell] user:
whoami
echo [jenkins-powershell] workspace: %CD%
echo [jenkins-powershell] exe: %GENARRATIVE_POWERSHELL%
if not exist "%CD%\\${scriptPath}" (
echo [jenkins-powershell] script not found: %CD%\\${scriptPath}
exit /b 1
)
"%GENARRATIVE_POWERSHELL%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "try { \$path = Join-Path (Get-Location).ProviderPath '${scriptPath}'; Write-Host '[jenkins-powershell] script:' \$path; \$text = [System.IO.File]::ReadAllText(\$path, [System.Text.Encoding]::UTF8); Write-Host '[jenkins-powershell] loaded bytes:' ([System.IO.File]::ReadAllBytes(\$path).Length); \$scriptBlock = [ScriptBlock]::Create(\$text); & \$scriptBlock; if (\$LASTEXITCODE -is [int] -and \$LASTEXITCODE -ne 0) { exit \$LASTEXITCODE } } catch { Write-Host '[jenkins-powershell] failed:' \$_.Exception.Message; if (\$_.ScriptStackTrace) { Write-Host \$_.ScriptStackTrace }; exit 1 }"
exit /b %ERRORLEVEL%
"""
}
pipeline {
agent none
@@ -46,11 +22,11 @@ pipeline {
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit')
string(name: 'SERVER_NAME', defaultValue: 'genarrative.example.com', description: '证书主域名;也作为 Nginx server_name 的第一个域名')
string(name: 'SERVER_ALIASES', defaultValue: '', description: '可选,额外 Nginx server_name多个用空格或逗号分隔例如 www.genarrative.world')
string(name: 'PROVISION_DOWNLOADS_DIR', defaultValue: 'provision-tool-downloads', description: 'Windows 下载阶段暂存 SpacetimeDB/otelcol 安装包的工作区相对目录')
string(name: 'PROVISION_DOWNLOADS_DIR', defaultValue: 'provision-tool-downloads', description: 'Linux 预下载阶段暂存 SpacetimeDB/otelcol 安装包的工作区相对目录')
string(name: 'PROVISION_TOOLS_DIR', defaultValue: 'provision-tools', description: '目标机工作区内由已下载安装包生成的工具包目录')
string(name: 'PROVISION_DOWNLOAD_PROXY', defaultValue: '', description: '可选,Windows 下载 SpacetimeDB 和 otelcol-contrib 时使用的代理地址,例如 http://127.0.0.1:7890留空不设置代理')
string(name: 'SPACETIME_DOWNLOAD_ROOT', defaultValue: 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download', description: 'Windows 下载 SpacetimeDB Linux release tarball 的根地址;目标机不访问该地址')
string(name: 'SPACETIME_TARGET_HOST', defaultValue: 'x86_64-unknown-linux-gnu', description: '目标机 SpacetimeDB 预编译包 host tripledevelopment/release Linux amd64 使用默认值')
string(name: 'PROVISION_DOWNLOAD_PROXY', defaultValue: '', description: '可选,Linux 预下载阶段下载 SpacetimeDB 和 otelcol-contrib 时使用的代理地址,例如 http://127.0.0.1:7890留空不设置代理')
string(name: 'SPACETIME_DOWNLOAD_ROOT', defaultValue: 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download', description: 'Linux 预下载阶段使用的 SpacetimeDB Linux release tarball 地址')
string(name: 'SPACETIME_TARGET_HOST', defaultValue: 'x86_64-unknown-linux-gnu', description: 'SpacetimeDB 预编译包 host tripledevelopment/release Linux amd64 使用默认值')
string(name: 'SPACETIME_ROOT', defaultValue: '/stdb', description: 'SpacetimeDB root-dir')
string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: 'release 根目录')
string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接')
@@ -66,7 +42,7 @@ pipeline {
stages {
stage('Prepare') {
agent {
label 'windows'
label 'linux && genarrative-build'
}
steps {
script {
@@ -134,258 +110,64 @@ pipeline {
}
}
stage('Download Provision Tool Archives') {
stage('Prepare Provision Tools') {
agent {
label 'windows'
label 'linux && genarrative-build'
}
steps {
script {
runWindowsPowerShell('server-provision-tool-downloads', '''
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$downloadsDir = if ($env:PROVISION_DOWNLOADS_DIR) { $env:PROVISION_DOWNLOADS_DIR } else { 'provision-tool-downloads' }
$otelVersion = if ($env:OTELCOL_VERSION) { $env:OTELCOL_VERSION } else { '0.151.0' }
$prepareOtel = if ($env:ENABLE_OTELCOL) { $env:ENABLE_OTELCOL } else { 'true' }
$otelRoot = if ($env:OTELCOL_DOWNLOAD_ROOT) { $env:OTELCOL_DOWNLOAD_ROOT.TrimEnd('/') } else { 'https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download' }
$spacetimeDownloadRoot = if ($env:SPACETIME_DOWNLOAD_ROOT) { $env:SPACETIME_DOWNLOAD_ROOT.TrimEnd('/') } else { 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download' }
$spacetimeTargetHost = if ($env:SPACETIME_TARGET_HOST) { $env:SPACETIME_TARGET_HOST } else { 'x86_64-unknown-linux-gnu' }
$downloadProxy = if ($env:PROVISION_DOWNLOAD_PROXY) { $env:PROVISION_DOWNLOAD_PROXY } else { '' }
$workspace = (Get-Location).ProviderPath
if ([System.IO.Path]::IsPathRooted($downloadsDir)) {
throw "[prepare-provision-downloads] PROVISION_DOWNLOADS_DIR 只能是工作区内相对路径: ${downloadsDir}"
}
$downloadsDir = Join-Path $workspace $downloadsDir
Write-Host "[prepare-provision-downloads] Windows workspace: ${workspace}"
Write-Host "[prepare-provision-downloads] download dir: ${downloadsDir}"
if (Test-Path -LiteralPath $downloadsDir) {
Write-Host "[prepare-provision-downloads] 复用已有下载目录: ${downloadsDir}"
} else {
New-Item -ItemType Directory -Force -Path $downloadsDir | Out-Null
Write-Host "[prepare-provision-downloads] 已创建下载目录: ${downloadsDir}"
}
if ($downloadProxy) {
$env:HTTP_PROXY = $downloadProxy
$env:HTTPS_PROXY = $downloadProxy
$env:ALL_PROXY = $downloadProxy
Write-Host "[prepare-provision-downloads] 已配置 Windows 下载代理: $($downloadProxy -replace '://.*', '://***')"
}
function Get-GithubReleaseAssetDigest {
param(
[Parameter(Mandatory=$true)][string]$Repository,
[Parameter(Mandatory=$true)][string]$ReleaseSelector,
[Parameter(Mandatory=$true)][string]$AssetName
)
$request = @{
Uri = "https://api.github.com/repos/${Repository}/${ReleaseSelector}"
Headers = @{
Accept = 'application/vnd.github+json'
'User-Agent' = 'Genarrative-Server-Provision'
}
ErrorAction = 'Stop'
}
if ($downloadProxy) {
$request.Proxy = $downloadProxy
}
Write-Host "[prepare-provision-downloads] 查询 GitHub digest: repo=${Repository} release=${ReleaseSelector} asset=${AssetName}"
$release = Invoke-RestMethod @request
$asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1
if (-not $asset) {
throw "[prepare-provision-downloads] GitHub release 未找到资产: ${Repository}/${AssetName}"
}
if (-not $asset.digest) {
throw "[prepare-provision-downloads] GitHub release 未返回 digest: ${Repository}/${AssetName}"
}
Write-Host "[prepare-provision-downloads] GitHub digest ${AssetName}: $($asset.digest)"
return $asset.digest
}
function Get-FileDigest {
param(
[Parameter(Mandatory=$true)][string]$Path,
[Parameter(Mandatory=$true)][string]$Algorithm
)
$fileHash = Get-FileHash -Algorithm $Algorithm -LiteralPath $Path
return $fileHash.Hash.ToLowerInvariant()
}
function Test-DownloadDigestMatch {
param(
[Parameter(Mandatory=$true)][string]$Path,
[Parameter(Mandatory=$true)][string]$ExpectedDigest
)
$parts = $ExpectedDigest.Split(':', 2)
if ($parts.Length -ne 2) {
throw "[prepare-provision-downloads] 无法解析 GitHub digest: ${ExpectedDigest}"
}
$algorithm = $parts[0].Trim().ToLowerInvariant()
$expectedHash = $parts[1].Trim().ToLowerInvariant()
if ($algorithm -ne 'sha256') {
throw "[prepare-provision-downloads] 暂不支持的 GitHub digest 算法: ${algorithm}"
}
$localHash = Get-FileDigest -Path $Path -Algorithm 'SHA256'
return $localHash -eq $expectedHash
}
function Invoke-ProvisionDownload {
param(
[Parameter(Mandatory=$true)][string]$Label,
[Parameter(Mandatory=$true)][string]$Url,
[Parameter(Mandatory=$true)][string]$Output,
[string]$ExpectedDigest = ''
)
if ($ExpectedDigest) {
if (Test-Path -LiteralPath $Output) {
if (Test-DownloadDigestMatch -Path $Output -ExpectedDigest $ExpectedDigest) {
$existingItem = Get-Item -LiteralPath $Output
Write-Host "[prepare-provision-downloads] 已存在且校验一致,跳过下载: ${Label} bytes=$($existingItem.Length) path=${Output}"
return
}
Write-Host "[prepare-provision-downloads] 已存在但校验不一致,重新下载: ${Label} path=${Output}"
}
}
Write-Host "[prepare-provision-downloads] 下载 ${Label}: ${Url}"
$tempOutput = "${Output}.download"
if (Test-Path -LiteralPath $tempOutput) {
$tempItem = Get-Item -LiteralPath $tempOutput
if ($ExpectedDigest -and $tempItem.Length -gt 0 -and (Test-DownloadDigestMatch -Path $tempOutput -ExpectedDigest $ExpectedDigest)) {
Move-Item -LiteralPath $tempOutput -Destination $Output -Force
$finalItem = Get-Item -LiteralPath $Output
Write-Host "[prepare-provision-downloads] 已复用校验通过的临时下载: ${Label} bytes=$($finalItem.Length) path=${Output}"
return
}
if ($tempItem.Length -gt 0) {
Write-Host "[prepare-provision-downloads] 发现未完成临时文件,后续尝试断点续传: ${Label} bytes=$($tempItem.Length) path=${tempOutput}"
} else {
Remove-Item -LiteralPath $tempOutput -Force
}
}
$curl = Get-Command curl.exe -ErrorAction SilentlyContinue
$maxAttempts = 8
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
$resumeBytes = 0
if (Test-Path -LiteralPath $tempOutput) {
$resumeBytes = (Get-Item -LiteralPath $tempOutput).Length
def checkoutFromRemote = { String remoteUrl ->
checkout([
$class: 'GitSCM',
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [
[$class: 'CleanBeforeCheckout'],
[$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true],
],
userRemoteConfigs: [[url: remoteUrl, refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]],
])
}
try {
if ($curl) {
$arguments = @('-fL', '--retry', '3', '--retry-delay', '3', '--retry-all-errors', '--connect-timeout', '30', '--speed-time', '60', '--speed-limit', '1024')
if ($resumeBytes -gt 0) {
$arguments += @('-C', '-')
Write-Host "[prepare-provision-downloads] curl 断点续传 ${Label}: attempt=${attempt}/${maxAttempts} resumeBytes=${resumeBytes}"
} else {
Write-Host "[prepare-provision-downloads] curl 下载 ${Label}: attempt=${attempt}/${maxAttempts}"
}
$arguments += @('-o', $tempOutput)
if ($downloadProxy) {
$arguments += @('--proxy', $downloadProxy)
}
$arguments += $Url
& $curl.Source @arguments
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
$currentBytes = if (Test-Path -LiteralPath $tempOutput) { (Get-Item -LiteralPath $tempOutput).Length } else { 0 }
Write-Host "[prepare-provision-downloads] curl 下载未完成: ${Label}, attempt=${attempt}/${maxAttempts}, exit=${exitCode}, tempBytes=${currentBytes}"
if ($attempt -lt $maxAttempts) {
Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt))
continue
}
throw "[prepare-provision-downloads] curl 下载失败: ${Label}, exit=${exitCode}, temp=${tempOutput}"
}
} else {
Write-Host "[prepare-provision-downloads] Invoke-WebRequest 下载 ${Label}: attempt=${attempt}/${maxAttempts}"
if ($resumeBytes -gt 0) {
Write-Host "[prepare-provision-downloads] Invoke-WebRequest 不支持断点续传,删除临时文件后重新下载: ${Label}, bytes=${resumeBytes}"
Remove-Item -LiteralPath $tempOutput -Force
}
$parameters = @{
Uri = $Url
OutFile = $tempOutput
UseBasicParsing = $true
}
if ($downloadProxy) {
$parameters.Proxy = $downloadProxy
}
Invoke-WebRequest @parameters
}
} catch {
$currentBytes = if (Test-Path -LiteralPath $tempOutput) { (Get-Item -LiteralPath $tempOutput).Length } else { 0 }
Write-Host "[prepare-provision-downloads] 下载尝试失败: ${Label}, attempt=${attempt}/${maxAttempts}, tempBytes=${currentBytes}, error=$($_.Exception.Message)"
if ($attempt -lt $maxAttempts) {
Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt))
continue
}
throw
}
if (-not (Test-Path -LiteralPath $tempOutput)) {
throw "[prepare-provision-downloads] 下载未生成临时文件: ${tempOutput}"
}
$item = Get-Item -LiteralPath $tempOutput
if ($item.Length -le 0) {
if ($attempt -lt $maxAttempts) {
Write-Host "[prepare-provision-downloads] 下载结果为空,将重试: ${Label}"
Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt))
continue
}
throw "[prepare-provision-downloads] 下载结果为空: ${tempOutput}"
}
if ($ExpectedDigest) {
if (-not (Test-DownloadDigestMatch -Path $tempOutput -ExpectedDigest $ExpectedDigest)) {
Write-Host "[prepare-provision-downloads] 下载结果校验未通过,将继续重试: ${Label}, attempt=${attempt}/${maxAttempts}, tempBytes=$($item.Length)"
if ($attempt -lt $maxAttempts) {
Remove-Item -LiteralPath $tempOutput -Force
Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt))
continue
}
throw "[prepare-provision-downloads] 下载结果校验失败: ${Label}, temp=${tempOutput}"
checkoutFromRemote(env.GIT_REMOTE_URL)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL
} catch (error) {
echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${env.GIT_REMOTE_FALLBACK_URL}"
checkoutFromRemote(env.GIT_REMOTE_FALLBACK_URL)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_FALLBACK_URL
}
}
Move-Item -LiteralPath $tempOutput -Destination $Output -Force
$finalItem = Get-Item -LiteralPath $Output
Write-Host "[prepare-provision-downloads] 已下载 ${Label}: bytes=$($finalItem.Length) path=${Output}"
return
sh '''
bash <<'BASH'
set -euo pipefail
chmod +x scripts/jenkins-checkout-source.sh
SOURCE_BRANCH="${SOURCE_BRANCH:-master}" \
COMMIT_HASH="${COMMIT_HASH:-}" \
GIT_REMOTE_URL="${EFFECTIVE_GIT_REMOTE_URL:-${GIT_REMOTE_URL}}" \
GIT_REMOTE_FALLBACK_URL="${GIT_REMOTE_FALLBACK_URL:-}" \
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
scripts/jenkins-checkout-source.sh
BASH
'''
sh '''
bash -lc '
set -euo pipefail
chmod +x scripts/prepare-server-provision-tools.sh
PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" \
PROVISION_DOWNLOADS_DIR="${PROVISION_DOWNLOADS_DIR:-provision-tool-downloads}" \
OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" \
PREPARE_OTELCOL="${ENABLE_OTELCOL:-true}" \
PROVISION_DOWNLOAD_PROXY="${PROVISION_DOWNLOAD_PROXY:-}" \
SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" \
SPACETIME_TARGET_HOST="${SPACETIME_TARGET_HOST:-x86_64-unknown-linux-gnu}" \
scripts/prepare-server-provision-tools.sh
'
'''
script {
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
echo "Provision 工具包源码 commit=${env.SOURCE_COMMIT}"
}
throw "[prepare-provision-downloads] 下载重试耗尽: ${Label}"
}
$spacetimeArchiveName = "spacetime-${spacetimeTargetHost}.tar.gz"
$spacetimeArchiveUrl = "${spacetimeDownloadRoot}/${spacetimeArchiveName}"
$spacetimeArchiveDigest = Get-GithubReleaseAssetDigest -Repository 'clockworklabs/SpacetimeDB' -ReleaseSelector 'releases/latest' -AssetName $spacetimeArchiveName
Invoke-ProvisionDownload -Label "SpacetimeDB release tarball ${spacetimeTargetHost}" -Url $spacetimeArchiveUrl -Output (Join-Path $downloadsDir $spacetimeArchiveName) -ExpectedDigest $spacetimeArchiveDigest
if ($prepareOtel -eq 'true') {
$otelArchiveName = "otelcol-contrib_${otelVersion}_linux_amd64.tar.gz"
$otelUrl = "${otelRoot}/v${otelVersion}/${otelArchiveName}"
$otelDigest = Get-GithubReleaseAssetDigest -Repository 'open-telemetry/opentelemetry-collector-releases' -ReleaseSelector "releases/tags/v${otelVersion}" -AssetName $otelArchiveName
Invoke-ProvisionDownload -Label "otelcol-contrib ${otelVersion} linux amd64" -Url $otelUrl -Output (Join-Path $downloadsDir $otelArchiveName) -ExpectedDigest $otelDigest
} else {
Write-Host "[prepare-provision-downloads] ENABLE_OTELCOL=${prepareOtel},跳过 otelcol-contrib 下载。"
}
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
$manifest = @(
"spacetime release tarball ${spacetimeArchiveUrl}",
"spacetime target host ${spacetimeTargetHost}",
"otelcol-contrib ${otelVersion} prepare=${prepareOtel}"
)
[System.IO.File]::WriteAllLines((Join-Path $downloadsDir 'DOWNLOADS-MANIFEST.txt'), $manifest, $utf8NoBom)
Get-ChildItem -LiteralPath $downloadsDir | Sort-Object Name | ForEach-Object {
Write-Host "[prepare-provision-downloads] artifact $($_.Length) $($_.Name)"
}
''')
}
stash name: 'server-provision-tool-downloads', includes: "${params.PROVISION_DOWNLOADS_DIR}/**", useDefaultExcludes: false
stash name: 'server-provision-tools', includes: "${params.PROVISION_TOOLS_DIR}/**", useDefaultExcludes: false
}
}
@@ -440,20 +222,10 @@ BASH
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
}
steps {
unstash 'server-provision-tool-downloads'
unstash 'server-provision-tools'
sh '''
bash <<'BASH'
set -euo pipefail
chmod +x scripts/prepare-server-provision-tools.sh
PROVISION_DOWNLOADS_DIR="${PROVISION_DOWNLOADS_DIR:-provision-tool-downloads}" \
PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" \
OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" \
PREPARE_OTELCOL="${ENABLE_OTELCOL:-true}" \
PROVISION_REQUIRE_LOCAL_DOWNLOADS="true" \
SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" \
SPACETIME_TARGET_HOST="${SPACETIME_TARGET_HOST:-x86_64-unknown-linux-gnu}" \
scripts/prepare-server-provision-tools.sh
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
chmod +x "${PROVISION_TOOLS_DIR:-provision-tools}/otelcol-contrib"
fi

View File

@@ -1,27 +1,6 @@
def runWindowsPowerShell(String scriptName, String scriptBody) {
def scriptPath = ".jenkins-${scriptName}.ps1"
writeFile file: scriptPath, text: scriptBody, encoding: 'UTF-8'
bat label: "PowerShell ${scriptName}", script: """
@echo off
setlocal
set "GENARRATIVE_POWERSHELL=%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
if not exist "%GENARRATIVE_POWERSHELL%" (
echo [jenkins-powershell] powershell.exe not found: %GENARRATIVE_POWERSHELL%
exit /b 1
)
echo [jenkins-powershell] user:
whoami
echo [jenkins-powershell] exe: %GENARRATIVE_POWERSHELL%
"%GENARRATIVE_POWERSHELL%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "\$path = '%CD%\\${scriptPath}'; \$text = [System.IO.File]::ReadAllText(\$path, [System.Text.Encoding]::UTF8); \$utf8Bom = New-Object System.Text.UTF8Encoding(\$true); [System.IO.File]::WriteAllText(\$path, \$text, \$utf8Bom)"
if errorlevel 1 exit /b %ERRORLEVEL%
"%GENARRATIVE_POWERSHELL%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%CD%\\${scriptPath}"
exit /b %ERRORLEVEL%
"""
}
pipeline {
agent {
label 'windows'
label 'linux && genarrative-build'
}
options {
@@ -31,12 +10,10 @@ pipeline {
}
environment {
GIT_REMOTE_URL = 'https://git.genarrative.world/GenarrativeAI/Genarrative.git'
CARGO_HOME = '${env.WORKSPACE_TMP}/cargo-home'
CARGO_TARGET_DIR = '${env.WORKSPACE_TMP}/cargo-target/prod-release'
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
GIT_REMOTE_FALLBACK_URL = 'https://git.genarrative.world/GenarrativeAI/Genarrative.git'
CARGO_INCREMENTAL = '0'
RUSTC_WRAPPER = 'sccache'
SCCACHE_DIR = '${env.USERPROFILE}\\.cache\\sccache-stdb-module'
SCCACHE_CACHE_SIZE = '30G'
}
@@ -56,6 +33,8 @@ pipeline {
stages {
stage('Checkout') {
steps {
script {
def checkoutFromRemote = { String remoteUrl ->
checkout([
$class: 'GitSCM',
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
@@ -64,98 +43,32 @@ pipeline {
[$class: 'CleanBeforeCheckout'],
[$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true],
],
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}", refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]],
userRemoteConfigs: [[url: remoteUrl, refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]],
])
}
try {
checkoutFromRemote(env.GIT_REMOTE_URL)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL
} catch (error) {
echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${env.GIT_REMOTE_FALLBACK_URL}"
checkoutFromRemote(env.GIT_REMOTE_FALLBACK_URL)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_FALLBACK_URL
}
}
sh '''
bash -lc '
set -euo pipefail
chmod +x scripts/jenkins-checkout-source.sh
SOURCE_BRANCH="${SOURCE_BRANCH:-master}" \
COMMIT_HASH="${COMMIT_HASH:-}" \
GIT_REMOTE_URL="${EFFECTIVE_GIT_REMOTE_URL:-${GIT_REMOTE_URL}}" \
GIT_REMOTE_FALLBACK_URL="${GIT_REMOTE_FALLBACK_URL:-}" \
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
scripts/jenkins-checkout-source.sh
'
'''
script {
runWindowsPowerShell('stdb-checkout', '''
$ErrorActionPreference = 'Stop'
$sourceBranch = if ($env:SOURCE_BRANCH) { $env:SOURCE_BRANCH } else { 'master' }
$commitHash = if ($env:COMMIT_HASH) { $env:COMMIT_HASH } else { '' }
$gitRemoteUrl = if ($env:GIT_REMOTE_URL) { $env:GIT_REMOTE_URL } else { 'https://git.genarrative.world/GenarrativeAI/Genarrative.git' }
function Invoke-GitCommand {
param(
[string]$Label,
[string[]]$Arguments
)
Write-Host "[stdb-checkout] $Label"
& git @Arguments
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "[stdb-checkout] $Label failed with exit code $exitCode"
}
}
Write-Host "[stdb-checkout] sourceBranch: $sourceBranch"
Write-Host "[stdb-checkout] remote: $gitRemoteUrl"
$currentCommit = (git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or -not $currentCommit) {
throw '[stdb-checkout] cannot resolve current HEAD'
}
Write-Host "[stdb-checkout] current HEAD: $currentCommit"
if ($commitHash) {
Write-Host "[stdb-checkout] requested commit: $commitHash"
$resolvedCommit = (git rev-parse --verify "${commitHash}^{commit}" 2>$null).Trim()
if ($LASTEXITCODE -eq 0 -and $resolvedCommit -eq $currentCommit) {
Write-Host '[stdb-checkout] requested commit already matches Jenkins GitSCM checkout'
} else {
Invoke-GitCommand -Label 'fetch source branch history' -Arguments @(
'fetch',
'--no-tags',
'--prune',
$gitRemoteUrl,
"+refs/heads/${sourceBranch}:refs/remotes/origin/${sourceBranch}"
)
$isShallowRepository = (git rev-parse --is-shallow-repository 2>$null).Trim()
if ($LASTEXITCODE -ne 0) {
throw '[stdb-checkout] cannot determine whether repository is shallow'
}
if ($isShallowRepository -eq 'true') {
Invoke-GitCommand -Label 'deepen source branch history' -Arguments @(
'fetch',
'--unshallow',
'--no-tags',
$gitRemoteUrl,
"+refs/heads/${sourceBranch}:refs/remotes/origin/${sourceBranch}"
)
}
Invoke-GitCommand -Label 'validate source branch ref' -Arguments @(
'cat-file',
'-e',
"refs/remotes/origin/${sourceBranch}^{commit}"
)
Invoke-GitCommand -Label 'validate requested commit' -Arguments @(
'cat-file',
'-e',
"${commitHash}^{commit}"
)
$resolvedCommit = (git rev-parse --verify "${commitHash}^{commit}").Trim()
if ($LASTEXITCODE -ne 0 -or -not $resolvedCommit) {
throw "[stdb-checkout] cannot resolve requested commit: $commitHash"
}
Invoke-GitCommand -Label 'validate requested commit belongs to branch' -Arguments @(
'merge-base',
'--is-ancestor',
$resolvedCommit,
"refs/remotes/origin/${sourceBranch}"
)
Invoke-GitCommand -Label "checkout commit $resolvedCommit" -Arguments @(
'checkout',
'--force',
$resolvedCommit
)
}
} else {
Write-Host "[stdb-checkout] COMMIT_HASH empty, reusing Jenkins GitSCM checkout result"
}
$resolvedCommit = (git rev-parse HEAD).Trim()
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path (Get-Location) '.jenkins-source-commit'), "$resolvedCommit`n", $utf8NoBom)
''')
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').replace('\uFEFF', '').trim()
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
}
}
@@ -165,45 +78,27 @@ pipeline {
steps {
script {
def buildStep = {
runWindowsPowerShell('stdb-build', '''
$ErrorActionPreference = 'Stop'
$workspaceTmp = if ($env:WORKSPACE_TMP) { $env:WORKSPACE_TMP } else { "$env:WORKSPACE@tmp" }
$env:CARGO_HOME = "$workspaceTmp/cargo-home"
$env:CARGO_TARGET_DIR = "$workspaceTmp/cargo-target/prod-release"
$env:SCCACHE_DIR = "$env:USERPROFILE/.cache/sccache-stdb-module"
$env:PATH = "$env:CARGO_HOME/bin;$env:PATH"
$gitBash = @(
$env:GENARRATIVE_BASH,
'C:/Program Files/Git/bin/bash.exe',
'C:/Program Files/Git/usr/bin/bash.exe',
'C:/msys64/usr/bin/bash.exe',
'bash'
) | Where-Object { $_ -and (($_ -eq 'bash') -or (Test-Path $_)) } | Select-Object -First 1
if (-not $gitBash) {
throw '[stdb-build] Windows 构建节点缺少 Git Bash无法执行仓库现有生产构建脚本。请先安装 Git for Windows并确保 bash 在 PATH 中。'
}
$env:GENARRATIVE_BASH = $gitBash
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
throw '[stdb-build] 缺少 cargo。请先在 Windows 构建节点安装 Rust 工具链,并确保 cargo 在 PATH 中。'
}
# sccache 只是可选缓存PATH 中存在但不可执行时必须回退到 rustc。
$sccacheCommand = Get-Command sccache -ErrorAction SilentlyContinue
$sccacheUsable = $false
if ($sccacheCommand) {
try {
& $sccacheCommand.Source --version | Out-Host
$sccacheUsable = $true
} catch {
Write-Host "[stdb-build] sccache 无法执行:$($_.Exception.Message)"
}
}
if (-not $sccacheUsable) {
Write-Host '[stdb-build] 未找到可用 sccache改用 rustc 直接构建。'
Remove-Item Env:RUSTC_WRAPPER -ErrorAction SilentlyContinue
}
npm run build:production-release -- --component spacetime-module --name "$env:EFFECTIVE_BUILD_VERSION"
sh '''
bash -lc '
set -euo pipefail
workspace_tmp="${WORKSPACE_TMP:-${WORKSPACE}@tmp}"
export CARGO_HOME="${workspace_tmp}/cargo-home"
export CARGO_TARGET_DIR="${workspace_tmp}/cargo-target/prod-release"
export CARGO_INCREMENTAL=0
export RUSTC_WRAPPER=sccache
export SCCACHE_DIR="${workspace_tmp}/sccache-stdb-module"
export SCCACHE_CACHE_SIZE=30G
mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" "${SCCACHE_DIR}"
chmod +x scripts/jenkins-prepare-cargo-env.sh
source scripts/jenkins-prepare-cargo-env.sh
if ! command -v sccache >/dev/null 2>&1; then
echo "[stdb-build] 未找到可用 sccache改用 rustc 直接构建。"
unset RUSTC_WRAPPER
fi
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
npm run build:production-release -- --component spacetime-module --name "${EFFECTIVE_BUILD_VERSION}"
'
'''
)
}
if (params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
withCredentials([