Files
Genarrative/jenkins/Jenkinsfile.ai-game-creator-shell-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

275 lines
14 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 'windows && win2022'
}
options {
disableConcurrentBuilds()
skipDefaultCheckout(true)
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
}
environment {
GIT_REMOTE_URL = 'ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git'
GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh'
GENARRATIVE_NPM_VERSION = '10.9.7'
AGC_OSS_BUCKET = 'agc-dev'
AGC_OSS_ENDPOINT = 'oss-rg-china-mainland.aliyuncs.com'
AGC_WINDOWS_PATH = 'C:\\Tools\\Git\\cmd;C:\\Program Files\\nodejs;C:\\Program Files\\dotnet;C:\\Users\\Administrator\\.cargo\\bin;C:\\Tools\\ossutil;C:\\BuildTools\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin;C:\\Python312;C:\\Python312\\Scripts;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\'
}
parameters {
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支')
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
string(name: 'AGC_RELEASE_VERSION', defaultValue: '', description: '三段版本号;统一构建由 Genarrative-Agc-Global-Version-Issue 发号后透传;留空时本地兜底发号(读 OSS 总号 +1 并写回)')
string(name: 'AGC_UPDATE_CHANNEL', defaultValue: 'dev', description: 'AGC 发布渠道:dev、release 或自定义小写名称;此 Job 构建 Windows,macOS 在对应构建机执行')
booleanParam(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false, description: '勾选后只构建并打印将要执行的上传命令,不写入 OSS')
text(name: 'AGC_UPDATE_RELEASE_NOTES', defaultValue: '', description: '可选,支持多行文本;留空则本次发布不携带更新摘要')
string(name: 'OSSUTIL_BIN', defaultValue: 'ossutil', description: 'ossutil 或 ossutil.exe 的绝对路径/命令名')
string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与持久收件人凭据合并发送')
}
stages {
stage('Checkout') {
steps {
withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GIT_SSH_KEY', usernameVariable: 'GIT_SSH_USER')]) {
powershell '''
$ErrorActionPreference = 'Stop'
$env:GIT_SSH_COMMAND = "ssh -i `"$env:GIT_SSH_KEY`" -o StrictHostKeyChecking=no"
$git = 'C:\\Tools\\Git\\cmd\\git.exe'
if (-not (Test-Path $git)) { throw "找不到 Git: $git" }
if (Test-Path '.git') {
& $git fetch --tags --force --prune origin "+refs/heads/$($env:SOURCE_BRANCH):refs/remotes/origin/$($env:SOURCE_BRANCH)"
if ($LASTEXITCODE -ne 0) { throw 'Git fetch 失败' }
# Tauri 的 NSIS 工具缓存位于工作区内的 target/.tauri:裸 git clean -fdx
# 会把它一起删掉,导致每个构建都重新从 GitHub 下载工具链(下载无重试,
# 响应截断即 `io: unexpected end of file`)。这里只排除该缓存目录,
# 其余 target 内容照常清空。
& $git clean -fdx -e 'apps/ai-game-creator-shell/src-tauri/target/.tauri'
& $git reset --hard "origin/$($env:SOURCE_BRANCH)"
} else {
& $git clone --no-single-branch $env:GIT_REMOTE_URL .
if ($LASTEXITCODE -ne 0) { throw 'Git clone 失败' }
& $git fetch --tags --force origin "refs/heads/$($env:SOURCE_BRANCH):refs/remotes/origin/$($env:SOURCE_BRANCH)"
if ($LASTEXITCODE -ne 0) { throw 'Git fetch 分支失败' }
& $git checkout --detach "origin/$($env:SOURCE_BRANCH)"
}
$commit = ([string]$env:COMMIT_HASH).Trim()
if ($commit) {
if ($commit -notmatch '^[0-9a-fA-F]{7,64}$') {
throw 'COMMIT_HASH 必须是 7-64 位十六进制 Git commit。'
}
& $git cat-file -e "${commit}^{commit}"
if ($LASTEXITCODE -ne 0) {
throw "COMMIT_HASH 不存在: $commit"
}
$branchRef = "refs/remotes/origin/$($env:SOURCE_BRANCH)"
& $git merge-base --is-ancestor $commit $branchRef
if ($LASTEXITCODE -ne 0) {
throw "COMMIT_HASH 不属于 SOURCE_BRANCH: $($env:SOURCE_BRANCH)"
}
& $git checkout --detach $commit
}
$resolved = (& $git rev-parse HEAD).Trim()
Set-Content -Path '.jenkins-source-commit' -Value $resolved -NoNewline
Write-Host "源码 commit=$resolved"
'''
}
}
}
stage('Toolchain preflight') {
steps {
withEnv(["PATH=${env.AGC_WINDOWS_PATH}", "OSSUTIL_BIN=${params.OSSUTIL_BIN}"]) {
powershell '''
$ErrorActionPreference = 'Stop'
$actualNpm = (npm --version).Trim()
if ($actualNpm -ne $env:GENARRATIVE_NPM_VERSION) {
throw "npm 版本不匹配:期望 $($env:GENARRATIVE_NPM_VERSION),实际 $actualNpm"
}
foreach ($commandName in @('node', 'npm', 'rustc', 'cargo', 'dotnet', 'cmake', 'python')) {
if (-not (Get-Command $commandName -ErrorAction SilentlyContinue)) {
throw "Windows AGC 节点缺少 $commandName,请安装并加入 AGC_WINDOWS_PATH。"
}
}
$dotnetSdks = @(dotnet --list-sdks)
if ($LASTEXITCODE -ne 0 -or -not ($dotnetSdks -match '^10[.]')) {
throw 'Windows AGC 节点需要 .NET 10 SDK 才能构建 Unity Attach helper。'
}
$cmakeVersion = ((cmake --version) | Select-Object -First 1).Trim()
if ($cmakeVersion -notmatch '([0-9]+)[.]([0-9]+)[.]([0-9]+)') {
throw "无法解析 CMake 版本:$cmakeVersion"
}
$cmakeMajor = [int]$Matches[1]
$cmakeMinor = [int]$Matches[2]
if ($cmakeMajor -lt 3 -or ($cmakeMajor -eq 3 -and $cmakeMinor -lt 25)) {
throw "Godot 原生扩展需要 CMake 3.25 及以上,当前:$cmakeVersion"
}
if (-not (@(cmake --help) -match 'Visual Studio 17 2022')) {
throw "Windows AGC 节点缺少 Visual Studio 2022 C++ 生成器,无法构建 Godot 原生扩展。"
}
$pythonVersion = ((python --version 2>&1) | Select-Object -First 1).Trim()
if ($pythonVersion -notmatch '^Python 3[.]') {
throw "Godot 原生扩展需要 Python 3,当前:$pythonVersion"
}
$ossutil = ([string]$env:OSSUTIL_BIN).Trim()
if (-not $ossutil) { $ossutil = 'ossutil' }
if (-not (Get-Command $ossutil -ErrorAction SilentlyContinue)) {
throw "Windows AGC 节点缺少 ossutil:$ossutil"
}
Write-Host "node=$((node --version).Trim())"
Write-Host "npm=$actualNpm"
Write-Host "rustc=$((rustc --version).Trim())"
Write-Host "cargo=$((cargo --version).Trim())"
Write-Host "dotnet-sdk=$((dotnet --version).Trim())"
Write-Host "cmake=$cmakeVersion"
Write-Host "python=$pythonVersion"
Write-Host "jenkins-user=$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Host "local-app-data=$env:LOCALAPPDATA"
$targetTools = Join-Path $PWD 'apps/ai-game-creator-shell/src-tauri/target/.tauri'
New-Item -ItemType Directory -Force -Path $targetTools | Out-Null
$probe = Join-Path $targetTools '.jenkins-write-probe'
Set-Content -Path $probe -Value 'ok' -NoNewline
Remove-Item -LiteralPath $probe -Force
Write-Host "tauri-tools-dir=$targetTools"
'''
}
}
}
stage('Install dependencies') {
steps {
withEnv(["PATH=${env.AGC_WINDOWS_PATH}"]) {
powershell '''
$ErrorActionPreference = 'Stop'
npm ci --no-audit --no-fund
'''
}
}
}
stage('Tauri NSIS toolchain') {
steps {
withEnv(["PATH=${env.AGC_WINDOWS_PATH}"]) {
// 必须早于 Rust 编译与打包:Tauri bundler 自己下载 NSIS 工具链时不重试,
// 响应截断只会报 `io: unexpected end of file`,让发布在数分钟后才失败。
powershell '''
$ErrorActionPreference = 'Stop'
node --test apps/ai-game-creator-shell/scripts/nsis-toolset.test.mjs
if ($LASTEXITCODE -ne 0) { throw "NSIS 工具链回归测试失败,退出码=$LASTEXITCODE" }
node apps/ai-game-creator-shell/scripts/ensure-nsis-toolset.mjs
if ($LASTEXITCODE -ne 0) { throw "NSIS 工具链预置失败,退出码=$LASTEXITCODE" }
$nsis = Join-Path $PWD 'apps/ai-game-creator-shell/src-tauri/target/.tauri/NSIS/makensis.exe'
if (-not (Test-Path $nsis)) { throw "NSIS makensis.exe 缺失:$nsis" }
& $nsis -VERSION
if ($LASTEXITCODE -ne 0) { throw "NSIS makensis.exe 无法执行,退出码=$LASTEXITCODE" }
'''
}
}
}
stage('Build and upload') {
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([
"PATH=${env.AGC_WINDOWS_PATH}",
"OSSUTIL_BIN=${params.OSSUTIL_BIN}",
"AGC_RELEASE_VERSION=${params.AGC_RELEASE_VERSION}",
"AGC_UPDATE_CHANNEL=${params.AGC_UPDATE_CHANNEL}",
'AGC_BUILD_TARGET=x86_64-pc-windows-msvc',
"AGC_RELEASE_DRY_RUN=${params.AGC_RELEASE_DRY_RUN ? '1' : '0'}",
"AGC_UPDATE_RELEASE_NOTES=${params.AGC_UPDATE_RELEASE_NOTES}",
]) {
powershell '''
$ErrorActionPreference = 'Stop'
npm run ai-game-creator-shell:release:upload
$buildExitCode = $LASTEXITCODE
if ($buildExitCode -ne 0) {
$nsis = Join-Path $PWD 'apps/ai-game-creator-shell/src-tauri/target/.tauri/NSIS/makensis.exe'
Write-Host "Tauri NSIS path=$nsis"
if (Test-Path $nsis) {
Get-Item $nsis | Select-Object FullName, Length, LastWriteTime
try { & $nsis -VERSION } catch { Write-Host "无法直接启动 Tauri NSIS:$($_.Exception.Message)" }
} else {
Write-Host 'Tauri NSIS makensis.exe 不存在。'
}
throw "AGC 发布构建失败,退出码=$buildExitCode"
}
'''
}
}
}
}
stage('Archive release') {
steps {
archiveArtifacts artifacts: 'apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.exe,apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.sig,apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json,apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/legacy-latest.json,apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/release-notes.txt,.jenkins-source-commit', fingerprint: true
}
}
}
post {
always {
script {
def ossDownloadUrl = ''
def releaseVersion = params.AGC_RELEASE_VERSION ?: ''
def sourceCommit = params.COMMIT_HASH ?: ''
def manifestPath = 'apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/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'}-win"),
string(name: 'OSS_DOWNLOAD_URL', value: ossDownloadUrl),
string(name: 'SUMMARY', value: params.AGC_RELEASE_DRY_RUN
? 'AGC Windows 渠道演练结束,未写入 OSS'
: 'AGC Windows 渠道构建与 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}"
}
}
}
success {
echo params.AGC_RELEASE_DRY_RUN
? "AGC ${params.AGC_UPDATE_CHANNEL} 渠道演练完成:已构建并生成清单,未写入 OSS。"
: "AGC ${params.AGC_UPDATE_CHANNEL} 渠道安装包、签名与渠道清单已构建并上传 OSS。"
}
}
}