Jenkins 定时构建增加版本未变化去重

- Full Build 改为每小时触发,并按已解析 commit 与全部构建参数判断是否重复
- AGC Windows Build 增加每小时触发与同一套去重逻辑,跳过时不再安装依赖、构建和上传
- 跳过记录为 NOT_BUILT,越过跳过记录后最近一次实际构建成功才允许跳过,失败或中断可重试
- 生产运维门禁补充两个 Job 的去重约束与各阶段跳过检查
- 同步开发运维文档与共享开发工作流说明
This commit is contained in:
kdletters
2026-09-16 10:32:03 +08:00
parent f7a6235012
commit 187b66c3b1
5 changed files with 209 additions and 3 deletions
+102 -1
View File
@@ -605,7 +605,7 @@ const checks = [
includes:
"choice(name: 'STDB_API_ROLLOUT_MODE', choices: ['normal', 'pause-after-stdb']",
reason:
'Full Build 的 04:00 定时任务必须默认 normal 完整发布仅供开发使用的 dev 服务器。',
'Full Build 的每小时定时任务必须默认 normal 完整发布仅供开发使用的 dev 服务器。',
},
{
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
@@ -7639,6 +7639,107 @@ 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'",
]) {
if (!agcPipelineContent.includes(snippet)) {
failed = true;
console.error(
`[check:production-ops] AGC Windows Build 缺少版本未变化去重约束: ${snippet}`,
);
}
}
for (const agcStageName of [
'Toolchain preflight',
'Install dependencies',
'Build and upload',
'Archive release',
]) {
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' }",
)
) {
failed = true;
console.error(
`[check:production-ops] AGC Windows Build 的 ${agcStageName} 必须在版本未变化时跳过。`,
);
}
}
const forcedBuildOnlyCalls = fullPipelineContent.match(
/booleanParam\(name: 'PUBLISH_AFTER_BUILD', value: false\)/gu,
);