Files
Genarrative/jenkins/Jenkinsfile.scheduled-revision-trigger
suzmii 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渠道纳入定时发布并共用总版本号
调度器在发号后同时触发macOS渠道构建,透传同一个总版本号与固定revision
macOS Job改为默认真实发布,演练需显式勾选
新增SKIP_IF_SUPERSEDED:节点离线期间排队的旧构建在恢复后自行让位
补充生产门禁规则与回归断言
同步调度与运维文档口径
2026-09-20 22:47:01 +08:00

218 lines
11 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.
// 定时版本调度管线:解析远端分支版本,只有和上一次触发过的版本不同时,
// 才用同一个固定 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 ?: ''}"
}
}
}