From dbbbef8a0def4036b84456769be0cf8dd8cad126 Mon Sep 17 00:00:00 2001 From: suzmii Date: Sun, 20 Sep 2026 22:47:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8AmacOS=E6=B8=A0=E9=81=93=E7=BA=B3?= =?UTF-8?q?=E5=85=A5=E5=AE=9A=E6=97=B6=E5=8F=91=E5=B8=83=E5=B9=B6=E5=85=B1?= =?UTF-8?q?=E7=94=A8=E6=80=BB=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调度器在发号后同时触发macOS渠道构建,透传同一个总版本号与固定revision macOS Job改为默认真实发布,演练需显式勾选 新增SKIP_IF_SUPERSEDED:节点离线期间排队的旧构建在恢复后自行让位 补充生产门禁规则与回归断言 同步调度与运维文档口径 --- .../scripts/prepare-macos-codex.test.mjs | 19 ++++++++++-- .../shared-memory/development-workflow.md | 2 +- ...发运维】本地开发验证与生产运维-2026-05-15.md | 4 +-- ...kinsfile.ai-game-creator-shell-macos-build | 29 ++++++++++++++++++- .../Jenkinsfile.scheduled-revision-trigger | 16 +++++++++- scripts/check-production-ops-guardrails.mjs | 12 ++++++++ 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs index c3aba538e..a0e46875b 100644 --- a/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs @@ -90,10 +90,25 @@ test('CI pipeline is manual, publishes the macOS partition and never reuses a de !pipeline.includes('genarrative-agc-macos-local'), 'Jenkinsfile 不得写死具体节点名', ); - // dry-run 必须是默认值:不显式取消勾选就不得写入 OSS。 + // 这条管线是正式发布入口(与 Windows 对称):默认真发布,演练需显式勾选。 assert.match( pipeline, - /booleanParam\(name: 'AGC_RELEASE_DRY_RUN', defaultValue: true/u, + /booleanParam\(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false/u, + 'Channel 发布默认必须是真发布,演练只能显式勾选', + ); + // 节点是办公机:离线期间排队的旧构建必须自行让位,且跳过要覆盖后续全部阶段。 + assert.match( + pipeline, + /booleanParam\(name: 'SKIP_IF_SUPERSEDED', defaultValue: false/u, + ); + assert.ok( + pipeline.includes('.jenkins-superseded-by'), + '必须记录被推进的标记供后续阶段判定', + ); + assert.equal( + (pipeline.match(/env\.AGC_BUILD_SUPERSEDED != 'true'/gu) ?? []).length, + 3, + 'Toolchain / Package / Archive 三个阶段都必须按跳过标记收口', ); for (const forbidden of [ 'triggers {', diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index b7084db6c..65d4e4c47 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -90,7 +90,7 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m ## Jenkins 定时版本调度 -定时与版本比较只保留在 `Genarrative-Scheduled-Revision-Trigger` 一处:每小时用 `git ls-remote` 解析 `SOURCE_BRANCH` 远端 HEAD,与上一次触发过的 revision 比较,变化时才把同一个 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy` 与 `Genarrative-Agc-Windows-Build`,保证两条管线构建同一个版本。`Genarrative-Full-Build-And-Deploy` 与 `Genarrative-Agc-Windows-Build` 不得自带 `triggers` / `cron`,也不得在管线内再做一套版本去重;`npm run check:production-ops` 会拦住这两类回退。调度状态与生效步骤见 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +定时与版本比较只保留在 `Genarrative-Scheduled-Revision-Trigger` 一处:每小时用 `git ls-remote` 解析 `SOURCE_BRANCH` 远端 HEAD,与上一次触发过的 revision 比较,变化时才把同一个 `COMMIT_HASH` 传给 `Genarrative-Full-Build-And-Deploy`,并先经 `Genarrative-Agc-Global-Version-Issue` 发号、再把同一个总版本号透传给 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,保证两个客户端的平台分区发布同一个版本。这三个下游 Job 都不得自带 `triggers` / `cron`,也不得在管线内再做一套版本去重;`npm run check:production-ops` 会拦住这两类回退。macOS 节点是日常办公机,调度触发它时置 `SKIP_IF_SUPERSEDED=true`:节点离线期间排队的旧构建在恢复后会自行让位,不发布过期版本。调度状态与生效步骤见 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 ## Gitea CI 依赖闭合 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 66e062526..dfb57b87c 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -143,7 +143,7 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去 `Genarrative-Scheduled-Revision-Trigger` 是唯一的定时入口,每小时检查一次(`H * * * *`,分钟由 Jenkins 按 Job 名散列,不等同于整点)。它只用 `git ls-remote` 解析 `SOURCE_BRANCH`(默认 `master`)的远端 HEAD,不 checkout 工作区;解析出的完整 commit 与上一次触发过的 revision 相同则标记 `NOT_BUILT` 并结束,不触发任何下游。 -revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy` 与 `Genarrative-Agc-Windows-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`,纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。两条下游各自判定: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` 插件)。调度管线与手动发布管线都先调用该 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`。 +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` 可强制两条都触发。两条下游各自判定: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` 插件)。调度管线与手动发布管线都先调用该 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`。 调度状态是调度 Job 工作区里的 `.jenkins-last-triggered-revision`,构建描述同时回显本次 revision 与结果。工作区被清理(例如 `Wipe Out Workspace`)或状态文件缺失时,下一次运行按“版本变化”处理并触发一次,之后恢复稳定;需要重建同一版本时勾选 `FORCE_TRIGGER`。Job 按仓库内 `jenkins/scheduled-revision-trigger-job-config.xml` 创建:`scriptPath=jenkins/Jenkinsfile.scheduled-revision-trigger`、Git 入口 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`、凭据 `genarrative-local-gitea-ssh`、`` 留空(定时器写在 Jenkinsfile 里)。推送后必须让三个 live Job 各自加载一次新 Jenkinsfile,并只读核对 `config.xml`:Full 与 AGC 不再有 cron,定时只来自新调度 Job;只改 Jenkinsfile 而不确认 live 配置时,旧 cron 仍会继续触发。 @@ -734,7 +734,7 @@ Job 名为 `Genarrative-Agc-MacOS-Build`,SCM 直接读取仓库内上述 Jenki 该 Job 的职责是构建并发布 `-mac` 分区更新:执行 `npm ci` 后使用锁文件校验并补齐两种 macOS Codex 原生依赖,再调用 `scripts/build-macos-ci.mjs`(AGC 应用目录下)生成 universal app、arm64/x86_64 隔离 smoke、universal DMG 与分区清单 `latest.json`,用产物内烘焙的公钥复核更新包签名(`verify-updater-signature.mjs`),最后按 `AGC_RELEASE_DRY_RUN` 决定是否上传 OSS。签到会同时取 `master`,让渠道清单里上一次发布的 commit 可解析——缺了它更新摘要会退化成「最近客户端改动」(该步失败只降级摘要,不阻断发布)。产物名(`*.app`、updater 归档、DMG、卷名)一律从 Tauri `productName` 推导,校验脚本从包内 `Info.plist` 读取可执行名,改产品名不会让入口静默找错对象;隔离 smoke 用 `ditto --clone` 复制副本(实测整轮 6.8 秒,此前整包复制约 1 分钟),并在构建前删除本次将写出的 DMG/更新包/签名,保证归档产物一定来自本次构建。归档限 `artifacts/` 下的 DMG、SHA-256、`latest.json`、更新包签名、更新摘要、非敏感构建清单和源码 commit;不归档用户 HOME、Jenkins secret、原始工作目录或全量日志。 -发布凭据全部走 Jenkins 全局凭据,并在 `withCredentials` 内注入当前进程:`AgcUpdaterSigningKey`(与 `AgcUpdaterSigningKeyPassword`)映射为 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,`AliyunAccessKeyId` / `AliyunaccessKeySecret` 映射为 `AGC_OSS_ACCESS_KEY_ID` / `AGC_OSS_ACCESS_KEY_SECRET`;私钥与凭据不写入 workspace、日志或归档产物。上传顺序为更新包、签名、首装包,三者全部成功后才覆盖 `agc/-mac/latest.json` 指针;`AGC_RELEASE_DRY_RUN` 默认开启,dry-run 只打印将上传的对象、不写任何 OSS 对象。Mac 节点需要 `ossutil`(实测 1.7.19 原生 arm64 可用,装在 `~/.local/bin`,已在 Job 的 PATH 内),可用 `OSSUTIL_BIN` 指定命令名或绝对路径。首次发布建议显式指定 `AGC_RELEASE_VERSION`,避免按渠道高水位递增时出现版本链回退。 +发布凭据全部走 Jenkins 全局凭据,并在 `withCredentials` 内注入当前进程:`AgcUpdaterSigningKey`(与 `AgcUpdaterSigningKeyPassword`)映射为 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,`AliyunAccessKeyId` / `AliyunaccessKeySecret` 映射为 `AGC_OSS_ACCESS_KEY_ID` / `AGC_OSS_ACCESS_KEY_SECRET`;私钥与凭据不写入 workspace、日志或归档产物。上传顺序为更新包、签名、首装包,三者全部成功后才覆盖 `agc/-mac/latest.json` 指针;`AGC_RELEASE_DRY_RUN` 与 Windows 渠道对称:默认关闭即真发布,勾选后才退化为演练(只打印将上传的对象、不写任何 OSS 对象)。本 Job 是正式发布入口,调度器在发号后与 Windows 一起触发它,并额外传 `SKIP_IF_SUPERSEDED=true`——Mac 节点是日常办公机,离线期间排队的旧构建在节点回来后若已被源码分支推进,直接跳过而不发布过期版本。Mac 节点需要 `ossutil`(实测 1.7.19 原生 arm64 可用,装在 `~/.local/bin`,已在 Job 的 PATH 内),可用 `OSSUTIL_BIN` 指定命令名或绝对路径。首次发布建议显式指定 `AGC_RELEASE_VERSION`,避免按渠道高水位递增时出现版本链回退。 产物边界:macOS 代码签名与公证暂缺,构建通过剥离 `APPLE_*` 凭据让 Tauri 跳过 Apple 签名,**不得使用 `--no-sign`**——该标志会连带跳过 updater 的 minisign 签名,产物缺少 `.sig` 会直接卡在验签门禁(首次实跑即命中该坑)。构建清单按 `codesign -dv` 实测记录 `appleSigned` / `appleSignatureKind`(如 `adhoc`),并固定记录 `notarized=false`。用户首次安装需要在 Gatekeeper 中手动放行;更新包校验本身只依赖 minisign 签名,因此未签名不阻断自动更新的校验环节,但「安装 → 重启接管新版本」的实机闭环仍未验证,不得以构建成功替代。 diff --git a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build index 8a72c1550..4744d0d64 100644 --- a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build +++ b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build @@ -12,7 +12,8 @@ pipeline { string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,固定属于源码分支的提交;不递增应用版本') string(name: 'AGC_UPDATE_CHANNEL', defaultValue: 'dev', description: 'AGC 发布渠道(基础名,不含系统):dev、release 或自定义小写名称;写入的分区固定为 -mac') string(name: 'AGC_RELEASE_VERSION', defaultValue: '', description: '可选三段版本号;留空则按 -mac 分区清单高水位递增 patch。首次发布建议显式指定,避免版本链回退') - booleanParam(name: 'AGC_RELEASE_DRY_RUN', defaultValue: true, description: '勾选后只构建、验签并打印将上传的对象,不写 OSS;取消勾选才真正发布到 -mac 分区') + booleanParam(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false, description: '默认直接发布到 -mac 分区;勾选后只构建、验签并打印将上传的对象,不写 OSS(演练)') + booleanParam(name: 'SKIP_IF_SUPERSEDED', defaultValue: false, description: '置真时:本次 COMMIT_HASH 若已被源码分支推进,则直接跳过而不构建。调度器触发本 Job 时置真,避免节点离线期间排队的旧构建在恢复后发布过期版本') string(name: 'AGC_UPDATE_RELEASE_NOTES', defaultValue: '', description: '可选单行更新摘要;留空则由发布脚本按提交自动汇总') string(name: 'OSSUTIL_BIN', defaultValue: 'ossutil', description: 'ossutil 命令名或绝对路径(Mac 节点默认装在 ~/.local/bin/ossutil)') string(name: 'CARGO_BUILD_JOBS', defaultValue: '8', description: '并行 rustc 任务数,默认吃满节点 8 核(4P+4E)。该值同时作为 rustc codegen 的 jobserver 令牌上限;节点只有 24 GB 内存且是日常办公机,若构建期间出现明显换页可临时调低。只影响本次构建') @@ -73,11 +74,31 @@ pipeline { # 不清除 node_modules/target 缓存;被跟踪内容始终来自上述 commit。 git clean -fd git rev-parse HEAD > .jenkins-source-commit + if [ "${SKIP_IF_SUPERSEDED}" = "true" ] && [ -n "$COMMIT_HASH" ]; then + latest="$(git rev-parse "refs/remotes/origin/$SOURCE_BRANCH")" + if [ "$latest" != "$(git rev-parse "$COMMIT_HASH^{commit}")" ]; then + # 只置标记,由后面的 when 条件统一收口;不在 shell 里 exit 0, + # 否则会带着旧 commit 继续往下构建。 + echo "[agc-macos] 本次 commit 已被 $SOURCE_BRANCH 推进:$COMMIT_HASH -> $latest,跳过发布" + printf '%s' "$latest" > .jenkins-superseded-by + fi + fi ''' } + script { + if (fileExists('.jenkins-superseded-by')) { + env.AGC_BUILD_SUPERSEDED = 'true' + currentBuild.result = 'NOT_BUILT' + currentBuild.description = "${env.COMMIT_HASH} 已被 ${env.SOURCE_BRANCH} 推进到 ${readFile('.jenkins-superseded-by').trim().take(12)},跳过本轮发布" + echo currentBuild.description + } + } } } stage('Toolchain and dependencies') { + when { + expression { return env.AGC_BUILD_SUPERSEDED != 'true' } + } steps { sh ''' set -eu @@ -96,6 +117,9 @@ pipeline { } } stage('Package, verify and publish') { + when { + expression { return env.AGC_BUILD_SUPERSEDED != 'true' } + } steps { withCredentials([ string(credentialsId: 'AliyunAccessKeyId', variable: 'AGC_OSS_ACCESS_KEY_ID'), @@ -147,6 +171,9 @@ pipeline { } } stage('Archive release') { + when { + expression { return env.AGC_BUILD_SUPERSEDED != 'true' } + } steps { archiveArtifacts artifacts: 'artifacts/*.dmg,artifacts/*.sha256,artifacts/latest.json,artifacts/*.sig,artifacts/release-notes.txt,artifacts/build-manifest.json,.jenkins-source-commit', fingerprint: true, allowEmptyArchive: false } diff --git a/jenkins/Jenkinsfile.scheduled-revision-trigger b/jenkins/Jenkinsfile.scheduled-revision-trigger index d3213b016..46aa861fe 100644 --- a/jenkins/Jenkinsfile.scheduled-revision-trigger +++ b/jenkins/Jenkinsfile.scheduled-revision-trigger @@ -1,5 +1,5 @@ // 定时版本调度管线:解析远端分支版本,只有和上一次触发过的版本不同时, -// 才用同一个固定 commit 触发 Full Build 与 AGC Windows Build 两个下游管线。 +// 才用同一个固定 commit 触发 Full Build、AGC 发号与 AGC Windows/macOS 渠道构建。 pipeline { agent { label 'linux && genarrative-build' @@ -20,6 +20,7 @@ pipeline { GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh' FULL_BUILD_JOB_NAME = 'Genarrative-Full-Build-And-Deploy' AGC_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-triggered-revision' @@ -177,6 +178,18 @@ pipeline { string(name: 'AGC_RELEASE_VERSION', value: env.AGC_GLOBAL_VERSION), ] build job: env.AGC_BUILD_JOB_NAME, wait: false, propagate: false, parameters: agcParameters + // macOS 与 Windows 共用发号 Job 下发的同一个总号:调度只透传号与固定 revision, + // 不允许 Mac 侧自行发号,否则同一轮两个平台会拿到不同的号。 + def agcMacosParameters = [ + string(name: 'SOURCE_BRANCH', value: env.SOURCE_BRANCH), + string(name: 'COMMIT_HASH', value: pinnedRevision), + string(name: 'AGC_UPDATE_CHANNEL', value: 'dev'), + string(name: 'AGC_RELEASE_VERSION', value: env.AGC_GLOBAL_VERSION), + // Mac 节点是办公机:离线期间排队的旧构建在节点回来后必须自行让位, + // 不允许发布一轮已经过期的版本。 + 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 "本轮提交不含 AGC 相关路径,跳过 ${env.AGC_BUILD_JOB_NAME};需要强制发布时勾选 FORCE_TRIGGER" @@ -185,6 +198,7 @@ pipeline { def triggered = [] if (fullTriggered) { triggered.add(env.FULL_BUILD_JOB_NAME) } if (agcTriggered) { triggered.add(env.AGC_BUILD_JOB_NAME) } + if (agcTriggered) { triggered.add(env.AGC_MACOS_BUILD_JOB_NAME) } def target = "${env.SOURCE_BRANCH}@${pinnedRevision.take(12)}" currentBuild.description = triggered.isEmpty() ? "本轮提交与两条下游都无关,未触发任何发布:${target}" diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index 998b8dec2..4c2fe6f44 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -7723,6 +7723,18 @@ for (const [snippet, reason] of [ "AGC_GLOBAL_VERSION_ARTIFACT = 'agc-global-version.txt'", '必须从发号 Job 的归档产物读取总版本号', ], + [ + "AGC_MACOS_BUILD_JOB_NAME = 'Genarrative-Agc-MacOS-Build'", + '必须把 macOS 渠道构建纳入同一轮调度', + ], + [ + 'build job: env.AGC_MACOS_BUILD_JOB_NAME, wait: false, propagate: false, parameters: agcMacosParameters', + '必须用独立参数列表触发 macOS 渠道构建(pinnedParameters 只允许 Full Build 使用一次)', + ], + [ + "booleanParam(name: 'SKIP_IF_SUPERSEDED', value: true)", + '必须让节点离线期间排队的 macOS 旧构建自行让位,不发布过期版本', + ], ]) { if (!scheduledRevisionTriggerContent.includes(snippet)) { failed = true;