From a377ece48ee6ad758167a637c36c3dfcb3e56596 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:37:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Mac=20release=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E7=9A=84=20Git=20=E5=BF=83=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 macOS Jenkins Checkout 增加 SSH 保活与 fetch 心跳。\n增加 fetch 超时诊断并补充生产运维门禁。\n更新 Mac 构建运维说明。 --- ...发运维】本地开发验证与生产运维-2026-05-15.md | 2 + ...kinsfile.ai-game-creator-shell-macos-build | 38 ++++++++++++++++++- scripts/check-production-ops-guardrails.mjs | 22 +++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 73b6b7d87..dc13e3894 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -908,6 +908,8 @@ Mac 单架构(arm64)构建脚本位于 `jenkins/Jenkinsfile.ai-game-creator- Job 名为 `Genarrative-Agc-MacOS-Build`,SCM 直接读取仓库内上述 Jenkinsfile,参数为 `SOURCE_BRANCH`、`COMMIT_HASH`、`AGC_UPDATE_CHANNEL`、`AGC_RELEASE_VERSION`、`AGC_RELEASE_DRY_RUN`、`AGC_UPDATE_RELEASE_NOTES`、`OSSUTIL_BIN`、`CARGO_BUILD_JOBS`、`NOTIFICATION_EMAILS`。渠道参数是基础名(不含系统,默认 `dev`),脚本不接受 `dev-mac` 这类系统后缀,写入分区固定推导为 `-mac`:这与 Windows Job 的 `-win` 对称,也延续已发布客户端的端点。 +Checkout 阶段的远端 `git fetch` 必须带 SSH `ConnectTimeout`、`ServerAliveInterval`/`ServerAliveCountMax`,并在前台以 20 秒间隔输出心跳;Mac agent 通过 WebSocket 接入 Jenkins,长时间没有 stdout 会被 Jetty idle timeout 断开,日志通常表现为 `ClosedChannelException`,不能误判成 Rust 或签名构建失败。心跳包装还设置 15 分钟可配置上限 `AGC_GIT_FETCH_TIMEOUT_SECONDS`,超时会输出 fetch 诊断并失败关闭。 + 该 Job 的职责是构建并发布 `-mac` 分区更新:执行 `npm ci` 后使用锁文件校验并补齐两种 macOS Codex 原生依赖,再调用 `scripts/build-macos-ci.mjs`(AGC 应用目录下)生成 arm64 单架构 app、arm64 隔离 smoke、arm64 DMG(`<产品名>_<版本>_aarch64.dmg`)与分区清单 `latest.json`,用产物内烘焙的公钥复核更新包签名(`verify-updater-signature.mjs`),最后按 `AGC_RELEASE_DRY_RUN` 决定是否上传 OSS。产物名(`*.app`、updater 归档、DMG、卷名)一律从渠道安装身份派生的产品名推导(`dev` 为开发版,`release` 复用正式产品名,自定义渠道带渠道后缀),校验脚本从包内 `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` 与 Windows 渠道对称:默认关闭即真发布,勾选后才退化为演练(只打印将上传的对象、不写任何 OSS 对象)。本 Job 是正式发布入口,调度器在发号后与 Windows 一起触发它,并额外传 `SKIP_IF_SUPERSEDED=true`——Mac 节点是日常办公机,离线期间排队的旧构建在节点回来后若已被源码分支推进,直接跳过而不发布过期版本。Mac 节点需要 `ossutil`(实测 1.7.19 原生 arm64 可用,装在 `~/.local/bin`,已在 Job 的 PATH 内),可用 `OSSUTIL_BIN` 指定命令名或绝对路径。首次发布建议显式指定 `AGC_RELEASE_VERSION`,避免按渠道高水位递增时出现版本链回退。 diff --git a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build index ae31d1ad9..f56cee042 100644 --- a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build +++ b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build @@ -62,13 +62,47 @@ pipeline { SOURCE_BRANCH="${SOURCE_BRANCH:-master}" COMMIT_HASH="${COMMIT_HASH:-}" SKIP_IF_SUPERSEDED="${SKIP_IF_SUPERSEDED:-false}" - export GIT_SSH_COMMAND="ssh -i \\"$GIT_SSH_KEY\\" -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" + # Mac 节点通过 WebSocket 接入 Jenkins;远端 Git fetch 长时间没有 stdout 时, + # Jenkins/Jetty 的 idle timeout 会先断开 agent channel,留下 ClosedChannelException。 + # SSH 自身开启保活,并由前台心跳包装 fetch,避免把网络等待误判成 agent 失联。 + export GIT_SSH_COMMAND="ssh -i \\"$GIT_SSH_KEY\\" -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=30 -o ServerAliveInterval=20 -o ServerAliveCountMax=6" if [ ! -d .git ]; then git init git remote add origin "$GIT_REMOTE_URL" fi test "$(git remote get-url origin)" = "$GIT_REMOTE_URL" - git fetch --no-tags origin "+refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH" + fetch_log="$(mktemp "${TMPDIR:-/tmp}/agc-git-fetch.XXXXXX")" + fetch_pid="" + cleanup_fetch() { + if [ -n "${fetch_pid}" ] && kill -0 "${fetch_pid}" 2>/dev/null; then + kill "${fetch_pid}" 2>/dev/null || true + wait "${fetch_pid}" 2>/dev/null || true + fi + rm -f "${fetch_log}" + } + trap cleanup_fetch EXIT INT TERM + git fetch --no-tags origin "+refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH" >"${fetch_log}" 2>&1 & + fetch_pid=$! + fetch_started_at="$(date +%s)" + fetch_timeout_seconds="${AGC_GIT_FETCH_TIMEOUT_SECONDS:-900}" + while kill -0 "${fetch_pid}" 2>/dev/null; do + fetch_elapsed=$(( $(date +%s) - fetch_started_at )) + if [ "${fetch_elapsed}" -ge "${fetch_timeout_seconds}" ]; then + echo "[agc-macos] git fetch 超时(${fetch_timeout_seconds}s),终止并输出诊断:${SOURCE_BRANCH}" + kill "${fetch_pid}" 2>/dev/null || true + wait "${fetch_pid}" 2>/dev/null || true + cat "${fetch_log}" + exit 1 + fi + echo "[agc-macos] git fetch 仍在进行(${fetch_elapsed}s)" + sleep 20 + done + if ! wait "${fetch_pid}"; then + cat "${fetch_log}" + exit 1 + fi + cat "${fetch_log}" + fetch_pid="" ref="refs/remotes/origin/$SOURCE_BRANCH" if [ -n "$COMMIT_HASH" ]; then case "$COMMIT_HASH" in *[!0-9a-fA-F]* ) echo 'COMMIT_HASH 必须为十六进制'; exit 1;; esac diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index d412db2c3..37ecd3ce6 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -7725,6 +7725,28 @@ for (const [file, content] of [ } } +for (const [snippet, reason] of [ + [ + '-o ServerAliveInterval=20 -o ServerAliveCountMax=6', + 'Mac AGC Checkout 的 Git SSH 必须启用保活,避免 WebSocket idle timeout 断开 agent。', + ], + [ + 'echo "[agc-macos] git fetch 仍在进行', + 'Mac AGC Checkout 的 git fetch 必须持续输出心跳,避免长时间无 stdout 被误判为 agent 失联。', + ], + [ + 'AGC_GIT_FETCH_TIMEOUT_SECONDS', + 'Mac AGC Checkout 的 git fetch 必须有可配置的超时和失败诊断。', + ], +]) { + if (!agcMacosPipelineContent.includes(snippet)) { + failed = true; + console.error( + `[check:production-ops] jenkins/Jenkinsfile.ai-game-creator-shell-macos-build ${reason}`, + ); + } +} + // 更新签名私钥只允许经 Jenkins 凭据绑定注入构建进程:既不得把凭据本身写进 Jenkinsfile, // 也不得回显到日志或进归档(对应「AGC 更新发布管线渠道化」第 5 条的可静态复核部分)。 for (const [file, content] of [ -- 2.52.0