新增定时版本调度管线并收回两条下游管线的定时与去重
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m9s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / AI game creator shell Rust crates (push) Successful in 2m36s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m20s
Project CI / Frontend tests (push) Successful in 4m23s
Project CI / Repository checks (push) Successful in 4m13s
Project CI / Native shell tests (push) Successful in 6m44s
Project CI / Backend tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 3m28s

- 新增 Genarrative-Scheduled-Revision-Trigger:每小时用 git ls-remote 解析分支版本,与上一次触发过的 revision 不同才继续
- 触发时把同一个固定 COMMIT_HASH 同时传给 Full Build 与 AGC Windows Build,保证两条管线构建同一个版本
- Full Build 与 AGC Windows Build 去掉自带定时触发器与管线内版本去重
- 新增 scheduled-revision-trigger Job 配置 XML,使用本机 Git 入口与既有 SSH 凭据,定时器留在 Jenkinsfile
- 生产运维门禁改为校验调度管线约束,并拦住两条下游管线重新加 cron 或自行去重
- 同步开发运维文档、共享开发工作流与踩坑记录
This commit is contained in:
kdletters
2026-09-16 10:55:16 +08:00
parent 5aa616134c
commit 576ff07a5e
8 changed files with 226 additions and 194 deletions
+78 -87
View File
@@ -7639,107 +7639,98 @@ const fullPipelineContent = readFileSync(
'jenkins/Jenkinsfile.production-full-build-and-deploy',
'utf8',
);
for (const stageName of [
'Build Components',
'Deploy Stdb',
'Stdb / Api Rollout Gate',
'Deploy Api',
'Deploy Web',
'Exit Maintenance',
]) {
const stageStart = fullPipelineContent.indexOf(`stage('${stageName}')`);
const stageBody =
stageStart >= 0 ? fullPipelineContent.slice(stageStart) : '';
const stageConditions = stageBody.split(/\b(?:steps|parallel)\s*\{/u)[0];
if (
!stageConditions.includes(
"expression { return env.SKIP_UNCHANGED_BUILD != 'true' }",
)
) {
failed = true;
console.error(
`[check:production-ops] Full Build 的 ${stageName} 必须在版本未变化时跳过,禁止触发下游构建、部署或维护操作。`,
);
}
}
for (const [snippet, reason] of [
["cron('H * * * *')", '必须每小时检查源码变化'],
[
"causes.any { it._class != 'hudson.triggers.TimerTrigger$TimerTriggerCause' }",
'去重只允许纯 timer 触发,手动或重放必须执行',
],
[
"previous.result == 'NOT_BUILT' && variables.SKIP_UNCHANGED_BUILD == 'true'",
'只允许越过本逻辑标记的跳过记录',
],
[
"previous.result == 'SUCCESS' && variables.SOURCE_COMMIT == sourceCommit && variables.FULL_BUILD_INPUTS == buildInputs",
'必须同时验证最近实际运行成功、源码相同和参数相同',
],
[
"groovy.json.JsonOutput.toJson(params.findAll { name, value -> name != 'NOTIFICATION_EMAILS' }.sort())",
'必须稳定记录全部构建发布参数,仅排除通知邮箱',
],
[
"currentBuild.result = 'NOT_BUILT'",
'跳过记录必须为 NOT_BUILT,不能污染成功基线',
],
]) {
if (!fullPipelineContent.includes(snippet)) {
failed = true;
console.error(`[check:production-ops] Full Build ${reason}`);
}
}
if (
!/always\s*\{\s*script\s*\{\s*if \(env\.SKIP_UNCHANGED_BUILD == 'true'\)\s*\{\s*return\s*\}/u.test(
fullPipelineContent,
)
) {
failed = true;
console.error(
'[check:production-ops] Full Build 跳过时不得触发结束邮件通知。',
);
}
const agcPipelineContent = readFileSync(
'jenkins/Jenkinsfile.ai-game-creator-shell-build',
'utf8',
);
for (const snippet of [
"cron('H * * * *')",
"causes.any { it._class != 'hudson.triggers.TimerTrigger$TimerTriggerCause' }",
"previous.result == 'NOT_BUILT' && variables.SKIP_UNCHANGED_BUILD == 'true'",
"previous.result == 'SUCCESS' && variables.SOURCE_COMMIT == sourceCommit && variables.FULL_BUILD_INPUTS == buildInputs",
'env.FULL_BUILD_INPUTS = groovy.json.JsonOutput.toJson(params.sort())',
"currentBuild.result = 'NOT_BUILT'",
const scheduledRevisionTriggerContent = readFileSync(
'jenkins/Jenkinsfile.scheduled-revision-trigger',
'utf8',
);
const scheduledRevisionTriggerJobConfig = readFileSync(
'jenkins/scheduled-revision-trigger-job-config.xml',
'utf8',
);
// 定时与版本比较统一收敛到调度管线,两个下游流水线不得再自带触发器。
for (const [file, content] of [
['jenkins/Jenkinsfile.production-full-build-and-deploy', fullPipelineContent],
['jenkins/Jenkinsfile.ai-game-creator-shell-build', agcPipelineContent],
]) {
if (!agcPipelineContent.includes(snippet)) {
if (/\btriggers\s*\{/u.test(content) || content.includes('cron(')) {
failed = true;
console.error(
`[check:production-ops] AGC Windows Build 缺少版本未变化去重约束: ${snippet}`,
`[check:production-ops] ${file} 不得自带定时触发器;版本检查与触发必须由 Genarrative-Scheduled-Revision-Trigger 统一负责。`,
);
}
}
for (const agcStageName of [
'Toolchain preflight',
'Install dependencies',
'Build and upload',
'Archive release',
for (const [snippet, reason] of [
["cron('H * * * *')", '必须每小时检查一次远端版本'],
['disableConcurrentBuilds()', '必须禁止并发触发,避免同一版本重复触发下游'],
['skipDefaultCheckout(true)', '不得依赖本地 SCM 工作区'],
['git ls-remote', '必须直接从远端解析分支版本'],
['> .jenkins-remote-revision', '解析出的版本必须先落盘再参与比较'],
['fileExists(env.REVISION_STATE_FILE)', '必须读取上一次触发过的版本'],
[
"currentBuild.result = 'NOT_BUILT'",
'版本未变化时必须标记为未触发,不能静默成功',
],
[
"FULL_BUILD_JOB_NAME = 'Genarrative-Full-Build-And-Deploy'",
'必须声明 Full Build 目标 Job',
],
[
"AGC_BUILD_JOB_NAME = 'Genarrative-Agc-Windows-Build'",
'必须声明 AGC Windows Build 目标 Job',
],
[
"string(name: 'COMMIT_HASH', value: pinnedRevision)",
'必须把同一个固定 revision 传给两个下游管线',
],
]) {
const agcStageStart = agcPipelineContent.indexOf(`stage('${agcStageName}')`);
const agcStageBody =
agcStageStart >= 0 ? agcPipelineContent.slice(agcStageStart) : '';
const agcStageConditions = agcStageBody.split(/\bsteps\s*\{/u)[0];
if (
!agcStageConditions.includes(
"expression { return env.SKIP_UNCHANGED_BUILD != 'true' }",
)
) {
if (!scheduledRevisionTriggerContent.includes(snippet)) {
failed = true;
console.error(
`[check:production-ops] AGC Windows Build 的 ${agcStageName} 必须在版本未变化时跳过。`,
);
console.error(`[check:production-ops] 调度管线${reason}`);
}
}
const scheduledPinnedParameterCalls = scheduledRevisionTriggerContent.match(
/parameters: pinnedParameters/gu,
);
if ((scheduledPinnedParameterCalls?.length ?? 0) !== 2) {
failed = true;
console.error(
'[check:production-ops] 调度管线必须用同一份 pinnedParameters 同时触发 Full Build 与 AGC Windows Build。',
);
}
if (
!scheduledRevisionTriggerJobConfig.includes(
'<scriptPath>jenkins/Jenkinsfile.scheduled-revision-trigger</scriptPath>',
) ||
!scheduledRevisionTriggerJobConfig.includes(
'ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git',
) ||
!scheduledRevisionTriggerJobConfig.includes(
'<credentialsId>genarrative-local-gitea-ssh</credentialsId>',
) ||
!scheduledRevisionTriggerJobConfig.includes('<triggers/>')
) {
failed = true;
console.error(
'[check:production-ops] scheduled-revision-trigger Job 必须指向调度 Jenkinsfile、使用本机 Git 入口与既有 SSH 凭据,并把定时器留在 Jenkinsfile。',
);
}
if (
!fullPipelineContent.includes(
"string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT)",
) ||
!agcPipelineContent.includes('checkout --detach $commit')
) {
failed = true;
console.error(
'[check:production-ops] Full Build 与 AGC Windows Build 必须按上游传入的固定 commit 构建同一个版本。',
);
}
const forcedBuildOnlyCalls = fullPipelineContent.match(
/booleanParam\(name: 'PUBLISH_AFTER_BUILD', value: false\)/gu,
);