diff --git a/Jenkinsfile.service b/Jenkinsfile.service index bc3edba32..19dac6adc 100644 --- a/Jenkinsfile.service +++ b/Jenkinsfile.service @@ -139,16 +139,21 @@ pipeline { String contextDir = ".docker-context/${env.RESOLVED_SERVICE}" String jar = serviceJars[env.RESOLVED_SERVICE] String registryHost = env.RESOLVED_IMAGE_REPOSITORY.tokenize('/')[0] + String dockerConfigDir = "${pwd(tmp: true)}/docker-auth" sh "mkdir -p '${contextDir}' && cp -f '${jar}' '${contextDir}/app.jar'" - sh "docker build -f deploy/docker/Dockerfile --build-arg BASE_IMAGE=${params.DOCKER_BASE_IMAGE} --build-arg JAR_FILE=app.jar -t ${image} '${contextDir}'" echo "推送共享镜像: ${image}" - withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) { - try { - sh 'printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "' + registryHost + '"' - sh "docker push ${image}" - } finally { - sh "docker logout ${registryHost} || true" + withEnv(["DOCKER_CONFIG=${dockerConfigDir}"]) { + sh 'mkdir -p "$DOCKER_CONFIG"' + withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) { + try { + sh 'printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "' + registryHost + '"' + sh "docker build -f deploy/docker/Dockerfile --build-arg BASE_IMAGE=${params.DOCKER_BASE_IMAGE} --build-arg JAR_FILE=app.jar -t ${image} '${contextDir}'" + sh "docker push ${image}" + } finally { + sh "docker logout ${registryHost} || true" + sh 'rm -rf "$DOCKER_CONFIG"' + } } } if (params.DEPLOY) { diff --git a/scripts/test-environment-pipeline-contract.test.mjs b/scripts/test-environment-pipeline-contract.test.mjs index 3ccf85117..0b2f25c21 100644 --- a/scripts/test-environment-pipeline-contract.test.mjs +++ b/scripts/test-environment-pipeline-contract.test.mjs @@ -64,6 +64,26 @@ test('development and test deployment locks are acquired before target executors assert.ok(lockIndex < nodeIndex, 'deployment lock must be acquired before target node allocation') }) +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/) +}) + 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"/)