Files
Genarrative/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build
kdletters f426944701
Project CI / AI game creator shell Rust crates (push) Successful in 1m18s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m46s
Project CI / Backend tests (push) Successful in 3m36s
Project CI / Frontend tests (push) Successful in 1m43s
Project CI / Native shell tests (push) Successful in 5m40s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m35s
Project CI / Repository checks (push) Successful in 1m50s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m31s
Project CI / AI game creator shell web tests (push) Successful in 1m16s
调整 AGC 更新弹窗并停止自动生成更新日志
- 更新弹窗改为窗口居中纵向排版

- 停止自动生成渠道清单更新摘要

- 仅保留 AGC_UPDATE_RELEASE_NOTES 手动更新说明

- 清理 Jenkins 摘要锚点与 macOS 额外拉取 master 逻辑

- 同步开发运维、技术方案和回归测试
2026-09-23 16:18:04 +08:00

257 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pipeline {
agent { label 'genarrative-agc-macos' }
options {
disableConcurrentBuilds()
skipDefaultCheckout(true)
// 实测参考:合并主线后的双架构构建 68.5 分钟、更早一次 78 分钟;节点是共用办公机,
// 冷 sccache + 机器被占用时会更慢,因此上限放到 150 分钟而不是贴着观测值。
timeout(time: 150, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '3'))
timestamps()
}
parameters {
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '必须含 macOS 单架构(arm64)发布实现的受信任分支')
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,固定属于源码分支的提交;不递增应用版本')
string(name: 'AGC_UPDATE_CHANNEL', defaultValue: 'dev', description: 'AGC 发布渠道(基础名,不含系统):dev、release 或自定义小写名称;写入的分区固定为 <channel>-mac')
string(name: 'AGC_RELEASE_VERSION', defaultValue: '', description: '可选三段版本号;留空则按 <channel>-mac 分区清单高水位递增 patch。首次发布建议显式指定,避免版本链回退')
booleanParam(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false, description: '默认直接发布到 <channel>-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 内存且是日常办公机,若构建期间出现明显换页可临时调低。只影响本次构建')
string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与持久收件人凭据合并发送')
}
environment {
GIT_REMOTE_URL = 'ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git'
GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh'
AGC_OSS_BUCKET = 'agc-dev'
AGC_OSS_ENDPOINT = 'oss-rg-china-mainland.aliyuncs.com'
// 与 Linux 生产管线(api-build / stdb-module-build)对齐:Rust 编译走 sccache 对象缓存。
// 本 Job 只编译 aarch64-apple-darwin 单架构(Intel 未支持):sccache 与工作区 target 目录跨构建复用。
// 缓存根固定在 HOME 下的稳定目录,避免 WORKSPACE 重建导致近似冷构建;可用下面的变量覆盖。
GENARRATIVE_AGC_MACOS_CACHE_ROOT = 'caches/genarrative-jenkins/agc-macos'
RUSTC_WRAPPER = 'sccache'
SCCACHE_CACHE_SIZE = '20G'
CARGO_INCREMENTAL = '0'
// 不把节点用户名写进仓库:PATH 在下面的 shell 步骤里按 $HOME 展开。
AGC_EXTRA_PATH = '/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin'
}
stages {
stage('Checkout') {
steps {
// 只允许在专用 Agent 目录下构建:默认按约定匹配 $HOME/Library/Jenkins/agents/<node>/workspace/,
// 不写死某个节点名(节点改名后仍成立),也可用 AGC_AGENT_ROOT 显式覆盖。
// 目的是防止把开发 checkout 当成 Jenkins workspace。
sh '''
set -eu
# 手工「立即构建」时可选参数可能根本没定义(不是空值),先统一成空串/默认值,
# 否则 set -u 会让 Checkout 直接失败。
SOURCE_BRANCH="${SOURCE_BRANCH:-master}"
if [ -n "${AGC_AGENT_ROOT:-}" ]; then
case "$WORKSPACE" in "$AGC_AGENT_ROOT"/workspace/*) ;; *) echo "拒绝非专用 Agent 工作区:$WORKSPACE"; exit 1;; esac
else
case "$WORKSPACE" in "$HOME"/Library/Jenkins/agents/*/workspace/*) ;; *) echo "拒绝非专用 Agent 工作区:$WORKSPACE"; exit 1;; esac
fi
test "$(uname -s)" = Darwin
git check-ref-format --branch "$SOURCE_BRANCH" >/dev/null
'''
withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GIT_SSH_KEY', usernameVariable: 'GIT_SSH_USER')]) {
sh '''
set -eu
# 同上:可选参数在「立即构建」里可能是未定义变量,统一兜底再使用。
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"
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"
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
test "${#COMMIT_HASH}" -ge 7 && test "${#COMMIT_HASH}" -le 40
git cat-file -e "$COMMIT_HASH^{commit}"
git merge-base --is-ancestor "$COMMIT_HASH" "$ref"
ref="$COMMIT_HASH"
fi
git reset --hard "$ref"
# 不清除 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
# LaunchAgent 只给最小 PATH:按 $HOME 展开工具链位置,不把节点用户名写进仓库。
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:${AGC_EXTRA_PATH}"
test "$(uname -m)" = arm64
node --version
npm --version
cargo --version
xcrun --find clang
xcrun --find lipo
arch -x86_64 /usr/bin/uname -m
if command -v sccache >/dev/null 2>&1; then sccache --version; else echo '[agc-macos] 未找到 sccache;本次回退到 rustc 直接构建。'; fi
rustup target add aarch64-apple-darwin x86_64-apple-darwin
npm ci --no-audit --no-fund
node apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs
'''
}
}
stage('Package, verify and publish') {
when {
expression { return env.AGC_BUILD_SUPERSEDED != 'true' }
}
steps {
withCredentials([
string(credentialsId: 'AliyunAccessKeyId', variable: 'AGC_OSS_ACCESS_KEY_ID'),
string(credentialsId: 'AliyunaccessKeySecret', variable: 'AGC_OSS_ACCESS_KEY_SECRET'),
string(credentialsId: 'AgcUpdaterSigningKey', variable: 'TAURI_SIGNING_PRIVATE_KEY'),
string(credentialsId: 'AgcUpdaterSigningKeyPassword', variable: 'TAURI_SIGNING_PRIVATE_KEY_PASSWORD'),
]) {
withEnv([
"OSSUTIL_BIN=${params.OSSUTIL_BIN}",
"CARGO_BUILD_JOBS=${params.CARGO_BUILD_JOBS}",
"AGC_UPDATE_CHANNEL=${params.AGC_UPDATE_CHANNEL}",
"AGC_RELEASE_VERSION=${params.AGC_RELEASE_VERSION}",
"AGC_RELEASE_DRY_RUN=${params.AGC_RELEASE_DRY_RUN ? '1' : '0'}",
"AGC_UPDATE_RELEASE_NOTES=${params.AGC_UPDATE_RELEASE_NOTES}",
]) {
sh '''
set -eu
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:${AGC_EXTRA_PATH}"
echo "[agc-macos] 渠道=${AGC_UPDATE_CHANNEL} 分区=${AGC_UPDATE_CHANNEL}-mac 目标=aarch64-apple-darwin(单架构,Intel 未支持)dry-run=${AGC_RELEASE_DRY_RUN}"
ossutil_bin="${OSSUTIL_BIN:-ossutil}"
if command -v "${ossutil_bin}" >/dev/null 2>&1; then
"${ossutil_bin}" --version | head -1
elif [ "${AGC_RELEASE_DRY_RUN}" = "1" ]; then
echo "[agc-macos] dry-run:未找到 ${ossutil_bin},只打印上传计划,不写 OSS"
else
echo "[agc-macos] 正式发布缺少 ossutil:${ossutil_bin};请安装或指定 OSSUTIL_BIN"
exit 1
fi
cache_root="${GENARRATIVE_AGC_MACOS_CACHE_ROOT:-caches/genarrative-jenkins/agc-macos}"
case "${cache_root}" in /*) ;; *) cache_root="${HOME:?HOME 不能为空}/${cache_root}" ;; esac
if command -v sccache >/dev/null 2>&1; then
export SCCACHE_DIR="${cache_root}/sccache"
mkdir -p "${SCCACHE_DIR}"
echo "[agc-macos] sccache 缓存目录: ${SCCACHE_DIR}"
else
echo '[agc-macos] 未找到 sccache,改用 rustc 直接构建。'
unset RUSTC_WRAPPER
fi
node --test apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs
node --test apps/ai-game-creator-shell/scripts/verify-updater-signature.test.mjs
node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs apps/ai-game-creator-shell/scripts/cargo-features.test.mjs
node apps/ai-game-creator-shell/scripts/build-macos-ci.mjs
if command -v sccache >/dev/null 2>&1; then
echo '[agc-macos] sccache 统计(自 server 启动累计):'
sccache --show-stats 2>&1 | sed -n '1,6p'
fi
'''
}
}
}
}
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
}
}
}
post {
always {
script {
def ossDownloadUrl = ''
def releaseVersion = params.AGC_RELEASE_VERSION ?: ''
def sourceCommit = params.COMMIT_HASH ?: ''
def manifestPath = 'artifacts/latest.json'
if (currentBuild.currentResult == 'SUCCESS' && fileExists(manifestPath)) {
try {
def manifest = new groovy.json.JsonSlurperClassic().parseText(readFile(file: manifestPath))
releaseVersion = manifest?.version?.toString()?.trim() ?: releaseVersion
if (!params.AGC_RELEASE_DRY_RUN) {
def firstDownload = manifest?.downloads instanceof Map
? manifest.downloads.values().find { it instanceof Map && it.url?.toString()?.trim() }
: null
ossDownloadUrl = firstDownload?.url?.toString()?.trim() ?: ''
}
} catch (error) {
echo "读取 AGC OSS 下载链接失败,邮件不附加链接:${error.message}"
}
}
if (!sourceCommit && fileExists('.jenkins-source-commit')) {
sourceCommit = readFile(file: '.jenkins-source-commit').trim()
}
def notificationParameters = [
string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME),
string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER),
string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''),
string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'),
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''),
string(name: 'SOURCE_COMMIT', value: sourceCommit),
string(name: 'BUILD_VERSION', value: releaseVersion),
string(name: 'DEPLOY_TARGET', value: "${params.AGC_UPDATE_CHANNEL ?: 'dev'}-mac"),
string(name: 'OSS_DOWNLOAD_URL', value: ossDownloadUrl),
string(name: 'SUMMARY', value: params.AGC_RELEASE_DRY_RUN
? 'AGC macOS 渠道演练结束,未写入 OSS'
: 'AGC macOS 渠道构建与 OSS 发布结束'),
]
def notificationRecipients = params.NOTIFICATION_EMAILS?.trim()
if (notificationRecipients) {
notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients))
}
try {
build job: 'Genarrative-Notify-Email',
wait: false,
propagate: false,
parameters: notificationParameters
} catch (error) {
echo "邮件通知触发失败: ${error.message}"
}
}
}
failure {
echo 'macOS 发布失败:先看本构建控制台末尾。常见原因——更新包缺 .sig(误传 --no-sign 或签名私钥未注入)、DMG 幂等失败(workspace 残留同名产物)、可用空间低于 8 GiB、以及被 SKIP_IF_SUPERSEDED 之外的提交校验拒绝。'
}
aborted {
echo 'macOS 发布被中断(超时或人工中止):构建未走到归档阶段时不会有任何产物,也不会写 OSS。'
}
success {
echo params.AGC_RELEASE_DRY_RUN
? 'macOS arm64 渠道演练完成:已构建、验签并生成清单,未写入 OSS;Apple 签名与公证暂缺。'
: "macOS arm64 更新包、签名、首装包与 ${params.AGC_UPDATE_CHANNEL}-mac 分区清单已上传 OSS(清单只登记 darwin-aarch64);Apple 签名与公证暂缺,首装需手动放行。"
}
}
}