fix(ci): bound Maven resources and serialize shared builder jobs
This commit is contained in:
Vendored
+5
-1
@@ -327,11 +327,15 @@ pipeline {
|
|||||||
.findAll { it }
|
.findAll { it }
|
||||||
.join(',')
|
.join(',')
|
||||||
echo "统一测试本次选中的服务及其公共依赖: ${selectedModules}"
|
echo "统一测试本次选中的服务及其公共依赖: ${selectedModules}"
|
||||||
|
lock(resource: 'firstleg-test-maven') {
|
||||||
|
timeout(time: 50, unit: 'MINUTES') {
|
||||||
sh """
|
sh """
|
||||||
test \"\$(git rev-parse HEAD)\" = \"${env.SOURCE_COMMIT}\"
|
test \"\$(git rev-parse HEAD)\" = \"${env.SOURCE_COMMIT}\"
|
||||||
find . -type f -path '*/target/surefire-reports/TEST-*.xml' -delete
|
find . -type f -path '*/target/surefire-reports/TEST-*.xml' -delete
|
||||||
mvn --settings deploy/maven/settings-ci.xml --batch-mode --errors -pl ${selectedModules} -am test
|
sh deploy/scripts/run-ci-maven.sh --settings deploy/maven/settings-ci.xml --batch-mode --errors -pl ${selectedModules} -am test
|
||||||
"""
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
echo "统一测试门禁通过: ${env.SOURCE_COMMIT};下游单服务任务不再重复执行测试"
|
echo "统一测试门禁通过: ${env.SOURCE_COMMIT};下游单服务任务不再重复执行测试"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -55,7 +55,6 @@ pipeline {
|
|||||||
environment {
|
environment {
|
||||||
REGISTRY_CREDENTIALS = 'gitea-registry-token'
|
REGISTRY_CREDENTIALS = 'gitea-registry-token'
|
||||||
MAVEN_SETTINGS = 'deploy/maven/settings-ci.xml'
|
MAVEN_SETTINGS = 'deploy/maven/settings-ci.xml'
|
||||||
MAVEN_OPTS = '-Dfile.encoding=UTF-8 -Dmaven.wagon.http.retryHandler.count=3'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
@@ -141,7 +140,11 @@ pipeline {
|
|||||||
if (params.SKIP_TESTS && coordinatorPinnedCommit) {
|
if (params.SKIP_TESTS && coordinatorPinnedCommit) {
|
||||||
echo "总控已处理提交 ${env.RESOLVED_SOURCE_COMMIT} 的测试门禁,本单服务仅编译测试源码并打包"
|
echo "总控已处理提交 ${env.RESOLVED_SOURCE_COMMIT} 的测试门禁,本单服务仅编译测试源码并打包"
|
||||||
}
|
}
|
||||||
sh "mvn --settings ${env.MAVEN_SETTINGS} --batch-mode --errors -pl ${env.RESOLVED_SERVICE} -am clean package ${testOption}"
|
lock(resource: 'firstleg-test-maven') {
|
||||||
|
timeout(time: 50, unit: 'MINUTES') {
|
||||||
|
sh "sh deploy/scripts/run-ci-maven.sh --settings ${env.MAVEN_SETTINGS} --batch-mode --errors -pl ${env.RESOLVED_SERVICE} -am clean package ${testOption}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Linux Jenkins entry; no service deployment, data cleanup or dependency installation.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
for tool in flock timeout mvn; do
|
||||||
|
command -v "$tool" >/dev/null 2>&1 || {
|
||||||
|
echo "Required CI tool missing: $tool" >&2
|
||||||
|
exit 69
|
||||||
|
}
|
||||||
|
done
|
||||||
|
for argument in "$@"; do
|
||||||
|
case "$argument" in
|
||||||
|
-T*|--threads*)
|
||||||
|
echo 'Parallel Maven reactors are disabled on the shared TEST builder.' >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# All jobs run as the same builder account. Keep the inode: flock releases on close.
|
||||||
|
# A controller-side abort must not permit another build over a surviving local build.
|
||||||
|
umask 077
|
||||||
|
lock_dir="${HOME:?HOME is required}/.cache/chongchong-ci"
|
||||||
|
mkdir -p "$lock_dir"
|
||||||
|
exec 9>"$lock_dir/maven.lock"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo 'Another local Maven build still holds the CI lock; inspect it before retrying.' >&2
|
||||||
|
exit 75
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Separate budgets: this process is Maven; ci-bounded configures its test JVM.
|
||||||
|
MAVEN_OPTS='-Xms128m -Xmx768m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=128m -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8 -Dmaven.wagon.http.retryHandler.count=3'
|
||||||
|
export MAVEN_OPTS
|
||||||
|
|
||||||
|
# Local watchdog still runs if the Jenkins controller/agent connection is lost.
|
||||||
|
# Do not use --foreground: timeout must signal the forked process group too.
|
||||||
|
exec timeout --signal=TERM --kill-after=30s 45m mvn -Pci-bounded "$@"
|
||||||
@@ -171,6 +171,30 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
<profiles>
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>ci-bounded</id>
|
||||||
|
<properties>
|
||||||
|
<!-- JaCoCo fills this property at runtime; retain late evaluation below. -->
|
||||||
|
<argLine></argLine>
|
||||||
|
</properties>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<argLine>@{argLine} -Xms128m -Xmx1024m -XX:MaxMetaspaceSize=384m -XX:MaxDirectMemorySize=128m -XX:+ExitOnOutOfMemoryError</argLine>
|
||||||
|
<forkCount>1</forkCount>
|
||||||
|
<reuseForks>true</reuseForks>
|
||||||
|
<enableProcessChecker>ping</enableProcessChecker>
|
||||||
|
<shutdown>kill</shutdown>
|
||||||
|
<forkedProcessTimeoutInSeconds>1200</forkedProcessTimeoutInSeconds>
|
||||||
|
<forkedProcessExitTimeoutInSeconds>30</forkedProcessExitTimeoutInSeconds>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
<profile>
|
<profile>
|
||||||
<id>local</id>
|
<id>local</id>
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# 后端脚本管理与全量 HTTP 验收索引
|
# 后端脚本管理与全量 HTTP 验收索引
|
||||||
|
|
||||||
|
`deploy/scripts/run-ci-maven.sh` 是 Linux Jenkins 的受限 Maven 构建入口,不部署、不访问业务数据库、不安装依赖。总控及单服务 Job 共用 `firstleg-test-maven` 锁;同一构建账号另用 `$HOME/.cache/chongchong-ci/maven.lock` 文件锁,已有本机持锁任务时失败退出,不删除锁文件或杀其他进程。入口禁止 `-T/--threads`,固定 Maven 堆768MiB、元空间256MiB、直接内存128MiB,启用根POM `ci-bounded`:单个测试JVM、堆1GiB、元空间384MiB、直接内存128MiB,保留JaCoCo迟绑定;OOM快速退出,父进程丢失检测、停止时kill、单测试JVM20分钟上限。独立于Jenkins连接的本机GNU timeout在45分钟后TERM、30秒后KILL进程组,流水线另设50分钟超时;缺少flock/timeout/mvn直接失败。构建输出及Surefire报告沿现有Jenkins归档;失败先确认原进程退出再重跑,不自动清理缓存。该预算不等于总RSS硬限额,代理本身和JVM原生内存仍需保留余量;节点执行器应设为1、保持systemd control-group清理,并在真实Linux节点验证峰值。`ci-maven-resource-contract.test.mjs` 检查双入口接线、参数拒绝/失败传播和子JVM配置,Shell替身检查不代替Linux实际锁与信号验收。
|
||||||
|
|
||||||
`client-address-isolation-test-env.mjs` 是客户地址提交隔离的 TEST 定向负向入口,默认 `--plan` 零网络;执行要求显式 TEST 配置及 `--execute --environment=TEST --run-id=<新批次> --customer-profile=<同目录test-ui-*.env>`。分别登录两个既有客户,从来源客户读取仍有效的取货/私人地址、从目标客户读取实际仓库和渠道;向目标客户发两次单单与两次单行批量 JSON 创建请求,必须精准返回“客户地址不存在或已停用”,普通必填错误不算通过。每个载荷只含一种外客户地址,成功返回订单或未知结果立即停止,报告保存幂等键/载荷摘要/意外订单标识供定位,不自动重试、删除或恢复;结束退出两个独立会话。独占报告 `reports/client-address-isolation-<run-id>/`,不记录凭据、地址明文或账号。该项不等于 XLSX 解析拒绝、数据库零漂移或真实页面验证;对应 `.test.mjs` 检查精准错误分类与探针载荷。
|
`client-address-isolation-test-env.mjs` 是客户地址提交隔离的 TEST 定向负向入口,默认 `--plan` 零网络;执行要求显式 TEST 配置及 `--execute --environment=TEST --run-id=<新批次> --customer-profile=<同目录test-ui-*.env>`。分别登录两个既有客户,从来源客户读取仍有效的取货/私人地址、从目标客户读取实际仓库和渠道;向目标客户发两次单单与两次单行批量 JSON 创建请求,必须精准返回“客户地址不存在或已停用”,普通必填错误不算通过。每个载荷只含一种外客户地址,成功返回订单或未知结果立即停止,报告保存幂等键/载荷摘要/意外订单标识供定位,不自动重试、删除或恢复;结束退出两个独立会话。独占报告 `reports/client-address-isolation-<run-id>/`,不记录凭据、地址明文或账号。该项不等于 XLSX 解析拒绝、数据库零漂移或真实页面验证;对应 `.test.mjs` 检查精准错误分类与探针载荷。
|
||||||
|
|
||||||
`client-xlsx-address-test-env.mjs` 补充真实 XLSX 地址隔离负向导入,默认计划零网络。执行用显式 TEST 配置,参数为 `--execute --environment=TEST --run-id=<新批次> --customer-profile=<同目录test-ui-*.env> --python=<bundled Python>`;内部 `client-xlsx-address-probe.py` 只在内存读取两个客户的真实模板,保留来源模板的隐藏选项/关系,仅填一行取货或私人地址后以对照客户上传。两次 PARTIAL 导入必须精准拒绝外客户地址,普通格式/必填错误不通过;导入源文件及错误报告会保留,意外成功/未知结果立即停止,不自动重试、删除或推进订单。凭据只经 stdin,报告 `reports/client-xlsx-address-<run-id>/report.json` 只保存摘要、响应码和返回资源标识,会话结束退出;不保存原工作簿/地址明文,不代表浏览器通过。可追加 `--database-profile=<同目录test-database.env>`:用现有 mysql CLI 的只读一致性事务,在导入前及每次拒绝后读取两客户 Order 的订单、货物、参与方、内部备注、状态历史、幂等和领域 Outbox 七表完整记录摘要(含逻辑删除),严格比较数量与完整摘要;不同、缺表、截断或查询失败即停止。DB 凭据只进入子进程环境,报告不含原始行;文件、审计及认证正常留存不属于这七表不变断言,不声称全环境零写入。未传此参数只验 HTTP。对应 Node/Python 测试检查零网络计划、环境边界、精准拒绝、模板其他部分不变及数据库快照完整性。
|
`client-xlsx-address-test-env.mjs` 补充真实 XLSX 地址隔离负向导入,默认计划零网络。执行用显式 TEST 配置,参数为 `--execute --environment=TEST --run-id=<新批次> --customer-profile=<同目录test-ui-*.env> --python=<bundled Python>`;内部 `client-xlsx-address-probe.py` 只在内存读取两个客户的真实模板,保留来源模板的隐藏选项/关系,仅填一行取货或私人地址后以对照客户上传。两次 PARTIAL 导入必须精准拒绝外客户地址,普通格式/必填错误不通过;导入源文件及错误报告会保留,意外成功/未知结果立即停止,不自动重试、删除或推进订单。凭据只经 stdin,报告 `reports/client-xlsx-address-<run-id>/report.json` 只保存摘要、响应码和返回资源标识,会话结束退出;不保存原工作簿/地址明文,不代表浏览器通过。可追加 `--database-profile=<同目录test-database.env>`:用现有 mysql CLI 的只读一致性事务,在导入前及每次拒绝后读取两客户 Order 的订单、货物、参与方、内部备注、状态历史、幂等和领域 Outbox 七表完整记录摘要(含逻辑删除),严格比较数量与完整摘要;不同、缺表、截断或查询失败即停止。DB 凭据只进入子进程环境,报告不含原始行;文件、审计及认证正常留存不属于这七表不变断言,不声称全环境零写入。未传此参数只验 HTTP。对应 Node/Python 测试检查零网络计划、环境边界、精准拒绝、模板其他部分不变及数据库快照完整性。
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync, mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join, resolve } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
|
||||||
|
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
const wrapper = resolve(root, 'deploy/scripts/run-ci-maven.sh')
|
||||||
|
const bash = process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : 'bash'
|
||||||
|
const shellAvailable = process.platform !== 'win32' || existsSync(bash)
|
||||||
|
|
||||||
|
test('both Jenkins Maven paths share one cross-job lock and bounded host entry', () => {
|
||||||
|
for (const file of ['Jenkinsfile', 'Jenkinsfile.service']) {
|
||||||
|
const source = readFileSync(resolve(root, file), 'utf8')
|
||||||
|
assert.match(source, /lock\(resource: 'firstleg-test-maven'\)\s*\{\s*timeout\(time: 50, unit: 'MINUTES'\)/)
|
||||||
|
assert.match(source, /sh deploy\/scripts\/run-ci-maven\.sh --settings/)
|
||||||
|
assert.doesNotMatch(source, /(?:^|\n)\s*mvn\s|sh "mvn /)
|
||||||
|
}
|
||||||
|
const source = readFileSync(wrapper, 'utf8')
|
||||||
|
assert.match(source, /exec 9>/)
|
||||||
|
assert.match(source, /flock -n 9/)
|
||||||
|
assert.match(source, /exec timeout --signal=TERM --kill-after=30s 45m mvn -Pci-bounded/)
|
||||||
|
assert.doesNotMatch(source, /exec timeout[^\n]*--foreground/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test JVM gets a separate heap, JaCoCo late binding, and parent-loss cleanup', () => {
|
||||||
|
const pom = readFileSync(resolve(root, 'pom.xml'), 'utf8')
|
||||||
|
const profile = pom.match(/<profile>\s*<id>ci-bounded<\/id>([\s\S]*?)<\/profile>/)?.[1]
|
||||||
|
assert.ok(profile)
|
||||||
|
assert.match(profile, /<argLine>@\{argLine\}[^<]*-Xmx1024m[^<]*-XX:\+ExitOnOutOfMemoryError<\/argLine>/)
|
||||||
|
for (const [name, value] of Object.entries({ forkCount: '1', enableProcessChecker: 'ping', shutdown: 'kill',
|
||||||
|
forkedProcessTimeoutInSeconds: '1200', forkedProcessExitTimeoutInSeconds: '30' }))
|
||||||
|
assert.ok(profile.includes(`<${name}>${value}</${name}>`))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shell rejects parallel reactors and held host lock before invoking Maven; propagates failure', { skip: !shellAvailable }, () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), 'ci-maven-command-test-'))
|
||||||
|
try {
|
||||||
|
// Stand-ins exercise command wiring, not Linux flock or process-group signalling.
|
||||||
|
writeFileSync(join(directory, 'flock'), '#!/bin/sh\nexit "${TEST_LOCK_STATUS:-0}"\n', { mode: 0o755 })
|
||||||
|
writeFileSync(join(directory, 'mvn'), '#!/bin/sh\nexit 99\n', { mode: 0o755 })
|
||||||
|
writeFileSync(join(directory, 'timeout'), '#!/bin/sh\nprintf "%s\\n" "$MAVEN_OPTS" "$@"\nexit "${TEST_CHILD_STATUS:-0}"\n', { mode: 0o755 })
|
||||||
|
const run = (args, extra = {}) => spawnSync(bash, ['-c',
|
||||||
|
'testbin=$(cd "$1" && pwd); shift; PATH="$testbin:$PATH"; export PATH; exec sh "$@"',
|
||||||
|
'fixture', directory.replaceAll('\\', '/'), wrapper.replaceAll('\\', '/'), ...args],
|
||||||
|
{ encoding: 'utf8', timeout: 10000, env: { ...process.env, ...extra } })
|
||||||
|
for (const args of [['-T', '3', 'test'], ['-T3', 'test'], ['--threads=3', 'test']]) {
|
||||||
|
const result = run(args)
|
||||||
|
assert.equal(result.status, 64, result.stderr)
|
||||||
|
assert.equal(result.stdout, '')
|
||||||
|
}
|
||||||
|
const held = run(['test'], { TEST_LOCK_STATUS: '1' })
|
||||||
|
assert.equal(held.status, 75, held.stderr)
|
||||||
|
assert.equal(held.stdout, '')
|
||||||
|
const normal = run(['-pl', 'file', '-am', 'test'], { TEST_CHILD_STATUS: '23', MAVEN_OPTS: '-Xmx8g' })
|
||||||
|
assert.equal(normal.status, 23, normal.stderr)
|
||||||
|
assert.match(normal.stdout, /-Xmx768m/)
|
||||||
|
assert.doesNotMatch(normal.stdout, /-Xmx8g/)
|
||||||
|
assert.match(normal.stdout, /--signal=TERM\r?\n--kill-after=30s\r?\n45m\r?\nmvn\r?\n-Pci-bounded\r?\n-pl\r?\nfile/)
|
||||||
|
} finally {
|
||||||
|
// Only this newly-created fixture directory; no project copies or shared caches removed.
|
||||||
|
assert.equal(resolve(directory).startsWith(resolve(tmpdir()) + (process.platform === 'win32' ? '\\' : '/')), true)
|
||||||
|
rmSync(directory, { recursive: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user