From cbcbb9c76461e9766054750b96ae37c9510544dd Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 26 Aug 2026 18:01:47 +0800 Subject: [PATCH] fix(ci): make AUTO deployment incremental --- Jenkinsfile | 123 +++++++++++++++--- ...est-environment-pipeline-contract.test.mjs | 24 ++++ 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a06ba1017..1c944c197 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -21,16 +21,77 @@ def serviceCheckboxes = [ 'audit-service': 'SERVICE_AUDIT' ] -def commonPaths = [ - 'pom.xml', 'platform-bom/', 'common-core/', 'common-web/', 'common-security/', - 'common-openapi/', 'common-audit/', 'common-event-contract/', 'Jenkinsfile', - 'Jenkinsfile.service', 'deploy/' +def fullRebuildPaths = [ + 'pom.xml', 'platform-bom/', 'common-core/', 'deploy/docker/', + 'deploy/docker-compose.yml', 'deploy/service-map.yml' +] + +def commonPathServices = [ + 'common-web/': [ + 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print', + 'integration-service', 'lastleg-service', 'warehouse-overseas-service', + 'event-bridge-service', 'audit-service' + ], + 'common-security/': [ + 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', + 'lastleg-service', 'warehouse-overseas-service', 'audit-service' + ], + 'common-openapi/': [ + 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print', + 'integration-service', 'lastleg-service', 'warehouse-overseas-service', + 'event-bridge-service', 'audit-service' + ], + 'common-audit/': [ + 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', + 'warehouse-overseas-service', 'audit-service' + ], + 'common-event-contract/': [ + 'firstleg', 'iam', 'lastleg-service', 'event-bridge-service' + ], + 'deploy/docker-compose.overseas.yml': [ + 'gateway-overseas', 'lastleg-service', 'warehouse-overseas-service', 'event-bridge-service' + ], + 'deploy/docker-compose.audit.yml': ['audit-service'], + 'deploy/docker-compose.rocketmq.yml': ['event-bridge-service'] ] def databaseMigrationPaths = ['deploy/sql/'] def downstreamResults = [:] +def parseAutoBuildState(String description, List allowedServices) { + String marker = ' | RESULTS' + if (!description?.startsWith('AUTO ') || !description.contains(marker)) return null + int markerIndex = description.indexOf(marker) + String commit = description.substring('AUTO '.length(), markerIndex).trim() + if (!(commit ==~ /[0-9a-fA-F]{7,40}/)) return null + String summary = description.substring(markerIndex + marker.length()).trim() + List failedServices = [] + if (summary) { + summary.split(',').collect { it.trim() }.findAll { it }.each { item -> + int separator = item.indexOf('=') + if (separator > 0) { + String service = item.substring(0, separator).trim() + String result = item.substring(separator + 1).trim() + if (allowedServices.contains(service) && result != 'SUCCESS') failedServices << service + } + } + } + return [commit: commit, failedServices: failedServices.unique()] +} + +def previousAutoBuildState(def previousBuild, List allowedServices) { + def candidate = previousBuild + int inspected = 0 + while (candidate != null && inspected < 30) { + def state = parseAutoBuildState(candidate.description, allowedServices) + if (state != null && ['SUCCESS', 'UNSTABLE'].contains(candidate.result)) return state + candidate = candidate.previousBuild + inspected++ + } + return null +} + def environmentChoicesForJob(String jobBaseName) { return jobBaseName == 'backend-java-dev' ? ['development', 'test', 'staging', 'production'] @@ -112,6 +173,8 @@ pipeline { error('生产环境禁止跳过测试') } + env.SOURCE_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + env.IMAGE_TAG = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim() List selected = [] if (params.BUILD_MODE == 'ALL') { selected = serviceModules @@ -123,9 +186,17 @@ pipeline { error('SELECT 模式至少需要勾选一个服务') } } else { - String base = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT?.trim() + def autoState = previousAutoBuildState(currentBuild.previousBuild, serviceModules) + String base = autoState?.commit?.trim() + List retryServices = autoState?.failedServices ?: [] + if (!base) { + // 首次启用新算法时兼容旧构建记录;后续只读取标记为 AUTO 的完成记录, + // SELECT/ALL 不会推进 AUTO 基线。 + base = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT?.trim() + } if (!base || sh(script: "git cat-file -e ${base}^{commit} 2>/dev/null", returnStatus: true) != 0) { selected = serviceModules + echo 'AUTO 未找到可用基线,安全回退为全量构建' } else { List changed = sh(script: "git diff --name-only ${base} HEAD", returnStdout: true) .trim().split('\\n').collect { it.trim() }.findAll { it } @@ -133,13 +204,27 @@ pipeline { databaseMigrationPaths.any { prefix -> path.startsWith(prefix) } } env.DATABASE_CHANGE_DETECTED = databaseChanged ? 'true' : 'false' - boolean commonChanged = changed.any { path -> - boolean databaseMigration = databaseMigrationPaths.any { prefix -> path.startsWith(prefix) } - !databaseMigration && commonPaths.any { prefix -> path == prefix || path.startsWith(prefix) } + List serviceRelevantChanges = changed.findAll { path -> + !databaseMigrationPaths.any { prefix -> path.startsWith(prefix) } } - selected = commonChanged ? serviceModules : serviceModules.findAll { module -> - changed.any { it == module || it.startsWith("${module}/") } + boolean fullRebuild = serviceRelevantChanges.any { path -> + fullRebuildPaths.any { prefix -> path == prefix || path.startsWith(prefix) } } + if (fullRebuild) { + selected = serviceModules + } else { + selected = serviceModules.findAll { module -> + serviceRelevantChanges.any { it == module || it.startsWith("${module}/") } + } + commonPathServices.each { prefix, impactedServices -> + if (serviceRelevantChanges.any { path -> path == prefix || path.startsWith(prefix) }) { + selected.addAll(impactedServices) + } + } + } + selected.addAll(retryServices) + echo "AUTO 基线: ${base};变更选中: ${selected.unique().join(',') ?: '无'};" + + "补跑失败服务: ${retryServices.join(',') ?: '无'}" } } @@ -165,14 +250,16 @@ pipeline { env.SELECTED_SERVICES = selected.join(',') env.NO_SERVICE_CHANGE = selected ? 'false' : 'true' if (!selected) { - currentBuild.description = env.DATABASE_CHANGE_DETECTED == 'true' - ? '仅数据库迁移变更 | 需单独审核执行' - : '没有需要构建的业务服务' + currentBuild.description = params.BUILD_MODE == 'AUTO' + ? "AUTO ${env.IMAGE_TAG} | RESULTS" + : (env.DATABASE_CHANGE_DETECTED == 'true' + ? '仅数据库迁移变更 | 需单独审核执行' + : '没有需要构建的业务服务') } else { - env.SOURCE_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() - env.IMAGE_TAG = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim() String databaseSuffix = env.DATABASE_CHANGE_DETECTED == 'true' ? ' | DB migration pending' : '' - currentBuild.description = "${env.SELECTED_SERVICES} | ${env.IMAGE_TAG}${databaseSuffix}" + currentBuild.description = params.BUILD_MODE == 'AUTO' + ? "AUTO ${env.IMAGE_TAG} | PENDING ${env.SELECTED_SERVICES}${databaseSuffix}" + : "${env.SELECTED_SERVICES} | ${env.IMAGE_TAG}${databaseSuffix}" echo "本次独立调度服务: ${env.SELECTED_SERVICES}" } if (env.DATABASE_CHANGE_DETECTED == 'true') { @@ -273,7 +360,9 @@ pipeline { .collect { service, result -> "${service}=${result}" } String summary = downstreamResults.collect { service, result -> "${service}=${result}" }.join(', ') echo "单服务执行汇总: ${summary}" - currentBuild.description = "${env.IMAGE_TAG} | ${summary}" + currentBuild.description = params.BUILD_MODE == 'AUTO' + ? "AUTO ${env.IMAGE_TAG} | RESULTS ${summary}" + : "${env.IMAGE_TAG} | ${summary}" if (failed) { unstable("部分服务执行失败,但其他服务已独立完成:${failed.join(', ')}") } diff --git a/scripts/test-environment-pipeline-contract.test.mjs b/scripts/test-environment-pipeline-contract.test.mjs index 2215862cb..70c1ea8f9 100644 --- a/scripts/test-environment-pipeline-contract.test.mjs +++ b/scripts/test-environment-pipeline-contract.test.mjs @@ -37,6 +37,30 @@ test('coordinator jobs present their own deployment environment as the default c assert.match(coordinatorPipeline, /downstreamJobPrefixForJob\(env\.JOB_BASE_NAME\)/) }) +test('AUTO keeps its own baseline and retries only previously failed services', () => { + assert.match(coordinatorPipeline, /def parseAutoBuildState\(/) + assert.match(coordinatorPipeline, /def previousAutoBuildState\(/) + assert.match(coordinatorPipeline, /previousAutoBuildState\(currentBuild\.previousBuild, serviceModules\)/) + assert.match(coordinatorPipeline, /selected\.addAll\(retryServices\)/) + assert.match(coordinatorPipeline, /AUTO \$\{env\.IMAGE_TAG\} \| PENDING/) + assert.match(coordinatorPipeline, /AUTO \$\{env\.IMAGE_TAG\} \| RESULTS/) + assert.doesNotMatch(coordinatorPipeline, /commonChanged \? serviceModules/) +}) + +test('AUTO maps shared modules to their real consumers instead of rebuilding every service', () => { + const securityImpact = coordinatorPipeline.slice( + coordinatorPipeline.indexOf("'common-security/': ["), + coordinatorPipeline.indexOf("'common-openapi/': [") + ) + assert.match(coordinatorPipeline, /def fullRebuildPaths = \[/) + assert.match(coordinatorPipeline, /'common-security\/': \[/) + assert.match(coordinatorPipeline, /'common-event-contract\/': \[/) + assert.match(coordinatorPipeline, /commonPathServices\.each \{ prefix, impactedServices ->/) + assert.match(coordinatorPipeline, /selected\.addAll\(impactedServices\)/) + assert.match(securityImpact, /'auth'.*'finance'.*'file'/s) + assert.doesNotMatch(securityImpact, /gateway|print|integration-service|event-bridge-service/) +}) + test('production deployments cannot skip tests', () => { assert.match(coordinatorPipeline, /env\.RESOLVED_ENVIRONMENT == 'production' && params\.SKIP_TESTS/) assert.match(coordinatorPipeline, /error\('生产环境禁止跳过测试'\)/)