Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45b5fe8f6f | ||
|
|
cb7d478288 |
@@ -125,6 +125,14 @@ pipeline {
|
|||||||
git cat-file -e "${RESOLVED_SOURCE_COMMIT}^{commit}"
|
git cat-file -e "${RESOLVED_SOURCE_COMMIT}^{commit}"
|
||||||
git checkout --detach "${RESOLVED_SOURCE_COMMIT}"
|
git checkout --detach "${RESOLVED_SOURCE_COMMIT}"
|
||||||
test "$(git rev-parse HEAD)" = "$(git rev-parse "${RESOLVED_SOURCE_COMMIT}^{commit}")"
|
test "$(git rev-parse HEAD)" = "$(git rev-parse "${RESOLVED_SOURCE_COMMIT}^{commit}")"
|
||||||
|
# TEST 重发旧提交也须包含限额文件及部署入口,避免重建时恢复无限制容器。
|
||||||
|
if [ "$RESOLVED_ENVIRONMENT" = "test" ]; then
|
||||||
|
if ! git cat-file -e "${RESOLVED_SOURCE_COMMIT}:deploy/docker-compose.test-limits.yml" 2>/dev/null ||
|
||||||
|
! git grep -q -F 'docker-compose.test-limits.yml' "$RESOLVED_SOURCE_COMMIT" -- deploy/scripts/deploy-built-service.sh; then
|
||||||
|
echo "目标 TEST 提交缺少完整内存保护,请选择包含限额修复的 test 提交" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
if [ "$RESOLVED_ENVIRONMENT" = "production" ]; then
|
if [ "$RESOLVED_ENVIRONMENT" = "production" ]; then
|
||||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||||
git merge-base --is-ancestor "$RESOLVED_SOURCE_COMMIT" origin/main || {
|
git merge-base --is-ancestor "$RESOLVED_SOURCE_COMMIT" origin/main || {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 仅由 TEST 部署入口叠加,固定 JVM 堆并为线程、元空间和直接内存保留余量。
|
||||||
|
x-test-limits: &test-limits
|
||||||
|
mem_limit: 1536m
|
||||||
|
memswap_limit: 1536m
|
||||||
|
environment: &test-java-environment
|
||||||
|
JAVA_OPTS: "-XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
|
||||||
|
JAVA_TOOL_OPTIONS: "-Xms128m -Xmx768m"
|
||||||
|
|
||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
auth:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
iam:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
order:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
firstleg:
|
||||||
|
<<: *test-limits
|
||||||
|
mem_limit: 2g
|
||||||
|
memswap_limit: 2g
|
||||||
|
environment:
|
||||||
|
<<: *test-java-environment
|
||||||
|
JAVA_TOOL_OPTIONS: "-Xms256m -Xmx1g"
|
||||||
|
|
||||||
|
finance:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
warehouse:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
warehouse-overseas-service:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
integration-service:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
audit-service:
|
||||||
|
<<: *test-limits
|
||||||
|
|
||||||
|
file:
|
||||||
|
<<: *test-limits
|
||||||
|
mem_limit: 3g
|
||||||
|
memswap_limit: 3g
|
||||||
|
environment:
|
||||||
|
<<: *test-java-environment
|
||||||
|
JAVA_TOOL_OPTIONS: "-Xms1g -Xmx2g"
|
||||||
|
|
||||||
|
print:
|
||||||
|
<<: *test-limits
|
||||||
|
environment:
|
||||||
|
<<: *test-java-environment
|
||||||
|
JAVA_TOOL_OPTIONS: "-Xms512m -Xmx1g"
|
||||||
|
|
||||||
|
lastleg-service:
|
||||||
|
<<: *test-limits
|
||||||
|
mem_limit: 3g
|
||||||
|
memswap_limit: 3g
|
||||||
|
environment:
|
||||||
|
<<: *test-java-environment
|
||||||
|
JAVA_TOOL_OPTIONS: "-Xms512m -Xmx1536m"
|
||||||
@@ -12,6 +12,7 @@ IMAGE_TAG="$3"
|
|||||||
SERVICE="$4"
|
SERVICE="$4"
|
||||||
ENV_FILE="$5"
|
ENV_FILE="$5"
|
||||||
COMPOSE_FILE="deploy/docker-compose.yml"
|
COMPOSE_FILE="deploy/docker-compose.yml"
|
||||||
|
TEST_LIMITS_FILE="deploy/docker-compose.test-limits.yml"
|
||||||
IMAGE="$IMAGE_REPOSITORY/$SERVICE:$IMAGE_TAG"
|
IMAGE="$IMAGE_REPOSITORY/$SERVICE:$IMAGE_TAG"
|
||||||
REGISTRY_HOST="${IMAGE_REPOSITORY%%/*}"
|
REGISTRY_HOST="${IMAGE_REPOSITORY%%/*}"
|
||||||
PROJECT_NAME="firstleg-$DEPLOY_ENV"
|
PROJECT_NAME="firstleg-$DEPLOY_ENV"
|
||||||
@@ -40,6 +41,9 @@ case "${ALLOW_SAME_CONTRACT_ROLLBACK:-false}" in true|false) ;; *) exit 2 ;; esa
|
|||||||
if [ "$DEPLOY_ENV" = "test" ] && [ "${TARGET_IS_LOCAL:-false}" != "true" ]; then
|
if [ "$DEPLOY_ENV" = "test" ] && [ "${TARGET_IS_LOCAL:-false}" != "true" ]; then
|
||||||
echo "TEST deploy must run on the target host" >&2; exit 2
|
echo "TEST deploy must run on the target host" >&2; exit 2
|
||||||
fi
|
fi
|
||||||
|
if [ "$DEPLOY_ENV" = "test" ] && [ ! -f "$TEST_LIMITS_FILE" ]; then
|
||||||
|
echo "TEST memory limits file missing: $TEST_LIMITS_FILE" >&2; exit 2
|
||||||
|
fi
|
||||||
[ -f "$ENV_FILE" ] || { echo "Environment file missing" >&2; exit 2; }
|
[ -f "$ENV_FILE" ] || { echo "Environment file missing" >&2; exit 2; }
|
||||||
env_file_value() { sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'; }
|
env_file_value() { sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'; }
|
||||||
for key in NACOS_SERVER_ADDR NACOS_NAMESPACE NACOS_USERNAME NACOS_PASSWORD REDIS_HOST REDIS_PORT REDIS_DATABASE REDIS_TOKEN_KEY_PREFIX INTERNAL_SERVICE_TOKEN JWT_RSA_JWK_SET JWT_CURRENT_KID ROCKETMQ_NAME_SERVER; do
|
for key in NACOS_SERVER_ADDR NACOS_NAMESPACE NACOS_USERNAME NACOS_PASSWORD REDIS_HOST REDIS_PORT REDIS_DATABASE REDIS_TOKEN_KEY_PREFIX INTERNAL_SERVICE_TOKEN JWT_RSA_JWK_SET JWT_CURRENT_KID ROCKETMQ_NAME_SERVER; do
|
||||||
@@ -74,7 +78,11 @@ local_registry_logout() {
|
|||||||
if [ -n "${deploy_docker_config:-}" ]; then rm -rf -- "$deploy_docker_config"; fi
|
if [ -n "${deploy_docker_config:-}" ]; then rm -rf -- "$deploy_docker_config"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TEST 的查询、部署和回滚共用同一份内存限额,缺失时由入口提前拒绝。
|
||||||
local_compose() {
|
local_compose() {
|
||||||
|
if [ "$DEPLOY_ENV" = "test" ]; then
|
||||||
|
set -- -f "$TEST_LIMITS_FILE" "$@"
|
||||||
|
fi
|
||||||
docker compose \
|
docker compose \
|
||||||
--project-name "$PROJECT_NAME" \
|
--project-name "$PROJECT_NAME" \
|
||||||
--env-file "$ENV_FILE" \
|
--env-file "$ENV_FILE" \
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ case "$command" in
|
|||||||
esac
|
esac
|
||||||
`
|
`
|
||||||
|
|
||||||
async function runDeployment({ imageTag, failingTag, failingStartTag, restartingTag, allowRollback = true, environment = 'test', service = 'audit-service', missingHealth = false, topicPrefix = `${environment}-`, remote = false }) {
|
// 通过临时 Docker/SSH 替身验证环境选择、健康门禁及失败后的镜像恢复。
|
||||||
|
async function runDeployment({ imageTag, failingTag, failingStartTag, restartingTag, allowRollback = true, environment = 'test', service = 'audit-service', missingHealth = false, missingTestLimits = false, topicPrefix = `${environment}-`, remote = false }) {
|
||||||
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'firstleg-rollback-'))
|
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'firstleg-rollback-'))
|
||||||
const mockBin = join(temporaryDirectory, 'bin')
|
const mockBin = join(temporaryDirectory, 'bin')
|
||||||
const dockerPath = join(mockBin, 'docker')
|
const dockerPath = join(mockBin, 'docker')
|
||||||
@@ -115,7 +116,7 @@ cp "$1" "\${2#*:}"
|
|||||||
service,
|
service,
|
||||||
envFile.replaceAll('\\', '/')
|
envFile.replaceAll('\\', '/')
|
||||||
], {
|
], {
|
||||||
cwd: repositoryRoot,
|
cwd: missingTestLimits ? temporaryDirectory : repositoryRoot,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -158,6 +159,16 @@ test('successful deployment keeps the new healthy image', async (context) => {
|
|||||||
assert.match(result.stdout, /部署前运行镜像: registry\.local\/group\/firstleg\/audit-service:oldgood/)
|
assert.match(result.stdout, /部署前运行镜像: registry\.local\/group\/firstleg\/audit-service:oldgood/)
|
||||||
assert.match(result.stdout, /新镜像健康检查通过: registry\.local\/group\/firstleg\/audit-service:newgood/)
|
assert.match(result.stdout, /新镜像健康检查通过: registry\.local\/group\/firstleg\/audit-service:newgood/)
|
||||||
assert.equal(result.currentImage, 'registry.local/group/firstleg/audit-service:newgood')
|
assert.equal(result.currentImage, 'registry.local/group/firstleg/audit-service:newgood')
|
||||||
|
for (const command of result.dockerLog.split(/\r?\n/).filter(line => line.startsWith('compose '))) {
|
||||||
|
assert.match(command, /-f deploy\/docker-compose\.yml -f deploy\/docker-compose\.test-limits\.yml /)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('TEST rejects a missing memory limits file before Docker', async () => {
|
||||||
|
const result = await runDeployment({ imageTag: 'newgood', missingTestLimits: true })
|
||||||
|
assert.equal(result.status, 2)
|
||||||
|
assert.equal(result.dockerLog, '')
|
||||||
|
assert.match(result.stderr, /TEST memory limits file missing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('unhealthy deployment restores the previous healthy image and still fails the job', async (context) => {
|
test('unhealthy deployment restores the previous healthy image and still fails the job', async (context) => {
|
||||||
@@ -232,6 +243,7 @@ test('gray deploy preserves health checks and uses an isolated Compose project',
|
|||||||
assert.equal(result.status, 0, result.stderr)
|
assert.equal(result.status, 0, result.stderr)
|
||||||
assert.match(result.dockerLog, /--project-name firstleg-production/)
|
assert.match(result.dockerLog, /--project-name firstleg-production/)
|
||||||
assert.doesNotMatch(result.dockerLog, /--project-name firstleg-test/)
|
assert.doesNotMatch(result.dockerLog, /--project-name firstleg-test/)
|
||||||
|
assert.doesNotMatch(result.dockerLog, /docker-compose\.test-limits\.yml/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('gray deployment rejects TEST message scope before any Docker operation', async () => {
|
test('gray deployment rejects TEST message scope before any Docker operation', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { readFile } from 'node:fs/promises'
|
import { existsSync } from 'node:fs'
|
||||||
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { dirname, join, resolve } from 'node:path'
|
||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
const pipeline = (await readFile(new URL('../Jenkinsfile.service', import.meta.url), 'utf8'))
|
const pipeline = (await readFile(new URL('../Jenkinsfile.service', import.meta.url), 'utf8'))
|
||||||
@@ -15,6 +19,60 @@ const serviceDockerfile = (await readFile(new URL('../deploy/docker/Dockerfile',
|
|||||||
const serviceCompose = (await readFile(new URL('../deploy/docker-compose.yml', import.meta.url), 'utf8'))
|
const serviceCompose = (await readFile(new URL('../deploy/docker-compose.yml', import.meta.url), 'utf8'))
|
||||||
.replaceAll('\r\n', '\n')
|
.replaceAll('\r\n', '\n')
|
||||||
|
|
||||||
|
test('TEST rejects old source commits even when the workspace contains newer limits files', {
|
||||||
|
skip: process.platform === 'win32' && !existsSync('C:/Program Files/Git/bin/bash.exe')
|
||||||
|
}, async () => {
|
||||||
|
const directory = await mkdtemp(join(tmpdir(), 'test-memory-source-'))
|
||||||
|
const bash = process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : 'bash'
|
||||||
|
const guardStart = pipeline.indexOf('if [ "$RESOLVED_ENVIRONMENT" = "test" ]; then')
|
||||||
|
const guardEnd = pipeline.indexOf('if [ "$RESOLVED_ENVIRONMENT" = "production" ]; then', guardStart)
|
||||||
|
assert.ok(guardStart >= 0 && guardEnd > guardStart)
|
||||||
|
assert.ok(guardStart < pipeline.indexOf("stage('Maven 测试与打包')"))
|
||||||
|
const guard = pipeline.slice(guardStart, guardEnd)
|
||||||
|
// 在微型 Git 夹具中验证目标提交本身,避免工作区残留文件误放行旧版本。
|
||||||
|
const git = (...args) => {
|
||||||
|
const result = spawnSync('git', args, { cwd: directory, encoding: 'utf8' })
|
||||||
|
assert.equal(result.status, 0, result.stderr)
|
||||||
|
return result.stdout.trim()
|
||||||
|
}
|
||||||
|
// 构建提交沿用正常身份设置,但禁止夹具触发全局签名及提交钩子。
|
||||||
|
const commit = (message) => {
|
||||||
|
git('add', '.')
|
||||||
|
git('-c', 'user.name=Resource Contract', '-c', 'user.email=resource@example.invalid',
|
||||||
|
'-c', 'core.hooksPath=/dev/null', 'commit', '--no-gpg-sign', '-m', message)
|
||||||
|
return git('rev-parse', 'HEAD')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
git('init')
|
||||||
|
await writeFile(join(directory, 'README.md'), 'Source guard fixture\n')
|
||||||
|
const oldCommit = commit('old source')
|
||||||
|
await mkdir(join(directory, 'deploy/scripts'), { recursive: true })
|
||||||
|
await writeFile(join(directory, 'deploy/docker-compose.test-limits.yml'), 'services: {}\n')
|
||||||
|
await writeFile(join(directory, 'deploy/scripts/deploy-built-service.sh'), '#!/bin/sh\nexit 0\n')
|
||||||
|
const incompleteCommit = commit('limits file without deployment integration')
|
||||||
|
await writeFile(join(directory, 'deploy/scripts/deploy-built-service.sh'), directDeployScript)
|
||||||
|
const protectedCommit = commit('complete memory protection')
|
||||||
|
git('checkout', '--detach', oldCommit)
|
||||||
|
await mkdir(join(directory, 'deploy/scripts'), { recursive: true })
|
||||||
|
await writeFile(join(directory, 'deploy/docker-compose.test-limits.yml'), 'services: {}\n')
|
||||||
|
await writeFile(join(directory, 'deploy/scripts/deploy-built-service.sh'), directDeployScript)
|
||||||
|
for (const [environment, sourceCommit, status] of [
|
||||||
|
['test', oldCommit, 1], ['test', incompleteCommit, 1],
|
||||||
|
['test', protectedCommit, 0], ['production', oldCommit, 0]
|
||||||
|
]) {
|
||||||
|
const result = spawnSync(bash, ['-c', guard], {
|
||||||
|
cwd: directory, encoding: 'utf8', timeout: 10000,
|
||||||
|
env: { ...process.env, RESOLVED_ENVIRONMENT: environment, RESOLVED_SOURCE_COMMIT: sourceCommit }
|
||||||
|
})
|
||||||
|
assert.equal(result.status, status, result.stderr)
|
||||||
|
if (status !== 0) assert.match(result.stderr, /目标 TEST 提交缺少完整内存保护/)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
assert.equal(dirname(resolve(directory)), resolve(tmpdir()))
|
||||||
|
await rm(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('both pipelines reject the placeholder registry from failed build 662 before Maven or Docker', () => {
|
test('both pipelines reject the placeholder registry from failed build 662 before Maven or Docker', () => {
|
||||||
for (const source of [coordinatorPipeline, pipeline]) {
|
for (const source of [coordinatorPipeline, pipeline]) {
|
||||||
assert.match(source, /string\(name: 'IMAGE_REPOSITORY', defaultValue: params\.IMAGE_REPOSITORY \?: ''/,
|
assert.match(source, /string\(name: 'IMAGE_REPOSITORY', defaultValue: params\.IMAGE_REPOSITORY \?: ''/,
|
||||||
|
|||||||
Reference in New Issue
Block a user