dbbbef8a0d
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
调度器在发号后同时触发macOS渠道构建,透传同一个总版本号与固定revision macOS Job改为默认真实发布,演练需显式勾选 新增SKIP_IF_SUPERSEDED:节点离线期间排队的旧构建在恢复后自行让位 补充生产门禁规则与回归断言 同步调度与运维文档口径
218 lines
11 KiB
Plaintext
218 lines
11 KiB
Plaintext
// 定时版本调度管线:解析远端分支版本,只有和上一次触发过的版本不同时,
|
||
// 才用同一个固定 commit 触发 Full Build、AGC 发号与 AGC Windows/macOS 渠道构建。
|
||
pipeline {
|
||
agent {
|
||
label 'linux && genarrative-build'
|
||
}
|
||
|
||
options {
|
||
disableConcurrentBuilds()
|
||
skipDefaultCheckout(true)
|
||
buildDiscarder(logRotator(numToKeepStr: '200', artifactNumToKeepStr: '10'))
|
||
}
|
||
|
||
triggers {
|
||
cron('H * * * *')
|
||
}
|
||
|
||
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_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'
|
||
AGC_SCOPE_CACHE_DIR = '.agc-release-scope-cache'
|
||
}
|
||
|
||
parameters {
|
||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '被检查的源码分支;默认 master')
|
||
booleanParam(name: 'FORCE_TRIGGER', defaultValue: false, description: '勾选后忽略版本比较,强制触发两个下游管线')
|
||
}
|
||
|
||
stages {
|
||
stage('Resolve Revision') {
|
||
steps {
|
||
withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) {
|
||
sh '''#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
export GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY:?缺少 Git SSH 凭据} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
||
revision="$(git ls-remote "${GIT_REMOTE_URL:?缺少 GIT_REMOTE_URL}" "refs/heads/${SOURCE_BRANCH}" | awk 'NR == 1 { print $1 }')"
|
||
if [[ ! "${revision}" =~ ^[0-9a-f]{40}$ ]]; then
|
||
echo "无法解析远端分支版本: branch=${SOURCE_BRANCH} revision=${revision:-<empty>}" >&2
|
||
exit 1
|
||
fi
|
||
printf '%s' "${revision}" > .jenkins-remote-revision
|
||
echo "远端版本: branch=${SOURCE_BRANCH} revision=${revision}"
|
||
'''
|
||
}
|
||
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'
|
||
if (env.REVISION_CHANGED != 'true') {
|
||
currentBuild.result = 'NOT_BUILT'
|
||
currentBuild.description = "版本未变化,跳过:${env.SOURCE_BRANCH} 仍是 ${env.REMOTE_REVISION.take(12)}"
|
||
echo currentBuild.description
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 按「本轮到达的提交」判定两条下游各自是否需要跑:
|
||
// - AGC:只有出现客户端相关路径才发布 Windows 客户端(避免纯文档提交推高版本号)。
|
||
// - Full Build:只在改动全部落在「与线上站点/后端无关」的路径时跳过(fail-open 到部署)。
|
||
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\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\nfull=changed\n'
|
||
exit 0
|
||
fi
|
||
mkdir -p "${AGC_SCOPE_CACHE_DIR}"
|
||
if [[ ! -d "${AGC_SCOPE_CACHE_DIR}/.git" ]]; then
|
||
git -C "${AGC_SCOPE_CACHE_DIR}" init --quiet
|
||
git -C "${AGC_SCOPE_CACHE_DIR}" remote add origin "${GIT_REMOTE_URL}" 2>/dev/null || true
|
||
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\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\nfull=changed\n'
|
||
exit 0
|
||
fi
|
||
changed_paths="$(git -C "${AGC_SCOPE_CACHE_DIR}" diff --name-only "${previous}" "${REMOTE_REVISION}" 2>/dev/null || true)"
|
||
agc_scope=unchanged
|
||
full_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
|
||
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 <<< "${changed_paths}"
|
||
printf 'agc=%s\nfull=%s\n' "${agc_scope}" "${full_scope}"
|
||
''', returnStdout: true).trim()
|
||
} catch (error) {
|
||
echo "发布范围判定失败,按需要发布处理:${error}"
|
||
output = 'agc=changed\nfull=changed'
|
||
}
|
||
def values = output.split('\n').collect { it.trim() }.findAll { it }
|
||
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 "发布范围:AGC=${env.AGC_RELEASE_SCOPE} FullBuild=${env.FULL_BUILD_SCOPE}(上一轮已触发 revision=${env.LAST_TRIGGERED_REVISION ?: '无'})"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Trigger Downstream Pipelines') {
|
||
when {
|
||
expression { return env.REVISION_CHANGED == 'true' }
|
||
}
|
||
steps {
|
||
script {
|
||
// 两个下游管线必须拿到同一个固定 revision,避免各自解析分支 HEAD 造成版本漂移。
|
||
def pinnedRevision = env.REMOTE_REVISION
|
||
def pinnedParameters = [
|
||
string(name: 'SOURCE_BRANCH', value: env.SOURCE_BRANCH),
|
||
string(name: 'COMMIT_HASH', value: pinnedRevision),
|
||
string(name: 'DATABASE_BACKUP_MODE', value: 'skip'),
|
||
]
|
||
def fullTriggered = false
|
||
if (params.FORCE_TRIGGER || env.FULL_BUILD_SCOPE != 'unchanged') {
|
||
build job: env.FULL_BUILD_JOB_NAME, wait: false, propagate: false, parameters: pinnedParameters
|
||
fullTriggered = true
|
||
} else {
|
||
echo "本轮提交全部与线上站点/后端无关,跳过 ${env.FULL_BUILD_JOB_NAME};需要强制发布时勾选 FORCE_TRIGGER"
|
||
}
|
||
def agcTriggered = false
|
||
if (params.FORCE_TRIGGER || env.AGC_RELEASE_SCOPE != 'unchanged') {
|
||
// 客户端版本号只在发号 Job 里产生:先发号,再让渠道构建接收号并发布。
|
||
def versionRun = build job: env.AGC_GLOBAL_VERSION_JOB_NAME,
|
||
wait: true,
|
||
propagate: true,
|
||
parameters: [
|
||
string(name: 'SOURCE_BRANCH', value: env.SOURCE_BRANCH),
|
||
string(name: 'COMMIT_HASH', value: pinnedRevision),
|
||
string(name: 'AGC_CHANNEL', value: 'dev-win'),
|
||
]
|
||
copyArtifacts(
|
||
projectName: env.AGC_GLOBAL_VERSION_JOB_NAME,
|
||
selector: specific(versionRun.number.toString()),
|
||
filter: env.AGC_GLOBAL_VERSION_ARTIFACT,
|
||
target: '.',
|
||
flatten: true,
|
||
)
|
||
env.AGC_GLOBAL_VERSION = readFile(env.AGC_GLOBAL_VERSION_ARTIFACT).trim()
|
||
echo "本轮 AGC 客户端版本号: ${env.AGC_GLOBAL_VERSION}"
|
||
def agcParameters = pinnedParameters + [
|
||
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"
|
||
}
|
||
writeFile file: env.REVISION_STATE_FILE, text: pinnedRevision
|
||
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}"
|
||
: "已触发 ${triggered.join(' 与 ')}:${target}"
|
||
echo currentBuild.description
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
post {
|
||
always {
|
||
echo "调度结束: branch=${env.SOURCE_BRANCH} revision=${env.REMOTE_REVISION ?: ''} changed=${env.REVISION_CHANGED ?: ''}"
|
||
}
|
||
}
|
||
}
|