新增定时版本调度管线并收回两条下游管线的定时与去重
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
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:
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user