diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index bcd2bc888..7f0647d1a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,7 +1,17 @@ # 决策记录 +## 2026-09-22 release 每日调度纳入正式 Full Build 并收集结果 + +- 背景:原 `Genarrative-Scheduled-Release-Trigger` 只处理 AGC Windows/macOS,且对下游使用 `wait: false` 后立即推进 revision;正式服务端 release 仍只能人工触发,客户端构建失败也不会回传调度器。 +- 决策:release 调度同时计算 `AGC` 与 `FullBuild` 两条 scope。服务端相关路径变化时以 `DEPLOY_TARGET=release`、`CONFIRM_RELEASE_DEPLOY_AGENT=true` 触发 `Genarrative-Full-Build-And-Deploy`;客户端相关路径变化时保持统一发号并触发 Windows/macOS release。Full Build、Windows、macOS 三路均等待结果并汇总,只有成功的 lane 才推进对应 `.jenkins-last-release-full-revision` / `.jenkins-last-release-agc-revision`,失败 lane 在下一轮单独补发,避免重复部署已成功的服务端版本。`DATABASE_BACKUP_MODE` 默认 `async`,并显式提供 `STDB_API_ROLLOUT_MODE` / approvers 以保留正式维护门禁。 +- 原因:正式 release 需要与 dev 小时调度隔离,同时不能继续依赖人工触发服务端;lane-level 成功状态可区分“已成功但另一 lane 失败”的部分发布,避免下一次调度重复发布 Full Build。 +- 影响范围:`jenkins/Jenkinsfile.scheduled-release-trigger`、`jenkins/scheduled-release-trigger-job-config.xml`、`scripts/check-production-ops-guardrails.mjs`、开发运维文档与共享开发工作流。 +- 验证方式:生产运维门禁检查 Full Build release 参数、双 scope、`wait: true` 结果聚合、三路分支和成功后才写 lane revision;并用 Jenkins 具体构建验证 Full Build/AGC 的 build number 与结果能回传到调度 Job。 + ## 2026-09-22 AGC release 增加每日调度,dev 调度保持双平台 +> 已被上一条决策取代:release 调度现已纳入正式服务端 Full Build。 + - 背景:原有 `Genarrative-Scheduled-Revision-Trigger` 每小时跟随 revision 发布 dev 客户端,但没有对应的 release 渠道自动入口;Mac 节点此前已纳入小时 dev 调度,需要避免新增 release 调度时再退回 Windows-only。 - 决策:新增 `Genarrative-Scheduled-Release-Trigger`,每天 04:00 检查 `SOURCE_BRANCH`,使用独立的 `.jenkins-last-release-revision` 与客户端相关路径白名单;上一轮 release 调度后有客户端变更时,先经 `Genarrative-Agc-Global-Version-Issue` 发统一总号,再以同一固定 `COMMIT_HASH` 触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build` 的 `AGC_UPDATE_CHANNEL=release` 分区,Mac 继续带 `SKIP_IF_SUPERSEDED=true`。小时 dev 调度保持同时触发 Windows 与 macOS dev。 - 原因:release 与 dev 是不同渠道和发布节奏,不能靠同一个小时 Job 隐式切换;独立 Job 能分别去重、记录状态和审计发号。release 调度不触发 Full Build,避免把桌面客户端发布与线上全栈部署绑定。 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 8977e7861..c445fd7f5 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -92,7 +92,7 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m ## Jenkins 定时版本调度 -定时与版本比较收口到两条调度器:`Genarrative-Scheduled-Revision-Trigger` 每小时处理 dev 渠道,变化时触发 Full Build、AGC Windows/macOS dev,并让两个客户端平台共用同一个总版本号;`Genarrative-Scheduled-Release-Trigger` 每天 04:00 处理 AGC release 渠道,只在上一轮 release 调度后出现客户端相关路径变化时,经 `Genarrative-Agc-Global-Version-Issue` 发同一个总号并触发 AGC Windows/macOS release。各下游 Job 都不得自带 `triggers` / `cron`,也不得在管线内再做一套版本去重;`npm run check:production-ops` 会拦住回退。macOS 节点是日常办公机,两条调度触发它时都置 `SKIP_IF_SUPERSEDED=true`:节点离线期间排队的旧构建在恢复后会自行让位,不发布过期版本。调度状态与生效步骤见 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +定时与版本比较收口到两条调度器:`Genarrative-Scheduled-Revision-Trigger` 每小时处理 dev 渠道,变化时触发 Full Build、AGC Windows/macOS dev,并让两个客户端平台共用同一个总版本号;`Genarrative-Scheduled-Release-Trigger` 每天 04:00 按服务端与客户端两条独立 scope 处理 release,服务端变化时用 `DEPLOY_TARGET=release` 触发正式 Full Build,客户端变化时经 `Genarrative-Agc-Global-Version-Issue` 发同一个总号并触发 AGC Windows/macOS release。两条调度都等待并汇总下游,按成功 lane 推进 revision;失败 lane 下一轮单独补发。各下游 Job 都不得自带 `triggers` / `cron`,也不得在管线内再做一套版本去重;`npm run check:production-ops` 会拦住回退。macOS 节点是日常办公机,两条调度触发它时都置 `SKIP_IF_SUPERSEDED=true`:节点离线期间排队的旧构建在恢复后会自行让位,不发布过期版本。调度状态与生效步骤见 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 ## Gitea CI 依赖闭合 diff --git a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md index 7702b9f39..d048ec805 100644 --- a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md +++ b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md @@ -137,7 +137,7 @@ - 发布入口只解析一次目标,优先级为 CLI `--target value` / `--target=value` / `-t value`、`AGC_BUILD_TARGET`、Windows 默认值;重复/空目标与不支持目标失败关闭。版本高水位、构建 feature/渠道端点、bundle 路径、产物后缀、清单平台键及摘要必须消费同一个发布上下文,不能分别回读默认目标。 - 渠道由 `AGC_UPDATE_CHANNEL` 显式指定,默认 dev;Windows 与 macOS 目标均支持 dev、release 和自定义渠道,目标校验独立进行。 - 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier` 与 updater 端点:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。 -- 定时调度只在本轮到达的提交包含 AGC 相关路径(客户端、共享包、`server-rs/crates`、AGC 插件、桌面壳图标、根依赖清单)时才触发渠道发布;纯文档或流水线自身的提交只跑 Full Build,不推高客户端版本号。判定失败或勾选强制触发时按"需要发布"处理。 +- 定时调度分别判断服务端与客户端 scope:dev 小时调度在提交含 AGC 相关路径时发布对应渠道,纯文档或流水线自身的提交仍只跑 Full Build;release 每日调度在服务端相关路径变化时发布正式 Full Build,在 AGC 相关路径变化时发布 release 客户端,并在同一调度内等待、汇总各 lane 结果,失败 lane 下一轮补发。判定失败或勾选强制触发时按"需要发布"处理。 - 更新摘要自动生成:发布脚本用渠道清单里的 `commit` 字段(上一次发布的提交)到本次提交之间、且只覆盖客户端相关路径的提交列表生成 `notes`(每条 `- 提交标题(短 SHA)`,最多 12 条、主题 80 字、整体 900 字,超出折叠或截断),同时写入旧协议清单的 `releaseNotes` 和归档文件 `release-notes.txt`。`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准;无法判定起点(缺少上次 `commit` 或本地没有该提交)时不写摘要。清单缺少 `commit` 时回退用上一次成功构建的 `COMMIT_HASH`(CI 通过 `AGC_UPDATE_PREVIOUS_COMMIT` 传入)作为锚点,因此首次启用摘要或更换渠道后也能立即产出摘要。锚点仍不可得(清单读取失败或没有 CI 锚点)时降级为「最近客户端改动」列表并注明可能与上一版重复 —— 摘要属于附注,任何情况下都不允许因为它让发布失败。 - 清单里的 `commit` 是非标准字段:更新插件忽略未知字段,发布脚本用它定位下一次摘要的起点。 - 上传:安装包与 `.sig` 上传到 `agc/-win|mac//`,清单以 `--force` 覆盖上传到对应分区的 `latest.json`,保证 latest 指针与清单内 URL 指向已存在的对象。 diff --git a/docs/technical/【技术方案】AGC总版本号与发号-2026-09-20.md b/docs/technical/【技术方案】AGC总版本号与发号-2026-09-20.md index fdef27adb..0d39ddb27 100644 --- a/docs/technical/【技术方案】AGC总版本号与发号-2026-09-20.md +++ b/docs/technical/【技术方案】AGC总版本号与发号-2026-09-20.md @@ -39,7 +39,7 @@ AGC 客户端此前按「渠道各自比高水位自增」发号:`dev-win` 与 - 发号模块:`apps/ai-game-creator-shell/scripts/agc-global-version.mjs`(读总号 / 播种 / 发号 / 写后回读 / 渠道高水位断言 / dry-run 预览)。 - 发号入口:`apps/ai-game-creator-shell/scripts/issue-global-version.mjs`(CI 与本地共用,输出固定为 `AGC_GLOBAL_VERSION=`)。 - 构建侧:`build-release.mjs` 的 `prepareReleaseVersion()` 优先采用传入总号,未传入时现场发号;`resolveRemoteHighWaterVersion()` 降级为断言来源,只用于「请求号低于本渠道清单版本即失败关闭」。 -- CI:`jenkins/Jenkinsfile.agc-global-version-issue` + `jenkins/agc-global-version-issue-job-config.xml`;dev 调度管线 `jenkins/Jenkinsfile.scheduled-revision-trigger`、每日 release 调度管线 `jenkins/Jenkinsfile.scheduled-release-trigger` 与手动管线先调发号 Job,再把号透传给 AGC Build。 +- CI:`jenkins/Jenkinsfile.agc-global-version-issue` + `jenkins/agc-global-version-issue-job-config.xml`;dev 调度管线 `jenkins/Jenkinsfile.scheduled-revision-trigger`、每日 release 调度管线 `jenkins/Jenkinsfile.scheduled-release-trigger` 与手动管线先调发号 Job,再把号透传给 AGC Build;release 调度的服务端 Full Build 不使用 AGC 总号,只使用固定 `COMMIT_HASH` 与 Full Build 自身版本。 - 发号 Job 的 Copy Artifact 采用生产权限模式,`options` 里的 `copyArtifactPermission(...)` 必须显式列出**全部**消费者:小时 dev 调度 `Genarrative-Scheduled-Revision-Trigger`、每日 release 调度 `Genarrative-Scheduled-Release-Trigger` 与手动发布 `Genarrative-Manual-Build-And-Deploy`。用户触发的构建按「认证用户」判权(SYSTEM 定时构建短路放行),漏列时手动发布会报 `Unable to find project for artifact copy: Genarrative-Agc-Global-Version-Issue`,且失败点在发号之后。改完该 `options` 后必须先单独跑一次发号 Job,让 Declarative Pipeline 把 Job property 写回 Jenkins。 ## 不改的东西 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 2c58e1424..e57caadba 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -141,9 +141,9 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去 普通图片错误素材类型清理使用 `npm run spacetime:editor-image-asset-kind:clean -- --database --server-url `,只能由已授权 migration operator 执行。先进入维护模式并发布包含 `clean_editor_image_asset_kind_and_return` 的 SpacetimeDB module,并保持旧版本 API / controller / worker 停止;随后运行默认全量 dry-run,核对 `asset → project-resource → showcase → canvas` 各 scope 的扫描数、命中行数、字段数和 blocker 均符合预期,再追加 `--apply`。脚本对每批重新 dry-run、绑定包含画布迁移摘要、结构化 layer 与 generation-dialog 权威 JSON 的 SHA-256,最后自动从头复核零命中;任一画布数据异常都会只输出哈希化 ID、scope 与原因并停止,不能跳过。清理只处理精确业务旧值,不修改 `asset_object.asset_kind`、MIME 或媒体类型;project-resource scope 在清行前验证同工程 migration 并将其状态纳入批次 hash,layout version 0 的 legacy 画布可以没有 migration,但 structured 画布缺 migration 必须立即形成 blocker,资源行不得先被清空;清行后能保持原 status 不变量时立即刷新摘要,否则只允许留给后续精确 canvas 字段清理收口。canvas scope 在任何布局写入前再次按 active / backfilled / rolled_back 状态验证原 migration 凭证和双份 legacy / structured 不变量,将 `editor_canvas_generation_dialog.dialog_json` 与 layer rows 一并扫描并在同一事务 patch;只允许本批资源清零及精确字段删除造成的差异,写入后从全部结构化权威行重建 layout、再次复核新状态才受控重签摘要,同时保持业务 revision、migration status 与全部时间戳不变。新版本 API、SpacetimeDB storage 创建入口、legacy 画布元数据提取和项目资源落表边界都会将 trim 后精确等于 `image` 的 `assetKind` 归一为 `NULL`,防止旧页面、滞留请求或 legacy 保存重新制造废弃值。完成零残留复核,并分别确认 cleaned backfilled 可激活、active 可继续保存、rolled_back 可重复复检后恢复应用版本,最后退出维护。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-clean-editor-image-asset-kind.mjs` 与 `scripts/spacetime-migration-common.mjs`。 -`Genarrative-Scheduled-Revision-Trigger` 是 dev 渠道的小时调度入口,每小时检查一次(`H * * * *`,分钟由 Jenkins 按 Job 名散列,不等同于整点)。`Genarrative-Scheduled-Release-Trigger` 是 AGC release 渠道的每日调度入口,固定在每天 04:00(`0 4 * * *`)检查同一个源码分支。两条调度器都只用 `git ls-remote` 解析 `SOURCE_BRANCH`(默认 `master`)的远端 HEAD,不 checkout 工作区;解析出的完整 commit 与各自上一次触发过的 revision 相同则标记 `NOT_BUILT` 并结束,不触发任何下游。 +`Genarrative-Scheduled-Revision-Trigger` 是 dev 渠道的小时调度入口,每小时检查一次(`H * * * *`,分钟由 Jenkins 按 Job 名散列,不等同于整点)。`Genarrative-Scheduled-Release-Trigger` 是 release 渠道服务端与客户端的每日调度入口,固定在每天 04:00(`0 4 * * *`)检查同一个源码分支。两条调度器都只用 `git ls-remote` 解析 `SOURCE_BRANCH`(默认 `master`)的远端 HEAD,不 checkout 工作区;解析出的完整 commit 与对应 lane 上一次成功的 revision 相同则标记 `NOT_BUILT` 并结束,不触发任何下游。 -dev 调度器发现 revision 变化时,把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy`、`Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,各条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。这些下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`(两个平台分区发布同一个发号 Job 下发的总版本号),纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。每日 release 调度器不触发 Full Build;它使用独立的 `.jenkins-last-release-revision` 与客户端路径白名单,只在上一轮 release 调度后出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`plugins/agc-unity-editor/`、`plugins/agc-godot-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 变更时,先经发号 Job 获取统一总号,再以同一个固定 commit 触发 AGC Windows 与 macOS 的 `release` 分区构建;macOS 仍带 `SKIP_IF_SUPERSEDED=true`,无客户端变化时只推进 release revision 状态、不烧号。dev 调度器的下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/`、`.codex/`、`jenkins/`、`apps/ai-game-creator-shell/`、`apps/mobile-shell/`、`apps/desktop-shell/`、`apps/preview-deployer-web/`、`tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。AGC 客户端版本号不再由渠道各自递增:唯一发号源是 OSS 对象 `agc/global-version.json`,发号收口到 `Genarrative-Agc-Global-Version-Issue`(`disableConcurrentBuilds()` + 写后回读校验;集群未装 `lockable-resources` 插件)。dev / release 两条调度管线与手动发布管线都先调用该 Job 发号,再用归档产物 `agc-global-version.txt` 读取总号并作为 `AGC_RELEASE_VERSION` 透传给客户端构建;统一构建只发一次号供各渠道共用,单渠道热修只把号传给该渠道。显式传入的号低于本渠道当前清单版本时构建失败关闭;`AGC_RELEASE_DRY_RUN` 只预览下一位,不写回、不烧号。一次性播种用 `SEED_ONLY`:基线取「仓库版本 / 各渠道清单 / 旧迁移指针」的最大值,播种本身不烧号。仓库里的 5 个版本文件仍由构建改写,只作构建输入参考,不是事实源。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build` 结束后都会触发 `Genarrative-Notify-Email`;只有正式 OSS 发布成功时,通知正文才附加本次生成的 `latest.json` 首装包 URL,演练、失败或跳过构建时留空,避免把未写入或不完整的对象地址发给收件人。 +dev 调度器发现 revision 变化时,把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy`、`Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,各条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。这些下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`(两个平台分区发布同一个发号 Job 下发的总版本号),纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。每日 release 调度器按服务端/客户端两个独立 scope 决定下游,固定使用 `DEPLOY_TARGET=release` 和 `CONFIRM_RELEASE_DEPLOY_AGENT=true`:服务端相关路径变化时触发 Full Build,`DATABASE_BACKUP_MODE` 默认 `async`,Stdb/API rollout 模式与审批人由调度参数显式透传;客户端相关路径变化时先经发号 Job 获取统一总号,再以同一个固定 commit 触发 AGC Windows 与 macOS 的 `release` 分区构建,macOS 仍带 `SKIP_IF_SUPERSEDED=true`。三类下游均 `wait: true` 并汇总结果,只有成功 lane 才推进 `.jenkins-last-release-full-revision` 或 `.jenkins-last-release-agc-revision`,失败 lane 下一轮单独补发,不重复已成功的服务端发布。dev 调度器的下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/`、`.codex/`、`jenkins/`、`apps/ai-game-creator-shell/`、`apps/mobile-shell/`、`apps/desktop-shell/`、`apps/preview-deployer-web/`、`tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。AGC 客户端版本号不再由渠道各自递增:唯一发号源是 OSS 对象 `agc/global-version.json`,发号收口到 `Genarrative-Agc-Global-Version-Issue`(`disableConcurrentBuilds()` + 写后回读校验;集群未装 `lockable-resources` 插件)。dev / release 两条调度管线与手动发布管线都先调用该 Job 发号,再用归档产物 `agc-global-version.txt` 读取总号并作为 `AGC_RELEASE_VERSION` 透传给客户端构建;统一构建只发一次号供各渠道共用,单渠道热修只把号传给该渠道。显式传入的号低于本渠道当前清单版本时构建失败关闭;`AGC_RELEASE_DRY_RUN` 只预览下一位,不写回、不烧号。一次性播种用 `SEED_ONLY`:基线取「仓库版本 / 各渠道清单 / 旧迁移指针」的最大值,播种本身不烧号。仓库里的 5 个版本文件仍由构建改写,只作构建输入参考,不是事实源。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;release 调度显式覆盖为 `release` 目标并透传备份、rollout、Pingora 和维护退出参数。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build` 结束后都会触发 `Genarrative-Notify-Email`;只有正式 OSS 发布成功时,通知正文才附加本次生成的 `latest.json` 首装包 URL,演练、失败或跳过构建时留空,避免把未写入或不完整的对象地址发给收件人。 手工发布入口 `Genarrative-Manual-Build-And-Deploy` 的 `DEPLOY_TARGET` 与 AGC 更新渠道是两个独立维度。手工入口必须把 `release` 映射为 `AGC_UPDATE_CHANNEL=release`、把 `development` 映射为 `dev`,并把该参数同时透传给 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`;只传 `AGC_RELEASE_VERSION` 时下游会落回各自默认 `dev`。统一号 `0.1.95` 的 release 包应分别位于 `agc/release-win/0.1.95/` 与 `agc/release-mac/0.1.95/`,渠道清单为对应目录下的 `latest.json`,不存在 `agc/release/` 这一层。补发本轮已烧号的版本时,直接以相同 `AGC_RELEASE_VERSION` 重跑两条 AGC Job,不重新发号。 diff --git a/jenkins/Jenkinsfile.scheduled-release-trigger b/jenkins/Jenkinsfile.scheduled-release-trigger index 044bd0883..28c87b17f 100644 --- a/jenkins/Jenkinsfile.scheduled-release-trigger +++ b/jenkins/Jenkinsfile.scheduled-release-trigger @@ -1,6 +1,6 @@ -// AGC release 每日调度管线:每天 04:00 检查源码分支, -// 只有上一轮 release 调度之后出现客户端相关路径变更时,才用同一个固定 commit -// 触发 AGC Windows/macOS 两个 release 分区构建。 +// release 每日调度管线:每天 04:00 检查源码分支, +// 按独立 scope 触发 release Full Build 与 AGC Windows/macOS, +// 等待并汇总结果;只有对应 lane 成功后才推进该 lane 的成功 revision。 pipeline { agent { label 'linux && genarrative-build' @@ -19,17 +19,25 @@ pipeline { environment { GIT_REMOTE_URL = 'ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git' GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh' + FULL_BUILD_JOB_NAME = 'Genarrative-Full-Build-And-Deploy' AGC_WINDOWS_BUILD_JOB_NAME = 'Genarrative-Agc-Windows-Build' AGC_MACOS_BUILD_JOB_NAME = 'Genarrative-Agc-MacOS-Build' AGC_GLOBAL_VERSION_JOB_NAME = 'Genarrative-Agc-Global-Version-Issue' AGC_GLOBAL_VERSION_ARTIFACT = 'agc-global-version.txt' REVISION_STATE_FILE = '.jenkins-last-release-revision' + FULL_REVISION_STATE_FILE = '.jenkins-last-release-full-revision' + AGC_REVISION_STATE_FILE = '.jenkins-last-release-agc-revision' AGC_SCOPE_CACHE_DIR = '.agc-release-scope-cache' } parameters { string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '被检查的源码分支;默认 master') - booleanParam(name: 'FORCE_TRIGGER', defaultValue: false, description: '勾选后忽略版本与路径比较,强制发布 AGC release 渠道') + booleanParam(name: 'FORCE_TRIGGER', defaultValue: false, description: '勾选后忽略版本与路径比较,强制发布 release 服务端与 AGC 双平台') + choice(name: 'DATABASE_BACKUP_MODE', choices: ['async', 'sync', 'skip'], description: 'release 发布前 SpacetimeDB 备份策略;常规使用 async,高风险迁移使用 sync,skip 仅限明确无需备份') + choice(name: 'STDB_API_ROLLOUT_MODE', choices: ['normal', 'pause-after-stdb'], description: 'normal 为自动完成 Stdb → API;pause-after-stdb 在发布 Stdb 后等待审批人放行 API') + string(name: 'STDB_API_ROLLOUT_APPROVERS', defaultValue: '', description: 'pause-after-stdb 必填:允许放行 API 的 Jenkins 用户/组,多个值用逗号分隔') + booleanParam(name: 'INCLUDE_PINGORA_GATEWAY', defaultValue: true, description: 'release API 是否构建、归档并部署 Pingora 影子网关') + booleanParam(name: 'EXIT_MAINTENANCE_MODE_AFTER_COMPLETION', defaultValue: true, description: '完整发布成功后是否退出维护模式;首次验收可暂时关闭') } stages { @@ -50,8 +58,15 @@ pipeline { } script { env.REMOTE_REVISION = readFile('.jenkins-remote-revision').trim() - env.LAST_TRIGGERED_REVISION = fileExists(env.REVISION_STATE_FILE) ? readFile(env.REVISION_STATE_FILE).trim() : '' - env.REVISION_CHANGED = (params.FORCE_TRIGGER || env.LAST_TRIGGERED_REVISION != env.REMOTE_REVISION) ? 'true' : 'false' + def legacyRevision = fileExists(env.REVISION_STATE_FILE) ? readFile(env.REVISION_STATE_FILE).trim() : '' + env.LAST_FULL_REVISION = fileExists(env.FULL_REVISION_STATE_FILE) ? readFile(env.FULL_REVISION_STATE_FILE).trim() : legacyRevision + env.LAST_AGC_REVISION = fileExists(env.AGC_REVISION_STATE_FILE) ? readFile(env.AGC_REVISION_STATE_FILE).trim() : legacyRevision + env.FULL_REVISION_CHANGED = (params.FORCE_TRIGGER || env.LAST_FULL_REVISION != env.REMOTE_REVISION) ? 'true' : 'false' + env.AGC_REVISION_CHANGED = (params.FORCE_TRIGGER || env.LAST_AGC_REVISION != env.REMOTE_REVISION) ? 'true' : 'false' + env.REVISION_CHANGED = (env.FULL_REVISION_CHANGED == 'true' || env.AGC_REVISION_CHANGED == 'true') ? 'true' : 'false' + if (params.STDB_API_ROLLOUT_MODE == 'pause-after-stdb' && !params.STDB_API_ROLLOUT_APPROVERS?.trim()) { + error('pause-after-stdb 必须填写 STDB_API_ROLLOUT_APPROVERS。') + } if (env.REVISION_CHANGED != 'true') { currentBuild.result = 'NOT_BUILT' currentBuild.description = "release 版本未变化,跳过:${env.SOURCE_BRANCH} 仍是 ${env.REMOTE_REVISION.take(12)}" @@ -61,24 +76,21 @@ pipeline { } } - stage('Resolve AGC Release Scope') { + stage('Resolve Release Scope') { when { expression { return env.REVISION_CHANGED == 'true' } } steps { withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) { script { - // 判定失败一律按「需要发布」处理,避免路径过滤阻塞客户端发布。 - def output = 'agc=changed' + // 判定失败一律按「需要发布」处理,避免路径过滤阻塞正式发布。 + def output = 'agc=changed\nfull=changed' try { output = sh(script: '''#!/usr/bin/env bash set -uo pipefail export GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY:?缺少 Git SSH 凭据} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" - previous="$(cat "${REVISION_STATE_FILE}" 2>/dev/null || true)" - if [[ -z "${previous}" ]]; then - printf 'agc=changed\n' - exit 0 - fi + previous_agc="$(cat "${AGC_REVISION_STATE_FILE}" 2>/dev/null || cat "${REVISION_STATE_FILE}" 2>/dev/null || true)" + previous_full="$(cat "${FULL_REVISION_STATE_FILE}" 2>/dev/null || cat "${REVISION_STATE_FILE}" 2>/dev/null || true)" mkdir -p "${AGC_SCOPE_CACHE_DIR}" if [[ ! -d "${AGC_SCOPE_CACHE_DIR}/.git" ]]; then git -C "${AGC_SCOPE_CACHE_DIR}" init --quiet @@ -86,48 +98,121 @@ pipeline { fi refspec="+refs/heads/${SOURCE_BRANCH}:refs/remotes/origin/${SOURCE_BRANCH}" if ! git -C "${AGC_SCOPE_CACHE_DIR}" fetch --quiet --depth=200 --no-tags --filter=blob:none origin "${refspec}"; then - git -C "${AGC_SCOPE_CACHE_DIR}" fetch --quiet --depth=200 --no-tags origin "${refspec}" || { printf 'agc=changed\n'; exit 0; } + git -C "${AGC_SCOPE_CACHE_DIR}" fetch --quiet --depth=200 --no-tags origin "${refspec}" || { printf 'agc=changed\nfull=changed\n'; exit 0; } fi - if ! git -C "${AGC_SCOPE_CACHE_DIR}" cat-file -e "${previous}^{commit}" 2>/dev/null; then - echo "浅取窗口内没有 ${previous},按需要发布处理" >&2 - printf 'agc=changed\n' - exit 0 - fi - changed_paths="$(git -C "${AGC_SCOPE_CACHE_DIR}" diff --name-only "${previous}" "${REMOTE_REVISION}" 2>/dev/null || true)" + changed_paths_for() { + local previous="$1" + if [[ -z "${previous}" ]] || ! git -C "${AGC_SCOPE_CACHE_DIR}" cat-file -e "${previous}^{commit}" 2>/dev/null; then + printf '__changed__\n' + return + fi + git -C "${AGC_SCOPE_CACHE_DIR}" diff --name-only "${previous}" "${REMOTE_REVISION}" 2>/dev/null || printf '__changed__\n' + } + agc_paths="$(changed_paths_for "${previous_agc}")" agc_scope=unchanged - while IFS= read -r changed_path; do - [[ -z "${changed_path}" ]] && continue - case "${changed_path}" in - apps/ai-game-creator-shell/*|packages/*|server-rs/crates/*|plugins/agc-cocos-editor/*|plugins/agc-unity-editor/*|plugins/agc-godot-editor/*|apps/desktop-shell/src-tauri/icons/*|package.json|package-lock.json) - agc_scope=changed - ;; - esac - done <<< "${changed_paths}" - printf 'agc=%s\n' "${agc_scope}" + if [[ "${agc_paths}" == '__changed__' ]]; then + agc_scope=changed + else + while IFS= read -r changed_path; do + [[ -z "${changed_path}" ]] && continue + case "${changed_path}" in + apps/ai-game-creator-shell/*|packages/*|server-rs/crates/*|plugins/agc-cocos-editor/*|plugins/agc-unity-editor/*|plugins/agc-godot-editor/*|apps/desktop-shell/src-tauri/icons/*|package.json|package-lock.json) + agc_scope=changed + ;; + esac + done <<< "${agc_paths}" + fi + full_paths="$(changed_paths_for "${previous_full}")" + full_scope=unchanged + if [[ "${full_paths}" == '__changed__' ]]; then + full_scope=changed + else + while IFS= read -r changed_path; do + [[ -z "${changed_path}" ]] && continue + case "${changed_path}" in + docs/*|.codex/*|jenkins/*|apps/ai-game-creator-shell/*|apps/mobile-shell/*|apps/desktop-shell/*|apps/preview-deployer-web/*|tools/*|*.md) + ;; + *) + full_scope=changed + ;; + esac + done <<< "${full_paths}" + fi + printf 'agc=%s\nfull=%s\n' "${agc_scope}" "${full_scope}" ''', returnStdout: true).trim() } catch (error) { echo "release 发布范围判定失败,按需要发布处理:${error}" - output = 'agc=changed' + output = 'agc=changed\nfull=changed' } def values = output.split('\n').collect { it.trim() }.findAll { it } - def entry = values.find { it.startsWith('agc=') } - def value = entry == null ? '' : entry.split('=')[1] - env.AGC_RELEASE_SCOPE = (value == 'unchanged') ? 'unchanged' : 'changed' - echo "release 发布范围:AGC=${env.AGC_RELEASE_SCOPE}(上一轮已触发 revision=${env.LAST_TRIGGERED_REVISION ?: '无'})" + def readScope = { String key -> + def entry = values.find { it.startsWith(key + '=') } + def value = entry == null ? '' : entry.split('=')[1] + return (value == 'unchanged') ? 'unchanged' : 'changed' + } + env.AGC_RELEASE_SCOPE = readScope('agc') + env.FULL_BUILD_SCOPE = readScope('full') + echo "release 发布范围:AGC=${env.AGC_RELEASE_SCOPE} FullBuild=${env.FULL_BUILD_SCOPE}(AGC=${env.LAST_AGC_REVISION ?: '无'},Full=${env.LAST_FULL_REVISION ?: '无'})" } } } } - stage('Trigger AGC Release Builds') { + stage('Trigger Release Downstreams') { when { expression { return env.REVISION_CHANGED == 'true' } } steps { script { def pinnedRevision = env.REMOTE_REVISION - def agcTriggered = false - if (params.FORCE_TRIGGER || env.AGC_RELEASE_SCOPE != 'unchanged') { + def fullTriggered = params.FORCE_TRIGGER || env.FULL_BUILD_SCOPE != 'unchanged' + def agcTriggered = params.FORCE_TRIGGER || env.AGC_RELEASE_SCOPE != 'unchanged' + def fullParameters = [ + string(name: 'SOURCE_BRANCH', value: env.SOURCE_BRANCH), + string(name: 'COMMIT_HASH', value: pinnedRevision), + string(name: 'DEPLOY_TARGET', value: 'release'), + booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: true), + string(name: 'DATABASE_BACKUP_MODE', value: params.DATABASE_BACKUP_MODE), + string(name: 'STDB_API_ROLLOUT_MODE', value: params.STDB_API_ROLLOUT_MODE), + string(name: 'STDB_API_ROLLOUT_APPROVERS', value: params.STDB_API_ROLLOUT_APPROVERS?.trim() ?: ''), + booleanParam(name: 'INCLUDE_PINGORA_GATEWAY', value: params.INCLUDE_PINGORA_GATEWAY), + booleanParam(name: 'EXIT_MAINTENANCE_MODE_AFTER_COMPLETION', value: params.EXIT_MAINTENANCE_MODE_AFTER_COMPLETION), + ] + if (!fullTriggered) { + echo "上一轮 release 调度后没有服务端相关变更,跳过 ${env.FULL_BUILD_JOB_NAME}" + } + if (!agcTriggered) { + echo "上一轮 release 调度后没有 AGC 客户端相关变更,跳过 ${env.AGC_WINDOWS_BUILD_JOB_NAME} 与 ${env.AGC_MACOS_BUILD_JOB_NAME}" + } + def outcome = [:] + def runDownstream = { String jobName, List jobParameters, int timeoutMinutes -> + try { + timeout(time: timeoutMinutes, unit: 'MINUTES') { + def run = build job: jobName, + wait: true, + propagate: false, + parameters: jobParameters + outcome[jobName] = "#${run.number}:${run.result}" + if (run.result != 'SUCCESS') { + error("${jobName} 结果=${run.result}") + } + } + } catch (err) { + if (!outcome[jobName]) { + outcome[jobName] = "异常:${err.message}" + } + echo "${jobName} 等待失败: ${err.message}" + throw err + } + } + def branches = [:] + def fullTimeout = params.STDB_API_ROLLOUT_MODE == 'pause-after-stdb' ? 300 : 150 + if (fullTriggered) { + branches[env.FULL_BUILD_JOB_NAME] = { + runDownstream(env.FULL_BUILD_JOB_NAME, fullParameters, fullTimeout) + } + } + if (agcTriggered) { // Windows 与 macOS 共用一个总号;发号 Job 是唯一发号源。 def versionRun = build job: env.AGC_GLOBAL_VERSION_JOB_NAME, wait: true, @@ -152,22 +237,45 @@ pipeline { string(name: 'AGC_UPDATE_CHANNEL', value: 'release'), string(name: 'AGC_RELEASE_VERSION', value: env.AGC_GLOBAL_VERSION), ] - build job: env.AGC_WINDOWS_BUILD_JOB_NAME, wait: false, propagate: false, parameters: agcParameters // macOS 节点是办公机:离线期间排队的旧构建在节点回来后必须自行让位。 def agcMacosParameters = agcParameters + [ booleanParam(name: 'SKIP_IF_SUPERSEDED', value: true), ] - build job: env.AGC_MACOS_BUILD_JOB_NAME, wait: false, propagate: false, parameters: agcMacosParameters - agcTriggered = true - } else { - echo "上一轮 release 调度后没有 AGC 客户端相关变更,跳过 ${env.AGC_WINDOWS_BUILD_JOB_NAME} 与 ${env.AGC_MACOS_BUILD_JOB_NAME};需要强制发布时勾选 FORCE_TRIGGER" + branches[env.AGC_WINDOWS_BUILD_JOB_NAME] = { + runDownstream(env.AGC_WINDOWS_BUILD_JOB_NAME, agcParameters, 120) + } + branches[env.AGC_MACOS_BUILD_JOB_NAME] = { + runDownstream(env.AGC_MACOS_BUILD_JOB_NAME, agcMacosParameters, 120) + } } - writeFile file: env.REVISION_STATE_FILE, text: pinnedRevision + if (!branches.isEmpty()) { + try { + parallel branches + } catch (err) { + echo "release 并行下游存在失败分支: ${err.message}" + } + } + def fullResult = outcome[env.FULL_BUILD_JOB_NAME] + def windowsResult = outcome[env.AGC_WINDOWS_BUILD_JOB_NAME] + def macosResult = outcome[env.AGC_MACOS_BUILD_JOB_NAME] + def fullSucceeded = !fullTriggered || (fullResult != null && fullResult.endsWith(':SUCCESS')) + def agcSucceeded = !agcTriggered || (windowsResult != null && windowsResult.endsWith(':SUCCESS') && macosResult != null && macosResult.endsWith(':SUCCESS')) + if (fullSucceeded) { + writeFile file: env.FULL_REVISION_STATE_FILE, text: pinnedRevision + } + if (agcSucceeded) { + writeFile file: env.AGC_REVISION_STATE_FILE, text: pinnedRevision + } + if (fullSucceeded && agcSucceeded) { + writeFile file: env.REVISION_STATE_FILE, text: pinnedRevision + } + def summary = outcome.collect { jobName, result -> "${jobName}=${result}" }.join(' ') def target = "${env.SOURCE_BRANCH}@${pinnedRevision.take(12)}" - currentBuild.description = agcTriggered - ? "已触发 AGC release:${env.AGC_WINDOWS_BUILD_JOB_NAME} 与 ${env.AGC_MACOS_BUILD_JOB_NAME} ${target}" - : "本轮没有 AGC 客户端相关变更,未触发 release 发布:${target}" + currentBuild.description = "release 调度 ${target};FullBuild=${fullTriggered ? 'triggered' : 'unchanged'};AGC=${agcTriggered ? 'triggered' : 'unchanged'}${summary ? ';' + summary : ''}" echo currentBuild.description + if (!fullSucceeded || !agcSucceeded) { + error("release 下游未全部成功: ${summary}") + } } } } @@ -175,7 +283,7 @@ pipeline { post { always { - echo "release 调度结束: branch=${env.SOURCE_BRANCH} revision=${env.REMOTE_REVISION ?: ''} changed=${env.REVISION_CHANGED ?: ''} agcScope=${env.AGC_RELEASE_SCOPE ?: ''}" + echo "release 调度结束: branch=${env.SOURCE_BRANCH} revision=${env.REMOTE_REVISION ?: ''} changed=${env.REVISION_CHANGED ?: ''} agcScope=${env.AGC_RELEASE_SCOPE ?: ''} fullScope=${env.FULL_BUILD_SCOPE ?: ''}" } } } diff --git a/jenkins/scheduled-release-trigger-job-config.xml b/jenkins/scheduled-release-trigger-job-config.xml index 6baaa851b..8a8afe6be 100644 --- a/jenkins/scheduled-release-trigger-job-config.xml +++ b/jenkins/scheduled-release-trigger-job-config.xml @@ -1,7 +1,7 @@ - 每天 04:00 检查源码分支版本(cron 定义在 Jenkinsfile 内):revision 变化且出现客户端相关路径变更时,先经 Genarrative-Agc-Global-Version-Issue 发号,再用同一个固定 commit 与总版本号触发 AGC Windows、macOS 两个 release 分区构建;macOS 额外带 SKIP_IF_SUPERSEDED。下游 Job 一律不自带触发器。 + 每天 04:00 检查源码分支版本(cron 定义在 Jenkinsfile 内):revision 变化且出现服务端相关路径时触发 release Full Build,出现客户端相关路径时经 Genarrative-Agc-Global-Version-Issue 发号并触发 AGC Windows、macOS;调度器等待并汇总三类下游结果,按成功 lane 推进 revision,失败 lane 下一轮补发。下游 Job 一律不自带触发器。 false @@ -14,9 +14,46 @@ FORCE_TRIGGER - 勾选后忽略版本与路径比较,强制发布 AGC release 渠道 + 勾选后忽略版本与路径比较,强制发布 release 服务端与 AGC 双平台 false + + DATABASE_BACKUP_MODE + release 发布前 SpacetimeDB 备份策略;常规使用 async,高风险迁移使用 sync,skip 仅限明确无需备份 + + + async + sync + skip + + + + + STDB_API_ROLLOUT_MODE + normal 为自动完成 Stdb → API;pause-after-stdb 在发布 Stdb 后等待审批人放行 API + + + normal + pause-after-stdb + + + + + STDB_API_ROLLOUT_APPROVERS + pause-after-stdb 必填:允许放行 API 的 Jenkins 用户/组,多个值用逗号分隔 + + true + + + INCLUDE_PINGORA_GATEWAY + release API 是否构建、归档并部署 Pingora 影子网关 + true + + + EXIT_MAINTENANCE_MODE_AFTER_COMPLETION + 完整发布成功后是否退出维护模式;首次验收可暂时关闭 + true + diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index 6bc31b5b6..a25f85cc3 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -7761,7 +7761,7 @@ for (const [file, content] of [ if (/\btriggers\s*\{/u.test(content) || content.includes('cron(')) { failed = true; console.error( - `[check:production-ops] ${file} 不得自带定时触发器;版本检查与触发必须由 Genarrative-Scheduled-Revision-Trigger 统一负责。`, + `[check:production-ops] ${file} 不得自带定时触发器;版本检查与触发必须由两条定时调度管线统一负责。`, ); } } @@ -7908,6 +7908,10 @@ for (const [snippet, reason] of [ ['disableConcurrentBuilds()', '必须禁止 release 调度器并发触发'], ['skipDefaultCheckout(true)', 'release 调度器不得依赖本地 SCM 工作区'], ['git ls-remote', 'release 调度器必须直接从远端解析分支版本'], + [ + "FULL_BUILD_JOB_NAME = 'Genarrative-Full-Build-And-Deploy'", + '必须声明 release Full Build 目标 Job', + ], [ "AGC_WINDOWS_BUILD_JOB_NAME = 'Genarrative-Agc-Windows-Build'", '必须声明 AGC Windows release 构建目标 Job', @@ -7916,6 +7920,19 @@ for (const [snippet, reason] of [ "AGC_MACOS_BUILD_JOB_NAME = 'Genarrative-Agc-MacOS-Build'", '必须声明 AGC macOS release 构建目标 Job', ], + [ + "FULL_REVISION_STATE_FILE = '.jenkins-last-release-full-revision'", + '必须独立记录 Full Build 成功 revision', + ], + [ + "AGC_REVISION_STATE_FILE = '.jenkins-last-release-agc-revision'", + '必须独立记录 AGC 双平台成功 revision', + ], + ['full=changed', 'release 调度器必须计算服务端 Full Build scope'], + [ + "env.FULL_BUILD_SCOPE = readScope('full')", + 'release 调度器必须解析 Full Build scope', + ], [ "string(name: 'AGC_CHANNEL', value: 'release-unified')", 'release 调度器必须先通过发号 Job 获取统一总版本号', @@ -7925,27 +7942,64 @@ for (const [snippet, reason] of [ 'release 调度器必须把 Windows 与 macOS 都发到 release 分区', ], [ - 'build job: env.AGC_WINDOWS_BUILD_JOB_NAME, wait: false, propagate: false, parameters: agcParameters', + "string(name: 'DEPLOY_TARGET', value: 'release')", + 'release Full Build 必须固定部署到 release', + ], + [ + "booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: true)", + 'release Full Build 必须确认使用独立 release 部署 agent', + ], + [ + "string(name: 'DATABASE_BACKUP_MODE', value: params.DATABASE_BACKUP_MODE)", + 'release Full Build 必须显式透传数据库备份策略', + ], + [ + "string(name: 'STDB_API_ROLLOUT_MODE', value: params.STDB_API_ROLLOUT_MODE)", + 'release Full Build 必须显式透传 Stdb/API rollout 策略', + ], + [ + 'def runDownstream = { String jobName, List jobParameters, int timeoutMinutes ->', + 'release 调度器必须统一等待并收集三个下游结果', + ], + [ + 'build job: jobName,\n wait: true,\n propagate: false,', + 'release 下游必须等待完成并保留每个 Job 的结果', + ], + [ + 'branches[env.FULL_BUILD_JOB_NAME] = {', + 'release 调度器必须触发 Full Build', + ], + [ + 'branches[env.AGC_WINDOWS_BUILD_JOB_NAME] = {', 'release 调度器必须触发 AGC Windows release 构建', ], [ - 'build job: env.AGC_MACOS_BUILD_JOB_NAME, wait: false, propagate: false, parameters: agcMacosParameters', + 'branches[env.AGC_MACOS_BUILD_JOB_NAME] = {', 'release 调度器必须触发 AGC macOS release 构建', ], [ "booleanParam(name: 'SKIP_IF_SUPERSEDED', value: true)", 'release 调度器必须让 macOS 节点恢复后跳过过期排队构建', ], + [ + 'if (fullSucceeded) {', + '只有 Full Build 成功后才能推进 Full Build revision', + ], + ['if (agcSucceeded) {', '只有 AGC 双平台成功后才能推进 AGC revision'], + [ + 'error("release 下游未全部成功: ${summary}")', + '任一 release 下游失败时调度器必须失败', + ], ]) { if (!scheduledReleaseTriggerContent.includes(snippet)) { failed = true; console.error(`[check:production-ops] release 调度管线${reason}。`); } } -if (scheduledReleaseTriggerContent.includes('FULL_BUILD_JOB_NAME')) { +if (scheduledReleaseTriggerContent.includes('wait: false')) { failed = true; console.error( - '[check:production-ops] release 调度管线只负责 AGC release 客户端构建,不得触发 Full Build。', + '[check:production-ops] release 调度管线不得 fire-and-forget,必须等待并汇总 Full Build 与 AGC 结果。', ); } if (