Files
baiyue-firstleg/scripts/test-environment-pipeline-contract.test.mjs
T

253 lines
15 KiB
JavaScript
Raw Normal View History

2026-08-13 17:40:55 +08:00
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
const pipeline = (await readFile(new URL('../Jenkinsfile.service', import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
2026-08-20 13:00:39 +08:00
const coordinatorPipeline = (await readFile(new URL('../Jenkinsfile', import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
2026-08-20 12:49:54 +08:00
const directDeployScript = (await readFile(new URL('../deploy/scripts/deploy-built-service.sh', import.meta.url), 'utf8'))
2026-08-20 10:22:51 +08:00
.replaceAll('\r\n', '\n')
2026-08-20 10:28:45 +08:00
const ciMavenSettings = (await readFile(new URL('../deploy/maven/settings-ci.xml', import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
2026-08-22 12:20:26 +08:00
const serviceDockerfile = (await readFile(new URL('../deploy/docker/Dockerfile', import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
const serviceCompose = (await readFile(new URL('../deploy/docker-compose.yml', import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
const overseasCompose = (await readFile(new URL('../deploy/docker-compose.overseas.yml', import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
2026-08-13 17:40:55 +08:00
test('service pipeline keeps boolean operators on parseable Groovy continuation lines', () => {
assert.doesNotMatch(pipeline, /^\s*(?:&&|\|\|)/m)
})
2026-08-13 17:40:55 +08:00
test('test deployments compile packages without rerunning the full unit test suite', () => {
2026-09-03 17:43:52 +08:00
assert.match(pipeline, /env\.RESOLVED_ENVIRONMENT == 'test' \|\| params\.SKIP_TESTS/)
2026-08-13 17:40:55 +08:00
assert.match(pipeline, /skipTestExecution \? '-DskipTests' : ''/)
assert.match(pipeline, /-pl \$\{env\.RESOLVED_SERVICE\} -am clean package/)
assert.doesNotMatch(pipeline, /-Dmaven\.test\.skip=true/)
})
2026-09-03 17:43:52 +08:00
test('coordinator and service jobs enforce their own deployment environments', () => {
assert.match(coordinatorPipeline, /case 'backend-java-dev':[\s\S]*?return 'development'/)
assert.match(coordinatorPipeline, /case 'backend-java':[\s\S]*?return 'test'/)
assert.match(coordinatorPipeline, /case 'backend-java-prod':[\s\S]*?return 'production'/)
2026-08-20 13:00:39 +08:00
assert.match(coordinatorPipeline, /environmentChoicesForJob\(env\.JOB_BASE_NAME\)/)
2026-09-03 17:43:52 +08:00
assert.match(coordinatorPipeline, /env\.RESOLVED_ENVIRONMENT = resolvedEnvironment/)
assert.match(coordinatorPipeline, /env\.RESOLVED_JOB_PREFIX = resolvedJobPrefix/)
assert.match(pipeline, /startsWith\('baiyue-firstleg-dev-'\)[\s\S]*?return 'development'/)
assert.match(pipeline, /startsWith\('baiyue-firstleg-prod-'\)[\s\S]*?return 'production'/)
assert.match(pipeline, /startsWith\('baiyue-firstleg-'\)[\s\S]*?return 'test'/)
assert.match(pipeline, /environmentChoicesForServiceJob\(env\.JOB_BASE_NAME\)/)
assert.match(pipeline, /env\.RESOLVED_ENVIRONMENT = resolvedEnvironment/)
assert.doesNotMatch(pipeline, /params\.ENVIRONMENT ==/)
assert.doesNotMatch(pipeline, /contains\(params\.ENVIRONMENT\)/)
2026-08-20 13:00:39 +08:00
})
2026-09-02 20:14:47 +08:00
test('AUTO advances interrupted baselines and retries only their selected services', () => {
2026-08-26 18:01:47 +08:00
assert.match(coordinatorPipeline, /def parseAutoBuildState\(/)
assert.match(coordinatorPipeline, /def previousAutoBuildState\(/)
assert.match(coordinatorPipeline, /previousAutoBuildState\(currentBuild\.previousBuild, serviceModules\)/)
2026-09-02 20:14:47 +08:00
assert.match(coordinatorPipeline, /state\.startsWith\('PENDING'\)/)
assert.match(coordinatorPipeline, /failedServices\.addAll\(selected\.split\(','\)/)
assert.match(coordinatorPipeline, /git rev-parse HEAD\^/)
2026-08-26 18:01:47 +08:00
assert.match(coordinatorPipeline, /selected\.addAll\(retryServices\)/)
assert.match(coordinatorPipeline, /AUTO \$\{env\.IMAGE_TAG\} \| PENDING/)
assert.match(coordinatorPipeline, /AUTO \$\{env\.IMAGE_TAG\} \| RESULTS/)
2026-09-02 20:14:47 +08:00
assert.doesNotMatch(coordinatorPipeline, /GIT_PREVIOUS_SUCCESSFUL_COMMIT/)
2026-08-26 18:01:47 +08:00
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/)
})
2026-08-13 17:40:55 +08:00
test('production deployments cannot skip tests', () => {
assert.match(coordinatorPipeline, /env\.RESOLVED_ENVIRONMENT == 'production' && params\.SKIP_TESTS/)
assert.match(coordinatorPipeline, /error\('生产环境禁止跳过测试'\)/)
assert.match(pipeline, /生产环境手工构建禁止跳过测试/)
2026-09-17 11:49:00 +08:00
assert.match(coordinatorPipeline, /refs\/heads\/main:refs\/remotes\/origin\/main/)
assert.match(coordinatorPipeline, /test "\$\(git rev-parse HEAD\)" = "\$\(git rev-parse origin\/main\)"/)
assert.match(pipeline, /git merge-base --is-ancestor "\$\{RESOLVED_SOURCE_COMMIT\}" origin\/main/)
2026-08-13 17:40:55 +08:00
})
2026-08-20 10:22:51 +08:00
test('Java builds use the shared builder pool and serialize incremental target deployments', () => {
2026-08-20 10:22:51 +08:00
assert.match(pipeline, /agent \{ label 'java-builder' \}/)
2026-09-03 17:43:52 +08:00
assert.match(pipeline, /\['development', 'test'\]\.contains\(env\.RESOLVED_ENVIRONMENT\)/)
assert.match(pipeline, /env\.RESOLVED_ENVIRONMENT == 'development' \? 'java-dev' : 'java-test'/)
assert.match(pipeline, /'TARGET_IS_LOCAL=true'/)
2026-09-03 17:43:52 +08:00
assert.match(pipeline, /String deploySlotLabel = "firstleg-\$\{env\.RESOLVED_ENVIRONMENT\}-deploy-slot"/)
assert.match(pipeline, /lock\(label: deploySlotLabel, quantity: 1, inversePrecedence: true\)/)
2026-08-20 12:49:54 +08:00
assert.match(pipeline, /deploy-built-service\.sh/)
2026-09-17 11:49:00 +08:00
assert.doesNotMatch(pipeline, /dev_deploy|test_deploy/)
assert.match(pipeline, /firstleg-production-ssh/)
assert.match(pipeline, /TARGET_SSH_KEY/)
assert.match(directDeployScript, /docker pull "\$IMAGE"/)
assert.match(directDeployScript, /run_ssh docker pull "\$IMAGE"/)
assert.doesNotMatch(directDeployScript, /docker save "\$IMAGE"/)
assert.match(directDeployScript, /up -d --no-deps --pull never/)
assert.doesNotMatch(directDeployScript, /--remove-orphans/)
2026-08-25 16:40:48 +08:00
assert.match(directDeployScript, /--project-name "\$PROJECT_NAME"/)
assert.doesNotMatch(directDeployScript, /overseas-security-service|remove_legacy_overseas_security_container/)
2026-08-20 12:49:54 +08:00
assert.match(directDeployScript, /--pull never/)
2026-08-20 10:22:51 +08:00
})
2026-08-20 10:28:45 +08:00
test('production only accepts skipped downstream tests for a coordinator-pinned commit', () => {
assert.match(pipeline, /boolean coordinatorPinnedCommit = params\.SOURCE_COMMIT\?\.trim\(\) as boolean/)
2026-09-03 17:43:52 +08:00
assert.match(pipeline, /env\.RESOLVED_ENVIRONMENT == 'production' && params\.SKIP_TESTS && !coordinatorPinnedCommit/)
assert.match(pipeline, /生产环境手工构建禁止跳过测试/)
})
test('development and test deployment locks are acquired before target executors', () => {
const directDeployStage = pipeline.slice(
pipeline.indexOf("stage('部署开发或测试环境')"),
pipeline.indexOf("stage('部署单个服务')")
)
2026-08-25 11:53:59 +08:00
const serviceLockIndex = directDeployStage.indexOf('lock(resource: serviceDeployResource')
const lockIndex = directDeployStage.indexOf('lock(label: deploySlotLabel')
const nodeIndex = directDeployStage.indexOf('node(targetLabel)')
assert.match(directDeployStage, /agent none/)
2026-09-03 17:43:52 +08:00
assert.match(directDeployStage, /String targetLabel = env\.RESOLVED_ENVIRONMENT == 'development' \? 'java-dev' : 'java-test'/)
assert.match(directDeployStage, /String serviceDeployResource = "firstleg-\$\{env\.RESOLVED_ENVIRONMENT\}-\$\{env\.RESOLVED_SERVICE\}-deploy"/)
2026-08-25 11:53:59 +08:00
assert.ok(serviceLockIndex >= 0, 'per-service deployment lock must exist')
assert.ok(lockIndex >= 0, 'deployment slot lock must exist')
assert.ok(nodeIndex >= 0, 'target node allocation must exist')
2026-08-25 11:53:59 +08:00
assert.ok(serviceLockIndex < lockIndex, 'per-service lock must be acquired before the shared deployment slot')
assert.ok(lockIndex < nodeIndex, 'deployment lock must be acquired before target node allocation')
})
test('development publishes every service on loopback without changing test defaults', () => {
assert.match(serviceCompose, /\$\{PUBLISHED_BIND_ADDRESS:-0\.0\.0\.0\}:\$\{GATEWAY_PORT:-9000\}:9000/)
assert.match(overseasCompose, /\$\{PUBLISHED_BIND_ADDRESS:-0\.0\.0\.0\}:\$\{GATEWAY_OVERSEAS_PORT:-9300\}:9300/)
assert.match(directDeployScript, /if \[ "\$DEPLOY_ENV" = "development" \]; then\s+PUBLISHED_BIND_ADDRESS=127\.0\.0\.1/)
assert.match(directDeployScript, /if \[ "\$deploy_env" = "development" \]; then\s+export PUBLISHED_BIND_ADDRESS=127\.0\.0\.1/)
assert.doesNotMatch(directDeployScript, /gateway\|gateway-overseas\) (?:export )?PUBLISHED_BIND_ADDRESS=0\.0\.0\.0/)
})
2026-09-09 18:29:42 +08:00
test('Firstleg deployment passes terminal drayage SMTP settings into the container', () => {
const firstlegService = serviceCompose.slice(
serviceCompose.indexOf(' firstleg:\n'),
serviceCompose.indexOf(' finance:\n')
)
for (const key of [
'TERMINAL_DRAYAGE_MAIL_ENABLED',
'TERMINAL_DRAYAGE_MAIL_HOST',
'TERMINAL_DRAYAGE_MAIL_PORT',
'TERMINAL_DRAYAGE_MAIL_USERNAME',
'TERMINAL_DRAYAGE_MAIL_PASSWORD',
'TERMINAL_DRAYAGE_MAIL_SSL',
'TERMINAL_DRAYAGE_MAIL_SCAN_DELAY'
]) {
assert.match(firstlegService, new RegExp(`^ ${key}: ` + '\\$\\{' + key, 'm'))
}
assert.match(directDeployScript, /TERMINAL_DRAYAGE_MAIL_ENABLED/)
assert.match(directDeployScript, /TERMINAL_DRAYAGE_MAIL_HOST TERMINAL_DRAYAGE_MAIL_PORT TERMINAL_DRAYAGE_MAIL_USERNAME TERMINAL_DRAYAGE_MAIL_PASSWORD/)
})
test('parallel image publications isolate registry credentials per Jenkins job', () => {
const imageStage = pipeline.slice(
pipeline.indexOf("stage('构建并发布单服务镜像')"),
pipeline.indexOf("stage('部署开发或测试环境')")
)
const loginIndex = imageStage.indexOf('docker login')
const buildIndex = imageStage.indexOf('docker build')
const pushIndex = imageStage.indexOf('docker push')
const logoutIndex = imageStage.indexOf('docker logout')
const cleanupIndex = imageStage.indexOf('rm -rf "$DOCKER_CONFIG"')
assert.match(imageStage, /String dockerConfigDir = "\$\{pwd\(tmp: true\)\}\/docker-auth"/)
assert.match(imageStage, /withEnv\(\["DOCKER_CONFIG=\$\{dockerConfigDir\}"\]\)/)
assert.ok(loginIndex >= 0 && loginIndex < buildIndex, 'registry login must happen before image build')
assert.ok(buildIndex < pushIndex, 'image build must happen before image push')
assert.ok(pushIndex < logoutIndex, 'registry logout must happen after image push')
assert.ok(logoutIndex < cleanupIndex, 'temporary Docker credentials must be removed last')
assert.doesNotMatch(imageStage, /\/home\/deploy\/\.docker\/config\.json/)
})
2026-08-21 17:19:49 +08:00
test('parallel remote deployments keep env and compose assets isolated by project', () => {
assert.match(directDeployScript, /REMOTE_ENV_NAME="\.env\.\$PROJECT_NAME"/)
assert.match(directDeployScript, /REMOTE_COMPOSE_NAME="docker-compose\.\$PROJECT_NAME\.yml"/)
assert.match(directDeployScript, /REMOTE_ENV_TMP="\$REMOTE_DIR\/\$REMOTE_ENV_NAME\.\$SUFFIX\.tmp"/)
assert.match(directDeployScript, /REMOTE_COMPOSE_TMP="\$REMOTE_DIR\/\$REMOTE_COMPOSE_NAME\.\$SUFFIX\.tmp"/)
assert.match(directDeployScript, /env_target="\$3"/)
assert.match(directDeployScript, /compose_target="\$4"/)
assert.match(directDeployScript, /mv -f "\$env_tmp" "\$env_target"/)
assert.match(directDeployScript, /mv -f "\$compose_tmp" "\$compose_target"/)
assert.match(directDeployScript, /env_file="\$7"/)
assert.match(directDeployScript, /compose_file="\$8"/)
assert.match(directDeployScript, /--env-file "\$env_file"/)
assert.match(directDeployScript, /-f "\$compose_file"/)
assert.doesNotMatch(directDeployScript, /mv -f "\$env_tmp" "\$remote_dir\/\.env"/)
assert.doesNotMatch(directDeployScript, /mv -f "\$compose_tmp" "\$remote_dir\/docker-compose\.yml"/)
assert.doesNotMatch(directDeployScript, /--env-file \.env/)
assert.doesNotMatch(directDeployScript, /-f docker-compose\.yml/)
})
2026-08-20 10:28:45 +08:00
test('shared builders use a reachable public Maven mirror instead of the retired private proxy', () => {
assert.match(pipeline, /MAVEN_SETTINGS = 'deploy\/maven\/settings-ci\.xml'/)
assert.match(ciMavenSettings, /https:\/\/maven\.aliyun\.com\/repository\/public/)
assert.doesNotMatch(ciMavenSettings, /:18081/)
})
2026-08-22 12:20:26 +08:00
2026-09-17 11:49:00 +08:00
test('Java service images use the canonical Gitea registry base image by default', () => {
const internalBaseImage = 'git.buggoglobal.com/java-project/eclipse-temurin:17-jre-alpine'
2026-08-22 12:20:26 +08:00
assert.match(coordinatorPipeline, new RegExp(internalBaseImage.replaceAll('.', '\\.')))
assert.match(pipeline, new RegExp(internalBaseImage.replaceAll('.', '\\.')))
2026-09-17 11:49:00 +08:00
assert.match(serviceDockerfile, /ARG BASE_IMAGE/)
2026-08-22 12:20:26 +08:00
assert.doesNotMatch(coordinatorPipeline, /docker\.m\.daocloud\.io\/library\/eclipse-temurin/)
assert.doesNotMatch(pipeline, /docker\.m\.daocloud\.io\/library\/eclipse-temurin/)
assert.doesNotMatch(serviceDockerfile, /docker\.m\.daocloud\.io\/library\/eclipse-temurin/)
})
test('source workspaces are preserved for incremental Git fetches', () => {
2026-08-22 17:55:50 +08:00
const buildStage = pipeline.slice(
pipeline.indexOf("stage('构建并发布单服务')"),
pipeline.indexOf("stage('部署开发或测试环境')")
)
const directDeployStage = pipeline.slice(
pipeline.indexOf("stage('部署开发或测试环境')"),
pipeline.indexOf("stage('部署单个服务')")
)
assert.match(pipeline, /checkout scm/)
2026-08-22 17:55:50 +08:00
assert.doesNotMatch(buildStage, /cleanWs\(/)
assert.match(directDeployStage, /cleanWs\(/)
assert.equal((pipeline.match(/cleanWs\(/g) ?? []).length, 2)
assert.doesNotMatch(coordinatorPipeline, /cleanWs\(/)
})
2026-08-22 18:26:47 +08:00
test('direct deployments wait for health and automatically restore the previous image', () => {
assert.match(directDeployScript, /HEALTH_TIMEOUT_SECONDS="\$\{DEPLOY_HEALTH_TIMEOUT_SECONDS:-240\}"/)
assert.match(directDeployScript, /previous_image="\$\(local_container_image \|\| true\)"/)
assert.match(directDeployScript, /if wait_for_local_health; then/)
assert.match(directDeployScript, /开始自动回滚到/)
assert.match(directDeployScript, /自动回滚成功/)
assert.match(directDeployScript, /自动回滚失败/)
assert.match(directDeployScript, /return 1/)
})
2026-09-20 19:57:47 +08:00
test('one event bridge build deploys both regional instances in every environment', () => {
assert.match(pipeline, /choice\(name: 'EVENT_BRIDGE_REGION', choices: \['BOTH', 'CN', 'OVERSEAS', 'SELECT'\]/)
assert.match(pipeline, /\['BOTH', 'SELECT'\]\.contains\(params\.EVENT_BRIDGE_REGION\)/)
assert.match(pipeline, /\? \['CN', 'OVERSEAS'\]/)
assert.match(pipeline, /bridgeRegions\.each \{ bridgeRegion ->/)
assert.match(pipeline, /EVENT_BRIDGE_REGION_OVERRIDE=\$\{bridgeRegion\}/)
assert.match(pipeline, /部署事件桥实例:/)
assert.match(pipeline, /imageTag\?\.endsWith\('-both'\)/)
assert.match(pipeline, /imageTag\.substring\(0, imageTag\.length\(\) - '-both'\.length\(\)\)/)
assert.match(pipeline, /env\.DEPLOY_BOTH_EVENT_BRIDGES = deployBothBridges \? 'true' : 'false'/)
2026-09-20 19:57:47 +08:00
assert.match(pipeline, /部署生产事件桥实例:/)
})